Python3学习之路~8.5 SocketServer实现多并发

时间:2022-05-16 06:15:11

前面几节我们写的socket都只能实现服务端与一个客户端通信,并不能实现服务端与多客户端同时通信。接下来我们就来学习一下如何实现服务端同时与多个客户端通信,即并发。

Socket Server

socketserver就是对socket的一个再封装,主要功能就是实现并发。

socketserver模块简化了编写网络服务器的任务。

socketserver一共有以下4种类型

class socketserver.TCPServer(server_address,RequestHandlerClass,bind_and_activate = True)

它使用Internet TCP协议,该协议在客户端和服务器之间提供连续的数据流。

class socketserver.UDPServer(server_address,RequestHandlerClass,bind_and_activate = True)

它使用数据报,这些数据报是可能无序到达或在传输过程中丢失的离散信息包。参数与TCPServer相同。

class socketserver.UnixStreamServer(server_address,RequestHandlerClass,bind_and_activate = True)
class socketserver.UnixDatagramServer(server_address,RequestHandlerClass,bind_and_activate = True)

这些是不经常使用的 类似于TCP和UDP类 的类,但使用Unix域套接字。它们不适用于非Unix平台,参数与TCPServer相同。

如下继承图中有五个类,并且分别显示了他们的继承关系,其中四个代表四种类型的同步服务器:

+------------+ | BaseServer | +------------+ | v +-----------+ +------------------+ | TCPServer |------->| UnixStreamServer | +-----------+ +------------------+ | v +-----------+ +--------------------+ | UDPServer |------->| UnixDatagramServer | +-----------+ +--------------------+

注意:它们都继承了同一个基类:BaseServer。

 

创建一个 socketserver 至少分以下几步:

1.自己创建一个请求处理类,并且这个类要继承BaseRequestHandler类,并且还要重写父亲类里的handle()方法,此方法将用来处理传入的请求,即跟客户端所有的交互都是在handle()里完成的。

2.实例化一个SocketServer类(4种类型选其1,比如TCPServer),并且传递server address和 你上面创建的请求处理类 给这个SocketServer。

3.调用SocketServer对象的handle_request()或者serve_forever()方法来处理一个或多个请求。
server.handle_request() # 只能处理一个请求,因为处理完一个就退出了,一般不用它
server.serve_forever() # 可处理多个请求,因为永远执行

4.调用server_close()来关闭套接字。

 

实例1:基本的socketserver代码

Python3学习之路~8.5 SocketServer实现多并发Python3学习之路~8.5 SocketServer实现多并发
import socketserver

class MyTCPHandler(socketserver.BaseRequestHandler):
    """
    The request handler class for our server.

    It is instantiated once per connection to the server, and must
    override the handle() method to implement communication to the
    client.
    """

    def handle(self):
        # self.request is the TCP socket connected to the client
        self.data = self.request.recv(1024).strip()
        print("{} wrote:".format(self.client_address[0]))
        print(self.data)
        # just send back the same data, but upper-cased
        self.request.send(self.data.upper())

if __name__ == "__main__":
    HOST, PORT = "localhost", 9999

    # Create the server, binding to localhost on port 9999
    server = socketserver.TCPServer((HOST, PORT), MyTCPHandler)

    # Activate the server; this will keep running until you
    # interrupt the program with Ctrl-C
    server.serve_forever()
socketserver1.py

上面是一个最简单的socketserver代码,只能处理一个请求,比如运行如下客户端代码,通过结果你就会发现,处理第二个请求就会失败。

Python3学习之路~8.5 SocketServer实现多并发Python3学习之路~8.5 SocketServer实现多并发
#Author:Zheng Na

# 客户端

import socket


client = socket.socket()
client.connect(('localhost',9999))

while True:
    msg = input(">>: ").strip()
    client.send(msg.encode("UTF-8"))

    data = client.recv(1024) # 接收1024字节
    print("recv from server: ",data)

client.close()
socketclient.py 客户端代码
Python3学习之路~8.5 SocketServer实现多并发Python3学习之路~8.5 SocketServer实现多并发
D:\software\Python3.6.5\python.exe D:/python-study/s14/Day08/socketclient.py
>>: aa
recv from server:  b'AA'
>>: bb
Traceback (most recent call last):
  File "D:/python-study/s14/Day08/socketclient.py", line 15, in <module>
    data = client.recv(1024) # 接收1024字节
ConnectionAbortedError: [WinError 10053] 您的主机中的软件中止了一个已建立的连接。

Process finished with exit code 1
运行结果

 

实例2:处理多个请求

如果想要它处理多个请求,那么就要自己在handle()方法中加循环,跟前面我们学的socket实例一样。

Python3学习之路~8.5 SocketServer实现多并发Python3学习之路~8.5 SocketServer实现多并发
#Author:Zheng Na

import socketserver

class MyTCPHandler(socketserver.BaseRequestHandler):

    def handle(self):
        while True:
            self.data = self.request.recv(1024).strip()
            print("{} wrote:".format(self.client_address[0]))
            print(self.data)
            # 下面3行代码防止服务器端随着客户端的断开而断开
            # 经过测试发现,在linux有效,Windows无效。
            if not self.data:
                print(self.client_address,"断开了")
                break
            self.request.send(self.data.upper())

if __name__ == "__main__":
    HOST, PORT = "localhost", 9999

    # Create the server, binding to localhost on port 9999
    server = socketserver.TCPServer((HOST, PORT), MyTCPHandler)

    # Activate the server; this will keep running until you
    # interrupt the program with Ctrl-C
    server.serve_forever()
socketserver2.py 可处理多个请求

上段代码你在linux运行是没问题的,但是在Windows上面运行的话,一旦你关闭了客户端,服务端就会报错ConnectionResetError

Python3学习之路~8.5 SocketServer实现多并发Python3学习之路~8.5 SocketServer实现多并发
D:\software\Python3.6.5\python.exe D:/python-study/s14/Day08/socketclient.py
>>: aa
recv from server:  b'AA'
>>: bb
recv from server:  b'BB'
>>: 
Process finished with exit code -1

D:\software\Python3.6.5\python.exe D:/python-study/s14/Day08/socketserver2.py
127.0.0.1 wrote:
b'aa'
127.0.0.1 wrote:
b'bb'
----------------------------------------
Exception happened during processing of request from ('127.0.0.1', 53532)
Traceback (most recent call last):
  File "D:\software\Python3.6.5\lib\socketserver.py", line 317, in _handle_request_noblock
    self.process_request(request, client_address)
  File "D:\software\Python3.6.5\lib\socketserver.py", line 348, in process_request
    self.finish_request(request, client_address)
  File "D:\software\Python3.6.5\lib\socketserver.py", line 361, in finish_request
    self.RequestHandlerClass(request, client_address, self)
  File "D:\software\Python3.6.5\lib\socketserver.py", line 696, in __init__
    self.handle()
  File "D:/python-study/s14/Day08/socketserver2.py", line 9, in handle
    self.data = self.request.recv(1024).strip()
ConnectionResetError: [WinError 10054] 远程主机强迫关闭了一个现有的连接。
Windows运行结果

 

实例3:

如果想要在Windows上运行不报错,我们可以尝试主动抓住这个错误。

Python3学习之路~8.5 SocketServer实现多并发Python3学习之路~8.5 SocketServer实现多并发
#Author:Zheng Na

import socketserver

class MyTCPHandler(socketserver.BaseRequestHandler):

    def handle(self):
        while True:
            try:
                self.data = self.request.recv(1024).strip()
                print("{} wrote:".format(self.client_address[0]))
                print(self.data)
                # 下面3行代码防止服务器端随着客户端的断开而断开
                # 经过测试发现,在linux有效,Windows无效。
                if not self.data:
                    print(self.client_address, "断开了")
                    break
                self.request.send(self.data.upper())
            except ConnectionResetError as e:
                print('出现错误',e)
                break

if __name__ == "__main__":
   
    HOST, PORT = "localhost", 9999
   
    server = socketserver.TCPServer((HOST, PORT), MyTCPHandler)
   
    server.serve_forever()
socketserver3.py linux & Windows运行正常
Python3学习之路~8.5 SocketServer实现多并发Python3学习之路~8.5 SocketServer实现多并发
D:\software\Python3.6.5\python.exe D:/python-study/s14/Day08/socketclient.py
>>: aa
recv from server:  b'AA'
>>: bb
recv from server:  b'BB'
>>: 
Process finished with exit code -1

D:\software\Python3.6.5\python.exe D:/python-study/s14/Day08/socketserver3.py
127.0.0.1 wrote:
b'aa'
127.0.0.1 wrote:
b'bb'
出现错误 [WinError 10054] 远程主机强迫关闭了一个现有的连接。
Windows运行结果

此时,你就可以实现关闭一个客户端后,继续重新打开另一个客户端向服务器发送数据。

 

实例4:ThreadingTCPServer 实现并发

但你发现,上面的代码,依然不能同时处理多个连接,哎?那我搞这个干嘛?别急,不是不能处理多并发,如果你想,你还要启用多线程,多线程我们现在还没学,但你大体知道,有了多线程,就能同时让cpu干多件事了。

让你的socketserver并发起来, 必须选择使用以下一个多并发的类

class socketserver.ForkingTCPServer # 多进程

class socketserver.ForkingUDPServer

class socketserver.ThreadingTCPServer # 多线程

class socketserver.ThreadingUDPServer

so 只需要把下面这句

server = socketserver.TCPServer((HOST, PORT), MyTCPHandler)

换成下面这个,就可以多并发了,这样,客户端每连进一个来,服务器端就会分配一个新的线程来处理这个客户端的请求

server = socketserver.ThreadingTCPServer((HOST, PORT), MyTCPHandler)

运行代码

Python3学习之路~8.5 SocketServer实现多并发Python3学习之路~8.5 SocketServer实现多并发
#Author:Zheng Na

import socketserver

class MyTCPHandler(socketserver.BaseRequestHandler):

    def handle(self):
        while True:
            try:
                self.data = self.request.recv(1024).strip()
                print("{} wrote:".format(self.client_address[0]))
                print(self.data)
                # 下面3行代码防止服务器端随着客户端的断开而断开
                # 经过测试发现,在linux有效,Windows无效。
                if not self.data:
                    print(self.client_address, "断开了")
                    break
                self.request.send(self.data.upper())
            except ConnectionResetError as e:
                print('出现错误',e)
                break

if __name__ == "__main__":

    HOST, PORT = "localhost", 9999

    server = socketserver.ThreadingTCPServer((HOST, PORT), MyTCPHandler)

    server.serve_forever()
socketserver4_thread_concurrence.py

输出结果

Python3学习之路~8.5 SocketServer实现多并发Python3学习之路~8.5 SocketServer实现多并发
D:\software\Python3.6.5\python.exe D:/python-study/s14/Day08/socketserver4_thread_concurrence.py
127.0.0.1 wrote:
b'from client1'
127.0.0.1 wrote:
b'from client2'
127.0.0.1 wrote:
b'from client3'

D:\software\Python3.6.5\python.exe D:/python-study/s14/Day08/socketclient.py
>>: from client1
recv from server:  b'FROM CLIENT1'
>>: 

D:\software\Python3.6.5\python.exe D:/python-study/s14/Day08/socketclient.py
>>: from client2
recv from server:  b'FROM CLIENT2'
>>: 

D:\software\Python3.6.5\python.exe D:/python-study/s14/Day08/socketclient.py
>>: from client3
recv from server:  b'FROM CLIENT3'
>>: 
windows同时运行3个客户端
Python3学习之路~8.5 SocketServer实现多并发Python3学习之路~8.5 SocketServer实现多并发
[root@hadoop my-test-files]# python3 socketserver4_thread_concurrence.py 
127.0.0.1 wrote:
b'from client1'
127.0.0.1 wrote:
b'from client2'
127.0.0.1 wrote:
b'from client3'

[root@hadoop my-test-files]# python3 socketclient.py 
>>: from client1
recv from server:  b'FROM CLIENT1'
>>: 

[root@hadoop my-test-files]# python3 socketclient.py 
>>: from client2    
recv from server:  b'FROM CLIENT2'
>>: 

[root@hadoop my-test-files]# python3 socketclient.py 
>>: from client3    
recv from server:  b'FROM CLIENT3'
>>: 
linux同时运行3个客户端

 

实例5:ForkingTCPServer 实现并发

同实例4类似,只是这次将下面这句

server = socketserver.TCPServer((HOST, PORT), MyTCPHandler)

换成了下面这个

server = socketserver.ForkingTCPServer((HOST, PORT), MyTCPHandler)

运行代码

Python3学习之路~8.5 SocketServer实现多并发Python3学习之路~8.5 SocketServer实现多并发
#Author:Zheng Na

import socketserver

class MyTCPHandler(socketserver.BaseRequestHandler):

    def handle(self):
        while True:
            try:
                self.data = self.request.recv(1024).strip()
                print("{} wrote:".format(self.client_address[0]))
                print(self.data)
                # 下面3行代码防止服务器端随着客户端的断开而断开
                # 经过测试发现,在linux有效,Windows无效。
                if not self.data:
                    print(self.client_address, "断开了")
                    break
                self.request.send(self.data.upper())
            except ConnectionResetError as e:
                print('出现错误',e)
                break

if __name__ == "__main__":

    HOST, PORT = "localhost", 9999

    server = socketserver.ForkingTCPServer((HOST, PORT), MyTCPHandler)

    server.serve_forever()
socketserver5_fork_concurrence.py

输出结果

Python3学习之路~8.5 SocketServer实现多并发Python3学习之路~8.5 SocketServer实现多并发
# 我的运行结果(Windows Python3.6.5):一运行就报错
D:\software\Python3.6.5\python.exe D:/python-study/s14/Day08/socketserver5_fork_concurrence.py
Traceback (most recent call last):
  File "D:/python-study/s14/Day08/socketserver5_fork_concurrence.py", line 27, in <module>
    server = socketserver.ForkingTCPServer((HOST, PORT), MyTCPHandler)
AttributeError: module 'socketserver' has no attribute 'ForkingTCPServer'

Process finished with exit code 1

老师的运行结果:启动3个客户端后才报错
AttributeError: module 'os' has no attribute 'fork'

总之一句话,ForkingTCPServer在Windows上不好使
Windows 运行报错
Python3学习之路~8.5 SocketServer实现多并发Python3学习之路~8.5 SocketServer实现多并发
[root@hadoop my-test-files]# python3 socketserver5_fork_concurrence.py 
127.0.0.1 wrote:
b'from client1'
127.0.0.1 wrote:
b'from client2'
127.0.0.1 wrote:
b'from client3'

[root@hadoop my-test-files]# python3 socketclient.py 
>>: from client1
recv from server:  b'FROM CLIENT1'
>>: 

[root@hadoop my-test-files]# python3 socketclient.py 
>>: from client2    
recv from server:  b'FROM CLIENT2'
>>: 

[root@hadoop my-test-files]# python3 socketclient.py 
>>: from client3    
recv from server:  b'FROM CLIENT3'
>>: 
linux同时运行3个客户端 成功

注意:ForkingTCPServer在Windows上不好使,在linux上百分比好使(效果与ThreadingTCPServer一模一样)。

 

总结:TCPServer VS ThreadingTCPServer VS ForkingTCPServer

TCPServer是接收到请求后执行handle方法,如果前一个的handle没有结束,那么其他的请求将不会受理,新的客户端也无法加入。

而ThreadingTCPServer和ForkingTCPServer则允许前一连接的handle未结束也可受理新的请求和连接新的客户端,区别在于前者用建立新线程的方法运行handle,后者用新进程的方法运行handle。

 

class socketserver.BaseServer(server_addressRequestHandlerClass) 主要有以下方法

Python3学习之路~8.5 SocketServer实现多并发Python3学习之路~8.5 SocketServer实现多并发
class socketserver.BaseServer(server_address, RequestHandlerClass)
This is the superclass of all Server objects in the module. It defines the interface, given below, but does not implement most of the methods, which is done in subclasses. The two parameters are stored in the respective server_address and RequestHandlerClass attributes.

fileno() # 返回文件描述符,这个一般是系统内部调用时用到的,我们一般用不到,知道即可
Return an integer file descriptor for the socket on which the server is listening. This function is most commonly passed to selectors, to allow monitoring multiple servers in the same process.

handle_request() # 处理单个请求,我们一般也不用
Process a single request. This function calls the following methods in order: get_request(), verify_request(), and process_request(). If the user-provided handle() method of the handler class raises an exception, the server’s handle_error() method will be called. If no request is received within timeout seconds, handle_timeout() will be called and handle_request() will return.
 
serve_forever(poll_interval=0.5) # 一直处理请求,直到收到一个明确的shutdown()请求。每0.5秒检查一下是否有程序给我发了shutdown的信号。
Handle requests until an explicit shutdown() request. Poll for shutdown every poll_interval seconds. Ignores the timeout attribute. It also calls service_actions(), which may be used by a subclass or mixin to provide actions specific to a given service. For example, the ForkingMixIn class uses service_actions() to clean up zombie child processes.

Changed in version 3.3: Added service_actions call to the serve_forever method.

service_actions() # Python3.3引入,被serve_forever()调用。
This is called in the serve_forever() loop. This method can be overridden by subclasses or mixin classes to perform actions specific to a given service, such as cleanup actions.

New in version 3.3.

shutdown() # 告诉serve_forever()停止处理请求
Tell the serve_forever() loop to stop and wait until it does.

server_close() # 关闭
Clean up the server. May be overridden.

address_family # 地址簇
The family of protocols to which the server’s socket belongs. Common examples are socket.AF_INET and socket.AF_UNIX.

RequestHandlerClass # 请求处理类
The user-provided request handler class; an instance of this class is created for each request.

server_address # 地址
The address on which the server is listening. The format of addresses varies depending on the protocol family; see the documentation for the socket module for details. For Internet protocols, this is a tuple containing a string giving the address, and an integer port number: ('127.0.0.1', 80), for example.

socket # 套接字
The socket object on which the server will listen for incoming requests.

The server classes support the following class variables:

allow_reuse_address # 允许重用地址
Whether the server will allow the reuse of an address. This defaults to False, and can be set in subclasses to change the policy.

request_queue_size # 暂时不用管它
The size of the request queue. If it takes a long time to process a single request, any requests that arrive while the server is busy are placed into a queue, up to request_queue_size requests. Once the queue is full, further requests from clients will get a “Connection denied” error. The default value is usually 5, but this can be overridden by subclasses.

socket_type # 协议类型
The type of socket used by the server; socket.SOCK_STREAM and socket.SOCK_DGRAM are two common values.

timeout # 超时时间,在handle_request()中使用,由于我们不同handle_request(),所以不用管它
Timeout duration, measured in seconds, or None if no timeout is desired. If handle_request() receives no incoming requests within the timeout period, the handle_timeout() method is called.

There are various server methods that can be overridden by subclasses of base server classes like TCPServer; these methods aren’t useful to external users of the server object.

finish_request()
Actually processes the request by instantiating RequestHandlerClass and calling its handle() method.

get_request()
Must accept a request from the socket, and return a 2-tuple containing the new socket object to be used to communicate with the client, and the client’s address.

handle_error(request, client_address)
This function is called if the handle() method of a RequestHandlerClass instance raises an exception. The default action is to print the traceback to standard output and continue handling further requests.

handle_timeout()
This function is called when the timeout attribute has been set to a value other than None and the timeout period has passed with no requests being received. The default action for forking servers is to collect the status of any child processes that have exited, while in threading servers this method does nothing.

process_request(request, client_address)
Calls finish_request() to create an instance of the RequestHandlerClass. If desired, this function can create a new process or thread to handle the request; the ForkingMixIn and ThreadingMixIn classes do this.

server_activate()
Called by the server’s constructor to activate the server. The default behavior for a TCP server just invokes listen() on the server’s socket. May be overridden.

server_bind()
Called by the server’s constructor to bind the socket to the desired address. May be overridden.

verify_request(request, client_address)
Must return a Boolean value; if the value is True, the request will be processed, and if it’s False, the request will be denied. This function can be overridden to implement access controls for a server. The default implementation always returns True.
View Code

 

 

 

ThreadingTCPServer