IT码农一个~
分类: Python/Ruby
2008-09-19 21:37:05
目录
中如何解析 Struct sockaddr_in 结构
看了很多地方的资料,关于Python与C语言struct之间的转换问题。对于基本的c语言struct在Python下面直接可以利用struct.unpack(fmt, s)便可以解析出来。但一些复杂的struct像系统的struct sockaddr_in 以及struct 里面包含struct的结构又该如何做?
首先查看python2.5的文档,关于struct的类型:
Format |
C Type |
Python |
Notes |
x |
pad byte |
no value |
|
c |
char |
string of length 1 |
|
b |
signed char |
integer |
|
B |
unsigned char |
integer |
|
h |
short |
integer |
|
H |
unsigned short |
integer |
|
i |
int |
integer |
|
I |
unsigned int |
long |
|
l |
long |
integer |
|
L |
unsigned long |
long |
|
q |
long long |
long |
(1) |
Q |
unsigned long long |
long |
(1) |
f |
float |
float |
|
d |
double |
float |
|
s |
char[] |
string |
|
p |
char[] |
string |
|
P |
void * |
integer |
|
图1. Python与C数据类型对照关系
这里面没有关于struct sockaddr_in这样的结构。后来一想,c语言中的struct的存放其实就是连续存放的基本数据类型(padding暂不谈)。Struct无非就是给他们归个类而已。那么我只要知道struct sockaddr_in的内部的具体数据类型,不就可以unpack了么?
接下来就是查找sockaddr_in的具体类型。因为我有《UNIX网络编程》第一卷这本书,所以就直接翻书。上面写的是如果的POSIX定义,具体定义在
struct in_addr { in_addr_t s_addr; /* 32-bit IPv4 address */ /* network byte ordered */ }; struct sockaddr_in { uint8_t sin_len; /* length of structure (16) */ sa_family_t sin_family; /* AF_INET */ in_port_t sin_port; /* 16-bit TCP or UDP port number*/ /* network byte ordered*/ struct in_addr sin_addr; /* 32-bit IPv4 address */ /*network byte ordered */ char sin_zero[8]; /*unused*/ }; |
图2. Stuct sockaddr_in的POSIX定义
OK, 这里面又出现了很多别名,好那么我全给你查出来,这个可以直接google搜索,也可以在系统include目录下找。至于哪个快点就看各自的熟悉程度了。
我找的结果如下:
typedef unsigned int in_addr_t typedef unsigned char uint8_t typedef unsigned short sa_family_t typedef unsigned short in_port_t |
图3 几个数据的别名
Ok, 各种数据类型全面搞定,开始编程:
服务器端是用python写的,目的是接收c语言发过来的struct sockaddr_in结构的字段。
程序如下:
from socket import * from time import time, ctime Host = '' Port = 21567 BUFSIZ = 1024 ADDR = (Host,
Port) tcpSerSock =
socket(AF_INET, SOCK_STREAM) tcpSerSock.bind(ADDR) tcpSerSock.listen(5) try: while 1: print 'waiting for connection...' tcpCliSock, addr =
tcpSerSock.accept() print '...connected from :' ,addr while 1: data = tcpCliSock.recv(BUFSIZ) if not data: break print "received :", len(data) import struct ip = struct.unpack('HHIII0l', data) #line 24 print "ip[0] ",ip[0] print "ip[1] ",ip[1] #netip = struct.unpack('!I', ip[2]) print "ip[2] ",ip[2] print "ip[3] ",ip[3] print "ip[4] ",ip[4] #tcpCliSock.send("hello ack") break #tcpCliSock.send('[%s] %s' % # (ctime(time()),
data)) tcpCliSock.close() except EOFError: print 'EOFError Occur' except KeyboardInterrupt: print 'KeyboardInterrupt
catched' finally: tcpSerSock.close() 运行结果: |
图4 python服务器端程序
注: 这个最终的正确程序,请注意我表明的line 24那一行,最开始不是这样的,一会再细解。
实现的
C语言: 01#include 运行结果: |
图5. Tcp客户端程序
这里面的46-51行也是正确的。一开始是错误的。
最初我46行没加注释,编译的时候没通过。说servaddr没有这个字段。 我就到netinet/in.h里面找sockadd_in的定义,找到是这样的:
216 /* Structure describing an Internet socket address. */ 217 struct sockaddr_in 218 { 219 __SOCKADDR_COMMON (sin_); 220 in_port_t sin_port; /* Port number. */ 221 struct in_addr sin_addr; /* Internet address. */ 222 223 /* Pad to size of `struct sockaddr'. */ 224 unsigned char sin_zero[sizeof (struct sockaddr) - 225 __SOCKADDR_COMMON_SIZE - 226 sizeof (in_port_t) - 227 sizeof (struct in_addr)]; 228 }; |
图6
又到linux/in.h下面发现另一个定义:
179 /* Structure describing an Internet (IP) socket address. */ 180 #define __SOCK_SIZE__ 16 /* sizeof(struct sockaddr) */ 181 struct sockaddr_in { 182 sa_family_t sin_family; /* Address family */ 183 __be16 sin_port; /* Port number */ 184 struct in_addr sin_addr; /* Internet address */ 185 186 /* Pad to size of `struct sockaddr'. */ 187 unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) - 188 sizeof(unsigned short int) - sizeof(struct in_addr)]; 189 }; 190 #define sin_zero __pad /* for BSD UNIX comp. -FvK */ |
图7
我把程序里面的include
至于最后一个__pad那是一个填充用的,目的是为了和通用地址结构相同大小。
好,我一开始是这样unpack的,struct.unpack(‘BHHIc0l’, buf); 这是严格按照图2和图1以及图3结合对应的。并加上了对齐’0l’,后面是字母’l’表示python 中的long类型对齐。Python中的long类型是4字节的,因为c语言的struct里面进行了对齐。结果解析出来前两项是对的,第3项就相差很远了。后来根据图7重新改成 struct.unpack(‘HHIII’,s)解析正确。
其中第一个’H’对应sin_family。 第二个’H’对应sin_port, 第三个’I’对应sin_addr. 这三个按4字节对齐才8个字节。而struct sockaddr_in的长度是16字节。由于一开始后面的解析出来都是0,所以我想是填充字节,就用了两个’II’凑够16字节。结构经试验,图4和图5的打印结构一致。
至此告一段落,本文后面将附上关于padding的示例:
以下是转载,原文:http://borland.mblogger.cn/topcat/posts/25118.aspx
今天老鼠问我一个关于Python的socket传输问题。他说socket需要传输一个C++的struct,但是Python接受到之后却不能使用struct模块正确解开。我就帮他看了看。
该结构大致如下:
struct TestStruct
{
int data1;
char data2;
char data3;
};
对应的Python代码:
import struct
s = struct.unpack("icc", buf) #buf是从网络接收的字节流
结果却报“Unpack str size does not match format”错。
很明显是C++ struct产生的size和Python解码所需的不同。于是检查C++的struct size:
printf ("size=%d\n", sizeof(TestStruct));
结果得8。
而
struct.calcsize("icc")
结果却是6。
仔细想想,icc这种排列方式在字段间的确不会产生padding字节,也就是说,python的结果是对的。但为什么C++的结果会是8呢?原来 C++的字节对齐,除了struct内部需要字节对齐之外,struct变量本身也是需要字节对齐的,这是为了当生成一个struct数组的时候仍然能够保证所有字段的字节对齐。因此,像icc这种本身没有padding,但整体需要padding的情形,编译器会在整个struct的末尾加上 padding字节(在这里是2个字节),也正是这2个字节导致了Python的解码错误。
幸亏Python已经考虑到了这个问题,在Python struct module document的最后一段的Hint的中说,如果出现这种末尾对齐的情况,可以在格式字符串的最后加上一个“0X”,其中这个“X”可以是一个有效的格式字符,这个字符所代表的长度等于整个struct对齐的长度。在上面这种情况中,对齐长度是4字节,因此我们使用“l”字母(long, 4字节)来进行对齐:
s = struct.unpack("icc0l", buf)
问题搞定。
额外的讨论
情形1:
struct Test2
{
char a1;
int a2;
char a3;
};
这个struct的size是12,原因是既有字段间padding又有整体padding。
对应的Python代码是:
s = struct.unpack("cic0l", buf) #cic时,Python会自己计算字节对齐。
情形2:
#pragma pack(1)
struct Test2
{
char a1;
int a2;
char a3;
};
#pragma pack()
在这里,使用pack编译指令强制改变为单字节对齐,其他不变,sizeof(Test2)=6。
对应的Python代码:
s = struct.unpack("=cic", buf) #在单字节对齐的情况下,使用=前缀。注意这时“0X”后缀已经没有意义了。
大家或许看到,我们在开始打印出来的ip只是一个很大的整数,如果我想以字符串的形式’192.168.1.235’来查看,又该怎么办? 下面就是解决方案:
http://www.cnblogs.com/thh/archive/2007/07/05/806866.html
def Ip2Int(ip):
import struct,socket
return struct.unpack("!I",socket.inet_aton(ip))[0]
此函数从’192.168.1.235’ 可以转换为数字’ 3120670912’,此数字为网络字节序
def Int2Ip(i):
import socket,struct
return socket.inet_ntoa(struct.pack("!I",i))
此函数从网络字节序的数字’’转换为ip
另附一个详细的转换:
http://blog.chinaunix.net/u1/57278/showart_481765.html
>>>
socket.inet_ntoa(struct.pack('I',socket.htonl(16909060)))
'1.2.3.4'
>>> socket.ntohl(struct.unpack("I",socket.inet_aton('1.2.3.4'))[0])
16909060
再加几个:
>>> struct.unpack("I",socket.inet_aton('1.2.3.4'))
(67305985L,)
>>> socket.ntohl(67305985)
16909060
>>> socket.htonl(16909060)
67305985
>>> struct.unpack('i',socket.inet_aton('1.2.3.4'))
(67305985,)
>>> struct.pack('i',16909060)
'\x04\x03\x02\x01'
>>> struct.pack('i',67305985)
'\x01\x02\x03\x04'
>>> socket.inet_ntoa(struct.pack('I',socket.ntohl(16909060)))
'1.2.3.4'
>>> socket.htonl(struct.unpack("I",socket.inet_aton('1.2.3.4'))[0])
16909060
这样很好玩吧~
(1). inet_aton 将ip地址的4段地址分别进行2进制转化,输出用16进制表示:
1.2.3.4 ——inet_aton——> 0000 0001,0000 0010,0000 0011,0000 0100
(2).unpack的处理是按16进制(4bit)将2进制字符,从后向前读入的,低位入,处理成:
00000100 00000011 00000010 00000001
也就是 4 3 2 1
pack也一样,从后向前读入字符,所以——
用16进制表示的1 2 3 4(16909060)打包成 4 3 2 1 的顺序;
用16进制表示的4 3 2 1(67305985)打包成 1 2 3 4 的顺序;
(3) ntohl, htonl 表示的是网络地址和主机地址之间的转换(network byte <==> host byte)
由于unpack/pack的解/打包的颠倒顺序,必须通过htonl 或者 ntohl 进行处理。
(4) network byte, host byte
这 2个名词折腾半天,host byte 由于处理器的方式不同
little-endian或者big-endian,将ip地址转换的最终输出是不一样的,参考(
/articles/article.aspx?p=169505&seqNum=4 )
()
big-endian: 最高位在左边(内存存储空间的最低位)
little-endian: 最高位在右边(内存存储空间的最低位)
i386-unknown-freebsd4.8: little-endian
powerpc-apple-darwin6.6: big-endian
sparc64-unknown-freebsd5.1: big-endian
powerpc-ibm-aix5.1.0.0: big-endian
hppa1.1-hp-hpux11.11: big-endian
i586-pc-linux-gnu: little-endian
sparc-sun-solaris2.9: big-endian
而为了统一这种传输标准(打个补丁,再创造一个名词函数),又有network byte,这个其实就是标准的big-endian;
由于x86本身的处理属于little-endian,所以上述应该按照标准的network
byte 进行处理,这样可避免cpu造成的不同:
socket.htonl(struct.unpack("I",socket.inet_aton('1.2.3.4'))[0])
socket.inet_ntoa(struct.pack('I',socket.htonl(16909060)))
###############################################################################
(1个64位amd+32位windows, 1个32位intel+linux,
出来的数值是一样的:
>>>
socket.ntohl(struct.unpack("I",socket.inet_aton('220.194.61.32'))[0])
-591250144
>>> socket.inet_ntoa(struct.pack('I',socket.htonl(-591250144)))
'220.194.61.32'
但是64位amd+64位linux,是这样:
>>> socket.ntohl(struct.unpack("I",socket.inet_aton('220.194.61.32'))[0])
3703717152
>>> socket.inet_ntoa(struct.pack('I',socket.htonl(-591250144)))
'220.194.61.32'
>>>
socket.inet_ntoa(struct.pack('I',socket.htonl(3703717152)))
'220.194.61.32'
unpack/pack和cpu,os无关;
socket.ntohl/htonl的输出结果int型
对32 os,处理成signed 类型,首位1被处理成负数;
对 64位 os,则是unsigned类型;
(晕了半天,那个值才明白是位数,和cpu不相干)
###############################################################################
(5)mysql里面的函数是inet_aton,
inet_ntoa 和python的socket不同,直接实现ip string到network byte的转换,python里面只能实现ip地址到network byte的2进制转换:
mysql> select inet_aton('1.2.3.4');
-> 16909060
mysql> select inet_ntoa(16909060);
-> 1.2.3.4