This article is participating in Python Theme Month. See the link for details

The socket encapsulates the TCP/IP protocol family and communicates on the network through the socket. Python also realizes the support of socket programming, which can be convenient to develop network applications

Client-server communication application

Here is a simple example of client-server communication

The communication process

  1. The server starts and creates a socket
  2. The server binds the socket to a port
  3. The server starts listening on the port
  4. The port on which the client requests the server to listen
  5. The server receives the connection request from the client and establishes a connection with the client
  6. The client and server send data to each other
  7. The client sends a request to the server to close the connection, and then exits
  8. The server closes the connection

Server-side implementation

import socket
HOST = '127.0.0.1' Set the listening address to local 127.0.0.1
PORT = 65432       Set listening port to 65432
Create a socket using socket.socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
	Bind the socket to the specified address and port
    s.bind((HOST, PORT))
	The server starts listening
    s.listen()
	The server waits for the connection request from the client
    conn, addr = s.accept()
    with conn:
        print('Connected by', addr)
        The server loop receives data from the client
		while True:
            data = conn.recv(1024)
            if not data:
                break
			Return the data to the client after receiving it
            conn.sendall(data)
Copy the code

Creating a Socket connection

At the beginning of the server implementation, a socket object is created using two parameters, socket.AF_INET and socket.SOCK_STREAM, to indicate the use of IPv4 address and TCP protocol respectively. If an IPv6 address is required, the first argument can be socket.af_inet6, for example:

import socket

with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as s:
	s.bind((HOST, PORT))
    s.listen()
Copy the code

If the socket is created based on a Unix file, the first argument can be socket.af_UNIX, and the bound address and port need to be replaced by the desired file path, for example:

import socket

with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s:
    s.bind("/tmp/socket_test.s")
    s.listen()
Copy the code

If you want to use UDP instead of TCP, change the second parameter to socket.sock_dgram and drop listen(), for example:

import socket

with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
    s.bind((HOST, PORT))
Copy the code

Listen () has an optional backlog parameter that sets the maximum number of clients waiting for connections. After the number of clients waiting for connections exceeds this value, new connections will be rejected.

Receive client connections

 conn, addr = s.accept()
 with conn:
     print('Connected by', addr)
Copy the code

Accept () is used to accept client connections. It is a blocking method that returns a addr representing the client’s address, consisting of conn and (Host, Port), when a new client connection is available. When you get conn, manipulate the CONN object in scope with conn, so that conn.close() is called automatically when you leave scope with conn, eliminating the need to manually close the connection.

Receive data

The conn.recv() method is used to read data from the connection object, where 1024 means a maximum of 1024 bytes are read at a time. If any data is received, it is sent back to the client using the sendall() method.

while True:
    data = conn.recv(1024)
    if not data:
        break
    conn.sendall(data)
Copy the code

The difference between send and SendAll

s.send() Send TCP data, sending the data in string to the connected socket. The return value is the number of bytes to send, which may be smaller than the size of the string.
s.sendall() Send TCP data in full. Sends the data in the string to the connected socket, but attempts to send all the data before returning. Returns None on success and raises an exception on failure.

Client-side implementation

import socket

HOST = '127.0.0.1'
PORT = 65432

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect((HOST, PORT))
    s.sendall(b'Hello, world')
    data = s.recv(1024)
Copy the code

The client implementation is similar to the server implementation, except that after the socket object is created, the client uses Connect () to initiate a connection request to the server. After a successful connection, the client can send data to the server using sendall() or receive data from the server using recv()

I/O multiplexing

In the previous example, the server can only handle requests from one client at a time and wait until the client exits before continuing to process other clients. Python’s library of selectors is used in order for a server to support simultaneous connections to multiple clients. Selectors multiplexing system calls for the encapsulation, using selectors. DefaultSelector can according to the code execution platform automatically select the most efficient method of system calls.

Selectors sample

I’m going to create a Selector object

import selectors

sel = selectors.DefaultSelector()
Copy the code

Here the DefaultSelector automatically selects one of the I/O multiplexing methods kqueue, epoll, Devpoll, poll, Select, depending on the platform on which the code is running, Priorities for epoll | kqueue | devpoll > poll > select. Create a socket object as before, bind it to port 65432, and enable listening:

host = '127.0.0.1'
port = 65432

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind((host, port))
sock.listen()
sock.setblocking(False)

print('listening on', (host, port))
Copy the code

Note that you need to use socket.setblocking(False) to set operations performed on the socket to non-blocking mode, otherwise the system call executing the socket will block. In non-blocking mode, send() or recv() is returned immediately, but an exception is thrown, even if the operation cannot be performed immediately. Now we register the socket with the selector object, where only readable events are listened for, so the second argument uses selectors.EVENT_READ

sel.register(sock, selectors.EVENT_READ, data=None)
Copy the code

We can then get the socket that generated the event from the selector object when there is a readable event, for example:

while True:
    # read event
    events = sel.select(timeout=None)
    for key, mask in events:
        Handle client connections
        if key.data is None:
            Accept client connections
            handle_conn(key)
        Handle client events
        else:
            Read and write client data
            handle_rw(key, mask)
Copy the code

Sel.select () returns a list of tuples, each made up of (selectorKey, events). Selectorkey.fileobj is used to get the socket where the event occurred, and events represents the mask for ready events. If selectorKey.data is None, selectorKey is the socket listening on port 65432, so it is a new connection request. You need to register it with a selector in order to receive subsequent incoming data. Otherwise, it indicates that the client is connected and receives data directly.

Receive client connections

Key. Data is None indicates that the key is the socket that listens on port 65432, needs to receive new connection requests through this socket, and then registers with the selector.

def handle_conn(sock) :
    # Receive connection
    conn, addr = sock.accept()
    Set the operation to non-blocking mode
    conn.setblocking(False)
    Create a data object to store the data associated with this connection
    data = types.SimpleNamespace(addr=addr, inb=b'', outb=b'')
    For new connections, you need to pay attention to readable events (client has data incoming) and writable events (allows server data to be written back to client).
    events = selectors.EVENT_READ | selectors.EVENT_WRITE
    Register the socket corresponding to the new connection with selector
    sel.register(conn, events, data=data)
Copy the code

Handles client reads and writes

Events triggered by the client can be classified into readable events and writable events:

  • Readable event: indicates that the client sends new data to the server, which needs to be invoked by the serverrecv()Read data from the corresponding socket of the client.
  • Writable event: indicates that the socket corresponding to the client can receive data. In this case, the server can send data to the clientsend()orsendall()Method to send data to the client.
def handle_data(key, mask) :
    Get the socket that triggers the event
    sock = key.fileobj
    Register = register; register = register;
    # is a types.SimpleNamespace object
    data = key.data
    
    Check if there are any readable events
    if mask & selectors.EVENT_READ:
        Receive data and add it to the outb property of data
        recv_data = sock.recv(1024)
        if recv_data:
            data.outb += recv_data
        The client sends empty data, indicating that the data has been sent
        else:
            print('closing connection to', data.addr)
            The selector doesn't need to listen on the socket anymore, so it does
            Unregister the corresponding socket in selector
            sel.unregister(sock)
            Close the socket connection
            sock.close()
            
    Check if there are writable events
    if mask & selectors.EVENT_WRITE:
        Return the data sent by the client to the client as is
        if data.outb:
            print('echoing'.repr(data.outb), 'to', data.addr)
            sent = sock.sendall(data.outb)
Copy the code

reference

  • Socket Programming in Python (Guide) – Real Python
  • Socket programming
  • TCP programming
  • Socket Programming HOWTO – Python 3.9.6 documentation
  • User Datagram Client and Server – Python Module of the Week
  • Selectors – high-level I/O multiplexing