Socket server for Python 3 network programming

Keywords: Python socket Unix Makefile

This section is mainly about the socket server of Python 3 network programming. In the last section, we talked about socket. Because socket can not support multi-user and multi-concurrency, there is socket server.

The main function of socket server is to implement concurrent processing.

There are two kinds of socket servers:

  1. server class: Provides many methods: binding, listening, running, etc. (that is, the process of establishing connections)
  2. request handle class: Focus on how to process data sent by users (that is, logic of things)

PS: In general, all services are connected first, that is to say, an instance of a service class is established, and then the user's request is processed, that is, an instance of a request processing class is established.

Next, let's look at these two classes.

Service class

There are five types of services:

  1. BaseServer: No direct external service.
  2. TCPServer: For TCP socket streams.
  3. UDP Server: For UDP data sockets.
  4. UnixStream: Not commonly used for Unix sockets.
  5. Unix Data Gram Server: Not commonly used for Unix sockets.

The inheritance relationship between them is as follows:

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

Method of service class:

ClassSocketServer. BaseServer: This is a superclass of all server objects in the module. It defines interfaces, as described below, but most methods are not implemented and refined in subclasses.

    BaseServer.fileno(): Returns the integer file descriptor of the server listening socket. It is usually passed to select.select() to allow a process to monitor multiple servers.

    BaseServer.handle_request(): Processing a single request. Processing order: get_request(), verify_request(), process_request(). If the user provides handle() method to throw an exception, the handle_error() method of the server is called. If no request is received within self.timeout, handle_timeout() is called and handle_request() is returned.

    BaseServer.serve_forever(poll_interval=0.5): Processes the request until a clear shutdown() request is made. Poll shutdown once per poll_interval second. Ignore self.timeout. If you need to do periodic tasks, it is recommended to place them on other threads.

    BaseServer.shutdown(): Tell serve_forever() to stop the loop and wait for it to stop. Version 2.6 of python.

    BaseServer.address_family: Address families, such as socket.AF_INET and socket.AF_UNIX.

    BaseServer. RequestHandler Class: User-provided request processing class that creates instances for each request.

    BaseServer.server_address: The address the server is listening on. The format varies according to the address of the protocol family. See the documentation of the socket module.

    BaseServer.socketSocket: The server on the server that listens for incoming request socket objects.

The server class supports the following class variables:

    BaseServer.allow_reuse_address: Does the server allow address reuse? The default is false and can be changed in subclasses.

    BaseServer.request_queue_size

The size of the request queue. If a single request takes a long time to process, when the server is busy, requests can be placed in a queue, up to request_queue_size. Once the queue is full, the request from the client will get a Connection denied error. The default value is usually 5, but can be overridden by subclasses.

    BaseServer.socket_type: The socket type used by the server; socket.SOCK_STREAM and socket.SOCK_DGRAM, etc.

    BaseServer.timeout: Timeout, in seconds, or None means no timeout. If handle_request() does not receive a request within timeout, handle_timeout() is called.

The following methods can be overloaded by subclasses, which have no effect on external users of server objects.

    BaseServer.finish_request(): Actually process requests from RequestHandlerClass and call its handle() method. Commonly used.

    BaseServer.get_request(): Accepts the socket request and returns the binary containing the new socket object to communicate with the client and the address of the client.

    BaseServer.handle_error(request, client_address): Called if the handle() method of RequestHandlerClass throws an exception. The default operation is to print traceback to standard output and continue processing other requests.

    BaseServer.handle_timeout(): timeout processing. By default, forking servers collect the exit status of the child processes, while threading servers do nothing.

    BaseServer.process_request(request, client_address): Call finish_request() to create an instance of RequestHandlerClass. If necessary, this function can create new processes or threads to process requests, as the ForkingMixIn and ThreadingMixIn classes do. Commonly used.

    BaseServer.server_activate(): Activate the server through its constructor. The default behavior is simply to listen for server sockets. It can be overloaded.

    BaseServer.server_bind(): Binding socket s to the desired address is called from the server's constructor. It can be overloaded.

    BaseServer.verify_request(request, client_address): Returns a Boolean value, if it is True, the request will be processed, and vice versa, the request will be rejected. This function can be rewritten to achieve access control to the server. The default implementation always returns True. Client_address can restrict clients, such as only handling requests for specified ip intervals. Commonly used.

These service classes process requests synchronously: one request is not processed and the next one cannot be processed. To support the asynchronous model, you can use multiple inheritance to let the server class inherit ForkingMixIn or ThreadingMixIn mix-in classes.

Forking MixIn uses multi-process (bifurcation) to achieve asynchrony.

Threading MixIn uses multithreading to achieve asynchronization.

Request Processing Class

To implement a service, you must also derive a handler class request processing class and override the handle() method of the parent class. The handle method is used specifically for processing requests. This module handles requests through a combination of service classes and request processing classes.

The request processing classes provided by the socketserver module are BaseRequestHandler, as well as its derived classes StreamRequestHandler and DatagramRequestHandler. It can be seen from the name that a processing stream socket and a processing datagram socket can be used.

The request processing class has three methods:

  1. setup()
  2. handle()
  3. finish()

setup()  

  Called before the handle() method to perform any initialization actions required. The default implementation does nothing.

Called before handle(), the main function is to perform various tasks related to initialization before processing requests. By default, nothing will be done. (If you want it to do something, you need the programmer to override this method in his request processor (because a custom request processor inherits the BaseRequestHandler provided in python, ps: mentioned below), and then add something to it.)

 

handle() 

  This function must do all the work required to service a request. The default implementation does nothing. Several instance attributes are available to it; the request is available as self.request; the client address as self.client_address; and the server instance as self.server, in case it needs access to per-server information.

  The type of self.request is different for datagram or stream services. For stream services,self.request is a socket object; for datagram services, self.request is a pair of string and socket.

Hand () does all the work related to processing requests. Default won't do anything. He has several instance parameters: self.request self.client_address self.server

 

finish()

  Called after the handle() method to perform any clean-up actions required. The default implementation does nothing. If setup() raises an exception, this function will not be called.

The handle() method is called after the handle() method, and its function is to perform the cleanup after processing the request. By default, nothing will be done.

Then let's look at the source code of Handler:

class BaseRequestHandler:

    """Base class for request handler classes.

    This class is instantiated for each request to be handled.  The
    constructor sets the instance variables request, client_address
    and server, and then calls the handle() method.  To implement a
    specific service, all you need to do is to derive a class which
    defines a handle() method.

    The handle() method can find the request as self.request, the
    client address as self.client_address, and the server (in case it
    needs access to per-server information) as self.server.  Since a
    separate instance is created for each request, the handle() method
    can define arbitrary other instance variariables.

    """

    def __init__(self, request, client_address, server):
        self.request = request
        self.client_address = client_address
        self.server = server
        self.setup()
        try:
            self.handle()
        finally:
            self.finish()

    def setup(self):
        pass

    def handle(self):
        pass

    def finish(self):
        pass


# The following two classes make it possible to use the same service
# class for stream or datagram servers.
# Each class sets up these instance variables:
# - rfile: a file object from which receives the request is read
# - wfile: a file object to which the reply is written
# When the handle() method returns, wfile is flushed properly


class StreamRequestHandler(BaseRequestHandler):

    """Define self.rfile and self.wfile for stream sockets."""

    # Default buffer sizes for rfile, wfile.
    # We default rfile to buffered because otherwise it could be
    # really slow for large data (a getc() call per byte); we make
    # wfile unbuffered because (a) often after a write() we want to
    # read and we need to flush the line; (b) big writes to unbuffered
    # files are typically optimized by stdio even when big reads
    # aren't.
    rbufsize = -1
    wbufsize = 0

    # A timeout to apply to the request socket, if not None.
    timeout = None

    # Disable nagle algorithm for this socket, if True.
    # Use only when wbufsize != 0, to avoid small packets.
    disable_nagle_algorithm = False

    def setup(self):
        self.connection = self.request
        if self.timeout is not None:
            self.connection.settimeout(self.timeout)
        if self.disable_nagle_algorithm:
            self.connection.setsockopt(socket.IPPROTO_TCP,
                                       socket.TCP_NODELAY, True)
        self.rfile = self.connection.makefile('rb', self.rbufsize)
        self.wfile = self.connection.makefile('wb', self.wbufsize)

    def finish(self):
        if not self.wfile.closed:
            try:
                self.wfile.flush()
            except socket.error:
                # An final socket error may have occurred here, such as
                # the local error ECONNABORTED.
                pass
        self.wfile.close()
        self.rfile.close()


class DatagramRequestHandler(BaseRequestHandler):

    # XXX Regrettably, I cannot get this working on Linux;
    # s.recvfrom() doesn't return a meaningful client address.

    """Define self.rfile and self.wfile for datagram sockets."""

    def setup(self):
        from io import BytesIO
        self.packet, self.socket = self.request
        self.rfile = BytesIO(self.packet)
        self.wfile = BytesIO()

    def finish(self):
        self.socket.sendto(self.wfile.getvalue(), self.client_address)
Handler source code

As you can see from the source code, setup()/handle()/finish() in BaseRequestHandler is undefined, while his two derivative classes StreamRequestHandler and DatagramRequestHandler rewrite setup()/finish().

So when we need to write our own socketserver program, we just need to choose one of StreamRequestHandler and DatagramRequestHandler as the parent class, then customize a request processing class, and rewrite handle() method in it.

The steps to create a socket server are as follows:

This subclass is used to process client requests

All interactions with clients are rewritten in handle() method

server.handle_request() only processes one request and exits after processing

server.serve_forever() Handles multiple requests and executes them forever

Code example:

Server side:

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.sendall(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()
Server side

Client:

import socket
import sys
 
HOST, PORT = "localhost", 9999
data = " ".join(sys.argv[1:])
 
# Create a socket (SOCK_STREAM means a TCP socket)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 
try:
    # Connect to server and send data
    sock.connect((HOST, PORT))
    sock.sendall(bytes(data + "\n", "utf-8"))
 
    # Receive data from the server and shut down
    received = str(sock.recv(1024), "utf-8")
finally:
    sock.close()
 
print("Sent:     {}".format(data))
print("Received: {}".format(received))
Client

But you find that the above code still can't handle multiple connections at the same time.

To make your socketserver concurrent, you must choose to use one of the following multi-concurrent classes

class socketserver.ForkingTCPServer

class socketserver.ForkingUDPServer

class socketserver.ThreadingTCPServer

class socketserver.ThreadingUDPServer

So, just use the following sentence

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

Replace this with the following one, and you can have more concurrency, so that every time a client comes in, the server will allocate a new thread to handle the client's request.

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

Posted by almightyegg on Sat, 08 Jun 2019 14:28:37 -0700