Linux——Socket粘包


Socket粘包

TCP和UDP的区别:

  • TCP(transport control
    protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,为了更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。
    即面向流的通信是无消息保护边界的

  • UDP(user datagram
    protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。
    即面向消息的通信是有消息保护边界的。

  • 由于TCP无消息保护边界, 需要在消息接收端处理消息边界问题。也就是为什么我们以前使用UDP没有此问题。 反而使用TCP后,出现粘包的现象。

以下两种情况发生粘包现象:

客户端粘包:
发送端需要等缓冲区满才发送出去,造成粘包
发送数据时间间隔很短,数据量很小,TCP优化算法会当做一个包发出去,产生粘包

服务端粘包:
接收方没能及时接收缓冲区的包(或没有接收完),造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,
服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)

粘包的分析:

上面说了原理,但可能有人使用TCP通信会出现多包/少包,而一些人不会。那么我们具体分析一下,少包,多包的情况(即粘包)。
正常情况,发送及时每消息发送,接收也不繁忙,及时处理掉消息。像UDP一样.
在这里插入图片描述
发送粘包,多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包. 这种情况和客户端处理繁忙,接收缓存区积压,用户一次从接收缓存区多个数据包的接收端处理一样。
在这里插入图片描述

怎样解决粘包:

问题的根源在于:接收端不知道发送端将要传送的字节流的长度
所以解决粘包的方法就是发送端在发送数据前,发一个头文件包,告诉发送的字节流总大小,然后接收端来一个死循环接收完所有数据
使用struct模块可以用于将Python的值根据格式符,转换为固定长度的字符串(byte类型)
struct模块中最重要的三个函数是pack(), unpack(), calcsize()
pack(fmt, v1, v2, ...) 按照给定的格式(fmt),把数据封装成字符串(实际上是类似于c结构体的字节流)
(将任意大小的数据转换成4个字节)

import struct

i代表int整数,代表4个字节
ret = struct.pack('i',3456)
print(ret,type(ret))
ret1 = struct.pack('i',12345)
print(ret1,type(ret1))

结果:

b'x80
x00x00' <class 'bytes'>
b'90x00x00' <class 'bytes'>

unpack(fmt, string) 按照给定的格式(fmt)解析字节流string,返回解析出来的tuple
(将)

r = struct.unpack('i',ret)
print(r)

r1 = struct.unpack('i',ret1)
print(r1)

结果:

(3456,)
(12345,)

说明:把任意大小的数据转换成4个字节,还能把它转回来;
calcsize(fmt) 计算给定的格式(fmt)占用多少字节的内存

解决思路:
第一:假设要发送两条数据,经过统计第一条数据是37个字节,第二个数据是75个字节,通过struck把37变成4个字节(b'%x00x00x00')固定的四个字节,把第二条数据通过struct变成4个字节(b'Kx00x00x00')

第二:首先发这四个字节,然后再发我要发的内容,在接受的时候先接受这四个字节(b'%x00x00x00'),再通过struct模块转回来,即37,我再来收,我收多少个哪,37个,我就拿到了子一个数据;

则:第二条数据也一样;
已经解决了粘包问题,还有一个问题:

当发送17246123 那么收的时候,也要收17246123,当这个数据达到上线,即如1200,那么要分成多分发送,那么一次性收到的不是这个数据了,那么需要记录一下收了多少的数据,直到与发送的数据的大小相同;否则一直不停,一直再收;

会发生黏包的两种情况:

情况一 :

  • 发送方的缓存机制 发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)

情况二 接收方的缓存机制:

  • 接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
总结:

黏包现象只发生在tcp协议中:

  • 从表面上看,黏包问题主要是因为发送方和接收方的缓存机制、tcp协议面向流通信的特点。

实际上,主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的

黏包解决方案
解决方案一:

问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据。
在这里插入图片描述
存在的问题:
程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗

解决方案进阶:

刚刚的方法,问题在于我们我们在发送

我们可以借助一个模块,这个模块可以把要发送的数据长度转换成固定长度的字节。这样客户端每次接收消息之前只要先接受这个固定长度字节的内容看一看接下来要接收的信息大小,那么最终接受的数据只要达到这个值就停止,就能刚好不多不少的接收完整的数据了。

struct模块
该模块可以把一个类型,如数字,转成固定长度的bytes

import struct
obj = struct.pack('i',123456)
print(len(obj))  # 4
obj = struct.pack('i',898898789)
print(len(obj))  # 4
# 无论数字多大,打包后长度恒为4

在这里插入图片描述
使用struct解决黏包
借助struct模块,我们知道长度数字可以被转换成一个标准大小的4字节数字。因此可以利用这个特点来预先发送数据长度。
在这里插入图片描述

# server端
		obj=subprocess.Popen(cmd.decode('utf-8'),
                             shell=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE
                             )
            stdout=obj.stdout.read()
            stderr=obj.stderr.read()
            # 1. 先制作固定长度的报头
            header=struct.pack('i',len(stdout) + len(stderr))
            # 2. 再发送报头
            conn.send(header)
            # 3. 最后发送真实的数据
            conn.send(stdout)
            conn.send(stderr)
# client端
		#1. 先收报头,从报头里解出数据的长度
    header=client.recv(4)
    total_size=struct.unpack('i',header)[0]
    #2. 接收真正的数据
    cmd_res=b''
    recv_size=0
    while recv_size < total_size:
        data=client.recv(1024)
        recv_size+=len(data)
        cmd_res+=data
    print(cmd_res.decode('gbk'))

我们还可以把报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节(4个自己足够用了)
在这里插入图片描述

# server端
		 # 1. 先制作报头
     header_dic = {
         'filename': 'a.txt',
         'md5': 'asdfasdf123123x1',
         'total_size': len(stdout) + len(stderr)
     }
     header_json = json.dumps(header_dic)
     header_bytes = header_json.encode('utf-8')
     # 2. 先发送4个bytes(包含报头的长度)
     conn.send(struct.pack('i', len(header_bytes)))
     # 3  再发送报头
     conn.send(header_bytes)
     # 4. 最后发送真实的数据
     conn.send(stdout)
     conn.send(stderr)
# client端
		#1. 先收4bytes,解出报头的长度
    header_size=struct.unpack('i',client.recv(4))[0]
    #2. 再接收报头,拿到header_dic
    header_bytes=client.recv(header_size)
    header_json=header_bytes.decode('utf-8')
    header_dic=json.loads(header_json)
    print(header_dic)
    total_size=header_dic['total_size']
    #3. 接收真正的数据
    cmd_res=b''
    recv_size=0
    while recv_size < total_size:
        data=client.recv(1024)
        recv_size+=len(data)
        cmd_res+=data
    print(cmd_res.decode('gbk'))
总结:先发字典报头,再发字典数据,最后发真实数据

SocketServer模块介绍

# TCP socketserver使用
import socketserver
class MyTcpServer(socketserver.BaseRequestHandler):
    def handle(self):
        while True:
            try:
                data = self.request.recv(1024)  # 对于tcp,self.request相当于conn对象
                if len(data) == 0:break
                print(data)
                self.request.send(data.upper())
            except ConnectionResetError:
                break
if __name__ == '__main__':
    server = socketserver.ThreadingTCPServer(('127.0.0.1',8081),MyTcpServer)
    server.serve_forever()

# UDP socketserver使用
import socketserver


class MyUdpServer(socketserver.BaseRequestHandler):
    def handle(self):
        while True:
            data, sock = self.request
            print(data)
            sock.sendto(data.upper(), self.client_address)


if __name__ == '__main__':
    server = socketserver.ThreadingUDPServer(('127.0.0.1', 8080), MyUdpServer)
    server.serve_forever()
有志者,事竟成,破釜沉舟,百二秦关终属楚; 苦心人,天不负,卧薪尝胆,三千越甲可吞吴。 想到与得到中间还有两个字——做到。
原文地址:https://www.cnblogs.com/huoxc/p/13999288.html