第十五章 联网
15.1联网背景
套接字是一个网络连接的端点。域名是为了方便记忆。
略,还有很多很基本的概念。
15.2处理地址和主机名
socket模块提供了几个函数处理主机名和地址。socket模块包装了C套接字库,并且与C版本一样,也支持所有种类的选项。模块内定义了直接映射到C等价的变量。
gethostname()将返回当前计算机的主机名。gethostbyname(name)将试图把指定主机名分解成IP地址。失败将产生异常。我测试的机器没有连接,抛出错误为:
socket.gaierror: (11001, 'getaddrinfo failed')
扩展格式gethostbyname_ex(name)将返回一个三元组,包含特定地址的主要主机名,相同IP地址的可选择机器名列表,以及一个主机界面上的其他IP地址列表:
>>> socket.gethostbyname('www.yahoo.com')
'64.58.76.178'
>>> socket.gethostbyname_ex('www.yahoo.com')
('www.yahoo.akadns.net',['www.yahoo.com'],
['64.58.76.178','64.58.76.176','216.32.74.52',
'216.32.74.50','64.58.76.179','216.32.74.53',
'64.58.76.177','216.32.74.51','216.32.74.55'])
gethostbyaddr(address)完成同样的任务,只不过参数是IP
getservbyname(service,protocol)获取一个服务名,如telnet或ftp,和一个协议,如tcp或udp。并返回服务端口号。
>>> socket.getservbyname('http','tcp')
80
通常,非Python程序以32位的打包格式存储和使用IP地址:
inet_aton(ip_addr)
inet_ntoa(packed)
这两个函数在32位打包格式和IP地址字符串之间进行转换。如:
>>> socket.inet_aton('177.20.1.201')
'/261/024/001/311' # 一个4字节的字符串
>>> socket.inet_ntoa('/x7F/x00/x00/x01')
'127.0.0.1'
socket定义了几个保留IP地址:
INADDR_ANY和INADDR_BROADCAST是保留的IP地址,用于任意IP和广播。INADDR_LOOPBACK引用127.0.0.1这个地址的环回设备。几个保留IP地址都是保留的32位格式。
getfqdn([name])函数返回主机名的合格域名,省略name则返回本机的。是Python2.0中的新特性:
>>> socket.getfqdn('')
'dialup84.lasal.net'
15.3与低层套接字通信
15.3.1创建和撤消套接字
socket模块的socket(family,type[,protocol])创建一个新的套接字对象。family常取AF_INET,偶尔也用其他值如AF_IPX,取决于平台。type最常用的是SOCK_STREAM(就是TCP)或SOCK_DGRAM(就是UDP)。
family和type的组合就可以指定一个协议了,但是仍然可以指定protocol参数,如IPPROTO_TCP或IPPROTO_RAW。可用函数getprotobyname(proto)获取协议的代码:
>>> getprotobyname('tcp')
6
>>> IPPROTO_TCP
6
fromfd(fd,family,type[,proto])用于从打开的文件描述符(文件的fileno()方法获得)创建套接字对象。套接字对象的方法fileno()将返回这个套接字的文件描述符(一个整数)。
用完套接字之后应该用close()方法关闭。显式的关闭较好,尽管可以自动关闭,使用弱引用?。也可用shutdown(how)方法关闭一端或两端,参数为0时停止接受,1停止发送,2双方停止。
15.3.2连接套接字
一个TCP连接,监听端先要bind(address)一个地址和端口的元组,之后调用listen([backlog])侦听传入连接,之后用accept()接受新的传入连接。
>>> s=socket(AF_INET,SOCK_STREAM)
>>> s.bind(('127.0.0.1',4444))
>>> s.listen(1)
>>> q,v=s.accept() #返回套接字q和地址v
上面的代码在未接到连接时会挂起。只要另外启动一个解释器连接即可,连接函数connect(address)。
发送数据q.send('hello'),返回已发送的字节数
接受数据s.recv(1024),最多一次接受1024字节的数据,返回数据
"地址"是由IP地址和端口构成的二元组。
扩展函数connect_ex(address)如果遇到调用的错误则返回错误代码,成功返回0,而不会产生异常,更接近于C socket。
调用listen时需要的参数是暂时无法响应时保存连接的队列长度,一般设为5。socket.SOMAXCONN变量指明了最大允许的数量。
accept()方法返回一个地址,包含IP和端口。
UDP套接字不是面向连接的,但仍可用connect()。
15.3.3发送和接收数据
send(string[,flags])发送指定数据到远程套接字。sendto(string [,flags],address)把指定字符串发送到特殊地址。通常send用于面向连接的,sendto用于无连接的。但是如果UDP套接字调用了connect(),就可以用send而不必是sendto了。
都返回实际发送的字节数。如下函数用于确保发送整个消息:
def safeSend(sock,msg):
sent=0
while msg:
i=sock.send(msg)
if i==-1: # Error
return -1
sent+=i
msg=msg[i:]
time.sleep(25) #等待队列为空
return sent
这样当一次发送不完时也可以持续的发送数据,并最终返回发送成功的字节数。
recv(bufsize[,flags])方法接收数据,如果有很多数据则只返回前bufsize字节的数据。recvfrom(bufsize[,flags])完成同样的任务,只是带有返回信息(data,(ipAddress,port))的AF_INET套接字,可以看到信息的发源地。
send、sendto、recv、recvfrom都接受一个flags参数,默认值为0。可以把一些socket.MSG_*变量按位OR操作之后创建flags值。可用值的平台相关的。
MSG_OOB 处理超区段数据
MSG_DONTROUTE 不使用路由表,直接发送到界面
MSG_PEEK 在不从队列删除的情况下,返回等待数据
例如使用MSG_PEEK就可以偷偷的查看消息而不必从数据队列中删除。
makefile([mode[,bufsize]])方法返回一个类似文件的对象,该对象包装了这个套接字。这样可以使用文件的操作方法操作套接字。可选的mode和bufsize参数获取的值与内置的open函数相同。
经测试在Windows2000SP4+Python2.4.2下的UDP编程中,最大允许发送的UDP数据报为65527字节,而这个数据报是收不到的。最大能够接收的数据报大小为65507字节。在65507到65527字节之间的数据报发送会丢失,发送端不会出错,接收端却不会收到。对于超过65527字节的数据报会发生socket.error错误,winsock错误号为10040,信息"Message too long" 。这个问题同样存在于Linux中。经测试linux-2.6.9内核中这个数据长度区间是62501到65507字节。
在Windows2000SP4+Python2.4.2下尝试在recv中使用MSG_OOB标志,但是返回10045号winsock错误,提示"Operation not supported"。另外,如果还没有bind()就使用recv()接收数据会发生10022号winsock错误,提示"Invalid argument"。
15.3.4使用套接字选项
套接字getpeername()和getsockname()方法将返回一个2元组,包含IP地址和端口。getpeername()返回远程套接字信息,getsockname()返回本地套接字信息。
默认情况下的套接字是阻塞的,例如发送缓冲区已满的情况下,send函数也会阻塞,直到可以把更多的数据写入到缓冲区。通过采用0值调用setblocking(flag)方法,就可以改变这种行为。
在非阻塞时,如果发现recv()方法没有返回数据则会发生错误socket.error。Winsock错误号为10035。在套接字运行中也可以随时改变阻塞状态。在非阻塞状态中,一般放入一个带有延时的循环中。在延时发生的时候,其他线程可以改变循环的进入条件,控制程序合法退出。
在非阻塞时,也可以在监听套接字上用select或poll来检测是否有新的连接到达。
setsockopt(level,name,value)和getsockopt(level,name[,buflen ])方法用于设置和返回套接字选项。level参数指定引用选项的层,如套接字层、TCP层、IP层。level的值以SOL_开头,如SOL_SOCKET、SOL_TCP等。选项的名字指定了正在交换的选项,socket模块定义了读者的平台上可以使用的选项。
C版本的setsockopt要求用缓冲区传递value参数,但在Python中,这个选项输入数字,可直接用数字或用字符串(代表缓冲区)。
getsockopt函数不指定buflen时会返回数字值,而提供了buflen时返回一个字符串(代表缓冲区),并且最大长度为buflen个字节。
各个平台的选项不同,各个选项的值类型也不尽相同。如设置发送缓冲区为65KB:
>>> s.setsockopt(SOL_SOCKET,SO_SNDBUF,65535)
SOL_SOCKET的选项(选项名、值、说明):
SO_TYPE (只读) 套接字类型(如SOCK_STREAM)
SO_ERROR (只读) 套接字的最后一个错误
SO_LINGER Boolean 如果数据存在则拖延close
SO_RCVBUF Number 接收缓冲区大小
SO_SNDBUF Number 发送缓冲区大小
SO_RCVTIMEO Time struct 接收超时
SO_SNDTIMEO Time struct 发送超时
SO_REUSEADDR Boolean 启用本地地址/端口的多个用户
SOL_TCP选项:
TCP_NODELAY Boolean 立即发送数据,不等待最小发送量
SOL_IP选项:
IP_TTL 0-255 包能够传送的最大转发器数
IP_MULTICAST_TTL 0-255 包能够广播的最大转发器数
IP_MULTICAST_IF inct_aton(ip) 选择界面,以便通过它传输
IP_MULTICAST_LOOP Boolean 启用发送器,以接收它发送的多
点传送包的副本
IP_ADD_MENBERSHIP ip_mreq 联接一个多点传送组
IP_DROP_MENBERSHIP ip_mreq 去掉多点传送组
15.3.5转换数字
平台字节序和网络字节序多半不同,所以通过网络传输之前需要先转换。nthol(x)和ntohs(x)函数从网络数字转换到本地字节序。htonl(x)和htons(x)函数从本地字节序转换到网络字节序。
15.4示例:多点传送的聊天应用程序
一个聊天程序,允许在共享的白板上绘画。
使用IP多播实现,代码太长见page224-228。
在互联网上运行会有问题,因为有些路由器不允许IP多播的转发。而且设置TTL最好足够大。SO_REUSEADDR选项允许一个地址(IP,port)允许被绑定到多个socket。IP_ADD_MEMBERSHIP连接一个IP多播组,退出多播组用IP_DROP_MENBERSHIP。
15.5使用SocketServers
15.5.1SocketServer族
TCPServer和UDPServer都是SocketServer的子类。
SocketServer模块同时提供UnixStreamServer(TCPServer的子类)和UnixDatagramServer(UDPServer的子类)。他们的父类相同,只是监听套接字是AF_UNIX族,而不是AF_INET。
默认的套接字服务器一次处理一个连接,但可以使用ThreadingMix和ForkingMixIn类创建SocketServer的线程版本或分支版本。还有一些已经实现好的类:ForkingUDPServer、ForkingTCPServer、 ThreadingUDPServer、ThreadingTCPServer、ThreadingUnixStreamServer、ThreadingUnixDatagramServer。线程版本只能在支持线程的操作系统上运行。分支版本也只可以在支持os.fork的平台上运行。
关于混合类见第7章;分支的解释见11章;线程见26章。
SocketServer以常规方式处理传入的连接,可以自己定义请求处理程序类,只要继承BaseRequestHandler类即可。然后将此类传递给构造函数即可。
import SocketServer
...#创建请求处理程序类
addr=('IP',port)
server=SocketServer.ThreadingTCPServer(addr,请求处理程序类)
server.serve_forever()
这样每次有新的连接时就创建一个"请求处理类"的实例对象,并调用他的handle()方法。代替用server_forever,也可用handle_request() ,可用于等待、接收单个连接。server_forever()仅限在无限循环中调用handle_request()。
如果需要创建自己的套接字服务器类,需要实现如下方法:
在__init__()函数中调用server_bind()方法,把self.socket绑定到正确的地址self.server_address。然后调用server_activate()激活服务器,默认情况下还会调用套接字的listen()方法。
在调用handle_request()或serve_forever()之前,套接字服务器不会进行任何操作。handle_request()调用get_request(),并等待接受一个新的套接字连接,然后调用verify_request(request,client_address) ,以便查看服务器是否应该处理该连接(可以把这种方法用于访问控制-默认情况下,verify_request()总是返回真)。如果处理请求成功,则handle_request()会调用process_request(request,client_address),如果process_request()产生了异常,则调用handle_error(request, client_address)。默认时,process_request()简单的调用finish_request(request,client_address)。
分支和线程混合类将会启动一个分支或线程来调用finish_request。finish_request()是一个新的请求处理程序,接下来会调用他的handle()方法。
当SocketServer创建一个新的请求处理程序时,会把处理程序的__init__函数传递给self变量。
SocketServer的