E语言易语言写的TCP留言功能核心就是两个字转发。一台电脑当服务器其他几台电脑当客户端客户端把留言发到服务器服务器把留言存下来再转发给所有在线的人。这套流程跑通之后你得到的不仅是一个留言板更是一套完整的TCP通信代码骨架以后做聊天工具、远程控制、数据上报都能复用。这篇实战记录从需求拆解讲起把易语言的服务器组件和客户组件怎么用、消息格式怎么定、粘包怎么处理、端口被占用怎么排查、断线重连怎么做一条线完整写出来。不管你是刚接触网络编程的新手还是想快速搭一个局域网小工具的老手都可以直接照着改。我做这个项目时踩过的坑也会一并交代清楚。1. 先把需求拆清楚一个“留言功能”到底要写哪些东西1.1 一条留言从发出到被看到的完整路径留言功能听起来简单但真正写到代码里你要处理的角色比你想象的多。电脑A上的程序输入一段文字点发送这段文字先经过TCP连接到达服务器所在电脑B服务器程序把这段文字记下来再根据当前在线列表把消息原样推送给正在连接的电脑C、D、E。也就是说一个最基础的留言系统至少包含三块能力第一服务器能监听端口并接受客户端的连接请求第二客户端能连接服务器、能把文字转成字节流发出去第三双方约定同一套消息格式让接收方知道一条消息在哪里开始、在哪里结束。很多人一上来就打开易语言拖两个编辑框写个“客户1.发送数据(到字节集(编辑框1.内容))”然后把服务器那边的“数据到达”事件一接就以为完事了。这个小Demo能通但真正能用是另一回事多个客户端同时发消息、消息长了被拆成两段、服务器重启后客户端怎么办、端口被别的程序占了怎么办这些才是实际开发里占八成时间的问题。所以这篇不是教你拖两个控件而是建议你把留言功能当成一个“小型C/S系统”来设计哪怕只有两台电脑在跑。另外要明确一点留言功能和即时聊天软件虽然长得像偏重点完全不同。聊天软件要求“实时到达”用户不在线消息就错过了留言功能要求的是“留得下来”发出去的消息即使对方不在线下次上线也应该能看到。因此服务端不仅要转发还要做存储。存储的载体在易语言里可以很灵活内存数组、文本文件、数据库组件都行看你的数据量。这也解释了为什么我们需要一个“服务端”而不是让客户端之间直接互发——如果A发给B时需要B在线那就退化成了聊天软件而不是留言板。1.2 为什么选TCP而不是UDP选TCP还是UDP这是网络编程的第一个岔路口。我把区别摆出来后面再解释选择理由。对比项TCPUDP可靠性面向连接确认重传丢包会重发无连接发出去就不管到达顺序保证字节流顺序不保证可能乱序消息边界没有边界是连续字节流有消息边界一个包就是一个包连接状态有握手、有挥手占用资源无状态开销小适用场景文件传输、留言、IM、网页音视频直播、游戏坐标、DNS查询留言功能要做的事情是“不能丢、不能乱”。一条留言如果因为网络抖动丢了一半接收方看到的是一段残缺文字这在留言板场景下不可接受。UDP虽然快但你需要自己在应用层做确认、重传、排序等于把TCP已经解决过的难题重新实现一遍对易语言开发者来说性价比很低。所以这个项目用TCP是明确的。有人会问那为什么不直接用HTTPHTTP协议底层就是TCP在易语言里用客户组件发HTTP请求也能把文本POST到服务器但这样做有两个别扭的地方一是HTTP请求每次都要重新握手、带请求头和响应头频繁收发留言开销大二是HTTP天生的“请求-响应”模型不适合服务端主动推送留言板需要的是“有人发了消息所有人立刻看到”也就是服务端主动下发。除非你上WebSocket或者轮询否则不如直接拿TCP做长连接来得干净。这个选择在局域网工具场景里尤其划算——不用搭Web服务、不用配网段、一台机器监听一个端口就够了。1.3 提前设计消息格式否则后面全是坑TCP是“流协议”这是整个项目里最重要的概念。什么意思呢你调用一次发送数据往网络里放了一段字节但对接收方来说看到的就是一根管子里的水流没有天然的“切分记号”。比如你发“你好”和“大家好”两条消息接收方的数据到达事件可能一次收到“你好大家好”也可能分三次才收到“你好”“大家”“好”。如果没有约定格式你根本不知道一条留言在哪结束。所以写任何TCP应用的第一步不是写代码是先定协议。我这套留言功能用的消息格式非常朴素每条消息前面固定放5个字节的头第1个字节是消息类型后面4个字节是消息体长度再往后才是真正的消息内容。类型字段预留了几种1代表登录带上昵称或口令2代表普通留言3代表系统通知比如某人上线、下线未来加心跳包就再定义一个类型。为什么长度要固定4字节因为足够大能表示接近4GB的长度对留言板来说绰绰有余同时接收方在解析时知道“先看5个字节再按长度取后面的内容”不会发生歧义。这套设计的好处是不管消息多长、网络怎么分包接收方永远有一条明确的解析路径。坏处是写起来比“直接发文本”多一点点代码但这笔成本换来的是稳定。后面第五节讲粘包时你会看到几乎所有“留言串台”的诡异问题根源都是没做这种长度字段的消息规划。2. 易语言里的 TCP 组件服务器、客户各自该干什么2.1 组件选型服务器组件和客户组件易语言核心库自带两组网络组件“服务器”和“客户”。名字起得很直白服务器组件负责在某一台电脑上监听端口、接受别人的连接客户组件负责主动去连接服务器。这个分工跟socket编程里的listen/accept和connect完全对应只是易语言把细节藏进了组件里。服务器组件需要关心的事件有三个客户进入、数据到达、客户离开。客户进入相当于TCP三次握手完成服务端accept到了一个新连接你能通过参数拿到一个客户标识本质是一个句柄或ID数据到达就是recv到了字节流客户离开就是连接断开可能是对方主动关闭也可能是网络断了。客户组件的事件少一点连接成功、连接断开、数据到达。注意客户组件没有“客户进入”这种概念因为对客户端来说它只有一个连接对象。最基本的启用流程就两行代码服务器那边是“服务器1.监听(8899)”客户端那边是“客户1.连接(“127.0.0.1”, 8899)”。但建议监听之前先判断返回值别直接往下走。易语言的服务器组件监听方法通常有返回结果返回假就说明端口出问题了直接提示比闷声报错好得多。这个习惯虽然简单能帮你省掉很多排查时间。2.2 三次握手和四次挥手在组件事件里怎么体现搜TCP的资料绕不开三次握手、四次挥手。第一次看的时候我也觉得抽象后来用“打电话”类比就通了三次握手是你拨号、对方接听、你说“能听到吗”对方回“听到了”双方确认链路建立四次挥手是挂电话时你说“我要挂了”对方说“知道了”对方也说“我要挂了”你回“知道了”四条消息把连接干干净净地收尾。在易语言里你不需要写SYN、ACK这些报文组件全帮你做了。但理解握手挥手的过程对排查问题非常有用客户端调连接方法时操作系统实际在发SYN服务器收到后回SYNACK并触发“客户进入”客户端收到ACK后进入已连接状态触发“连接成功”。这四个步骤只要有一个环节出问题——比如防火墙把SYN丢了或者对端端口没监听——表现在应用层就是“连接失败”或“一直转圈”。反过来连接断开时正常的关闭流程是四次挥手服务器和客户端都会触发“连接断开/客户离开”事件你要在这个事件里做资源清理。有一个容易被忽略的点客户端连接成功和服务器“客户进入”这两个事件触发时机不完全同步。客户端可能已经触发连接成功并开始发数据而服务器那边的数据到达事件还没起来。所以在协议里我设计了登录消息客户端连上后第一时间发送一条类型为1的登录消息服务器收到后才把它加入正式的消息转发列表这样就避免了“消息发到半路上没人接”的窗口期。2.3 粘包、半包为什么必须在组件层面就解决不管什么语言做TCP粘包和半包都是绕不开的两座山。所谓粘包就是因为TCP是字节流发送方分两次发的数据接收方可能一次就收到了所谓半包就是一条消息太大接收方一次只收了一部分。两者方向相反本质一样字节流没有边界。易语言的数据到达事件参数是一段字节集。很多新手直接“到文本(数据字节集)”然后往编辑框里塞结果就是留言莫名其妙拼在一起。C里处理粘包通常用缓冲区和包头长度原理一模一样只是语法不同。我在易语言里的做法是用一个程序集变量作为接收缓冲区每次数据到达就把新数据追加进去然后在一个循环里反复尝试解析只要缓冲区里够一个完整的“头体”就取出来取完继续看还有没有下一条。这样无论网络层怎么切、切成几段应用层都能得到一条条完整留言。这里特别强调一点粘包的“包”是应用层的消息不是TCP报文段的包。TCP为了保证效率可能把多次send的内容合并成一个报文发出去这是TCP的正常行为不是bug。见过不少人在群里问“为什么两条消息合一起了是不是我组件用错了”其实原因就是这个。所有语言写TCP都会面对它所以不要在数据到达事件里做简单的“收到就处理”一定要走缓冲区。3. 服务端的具体实现监听、接收、存储、广播3.1 把端口选好监听这件事就成了一半端口号是TCP连接的地基。端口范围是0到65535但0到1023基本被系统或知名服务占了1024到49151是注册端口49152到65535是动态端口。搞局域网留言工具我建议从8000到10000之间选一个不常用的比如8899。为什么不在1024以下你说你抢80端口HTTP服务器可能没跑但系统里一堆服务默认占着低端口你bind上去大概率报错。监听代码很简单服务器1.监听(8899)。但监听失败的原因值得先说清楚最常见的就是端口被占用。我见过一个真实案例某调试工具要启动结果报“starting now at tcp:5037 could not read ok”就是因为电脑上已经有一个程序把5037占住了。Windows上排查端口占用就一条命令netstat -ano | findstr 端口号看到监听状态的PID后再打开任务管理器找到对应进程要么关掉它要么换一个端口。注意如果你在易语言里改了端口客户端那边的连接端口必须同步改这种“两边不一致”的低级错误我犯过不止一次。另外防火墙也是个坑。Windows防火墙默认对陌生程序弹窗你如果点了取消服务器监听虽然成功但外部电脑死活连不上。判断方法很简单监听后在本机用客户端连127.0.0.1能通换局域网真实IP就连不上十有八九是防火墙拦截。要么在防火墙放行这个程序要么开发阶段临时关闭防火墙做测试但正式用的时候一定要开回来并放行指定端口别整个关掉。3.2 客户进入和客户离开维护在线列表服务器接受连接后必须知道“现在有谁在线”。我用的数据结构非常简单一个整数型数组专门存客户标识。客户进入事件里把这个标识加入数组客户离开事件里把它移除。有人可能会问能不能不维护这个数组答案是如果你不打算给其他客户端转发消息确实可以不维护但留言功能的核心就是广播你没有这个列表就不知道消息该发给谁。客户进入事件里还有一件要做的事给这个新客户发一条欢迎消息同时通知老客户“有人上线了”。这正好用到协议里的类型3系统通知。具体代码可以是服务器1.发送数据(客户列表[i], 组装消息(3, “新用户上线”))。这里有个细节提示上线时如果数组里某个客户已经断开但还没触发客户离开事件发送数据可能会失败或抛异常。稳妥的做法是发送前判断或者干脆在数据到达时、定期扫描时清理那些“看起来死了”的连接。这个问题的根源是TCP断开检测有延迟后面心跳流程会彻底解决。客户离开事件不要只是弹个提示。你需要把界面里的状态提示做起来同时立刻从数组里移除这个标识。最容易被忽视的是如果你不移除后续广播时向已断开的标识发送数据轻则无效重则程序报错。另外离开事件里释放的自定义资源比如这个客户上一次未发完的缓冲区数据也在这一步做。3.3 数据到达解析、存储、广播一条龙数据到达是服务端最核心的事件。按照约定的协议接收方先看头5个字节再把消息体完整取出来。示意代码如下.子程序 _服务器1_数据到达 .参数 客户标识, 整数型 .参数 数据字节集, 字节集 追回到该客户自己的接收缓冲区解决粘包半包 接收缓冲区 [客户标识] 接收缓冲区 [客户标识] 数据字节集 判断循环首 (取字节集长度 (接收缓冲区 [客户标识]) ≥ 5) 消息类型 取字节集数据 (接收缓冲区 [客户标识], 1, 1) 消息长度 取字节集数据 (接收缓冲区 [客户标识], 2, 4) 如果真 (取字节集长度 (接收缓冲区 [客户标识]) ≥ 5 消息长度) 消息内容 取字节集中间 (接收缓冲区 [客户标识], 6, 消息长度) 如果真 (消息类型 2) 编辑框_留言板.加入文本 (到文本 (消息内容) #换行符) 存储留言 (消息内容) 广播给所有在线客户 (消息内容) 如果真结束 接收缓冲区 [客户标识] 取字节集右边 (接收缓冲区 [客户标识], 取字节集长度 (接收缓冲区 [客户标识]) 5 消息长度) 否则 跳出循环 () 如果真结束 判断循环尾 ()这里我把接收缓冲区按客户标识分开存每条连接一套避免不同客户的消息串在一起。写完消息后“编辑框_留言板.加入文本”只是给你自己看的操作面板真正重要的是“存储”和“广播”两个动作。存储就是把这条留言追加到历史列表里可以是数组、文本文件或数据库广播就是遍历在线客户数组把消息发给所有在线客户端。为什么广播时要带着原始消息而不是只发文字因为客户端收到的数据要能区分类型。收到类型2才知道这是一条普通留言并显示在留言区收到类型3才知道是上线通知并显示在状态栏。如果你只把文字发了过去客户端无法分辨这条消息是留言还是系统通知。这个设计在做完客户端后你会体会更深。3.4 离线客户的数据怎么办留言功能要求“留得下来”所以服务端还有一个任务客户端下线期间产生的留言要不要在它上线后补发在纯内存数组里做起来很简单每一条留言结构体里存一个递增编号客户登录消息里带上“我已读到的最大编号”服务端把大于该编号的留言全部补发过去。我用过一种更省事的方案登录消息里的编号默认是0客户端每次满屏后记录当前最大编号下次登录时带给服务器服务器做一次过滤即可。如果数据量大到内存扛不住再考虑落地到数据库。易语言操作SQLite比操作MySQL省心单机留言板用SQLite足够。字段最少就四列编号、发送人、内容、时间。补发逻辑就是一条SQL查询按编号大于某个值排序。这样即使服务器重启历史留言也在留言板才真正名副其实。这一节看起来像在讲存储实际上是整个留言功能区别于聊天工具的价值所在别省。4. 客户端的具体实现连接、发送、接收、断线重连4.1 连接服务器IP、端口、超时三要素客户端比服务端简单一点但它有一个服务端没有的烦恼连不上时你得自己判断是服务器没开、IP写错、防火墙拦截还是端口写错。客户组件的连接方法第一个参数是服务器地址第二是端口。本机测试写“127.0.0.1”局域网里要写服务器的真实IP。如果你发现本机能连、换到别的机器就连不上优先怀疑防火墙和IP地址其次是网络是否在同一网段。连接方法通常是异步的调用后会立刻返回真正成功与否要看连接成功事件。这就有个问题你想在界面上提示“连接中...”但如果服务器根本没启动连接失败事件要过几秒才会触发用户体验很差。我的做法是加一个系统时钟连接方法调用后启动时钟计时比如8秒内没触发连接成功就提示超时并断开。这个超时机制在连接远程服务器时尤其重要不要傻等。实际项目里还有一个很常见的错误连接成功后因为某些原因断开你直接再次调用连接方法但上一次连接的状态还没清理干净导致反复失败。所以每次重连前要先调用断开方法把旧连接彻底关闭再发起新连接。这一点在4.4小节还会提到。4.2 发送留言别让中文变成乱码易语言处理文本默认是ANSI编码中文Windows下就是GBK但网络传输和跨平台场景下UTF-8更稳妥。如果服务端和客户端都是你自己的易语言程序两边都按ANSI收发倒也能跑通但一旦你将来用手机端、用C/Python写客户端编码不一致就会出现经典乱码。所以我从一开始就规定所有消息内容统一转成UTF-8字节集再发送接收端解码时也按UTF-8处理。易语言里转UTF-8有几种方式老版本核心库有编码转换命令也可以用精易模块等第三方模块。发送留言的组装逻辑是这样先把文本转成UTF-8字节集取它的字节集长度放到消息头长度字段里最后加上消息类型拼成一个完整消息。注意消息体里不要只放文本可以把发言人的昵称也放进消息体格式可以是“昵称|内容”接收端用分割文本把两者拆开。这样留言板上能显示是谁发的而不是一堆分不清来源的文字。发送后要不要清空输入框我的习惯是发送成功后再清空没成功则保留内容方便用户重发。判断发送成功最直接的办法是看服务器有没有回一条ACK消息。不过ACK会引入额外流量和代码复杂度局域网场景下发送方其实可以先乐观显示后续如果服务器通知失败再回滚。对留言板这种低频场景我倾向于简单处理点了发送就把消息交到组件不做复杂的确认机制。组装消息的示意代码.子程序 发送留言 .参数 留言内容, 文本型 局部变量 内容字节集, 字节集 局部变量 消息字节集, 字节集 内容字节集 编码转换_文本到UTF8 (昵称 “|” 留言内容) 消息字节集 取空白字节集 (5 取字节集长度 (内容字节集)) 置字节集数据 (消息字节集, 2, 1) 第1字节消息类型2代表普通留言 置字节集数据 (消息字节集, 取字节集长度 (内容字节集), 2, 4) 第2字节开始4字节长度 置字节集中间 (消息字节集, 内容字节集, 6) 第6字节开始内容 客户1.发送数据 (消息字节集)这里用到的编码转换和置字节集命令不同版本的易语言写法会有差异对照你本机帮助文档的参数顺序调整即可关键是理解“类型 长度 内容”这个拼装过程。4.3 接收留言解析、显示、去重客户端的“数据到达”事件处理逻辑和服务端解码的思路一致也要处理粘包半包。我在客户端同样维护一个字节集缓冲区同样按“5字节头消息体”循环解析。解析出消息类型后类型2去留言列表显示类型3去状态栏提示类型1可以用于服务器下发的历史补发数据。界面显示用的控件编辑框比标签方便支持多行文本且自带滚动条。每收到一条就在末尾加入文本加换行。.子程序 _客户1_数据到达 .参数 数据字节集, 字节集 接收缓冲区 接收缓冲区 数据字节集 判断循环首 (取字节集长度 (接收缓冲区) ≥ 5) 消息类型 取字节集数据 (接收缓冲区, 1, 1) 消息长度 取字节集数据 (接收缓冲区, 2, 4) 如果真 (取字节集长度 (接收缓冲区) ≥ 5 消息长度) 消息内容 取字节集中间 (接收缓冲区, 6, 消息长度) 处理收到的消息 (消息类型, 消息内容) 接收缓冲区 取字节集右边 (接收缓冲区, 取字节集长度 (接收缓冲区) 5 消息长度) 否则 跳出循环 () 如果真结束 判断循环尾 ()有一个体验细节留言多了以后编辑框的加入文本会越来越慢因为每次重绘整个文本。解决办法是限制只显示最近200条到上限就把最旧的行删掉。另一个细节是线程和UI的问题如果你的数据到达事件里做了太多耗时操作界面会卡因为易语言默认的事件回调在界面线程里跑。批量补发几百条留言时特别明显建议补发时一次性组装成一个大文本再赋值到编辑框而不是几百次加入文本。4.4 断线重连别让用户手动重启程序局域网环境看着稳定但服务器电脑休眠、网线松动、软件崩溃都会让客户端断开。正常的客户端程序应该具备自动重连能力。实现思路很直白在连接断开事件里启动一个时钟每3秒尝试一次连接连接成功事件里停止时钟。复杂一点就做退避连续失败时拉长间隔比如3秒、6秒、12秒防止服务器一恢复客户端就扎堆重连。重连时要注意身份问题。服务器已经把这个客户从在线列表清掉了客户端重连成功后要重新发一次登录消息服务端才能把它加回列表。不然就会出现“客户端显示已连接但别人发的留言他收不到”的情况。这个问题我在初版程序里踩过当时只重连了TCP没有重发登录消息结果列表里的人越攒越乱。另外重连期间用户发送的留言别直接丢掉可以存在一个待发队列里连上后自动补发。这个机制做出来你的留言板基本就具备可以长期挂机使用的稳定性了。5. 实战踩坑记录那些报错和诡异现象5.1 端口绑定失败最典型的bind报错如果你看到类似“listen tcp 127.0.0.1:11434: bind: only one usage of each socket address”这样的报错翻译成人话就是这个IP和端口组合已经被谁占用了系统不让你再绑定一次。这种错误不只易语言会遇到任何语言、任何工具在端口被占时都会给你类似的提示。原因通常有三个同一个程序开了两个实例、上一个程序崩溃但端口还处于TIME_WAIT状态、系统里有别的服务恰好用了同一个端口。排查步骤整理如下先在命令行跑 netstat -ano | findstr 8899查看8899端口监听进程的PID然后 tasklist | findstr PID 看是哪个程序如果是残留的僵尸进程用 taskkill /PID PID /F 结束它如果是不想动的程序最快的方式是换一个端口。还要说一句TIME_WAIT状态下的端口被占用很常见尤其是服务器频繁重启时。尽量避免“短周期内反复监听同一个端口”如果一定要这么做可以尝试设置地址重用SO_REUSEADDR但易语言的组件不一定暴露这个选项那你就老实等一会儿或者换端口。5.2 连接不上、连上就被踢排查顺序很重要我把连接问题分成三层来排查底层网络、服务端监听、客户端配置。底层网络用ping测能通说明IP和链路没问题服务端用TCP调试助手来测直接用助手连服务器的IP和端口能连通说明监听正常问题在客户端配置如果助手也连不上问题在服务端或防火墙。这个“二分定位”的思路比盲目改代码快十倍。像“TCP调试助手1.17”这种小工具界面简单能输入地址端口能显示收发数据就是为这种场景准备的值得常备。连上就被踢还有一个隐蔽原因服务器单次处理数据死循环卡死导致网络事件堆积看起来像客户端一活动就被断开。我遇到过数据到达事件里解析字节集时长度字段取错了循环退不出来整个程序假死。排查这类问题给代码里加输出调试文本或者在关键循环处加计数器限制最大循环次数能帮你快速定位是不是死循环。还有一个经常被忽略的原因客户端连接后长时间不发言某些路由或防火墙的会话超时机制会自动清理空闲TCP连接表现就是“用着用着就断了过一会又好了”。这就是心跳存在的意义。5.3 粘包、半包的经典现场我第一次写的时候两台电脑互相发“你好”“在吗”“晚上吃什么”结果留言板里显示的是“你好在吗晚上吃什么”当时还以为是易语言组件问题查了半天。后来抓包才发现TCP把这些小数据合并成一段了。接收方一次数据到达收到三句完整留言就是粘包。反过来如果发一大段话接收方可能分两次到达第一次只有前半句就是半包。解决的核心思路在1.3节已经说过接收缓冲区 长度字段。这里补充几个实操细节。第一缓冲区要按连接分开服务端每个客户一个缓冲区客户端全局一个缓冲区第二解析循环里必须使用“不够一条完整消息就跳出”不要用“取字节集数据”强行读否则读出来的长度是错的第三解析完要把已消费的字节从缓冲区头部移除我习惯用取字节集右边截取剩余部分注意别把下标算错头部长度是5消息体长度是4字节的数值合起来刚好5 长度。把这三条做到粘包半包从此消失。5.4 心跳机制让服务器知道客户真的活着很多人觉得局域网很稳定不需要心跳但实际情况恰恰相反。服务器计算机休眠、网络交换机老化、无线信号波动这些都不会触发正常的四次挥手TCP连接看起来还连着实际上数据已经发不过去了。如果服务器一直把这种“死连接”留在在线列表里接着给它广播留言发送方会感觉程序偶尔卡一下甚至报错。心跳的做法很简单协议里增加一个消息类型比如类型10代表心跳客户端每10秒发送一条心跳消息服务器记录每个客户最后一次心跳时间服务器每隔30秒扫描一次超过60秒没收到心跳的客户强制认为掉线触发客户离开的处理逻辑。注意心跳消息本身要走正常的收发流程也要过粘包解析那套逻辑只是解析出来后不做界面显示而已。心跳间隔和超时阈值要合理太频繁浪费带宽太迟钝会导致掉线感知不实时。局域网里10秒心跳、30秒扫描、60秒超时是我常用的组合稳。5.5 用wireshark抓包验证你的TCP逻辑到了排查疑难问题的时候光靠猜不行要抓包。wireshark是网络分析的老牌工具打开后在过滤器里输入 tcp.port 8899就能看到这个端口上所有的TCP报文。你能亲眼看到三次握手的SYN、SYNACK、ACK也能看到客户端发送数据时对应的PSH、ACK段还能看到连接关闭时的FIN报文和四次挥手过程。我第一次看清这些报文时之前记的那些理论瞬间就串起来了。抓包还有一个特别有用的场景验证粘包到底发生在哪一层。你可以对比发送方调了几次发送数据、接收方到达了几次数据、wireshark里显示了几个TCP段。如果发送方三次发送但只有一个TCP段说明TCP层合并了应用层只能靠协议解析如果wireshark里显示两个段但你一次到达就收到了也是合并逻辑一样。另外抓包还能发现你写的消息头是不是真的发过去了。我曾经遇到过长度字段高低字节序反了的问题抓包里的十六进制一眼就看出来了。建议所有易语言网络开发者都找时间抓一次自己的程序比看十遍理论有用。6. 几个能进一步提升稳定性的细节6.1 别让界面卡死事件处理和线程分配易语言的网络事件默认跑在界面线程上如果你的“数据到达”事件里做了解析、存库、广播、界面刷新一整套操作在消息频率高的时候界面会明显卡顿。留言场景频率不高一般还好但“批量补发历史留言”这种操作很容易瞬间进入大循环。我的办法是耗时操作拆出去用易语言的线程命令或者先把数据处理完再通过标签反馈事件回到界面线程做UI更新。跨线程访问组件是易语言新手最容易出bug的地方内存变量加临界区、UI操作统一丢回界面线程能避免很多偶发崩溃。另外广播给几十个客户端时发送数据这个动作也耗时间不要在同一个界面函数里连续循环发几十次。可以先把要发送的字节集组装好循环里只做发送中间适当加处理事件。对于100人以下的留言板这个优化足够再大就要考虑异步发送队列了但这已经超出易语言的舒适区不建议硬上。6.2 留言的持久化写文件还是上数据库内存数组的留言服务器一重启就全没了。如果这是自己玩玩无所谓如果要给同事当留言板用历史数据丢了会挨骂。最简单的持久化方案是把留言按行追加到文本文件格式用“时间|昵称|内容”启动时读入新增时追加。这个方案零依赖易语言直接支持数据量几千条没问题。缺点是不支持索引和高效查询查找某条历史留言得逐行扫。数据量再大或者需要按条件查就上SQLite。SQLite不用安装服务一个文件就是整个数据库易语言有对应的操作模块。表结构设计成id、from_user、content、create_time四列业务逻辑里补发历史就是一条select语句。用在生产环境前记得给数据库文件做备份我的习惯是每天启动时把db文件复制一份带日期后缀。这个习惯救过我一次某次测试把数据清错了直接恢复到前一天的备份。6.3 协议预留扩展位别把自己锁死当初设计消息头只用了类型和长度两个字段其实可以在头部再加一个版本号。比如消息头改成“版本1字节 类型1字节 保留2字节 长度4字节”这样后续加图片消息、私聊功能、表情包不需要变更整个协议框架。客户端解析时先看版本号不认识的版本做兼容处理或者提示升级。这个道理跟TCP/IP协议栈每一层都有头部字段一样头部预留空间换来的是演进空间。如果你后续想跟嵌入式设备互通比如ESP8266、W5500 Modbus TCP这类硬件同样可以复用这套“头 长度”的思路。我见过有人把易语言写的留言协议直接移植到STM32上嵌入式端用lwIP协议栈实现TCP客户端发过来的消息PC端完全能解析因为字节流层面的协议只要长度、类型约定一致语言和平台根本不重要。这也是我坚持在协议设计时多想一步的原因就怕将来要扩时重写。6.4 安全局域网不等于没风险写这个留言板时我把安全放到了最后但对实际使用来说它反而重要。第一不要监听公网IP。如果只在内网使用监听时绑定的地址应该是局域网地址或127.0.0.1别在公网上裸奔。第二做一个简单的认证。登录消息里除了昵称还带一个共享口令只有口令正确的客户端才被加入在线列表否则直接断开。这能挡掉大部分扫描器乱连。第三留言内容要有长度上限比如单条不超过2KB服务端超长直接丢弃防止恶意刷屏把数据库撑爆。第四单IP限制连接数比如同一个IP最多3个连接防脚本批量连。做到这四条你的留言板在局域网里基本安全。还有一条比较隐蔽TCP会话劫持和中间人攻击在局域网里用scapy这类工具是可以做到的。不过对普通留言板来说威胁模型没那么高做认证和限流就够用不用上升到加密传输。真要做到消息内容保密那得上TLS但在易语言里工程量大性价比低不推荐。最后说一下我的个人体会。做完这个TCP留言功能我最大的收获不是代码本身而是对“TCP是字节流”这句话有了真正的体感。组件把握手、收发、断开都封装好了但该有的粘包、半包、端口占用、心跳、断线重连一个坑都没少踩。也正因为这样后来我去看C、Python的socket代码发现思路完全一致协议设计、缓冲区解析、超时重连都是同一套逻辑。易语言只是让你用中文把这件事写出来网络世界的规则不会因为语言而改变。如果你也正在写类似的小工具建议先从最简单的两端直连跑通再用抓包工具看看实际报文最后把粘包和重连补上。按这条路线走你踩的坑会比我要少很多。
