今日内容大纲
- 粘包现象
- socket缓冲区
- recv的工作原理
- 产生粘包的情况讨论
- 粘包的解决方案
1. 粘包现象
-
测试
s1 = '中国万岁' print(len(s1)) # 获取该字符串字符总字数 b1 = s1.encode('utf-8') print(len(b1)) # 获取该bytes数据的字节的总个数
-
执行远程命令
import socket import subprocess # server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 基于网络的TCP协议 server = socket.socket() # 创建socket对象 server.bind(('127.0.0.1',8848)) # 127.0.0.1 本地回环地址,只允许本机自己连接。 # 3. 待机等待其他人连接 server.listen(5) while 1: conn, addr = server.accept() # 阻塞,等待客户端链接 while 1: try: # 4. 接电话收发消息 from_client_cmd = conn.recv(1024) if from_client_cmd.upper() == b'Q': break obj = subprocess.Popen(from_client_cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) result = obj.stdout.read() + obj.stderr.read() print(f'总字节个数{len(result)}') conn.send(result) except ConnectionResetError: break # 关闭管道 conn.close() server.close() import socket client = socket.socket() # 创建客户端对象 client.connect(('127.0.0.1', 8848)) # 链接服务端 while 1: to_server_data = input('请输入命令:>>>>') client.send(to_server_data.encode('utf-8')) if to_server_data.upper() =+--= 'Q': break from_server_data = client.recv(2048) # 阻塞 print(f'获取的总字节个数{len(from_server_data)}') print(f"结果:{from_server_data.decode('gbk')}") client.close()
粘包现象(个人理解):recv(n)当客户端输入指令时,服务端会返回数据,当这个数据大于n时,客户端只接收自己能接收的最大数据量,剩余的会储存在socket缓冲区,当执行想一次命令时,才会将之前未输出数据再一次输出,每次大小为n,不足n时全部取出,而之后的其他指令对应的数据就会在socket缓冲池中排序,当某条指令的对应的数据第二次仍旧没有取完时,剩下的数据则会与下一次命令对应的数据进行拼接这种现象被称为,粘包现象(就是数据粘在一起的意思)(n的单位为字节)
2. socket缓冲区
每个socket被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。
write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送至目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管他们有没有达到目标机器,也不管他们何时被发送到网络,这些都是TCP协议负责的事。
TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。
read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取
输入输出缓冲区的默认大小一般都是 8K(可修改)
-
为什么设置缓冲区?
-
可以让数据暂存,防止数据丢失。
当数据大于客户端可接收最大数据量时,会暂存在缓冲区,如果没有的话,由于其他地方无法存入这些数据那么,其余的数据将会丢失。
-
收发数据流畅不间断、稳定,提升收发数据的效率 。
因为所有的数据在传输中都有可能被硬件影响导致效率降低,现在两端获取数据都是从缓冲池中获取,避免了这个问题。
-
3. recv工作原理
-
源码分析
源码解释: Receive up to buffersize bytes from the socket. 接收来自socket缓冲区的字节数据, For the optional flags argument, see the Unix manual. 对于这些设置的参数,可以查看Unix手册。 When no data is available, block untilat least one byte is available or until the remote end is closed. 当缓冲区没有数据可取时,recv会一直处于阻塞状态,直到缓冲区至少有一个字节数据可取,或者远程端关闭。 When the remote end is closed and all data is read, return the empty string. 关闭远程端并读取所有数据后,返回空字符串。
-
代码测试
- 测试 recv会一直从缓冲区取值,直到取完为止
#服务端 import socket phone =socket.socket(socket.AF_INET, socket.SOCK_STREAM) phone.bind(('127.0.0.1',8080)) phone.listen(5) conn, client_addr = phone.accept() from_client_data1 = conn.recv(2) print(from_client_data1) from_client_data2 = conn.recv(2) print(from_client_data2) from_client_data3 = conn.recv(1) print(from_client_data3) conn.close() phone.close() #客户端 import socket import time phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.connect(('127.0.0.1',8080)) phone.send('hello'.encode('utf-8')) phone.close()
- 验证服务端缓冲区取完了,又执行recv,此时客户端20秒内不关闭的前提下,recv处于阻塞状态。
# 服务端 import socket phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.bind(('127.0.0.1',8080)) phone.listen(5) conn, client_addr = phone.accept() from_client_data = conn.recv(1024) print(from_client_data) print(111) conn.recv(1024) print(222) conn.close() phone.close() # 客户端 import socket import time phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) phone.connect(('127.0.0.1', 8080)) phone.send('hello'.encode('utf-8')) time.sleep(20) # 此时程序阻塞20秒左右,因为缓冲区数据取完了,并且20秒内,客户端没有关闭。 phone.close()
- 验证服务端缓冲区取完了,又执行了recv,此时客户端处于关闭状态,则recv会取到空字符串。
# 客户端 import socket phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) phone.bind(('127.0.0.1', 8080)) phone.listen(5) conn, client_addr = phone.accept() from_client_data1 = conn.recv(1024) print(from_client_data1) from_client_data2 = conn.recv(1024) print(from_client_data2) from_client_data3 = conn.recv(1024) print(from_client_data3) conn.close() phone.close() # 客户端 import socket import time phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) phone.connect(('127.0.0.1', 8080)) phone.send('hello'.encode('utf-8')) phone.close()
4. 产生粘包的情况讨论
-
如果给客户端一定时间,输入缓冲区未取完的数据会与新到的数据粘在一起(上述已测试)
-
连续短暂的发送少量数据, 接收时数据可能会粘在一起(没有明文解释原理,依系统而定)
# 服务端 import socket phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) phone.bind(('127.0.0.1', 8080)) phone.listen(5) conn, client_addr = phone.accept() from_client_data = conn.recv(1024) print(from_client_data) conn.close() phone.close() # 客户端 import socket import time phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) phone.connect(('127.0.0.1', 8080)) phone.send('he'.encode('utf-8')) time.sleep(1) phone.send('ll'.encode('utf-8')) phone.send('o'.encode('utf-8')) phone.close
-
如何解决粘包
-
连续send这种粘包怎么解决?
代码忽略
这种粘包只能通过代码避免, 不能根本解决。
就是不要连续的send数据。
-
执行远程命令程序,如何每次将产生的结果全部取出?
import socket import time client = socket.socket() client.connect(('127.0.0.1', 8080)) while 1: to_server_data = input('请输入命令>>') client.send(to_server_data.encode('utf-8')) if to_server_data.upper() == 'Q': break time.sleep(2) from_server_data = client.recv(10000000)# 设置上限不是最终解决方式。 print(f"获取的总字节个数{len(from_server_data)}") print(f"结果:{from_server_data.decode('gbk')}") client.close()
-
5. 粘包的解决方案
-
low版
-
思路
-
socket收发数据并不是send与recv一一对应的,客户端发送一次命令给服务端,服务端通过命令产生结果发送回客户端,客户端循环接收完毕所有的数据之后,再次发送命令。
-
那么我们的循环终止条件是什么?
假设:
服务端产生的结果 客户端每次接受的数据 循环次数 1025 recv(1024) 2 8000 recv(1024) 8 10000 recv(1024) 10 客户端循环接收数据,想要让客户端循环终止,我们应该需要让客户端知道每次产生的总数据量,那么怎么让客户端知道总数据量呢?
-
服务端通过len一下,获取总的数据字节量,然后发给客户端;然后服务端再把所有的总数据发送给客户端
如:len(‘fhiabnkghoihgoahjgiher’) --------> 数据类型一定是int类型(整型)
服务端产生的总数据量int类型 转换成bytes类型 客户端接收结果 2584 str(2584).encode(‘utf-8’) recv(4) 10000 str(10000).encode(‘utf-8’) recv(5) 11456 str(11456).encode(‘utf-8’) recv(6) 我们现在需要将不固定的int数据转化成固定长度的bytes类型。
struct模块可以解决我们的问题。
-
struct模块
import struct # 讲一个数字转化成等长度的bytes类型。 ret = struct.pack('i', 123456) print(ret, type(ret), len(ret)) # 通过unpack反解回来 ret1 = struct.unpack('i', ret)[0] print(ret1, type(ret1))
-
代码展示
# 服务端 import socket import subprocess import struct server = socket.socket() server.bind(('127.0.0.1', 8080)) server.listen(5) while 1: conn, addr = server.accept() while 1: try: from_client_cmd = conn.recv(1024) if from_client_cmd.upper() == b'Q': break obj = subprocess.Popen(from_client_cmd.encode('utf-8'), shell = True, stdout = subprocess.PIPE, stderr = subprocess.PIPE) result = obj.stdout.read() + obj.stderr.read() print(f'总字节个数{len(result)}') head_bytes = struct.pack('i', len(result)) # 固定4个字节 conn.send(head_bytes) conn.send(result) except ConnectionResetError: break # 关闭管道 conn.close() server.close() # 客户端 import socket import struct client = socket.socket() client.connect(('127.0.0.1', 8080)) while 1: tto_server_data = input('请输入命令:>>>>') client.send(to_server_data.encode('utf-8')) if to_server_data.upper() == 'Q': break # from_server_data = client.recv(1024) # 阻塞 # 获取固定的头部 head_bytes = client.recv(4) total_len = struct.unpack('i',head_bytes)[0] total = b'' while len(total) < total_len: total += client.recv(1024) print(f'获取的总字节个数{total_len}') print(f"结果:{total.decode('gbk')}") client.close()
获取固定的头部
head_bytes = client.recv(4) total_len = struct.unpack('i',head_bytes)[0] total = b'' while len(total) < total_len: total += client.recv(1024) print(f'获取的总字节个数{total_len}') print(f"结果:{total.decode('gbk')}")
client.close()
-
-