[转载]TCP Socket服务器编程,粘包

转:开发了这么多年,发现最困难的程序开发就是通信系统。html

 

其余大部分系统,例如CRM/CMS/权限框架/MIS之类的,不管怎么复杂,基本上都可以本地代码本地调试,性能也不过重要。(也许这个就是.net的企业级开发的战略吧)java

 

但是来到通信系统,一切变得困难复杂。缘由实在太多了,如:web

  • 性能永远是第一位:有时候一个if判断都要考虑性能,毕竟要损耗一个CPU指令,而在通信系统服务器,每秒钟都产生上百万级别的通信量,这样一个if就浪费了1个毫秒了。
  • 系统环境极其恶劣:全部咱们能够想象的恶意攻击、异常输入等都要考虑;
  • 网络说断就断:在socket环境下,客户端能够以各类理由断开连接,并且服务器根本不会知道,连一个流水做业的业务逻辑都没法保证正常执行,所以须要设计各类辅助的协议、架构去监督。
  • 各类网络连接问题:例如代理、防火墙等等。。。

通过了1年的跌跌撞撞,我总算收获了点有用的经验,本文先从设计角度介绍一些我在Socket编程中的经验,下一篇在放出源代码。编程

 

------------------c#

现有的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比较稳定,固然若是代码调优以后,能提升一点点。

 

------------------

TCP Socket协议定义

------------------

本文从这里开始,主要介绍TCP的socket编程。

新手们(例如当初的我),第一次写socket,老是觉得在发送方压入一个"Helloworld",接收方收到了这个字符串,就“精通”了Socket编程了。而实际上,这种编程根本不可能用在现实项目,由于:

 

1. socket在传输过程当中,helloworld有可能被拆分了,分段到达客户端),例如 hello   +   world,一个分段就是一个包(Package),这个就是分包问题

 

2. socket在传输过成功,不一样时间发送的数据包有可能被合并,同时到达了客户端,这个就是黏包问题。例如发送方发送了hello+world,而接收方可能一次就接受了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, 传输过程当中分割成为hello+world,那么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. 读取包体,因为已知包体长度,所以读取包体就变得很是简单了,只要一直读取到长度未知,剩余的又回到第一条寻找包头。

 

这个逻辑不要小看,就这点东西忙了我1天时间。而很是奇怪的是,我发现c#写的socket,彷佛没有我说的这么复杂逻辑。你们能够看看 LumaQQ.net / DotMsn等,他们的socket接收代码都很是简单。我猜测:要么是.net的socket进行了优化,不会对int之类的进行分割传输;要么就是做 者偷懒,随便写点代码开源糊弄一下。

 

------------------

Socket服务器参数概述

------------------

我在开篇也说了,Socket服务器的环境是很是糟糕了,最糟糕的就是客户端断线以后服务器没有收到通知。 由于socket断线这个也是个信息,也要从客户端传递到咱们socket服务器。有可能网络阻塞了,致使服务器连断开的通知都没有收到。

所以,咱们写socket服务器,就要面对2个环境:

1. 服务器在处理业务逻辑中的任什么时候候都会收到Exception, 任什么时候候都会由于连接中断而断开。

2. 服务器接收到的客户端请求能够是任意字符串,所以在处理业务逻辑的时候,必须对各类可能的输入都判断,防止恶意攻击。

 

针对以上几点,咱们的服务器设计必须包含如下参数:

1. 客户端连接时间记录:主要判断客户端空链接状况,防止链接数被恶意占用。

2. 客户端请求频率记录:要防止客户端频繁发送请求致使服务器负荷太重。

3. 客户端错误记录:一次错误可能致使服务器产生一次exception,而这个性能损耗是很是严重的,所以要严格监控客户端的发送协议错误状况。

4. 客户端发送信息长度记录:有可能客户端恶意发送很是长的信息,致使服务器处理内存爆满,直接致使宕机。

 

5. 客户端短期暴涨:有可能在短期内,客户端忽然发送海量数据,直接致使服务器宕机。所以咱们必须有对服务器负荷进行监控,一旦发现负荷太重,直接对请求的socket返回处理失败,例如咱们常见的“404”。

 

6. 服务器短期发送信息激增:有可能在服务器内部处理逻辑中,忽然产生了海量的数据须要发送,例如游戏中的“群发”;所以必须对发送进行队列缓存,而后进行合并发送,减轻socket的负荷。

相关文章
相关标签/搜索