说粘包之前,先了解两个内容:

  1.缓冲区

  2.windows下cmd窗口调用系统指令

缓冲区(下面粘包现象的图里面还有关于缓冲区的解释)

python tcp缓冲区最大 python socket 缓冲区_数据

每个socket被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区.
write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器.一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情

TCP协议独立于 write()/send()函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制

read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取

这些I/O缓冲区特性可整理如下:
1.I/O缓冲区在每个TCP套接字中单独存在;
2.I/O缓冲区在创建套接字时自动生成;
3.即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
4.关闭套接字将丢失输入缓冲区中的数据.

输入输出缓冲区的默认大小一般都是8K,可以通过 getsockopt() 函数获取

粘包现象: 有两种

先看一下这两种粘包现象流程图

python tcp缓冲区最大 python socket 缓冲区_TCP_02

MTU简单解释:
MTU是Maximum Transmission Unit的缩写,意思是网络上传送的最大数据包.MTU的单位是字节.大部分网络设备的MTU都是1500个字节,也就是1500B.如果本机一次需要发送的数据比网关的MTU大,大的数据包就会被拆开来传送,这样会产生很多数据包碎片,增加丢包率,降低网络速度

关于MTU介绍:
https://yq.aliyun.com/articles/222535百度百科 MTU百科

关于图中提到的Nagle算法等建议自己查一下Nagle算法、延迟ACK、linux下的TCP_NODELAY和TCP_CORK

超出缓冲区大小会报错误,或者udp协议的时候,你的一个数据包的大小超过了你一次recv能接受的大小,也会报错误,但是tcp不会报错,因为tcp是长连接的,不是连续的发送数据包,但tcp超出缓存区大小的时候,肯定会报错误

 

在模拟粘包之前,先了解一下subprocess 模块介绍及使用

1 import subprocess
 2 
 3 sub_obj = subprocess.Popen(
 4     'dir',          #系统指令
 5     shell=True,     #固定格式,相当于cmd窗口
 6     stdout=subprocess.PIPE, #标准输出,保存正确指令的执行结果(PIPE 管道)
 7     stderr=subprocess.PIPE  #标准错误输出,错误的指令执行结果会被它拿到
 8 )
 9 print('错误输出',sub_obj.stderr.read().decode('gbk'))
10 print('正确输出',sub_obj.stdout.read().decode('gbk'))

注意: 如果是windows,那么res.stdout.read()读出的就是GBK编码的,在接收端需要用GBK解码,且只能从管道里读一次结果,PIPE称为管道

下面是subprocess和windows上cmd下的指令的对应示意图: subprocess的stdout.read()和stderr.read(),拿到的结果是bytes类型,所以需要转换为字符串打印出来看

python tcp缓冲区最大 python socket 缓冲区_python tcp缓冲区最大_03

TCP粘包一: 发送数据时间间隔很短,数据也很小,会被优化算法合到一起,产生粘包

1 服务端
 2 如果两次发送有一定的时间间隔,那么就不会出现这种粘包情况,例如在两次发送的中间加一个time.sleep(1)
 3 from socket import *
 4 ip_port = ('127.0.0.1',8888)
 5 tcp_socket_server = socket(AF_INET,SOCK_STREAM)
 6 tcp_socket_server.bind(ip_port)
 7 tcp_socket_server.listen(5)
 8 conn,addr = tcp_socket_server.accept()
 9 
10 #服务端连接接收两个信息
11 data1 = conn.recv(10)
12 data2 = conn.recv(10)
13 
14 print('-------->',data1.decode('utf-8'))
15 print('-------->',data2.decode('utf-8'))
16 
17 conn.close()
18 
19 
20 客户端
21 import socket
22 BUFSIZE = 1024
23 ip_port = ('127.0.0.1',8888)
24 s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
25 # res = s.connect_ex(ip_port)   #同s.connect功能一样,但有返回结果
26 res = s.connect(ip_port)
27 
28 s.send('hello'.encode('utf-8'))
29 s.send('world'.encode('utf-8'))
30 
31 
32 服务端打印结果(全部被第一个recv接收了)
33 --------> helloworld
34 -------->

粘包方式二:
接收方没有及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
例如: 第一次如果发送的数据大小2000B接收端一次性接受大小为1024,这就导致剩下的内容会被下一次recv接收到,导致结果错乱

1 服务端
 2 import socket
 3 import subprocess
 4 server = socket.socket()
 5 ip_port = ('127.0.0.1',8001)
 6 
 7 server.bind(ip_port)
 8 
 9 server.listen()
10 
11 conn,addr = server.accept()
12 
13 while 1:
14     #接收客户端发送给服务端的消息,并打印
15     from_client_cmd = conn.recv(1024)       #一次性接收1024b
16     print(from_client_cmd.decode('utf-8'))
17 
18 
19     sub_obj = subprocess.Popen(
20         from_client_cmd.decode('utf-8'),
21         shell=True,
22         stdout=subprocess.PIPE,
23         stderr=subprocess.PIPE
24     )
25     
26     #读取给客户端返回的消息,并在服务端打印消息的长度
27     std_msg = sub_obj.stdout.read()
28     print('指令的执行结果长度>>>>',len(std_msg))
29 
30     #把消息发送给客户端
31     conn.send(std_msg)
32 
33 
34 客户端
35 import socket
36 
37 client = socket.socket()
38 client.connect(('127.0.0.1',8001))
39 
40 while 1:
41     cmd = input('请输入指令:')
42 
43     client.send(cmd.encode('utf-8'))
44 
45     #接收服务端消息
46     server_cmd_result = client.recv(1024)   #一次性打印1024b,如果没有打印完,下次再继续打印
47     
48     #把接收到的服务端消息转换成gbk编码打印
49     print(server_cmd_result.decode('gbk'))

注意: UDP是面向包的,所以UDP是不存在粘包的

在udp的代码中,我们在server端接收返回消息的时候,我们设置的recvfrom(1024),那么当我输入的执行指令
返回的内容大于1024,就会报错

解释原因:是因为udp是面向报文的,意思就是每个消息是一个包,你接收端设置接收大小的时候,必须要比你发的这个包要大,不然一次接收不了就会报这个错误,而tcp不会报错,这也是为什么ucp会丢包的原因之一,这个和我们上面缓冲区那个错误的报错原因是不一样的

TCP会粘包、UDP永远不会粘包,原因是:

1 发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因.而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的.怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区.
 2 
 3 例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束
 4 
 5 所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的.
 6 
 7 此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段.若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据.
 8 
 9     1.TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务.收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包.这样,接收端,就难于分辨出来了,必须提供科学的拆包机制. 即面向流的通信是无消息保护边界的.
10     2.UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务.不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了. 即面向消息的通信是有消息保护边界的.
11     3.tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头,实验略
12 udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠
13 
14 tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容.数据是可靠的,但是会粘包.
15 
16 为何tcp是可靠传输,udp是不可靠传输
17     tcp在数据传输时,发送端先把数据发送到自己的缓存中,然后协议控制将缓存中的数据发往对端,对端返回一个ack=1,发送端则清理缓存中的数据,对端返回ack=0,则重新发送数据,所以tcp是可靠的.
18     而udp发送数据,对端是不会返回确认信息的,因此不可靠
19 
20 send(字节流)和sendall
21     send的字节流是先放入己端缓存,然后由协议控制将缓存内容发往对端,如果待发送的字节流大小大于缓存剩余空间,那么数据丢失,用sendall就会循环调用send,数据不会丢失,一般的小数据就用send,因为小数据也用sendall的话有些影响代码性能,简单来讲就是还多while循环这个代码呢.
22   
23 
24 用UDP协议发送时,用sendto函数最大能发送数据的长度为:65535- IP头(20) – UDP头(8)=65507字节.用sendto函数发送数据时,如果发送数据长度大于该值,则函数会返回错误.(丢弃这个包,不进行发送) 
25 
26 用TCP协议发送时,由于TCP是数据流协议,因此不存在包大小的限制(暂不考虑缓冲区的大小),这是指在用send函数时,数据长度参数不受限制.而实际上,所指定的这段数据并不一定会一次性发送出去,如果这段数据比较长,会被分段发送,如果比较短,可能会等待和下一次数据一起发送.

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

粘包的解决方案一:
    接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端发一个确认消息给发送端,然后发送端再发送过来后面的真实内容,接收端再来一个死循环接收完所有数据。
(由于双方不知道对方发送数据的长度,导致接收的时候,可能接收不全,或者多接收另外一次发送的信息内容,所以在发送真实数据之前,要先发送数据的长度,接收端根据长度来接收后面的真实数据,但是双方有一个交互确认的过程)

python tcp缓冲区最大 python socket 缓冲区_数据_04

1 服务端
 2 import socket
 3 import subprocess
 4 server = socket.socket()
 5 ip_port = ('127.0.0.1',8001)
 6 
 7 server.bind(ip_port)
 8 
 9 server.listen()
10 
11 conn,addr = server.accept()
12 
13 while 1:
14     from_client_cmd = conn.recv(1024)
15     print(from_client_cmd.decode('utf-8'))
16 
17     #接收到客户端发送来的系统指令,我服务端通过subprocess模块到服务端自己的系统里面执行这条指令
18     sub_obj = subprocess.Popen(
19         from_client_cmd.decode('utf-8'),
20         shell=True,
21         stdout=subprocess.PIPE,  #正确结果的存放位置
22         stderr=subprocess.PIPE   #错误结果的存放位置
23     )
24 
25     #从管道里面拿出结果,通过subprocess.Popen的实例化对象.stdout.read()方法来获取管道中的结果
26     std_msg = sub_obj.stdout.read()
27     #为了解决黏包现象,我们统计了一下消息的长度,先将消息的长度发送给客户端,客户端通过这个长度来接收后面我们要发送的真实数据
28     std_msg_len = len(std_msg)
29     # std_bytes_len = bytes(str(len(std_msg)),encoding='utf-8')
30 
31     #首先将数据长度的数据类型转换为bytes类型
32     std_bytes_len = str(len(std_msg)).encode('utf-8')
33     print('指令的执行结果长度>>>>',len(std_msg))
34     conn.send(std_bytes_len)
35 
36     status = conn.recv(1024)
37     if status.decode('utf-8') == 'ok':
38 
39         conn.send(std_msg)
40     else:
41         pass
42 
43 
44 客户端
45 import socket
46 
47 client = socket.socket()
48 client.connect(('127.0.0.1',8001))
49 
50 while 1:
51     cmd = input('请输入指令:')
52 
53     # 发送编码后的消息给服务端(第一步)
54     client.send(cmd.encode('utf-8'))
55 
56     #接收服务返回的消息长度,并解码打印(第二步)
57     server_res_len = client.recv(1024).decode('utf-8')
58     print('来自服务端的消息长度',server_res_len)
59 
60     #发送给服务端'ok'指令,让服务端发送指令返回结果(第三步)
61     client.send(b'ok')
62 
63     #客户端根据服务端返回的数据长度接收服务端返回的结果(第四步)
64     server_cmd_result = client.recv(int(server_res_len))
65 
66     #打印服务端返回的消息
67     print(server_cmd_result.decode('gbk'))

解决方案二:
    通过struck模块将需要发送的内容的长度进行打包,打包成一个4字节长度的数据发送到对端,对端只要取出前4个字节,然后对这四个字节的数据进行解包,拿到你要发送的内容的长度,然后通过这个长度来继续接收我们实际要发送的内容。为什么要说一下这个模块呢,因为解决方案一里面你发现,我每次要先发送一个我的内容的长度,需要接收端接收,并切需要接收端返回一个确认消息,我发送端才能发后面真实的内容,这样是为了保证数据可靠性,也就是接收双方能顺利沟通,但是多了一次发送接收的过程,为了减少这个过程,我们就要使struck来发送你需要发送的数据的长度,来解决上面我们所说的通过发送内容长度来解决粘包的问题

先了解一下struct模块
struck模块的使用: struct模块中最重要的两个函数
struck.pack('i',数字)  #打包
struck.unpack('i',bytes)    #解包(元组类型,需要索引取数据)

1 import struct
 2 
 3 num = 100
 4 #打包,将int类型的数据打包成4个长度的bytes类型的数据
 5 byt = struct.pack('i',num)
 6 print(byt)
 7 
 8 #解包,将bytes类型的数据,转换为对应的那个int类型的数据
 9 int_num = struct.unpack('i',byt)[0]
10 print(int_num) #(100,)
11 
12 结果
13 b'd\x00\x00\x00'
14 100
15 
16 注意: 如果int类型数据太大会报错struck.error
17 struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #这个是范围

初级版:

1 服务端
 2 import socket,subprocess,struct
 3 server = socket.socket()
 4 server.bind(('127.0.0.1',8888))
 5 server.listen()
 6 while 1:
 7     conn,addr = server.accept()
 8     while 1:
 9         try:
10             # 最大接受的字节数
11             cmd = conn.recv(1024).decode('utf-8')
12             obj = subprocess.Popen(
13                 cmd,
14                 shell=True,
15                 stdout=subprocess.PIPE,
16                 stderr=subprocess.PIPE
17             )
18             right_msg = obj.stdout.read()
19             error_msg = obj.stderr.read()
20 
21             # 获取总数据长度
22             count_len = len(right_msg+error_msg)
23 
24             # 将总数据长度转换成固定长度的bytes
25             from_server_msg = struct.pack('i',count_len)
26             conn.send(from_server_msg)
27 
28             # 发送总数据
29             conn.send(right_msg+error_msg)
30         except Exception:
31             break
32     conn.close()
33 server.close()
34 
35 
36 
37 客户端
38 import socket, struct
39 client = socket.socket()
40 client.connect(('127.0.0.1', 8888))
41 while 1:
42     cmd = input('<<<:').strip()
43     client.send(cmd.encode('utf-8'))
44 
45     # 接收固定长度
46     from_server_len = client.recv(4)
47     print(from_server_len)
48 
49     # 将from_server_len还原成原int类型
50     count_server_len = struct.unpack('i', from_server_len)[0]
51 
52     # 循环接收总数据
53     total_data = b''
54     while len(total_data) < count_server_len:
55         data = client.recv(1024)
56         total_data += data
57     print(total_data.decode('gbk'))
58 client.close()

终极版:
初级版缺点:
1,数据如果过大,strcut会报错
例如
import struct
file_size= 85899345923424323123213
ret = struct.pack('q',file_size)
print(ret)
2,如果上传下载一个文件(视频,音频,文字类文件),自定义一个报头: 文件名,文件大小,文件路径,md5等等

1 服务端
 2 import socket,subprocess,struct,json
 3 server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
 4 server.bind(('127.0.0.1',8888))
 5 server.listen(5)
 6 while 1:
 7     conn,addr = server.accept()
 8     while 1:
 9         try:
10             cmd = conn.recv(1024).decode('utf-8')
11             obj = subprocess.Popen(
12                 cmd,
13                 shell=True,
14                 stdout=subprocess.PIPE,
15                 stderr=subprocess.PIPE,
16             )
17             right_msg = obj.stdout.read()
18             error_msg = obj.stderr.read()
19 
20             # 1,自定制报头
21             head_dic = {
22                 'file_name': 'xx.mp4',
23                 'file_path': r'C:/Py3Practise/day01/test2.py',
24                 'file_size': len(error_msg + right_msg),
25             }
26 
27             # 2,将head_dic利用json转化成json字符串
28             head_dic_json = json.dumps(head_dic)
29 
30             # 3,将json字符串转化成bytes
31             head_dic_json_bytes = head_dic_json.encode('utf-8')
32 
33             # 4,内容bytes长度固定的4个字节
34             head_dic_json_bytes_struct = struct.pack('i',len(head_dic_json_bytes))
35 
36             # 5,发送固定4个字节
37             conn.send(head_dic_json_bytes_struct)
38 
39             # 6,发送bytes类型的字典的报头数据head_dic_json_bytes
40             conn.send(head_dic_json_bytes)
41 
42             # 7,发送数据
43             conn.send(right_msg + error_msg)
44         except Exception:
45             break
46     conn.close()
47 server.close()
48 
49 
50 客户端
51 import socket,struct,json
52 client = socket.socket()
53 client.connect(('127.0.0.1',8888))
54 while 1 :
55     cmd = input('>>>').strip()
56     client.send(cmd.encode('utf-8'))
57 
58     # 1,接受4个字节
59     head_dic_json_bytes_size_strcut = client.recv(4)
60 
61     # 2,利用struct反解出head_dic_json_bytes的具体size
62     head_dic_json_bytes_size = struct.unpack('i',head_dic_json_bytes_size_strcut)[0]
63 
64     # 3,接受head_dic_json_bytes具体数据
65     head_dic_json_bytes = client.recv(head_dic_json_bytes_size)
66 
67     # 4,将head_dic_json_bytes转化成json的格式
68     head_dic_json = head_dic_json_bytes.decode('utf-8')
69 
70     # 5, head_dic_json 反序列化成字典类型
71     head_dic = json.loads(head_dic_json)
72 
73     # 6,循环接收数据
74     total_data = b''
75     while len(total_data) < head_dic['file_size']:
76         total_data += client.recv(1024)
77     print(total_data.decode('gbk'))
78 client.close()