Day 6-3 粘包现象

服务端:

 1 import socket
 2 import subprocess
 3 
 4 phone = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
 5 phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 6 phone.bind(("127.0.0.1", 8990))
 7 
 8 phone.listen(10)
 9 
10 print("运行中...")
11 while True:
12     conn, client_ipaddr = phone.accept()
13     print("客户端IP:%s,端口:%s" % (client_ipaddr[0], client_ipaddr[1]))
14     while True:  # 通信循环
15         try:
16             # 1,接收客户端发送的命令
17             cmd = conn.recv(1024)
18             if not cmd: break
19             # 2,在服务器上执行客户端发过来的命令
20             cmd = subprocess.Popen(cmd.decode("utf-8"), shell=True,
21                                    stdout=subprocess.PIPE,
22                                    stderr=subprocess.PIPE)
23             stdout = cmd.stdout.read()
24             stderr=cmd.stderr.read()
25             # 3,把执行结果发送给客户端
26             conn.send(stdout+stderr)
27         except ConnectionResetError:  # 针对windows系统,客户端强制断开后,会报这个错误.
28             break
29     conn.close()
30 phone.close()

客户端:

 1 import socket
 2 import os
 3 if os.name =="nt":
 4     code = "GBK"
 5 else:
 6     code="utf-8"
 7 
 8 phone1 = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
 9 
10 phone1.connect(("127.0.0.1", 8990))
11 
12 while True:
13     #1,发送命令给服务器
14     cmd = input("请输入你要发送的信息:").strip()
15     if not cmd:continue  
16     phone1.send(cmd.encode("utf-8"))
17     #2,接收服务器执行命令后的结果.
18     data = phone1.recv(1024)
19     print(data.decode(code))
20 phone1.close()

我们分别启动服务端和客户端.然后在客户端上执行一个名 tree c:\ (windows系统).服务端返回的结果如下:

 1 C:\
 2 ├─e_object
 3 ├─GeePlayerDownload
 4 ├─Intel
 5 │  └─Logs
 6 ├─Program Files
 7 │  ├─Common Files
 8 │  │  ├─Microsoft Shared
 9 │  │  │  ├─Filters
10 │  │  │  ├─ink
11 │  │  │  │  ├─ar-SA
12 │  │  │  │  ├─bg-BG
13 │  │  │  │  ├─cs-CZ
14 │  │  │  │  ├─da-DK
15 │  │  │  │  ├─de-DE
16 │  │  │  │  ├─el-GR
17 │  │  │  │  ├─en-US
18 │  │  │  │  ├─es-ES
19 │  │  │  │  ├─et-EE
20 │  │  │  │  ├─fi-FI
21 │  │  │  │  ├─fr-FR
22 │  │  │  │  ├─fsdefinitions
23 │  │  │  │  │  ├─auxpad
24 │  │  │  │  │  ├─keypad
25 │  │  │  │  │  ├─main
26 │  │  │  │  │  ├─numbers
27 │  │  │  │  │  ├─oskmenu
28 │  │  │  │  │  ├─osknumpad
29 │  │  │  │  │  ├─oskpred
30 │  │  │  │  │  ├─symbols
31 │  │  │  │  │  └─web
32 │  │  │  │  ├─he-IL
33 │  │  │  │  ├─hr-HR
34 │  │  │  │  ├─hu-HU
35 │  │  │  │  ├─HWRCustomization
36 │  │  │  │  ├─it-IT
37 │  │  │  │  ├─ja-

我们此时,在客户端继续输入ifconfig 命令,发现返回的数据依然是上次tree c:\的结果.这是为什么呢?

这是因为,客户端一次只能接收1024个字节的数据,如果超过1024个字节,那么这些数据就会在服务器的IO缓存区里暂存下来.如果现在在客户端输入ipconfg命令后,在服务端返回数据给客户端时,因为IO缓存区还有上次tree命令存留的信息,所以会先把上次的信息返回给客户端.等tree命令所有的数据都返回给客户端后,才会返回ipconfig的数据.就造成了两条命令的结果都在某一次的返回数据中.这种现象就叫做粘包.

粘包发生需要满足的条件:

一,在客户端:

  由于TCP协议使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。如果连续发送2个2bytes的包,这时候在客户端就已经发生了粘包现象.但是此时在服务端不一定会发生粘包.

二,服务端:

  如果这2个包没有超出服务器接收的最大字节数(1024),就不会发生粘包.如果服务器每次只接收1bytes,那么在服务端也会发生粘包.

怎么解决粘包这种现象呢?有人说把客户端接收的最大字节值改成其他更大的数字,不就可以了吗?一般情况下,最大接收字节数的值不超过8192.超过这个数,会影响接收的稳定性和速度.

send和recv对比:

1.不管是send还是recv,都不是直接把数据发送给对方,而是通过系统发送.然后从系统内存中读取返回的数据.

2.send和recv不是一一对应的.

3.send工作流程:把数据发送给操作系统,让系统调用网卡进行发送.send就完成了工作

recv工作流程,等待客户端发送过来的数据.这个时间比较长.接收到数据后,再从系统内存中调用数据.

粘包问题只存在于TCP中,Not UDP

还是看上图,发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。

例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束

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

总结

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

  

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

  3. tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头

解决粘包现象的思路:

  通过上述的实验和例子,我们知道,粘包现象的产生,主要是客户端不知道要接收多少数据(或者说多大的数据).那么,按照这个思路,那么我们知道,在服务端执行完命令后,我们可以在服务端获取结果的大小.再发送给客户端,让客户端知道被接收数据的大小,然后再通过一个循环,来接收数据即可.这时我们需要用一个新的模块,struct来制作报头信息.发送给客户端.

import struct
pack = struct.pack("i",10000)   # 定义格式
print(pack,len(pack),type(pack))    # pack的类型是bytes,传输的时候,就不用encode了.

t = struct.unpack("i",pack)     #解包,
print(t)        # 获取元组形式的数据.
t = struct.unpack("i",pack)[0] # 直接获取数据的值.

"""
b"\x10'\x00\x00" 4 <class 'bytes'>
(10000,)
直接获取: 10000
"""
 1 #!_*_ coding:utf-8 _*_
 2 import socket
 3 import subprocess
 4 import struct
 5 
 6 phone = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
 7 phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 8 phone.bind(("127.0.0.1", 8990))
 9 
10 phone.listen(10)
11 
12 print("运行中...")
13 while True:
14     conn, client_ipaddr = phone.accept()
15     print("客户端IP:%s,端口:%s" % (client_ipaddr[0], client_ipaddr[1]))
16     while True:  # 通信循环
17         try:
18             # 1,接收客户端发送的命令
19             cmd = conn.recv(1024)
20             if not cmd: break
21             # 2,在服务器上执行客户端发过来的命令
22             cmd = subprocess.Popen(cmd.decode("utf-8"), shell=True,
23                                    stdout=subprocess.PIPE,
24                                    stderr=subprocess.PIPE)
25             stdout = cmd.stdout.read()
26             stderr=cmd.stderr.read()
27             # 3,把执行结果发送给客户端
28             #3-1 把报头(固定长度)发送给客户端
29             total_size = len(stdout+stderr)
30             print(total_size)
31             header = struct.pack("i",total_size) # i是类型,total_size是值.这个命令会把total_size打包成一个4个字节长度的字节数据类型
32             conn.send(header) # 把报头发送给客户端
33             #302 发送数据给客户端
34 
35             conn.send(stdout)
36             conn.send(stderr)
37         except ConnectionResetError:  # 针对windows系统,客户端强制断开后,会报这个错误.
38             break
39     conn.close()
40 phone.close()
粘包解决服务端
 1 #!_*_ coding:utf-8 _*_
 2 import socket
 3 import os
 4 import struct
 5 
 6 if os.name == "nt":
 7     code = "GBK"
 8 else:
 9     code = "utf-8"
10 
11 phone1 = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
12 
13 phone1.connect(("127.0.0.1", 8990))
14 
15 while True:
16     # 1,发送命令给服务器
17     cmd = input("请输入你要发送的信息:").strip()
18     if not cmd: continue
19     phone1.send(cmd.encode("utf-8"))
20     # 2,接收服务器执行命令后的结果.
21     # 2-1 接收服务器发过来的报头
22     header = phone1.recv(4)  # 收报头
23     total_size = struct.unpack("i", header)[0] #解包,并取出报头中数据
24 
25     # 2-2 循环接收数据
26     recv_size = 0
27     recv_data = b""
28     while recv_size < total_size:
29         data = phone1.recv(1024)  # 接收数据
30         recv_data += data  # 拼接数据
31         recv_size += len(data)  # 设置已接收数据的大小
32     print(recv_data.decode(code))
33 phone1.close()
粘包解决客户端

猜你喜欢

转载自www.cnblogs.com/lovepy3/p/9127541.html
6-3
今日推荐