(三)通过线程编写一个简单的并发服务器
概述
之前在上一节通过使用fork来实现了一个并发程序,它很经典但是效率不高主要是太消耗资源因为fork一个进程的开销很大,假如100客户端连接就需要100个进程,这样不是不可以只是这种方式不太高级,下面我们通过使用线程来实现并发,因为产生一个线程的开销要小的多,当然对于大规模并发的场景使用线程也不是最好的选择,但是学习socket编程的过程中这些东西是需要了解的。
代码段
服务器端
1 #!/usr/bin/env python 2 # -*- coding: utf-8 -*- 3 # Author: rex.cheny 4 # E-mail: rex.cheny@outlook.com 5 6 import socket 7 import time 8 import threading 9 10 11 def echoStr(connFd): 12 print("新连接:", connFd.getpeername()) 13 while True: 14 bytesData = connFd.recv(1024) 15 data = bytesData.decode(encoding="utf-8") 16 print("收到客户端消息:", data) 17 if data == "Bye": 18 """ 19 这里需要关闭连接,在之前的fork模式中这里是直接返回的 20 不过使用线程则需要先关闭再返回。 21 """ 22 connFd.close() 23 return 24 else: 25 time.sleep(1) 26 connFd.send(data.encode(encoding="utf-8")) 27 28 29 def main(): 30 sockFd = socket.socket() 31 sockFd.bind(("", 5555)) 32 sockFd.listen(5) 33 34 print("等待客户端连接......") 35 while True: 36 connFd, remAddr = sockFd.accept() 37 try: 38 """ 39 这里产生一个线程来处理连接,我们在这里启动线程后不能像fork模式那样关闭连接套接字 40 因为线程是共享进程资源的所以你这里如果关闭那么这个TCP连接也就断了。而之前在fork 41 模式中需要关闭是因为进程的资源是隔离的父子进程对同一个文件描述符的两次引用,而在 42 线程里对这个一个文件描述符只引用了一次,所以这里不能关闭。 43 """ 44 t = threading.Thread(target=echoStr, args=(connFd,)) 45 t.start() 46 except Exception as err: 47 print(err) 48 49 50 if __name__ == '__main__': 51 main()
客户端代码
与之前的相同,不过这里还是放进来
1 #!/usr/bin/env python 2 # -*- coding: utf-8 -*- 3 # Author: rex.cheny 4 # E-mail: rex.cheny@outlook.com 5 6 import socket 7 8 9 def echoStr(sockFd, data): 10 sockFd.send(data) 11 bytesData = sockFd.recv(1024) 12 data = bytesData.decode(encoding="utf-8") 13 print(data) 14 15 16 def main(): 17 sockFd = socket.socket() 18 sockFd.connect(("127.0.0.1", 5555)) 19 20 for i in range(1, 11): 21 data = "第:" + str(i) + " 条消息。" 22 echoStr(sockFd, data.encode(encoding="utf-8")) 23 24 echoStr(sockFd, "Bye".encode(encoding="utf-8")) 25 sockFd.close() 26 27 28 if __name__ == '__main__': 29 main()
结果演示
效果一样。不过这里有一个问题其实包括之前的fork版本的程序也有问题就是如果处理客户端请求的进程崩溃,那么服务器端TCP协议栈会发送FIN,这时候客户端肯定可以收到并回复ACK,但是如果客户端这时候刚好要发送下一条消息,这时候客户端就是在一个已收到FIN的套接字里写数据,那么它会收到一个RST,它写完了马上会调用读(从代码里可以看出来)那么读一个收到RST的套接字将会报错。而如果它收到FIN的时候刚好是读则会得到一个EOF。这也就是上一节里末尾的图所说明的内容。其实如果客户端能够及时对收到的FIN做出反应那么将避免对收到FIN的套接字进行操作,其实在我们的例子中效果不明显因为我的客户端程序是自动发送消息的,如果你改成输入消息则会非常明显。因为会阻塞在用户输入的地方,因为这种阻塞模式的套接字程序,收到FIN后它无法处理,换句话说对于客户端来说它在处理两个文件描述符一个是套接字的一个是标准输入的,要想解决这个问题我们就需要用到多路复用。