Python网络编程04 /recv工作原理、展示收发问题、粘包现象
目录
1. 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工作原理
1.验证服务端缓冲区数据没有取完,又执行了recv执行,recv会继续取值
# server服务端 import socket server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) server.bind((\'127.0.0.1\',8080)) server.listen(5) conn, client_addr = server.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() server.close() # client客户端 import socket client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect((\'127.0.0.1\',8080)) client.send(\'hello\'.encode(\'utf-8\')) client.close()
2.验证服务端缓冲区取完了,又执行了recv执行,此时客户端20秒内不关闭的前提下,recv处于阻塞状态
# server服务端 import socket server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) server.bind((\'127.0.0.1\',8080)) server.listen(5) conn, client_addr = server.accept() from_client_data = conn.recv(1024) print(from_client_data) print(111) conn.recv(1024) # 此时程序阻塞20秒左右,因为缓冲区的数据取完了,并且20秒内,客户端没有关闭。 print(222) conn.close() server.close() # client客户端 import socket import time client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect((\'127.0.0.1\',8080)) client.send(\'hello\'.encode(\'utf-8\')) time.sleep(20) client.close()
3.验证服务端缓冲区取完了,又执行了recv执行,此时客户端处于关闭状态,则recv会取到空字符串
# server服务端 import socket server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) server.bind((\'127.0.0.1\',8080)) server.listen(5) conn, client_addr = server.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() server.close() # client客户端 import socket import time client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect((\'127.0.0.1\',8080)) client.send(\'hello\'.encode(\'utf-8\')) client.close() # recv空字符串: 对方客户端关闭了,且服务端的缓冲区没有数据了,我再recv取到空bytes.
2. 展示收发问题示例
-
发多次收一次
# server服务端 import socket server = socket.socket() server.bind((\'127.0.0.1\',8848)) server.listen(5) conn,addr = server.accept() from_client_data = conn.recv(1024) print(f\'来自客户端{addr}消息:{from_client_data.decode("utf-8")}\') conn.close() server.close() # client客户端 import socket phone = socket.socket() phone.connect((\'127.0.0.1\',8848)) phone.send(b\'he\') phone.send(b\'llo\') phone.close()
-
发一次收多次
# server服务端 import socket server = socket.socket() server.bind((\'127.0.0.1\',8848)) server.listen(5) conn,addr = server.accept() from_client_data = conn.recv(3) print(f\'来自客户端{addr}消息:{from_client_data.decode("utf-8")}\') from_client_data = conn.recv(3) print(f\'来自客户端{addr}消息:{from_client_data.decode("utf-8")}\') from_client_data = conn.recv(3) print(f\'来自客户端{addr}消息:{from_client_data.decode("utf-8")}\') from_client_data = conn.recv(3) print(f\'来自客户端{addr}消息:{from_client_data.decode("utf-8")}\') conn.close() server.close() # client客户端 import socket client = socket.socket() client.connect((\'127.0.0.1\',8848)) client.send(b\'hello world\') client.close()
3. 粘包现象
-
粘包现象概述:
发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾
-
粘包第一种:
send的数据过大,大于对方recv的上限时,对方第二次recv时,会接收上一次没有recv完的剩余的数据。
# server服务端 import socket import subprocess server = socket.socket() server.bind((\'127.0.0.1\',8848)) server.listen(2) while 1: server,addr = server.accept() # 等待客户端链接我,阻塞状态中 while 1: try: from_client_data = conn.recv(1024) if from_client_data.upper() == b\'Q\': print(\'客户端正常退出聊天了\') break obj = subprocess.Popen(from_client_data.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: print(\'客户端链接中断了\') break conn.close() server.close() # client客户端 import socket client = socket.socket() client.connect((\'127.0.0.1\',8848)) while 1: to_server_data = input(\'>>>输入q或者Q退出\').strip().encode(\'utf-8\') if not to_server_data: print(\'发送内容不能为空\') continue client.send(to_server_data) if to_server_data.upper() == b\'Q\': break from_server_data = client.recv(300) # 最多接受1024字节 # 这里可能出现一个字符没有接受完整,进而在解码的时候会报错 # print(f\'{from_server_data.decode("gbk")}\') print(len(from_server_data)) client.close()
-
粘包第二种:
连续短暂的send多次(数据量很小),的数据会统一发送出去.
# server服务端 import socket server = socket.socket() server.bind((\'127.0.0.1\',8848)) server.listen(5) conn,addr = server.accept() from_client_data = conn.recv(1024) print(f\'来自客户端{addr}消息:{from_client_data.decode("utf-8")}\') conn.close() server.close() # client客户端 import socket client = socket.socket() client.connect((\'127.0.0.1\',8848)) client.send(b\'he\') client.send(b\'ll\') client.send(b\'o\') client.close() # Nagle算法:就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。 # Nagle算法的规则: # 1.如果包长度达到MSS,则允许发送; # 2.如果该包含有FIN,则允许发送; # 3.设置了TCP_NODELAY选项,则允许发送; # 4.未设置TCP_CORK选项时,若所有发出去的小数据包(包长度小于MSS)均被确认,则允许发送; # 5.上述条件都未满足,但发生了超时(一般为200ms),则立即发送。
3. 解决粘包现象
-
解决粘包现象的思路:
服务端发一次数据 5000字节, 客户端接收数据时,循环接收,每次(至多)接收1024个字节,直至将所有的字节全部接收完毕.将接收的数据拼接在一起,最后解码. 1. 遇到的问题: recv的次数无法确定. 发送总具体数据之前,先发一个总数据的长度:5000个字节。然后在发送总数据。 客户端: 先接收一个总数据的长度,再根据总数据的长度接收相应长度的字节。 然后再循环recv 控制循环的条件就是只要接收的数据< 5000 一直接收。 2. 遇到的问题: 如何将总数据的长度转化成固定的字节数 3.将不固定长度的int类型转化成固定长度的bytes并且还可以翻转回来:struct模块
-
instruct模块的使用:
import struct # 将一个数字转化成等长度的bytes类型。 ret = struct.pack(\'i\', 183346) print(ret, type(ret), len(ret)) # 结果:b\'2\\xcc\\x02\\x00\' <class \'bytes\'> 4 # 通过unpack反解回来 ret1 = struct.unpack(\'i\',ret)[0] print(ret1, type(ret1)) # 结果:183346 <class \'int\'> # 但是通过struct 处理不能处理太大 ret = struct.pack(\'l\', 4323241232132324) print(ret, type(ret), len(ret)) # 报错 # struct.error: argument out of range
4. low版解决粘包现象
-
server服务端
import socket import subprocess import struct server = socket.socket() server.bind((\'127.0.0.1\',8848)) server.listen(2) while 1: conn,addr = server.accept() while 1: try: from_client_data = conn.recv(1024) # 接收命令 if from_client_data.upper() == b\'Q\': print(\'客户端正常退出聊天了\') break obj = subprocess.Popen(from_client_data.decode(\'utf-8\'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) result = obj.stdout.read() + obj.stderr.read() total_size = len(result) print(f\'总字节数:{total_size}\') # 1. 制作固定长度的报头 head_bytes = struct.pack(\'i\',total_size) # 2. 发送固定长度的报头 conn.send(head_bytes) # 3. 发送总数据 conn.send(result) except ConnectionResetError: print(\'客户端链接中断了\') break conn.close() server.close()
-
client客户端
import socket import struct phone = socket.socket() phone.connect((\'127.0.0.1\',8848)) while 1: to_server_data = input(\'>>>输入q或者Q退出\').strip().encode(\'utf-8\') if not to_server_data: print(\'发送内容不能为空\') continue phone.send(to_server_data) if to_server_data.upper() == b\'Q\': break # 1. 接收报头 head_bytes = phone.recv(4) # 2. 反解报头 total_size = struct.unpack(\'i\',head_bytes)[0] total_data = b\'\' while len(total_data) < total_size: total_data += phone.recv(1024) print(len(total_data)) print(total_data.decode(\'gbk\')) phone.close()
5. 高级版解决粘包方式(自定制报头)
-
解决思路
制作固定的报头,现在有两段不固定长度的bytes类型,需要固定的报头,所以 1. 获取不固定报头的长度,总数据的长度放在报头中(字典) 2. 利用struct模块将不固定的长度转化成固定的字节数 4个字节 3. 先发4个字节,再发报头数据,再发总数据
-
server服务端
import socket import subprocess import struct import json server = socket.socket() server.bind((\'127.0.0.1\',8848)) server.listen(2) while 1: conn,addr = server.accept() while 1: try: from_client_data = conn.recv(1024) # 接收命令 if from_client_data.upper() == b\'Q\': print(\'客户端正常退出聊天了\') break obj = subprocess.Popen(from_client_data.decode(\'utf-8\'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) result = obj.stdout.read() + obj.stderr.read() total_size = len(result) # 1. 自定义报头 head_dic = { \'file_name\': \'test1\', \'md5\': 6567657678678, \'total_size\': total_size, } # 2. json形式的报头 head_dic_json = json.dumps(head_dic) # 3. bytes形式报头 head_dic_json_bytes = head_dic_json.encode(\'utf-8\') # 4. 获取bytes形式的报头的总字节数 len_head_dic_json_bytes = len(head_dic_json_bytes) # 5. 将不固定的int总字节数变成固定长度的4个字节 four_head_bytes = struct.pack(\'i\',len_head_dic_json_bytes) # 6. 发送固定的4个字节 conn.send(four_head_bytes) # 7. 发送报头数据 conn.send(head_dic_json_bytes) # 8. 发送总数据 conn.send(result) except ConnectionResetError: print(\'客户端链接中断了\') break conn.close() server.close()
-
client客户端
import socket import struct import json client = socket.socket() client.connect((\'127.0.0.1\',8848)) while 1: to_server_data = input(\'>>>输入q或者Q退出\').strip().encode(\'utf-8\') if not to_server_data: print(\'发送内容不能为空\') continue client.send(to_server_data) if to_server_data.upper() == b\'Q\': break # 1. 接收固定长度的4个字节 head_bytes = client.recv(4) # 2. 获得bytes类型字典的总字节数 len_head_dic_json_bytes = struct.unpack(\'i\',head_bytes)[0] # 3. 接收bytes形式的dic数据 head_dic_json_bytes = client.recv(len_head_dic_json_bytes) # 4. 转化成json类型dic head_dic_json = head_dic_json_bytes.decode(\'utf-8\') # 5. 转化成字典形式的报头 head_dic = json.loads(head_dic_json) \'\'\' head_dic = { \'file_name\': \'test1\', \'md5\': 6567657678678, \'total_size\': total_size, } \'\'\' total_data = b\'\' while len(total_data) < head_dic[\'total_size\']: total_data += client.recv(1024) # print(len(total_data)) print(total_data.decode(\'gbk\')) client.close()
-
总结:
1. 高大上版: 自定制报头 dic = {\'filename\': XX, \'md5\': 654654676576776, \'total_size\': 26743} 2. 高大上版:可以解决文件过大的问题.