总结python的socket编程技巧和坑

Last Updated: 2023-04-18 09:44:08 Tuesday

-- TOC --

本文汇总一些用Python编写socket程序的一些知识点。

弃用gethostbyname

这个接口就不要用了,已经被getaddrinfo取代!

反正用这个函数来获取本机ip地址是基本不行的。

后来发现,此函数居然还是一个DNS接口:

>>> import socket
>>> socket.gethostbyname('www.python.org')
'151.101.108.223'

但是不建议使用gethostbyname来做DNS查询,存在两个问题:

  1. 此接口不能很容易地设置超时时间,可能会导致长时间阻塞;
  2. 此接口不支持并发!

Unix/Linux下的gethostbyname函数常用来向DNS查询一个域名的IP地址。 由于DNS的递归查询,常常会发生gethostbyname函数在查询一个域名时严重超时。而该函数又不能像connect和read等函数那样通过setsockopt或者select函数那样设置超时时间,因此常常成为程序的瓶颈。有人提出一种解决办法是用alarm设置定时信号,如果超时就用setjmp和longjmp跳过gethostbyname函数(这种方式我没有试过,不知道具体效果如何)。

在多线程下面,gethostbyname会一个更严重的问题,就是如果有一个线程的gethostbyname发生阻塞,其它线程都会在gethostbyname处发生阻塞。我在编写爬虫时也遇到了这个让我疑惑很久的问题,所有的爬虫线程都阻塞在gethostbyname处,导致爬虫速度非常慢。在网上google了很长时间这个问题,也没有找到解答。今天凑巧在实验室的googlegroup里面发现了一本电子书"Mining the Web - Discovering Knowledge from Hypertext Data",其中在讲解爬虫时有下面几段文字:

Many clients for DNS resolution are coded poorly. Most UNIX systems provide an implementation of gethostbyname (the DNS client API—application program interface), which cannot concurrently handle multiple outstanding requests. Therefore, the crawler cannot issue many resolution requests together and poll at a later time for completion of individual requests, which is critical for acceptable performance.

Python的这个接口,底层应该就是C语言的同名接口。

Linux下据说有一个接口gethostbyname_r支持多线程,但是还是不确定是否由超时设置。严肃的项目,请自己写DNS查询接口!

另外还有一个兄弟接口:

>>> socket.gethostbyname_ex('python.org')
('python.org', [], ['138.197.63.241'])
>>> socket.gethostbyname_ex('www.python.org')
('dualstack.python.map.fastly.net', ['www.python.org'], ['151.101.108.223'])

getaddrinfo

查询域名对应的IP地址,请使用这个接口。注意此接口可能返回多个IP地址,在一个iterable对象中。man getaddrinfo明确说了这个接口包含了gethostbyname的功能,并且支持reentry。

inet_aton和inet_ntoa

a是ascii,n是network,这两个接口用来转换ip地址非常方便:

>>> from binascii import hexlify
>>> socket.inet_aton('1.2.3.4')
b'\x01\x02\x03\x04'
>>> hexlify(socket.inet_aton('1.2.3.4'))
b'01020304'
>>> socket.inet_aton('1.2.3.4').hex()
'01020304'
>>> socket.inet_ntoa(socket.inet_aton('1.2.3.4'))
'1.2.3.4'

网络序是大字节序!

Byte Order转换

>>> a = 12345
>>> socket.htonl(a)
959447040
>>> socket.htons(a)
14640
>>> socket.ntohl(socket.htonl(a))
12345
>>> socket.ntohs(socket.htons(a))
12345

这个的l表示long,但是只能是32bit的数据,s表示short,16bit数据。

注意区别:

下面的Python代码,是将一个多字节的数按两种顺序表达出来,得到的bytes:

>>> cc = 12345
>>> hex(12345)
'0x3039'
>>> (12345).to_bytes(2, 'big')
b'09'
>>> (12345).to_bytes(2, 'little')
b'90'
>>> (12345).to_bytes(2, 'big').hex()
'3039'
>>> (12345).to_bytes(2, 'little').hex()
'3930'

socket.hton*socket.ntoh*,是直接修改交换内存中的多个字节,形成新的数!

getservbyport

这个接口可以用来通过port number获得service name:

>>> for p in range(100):
...   try:
...     print(p, socket.getservbyport(p))  # default is tcp
...   except:
...     pass
... 
1 tcpmux
2 nbp
4 echo
6 zip
7 echo
9 discard
11 systat
13 daytime
15 netstat
17 qotd
19 chargen
20 ftp-data
21 ftp
22 ssh
23 telnet
25 smtp
37 time
43 whois
49 tacacs
53 domain
67 bootps
68 bootpc
69 tftp
70 gopher
79 finger
80 http
88 kerberos
>>> for p in range(100):
...   try:
...     print(p, socket.getservbyport(p,'udp'))
...   except:
...     pass
... 
7 echo
9 discard
13 daytime
19 chargen
21 fsp
37 time
49 tacacs
53 domain
67 bootps
68 bootpc
69 tftp
88 kerberos

收发buffer

>>> s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
>>> s.getsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF)
16384
>>> s.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF)
131072
>>> us = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
>>> us.getsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF)
212992
>>> us.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF)
212992

udp的默认buffer,居然这么大!

>>> s.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 12345)
>>> s.getsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF)
24690
>>> s.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 4096)
>>> s.getsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF)
8192

有点诡异!

本文链接:https://cs.pynote.net/sf/python/202203041/

-- EOF --

-- MORE --