zmqNut

明知会散落, 仍不惧盛开

0%

python使用struct处理二进制数据

35前言

背景

很多时候我们需要用python处理二进制数据。例如,存储文件、进行socket操作等。这个时候就需要用到struct模块。

python 中的struct方法主要是用来处理C结构数据的,读入时先转换为Python的 字符串 类型,然后再转换为Python的结构化类型,比如元组(tuple)啥的。一般输入的渠道来源于文件或者网络的二进制流。

struct用途

(1)按照指定格式将Python数据转换为字符串(字节流)。如网络传输时不能直接传输int/long数据,此时要先将int/long转化为字节流,然后再发送;
(2)按照指定格式将字节流转换为Python指定的数据类型;
(3)处理二进制数据,如果用struct来处理文件的话,需要用’wb’,’rb’以二进制(字节流)写,读的方式来处理文件;
(4)处理C或者C++语言中的结构体;

接下来我们介绍struct模块中最重要的pack,unpack两个函数。

方法讲解

在转化过程中,主要用到了一个格式化字符串(format strings),用来规定转化的方法和格式。

struct.pack

1
2
#使用形式
struct.pack(fmt, v1, v2, .....)

将v1,v2等参数的值进行一层包装,包装的方法由fmt指定。被包装的参数必须严格符合fmt。最后返回一个包装后的字符串。

struct.unpack

1
2
#使用形式
struct.unpack(fmt, string)

pack打包后,然后就可以用unpack解包了。返回一个由解包数据(string)得到的一个元组(tuple), 即使仅有一个数据也会被解包成元组。其中len(string) 必须等于 calcsize(fmt),这里面涉及到了一个calcsize函数。

struct.calcsize(fmt):这个就是用来计算fmt格式所描述的结构的大小。

如果碰见类似于以下报错:

struct.error: unpack_from requires a buffer of at least 4 bytes

基本就是因为 len(string) 不等于等于 calcsize(fmt) 导致的

fmt说明

格式字符串(format string)由一个或多个格式字符(format characters)组成,对于这些格式字符的描述参照Python manual如下:

13

在format string的首位,有一个可选字符来决定大端和小端,列表如下

@ native native
= native standard
< little-endian standard
> big-endian standard
! network(= big-endian) standard

如果没有附加,默认为@,即使用本机的字符顺序(大端or小端)。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import  struct
# native byteorder
buffer = struct.pack( "ihb" , 1 , 2 , 3 )
print repr ( buffer )
print struct.unpack( "ihb" , buffer )
# data from a sequence, network byteorder
data = [ 1 , 2 , 3 ]
buffer = struct.pack( "!ihb" , * data)
print repr ( buffer )
print struct.unpack( "!ihb" , buffer )

# Output:
# '\x01\x00\x00\x00\x02\x00\x03'
# ( 1 , 2 , 3 )
# '\x00\x00\x00\x01\x00\x02\x03'
# ( 1 , 2 , 3 )

首先将参数1,2,3打包,打包前1,2,3明显属于python数据类型中的integer, pack后就变成了C结构的二进制串,转成 python的string类型来显是:‘\x01\x00\x00\x00\x02\x00\x03’。i 代表C struct中的int类型,故而本机占4位,1则表示为01000000; h 代表C struct中的short类型,占2位,故表示为0200; 同理b 代表C struct中的signed char类型,占1位,故而表示为03

比如程序的后半部分,使用的format string中首位为,即为大端模式标准对齐方式,故而输出为’\x00\x00\x00\x01\x00\x02\x03’,其中高位自己就被放在内存的高地址位了。

由于本机是小端(‘little- endian’),故数据的高字节保存在内存的高地址中。

大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中。

小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。

实际应用

以下为本人实验过程中写的小demo,使用 python实现客户端和服务端的TCP通信,以及发送大包数据。

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import time
import socket
import struct

def get_send_msgflowbytes(length, data):
if length != psize:
print('data lost!')
pass
else:
fmt = "!%dQ" %psize #将每个num转换成64bit
a = struct.pack(fmt, *data) #按照指定格式将Python数据转换为字符串(字节流)
return a

def server():
tcp_server_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)#创建套接字
tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 ) #设置端口复用
tcp_server_socket.bind(('server_ip',port))#绑定本机地址和接收端口
tcp_server_socket.setsockopt(socket.IPPROTO_TCP,socket.TCP_NODELAY,True)#设置为只要系统缓冲区有数据就立刻发送

print('Waiting connecting')
tcp_server_socket.listen(1)#监听()内为最大监听值
client_socket, client_addr = tcp_server_socket.accept() #建立连接
print('Someone has connected to this sever')

num = 0
cnt = 0
psize = 1000
while True:
data = list(range(cnt, cnt + psize))
cnt += psize

msg = get_send_msgflowbytes(len(data), data) #构建符合要求的数据包格式

print('sending msg:', data[0:1], msg[0:8], len(data))

client_socket.send(msg)

time.sleep(1)

client_socket.close()

if __name__=='__main__':
server()

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import socket
import struct

def client():
#创建套接字
tcp_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

#链接服务器
server_addr = ('server_ip',port)
print('Connecting server')
tcp_socket.connect(server_addr)
print('Connected sonmeone!')

while True:
# 接收对方发送过来的数据
recv_data = tcp_socket.recv(1000, socket.MSG_WAITALL) #socket.MSG_WAITALL是为了保证每次凑齐长度为1000的数据才返回

psize = int(len(recv_data) / 8)
fmt = "!%dQ" %psize
# print(struct.calcsize(fmt))

msg = struct.unpack(fmt, recv_data)

print('package length:', psize, 'First bunchid:', msg[0:1], 'First raw data:', recv_data[0:8] )

tcp_socket.close()

if __name__=='__main__':
client()

发送和接收缓冲区

例如:

1
2
3
4
5
6
7
8
9
10
# 设置接收缓冲区大小为 8388608
tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF,8388608)

# 设置发送缓冲区大小为 8388608
tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF,8388608)

# 查看发送和接收缓冲区大小,单位bit
recv_buff = tcp_socket.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF)
send_buff = tcp_socket.getsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF)
print(recv_buff, send_buff)

注意,实际上系统默认会设置buffer大小是你设置的两倍。

---------- End~~ 撒花ฅ>ω<*ฅ花撒 ----------