跳到主要内容

创建一个高可用Tcp服务器

· 阅读需 17 分钟
若汝棋茗
Software Engineer Ⅱ

一、引言

在当今万物互联的时代,物联网(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.Plugins
  • HighlyAvailableTcpService.DataHandlingAdapters
  • HighlyAvailableTcpService.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 注册扩展方法

因为 HighlyAvailableTcpServiceinternal 的,外部项目不能直接 new 它出来,所以我们提供一个扩展方法来暴露注册能力。这样外部只需要调用一个方法,内部的脏活我们全包了:

🔄 正在加载代码...

5.5 主项目中应用

首先,在 Program.cs 中绑定配置文件:

🔄 正在加载代码...

对应的开发环境配置文件如下:

appsettings.Development.json
{
"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"。虽然逻辑简单,但它展示了插件链式处理数据的核心思路——如果数据不归我处理,就往后传:

🔄 正在加载代码...
提示

注意 returnawait e.InvokeNext() 的区别:

  • return:我处理完了,后续插件不用再看这条消息了(像个霸道的门卫)。
  • await e.InvokeNext():我处理完了,但后面的插件也可以接着看看有没有它们要做的事(像个礼貌的接待员)。

7.5 登录插件与会话状态暂存

在实际业务中,我们经常需要记录某个会话的状态,比如"这个设备登录了吗?用户名叫什么?"。

TouchSocket 提供了基于依赖属性的扩展机制。我们先定义扩展属性——注意 SetUserNameinternal 的,只有在我们自己的插件里才能设置用户名,外部只能读取,防止外部乱改:

🔄 正在加载代码...

然后定义登录处理插件:

🔄 正在加载代码...

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 安装、启动服务

cmd(以管理员身份运行)
sc create HighlyAvailableTcpService binPath= %~dp0HighlyAvailableTcpService.App.exe start= auto
sc description HighlyAvailableTcpService "高可用Tcp服务"
Net Start HighlyAvailableTcpService
pause

10.3 卸载、停止服务

cmd(以管理员身份运行)
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 从不出现在生产!

十三、参考资料

  1. TouchSocket 官网
  2. .NET 通用主机
  3. 使用 BackgroundService 创建 Windows 服务
  4. 适配器介绍与使用
  5. 插件系统文档

十四、本文示例 Demo

信息

本文所涉及所有技术均是开源的,大家可以直接按照教程一步步搭建。

但作为 Pro 用户的专属福利,完整的成品示例工程仅对企业版 Pro 用户开放。

欢迎通过 Pro 的购买来支持我们的开源工作,让这个项目越来越好!