经过一年时间的沉淀 再次回首 TCP Socket服务器编程 开发了这么多年发现最困难的程序开发就是通讯系统。其他大部分系统例如CRM/CMS/权限框架/MIS之类的无论怎么复杂基本上都能够本地代码本地调试性能也不太重要。也许这个就是.net的企业级开发的战略吧可是来到通讯系统一切变得困难复杂。原因实在太多了如性能永远是第一位有时候一个if判断都要考虑性能毕竟要损耗一个CPU指令而在通讯系统服务器每秒钟都产生上百万级别的通讯量这样一个if就浪费了1个毫秒了。系统环境极其恶劣所有我们可以想象的恶意攻击、异常输入等都要考虑网络说断就断在socket环境下客户端可以以各种理由断开链接而且服务器根本不会知道连一个流水作业的业务逻辑都无法保证正常执行因此需要设计各种辅助的协议、架构去监督。各种网络链接问题例如代理、防火墙等等。。。经过了1年的跌跌撞撞我总算收获了点有用的经验本文先从设计角度介绍一些我在Socket编程中的经验下一篇在放出源代码。------------------现有的Socket编程资源------------------1. 首选推荐开源的XMPP框架也就是Google的Gtalk的开源版本。里面的架构写的非常漂亮。特点就是简洁、清晰。2. 其次推荐LumaQQ.net这套框架本身写的一般般但是腾讯的服务器非常的猛这样必然导致客户端也要比较猛。通过学习这套框架能够了解腾讯的IM传输协议设计而且他们的协议是TCP/UDP结合一举两得。3. 最后就是DotMsn。这个写的实在很一般般而且也主要针对了MSN的协议特点。是能够学习到一点点的框架知识的不过要有所鉴别。------------------Socket的选择------------------在Java到了Java5终于出现了异步编程NIO于是各种所谓的框架冒了出来例如MINA, xsocket等等而在.NET微软一早就为我们准备好了完善的Socket模型。主要包括同步Socket、异步Socket我还听说了.net 3.x之后异步的Socket内置了完成端口。综合各种模型的性能我总结如下1. 如果是短链接使用同步socket。例如http服务器、转接服务器等等。2. 如果是长链接使用异步socket。例如通讯系统QQ / Fetion、webgame等。3. .net的异步socket的连接数性能在 7500/s每秒并发7500个socket链接。而听说完成端口在1.5w所有。但是我到目前还没有正式见过所谓的完成端口不知道到底有多牛逼。4. 我听说了java的NIO性能在5000/s所有我们项目内部也进行了链接测试在4000~5000比较稳定当然如果代码调优之后能提高一点点。------------------TCPSocket协议定义------------------本文从这里开始主要介绍TCP的socket编程。新手们例如当初的我第一次写socket总是以为在发送方压入一个Helloworld,接收方收到了这个字符串就“精通”了Socket编程了。而实际上这种编程根本不可能用在现实项目因为1. socket在传输过程中helloworld有可能被拆分了分段到达客户端例如 hello world一个分段就是一个包Package这个就是分包问题。2. socket在传输过成功不同时间发送的数据包有可能被合并同时到达了客户端这个就是黏包问题。例如发送方发送了helloworld,而接收方可能一次就接受了helloworld.3. socket会自动在每个包后面补n个 0x0 byte分割包。具体怎么去补这个我就没有深入了解。4. 不同的数据类型转化为byte的长度是不同的例如int转为byte是4位int32这样我们在制作socket协议的时候要特别小心了。具体可以使用以下代码去测试代码尽管socket环境如此恶劣但是TCP的链接也至少保证了包发送顺序在传输过程中是不会改变的例如发送方发送 H E L L那么接收方一定也是顺序收到H E L L这个是TCP协议承诺的因此这点成为我们解决分包、黏包问题的关键。如果发送方发送的是helloworld, 传输过程中分割成为helloworld那么TCP保证了在hello与world之间没有其他的byte。但是不能保证helloworld和下一个命令之间没有其他的byte。因此如果我们要使用socket编程就一定要编写自己的协议。目前业界主要采取的协议定义方式是包头包体长度包体。具体如下1. 一般包头使用一个int定义例如int 173173173作用是区分每一个有效的数据包因此我们的服务器可以通过这个int去切割、合并包组装出完整的传输协议。有人使用回车字符去分割包体例如常见的SMTP/POP协议这种做法在特定的协议是没有问题的可是如果我们传输的信息内容自带了回车字符串那么就糟糕了。所以在设计协议的时候要特别小心。2. 包体长度使用一个int定义这个长度表示包体所占的比特流长度用于服务器正确读取并分割出包。3. 包体就是自定义的一些协议内容例如是对像序列化的内容现有的系统已经很常见了使用对象序列化、反序列化能够极大简化开发流程等版本稳定后再转入手工压入byte操作。一个实际编写的例子比如我要传输2个整型 int 1, int 2那么实际传输的数据包如下173173173 8 1 2|------包头------|----包体长度----|--------包体--------|这个数据包就是4个整型总长度 4*4 16。说说我走的弯路我曾经偷懒使用特殊结束符去分割包体这样传输的数据包就不需要指名长度了。可是后来高人告诉我如果使用特殊结束符去判断包性能会损失很大因为我们每次读取一个byte都要做一次if判断这个性能损失是非常严重的。所以最终还是走主流使用以上的结构体。------------------Socket接收的逻辑概述------------------针对了我们的数据包设计socket的传输特点我们的接收逻辑主要是1. 寻找包头。这个包头就是一个int整型。但是写代码的时候要非常注意一个int实际上占据了4个byte而可悲的是这4个byte在传输过程中也可能被socket 分割了因此读取判断的逻辑是判断剩余长度是否大于4读取一个int判断是否包头如果是就跳出循环。如果不是包头则倒退3个byte回到第一点。如果读取完毕也没有找到则有可能包头被分割了因此当前已读信息压入接收缓存等待下一个包到达后合并判断。2. 读取包体长度。由于长度也是一个int因此判断的时候也要小心同上。3. 读取包体由于已知包体长度因此读取包体就变得非常简单了只要一直读取到长度未知剩余的又回到第一条寻找包头。