网络服务器(python)
服务器的特点是等待来自客户端的请求,发送应答。通常来说,服务器可以做任何事情。某些方面,服务器程序和客户端程序很类似。很多用在客户端程序的指令同样可以用在服务器程序中,因为服务器和客户端使用同样的socket接口。但是还是有一些重要的细节是不同的,最明显的是建立socket.
准备连接
对客户端来说,建立一个TCP连接的过程分两步,包括建立socket对象以及调用connect()来建立一个和服务器的连接。
对于服务器,这个过程需要如下4步:
- 建立socket对象
- 设置socket选项(可选)
- 绑定到一个端口(可以指定网卡)
- 监听连接
下面代码可以实现这些功能:
host=' '
port=51423
#step 1
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
#step 2
s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
#step 3
s.bind((host,port))
#step 4
s.listen(5)
来详细分析下上面的代码
建立socket对象
为了建立一个socket对象,使用和客户端中相同的命令。
设置和得到socket选项
对于一个socket,可以设置很多不同的选项,对于那些一般用途的服务器,最让人感兴趣的socket选项是SO_REUSEADDR。通常一个服务器进程终止后,操作系统会保留几分钟它的端口,从而防止其他进程在超时之前使用这个端口。如果设置SO_REUSEADDR为true,操作系统就会在服务器socket被关闭或服务器终止后马上释放该服务器的端口。这样可以使调试程序更简单。
python定义setsockopt()和getsockopt()如下:
setsockopt(level,optname,value)
getsockopt(level,optname,buflen)
value参数的内容是由level和optname决定的。level定义了哪个选项将被使用。通常情况下是SOL_SOCKET。意思是正在使用socket选项。还可以通过设置一个特殊协议号码来设置协议选项。然而,对于一个给定的操作系统,大多数协议选项都是明确的,所以为了简洁,很少用于为便携移动设备设计的应用程序。
如果level设置为SOL_SOCKET,optname参数对应的有可以使用的特殊选项。
SOL_SOCKET常用的选项:
下面的python程序可以给出机器上安装的python所支持的socket选项列表:
#!/usr/bin/env python
# Get list of avaliable socket options - socketopts.py
import socket
solist = [x for x in dir(socket) if x.startswith('SO_')]
solist.sort()
for x in solist:
print x
绑定socket
为服务器要求一个端口号这个过程就是绑定。每个服务器程序都有自己的端口,而且这个端口是众所周知的。
为了绑定一个端口,需要执行下面的指令:
s.bind(('',80))
这条指令请求80端口,是标准的HTTP端口。通常操作系统约定使用的端口号小于1024,这样只有root用户可以绑定。
bind()函数的第一个参数是要绑定的IP地址。通常为空,意思是可以绑定到所有的接口和地址。
有的机器会有多个网络接口,例如,一个防火墙或许会有一个以太网卡连接公共的Internet,外加另一个以太网卡连接内部网络。这种情况下,或许需要服务只对一个接口使用,所以需要提供内部网络的IP地址来绑定。这种情况下,对于通过外部接口连接的客户端来说,看上去根本没有80端口。事实上,可以运行另一台单独的服务器来绑定一台外部服务器的80端口。
事实上,可以通过调用bind()函数来吧客户端socket把绑定到一个特定的IP地址和端口号。然而,客户端很少使用,因为操作系统会自动提供合适的值。
监听连接
接受客户端连接之前的最后一步就是调用listen()函数,这个调用通知操作系统准备接受连接。函数只有一个参数,这个参数指明了服务器实际处理连接的时候,允许有多少个等待的连接在队列中等待。对于现代多线程或者多任务服务器来说,这个参数意义不大,但也是必须的。
listen()调用如下:
s.listen(5)
接受连接
大多数服务器都设计成运行不确定时间和同时服务于多个连接。与此相反,客户端一般只有几个连接,并且会运行到任务完成或者用户终止。
通常是服务器连续运行的办法是小心地设计一个无限循环。下面是一个基本服务器的例子:
#!/usr/bin/env python
# Base Server - basicserver.py
import socket
host = ''
port = 51423
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
s.bind((host,port))
print "Waiting for connections..."
s.listen(1)
while 1:
clientscok,clientaddr = s.accept()
print "Got connection from",clientsock.getpeername()
clientsock.close()
错误处理
对于客户端任何没有捕获到的异常都会终止程序是可以接受的,很多时候客户端程序发生错误后退出是可以理解的。但是对于服务器,这种情况很糟糕。
在以python为基础的网络程序中,一个错误 处理就是一个简单的标准的python异常处理,可以处理网络相关的问题。
下面的例子试图捕获所有可能的网络错误,同时保证不会终止服务器的方法来处理这些错误。在调用accept()出现的错误被打印出来,但是会被忽略,continue可以返回到循环开始的位置,开始下一次accept()调用:
!/usr/bin/env python
Server With Error Handling - errorserver.py
host = ''
port = 51423
s = socket.socket(socket.AF_INET,socket.STREAM)
s.sersockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
s.bind((host,port))
s.listen(1)
while 1:
try:
clientsock,clientaddr = s.accept()
except KeyboardInterrupt:
traceback.print_exc()
continue
# connect
try:
print "Got connection from ",clientsock.getpeername()
execpt (KeyboardInterrupt,SystemExit):
raise
except:
traceback.print_exc()
# close connect
try:
clientsock.close()
execpt KeyboardInterrupt:
raise
except:
traceback.print_exc()
上面的程序有三个独立的try块。第一个包含对accept()的调用,会产生异常。程序会重新产生KeyboardInterrupt,所以运行服务器的人按Ctrl-C同样会终止程序。所有其他的异常被打印出来,但是程序却不会终止。相反,continue会跳跃式地回到循环开始的部分,否则代码会处理不存在的客户端连接。第二个包含真正处理连接的代码,包含两个异常,一个是和前面一样的KeyboardInterrupt,还有SystemExit。SystemExit是在堆sys.exit()调用时产生的,如果没有成功的传送它就会使程序不能再需要终止的时候终止。第三个包含对close()的调用。用这种方法可以保证在需要的时候,close()总是能够被调用。如果使用文件类对象,也应该在这里关闭它。
在大型程序中,使用python的try..finally语句来确保socket被关闭是很有意义的,在对accept()调用成功后,马上就可以插入try,最后在调用close()之前,使用一个finally来关闭socket。这种情况下,任何捕获不到的异常如果在try和finally之间发生,都会使socket关闭,同时异常被显示出来。
使用UDP
从客户端来看,UDP比TCP要困难,因为客户端必须要注意丢失信息的问题,另一方面,对于服务器使用UDP就容易的多。
下面是一个简单的UDP应答服务器,可以用来测试UDP客户端。
#!/usr/bin/env python
# UDP Echo Server - udpechoserver.py
import socket,traceback
host = ''
port = 51423
s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
s.bind((host,port))
while 1:
try:
message,address = s.recvfrom(8192)
print "Got data from ",address
s.sendto(message,address)
except (KeyboardInterrupt,SystemExit):
raise
except:
traceback.print_exc()
通过syslog来记录日志
对于服务器来说,通信状态的一个重要内容就是记录日志文件,unix和类unix系统提供了一个syslog来帮助记录日志。
总结
服务器主要是等待来自客户端的请求并发送响应。和客户端一样使用socket接口,但是建立过程是4步。
socket选项是针对一个特殊连接而改变网络系统的行为,最常用的是SO_REUSEADDR,它允许端口在socket关闭后,马上被重新使用。
TCP服务器一般会使用accept()来为每个连接的客户端建立一个新的socket。UDP服务器一般只是使用一个单一的socket,完全依靠从recvfrom()返回值来判断该往哪里发送响应。
当服务器和客户端都停下来等待的时候,有可能出现死锁。小心设计协议,并适当的使用超时,可以把死锁出现的频率和影响减到最小。
网友评论