尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
C# TCP/IP服务端实现LIS双向通讯与ASTM协议解析
简介面向医疗检验与工业自动化开发者的TCP/IP双向通讯示例工程演示如何通过C#搭建服务端、等待客户端接入并自动收发数据可自行回应对方功能类似TCP/IP调试助手同时附带ASTM协议数据解析Demo适合LIS系统对接检验仪器时参考。包体共143个文件约3.07MB核心为9个cs源码文件与36个dll依赖库另有xml配置、txt说明文档、exe可执行程序及png示意图片目录结构完整可独立打开调试。目前已有1355人学习下载。通过源码可快速掌握服务端监听、客户端连接、数据自动接收与回应的完整流程并理解ASTM协议字段拆解与解析思路医疗或工业场景中需要与设备做双向通讯的开发者可直接复用核心代码或在此基础上扩展业务逻辑。1. LIS 双向通讯不是写个 Socket 就完事这份 C# TCP/IP 示例能当通讯底座在检验科或工业现场仪器和 LIS 系统之间的数据交换绝大多数走的是 TCP/IP。很多人以为双向通讯就是开一个 Socket 收发字符串但真拿生化仪、血球仪联调时帧格式、校验位、粘包、半包、断线重连会一个个冒出来。这份资源是一个用 C# 写好的 TCP/IP 服务端示例核心逻辑是创建服务端等待仪器作为客户端接入连接成功后自动接收对方发来的数据并允许你按协议自行回应。它还附带一段 ASTM 协议数据解析 Demo适合做医疗检验或工业设备数据采集的开发者参考。它不是完整产品但完全能当通讯底座来用。2. TCP/IP 服务端底座监听模型、连接生命周期与参数选择2.1 为什么选“服务端等待连接”而不是主动连仪器大多数 LIS 对接场景里生化仪、血球仪、免疫分析仪都把自己当作客户端开机后配置一个服务器 IP 和端口只要有检验数据要传就主动连过来。LIS 那边则固定监听一个端口等仪器来连。这样设计有三个现实原因一是仪器端基本没有长期监听端口的必要它在业务里是主动发起方二是 LIS 要同时接入多台型号不同的仪器每台仪器对应一个设备编号或 IP服务端统一监听、按连接来源区分身份管理起来更清晰三是多数仪器厂商的协议文档会明确写“仪器作为 TCP 客户端连接 LIS 服务端”。如果你遇到少数需要 LIS 主动连仪器的设备比如某些老式酶标仪可以把角色反过来。这份资源里的监听模型依然适用只是把TcpListener放到仪器侧。但按我的经验95% 以上的检验仪器都是客户端主动连服务端所以你直接照示例做服务端基本上不会踩到反向连接的坑。2.2 TcpListener 与异步 Accept阻塞与非阻塞怎么选C# 里建 TCP 服务端最直接的方式是TcpListener。示例工程大概率就是它因为代码短、逻辑清楚不用手动管理原生 Socket。它的监听核心代码大致如下TcpListener listener new TcpListener(IPAddress.Any, 15000); listener.Start(10); // backlog最多排队的未处理连接数 while (true) { TcpClient client listener.AcceptTcpClient(); Task.Run(() HandleClient(client)); }代码说明IPAddress.Any表示监听本机所有网卡回环地址127.0.0.1和局域网 IP 都能连进来。端口15000是示例值部署时要避开 0-1024 的常用端口和已被占用的区间。listener.Start(10)里的 10 是 backlog 参数代表操作系统层面允许排队等待处理的连接数如果预计几十台仪器同时连就调大一些。AcceptTcpClient是阻塞式调用执行到这里会卡住直到一个客户端连进来。用Task.Run把每个客户端的后续逻辑放到线程池线程上才不至于处理第一台仪器时把后面仪器的连接全部堵在队列里。这个模型的优点是简洁把并发问题丢给线程池缺点是每个连接占用一个线程如果连接数到几千上万线程切换开销会变大。但一个 LIS 前置机一般也就同时挂几台到几十台仪器完全够用。如果你是做工业现场的大规模采集可以考虑用SocketAsyncEventArgs做纯异步但资源里没涉及那是后话。从资源工程的源码组织来看使用TcpListener的方案意味着你必须处理好客户端断开时的资源释放。否则每台仪器反复重连线程池会被僵尸连接占满程序表面还活着实际已经拒绝服务。2.3 连接生命周期从 Accept 到断开双向数据流怎么维护一个双向连接建立后要做两件事读仪器发来的数据以及回写数据给仪器。最容易出的问题是把“读”和“写”串行化仪器不会按你的节奏来它可能连上后立刻发一包也可能半小时不发服务端如果一直阻塞在Read上就没办法主动给仪器发心跳或查询指令。所以读和写要分离资源里的处理可以抽象成这个循环public void HandleClient(TcpClient client) { using (TcpClient tcpClient client) using (NetworkStream stream tcpClient.GetStream()) { byte[] buffer new byte[4096]; while (true) { int bytesRead stream.Read(buffer, 0, buffer.Length); if (bytesRead 0) { break; // 客户端关闭连接 } string received Encoding.ASCII.GetString(buffer, 0, bytesRead); string response ProcessReceived(received); byte[] responseBytes Encoding.ASCII.GetBytes(response); stream.Write(responseBytes, 0, responseBytes.Length); } } }这里展示的是最简单的“收到什么就处理什么处理完立刻回写”模型。stream.Read是同步阻塞读返回 0 表示对端已经正常关闭此时要跳出循环并释放资源。ProcessReceived是业务协议入口也就是 ASTM 解析 Demo 要接管的地方。这段代码有两个隐患。第一Encoding.ASCII遇到中文字符会丢信息后面避坑章节专门讲。第二一次Read读到的字节数不等于一条完整消息的长度。TCP 是流协议不保证一次Write对应一次Read。你发了 100 字节对方可能一次收到 100也可能分两次收 6040还可能三包数据拼一次收到。所以收到字节后不要立刻GetString再拼字符串而是先把byte[]追加到一个接收缓冲区去缓冲区里找帧头和帧尾解析出一条帧后再把剩余的字节留给下一帧。这个思路是 ASTM 解析 Demo 的立足点。如果你还要管理多台仪器就得维护一个Dictionarystring, TcpClient用仪器编号做 key。例如private readonly ConcurrentDictionarystring, TcpClient _clients new ConcurrentDictionarystring, TcpClient(); private void RegisterClient(string instrumentId, TcpClient client) { _clients[instrumentId] client; } public void SendToInstrument(string instrumentId, string message) { if (_clients.TryGetValue(instrumentId, out TcpClient client)) { NetworkStream stream client.GetStream(); byte[] data Encoding.ASCII.GetBytes(message); stream.Write(data, 0, data.Length); } }说明ConcurrentDictionary保证多线程下操作安全。每次新连接握手后根据仪器标识比如 ASTM 头消息里的仪器名称把它放到字典里仪器断开时手动TryRemove。否则字典里全是死连接往里面Write会抛异常。资源示例可能没有做这层封装你自己扩展时要补上。3. ASTM 协议解析帧结构、Checksum 与粘包拆包3.1 ASTM E1394 帧格式STX、ETX、Checksum 到底长什么样LIS 最常用的通讯协议是 ASTM实际上是 E1394 标准的一个子集国内很多仪器厂商在此基础上做了简化。ASTM 消息不是裸字符串而是包在一种“帧”里。帧的基本结构是STXStart of TextASCII 0x02表示一帧开始Frame Number一位字符通常是 0-9 循环使用Text Type一个字符比如 1 表示头消息、2 表示查询/订单、3 表示检验结果、L 表示终止帧数据部分以竖线|分隔字段字段内用^分隔子字段ETXEnd of TextASCII 0x03表示数据部分结束Checksum两个十六进制大写字符帧尾通常还有 CR0x0D或 LF0x0A校验和计算有两种常见变体一种是从 STX 之后到 ETX 之前的所有字节求和取低 8 位另一种是把 STX 本身也算进去。资源里的 Demo 用哪种你去翻它的Checksum方法就知道。这里给一个可对照的实现public static string ComputeChecksum(byte[] data, int start, int end) { int sum 0; for (int i start; i end; i) { sum data[i]; } return (sum 0xFF).ToString(X2); }参数说明start指向 STX 的后一个字节end指向 ETX 的位置也就是求和范围覆盖帧号、类型、数据部分。sum 0xFF取低八位ToString(X2)转成两位大写十六进制。很多初次对接的人在这里翻车因为校验和如果只有一位比如0xA必须补0写成0A否则仪器直接丢帧。3.2 增量接收与缓冲区处理把一串字节切成一帧一帧ASTM 解析 Demo 的核心是把网络层送来的连续字节流切分成一条条完整帧。下面是我见过的主流拆包骨架private Listbyte buffer new Listbyte(); private void AppendData(byte[] data, int length) { for (int i 0; i length; i) { buffer.Add(data[i]); } TryExtractFrames(); } private void TryExtractFrames() { while (true) { int stxIndex buffer.IndexOf(0x02); if (stxIndex 0) { buffer.Clear(); return; } if (stxIndex 0) { buffer.RemoveRange(0, stxIndex); stxIndex 0; } int etxIndex buffer.IndexOf(0x03, stxIndex); if (etxIndex 0) { return; // 数据还没到 ETX等待更多字节 } if (etxIndex 2 buffer.Count) { return; // 校验和字符还没收全 } string checksumFromFrame Encoding.ASCII.GetString(buffer.ToArray(), etxIndex 1, 2); string computedChecksum ComputeChecksum(buffer.ToArray(), stxIndex 1, etxIndex); if (!string.Equals(checksumFromFrame, computedChecksum, StringComparison.OrdinalIgnoreCase)) { throw new InvalidDataException($Checksum mismatch: {checksumFromFrame} ! {computedChecksum}); } byte[] frame buffer.GetRange(stxIndex, etxIndex - stxIndex 3).ToArray(); ProcessFrame(frame); buffer.RemoveRange(0, etxIndex 3); } }这段代码有三个关键点。第一buffer是累积缓冲区每次网络层收到新数据就AppendData解析完不能清空因为后面可能还残留着半条帧。第二TryExtractFrames用 while 循环因为缓冲区里可能同时存在多条完整帧解析一条之后要接着看后面还有没有。第三如果已经找到 ETX 但校验和还没凑齐就return等下一次数据到达这是处理半包的核心手法。我额外加了一个stxIndex 0的判断当缓冲区里出现 STX 之前有脏数据时先把它丢弃。真实网络环境里仪器刚上电或重连时可能发几个字节的空包不做这个清理就会帧头定位错误。3.3 解析 Demo 的骨架从字节流到结构化检验数据帧外壳拆下来后数据部分是一串竖线分隔的字段。以最常见的检验结果帧Text Type 3为例结构大致是3O|1|12345678^^^|||.....但不同仪器字段位置差异很大有些仪器会在空字段里塞入\F转义符。示例 Demo 通常做的就是把字段切开存进字典public Dictionarystring, string ParseResultFrame(string dataPart) { string[] fields dataPart.Split(|); var dict new Dictionarystring, string(); if (fields.Length 0) dict[RecordType] fields[0]; if (fields.Length 1) dict[SequenceNumber] fields[1]; if (fields.Length 2) dict[SampleId] fields[2].Split(^)[0]; // 其余字段按协议文档补齐 return dict; }这里必须注意Split(|)的前提是数据字段里没有转义竖线。ASTM 规范里如果字段内需要包含竖线可以用\F转义但不少厂商不按规范来。你接真实仪器时先抓 100 条消息确认哪些字段会产生空串、哪些会多出\F再决定解析逻辑。资源里的 Demo 能帮你把帧外壳拆下来但字段映射必须针对手头仪器型号微调。4. 把示例跑起来编译、启动服务端与模拟仪器联调4.1 工程配置.NET 版本、端口、编码与超时参数这份资源是 Visual Studio 工程从缓存文件看是 .NET Framework 项目。拿到后打开.csproj先确认三件事目标框架版本、启动项目、端口占用。我一般先在工程里搜TcpListener和port把端口改成不冲突的本地端口比如15000。如果端口是硬编码直接改代码如果用了app.config配置项改那里更省事。编码方面先确认Encoding用的是ASCII还是GBK。很多国产仪器虽然标称 ASTM但实际传输用 GBK 或扩展 ASCII。示例默认ASCII中文样本号会变成?我的建议是写成可配置项。超时参数容易被忽视。NetworkStream.Read没有默认超时可能一直挂着。可以设置stream.ReadTimeout 30000但超时后Read会抛IOException你需要捕获后判断是超时还是断开。资源示例如果没做超时你在生产环境必须补上否则某个仪器掉线后连接线程会永远傻等。4.2 启动服务端用 TCP 调试助手扮演仪器客户端编译并运行后服务端进入监听状态。用你顺手的 TCP 调试助手网络调试助手、Sokit甚至 Python 脚本模拟仪器作为客户端去连127.0.0.1:15000。连接成功后服务端应出现新连接记录这验证了 TCP 握手没问题。如果连不上优先检查三处服务端监听的是不是IPAddress.Any端口是否被占用Windows 防火墙是否放行了入站规则本机测试用127.0.0.1可能绕过防火墙换成局域网 IP 就连不上。生产环境部署时记得在防火墙入站规则里放行这个 TCP 端口。4.3 请求与响应手动触发一条 ASTM 消息并观察回应连接建立后往发送框里输入一条完整 ASTM 帧。手工算校验和容易错建议用 Python 快速生成import socket def calc_checksum(data: bytes) - str: s sum(data) 0xFF return f{s:02X} def build_frame(msg_type: int, message: str) - bytes: stx b\x02 etx b\x03 body f{msg_type}.encode(ascii) message.encode(ascii) checksum calc_checksum(body) return stx body etx checksum.encode(ascii) b\r s socket.create_connection((127.0.0.1, 15000)) frame build_frame(1, H|\\^|||demoInstrument|||||LIS||P|1) s.sendall(frame) print(s.recv(4096))这段脚本的作用是计算 ASTM 帧校验和组装成完整帧作为客户端连接服务端并发送最后打印服务端响应。注意build_frame里msg_type是 1对应头消息实际业务要换成仪器手册里的类型。calc_checksum的输入是“帧号 类型 消息体”也就是 STX 之后到 ETX 之前的全部内容。recv(4096)只读一次如果服务端回应超长回显会截断那只是测试脚本的问题不是服务端问题。服务端收到这条帧后ASTM Demo 应打印出解析后的字段并回一个类似ACK的 ASCII 响应。你看到 ACK双向通讯链路就算跑通了。有些仪器要求回EOT或NAK具体看协议文档资源示例里一般留了ResponseBuilder之类的方法给你改。5. TCP/IP 双向通讯避坑指南5 个我调了两天才发现的细节5.1 半包一次 Receive 收到的不是一整条 ASTM 帧现象第一次联调服务端收到的字符串缺了一半报文头还在数据字段被截断解析直接抛异常。多试几次发现截断位置每次都不一样。原因TCP 是字节流协议发送端Write100 字节接收端Read可能只返回 40 字节剩下 60 字节要等下一次Read。如果按“收到多少就解析多少”处理就会出现半个帧。解决把每次收到的字节追加到接收缓冲区去缓冲区里找 STX、ETX 和校验和等一条帧的所有字节齐了再解析。用第 3 章讲的AppendDataTryExtractFrames结构缓冲区没攒够就等下一次。这是从串口转网口的开发者最容易翻车的地方串口按帧发送网口不保证帧边界。5.2 粘包仪器连发两条消息Socket 全粘在一起现象服务端收到的一段完整数据里包含了两条完整 ASTM 帧首尾相连。解析器只认出第一条第二条被当垃圾数据丢掉或者字段错乱。原因TCP 不按发送方的Write次数切分数据。仪器快速连续发两条帧网络栈可能合并成一个 TCP 段送过来缓冲区里会出现两个 STX 开头的数据块。解决拆包逻辑必须用 while 循环反复提取帧直到剩余字节构不成完整帧。注意移除时要连同校验和和帧尾一起移除否则第二条帧会带一个前导垃圾字节stxIndex定位错整个解析就乱了。5.3 端口占用重启服务端报“地址已被使用”现象改代码后重启服务端控制台直接抛SocketException: Address already in use。等一分钟再启动又好了有时候彻底不行。原因TCP 连接断开时主动关闭的一方会进入TIME_WAIT状态占用端口一段时间通常两分钟。服务端如果是主动断开方端口会被系统锁住。另外调试时开多个服务端实例也会抢端口。解决给 Socket 设置ReuseAddress选项。C# 写法Socket socket new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); socket.Bind(new IPEndPoint(IPAddress.Any, 15000)); socket.Listen(10); TcpListener listener new TcpListener(socket); listener.Start();代码说明用显式Socket替代TcpListener的简化构造ReuseAddress允许端口在TIME_WAIT期间重新绑定。注意 Windows 和 Linux 上该选项的语义不完全一致Windows 下基本能解决重启报错。更稳妥的做法是程序退出时调用listener.Stop()并确保所有TcpClient都 Dispose。5.4 编码不对ASCII 里没有中文字符集结果全变问号现象仪器发来的数据里有中文样本号或备注服务端收到后全是?或者乱码。原因ASTM 协议基于 ASCII但很多国内仪器实际用 GBK/GB2312甚至把扩展 ASCII 的 0x80-0xFF 区间用作中文编码。服务端用ASCII解码时非 ASCII 字节被替换成?信息直接丢失。解决不要写死Encoding.ASCII把编码做成可配置项支持GBK、UTF-8、Latin-1。C# 里用Encoding.GetEncoding(GBK)应急同时确认仪器协议文档。更稳妥的方式是先用字节缓冲拆 ASTM 帧帧内字段再按目标编码解码。Encoding.Default在不同系统上结果不同千万别依赖它。5.5 假死连接仪器不主动发包服务端怎么维持心跳现象连接建立后仪器很久不发数据看起来一切正常。等仪器要上报结果时服务端发请求才发现连接早就断了。连接还占着一个线程资源没释放。原因TCP 没有应用层心跳两端都不发数据时操作系统不会立刻发现对端已断开。服务端的Read一直阻塞等待客户端默默重启或断网后这条连接就成了一条僵尸连接。解决应用层加心跳。ASTM 协议里有ENQ0x05和EOT0x04控制字符很多仪器会周期发心跳或响应 LIS 发来的ENQ。你可以起一个定时器每 30 秒发一条ENQ探测如果在规定时间没收到任何响应就主动Close连接并重新等待接入。同时给NetworkStream.ReadTimeout设一个值超时捕获后判断是重连还是标记断开。这样才能保证某台设备死机后服务端能在一分钟内释放资源不会越积越多。6. 把双向通讯模块化协议层与界面分离再用回环脚本自检拿到这份资源跑通基本链路后别急着接真机。我建议先做一个内部改造把 Socket 收发、ASTM 拆帧、业务字段解析三层拆开。Socket 层只负责字节流不感知 ASTMASTM 层把二进制流切成帧并校验业务层拿到解析后的字段对象再决定存库还是上报告警。以后换仪器型号只需要改第三层的字段映射Socket 和 ASTM 层基本不动。验证方法上我习惯用一个“回环自检”脚本在同一台机器上开两个线程一个启动资源里的服务端另一个创建TcpClient连上来连续发送 100 条带随机校验和的 ASTM 帧然后对比服务端回发的 ACK 数量和收到的帧数。这个脚本能验拆包逻辑还能压出半包和粘包。关键点是每条帧之间随机等待 0 到 5 毫秒故意制造 TCP 段合并拆包逻辑如果不够稳健这一轮就会暴露。之前我在某跨平台系统上调试时就是因为漏了“帧号要循环 0-9”这个细节导致仪器一直回 NAK。从那以后我每次对接新仪器都强制自己先跑一遍回环自检确认帧结构、校验和、ACK 三方都对才敢接真机。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

透明加密软件别急着全量推:先搞懂这 6 件事

透明加密软件别急着全量推:先搞懂这 6 件事

很多刚接手终端安全的管理员,一听到“透明加密”,脑子里第一反应往往是:“是不是装上这玩意儿,所有文件就自动安全了?”但真等到要部署的时候,才发现坑多着呢:CAD 图纸打不开、PDM 报错、临时文…

📅 2026/10/14 12:57:50
VS2013下PoDoFo 0.9.5 x86库的编译与集成指南

VS2013下PoDoFo 0.9.5 x86库的编译与集成指南

简介:PDF 文件的程序化读写离不开稳定可用的 C 库支持,存量 Windows 工程则往往受制于编译器版本与平台位数。理解 PDF 库的文档对象模型、流式解析机制以及它与渲染引擎的差异,是选型与二次开发的基础。PoDoFo 0.9.5 提供 PdfDocument、PdfP…

📅 2026/10/14 12:57:50
Windows SDK 10.0.19041.0 编译报错 MSB8036 排查与版本管理指南

Windows SDK 10.0.19041.0 编译报错 MSB8036 排查与版本管理指南

简介:Windows SDK Version 10.0.19041.0 是微软官方提供的开发工具集,对应 Windows 10 2020 年 5 月更新(20H1),面向使用 C、C# 等语言构建桌面应用、UWP 应用及驱动的开发者。它解决的是在 Windows 10 平台上调用系统…

📅 2026/10/14 12:57:50
MORE NEWS

更多资讯

📰

Java语言基础入门与开发环境配置指南

1. Java语言基础入门:从看懂到写出的关键跨越刚接触Java那会儿,我经常陷入"看代码似懂非懂,自己动手就卡壳"的困境。后来发现,这种脱节往往源于对语言基础的理解不够系统。今天我们就来拆解Java语言最核心的基础要素&am…

📰

2026跨境电商新规落地后,这5个长尾词还能闷声出单,第3个多数人没听过

新规落地后,很多卖家发现过去惯用的粗放式关键词打法开始失效:流量贵了,转化散了,广告审核也更严。但长尾词的价值反而在上升——搜索意图明确、竞争密度低、转化路径短。以下五个方向,在2026年的规则环境下依然能稳定…

📰

加氢站系统仿真:Python实现加注过程建模与参数扫描

简介:这份加氢站系统仿真资源面向氢能源基础设施方向的工程师、研究人员及高校学生,用于在计算机中模拟加氢站的实际运行流程,帮助理解并优化氢气储存、压缩、加注、控制与安全等核心环节,在设计阶段提前发现潜在问题、降低风险。…

📰

盲盒经济背后的消费心理学与商业创新

1. 盲盒经济的崛起与市场现状过去三年里,盲盒这种销售模式从潮玩圈的小众玩法迅速破圈,成为零售行业的现象级产品。我亲眼见证过上海某商场泡泡玛特门店前排队上百米的盛况,也研究过不少消费者为集齐整套玩偶而重复购买数十个同款盲盒的案例。…

📰

Java项目调Python跑YOLO视频检测:ONNX导出与进程通信实战

简介:面向以 Java 为主技术栈、需在业务系统中接入 YOLO 视频目标检测能力的开发人员,这份资源提供了一套 Java 与 Python 混合调用 ONNX 模型的完整参考实现,可解决跨语言推理集成中的衔接与调试难题。方案覆盖 YOLOv5、YOLOv7、YOLOv8 三款…

📰

Python爬虫实战:从网页请求到数据存储的完整指南

做爬虫这件事,最初往往源于一句很朴素的抱怨:“这网站上明明有我要的数据,为什么不能直接复制下来?”我第一次写Python爬虫,就是帮朋友整理一个行业网站上的商品价格信息,手动复制粘贴到第两百条的时候&…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬