一、struct
1、简述
我们可以借助一个模块,这个模块可以把要发送的数据长度转换成固定长度的字节。这样客户端每次接收消息之前只要先接受这个固定长度字节的内容看一看接下来要接收的信息大小,那么最终接受的数据只要达到这个值就停止,就能刚好不多不少的接收完整的数据了。
该模块可以把一个类型,如数字,转成固定长度的bytes
View Code
server
client
View Code
View Code
1 >>> struct.pack('i',1111111111111)
2 struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #这个是范围
1 import json,struct
2 #假设通过客户端上传1T:1073741824000的文件a.txt
3
4 #为避免粘包,必须自定制报头
5 header={'file_size':1073741824000,'file_name':'/a/b/c/d/e/a.txt','md5':'8f6fbf8347faa4924a76856701edb0f3'} #1T数据,文件路径和md5值
6
7 #为了该报头能传送,需要序列化并且转为bytes
8 head_bytes=bytes(json.dumps(header),encoding='utf-8') #序列化并转成bytes,用于传输
9
10 #为了让客户端知道报头的长度,用struck将报头长度这个数字转成固定长度:4个字节
11 head_len_bytes=struct.pack('i',len(head_bytes)) #这4个字节里只包含了一个数字,该数字是报头的长度
12
13 #客户端开始发送
14 conn.send(head_len_bytes) #先发报头的长度,4个bytes
15 conn.send(head_bytes) #再发报头的字节格式
16 conn.sendall(文件内容) #然后发真实内容的字节格式
17
18 #服务端开始接收
19 head_len_bytes=s.recv(4) #先收报头4个bytes,得到报头长度的字节格式
20 x=struct.unpack('i',head_len_bytes)[0] #提取报头的长度,解包出来是元组
21
22 head_bytes=s.recv(x) #按照报头长度x,收取报头的bytes格式
23 header=json.loads(json.dumps(header)) #提取报头
24
25 #最后根据报头的内容提取真实的数据,比如
26 real_data_len=s.recv(header['file_size'])
27 s.recv(real_data_len)
2、struct解决黏包问题
借助struct模块,我们知道长度数字可以被转换成一个标准大小的4字节数字。因此可以利用这个特点来预先发送数据长度。
发送时 | 接收时 |
先发送struct转换好的数据长度4字节 | 先接受4个字节使用struct转换成数字来获取要接收的数据长度 |
再发送数据 | 再按照长度接收数据 |
服务端
1 import socket
2 import subprocess
3 import struct
4
5 server = socket.socket()
6 ip_port = ('192.168.15.113',8001)
7 server.bind(ip_port)
8 server.listen()
9 conn,addr = server.accept()
10 while 1:
11 #来自客户端的指令
12 print('等待接受信息。。。')
13 from_client_cmd = conn.recv(1024).decode('utf-8')
14 print(from_client_cmd)
15 #通过subprocess模块执行服务端的系统指令,并且拿到指令执行结果
16 sub_obj = subprocess.Popen(
17 from_client_cmd, #客户端的指令
18 shell=True,
19 stdout=subprocess.PIPE, #标准输出:正确指令的执行结果在这里
20 stderr=subprocess.PIPE, #标准错误输出:错误指令的执行结果在这里
21 )
22 #接受到的返回信息是bytes类型的,并且windows系统的默认编码为gbk
23 server_cmd_msg = sub_obj.stdout.read()
24 # server_cmd_err = sub_obj.stderr.read().decode('gbk')
25 #首先计算出你将要发送的数据的长度
26 cmd_msg_len = len(server_cmd_msg)
27 #先对数据长度进行打包,打包成4个字节的数据,目的是为了和你将要发送的数据拼在一起,就好我们自定制了一个消息头
28 msg_len_stru = struct.pack('i',cmd_msg_len)
29 conn.send(msg_len_stru) #首先发送打包成功后的那4个字节的数据
30 conn.sendall(server_cmd_msg) #循环send数据,直到数据全部发送成功
客户端
1 import socket
2 import struct
3 client = socket.socket()
4 server_ip_port = ('192.168.15.113',8001)
5 client.connect(server_ip_port)
6 while 1:
7 msg = input('请输入要执行的指令>>>')
8 client.send(msg.encode('utf-8'))
9 #先接收服务端要发送给我的信息的长度,前4个字节,固定的
10 from_server_msglen = client.recv(4)
11 unpack_len_msg = struct.unpack('i',from_server_msglen)[0]
12 #接收数据长度统计,和服务端发给我的数据长度作比较,来确定跳出循环的条件
13 recv_msg_len = 0
14 #统计拼接接收到的数据,注意:这个不是统计长度
15 all_msg = b''
16 while recv_msg_len < unpack_len_msg:
17 every_recv_data = client.recv(1024)
18 #将每次接收的数据进行拼接和统计
19 all_msg += every_recv_data
20 #对每次接收到的数据的长度进行累加
21 recv_msg_len += len(every_recv_data)
22
23 print(all_msg.decode('gbk'))
复杂的服务端(自定义报头)
1 import socket,struct,json
2 import subprocess
3 phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
4 phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
5
6 phone.bind(('127.0.0.1',8080))
7 phone.listen(5)
8
9 while True:
10 conn,addr=phone.accept()
11 while True:
12 cmd=conn.recv(1024)
13 if not cmd:break
14 print('cmd: %s' %cmd)
15
16 res=subprocess.Popen(cmd.decode('utf-8'),
17 shell=True,
18 stdout=subprocess.PIPE,
19 stderr=subprocess.PIPE)
20 err=res.stderr.read()
21 print(err)
22 if err:
23 back_msg=err
24 else:
25 back_msg=res.stdout.read()
26
27 headers={'data_size':len(back_msg)}
28 head_json=json.dumps(headers)
29 head_json_bytes=bytes(head_json,encoding='utf-8')
30
31 conn.send(struct.pack('i',len(head_json_bytes))) #先发报头的长度
32 conn.send(head_json_bytes) #再发报头
33 conn.sendall(back_msg) #在发真实的内容
34
35 conn.close()
1 from socket import *
2 import struct,json
3
4 ip_port=('127.0.0.1',8080)
5 client=socket(AF_INET,SOCK_STREAM)
6 client.connect(ip_port)
7
8 while True:
9 cmd=input('>>: ')
10 if not cmd:continue
11 client.send(bytes(cmd,encoding='utf-8'))
12
13 head=client.recv(4)
14 head_json_len=struct.unpack('i',head)[0]
15 head_json=json.loads(client.recv(head_json_len).decode('utf-8'))
16 data_len=head_json['data_size']
17
18 recv_size=0
19 recv_data=b''
20 while recv_size < data_len:
21 recv_data+=client.recv(1024)
22 recv_size+=len(recv_data)
23
24 #print(recv_data.decode('utf-8'))
25 print(recv_data.decode('gbk')) #windows默认gbk编码
26
27 tcp_client.py
head = {'file name': 'test', 'filesize': 8192, 'filetype': 'txt', 'filepath': r'userin'}
报头长度 ——> 先接收4字节
send(head) 报头 ——> 根据这4个字节获取报头
send(file) 报文 ——> 从报头中获取filesize,然后根据filesize接收文件
整个流程的大致解释:
我们可以把报头做成字典,字典里包含将要发送的真实数据的描述信息(大小啊之类的),然后json序列化,然后用struck将序列化后的数据长度打包成4个字节。
我们在网络上传输的所有数据 都叫做数据包,数据包里的所有数据都叫做报文,报文里面不止有你的数据,还有ip地址、mac地址、端口号等等,其实所有的报文都有报头,这个报头是协议规定的,看一下
发送时:
先发报头长度
再编码报头内容然后发送
最后发真实内容
接收时:
先手报头长度,用struct取出来
根据取出的长度收取报头内容,然后解码,反序列化
从反序列化的结果中取出待取数据的描述信息,然后去取真实的数据内容