
简介这份资源面向希望摆脱IIS依赖、快速搭建轻量级HTTP服务的C#开发者与学习者聚焦用内置HttpListener实现自托管Web服务的完整示例。压缩包共17个文件、约18KB以8个cs源码文件为核心配合csproj项目配置、resx资源与xml配置以及wwwroot下的htm静态页面构成可直接编译运行的小型工程。代码中清晰呈现监听前缀设置、启动与异步接收请求、请求处理回调、停止关闭等关键环节并附带客户端模拟线程与简单窗体交互便于理解服务端与调用端的配合方式。目前已有2364人学习下载适合作为入门自托管Web服务、验证接口逻辑或嵌入桌面程序的参考样例读者可据此掌握不依赖IIS的部署思路与请求响应处理流程。1. 从一次“IIS 装不上”的现场说起C# 自托管 Web 服务到底装什么上周帮一个做内部工具的朋友收拾残局他在一台 Windows Server 2012 R2 的测试机上折腾了一下午 IIS角色装到一半报错回滚站点没起来防火墙规则倒是加了一堆。其实他的需求简单得不能再简单——用 C# 起一个只在内网跑的 Web 服务提供几个 JSON 接口和一张静态页面压根用不上 IIS 那套完整的站点、应用程序池、模块托管体系。这就是“C# 建立最简单的 Web 服务无需 IIS”这个场景的真实来源不是 IIS 不好而是很多轻量需求里它太重、太容易在权限和组件依赖上翻车。这篇笔记记录的是我实际装过、跑通过的一套最小环境用 .NET 的自托管能力HttpListener 或 ASP.NET Core 的 Kestrel把 Web 服务直接跑成一个控制台进程不依赖 IIS、不注册系统服务、不改注册表。适合谁适合手上有 .NET 环境、想快速验证接口、做内网小工具、或者给非技术同事演示一个本地页面的开发者。装不好会怎样最常见的是端口被占、权限不足导致 HttpListener 拒绝监听、或者 .NET 版本对不上导致编译能过运行报错。下面按“装什么版本 → 怎么装 → 怎么配 → 坏了怎么救 → 怎么验证”走一遍。2. 环境与版本选型.NET SDK 装哪个、装在哪2.1 先定运行时模型HttpListener 还是 Kestrel“无需 IIS”在 C# 里有两条主流路线选错了后面全是返工。第一条是System.Net.HttpListener属于 .NET Framework 和 .NET 都自带的基础类零额外依赖一个控制台程序几十行就能监听端口、处理请求适合“就想要个能返回字符串的 HTTP 端点”的极简场景。第二条是 ASP.NET Core 的 Kestrel自带在Microsoft.AspNetCore.App共享框架里功能完整路由、中间件、静态文件、JSON 序列化都有但需要对应的 ASP.NET Core 运行时或 SDK。我的取舍习惯是如果只是临时接口、内网调试、给脚本当回调地址用 HttpListener装个 .NET SDK 就够如果要做稍微像样的服务多路由、返回 JSON、托管静态页直接上 ASP.NET Core 最小 API别在 HttpListener 上手工拼路由那是给自己挖坑。两条路线对 SDK 的要求不同先看下表再决定装什么。路线需要的组件典型适用额外依赖HttpListener.NET SDK6/8 均可或 .NET Framework 4.7.2单端点、回调、极简演示无ASP.NET Core 最小 APIKestrel.NET SDK 8.0含 ASP.NET Core 运行时多路由、JSON 接口、静态文件无SDK 自带2.2 版本选择为什么我建议直接上 .NET 8 LTS版本这块有个反直觉的坑很多人以为“最简单的 Web 服务”用越老的 .NET Framework 越省事实际上在 Windows 上装 .NET Framework 4.8 往往要重启、要匹配系统补丁反而更折腾。而 .NET 6/8 的 SDK 是绿色安装、单文件安装器、装完即用卸载也干净。截至我写这篇笔记时.NET 8 是长期支持版本LTS支持周期到 2026 年 11 月.NET 6 已经过了主流支持期新项目没必要再选。具体到“无需 IIS”这个场景.NET 8 的 SDK 自带 ASP.NET Core 运行时装一个 SDK 两条路线都能走不用再单独装 Hosting Bundle。如果你机器上已经有 Visual Studio 202217.8 及以上它默认就带 .NET 8 SDK可以先跑dotnet --list-sdks确认避免重复安装。没有的话去微软官方 .NET 下载页拿对应架构的安装包——x64 机器装 x64ARM 机器比如某些轻薄本装 arm64装错架构的典型现象是dotnet命令能跑但编译报“平台不匹配”。提示不要为了“省空间”只装 Runtime 不装 SDK。Runtime 只能跑已编译好的程序你要新建项目、dotnet build必须有 SDK。这是新手最常混的一对概念。2.3 安装前的环境自检清单动手前花两分钟确认三件事能省掉后面一半的报错。第一确认系统版本.NET 8 要求 Windows 10 1607 及以上或 Windows Server 2012 R2 及以上老系统比如 Win7只能退回 .NET Framework 路线。第二确认端口可用默认演示常用 5000、8080先用netstat看一眼有没有被占。第三确认当前用户有没有监听端口的权限——HttpListener 在 Windows 上对非管理员用户监听http://:端口/这种通配前缀是会拒绝的后面第 4 章会专门讲怎么绕。# 查看已安装的 .NET SDK 版本列表 dotnet --list-sdks # 查看系统里 5000 和 8080 端口是否被占用 netstat -ano | findstr :5000 :8080 # 确认当前系统版本Windows winver第一行输出形如8.0.100 [C:\Program Files\dotnet\sdk]说明 SDK 已就位如果提示“不是内部或外部命令”说明 PATH 没配或根本没装。第二行有输出就说明端口被占记下最后一列的 PID用任务管理器对一下是哪个进程。第三行弹窗看版本号低于 Win10 1607 就别装 .NET 8 了直接走 .NET Framework 4.7.2 HttpListener 的组合。3. 安装与最小服务落地从空目录到能返回 JSON3.1 安装 .NET 8 SDK 并验证安装本身没什么玄学双击安装器一路下一步即可但有两个细节值得说。一是安装器默认会勾选“将 dotnet 添加到 PATH”别取消否则后面所有命令都得写全路径。二是装完必须新开一个终端窗口老窗口的环境变量不会刷新这是“明明装了却提示找不到命令”的头号原因。装完用下面三条命令做一次闭环验证缺一不可。# 1. 确认 SDK 版本应输出 8.x dotnet --version # 2. 确认运行时列表里包含 Microsoft.AspNetCore.App dotnet --list-runtimes # 3. 建一个临时目录试跑模板验证 SDK 完整 dotnet new console -o %TEMP%\dotnet-selftest cd %TEMP%\dotnet-selftest dotnet run第一条输出8.0.x就对了如果输出的是6.x而你以为是 8说明机器上装了多个 SDKdotnet默认取最高版本但项目里可能被global.json锁死后面会讲。第二条重点看有没有Microsoft.AspNetCore.App 8.0.x没有的话 Kestrel 路线走不通只能退回 HttpListener。第三条是很多人忽略的“完整性自检”dotnet new能建项目、dotnet run能编译运行说明 SDK 的模板和编译器都齐了比只看版本号靠谱。3.2 路线一HttpListener 版最小服务先给最极简的那条路。新建一个控制台项目把Program.cs换成下面的内容。这段代码的核心是HttpListener绑定前缀、循环GetContext()取请求、写响应、关闭。注意前缀必须以/结尾http://localhost:8080/合法http://localhost:8080会抛ArgumentException这是新手第一个跟头。using System.Net; using System.Text; // 前缀必须以斜杠结尾否则构造时直接抛异常 var listener new HttpListener(); listener.Prefixes.Add(http://localhost:8080/); listener.Start(); Console.WriteLine(Listening on http://localhost:8080/ ...); while (true) { // 阻塞等待下一个请求 var ctx listener.GetContext(); var req ctx.Request; var res ctx.Response; // 只处理根路径其余返回 404 if (req.Url!.AbsolutePath /) { var body Encoding.UTF8.GetBytes({\ok\:true,\msg\:\hello from HttpListener\}); res.StatusCode 200; res.ContentType application/json; charsetutf-8; res.ContentLength64 body.Length; res.OutputStream.Write(body, 0, body.Length); } else { res.StatusCode 404; } res.OutputStream.Close(); // 必须关闭否则客户端一直等 }逐段说逻辑Prefixes.Add决定监听地址用localhost只允许本机访问改成或*才能被局域网访问但后者在 Windows 上需要管理员权限或 URL ACL 授权第 4 章展开。GetContext()是同步阻塞的单线程一次只能处理一个请求做演示够用真要多并发得配合Task.Run或换 Kestrel。ContentLength64不设的话部分客户端会一直挂起等数据结束这是“浏览器转圈但服务端日志显示已处理”的经典原因。最后OutputStream.Close()是收尾动作漏了它连接不释放压测几轮就卡死。3.3 路线二ASP.NET Core 最小 API 版如果需求稍微正经一点用最小 API。dotnet new web建出来的模板自带 Kestrel改Program.cs即可。下面这段提供根路径和/api/time两个端点返回 JSON还顺手托管了wwwroot下的静态文件——这就是“无需 IIS 也能给页面”的关键静态文件中间件直接由进程自己发。var builder WebApplication.CreateBuilder(args); // 显式指定监听地址避免默认端口和别的服务打架 builder.WebHost.UseUrls(http://localhost:5000); var app builder.Build(); // 托管 wwwroot 下的静态文件等价于 IIS 的静态内容角色 app.UseStaticFiles(); app.MapGet(/, () Results.Json(new { ok true, msg hello from Kestrel })); app.MapGet(/api/time, () Results.Json(new { now DateTimeOffset.Now.ToString(O), machine Environment.MachineName })); app.Run();UseUrls是显式锁端口不写的话默认 5000/5001容易和本机其他开发服务撞车。UseStaticFiles要求项目根目录下有wwwroot文件夹没有就建一个放个index.html进去访问http://localhost:5000/index.html就能出来全程不碰 IIS。MapGet是路由映射返回Results.Json会自动设Content-Type: application/json比 HttpListener 手工拼字符串省心得多。跑起来用dotnet run控制台会打印Now listening on: http://localhost:5000看到这行才算真起来了。3.4 发布成可独立运行的单文件开发时dotnet run方便但要拷到别的机器上跑得发布。我一般用自包含单文件发布目标机器连 .NET 都不用装代价是文件大几十 MB。命令里的-r指定运行时标识符Windows x64 是win-x64--self-contained true表示把运行时打进去PublishSingleFile打成单文件方便拷贝。# 在项目根目录执行输出到 publish 文件夹 dotnet publish -c Release -r win-x64 --self-contained true ^ -p:PublishSingleFiletrue -p:IncludeNativeLibrariesForSelfExtracttrue ^ -o .\publish-c Release走优化编译别用 Debug 发布。IncludeNativeLibrariesForSelfExtract把原生库也塞进单文件否则某些依赖会散落在旁边。发布完publish目录里会有一个 exe双击或命令行启动即可日志直接打在控制台。这一步是“无需 IIS”落地的最后一公里目标机器上既没有 IIS 也没有 SDK照样能跑。4. 配置与避坑端口、权限、防火墙三座大山4.1 端口与前缀配置localhost 和 的区别前缀写法直接决定“谁能访问”。http://localhost:8080/只绑定回环地址本机浏览器能开同一局域网的另一台机器访问不了http://:8080/绑定所有网卡局域网可访问但在 Windows 上非管理员身份调用listener.Start()会抛HttpListenerException: 拒绝访问。这不是代码 bug是系统安全策略。常见做法是开发期用localhost图省事要对外时再处理权限别一上来就用然后被报错劝退。Kestrel 路线相对宽松UseUrls(http://0.0.0.0:5000)就能对外普通用户权限即可这也是我推荐正经服务用 Kestrel 的原因之一——少一层 URL ACL 的坑。但要注意0.0.0.0和localhost的语义差别前者监听所有 IPv4 网卡后者只回环写混了会出现“本机能开、同事打不开”的经典现象。4.2 Windows 下 HttpListener 的 URL ACL 授权非管理员要用前缀得给当前用户授 URL ACL。用netsh http add urlacl命令user填当前 Windows 账户url填要监听的前缀。这条命令需要管理员权限执行一次之后普通用户就能监听该前缀。撤销用delete urlacl排查权限问题时先show urlacl看有没有残留规则。# 以管理员身份运行给当前用户授权监听 8080 端口的所有地址 netsh http add urlacl urlhttp://:8080/ userDOMAIN\username # 查看已有规则确认是否添加成功 netsh http show urlacl | findstr 8080 # 不再需要时删除避免规则堆积 netsh http delete urlacl urlhttp://:8080/user里的账户名要写对本地账户一般是计算机名\用户名域账户是域\用户名写错不报错但授权不生效表现为“命令成功但启动仍拒绝访问”。show urlacl那行用来验证看到 8080 的记录才算成。这套操作是 HttpListener 路线的必修课Kestrel 路线可以跳过。4.3 防火墙放行与端口冲突处理服务起来了但同事访问不了九成是 Windows 防火墙拦了入站。放行用netsh advfirewall firewall add rule指定端口和协议。同时要防端口冲突启动前netstat -ano | findstr :8080有输出就换端口别硬刚。我见过有人反复重启服务以为是代码问题实际是上一个进程没退干净占着端口。# 放行 8080 端口入站 TCP管理员权限 netsh advfirewall firewall add rule nameCSharpWebDemo8080 ^ dirin actionallow protocolTCP localport8080 # 查端口占用最后一列是 PID netstat -ano | findstr :8080 # 按 PID 结束占用进程谨慎确认不是系统进程 taskkill /PID 12345 /F防火墙规则名自己起方便以后删。netstat输出里LISTENING状态加对应 PID去任务管理器“详细信息”页对 PID 就能定位是哪个程序。taskkill /F是强杀动手前确认 PID 不是关键系统进程杀错了轻则服务异常重则要重启机器这个后悔药不好吃。4.4 避坑与常见问题排查现象一HttpListenerException: 拒绝访问。原因是用或*前缀但当前用户没有 URL ACL 授权。解决要么改回localhost前缀要么按 4.2 用管理员执行netsh http add urlacl授权二选一别在代码里反复改前缀试。现象二dotnet命令提示“不是内部或外部命令”。原因是安装时没勾 PATH或者装完没开新终端。解决新开一个 cmd 或 PowerShell 再试仍不行就手动把C:\Program Files\dotnet加进系统 PATH然后重启终端。别急着重装 SDK八成是环境变量没刷新。现象三编译报NETSDK1045 当前 .NET SDK 不支持目标框架。原因是项目文件里TargetFramework写的是net8.0但机器上只有 .NET 6 SDK。解决dotnet --list-sdks确认实际版本要么装对应 SDK要么把项目文件里的目标框架改成现有版本。多 SDK 共存时项目根目录若有global.json会锁定版本检查它是不是把版本钉死在了旧版。现象四浏览器一直转圈服务端日志显示已处理。原因是 HttpListener 没设ContentLength64或没关OutputStream。解决响应体写完必须设长度并Close()缺一个客户端就等不到结束标志。Kestrel 路线不会有这个问题框架帮你处理了。现象五发布后的 exe 在目标机器双击闪退。原因是用了框架依赖发布没加--self-contained true目标机器没装对应运行时。解决要么改成自包含发布要么在目标机器装 .NET 运行时。闪退时看不到报错用命令行启动 exe 才能看到异常信息这是定位这类问题的黑匣子。5. 装完怎么验证跑通三条命令加一张对照表验证这件事我吃过“看起来起来了其实没通”的亏所以现在固定走三步缺一步都不算完。第一步进程内自检服务启动后控制台必须打印监听地址HttpListener 版是手写的Listening on ...Kestrel 版是框架的Now listening on: ...没这行说明根本没绑上端口。第二步本机回环验证用curl打本机地址看状态码和响应体这一步排除代码逻辑问题。第三步跨机验证从另一台机器打目标机的局域网 IP这一步才真正验证防火墙和绑定地址前两步过了第三步挂问题一定在 4.1 和 4.3。# 本机验证应返回 200 和 JSON 体 curl -i http://localhost:8080/ # 跨机验证把 IP 换成服务所在机器的局域网地址 curl -i http://192.168.1.50:8080/ # 只看状态码脚本里做健康检查常用 curl -s -o NUL -w %{http_code} http://localhost:8080/-i会把响应头一起打出来重点看HTTP/1.1 200和Content-Type对不对-w %{http_code}只输出状态码适合塞进批处理做探活。跨机那步如果超时先回 4.3 查防火墙规则再回 4.1 确认前缀不是localhost顺序别乱否则会在错误的方向上浪费半天。下面这张对照表是我排障时贴在显示器边上的按“现象 → 先查哪 → 大概率原因”走比漫无目的翻日志快得多。现象先查什么大概率原因启动即抛拒绝访问前缀写法 URL ACL用了但没授权本机能开、他机打不开绑定地址 防火墙绑了 localhost 或规则没放行浏览器转圈不结束响应长度 流关闭漏设 ContentLength64发布 exe 闪退发布方式框架依赖但目标机无运行时编译报目标框架不支持SDK 版本 global.json版本不匹配或被锁死最后说个习惯从那以后我每次交付这类“无需 IIS”的小服务都会在目标机器上先跑一遍dotnet --list-runtimes和netstat再启动服务最后用另一台机器curl一次三步全绿才敢说“装好了”。这套流程不复杂但能把端口、权限、防火墙这三类翻车点提前挡掉。希望帮到你。本文还有配套的精品资源点击获取