简介
struct模块常常用在网络编程中,
- 将要发送的数据转换成字节流形式使用函数
struct.pack()
; - 将收到的字节流解析成具体数据使用函数
struct.unpack()
; - 计算格式字符串的长度使用函数
struct.calcsize()
;
它们是struct模块中最常使用的函数,其函数声明为:
pack(fmt, v1, v2, ...)
------ 根据所给的fmt描述的格式将值v1,v2,…转换为一个字符串。unpack(fmt, bytes)
------ 根据所给的fmt描述的格式将bytes反向解析出来,返回一个元组。calcsize(fmt)
------ 根据所给的fmt描述的格式返回该结构的大小。
重点
可以发现,这三个函数中都有一个fmt
的格式字符串,下面给出的是格式字符串中字符的实际含义:
网络发送数据时,有字节序的问题,它的字符含义如下:
以一个简单的网络CS程序为例,服务器程序等待客户端的连接,连接成功后,接收客户端发送的信息并输出;客户端程序连接服务器程序,然后等待输入并发送给服务器程序。
网络编程中,发送端会先发送一个包头,再发送具体的包体(实际发送数据)。包头的意义在于标识出包体数据的长度,包头的长度是固定的,事先预定好的。接收端会先接收的固定长度的包头,再根据包头中标识包体长度的字段来决定接收多少包体数据。例如,包头的结构如下:
struct msg_header
{
unsigned short unused1; // 暂未使用
unsigned int msgBodyLen; // 包体长度
unsigned long long unuse2; // 暂未使用
}
msg_header
对应的fmt
为'HIL'
,根据上面表中信息不难看懂其中的对应关系。
这里没有使用字节序对应的字符。只要客户端与服务端对应即可,如果客户端使用小端字节序
,服务端也需要使用小端字节序
;如果客户端使用大端字节序
,服务端也必须使用大端字节序
。
客户端发送数据的代码片段如下:
client_sock = socket.socket()
...
send_msg = input()
msg_header_fmt = 'HIL' # 包头的格式,与msg_header中字段一一对应
msg_header_data = struct.pack(msg_header_fmt, 0, len(send_msg), 0) # 包头中只有到msgBody字段
client_sock.sendall(msg_header_data)
msg_body_data = send_msg.encode() # send_msg类型为str,发送时需要使用encode()转换成二进制数据
client_sock.sendall(msg_body_data)
客户端发送数据时先发送包头再发送包体,当然也可以包头包体一起发送:
send_msg = input('input:')
msg_fmt = 'HIL{}s'.format(len(send_msg)) # 包头包体的格式
msg_data = struct.pack(msg_fmt, 0, len(send_msg), 0, send_msg.encode()) # 包头中只用到msgBody字段
self.sock .sendall(msg_data)
msg_fmt = 'HIL{}s'.format(len(send_msg))
会根据发送数据的长度生成对应的fmt,例如发送的数据为'hello world'
,msg_fmt
等于就'HIL11s'
,也就表示包头后面跟着长度为11个字节的包体。
服务端接收数据的代码片段如下:
def recv_some(client_sock, data_len)):
...
msg_header_format = 'HIL'
msg_header_len = struct.calcsize(msg_header_format) # 包头长度
# 接收包头收据
recv_suc, msg_header_data = recv_some(client_sock, msg_header_len)
if not recv_suc:
break
# 获取包体长度(只取包体长度字段) 注意strcut.unpack()返回的是一个tuple
_, msg_body_len, _ = struct.unpack(msg_header_format, msg_header_data)
# 接收包体数据
recv_suc, msg_body_data = recv_some(client_sock, msg_body_len)
if not recv_suc:
break
# msg_body_data为二进制数据,需要使用decode()进行装换
print('recv msg: {}'.format(msg_body_data.decode()))
示例
客户端代码为:
import socket
import struct
class TestClient:
def __init__(self, ip_addr, port):
self.ip_addr = ip_addr
self.port = port
self.sock = socket.socket()
def start(self):
print('connect to {}:{}'.format(self.ip_addr, self.port))
self.sock.connect((self.ip_addr, self.port))
print('connect OK')
while True:
send_msg = input('input:')
msg_header_fmt = 'HIL' # 包头的格式,与msg_header中字段一一对应
msg_header_data = struct.pack(msg_header_fmt, 0, len(send_msg), 0) # 包头中只有到msgBody字段
msg_body_data = send_msg.encode()
self.sock .sendall(msg_header_data + msg_body_data)
if __name__ == '__main__':
one_client = TestClient('127.0.0.1', 10088)
one_client.start()
服务端代码为:
import socket
import struct
def recv_some(client_sock, data_len):
recv_data = b''
remain_len = data_len
while True:
try:
cur_data = client_sock.recv(remain_len)
except ConnectionResetError as error:
return False, recv_data
recv_data += cur_data
remain_len -= len(cur_data)
if remain_len == 0:
break
return True, recv_data
class TestServer:
def __init__(self, ip_addr, port):
self.ip_addr = ip_addr
self.port = port
self.sock = socket.socket()
def start(self):
self.sock.bind((self.ip_addr, self.port))
self.sock.listen(5)
print('server listen on {}:{}'.format(self.ip_addr, self.port))
client_sock, client_info = self.sock.accept()
print('recv connection from {}'.format(client_info))
msg_header_format = 'HIL'
msg_header_len = struct.calcsize(msg_header_format)
while True:
# 接收包头
recv_suc, msg_header_data = recv_some(client_sock, msg_header_len)
if not recv_suc:
break
# 获取包体长度
_, msg_body_len, _ = struct.unpack(msg_header_format, msg_header_data)
# 接收包体数据
recv_suc, msg_body_data = recv_some(client_sock, msg_body_len)
if not recv_suc:
break
print('recv msg: {}'.format(msg_body_data.decode()))
if __name__ == '__main__':
one_server = TestServer('127.0.0.1', 10088)
one_server.start()