在进入正文之前强烈推荐IPython 来学习网络编程,和C语言繁琐的语法和编译相比,IPython交互的输入输出效率奇高,打开两个IPython页面就可以方便的编写服务端和客户端程序了。推荐anaconda环境,集成了很多必备的库,以及IPython notebook 。
废话说完了,下面进入正题。本文主要以简单的回显程序为例,实践测试了不同异常情况下服务端和客户端的反应,由此逐步提高程序的健壮性。异常情况主要参考网络编程著作《UNIX网络编程》第三版。
我的服务器代码和客户端代码是这样的:
#Echo Server
import socket
import sys
import signal
import os
def server_echo(connefd):
while 1:
try :
print ("before recv")
mesg = connefd.recv(8)
print (mesg)
if not mesg:
return
connefd.send(mesg)
except os.error:
print ("socket error")
return
def signal_handlers(signo,frame): #信号处理函数
print ("child %d terminnated",os.wait())
serverPort = 50000
listenfd = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
listenfd.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
listenfd.bind(("",serverPort))
listenfd.listen(5)
signal.signal(signal.SIGCHLD,signal_handlers)
while 1:
connefd,address = listenfd.accept()
childpid = os.fork()
if(childpid == 0):
listenfd.close()
server_echo(connefd)
exit(0)
connefd.close()
#客户端
import socket
connefd = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
connefd.connect(('127.0.0.1',5000))
while 1:
sentence = input("Enter your sentence:")
if sentence == "":
connefd.close()
break
connefd.send(sentence.encode())
mesg = connefd.recv(8)
print mesg
代码很简单,比较值得一提的是服务器程序中利用系统调用fork函数进行实现并发服务器。除此之外,必须提供一个SIGCHLD信号的处理程序。当子进程结束时,内核会自动发送SIGCHLD信号给父进程,默认处理方式是忽略。那么我们为什么要处理SIGCHLD信号呢?原因是如果不主动处理SIGCHLD会产生大量僵尸进程。
一个进程在调用exit命令结束自己的生命的时候,其实它并没有真正的被销毁,而是留下一个称为僵尸进程(Zombie)的数据结构(系统调用exit,它的作用是使进程退出,但也仅仅限于将一个正常的进程变成一个僵尸进程,并不能将其完全销毁)
因此,仅仅调用exit()不能完全的释放进程资源,我们必须**捕捉(capture)**SIGCHLD信号,在父进程的信号处理程序中释放资源。为此,Unix系统提供了wait,waitpid等函数获取子进程的终止状态,并释放其资源。那么wait和waitpid这些函数有什么作用呢?顾名思义,wait函数是一个等待的函数,父进程调用wait函数来等待一个子进程终止,并清理第一个终止子进程的“尸体”。那么waitpid函数的作用又是什么呢?waitpid函数相当于一个加强版的wait函数,他的函数参数中可以设定需要等待的子进程ID,如果没有子进程已终止则可选择不阻塞等选项。参阅UNPV1第三版可以了解waitpid更详细的信息。
接下来就UNP 5.11节 至 5.16节的小节所述的异常情况进行模拟,并总结一些异常处理方法和编程技巧。
(1) 服务器程序在accept返回前连接终止
accept函数是服务器程序调用的一个函数,作用是服务器接受一个ESTABLISHED状态的TCP连接,并生成一个新的套接字,叫做已连接套接字(connected socket) 。函数接受的套接字是由listening socket 维护的 已连接队列中的队首的连接,如果队列为空,则accept函数阻塞。
注意:TCP连接在客户端调用connect函数返回时就已经完成连接,这意味着服务端在调用 accept函数之前TCP连接已经建立。
为了模拟accept函数在返回前连接终止,我们可以让客户端在connect函数返回后,利用SO_LINGER套接字选项来改变close函数的行为(让其向服务端发送RST报文段)。在RST报文段发送之前,我们的服务端accept函数应该返回。因此可以在服务端调用accept函数前利用input函数或是sleep函数阻塞程序,当RST报文端发送后再调用accept函数。服务端和客户端代码改变如下:
#服务端
while 1:
input("Enter Any key to continue") #添加的阻塞语句
connefd,address = listenfd.accept()
#客户端:
connefd.connect(('127.0.0.1',5000))
connefd.setsockopt(socket.SOL_SOCKET,
socket.SO_LINGER,struct.pack("ii",True,0))#添加的套接字选项,改变了套接字close函数的执行内容,关于SO_LINGER套接字选项可以查阅UNP7.5节
connefd.close()
让我们运行代码测试一下结果,同时可以利用tcpdump或者是Wireshark抓包工具监测RST报文段是否发送成功。
客户端代码执行时的Wireshark抓包截图
最后让我们来看一下服务端收到RST报文段后服务端的响应。
ConnectionResetError Traceback (most recent call last)
<ipython-input-1-4ac06058bc37> in <module>()
32 if(childpid == 0):
33 listenfd.close()
---> 34 server_echo(connefd)
35 os._exit(0)
36 connefd.close()
<ipython-input-1-4ac06058bc37> in server_echo(connefd)
8 # try :
9 print ("before recv")
---> 10 mesg = connefd.recv(8)
11 print (mesg)
12 if not mesg:
ConnectionResetError: [Errno 104] Connection reset by peer
可以看到服务端返回一个ConnectionResetError错误,而且继续调用accept函数,之后其他客户端也能继续连接服务端正常运行程序。我们也能捕捉ConnectionResetError,进而实现自定义的错误处理方法。
后续将继续对不同异常情况进行试验,记录学习TCP/IP协议的过程与经验。
W.Richard Stevens.UNIX网络编程 卷1:套接字联网API[M].北京:人民邮电出版社,2010