关于TCP粘包/拆包问题

为什么会产生TCP粘包/拆包?

tcp传输的最小单位是一个报文段,而它在传输层是以流动的方式进行传输数据,而在连接层每次传输也会有最大限制,这个最大限制成为MTU ,一般的是1500byte ,如果超过这个将会被分割成多个报文段,而mss就等于MTU减去TCP的首部。

知道了这些,就开始探讨tcp是怎么发送的,这个协议为了提高自身的性能,在发送端(可能是client或者server)会将发送的数据先发送到缓冲区,一直等到缓冲区满了然后才会将数据发送到接受方,而在接收的一端也存在同样的缓冲区机制来接收数据。
下面解释几个专业名词:

数据报大小

IPv4的数据报最大大小是65535字节,包括IPv4首部。因为首部中说明大小的字段为16位。
IPv6的数据报最大大小是65575字节,包括40字节的IPv6首部。同样是展16位,但是IPv6首部大小不算在里面,所以总大小比IPv4大一个首部(40字节)。

MTU

许多网络有一个可由硬件规定的MTU。以太网的MTU为1500字节。。IPv4要求的最小链路MTU为68字节。这允许最大的IPv4首部(包括20字节的固定长度部分和最多40字节的选项部分)拼接最小的片段(IPv4首部中片段偏移字段以8个字节为单位)IPv6要求的最小链路MTU为1280字节。

分片

当一个IP数据报从某个接口送出时,如果它的大小超过相应链路的MTU,IPv4和IPv6都将执行分片。这些片段在到达终点之前通常不会被重组(reassembling)。IPv4主机对其产生的数据报执行分片,IPv4路由器则对其转发的数据报进行分片。然后IPv6只有主机对其产生的数据报执行分片,IPv6路由器不对其转发的数据报执行分片。
IPv4首部的“不分片”位(即DF位)若被设置,那么不管是发送这些数据报的主机还是转发他们的路由器,都不允许对它们分片。当路由器接收到一个超过其外出链路MTU大小且设置了DF位的IPv4数据报时,它将产生一个ICMPv4(目的不可到达,需分片但DF位已设置)的出错消息。
既然IPv6路由器不执行分片,每个IPv6数据报于是隐含一个DF位。当IPv6路由器接收到一个超过其外出链路MTU大小的IPv6数据报时,它将产生一个ICMPv6 “packet too big”的出错消息。IPv4的DF位和隐含DF位可用于路径MTU发现。

最小重组缓冲区大小

IPv4和IPv6都定义了最小缓冲区大小,它是IPv4或IPv6任何实现都必须保重支持的最小数据报大小。其值对IPv4为576字节,对于IPv6为1500字节。例如,对于IPv4而言,我们不能判定某个给定的目的能否接受577字节的数据报,为此很多应用避免产生大于这个大小的数据报。

MSS

TCP有一个最大分段大小,用于对端TCP通告对端每个分段中能发送的最大TCP数据量。MSS的目的是告诉对端其重组缓冲区大小的实际值,从而避免分片。MSS经常设计成MTU减去IP和TCP首部的固定长度。以太网中使用IPv4MSS值为1460,使用IPv6的MSS值为1440(两者TCP首部都是20字节,但是IPv6首部是40字节,IPv4首部是20字节)。

TCP发送缓冲区

每个TCP套接字有一个发送缓冲区,我们可以用SO_SNDBUF套接字选项来更改该缓冲区的大小。当某个应用进程调用write时,内核从该应用进程的缓冲区复制所有数据到缩写套接字的发送缓冲区。如果该套接字的发送缓冲区容不下该应用进程的所有数据(或是应用进程的缓冲区大于套接字的发送缓冲区,或是套接字的发送缓冲区中已有其他数据),该应用进程将被投入睡眠。这里假设该套接字是阻塞的,它通常是默认设置。内核将不从write系统调用返回,直到应用进程缓冲区中的所有数据都复制到套接字发送缓冲区。因此,从写一个TCP套接字的write调用成功返回仅仅表示我们可以重新使用原来的应用进程缓冲区,并不表明对端的TCP或应用进程已接受到数据。

这一端的TCP提取套接字发送缓冲区中的数据并把它发送给对端的TCP,其过程基于TCP数据传送的所有规则。对端TCP必须确认收到的数据,伴随来自对端的ACK的不断到达,本段TCP至此才能从套接字发送缓冲区中丢弃已确认的数据。TCP必须为已发送的数据保留一个副本,直到它被对端确认为止。本端TCP以MSS大小或是更小的块把数据传递给IP,同时给每个数据块安上一个TCP首部以构成TCP分节,其中MSS或是由对端告知的值,或是536(若未发送一个MSS选项为576-TCP首部-IP首部)。IP给每个TCP分节安上一个IP首部以构成IP数据报,并按照其目的的IP地址查找路由表项以确定外出接口,然后把数据报传递给相应的数据链路。每个数据链路都有一个数据队列,如果该队列已满,那么新到的分组将被丢弃,并沿协议栈向上返回一个错误:从数据链路到IP,在从IP到TCP。TCP将注意到这个错误,并在以后某个时候重传相应的分节。应用程序不知道这种暂时的情况。

UDP发送缓冲区

任何UDP套接字都有发送缓冲区大小(我们可以用SO_SNDBUF套接字选项更改它),不过它仅仅是可写道套接字UDP数据报大小上限。如果一个应用进程写一个大于套接字发送缓冲区大小的数据报,内核将返回该进程一个EMSGSIZE错误。既然UDP是不可靠的,它不必保存应用进程数据的一个副本,因此无需一个真正的发送缓冲区。(应用进程的数据在沿协议栈向下传递时,通常被复制到某种格式的一个内核缓冲区中,然而当该数据被发送之后,这个副本被数据链路层丢弃了。)

UDP简单地给来自用户的数据报安上8字节首部以构成UDP数据报,然后传递给IP。IPv4或IPv6给UDP数据报安上相应的IP首部以构成IP数据报,执行路由操作确定外出接口,然后或者直接把数据报加入数据链路层输出队列(如果适合于MTU),或者分片后在把每个片段加入数据集链路层的输出队列。如果某个UDP进程发送大数据报,那么它们相比TCP应用数据更有可能被分片,因为TCP会把应用数据划分成MSS大小的块,而UDP却没有对等的手段。

从写一个UDP套接字的write调用成功返回表示所写的数据报或其所有片段已被加入数据链路层的输出队列。如果该队列没有足够的空间存放该数据报或它的某个片段,内核通常会返回一个ENOBUFS错误给它的应用进程。有些UDP实现不返回这种错误,这样甚至数据报未经发送就被丢弃的情况进程也不知道。

然后,知道了这些概念后,在发送过程中,如果在发送端的应用程序写入的数据大于socket缓冲区大小的话就会发生拆包;
如果发送端的应用程序写入的数据小于socket缓冲区大小的话就会发生粘包。
也就是说,(TCP报文长度 - TCP首部 > mss ) 拆包
反之,就是粘包。

到这里基本理解了为什么会粘包和拆包,下面就是要考虑解决方法。

首先应该明确,TCP协议本身是无法避免拆包和粘包的发生,既然传输层无法下手,因此就应该从应用层下手,在对应用层的数据协议进行控制,而比较通用的方法有下面几种:
1.消息边界 :server需要从网络流中,按照消息边界然后进行编辑分离出完整的消息内容
2.将消息设置为定长消息: server需要读取一定的长度,才为完整的一条消息
3.在首部自己设定协议:消息头存标记位和数据域长度,server先解析消息头得到标记位以及这条消息的数据域长度,在做定向循环截取。

下面用一种类似三次握手四次挥手的方式解决粘包拆包问题,流程图:
在这里插入图片描述

代码如下:

ps: 如果想模拟拆包/粘包,可以将发送端进行循环发送数据,接收端recv的参数调小一点。

server端

#!/usr/bin/python
# -*- coding:utf-8 -*-
import sys
reload(sys)
sys.setdefaultencoding('utf8')
import SocketServer

SEPERATE_FLAG = '|'
READY_FLAG = 'Ready'
START_FLAG = 'Start'
QUIT_FLAG = ''


class InitServer(object):

    def __init__(self, szHostIP, iServerPort):

        self.m_szHostIP = szHostIP
        self.m_iHostPort = iServerPort
        pass

    def startServer(self):

        server = SocketServer.ThreadingTCPServer((self.m_szHostIP, self.m_iHostPort), Myserver)

        try:
            server.serve_forever()
        except KeyboardInterrupt:
            pass
        print 'end startServer'


class Myserver(SocketServer.BaseRequestHandler):

    def handle(self):
        conn = self.request
        print 'ip:' + self.client_address[0] + ' port:' + str(self.client_address[1]) + ' msg:connect success'
        while True:
            try:
                szDataHeader = conn.recv(1024)  # 消息头
            except Exception, e:
                print e
                return
            szDataHeader = str(szDataHeader)
            if szDataHeader == '':
                break
            data_head, data_size = szDataHeader.split(SEPERATE_FLAG)  

            if data_head == READY_FLAG:
                conn.send(START_FLAG)  # 'Start'
            else:
                continue
            msgContent = ''
            while True:
                szmsg = conn.recv(1024)
                msgContent = msgContent + szmsg
                if len(msgContent) == int(data_size):
                    break
            msg = msgContent.decode('gbk')

            if msg == QUIT_FLAG:
                break
        conn.shutdown(1)
        conn.close()


def usageHelp():
    print 'example:python Server.py localip'
    print '       :python Server.py 192.168.1.222'


if __name__ == "__main__":
    if len(sys.argv) != 2:
        usageHelp()
        sys.exit()
    szHostIP = sys.argv[1]
    iHostPort = 6698

    objMyServer = InitServer(szHostIP, iHostPort)
    objMyServer.startServer()

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值