织网

身体和灵魂,总有一个在路上

socket黏包引发的YY

| Comments

这段时间在帮朋友写一个网络程序, 这个过程中重温了原生的socket编程, 在调试中遇到了一些问题,为此好好回顾了计算机网络, 学生时代学的不好啊! 并由此引出的思考, 遂做笔记如下。

如果你也写过socket程序,作为一个没有太多经验的人,有时候你会发现明明要发送的数据只发送了一部分;或者接收到的数据里面包含发送端几次发送过来的数据。于是悲剧就发生在一行行 debug 之中,特别是夜深人静的时候,双眼泛着血丝, 带着一阵阵脊椎的刺痛。

后来从 Google 那里知道前者是粘包,后者是半包状况。

什么是粘包和半包

为什么会出现粘包与半包状况,是因为 TCP 是基于数据流的传输协议,通常是建立连接之后,就持续的像水流一样发送数据,直到关闭连接,也就是我们传说中的长连接. (PS: HTTP 1.1以后keep alive也是保持长连接,无状态与否是业务层面来决定的,比如HTTP 本身是无状态的,但我们可以通过 Cookie Session 来使其有状态, HTTP 其下层 协议是 TCP,两者本身没有可比性。)

粘包:发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾, 导致了接收的数据冗杂

半包: 接收方没有接收完整的一个包。

可能导致的原因

  • 粘包可能是 TCP 协议造成的,TCP 为提高传输效率(优化算法),发送方往往会收集足够多的数据才发送一包数据,导致了接收方粘包。或者是由于接收方进程来不及接收数据,导致数据在接收端缓冲区黏住了。

  • 半包可能是因为TCP为提高传输效率, 分配一个足够大的包,导致发送端的数据太大,以至于接收方并不一定能一次接受完。

怎么避免

我们需要了解的网络知识是。Socket内部默认的收发缓冲区大小是8k左右, 根据业务重新配置这个值,让系统达到最佳状态, 可以用SetSockOpt 函数配置。

在谈及TCP/UDP 发包的时候,势必需要了解 IP 层和链路成会将包分片发送, 如果分片的大小太大就可能导致丢包的几率大, 特别是对于 UDP 这样不可靠的传输协议, 包太小又导致效率低, 广域网的分片大小是几十字节,

好了,该谈谈怎么避免粘包和半包。 如果你阅读过网络通信的代码,你大概会看到一个包的设计通常会由 Header 部分和 Body 部分,Header 部分通常会有一个域叫做消息长度,通常是为了给接收端做预处理用的. 曾经天真的以为这样的设计是多余的。

通常是 包头+包长+包体 的协议形式(好吧扯淡这么多这是个重点),当服务器端获取到指定的包长时才说明获取完整。

接收端的处理

发包的时候一旦有包的长度那么接收方可以按长度组合。

  • TCP 作为面向连接的数据流协议, 是像水流一样发包,不会整个包到达。
  • UDP 是数据报的协议, 接收端是整包到达的。

你会问,如果接收端的缓冲区比较小,处理 TCP 的时候可以持续收数据,返回实际接受的长度, 这点在使用 epoll 边缘触发的时候需要特别注意,它只被内核唤醒一次状态的改变。而 UDP 由于是整个包接收的,大于接收端缓冲区的数据就会被丢弃返回 WSAEMSGSIZE 。

感觉这样的理解是可以对面向数据流的TCP连接与数据报的UDP连接有更深的认识。

实时网游中多数使用什么网络协议呢

这几天一直在抠网络的知识,然后也思考这个问题,虽然以前也想过。

TCP 是可靠的传输协议,他具有超时重传, 拥塞控制, 流量控制等,这是靠协议来保证的,因此他的数据传输会有一定的代价 ,体现在传输的开销以及时延会比UDP大, UDP 仅仅是将数据包提供给对方,不做任何多余(可能这个词用的不好)的协议上保障。

我们来假设这样的场景:

一个实时网络游戏上,玩家A发包给玩家B,如果采用TCP 协议, 因为网络波动 TCP 会超时重传导致数据发送延迟,导致 玩家B在做下一个动作的时候收到的还是上次动作的消息,这对用户体验不利。 反之如果是 UDP 的话,由于其开销小,而且尽力而为将数据发送给玩家B, 碰到丢包的时候就忽略之,只要新的包能被接收到用户也是无痛感知的。如果真的需要保证安全性,我们可以兼用TCP 来做一些道具购买这样的安全性功能, 保证消息的可靠传输。

遗憾的是, 我没做过网游,只能自己YY, 很多时候看不懂大神云风的博客,就一点点去抠去代入,终究还是觉得环境很重要, 如果有一天能将其所有文章都领会我想会是件令人高兴的事。 我又YY了。。。

夜深了,晚安, 愿有梦的程序猿好梦。

Comments