创建一个高可用Tcp服务器
一、引言
在当今万物互联的时代,物联网(IoT)设备的数量正以一种让程序员们头皮发麻的速度增长。这些设备每天通过网络传输的数据量,大概可以把一个普通开发者的大脑硬盘塞到格式化重装的程度。
而在这个"一切皆联网"的洪流中,TCP 服务器扮演着承上启下的核心角色——它既要接受来自四面八方、操着各种稀奇古怪协议方言的设备连接,又要把这些数据可靠地处理并转发出去。说白了,它就是那个全能保姆,还不能休假。
作为 TouchSocket 网络通信框架的作者,我经常在各种群里看到这样的场景:
某开发者:为什么我的 TCP 服务器运行一段时间就崩了?
我:你的服务器是怎么创建的?
某开发者:
new TcpService()啊,哪里有问题?
这就好比你找了一个世界顶级主厨,然后让他站在路边摆摊煎饼果子——不是不行,但总感觉哪里不对劲。
TCP 服务器不是一个随手创建、用完即弃的玩具。它需要精心设计的架构、合理的配置管理、优雅的插件化扩展,以及——最重要的——要能活得久。
所以,我决定写这篇博客,手把手带你构建一个真正高可用的 TCP 服务器。用通俗的话说——就是那种即使凌晨 3 点宕机了,你的手机也不会被运维的电话打爆的服务器。
二、技术细节
2.1 技术框架
本文使用微软提供的通用主机(Generic Host) 进行构建,这套体系就像是给你的应用装了一套 PM(进程管理)+ 配置中心 + IOC 容器的豪华套餐,支持:
- 跨平台(Windows、Linux、macOS,就差运行在烤面包机上了)
- Windows 服务(让服务器在后台默默干活,不打扰你工作摸鱼)
- IIS 托管
- Options 选项配置(告别硬编码端口,yyds)
- 插件化加载(像乐高一样搭积木)
- IOC 容器(依赖注入,让代码松耦合不粘手)
- Native AOT(让你的 exe 小到让同事以为你在传病毒)
2.2 框架版本
支持:
- .NET Framework 4.6.2 及以上(是的,古老的 462 也没被抛弃)
- .NET 6 及以上
2.3 实现功能
本示例将实现以下功能(画的饼,本文全部会兑现):
- 接收、发送数据
- 消息单次响应
- 消息广播(群发,比你微信群艾特所有人还快)
- 数据库持久化(把数据存起来,不然断电就白干了)
三、项目结构设计
本文示例使用 .NET 10.0 构建,拆分为 5 个项目,如果你觉得这样太麻烦,可以先把所有代码塞在一个文件里……不过等你的代码行数超过 3000 行时,你会来感谢我的。
也支持完整的 Native AOT。
项目结构如下:
HighlyAvailableTcpService.App ← 主程序,启动入口
HighlyAvailableTcpService.Core ← 核心库,定义接口和 Options
HighlyAvailableTcpService.Plugins ← 插件层,业务逻辑的家
HighlyAvailableTcpService.DataHandlingAdapters ← 数据适配器,数据解包的地方
HighlyAvailableTcpService.Db ← 数据库层,数据的最终归宿
项目引用关系:
App → Core, Plugins, DataHandlingAdapters, Db
Plugins → Core, DataHandlingAdapters, Db
DataHandlingAdapters → Core
Db(无额外引用)
四、创建项目
首先,使用 辅助角色服务 模板创建一个 .NET 10.0 项目,名称为 HighlyAvailableTcpService.App。这是我们的主程序,也是最终启动的入口。
如果你仍在坚守 .NET Framework 4.6.2,也可以先按 net10 创建,再把 .csproj 里的 <TargetFramework>net10.0</TargetFramework> 改成 <TargetFramework>net462</TargetFramework>。记得勾选不使用顶级语句和取消勾选 AOT,否则编译时会给你一个"惊喜"。
然后用 nuget 安装 TouchSocket.Hosting。如果不熟悉 nuget,可以参考入门指南。
接下来,再分别新建以下类库项目:
HighlyAvailableTcpService.Core
HighlyAvailableTcpService.PluginsHighlyAvailableTcpService.DataHandlingAdaptersHighlyAvailableTcpService.Db
按照第三章的项目引用关系配置好后,我们就可以愉快地开始码字了。
五、完善 Core 核心库
HighlyAvailableTcpService.Core 是整个项目的基础设施层,里面住着各种接口、配置类——用一句流行语来说,它是整个系统的"灵魂"。
5.1 创建自定义 Tcp 会话客户端接口
首先定义 IHighlyAvailableSessionClient 接口,继承 ITcpSessionClient。这个接口就像是给每个连进来的设备颁发的"身份证",让我们统一管理:
接口里定义了 SetHighlyAvailableAdapter 方法,用于为会话单独设置数据处理适配器(别把适配器搞混了,它不是充电头)。详情见适配器介绍与使用。
接下来,定义实现类 HighlyAvailableSessionClient,继承 TcpSessionClient 并实现上面的接口:
5.2 创建自定义 Tcp 服务接口与实现
再定义服务接口 IHighlyAvailableTcpService,继承 ITcpService<HighlyAvailableSessionClient>。有了这个接口,以后想扩展功能就直接往接口里加,不用到处乱找实现类:
然后是内部实现类 HighlyAvailableTcpService,注意这里用了 internal——外面的世界看不到它,就让它安安静静地在内部干活:
5.3 定义 Options 配置类
接下来定义配置选项类 HighlyAvailableTcpServiceOption,用于映射配置文件中的每条监听配置:
因为服务器可能需要同时监听多个端口(比如同时对外提供 HTTP 和私有协议的服务),所以还需要一个集合配置类。顺带考虑了 Native AOT 的兼容性:
5.4 注册扩展方法
因为 HighlyAvailableTcpService 是 internal 的,外部项目不能直接 new 它出来,所以我们提供一个扩展方法来暴露注册能力。这样外部只需要调用一个方法,内部的脏活我们全包了:
5.5 主项目中应用
首先,在 Program.cs 中绑定配置文件:
对应的开发环境配置文件如下:
{
"HighlyAvailableTcpServiceOptions": {
"Options": [
{
"Name": "HighlyAvailableTcpService",
"Port": 7789,
"Ip": "127.0.0.1",
"Backlog": 100,
"SslPath": "",
"SslKey": ""
}
]
}
}
配置文件有两个:生产环境(appsettings.json)和开发环境(appsettings.Development.json)。开发时只改开发配置文件,上线时再参考配置生产配置文件,这样可以避免线上线下配置打架的惨剧。
然后注册 TCP 服务。此刻先不配置监听端口——我们计划用插件机制动态添加监听,避免在启动逻辑里硬编码一堆端口号,顺带利用插件机制 和动态监听 来完成:
完成以上步骤后,可以尝试启动项目,控制台应该会出现类似这样的日志——说明服务器已经成功站岗了:
info: TouchSocket.Sockets.CheckClearPlugin begin polling
info: TouchSocket.Hosting.Sockets.HostService.ServiceHost[0]
服务器已启动。
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
六、完善数据适配器项目
数据适配器项目 HighlyAvailableTcpService.DataHandlingAdapters 的职责是:把从 TCP 流里读出来的一坨字节,翻译成程序能理解的结构化数据。关于适配器的详细介绍,可以参考适配器文档。
本示例的数据解析规则很简单:
- 接收字符串数据,遇到
\r\n就认为一条消息结束了
(就像古代的竹简,遇到空白就代表一句话说完了。)
6.1 解析数据
首先定义承载解析结果的类 HighlyAvailableRequestInfo,以及继承自 CustomBetweenAndDataHandlingAdapter 的适配器类 HighlyAvailableDataAdapter:
关于 CustomBetweenAndDataHandlingAdapter 的详细介绍,可以参考文档。
6.2 关于适配器的应用
适配器可以在 TouchSocketConfig 中进行全局统一配置,但在本示例中,我们把适配器的设置交给了插件层来管理——这样可以为不同类型的设备连接单独设置不同的适配器,具体实现见下一章的 AdapterPlugin。
七、完善插件项目
插件项目 HighlyAvailableTcpService.Plugins 是业务逻辑的主战场。我们遵循单一职责原则:每个插件只做一件事,处理不了的就传给下一个插件。这套机制就像流水线作业,每个工人只负责自己这道工序。
关于插件系统的介绍,可以参考插件系统文档。
7.1 监听配置插件
监听配置插件负责在服务器启动时,从配置文件读取端口信息并动态添加监听。
这样做的好处是:改个端口只需要改配置文件,不用重新编译代码。运维同学会感谢你的:
7.2 适配器插件
当一个新设备连接进来时,这个插件立刻为它分配专属的数据适配器。你可以在这里根据 IP、端口、连接时间……甚至月相来决定给它用什么适配器:
7.3 日志插件
日志是程序员最好的朋友,尤其是在凌晨 3 点被电话叫醒排查问题的时候。
这里使用了 PluginOption(FromIoc = true),让日志插件从 IOC 容器中取实例,支持 Scoped 依赖注入:
PluginOption(FromIoc = true) 特性可以让插件从依赖注入容器中获取实例,实现 Scoped 生命周期——也就是说每个会话有自己独立的插件实例。别小看这个细节,它在处理有状态业务时能救你的命。
配置 NLog 日志组件。首先 nuget 安装 NLog.Extensions.Logging,然后在 Program.cs 中配置:
并且把 TouchSocket 的内部日志也接入 AspNetCore 日志体系:
7.4 Hello 数据处理插件
这是一个最简单的业务插件示例:收到 "hello" 就回复 "hi"。虽然逻辑简单,但它展示了插件链式处理数据的核心思路——如果数据不归我处理,就往后传:
注意 return 和 await e.InvokeNext() 的区别:
return:我处理完了,后续插件不用再看这条消息了(像个霸道的门卫)。await e.InvokeNext():我处理完了,但后面的插件也可以接着看看有没有它们要做的事(像个礼貌的接待员)。
7.5 登录插件与会话状态暂存
在实际业务中,我们经常需要记录某个会话的状态,比如"这个设备登录了吗?用户名叫什么?"。
TouchSocket 提供了基于依赖属性的扩展机制。我们先定义扩展属性——注意 SetUserName 是 internal 的,只有在我们自己的插件里才能设置用户名,外部只能读取,防止外部乱改:
然后定义登录处理插件:
7.6 广播插件
广播功能是很多物联网平台的刚需——比如把某个传感器的告警消息推送给所有连接中的监控终端。
这里使用了 Channel<string> 来做异步消息队列,避免在接收数据的线程上直接遍历所有会话导致性能抖动:
当收到 "broadcast" 消息时,就把数据塞进 Channel,由后台任务负责挨个发送给所有在线会话。即使某个会话发送失败,也不会影响其他会话的发送——这才叫"高可用"。
7.7 注册插件到容器(Scoped 支持)
需要使用 IOC 注入的插件(即标注了 [PluginOption(FromIoc = true)] 的),需要先在服务容器中注册:
7.8 插件注册扩展
和 Core 层一样,插件层也提供一个统一的扩展方法,让外部只需一行代码就能启用所有插件:
最终在主项目中调用:
八、完善 Db 数据库层
数据库层 HighlyAvailableTcpService.Db 负责把重要数据持久化到磁盘中。毕竟,内存是易忘症患者,断电就失忆;数据库才是靠谱的日记本。
本项目使用 SQLite + SqlSugar 的组合:
- SQLite:轻量级嵌入式数据库,零配置,随项目走,适合中小规模的 IoT 数据存储
- SqlSugar:国产 ORM 框架,API 简洁,性能优秀(而且 Native AOT 支持做得很好)
8.1 定义数据库实体
定义用户实体类,这里只是一个示例结构:
8.2 注册数据库服务
注册 SQLite 连接和仓储模式。这里通过 CodeFirst 方式在首次运行时自动建表,省去了手动执行 SQL 脚本的麻烦:
在 Program.cs 中调用:
8.3 DbPlugin 数据库插件
当收到 "Add" 消息时,自动往数据库里插入一条用户记录。需要注意的是,AOT 环境下 ORM 的反射功能受限,所以这里用了条件编译符来区分:
九、Native AOT
如果你追求极致的启动速度和极小的发布体积,.NET 10.0 及以上版本可以使用 Native AOT 发布。
只需在 .csproj 中添加:
<PublishAot>true</PublishAot>
然后右击项目 → 发布 → 新建配置,最终发布配置如下:
发布后,可执行文件仅 6.6 MB,是的你没看错,6.6 MB,比你手机上随手下载的一个表情包 App 还小:
TouchSocket 已完整支持 Native AOT。但如果你引入了其他第三方库,请确认它们也支持 AOT——否则编译时会给你一些意想不到的"惊喜警告"。
十、设为 Windows 服务
10.1 配置 Windows 服务
通用主机天然支持作为 Windows 服务运行。详细介绍,请参考:使用 BackgroundService 创建 Windows 服务。
首先 nuget 安装 Microsoft.Extensions.Hosting.WindowsServices,然后在 Program.cs 中加入:
这段代码我们已经在主程序里写好了——而且加了跨平台判断,在 Linux 上运行时不会出错,毕竟我们承诺过跨平台。
10.2 安装、启动服务
sc create HighlyAvailableTcpService binPath= %~dp0HighlyAvailableTcpService.App.exe start= auto
sc description HighlyAvailableTcpService "高可用Tcp服务"
Net Start HighlyAvailableTcpService
pause
10.3 卸载、停止服务
net stop HighlyAvailableTcpService
sc delete HighlyAvailableTcpService
pause
十一、最佳实践
积累了无数次被用户"温柔鞭打"的经验后,给大家整理以下最佳实践——希望你少踩一点坑。
11.1 日志分级别输出
TouchSocket 内部会把网络异常(比如对端突然断开)记录为日志,这些日志在生产环境中往往是"已知噪音"。建议在 NLog 配置中:
- 通信组件的日志输出到单独的文件(比如
touchsocket.log) - 业务日志输出到主日志文件
这样排查问题时就不会被一堆 "连接已断开" 日志淹没。
11.2 使用依赖属性暂存会话状态
如果需要为每个连接的设备暂存状态(比如认证信息、设备类型、最后活跃时间),推荐使用 TouchSocket 提供的 DependencyProperty 机制,而不是用 ConcurrentDictionary 满天飞。
好处:
- 随会话生命周期自动管理,不用担心内存泄漏
- 访问接口统一,代码整洁
SetUserName设为internal后,外部只能读不能写,天然防止误操作
11.3 插件顺序很重要
插件的执行顺序就是注册顺序。日志插件要放在业务数据处理插件前面,这样才能记录到完整的处理耗时。否则日志只记录到"数据到了",不知道后续花了多少时间处理。
11.4 广播消息用 Channel 解耦
不要在接收数据的回调里直接遍历所有会话发送消息!这会导致发送耗时阻塞接收线程。正确姿势是用 Channel<T> 或其他异步队列,把消息投递交给专属后台任务处理。
BroadcastPlugin 就是这个思路的最佳实践。
11.5 Native AOT 兼容性注意
如果使用 Native AOT 发布,需要注意:
- 反射受限,某些 ORM 的动态查询功能不可用(如示例中的
DbPlugin用了#if !AOT) - 检查所有第三方库是否支持 AOT(SqlSugar 需要手动开启
StaticConfig.EnableAot = true)
十二、总结
好了,我们用一整篇博客,把一个"高可用 TCP 服务器"的各个器官全部安装完毕:
| 项目 | 职责 |
|---|---|
Core | 定义接口和配置类,是整个系统的骨架 |
DataHandlingAdapters | 数据解包,把字节流翻译成业务数据 |
Plugins | 业务逻辑,插件化、可扩展、可复用 |
Db | 数据持久化,让数据不会随断电而消逝 |
App | 主程序入口,把所有模块串联起来 |
核心设计理念:
- 插件化:每个业务逻辑一个插件,互不干扰,随时可插拔
- 配置驱动:端口、IP、证书全部走配置文件,零硬编码
- 分层解耦:Core / Adapters / Plugins / Db 各司其职,改一层不影响另一层
- 高可用:健康清理插件 + 异步广播 + AOT 支持 = 稳定运行,不让运维老哥半夜起床
希望这篇博客能帮你少走弯路,早日构建出那个不会让手机被叫爆的 TCP 服务器。祝编译一次通过,Bug 从不出现在生产!
十三、参考资料
十四、本文示例 Demo
本文所涉及所有技术均是开源的,大家可以直接按照教程一步步搭建。
但作为 Pro 用户的专属福利,完整的成品示例工程仅对企业版 Pro 用户开放。
欢迎通过 Pro 的购买来支持我们的开源工作,让这个项目越来越好!
