instructions

Socket Programming in Python (Guide) this book is adapted from the realPython tutorial Socket Programming in Python (Guide). You can download PDF/Mobi/ePub files on the home page or read them online

The original author

Nathan Jennings, a member of the Real Python tutorial team, began his programming career long ago using C, but eventually discovered Python, from Web applications and network data collection to network security, He loves anything Pythonic — realpython

.

The translator is a front-end engineer who usually writes a lot of JavaScript. But when I’ve been working with JavaScript for a long time, I’ve become interested in language neutral programming concepts like network /socket programming, asynchronous/concurrent, line/process communication, etc. However, these things happen to be rare in the JavasScript world

Having been involved in Web development all my life, I thought that understanding network communication and socket programming meant understanding some of the essence of Web development. Along the way, I discovered that the Python community has a lot of content that I like, and much of it is high quality, publicly available and open source.

I recently found this article, which systematically goes from low-level network communication to application layer protocols and their C/S architecture applications. Although the code and API use Python, the underlying principles are the same. Well worth reading and recommended

In addition, due to my limited level, there will inevitably be deviations in the translation content. If you find any problems in the reading process, please do not hesitate to remind me or open a new PR. Or if there is something you don’t understand, you can also open an issue discussion. Of course, STAR is also welcome

authorization

This article is authorized by RealPython. The copyright of the original article belongs to realPython. Please contact them for any reprint. The translation follows the license agreement of this site

start

Sockets and Socket apis in networks are used for messaging across networks and provide a form of interprocess communication (IPC). The network can be a logical, local computer network, or a network that can be physically connected to an external network and can be connected to other networks. An obvious example is the Internet, the network you connect to through your ISP

This tutorial has three different iterations to show how to build a Socket server and client using Python

  1. We’ll start the tutorial with a simple Socket server and client program
  2. After you look at the API and see how the example works, we’ll see an improved version of the example with the ability to handle multiple connections at once, right
  3. Finally, we will develop a more complete Socket application with complete custom header information and content

At the end of this tutorial, you will learn how to write your own client/server application using the Socket module in Python. And show you how to use custom classes in your application to send messages and data between different ends

All sample programs are written in Python 3.6, the source code of which can be found on Github

Networks and sockets are a big topic. There are already literal explanations for them on the Internet, if you are not familiar with sockets and networks. It’s normal to feel overwhelmed when you read those explanations. Because that’s what HAPPENED to me

Don’t be discouraged, though. I’ve written this tutorial for you. Just like learning Python, we can learn a little bit at a time. Use your browser to bookmark this page so you can find it when you work on the next section

Let’s get started!

background

Sockets have a long history, first used for ARPANET in 1971 and then as an API for the Berkeley Software Distribution (BSD) operating system released in 1983, And named Berkeleysocket

When the Internet emerged with the World Wide Web in the 1990s, network programming caught fire. Web services and browsers are not the only applications using new connections to networks and sockets. They are widely used by client/server applications of all types and sizes

Today, although the underlying protocol used by the Socket API has evolved over many years and many new protocols have emerged, the underlying API remains the same

The most common type of Socket application is the client/server application. The server waits for the connection from the client. This is the kind of application we cover in our tutorial. More specifically, we’ll see the Socket API for InternetSocket, sometimes called Berkeley or BSD Sockets. There are also Unix Domain Sockets, which are used for communication between the same host process

The Socket API overview

Python’s socket module provides an interface using the Berkeley Sockets API. This will be used and discussed in this tutorial

The main Socket API functions and methods used are as follows:

  • socket()
  • bind()
  • listen()
  • accept()
  • connect()
  • connect_ex()
  • send()
  • recv()
  • close()

Python provides a convenient API that is consistent with THE C language. We’ll use them in the next section

As part of the standard library, Python also has classes that make it easy to call these underlying Socket functions. Although this is not covered in this tutorial, you can also find documentation in the SocketServer module. Of course, there are many modules that implement high level network Protocols (e.g., HTTP, SMTP). You can find Internet Protocols and Support in the link below

TCP Sockets

As you’ll see in a moment, we’ll use socket.socket() to create a socket object of type socket.sock_stream, which by default uses the Transmission Control Protocol(TCP), This is basically the default value you want to use

Why should TCP be used?

  • Reliable: Lost packets in network traffic are detected and resend
  • Sequential transmission: Data is read in the order in which the sender writes

In contrast, user datagram protocol (UDP) sockets created using socket.sock_dgram are unreliable, and data reads and writes can be sent out of order

Why is this important? The network will do its best (and often not best) to transmit complete data. There is no guarantee that your data will be sent to its destination or that it will receive the data sent to you

Network devices (such as routers and switches) have bandwidth limits, or limits on the system itself. They also have CPU, memory, bus, and interface packet buffers, just like our clients and servers. TCP eliminates your worries about packet loss, out-of-order, and other common problems in network communication

In the following diagram, we can see the order of Socket API calls and TCP data flow:

The left side represents the server and the right side represents the client

Starting at the top left, notice that the server creates API calls that “listen” for the Socket:

  • socket()
  • bind()
  • listen()
  • accept()

The “listening” Socket does just as its name suggests. It listens for client connections, and when a client connects, the server calls Accept () to “accept” or “complete” the connection

The client calls the connect() method to establish a link to the server and initiate a three-way handshake. The handshake is important because it ensures that both sides of the network are reachable, that is, the client can connect to the server and vice versa

The round-trip section in the middle of the figure represents the data exchange between the client and server, calling the send() and recv() methods

In the following section, the client and server call the close() method to close their sockets

Print client and server

Now that you know the basic Socket API and how the client and server communicate, let’s create a client and server. We’ll start with a simple implementation. The server prints what the client sends back

Print program server

Here is the server code, echo-server.py:

#! /usr/bin/env python3

import socket

HOST = '127.0.0.1'  Localhost = localhost;
PORT = 65432        # Listen on port (non-system port: > 1023)

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind((HOST, PORT))
    s.listen()
    conn, addr = s.accept()
    with conn:
        print('Connected by', addr)
        while True:
            data = conn.recv(1024)
            if not data:
                break
            conn.sendall(data)
Copy the code

Note: You may not fully understand the code above, but don’t worry. These few lines of code do a lot of things, and this is just a starting point, to help you see how this simple server works. There’s a reference section at the end of the tutorial, and there’s a lot of additional references to resources. Where am I going to put the links in this tutorial

Let’s take a look at the API call and what happens

Socket.socket () creates a socket object and supports the Context Manager Type. You can use the with statement so that you don’t have to manually call s.close() to close the socket

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    pass  # Use the socket object without calling s.close().
Copy the code

The socket address family parameter socket.af_inet represents the Internet IPv4 address family, and SOCK_STREAM represents the type of socket that uses TCP, the protocol that will be used to transport messages across the network

Bind () is used to associate the socket with the specified network interface (IP address) and port number:

HOST = '127.0.0.1'
PORT = 65432

#...

s.bind((HOST, PORT))
Copy the code

The input to the bind() method depends on the address family of the socket. In this case we use socket.af_inet (IPv4), which will return a tuple of two elements :(host, port)

127.0.0.1 is the standard IPv4 loopback address. Only processes on the host can connect to the server. If you pass an empty string, The server will accept all IPv4 addresses available on the host

The port number must be an integer ranging from 1 to 65535 (0 is reserved). This integer is the TCP port number used to accept client connections. If the port number is less than 1024, some operating systems require administrator rights

When using bind() to pass a host name, note:

If you use the host name as the address of the IPv4/v6 socket in the host section, the program may behave inaccurately because Python uses the first address resolved by DNS. Setting the socket address according to the result of DNS resolution or host will resolve to the actual IPv4/v6 address in different ways. It is recommended to use a numeric address reference for the host argument if you want to get a definite result

I’ll discuss this later in the section on using host names, but it’s worth mentioning for now. For now, all you need to know is that when using a host name, you will get different results because of DNS resolution

It could be any address. For example, the program runs 10.1.2.3 the first time, 192.168.0.1 the second time, 172.16.7.8 the third time, and so on

Continuing with the server code example above, the listen() method call enables the server to accept connection requests, which makes it a “listening” socket

s.listen()
conn, addr = s.accept()
Copy the code

The Listen () method has a backlog argument. It specifies the number of unaccepted connections that the system will allow to be used before rejecting new connections. As of Python 3.5, this is optional. If not specified, Python takes a default value

If your server needs to receive many connection requests at the same time, increasing the backlog parameter can increase the length of the queue waiting for link requests, depending on the operating system. Under Linux, for example, reference/proc/sys/net/core/somaxconn

The Accept () method blocks and waits for an incoming connection. When a client connects, it returns a new socket object with a CONN representing the current connection and a tuple of IPv4/ V6 connections made up of hosts and port numbers. For more information on tuple values, see the socket address family section

It is important to understand that we have a new socket object by calling the Accept () method. This is important because you will use this socket object to communicate with the client. Unlike listening on a socket, which only receives new connection requests

conn, addr = s.accept()
with conn:
    print('Connected by', addr)
    while True:
        data = conn.recv(1024)
        if not data:
            break
        conn.sendall(data)
Copy the code

After receiving the client socket connection object conn from Accept (), use an infinite while loop to block the call to conn.recv(), and whatever data the client passes will be printed out with conn.sendall()

If the conn.recv() method returns an empty byte object (b “”) and the client closes the connection, the loop ends. When the with statement is used with CONN, the socket connection is automatically closed at the end of the communication

Print program client

Now let’s look at the client program, echo-client.py:

#! /usr/bin/env python3

import socket

HOST = '127.0.0.1'  The host name or IP address of the server
PORT = 65432        # Port used by the server

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

print('Received', repr(data))
Copy the code

Compared to the server program, the client program is much simpler. It creates a socket object that connects to the server and calls s.sendall() to senda message, which then calls s.recv() to read what the server returns and print it out

The client and server that run the printing program

Let’s run the client and server side of the print program, watch them perform, and see what happens

If you have problems running the sample code, read how to develop command-line commands in Python, and if you’re running Windows, check out the Python Windows FAQ

Open a command line program, go to the directory where your code resides, and run the server side of the print program:

/ $.echo-server.py
Copy the code

Your command line is suspended because the program has a blocking call

conn, addr = s.accept()
Copy the code

It will wait for the client to connect, and now open a command line window to run the print client:

/ $.echo-client.py
Received b'Hello, world'
Copy the code

In the server window you will see:

/ $.echo-server.py
Connected by ('127.0.0.1', 64623).Copy the code

In the output above, the server prints out the ADDR tuple returned by s.acept (), which is the client’s IP address and TCP port number. The port number in the example is 64623 which is probably different from the result on your machine

Checking socket Status

To find the current state of sockets on your host, use the netstat command. This command is available by default on macOS, Windows, and Linux

Here is the output of the netstat command after starting the service:

$ netstat -an
Active Internet connections (including servers)
Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)
tcp4       0      0  127.0.0.1.65432        *.*                    LISTEN
Copy the code

Note that the local address is 127.0.0.1.65432. If HOST is set to an empty string ” in the echo-server.py file, the netstat command will display:

$ netstat -an
Active Internet connections (including servers)
Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)
tcp4       0      0  *.65432                *.*                    LISTEN
Copy the code

The local address is *.65432, which means that all IP address families supported by the host can accept incoming connections. In our example, the socket.af_inet parameter passed when calling socket() indicates that IPv4 TCP sockets are used. You can see it in the Proto column in the output (tcp4)

The output above, which I’ve captured, only shows our printer server process, and you may see more output depending on what system you’re running. Note the Proto, Local Address, and State columns. Indicates the TCP socket type, local address port, and current status

Another way to view this information is to use the lsof command, which is installed by default on macOS and requires you to install it manually on Linux

$ lsof -i -n
COMMAND     PID   USER   FD   TYPE   DEVICE SIZE/OFF NODE NAME
Python    67982 nathan    3u  IPv4 0xecf272      0t0  TCP *:65432 (LISTEN)
Copy the code

The isof COMMAND uses the -i parameter to view the COMMAND, PID(Process ID), and USER(USER ID) of the open socket. The output above is the print program server

The netstat and isof commands have a number of parameters available, depending on the operating system you are using. You can use the man Page to view their usage documentation, which is definitely worth a little time to understand, you will learn a lot, macOS and Linux use the man netstat or man lsof command, Windows use netstat /? To view the help documentation

A common mistake to make is to try to connect without listening on the socket port:

/ $.echo-client.py
Traceback (most recent call last):
  File "./echo-client.py", line 9, in <module>
    s.connect((HOST, PORT))
ConnectionRefusedError: [Errno 61] Connection refused
Copy the code

If a Connection is timed out, remember to add a port rule to your firewall that allows us to use it

There are some common errors in the reference section

Process decomposition of communication

Let’s take a closer look at how the client communicates with the server:

When loopback addresses are used, the data does not touch the external network. In the figure above, the loopback addresses are included in the host. This is the nature of the loopback address, the connection data transfer is from the local to the host, which is why you hear IP addresses that have loopback addresses or 127.0.0.1, ::1 and represent localhost

The application uses the loopback address to communicate with other processes on the host, which makes it securely isolated from the external network. Since it is internal and can only be accessed from within the host, it cannot be exposed

If your application server uses its own dedicated (non-public) database, you can configure the server to listen only for loopback addresses so that no other host on the network can connect to your database

If your application uses an IP address other than 127.0.0.1 or ::1, it may be bound to an Ethernet network connected to an external network. This is your gateway to other hosts outside the localhost kingdom

Be careful here, and it may make you feel uncomfortable and even suspicious of the world. Before you explore the security limitations of localhost, be sure to read the section on using host names. One security note is to use IP addresses instead of host names

Handling multiple connections

The printer server certainly has its own limitations. This program can only serve one client and then terminate. The printer’s client has its own limitations, but there is a problem. If the client calls the following method, s.recv(), the method will return a byte b ‘h ‘in b’Hello, world’

data = s.recv(1024)
Copy the code

The bufsize parameter does not mean that recv() only returns 1024 bytes of content

The send() method works the same way. It returns the number of bytes sent, which may be smaller than the number of bytes sent in. You have to deal with the situation and call send() as many times as necessary to send the full data

The application is responsible for checking that all data has been sent; If only some data is transferred, the application needs to try to pass the remaining data references

We can circumvent this process by using the sendall() method

Unlike send(), sendall() sends bytes until all data is transferred or an error occurs. A None reference is returned on success

So far, we have two questions:

  • How do I process multiple connection requests at the same time
  • We need to keep callingsend()orrecv()Until all data transfer is complete

So how do you do that? There’s a lot of ways to do concurrency. More recently, a very fluid library called Asynchronous I/O can be implemented, and the Asyncio library was added to the standard library by default after Python 3.4. The traditional approach is to use threads

The problem with concurrency is that it’s hard to get it right, and there are many nuances to consider and guard against. One of these details could cause the entire program to crash

I’m not saying this to scare you off or keep you from learning and using concurrent programming. Using multiple processors and cores is essential if you want your program to support large-scale use. In this tutorial, however, we will use more traditional methods than threads to make logic easier to reason with. We’ll use a very old system call: select()

Select () allows you to check the I/O completion of multiple sockets, so you can use it to check which socket I/O is ready to perform a read or write operation, but this is Python, there are always more alternatives, We’ll use the selectors module from the standard library, so we’re using the most efficient implementation, regardless of which operating system you’re using:

This module provides high level and efficient I/O multiplexing. Built on the original SELECT module, users are recommended to use this module unless they need precise usage control references at the operating system level

However, you can’t execute concurrently with select(). Depending on your workload, this implementation can still be fast. It also depends on what your application does with the connection or how many clients it needs to support

Asyncio uses a single thread to handle multiple tasks and an event loop to manage tasks. By using select(), we can create our own event loop, which is simpler and synchronized. When using multithreading, even to deal with concurrency, we have to face the “global parser lock GIL” in CPython or PyPy, which effectively limits the amount of work we can do in parallel

To explain why using select() might be a better choice, don’t feel like you have to use asyncio, threads, or the latest asynchronous library. Typically, in a network application, your application is an I/O binding: it can wait on the local network, on the other end of the network, on disk

If you receive a request from a client to start CPU binding work, look at the concurrent.futures module, which contains a ProcessPoolExecutor class to asynchronously execute calls from the process pool

If you use multiple processes, your Python code is scheduled to run in parallel by the operating system on different processors or cores, and there is no global parser lock. You can get more ideas from the Python Conference presentation John Reese – Thinking Outside the GIL with AsyncIO and Multiprocessing – PyCon 2018

In the next section, we’ll look at examples of servers and clients that solve these problems. They use select() to process multiple connection requests simultaneously, calling send() and recv() as many times as needed

Multi-connected client and server

In the next two sections, we’ll use the Selector object in the selectors module to create a client and server that can handle multiple requests simultaneously

Multi-connected server

On the first page, we’ll look at the code for the multiconnection server, multiconn-server.py. This is the start of the listening socket section

import selectors
sel = selectors.DefaultSelector()
#...
lsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
lsock.bind((host, port))
lsock.listen()
print('listening on', (host, port))
lsock.setblocking(False)
sel.register(lsock, selectors.EVENT_READ, data=None)
Copy the code

This application and before print server. The big difference is that the used lsock setblocking (False) configuration socket for non-blocking mode, the call will not socket is blocked. When used with sel.select() (mentioned below), we can wait for socket-ready events and then perform reads and writes

Sel.register () uses sel.select() to register socket monitoring for events you are interested in. For listening sockets, we want to use selectors.EVENT_READ to read events

Data is used to store any data you want in the socket, and will be returned when select() returns. We’ll use data to track what’s being sent or received on the socket

Here is the event loop:

import selectors
sel = selectors.DefaultSelector()

#...

while True:
    events = sel.select(timeout=None)
    for key, mask in events:
        if key.data is None:
            accept_wrapper(key.fileobj)
        else:
            service_connection(key, mask)
Copy the code

Sel.select (timeout=None) will block until socket I/O is ready. It returns a (key, events) tuple, one for each socket. Key is a named tuple that contains the Fileobj attribute. Fileobj is a socket object, and mask represents an operation-ready event mask

If key.data is null, we know that it came from the listening socket, and we need to call the Accept () method to accept the connection request. We’ll use an Accept () wrapper function to get the new socket object and register it with the selector, as we’ll see in a moment

If key.data is not empty, we know that it is an accepted client socket and we need to service it. Then service_Connection () passes in the key and mask arguments and calls, This contains everything we need to operate on the socket

Let’s take a look at what the accept_wrapper() method does:

def accept_wrapper(sock):
    conn, addr = sock.accept()  # Should be ready to read
    print('accepted connection from', addr)
    conn.setblocking(False)
    data = types.SimpleNamespace(addr=addr, inb=b'', outb=b'')
    events = selectors.EVENT_READ | selectors.EVENT_WRITE
    sel.register(conn, events, data=data)
Copy the code

Since the listening socket is registered with selectors.EVENT_READ, it can now be read, and immediately after calling sock.accept() we call conn.setblocking(False) immediately to put the socket into non-blocking mode

Keep in mind that this is the main goal of this version server program, because we don’t want it to get blocked. If blocked, the entire server is suspended until it returns. This means that other sockets are in a wait state, which is a very serious state in which no one wants to see the service suspended

Then we use the types.SimpleNamespace class to create an object to hold the socket and data we want. Since we need to know when the client connection can be written or read, the following two events are used:

events = selectors.EVENT_READ | selectors.EVENT_WRITE
Copy the code

Event masks, sockets, and data objects are passed to sel.register()

Now let’s see how connection requests are handled using service_Connection () when the client socket is ready

def service_connection(key, mask):
    sock = key.fileobj
    data = key.data
    if mask & selectors.EVENT_READ:
        recv_data = sock.recv(1024)  # Should be ready to read
        if recv_data:
            data.outb += recv_data
        else:
            print('closing connection to', data.addr)
            sel.unregister(sock)
            sock.close()
    if mask & selectors.EVENT_WRITE:
        if data.outb:
            print('echoing', repr(data.outb), 'to', data.addr)
            sent = sock.send(data.outb)  # Should be ready to write
            data.outb = data.outb[sent:]
Copy the code

This is the core of the multi-connection server. The key is a named tuple returned from a call to select() that contains the socket object “fileobj” and the data object. Masks contain ready events

If the socket is ready and can be read, mask & selectors.EVENT_READ is true and sock.recv() is called. All read data is appended to data.outb. It’s then sent out

Note the else: statement, if no data is received:

if recv_data:
    data.outb += recv_data
else:
    print('closing connection to', data.addr)
    sel.unregister(sock)
    sock.close()
Copy the code

This indicates that the client has closed its socket connection, and the server should also close its own connection. But don’t forget to unmonitor select() by calling sel.unregister() first

When a socket is ready to be read, as normal sockets should always be, any data received and stored by data.outb will be printed out using sock.send(). The sent bytes are then deleted from the buffer

data.outb = data.outb[sent:]
Copy the code

Multi-connected client

Now let’s take a look at the multi-connected client program, multiconn-client.py, which is similar to the server except that it does not listen for connection requests. Instead, it initiates the connection with a call to start_connections() :

messages = [b'Message 1 from client.'.b'Message 2 from client.']


def start_connections(host, port, num_conns):
    server_addr = (host, port)
    for i in range(0, num_conns):
        connid = i + 1
        print('starting connection', connid, 'to', server_addr)
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.setblocking(False)
        sock.connect_ex(server_addr)
        events = selectors.EVENT_READ | selectors.EVENT_WRITE
        data = types.SimpleNamespace(connid=connid,
                                     msg_total=sum(len(m) for m in messages),
                                     recv_total=0,
                                     messages=list(messages),
                                     outb=b'')
        sel.register(sock, events, data=data)
Copy the code

The num_conns parameter is read from the command line and represents how many links are made to the server. Just like server programs, each socket is set to non-blocking mode

Since the connect() method immediately raises a BlockingIOError exception, we use the connect_ex() method instead. Connect_ex () returns an error indicating errno.EINPROGRESS, unlike the connect() method that returns an exception directly in the process. Once the connection ends, the socket can read and write and return via the select() method

Once the socket is set up, we use the types.SimpleNamespace class to create the data we want to send. Since socket.send() is called for each connection request, messages sent to the server are converted to a list structure using the list(messages) method. Everything you want to know about the messages that the client is about to send, sent, received, and the total number of bytes of the message is stored in the Data object

Let’s look at service_Connection () again. Basically the same as the server:

def service_connection(key, mask):
    sock = key.fileobj
    data = key.data
    if mask & selectors.EVENT_READ:
        recv_data = sock.recv(1024)  # Should be ready to read
        if recv_data:
            print('received', repr(recv_data), 'from connection', data.connid)
            data.recv_total += len(recv_data)
        if not recv_data or data.recv_total == data.msg_total:
            print('closing connection', data.connid)
            sel.unregister(sock)
            sock.close()
    if mask & selectors.EVENT_WRITE:
        if not data.outb and data.messages:
            data.outb = data.messages.pop(0)
        if data.outb:
            print('sending', repr(data.outb), 'to connection', data.connid)
            sent = sock.send(data.outb)  # Should be ready to write
            data.outb = data.outb[sent:]
Copy the code

One difference is that the client tracks the number of bytes received from the server and decides whether to close the socket connection based on the result. The server also closes the connection if it detects that the client is closed

Running multi-connected clients and servers

Now let’s run two programs multiconn-server.py and multiconn-client.py. They both use command-line arguments. If no argument is specified, you can see the method called by the argument:

Server program, pass in the host and port number

$ ./multiconn-server.py
usage: ./multiconn-server.py <host> <port>
Copy the code

The client program, passing in the same host and port number and connection number as when the server program was started

$ ./multiconn-client.py
usage: ./multiconn-client.py <host> <port> <num_connections>
Copy the code

The following is the output of the server program running to listen for the loopback address on port 65432:

$./multiconn-server.py 127.0.0.1 65432 Listening on ('127.0.0.1', 65432)
accepted connection from ('127.0.0.1', 61354)
accepted connection from ('127.0.0.1', 61355)
echoing b'Message 1 from client.Message 2 from client.' to ('127.0.0.1', 61354)
echoing b'Message 1 from client.Message 2 from client.' to ('127.0.0.1', 61355)
closing connection to ('127.0.0.1', 61354)
closing connection to ('127.0.0.1', 61355).Copy the code

Here is the client, which creates two connection requests to the server above:

/multiconn-client.py 127.0.0.1 65432 2 Starting Connection 1 to ('127.0.0.1', 65432)
starting connection 2 to ('127.0.0.1', 65432)
sending b'Message 1 from client.' to connection 1
sending b'Message 2 from client.' to connection 1
sending b'Message 1 from client.' to connection 2
sending b'Message 2 from client.' to connection 2
received b'Message 1 from client.Message 2 from client.' from connection 1
closing connection 1
received b'Message 1 from client.Message 2 from client.' from connection 2
closing connection 2
Copy the code

Application client and server

The multi-connected client and server application versions are certainly a big improvement over the original original version, but let’s address the shortcomings of the “multi-connected” version above further before completing the final implementation: the client/server application

We want a client and a server to handle errors without affecting other connections. Obviously, if no exception occurs, our client and server can’t crash into a mess. This is something that we haven’t talked about so far, but I purposely didn’t introduce error handling because it made the previous program easier to understand

You now have an understanding of the basic APIS, non-blocking sockets, select(), and other concepts. We can continue to add some error handling and talk about the elephant in the room, which I’ve left behind. As you may recall, I discussed custom classes in my introduction

First, let’s fix the error:

All errors trigger exceptions, common exceptions like invalid parameter types and out of memory can be thrown; Starting with Python 3.3, errors related to socket or address semantics raise an exception reference to OSError or one of its subclasses

We need to catch OSError exceptions. Another issue I didn’t mention is latency, which you’ll see discussed in many parts of the document. Latency happens and is a “normal” error. Host or router restarts, switch ports fail, cables fail or are pulled out, you should handle all kinds of errors in your code

What about the elephant in the room problem? As socket.SOCK_STREAM literally means, when using a TCP connection, you read data from a continuous stream of bytes, just like reading data from a disk, except that you read the stream from the network

However, unlike reading a file with f.seek(), in other words, there is no way to locate the socket’s data stream. If you can locate the data stream like a file (using subscripts), you can read as much data as you want

When bytes flow into your socket, you need to have a different network buffer. If you want to read them, you must first save them somewhere else. Use the recv() method to continuously read the available byte stream from the socket

Instead of reading chunks of data from the socket, you must use the recv() method to keep reading from the buffer until your application is sure it has read enough data

What counts as “enough” depends on your definition. In the case of a TCP socket, it only sends or receives raw bytes over the network. It doesn’t know what these raw bytes mean

This allows us to define an application layer protocol. What is an application layer protocol? Simply put, your application sends or receives messages that are essentially your application’s protocol

In other words, the length and format of these messages can define the semantics and behavior of the application. This is related to reading bytes from the socket. When you use recv() to read bytes, you need to know the number of bytes read and decide when the read is complete

How is all this done? One approach is to read only fixed-length messages, which is easy if they are always the same length. When you receive a fixed-length byte message, you know that it is a complete message

However, if you use fixed-length mode to send shorter messages, it’s inefficient because you have to deal with filling the rest of the message, and you also have to deal with the fact that the data doesn’t fit into a fixed-length message

In this tutorial, we will use a generic scheme that is used by many protocols, including HTTP. We will append a header to each message containing the length of the message and any other fields we need. To do this we only need to track the header, and when we read the header, we can look up the length of the message and read all the bytes and consume it

We will implement receiving text/binary data by using a custom class. You can improve on this or extend your application by inheriting this class. The important thing is that you will see an example of how to implement it

I’ll mention a few things about sockets and bytes, as discussed earlier. When you send or receive data through a socket, you are actually sending or receiving raw bytes

If you receive data and want it to be used in a multi-byte interpretation context, such as a 4-byte integer, you need to consider that it may be in a format that is not native to your machine’s CPU. The other end of the client or server may have another CPU that uses a different byte sequence, in which case you have to convert them to your host’s native byte sequence

The byte order mentioned above is the CPU byte order, which you can see more about in the byte order section of the References section. We will circumvent this problem by taking advantage of the Unicode character set and using UTF-8 encoding. Since UTF-8 uses 8-byte encoding, there will be no problem with byte sequences

You can check out Python’s documentation on encoding and Unicode, and note that we only encode the header of the message. We will use strict typing, and the encoding format of the sent message will be defined in the header. This will allow us to transfer data of any type/format we find useful

You can determine the byte sequence of your machine by calling sys. byteOrder, such as on my Intel laptop, by running the following code:

$ python3 -c 'import sys; print(repr(sys.byteorder))'
'little'
Copy the code

If I run this code on a virtual machine that emulates a large byte order CPU “PowerPC”, it should look like this:

$ python3 -c 'import sys; print(repr(sys.byteorder))'
'big'
Copy the code

In our example application, the application layer protocol defines Unicode characters encoded in UTF-8 mode. For the actual transmission of messages, you still have to manually swap byte sequences if necessary

Depending on your application, whether you need it to handle multi-byte binary data between different terminals, you can make your client or server support binary by adding additional headers, like HTTP, that are passed in as parameters

Don’t worry if you haven’t figured it out yet, we’ll see how it works in the next section

Application protocol header

Let’s define a complete protocol header:

  • Variable length text
  • Unicode character set based on UTF-8 encoding
  • A Python dictionary that uses JSON serialization

The required headers should be the following:

The name of the describe
byteorder The machine’s byte sequence (uses sys.byteorder) may not be used by applications
content-length The length of the content in bytes
content-type The type of the content, such as text/json or binary/my-binary-type
content-encoding Encoding type of content, such as UTF-8 encoding of Unicode text, binary data

These headers tell the receiver the message data, so you can send any data to it by giving the receiver enough information to decode it correctly when it receives it. Since the header is in dictionary format, you can add key-value pairs to the header as you like

Send application messages

One problem is that since we’re using variable headers, which is easy to expand, how do you know the length of the header when you read the message using recv()

We talked earlier about using recv() to receive data and how to determine if the receive is complete. I said fixed-length headers can be inefficient, and they are. But we will use a small 2-byte fixed-length header prefix to indicate the length of the header

You can think of this as a hybrid implementation of sending a message, where we guide the recipient through the length of the sending header so they can parse the message body

To give you a better explanation of the message format, let’s look at the message in its entirety:

The message starts with a 2-byte fixed-length header, which is a sequence of network bytes of integer type that represents the length of the following variant-length JSON header. We know it represents an integer of header length when we read 2 bytes from the recv() method, and then read that many bytes before decoding the JSON header

JSON headers contain a dictionary of header information. One of these is Content-Length, which indicates the amount of content (not the JSON header) in the message. When we read content-Length bytes using recv(), we are done and have read the complete message

Application class

Finally, let’s look at the results. We use a message class. How does it work with select() on socket read/write events

For this sample application, I had to figure out what type of messages the client and server would use, which at this point is far beyond the toy-like printing program we originally wrote, right

To keep the program simple and still demonstrate how it works in a real program, I created an application protocol to implement the basic search functionality. The client sends a search request, the server does a matching search, and if the client’s request is not recognized as a search request, the server assumes it is a binary request and returns a binary response

After you run the examples and experiment with the code in the following section you will see how it works, and then you can use the message class as a starting point to modify it for your own use

As we discussed earlier, you will see below that state needs to be saved when processing sockets. By using classes, we can package all the state, data, and code into one place. The message class creates an instance for each socket when a connection is opened or accepted

Many of the wrapper methods and utility methods in the class are the same on both the client and server sides. They start with an underscore, like message._json_encode (), and these methods are simple to use through classes. This makes them shorter to call in other methods and DRY compliant

The server program for the message class is essentially the same as the client. The difference is that the client initializes the connection and sends the request message, which is then processed by the server. The server waits for the connection request, processes the client request message, and then sends the response message

It looks something like this:

steps end Action/message content
1 The client Send a message with the request content
2 The service side Receive and process the request message
3 The service side Sends a message with response content
4 The client Receive and process the response message

Here is the structure of the code:

The application file code
The service side app-server.py Server main program
The service side libserver.py Server side message class
The client app-client.py Client main program
The client libclient.py Client message class

Message entry point

I’d like to discuss how the Message class works by first mentioning its design aspects, but it’s not immediately clear to me that I can get to its current state until I’ve refactored it at least five times. Why is that? Because you have to manage state

When the message object is created, it is associated with a socket monitored using the selector. Register () event

message = libserver.Message(sel, conn, addr)
sel.register(conn, selectors.EVENT_READ, data=message)
Copy the code

Note that some of the code in this section comes from the server main program and message classes, but this discussion is the same on the client side, and I’ll explain the client version when there are differences

When the event on the socket is ready, it is returned by selector. Select (). Get a reference to Message from the key’s data property, and call a method on the message:

while True:
    events = sel.select(timeout=None)
    for key, mask in events:
        #...
        message = key.data
        message.process_events(mask)
Copy the code

Looking at the event loop above, you can see that sel.select() is in the “driver position”, which is blocked and waits at the top of the loop. It serves read and write events on the socket when they are ready. This means that indirectly it is also responsible for calling the process_events() method. That’s why I say the process_events() method is the entry point

Let’s see what the process_events() method does

def process_events(self, mask):
    if mask & selectors.EVENT_READ:
        self.read()
    if mask & selectors.EVENT_WRITE:
        self.write()
Copy the code

This is good because the process_events() method is concise and can only do two things: call the read() and write() methods

This brings us back to state management. After a few refactorings, I decided that if other methods depend on a certain value in a state variable, they should only be called from the read() and write() methods, which would keep the logic for handling socket events as simple as possible

It may sound simple, but after several previous iterations of the class: mixing methods, checking the current state, relying on other values, and calling methods that process data outside of the read() or write() methods, it turned out to be complicated to manage

Of course, you’ll definitely need to tailor the class to your own needs to make it fit your expectations, but I recommend that you put the logic for state checking and state-dependent calls into the read() and write() methods whenever possible

Let’s look at the read() method, which is the server version, but the client is the same. The difference is in the method name, one (client) is process_response() and the other (server) is process_request()

def read(self):
    self._read()

    if self._jsonheader_len is None:
        self.process_protoheader()

    if self._jsonheader_len is not None:
        if self.jsonheader is None:
            self.process_jsonheader()

    if self.jsonheader:
        if self.request is None:
            self.process_request()
Copy the code

The _read() method is called, and socket.recv() is called to read data from the socket and store it into the receive buffer

Remember that when the socket.recv() method is called, not all of the data that makes up the message arrives at once. The socket.recv() method may need to be called many times, which is why the state should be checked each time before the relevant method is called to process the data

When a method starts processing a message, the home page checks that the receiving buffer holds enough read data, and if so, they proceed to process their own data, save the data to variables that other processes might use, and clear their own buffers. Since there are three components to a message, there are three calls to status checking and handling methods:

Message Component Method Output
Fixed-length header process_protoheader() self._jsonheader_len
JSON header process_jsonheader() self.jsonheader
Content process_request() self.request

Next, let’s take a look at the write() method, which is the server-side version:

def write(self):
    if self.request:
        if not self.response_created:
            self.create_response()

    self._write()
Copy the code

The write() method first checks if there is a request, and if there is and the response has not yet been created, the create_response() method is called, setting the status variable response_created, and then writing the response to the send buffer

If the send buffer has data, the write() method calls socket.send()

Remember, when socket.send() is called, all the data in the send buffer may not be in the send queue, the socket’s network buffer may be full, and socket.send() may need to be called again. This is why it is necessary to check the status. Create_response () should only be called once, but the _write() method needs to be called multiple times

Write () on the client is roughly the same as on the server:

def write(self):
    if not self._request_queued:
        self.queue_request()

    self._write()

    if self._request_queued:
        if not self._send_buffer:
            # Set selector to listen for read events, we're done writing.
            self._set_selector_events_mask('r')
Copy the code

Because the client home page initializes a connection request to the server, the status variable _request_queued is checked. If the request is not already queued, the queue_request() method is called to create a request to write to the send buffer, and the variable _request_queued is also used to record the status to prevent multiple calls

Just like the server, the _write() method calls socket.send() if there is data in the send buffer

Note that the client version of the write() method differs from the server in checking whether the last request is queued, which we’ll explain in detail in the client main program. The reason is to tell selector. Select () to stop monitoring socket write events and we’re only interested in reading events, there’s no way to tell a socket that it’s writable

I’ll leave you with a cliffhanger in this section. The main purpose of this section is to explain how the selector. Select () method calls the message class through the process_events() method and how it works

This is important because the process_events() method will be called many times during the lifetime of the connection, so to make sure that methods that can only be called once need to check either their own state variables or those in the caller’s method

Server main program

In the main application app-server.py, the host and port parameters are passed to the application through the command line:

$ ./app-server.py
usage: ./app-server.py <host> <port>
Copy the code

For example, to listen on port 65432 above the local loopback address, run the following command:

$./app-server.py 127.0.0.1 65432 Listening on ('127.0.0.1', 65432).Copy the code

The

parameter is null to listen for all IP addresses on the host

After the socket is created, a method to socket.setsockopt() is called, passing in the parameter socket.so_reuseaddr

# Avoid bind() exception: OSError: [Errno 48] Address already in use
lsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
Copy the code

This parameter is set to avoid port occupied errors. If the current application uses the same port as the previous application, you will find the connection in TIME_WAIT state

For example, if the server actively closes a connection, the server will remain in TIME_WAIT state for about two minutes, depending on your operating system. If you try to start another service within two minutes, you will get an OSError indicating that the port has been defeated. This is done to ensure that some packets in transit are handled correctly

The event loop catches all errors to keep the server running:

while True:
    events = sel.select(timeout=None)
    for key, mask in events:
        if key.data is None:
            accept_wrapper(key.fileobj)
        else:
            message = key.data
            try:
                message.process_events(mask)
            except Exception:
                print('main: error: exception for'.f'{message.addr}:\n{traceback.format_exc()}')
                message.close()
Copy the code

When the server receives a client connection, the message object is created:

def accept_wrapper(sock):
    conn, addr = sock.accept()  # Should be ready to read
    print('accepted connection from', addr)
    conn.setblocking(False)
    message = libserver.Message(sel, conn, addr)
    sel.register(conn, selectors.EVENT_READ, data=message)
Copy the code

The message object is associated with the socket through the sel.register() method, and it is initialized to monitor only read events. When the request is read, we modify it by listening for write events

One advantage of this approach on the server side is that most of the time, when the socket is healthy and there are no network problems, it is always writable

If we tell sel.register() to monitor EVENT_WRITE events, the event loop will immediately wake up and notify us of the situation, whereas the socket does not call send() with wake up. Since the request has not yet been processed, there is no need to send back a response. This consumes and wastes valuable CPU cycles

Server side message class

In the message Pointcut section, we can see how message objects emit actions when the socket event is known to be ready through process_events(). Now let’s look at what happens when data is read on the socket and what happens to the component fragments of the message ready for the server

The server message class is in the libserver.py file. The source code can be found on Github

These methods appear in the class in the order in which they are processed

The fixed-length header logic starts when the server reads at least two bytes

def process_protoheader(self):
    hdrlen = 2
    if len(self._recv_buffer) >= hdrlen:
        self._jsonheader_len = struct.unpack('>H',
                                             self._recv_buffer[:hdrlen])[0]
        self._recv_buffer = self._recv_buffer[hdrlen:]
Copy the code

The fixed-length two-byte integer in the network byte sequence contains the length of the JSON header, which is read and decoded by the struct.unpack() method and stored in self._jsonheader_len. When this part of the message is processed, The process_Protoheader () method is called to remove the processed message from the receive buffer

Like the fixed-length header logic above, it also needs to be processed when the receive buffer has enough JSON header data:

def process_jsonheader(self):
    hdrlen = self._jsonheader_len
    if len(self._recv_buffer) >= hdrlen:
        self.jsonheader = self._json_decode(self._recv_buffer[:hdrlen],
                                            'utf-8')
        self._recv_buffer = self._recv_buffer[hdrlen:]
        for reqhdr in ('byteorder'.'content-length'.'content-type'.'content-encoding') :if reqhdr not in self.jsonheader:
                raise ValueError(f'Missing required header "{reqhdr}". ')
Copy the code

The self._json_decode() method is used to decode and deserialize the JSON header into a dictionary. Since the JSON header we defined is in UTF-8 format, we write this out when we decode the method call, and the result will be stored in self.jsonHeader. After the process_jsonheader method has done what it’s supposed to do, You also need to delete processed messages from the receive buffer

Next comes the actual message content, which should be processed when the receive buffer has the number of bytes of the Content-Length value defined in the JSON header:

def process_request(self):
    content_len = self.jsonheader['content-length']
    if not len(self._recv_buffer) >= content_len:
        return
    data = self._recv_buffer[:content_len]
    self._recv_buffer = self._recv_buffer[content_len:]
    if self.jsonheader['content-type'] = ='text/json':
        encoding = self.jsonheader['content-encoding']
        self.request = self._json_decode(data, encoding)
        print('received request', repr(self.request), 'from', self.addr)
    else:
        # Binary or unknown content-type
        self.request = data
        print(f'received {self.jsonheader["content-type"]} request from',
              self.addr)
    # Set selector to listen for write events, we're done reading.
    self._set_selector_events_mask('w')
Copy the code

After saving the message to the data variable, process_request() removes the processed data from the receive buffer. Then, if the Content Type is JSON, it decodes and deserializes the data. Otherwise (in our case) the data will be treated as binary and printed out

Finally, the process_request() method changes the selector to monitor only write events. In the server application app-server.py, socket initialization is set to monitor only read events. Now that the request has been fully processed, we are not interested in reading the event

You can now create a response to write into the socket. Create_response () will be called from the write() method when the socket is writable:

def create_response(self):
    if self.jsonheader['content-type'] = ='text/json':
        response = self._create_response_json_content()
    else:
        # Binary or unknown content-type
        response = self._create_response_binary_content()
    message = self._create_message(**response)
    self.response_created = True
    self._send_buffer += message
Copy the code

The response calls a different method creation depending on the content type. In this example, a simple dictionary lookup is performed when action == ‘search’. This is where you can add your own handler and call

One tricky problem is how to close the connection when the response is written, so I’ll call close() in the _write() method

def _write(self):
    if self._send_buffer:
        print('sending', repr(self._send_buffer), 'to', self.addr)
        try:
            # Should be ready to write
            sent = self.sock.send(self._send_buffer)
        except BlockingIOError:
            # Resource temporarily unavailable (errno EWOULDBLOCK)
            pass
        else:
            self._send_buffer = self._send_buffer[sent:]
            # Close when the buffer is drained. The response has been sent.
            if sent and not self._send_buffer:
                self.close()
Copy the code

While the call to the close() method is a bit subtle, I think it’s a tradeoff. Because the message class handles only one message per connection. After writing the response, the server does not need to do anything. Its job is done

Client main program

In the main client program, app-client.py, parameters are read from the command line and used to create the request and connect to the server

$ ./app-client.py
usage: ./app-client.py <host> <port> <action> <value>
Copy the code

Here’s an example:

$./app-client.py 127.0.0.1 65432 Search needleCopy the code

When a dictionary is created from the command line argument to represent the request, the host, port, and request dictionary are passed together to start_Connection ()

def start_connection(host, port, request):
    addr = (host, port)
    print('starting connection to', addr)
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setblocking(False)
    sock.connect_ex(addr)
    events = selectors.EVENT_READ | selectors.EVENT_WRITE
    message = libclient.Message(sel, sock, addr, request)
    sel.register(sock, events, data=message)
Copy the code

The socket connection to the server is created, and the message object is passed into the request dictionary and created

As with the server, the message object is associated with the socket in the sel.register() method. However, the difference on the client side is that the socket is initialized to monitor read and write events. Once the request is written, we will change to monitor only read events

This implementation has the same benefits as the server side: no WASTED CPU life cycles. After the request is sent, we don’t care about the write event, so we don’t hold the state waiting for processing

Client message class

In the message entry Point section, we saw how message objects invoke specific actions when socket usage is ready. Now let’s look at how data on the socket is read and written, and what happens when the message is ready to be processed

The client message classes are in the libclient.py file, and the source code can be found on Github

These methods appear in the class in the order in which they are processed

The client’s first task is to queue the request:

def queue_request(self):
    content = self.request['content']
    content_type = self.request['type']
    content_encoding = self.request['encoding']
    if content_type == 'text/json':
        req = {
            'content_bytes': self._json_encode(content, content_encoding),
            'content_type': content_type,
            'content_encoding': content_encoding
        }
    else:
        req = {
            'content_bytes': content,
            'content_type': content_type,
            'content_encoding': content_encoding
        }
    message = self._create_message(**req)
    self._send_buffer += message
    self._request_queued = True
Copy the code

The dictionary used to create the request depends on the command line arguments passed in the client program app-client.py. The request dictionary is passed as arguments when the message object is created

The request message is created and appended to the send buffer, the message is sent by the _write() method, and the status parameter self._request_queued is set so that the queue_request() method is not called twice

After the request is sent, the client waits for a response from the server

The client reads and processes messages in the same way as the server. Since the response data is read from the socket, the header processing methods are called: process_protoheader() and process_jsonheader().

The final handler name differs in processing a response rather than creating it: process_response(),_process_response_json_content(), and _process_response_binary_content()

Last, but certainly not least — the final process_response() call:

def process_response(self):
    #...
    # Close when response has been processed
    self.close()
Copy the code

Wrap the message class

I’ll close my discussion of message classes by mentioning some important points about methods

Any class-triggered exception in the main program is handled by an except clause:

try:
    message.process_events(mask)
except Exception:
    print('main: error: exception for'.f'{message.addr}:\n{traceback.format_exc()}')
    message.close()
Copy the code

Notice the method message.close() on the last line

This line is important for a number of reasons, not least to ensure that the socket is closed, but also to remove the socket monitored using select() by calling message.close(), which is a very neat piece of code in the class that reduces complexity. If an exception occurs or we throw it on our own initiative, we know that the close() method will handle the aftermath

Message._read() and message._write () methods both contain some interesting things:

def _read(self):
    try:
        # Should be ready to read
        data = self.sock.recv(4096)
    except BlockingIOError:
        # Resource temporarily unavailable (errno EWOULDBLOCK)
        pass
    else:
        if data:
            self._recv_buffer += data
        else:
            raise RuntimeError('Peer closed.')
Copy the code

Note except line: except BlockingIOError:

The _write() method also has these lines, which are important because they catch temporary errors and skip them by using pass. Temporary errors occur when a socket is blocked, such as waiting for a network response or other end of the connection

By using pass to skip the exception, the select() method will be called again and we’ll have a chance to read and write data again

The client and server that run the application

After all that hard work, let’s get the program up and running and have some fun!

In this save, we pass an empty string as the value of the host parameter, which listens for all IP addresses on the server side. So THAT I can run the client program from a virtual machine on another network, I will simulate a PowerPC machine

Home page, run the server program in:

$ ./app-server.py ' ' 65432
listening on (' '.65432)
Copy the code

Now let’s run the client, pass in the search content, and see if we can see him:

$ ./app-client.py 10.01.1. 65432 search morpheus
starting connection to ('10.0.1.1'.65432)
sending b'\x00d{"byteorder": "big", "content-type": "text/json", "content-encoding": "utf-8", "content-length": 41}{"action": "search", "value": "morpheus"}' to ('10.0.1.1'.65432)
received response {'result': 'Follow the white rabbit. 🐰'} from ('10.0.1.1'.65432)
got result: Follow the white rabbit. 🐰
closing connection to ('10.0.1.1'.65432)
Copy the code

My command-line shell uses UTF-8 encoding, so the output above could be emojis

Then try to find the dog:

$ ./app-client.py 10.01.1. 65432Search 🐢 starting connection to ('10.0.1.1'.65432)
sending b'\x00d{"byteorder": "big", "content-type": "text/json", "content-encoding": "utf-8", "content-length": 37}{"action": "search", "value": "\xf0\x9f\x90\xb6"}' to ('10.0.1.1'.65432)
received response {'result': '🐾 Playing ball! 🏐 '} from ('10.0.1.1'.65432)
got result: 🐾 Playing ball! 🏐
closing connection to ('10.0.1.1'.65432)
Copy the code

Note the byte string in the request line. It’s easy to see that the puppy emoji you sent is printed as the hexadecimal string \xf0\x9f\x90\ XB6. I can use emoji to search because my command line supports UTF-8 encoding

In this example we send the network raw bytes that need to be interpreted correctly by the receiver. Is that why you had to append header information to the message and include the encoding type field

Here is the output from the server for the above two client connections:

accepted connection from ('10.0.2.2', 55340)
received request {'action': 'search'.'value': 'morpheus'} from ('10.0.2.2', 55340)
sending b'\x00g{"byteorder": "little", "content-type": "text/json", "content-encoding": "utf-8", "content-length": 43}{"result": "Follow the white rabbit. \xf0\x9f\x90\xb0"}' to ('10.0.2.2', 55340)
closing connection to ('10.0.2.2', 55340)

accepted connection from ('10.0.2.2', 55338)
received request {'action': 'search'.'value': '🐢'} from ('10.0.2.2', 55338)
sending b'\x00g{"byteorder": "little", "content-type": "text/json", "content-encoding": "utf-8", "content-length": 37}{"result": "\xf0\x9f\x90\xbe Playing ball! \xf0\x9f\x8f\x90"}' to ('10.0.2.2', 55338)
closing connection to ('10.0.2.2', 55338).Copy the code

Notice the bytes written to the client in the send line, which is the response message from the server

If the action parameter is not a search, you can also try sending binary requests to the server

$./ app.py 10.0.1.1 65432 binary πŸ˜ƒ Starting connection to ('10.0.1.1', 65432)
sending b'\x00|{"byteorder": "big", "content-type": "binary/custom-client-binary-type", "content-encoding": "binary", "content-length": 10}binary\xf0\x9f\x98\x83' to ('10.0.1.1', 65432)
received binary/custom-server-binary-type response from ('10.0.1.1', 65432)
got response: b'First 10 bytes of request: binary\xf0\x9f\x98\x83'
closing connection to ('10.0.1.1', 65432).Copy the code

Since the content-Type of the request is not text/ JSON, the server treats the content as a binary type and does not decode JSON, it simply prints the Content-type and the first 10 bytes returned to the client

$ ./app-server.py ' ' 65432
listening on (' ', 65432)
accepted connection from ('10.0.2.2', 55320)
received binary/custom-client-binary-type request from ('10.0.2.2', 55320)
sending b'\x00\x7f{"byteorder": "little", "content-type": "binary/custom-server-binary-type", "content-encoding": "binary", "content-length": 37}First 10 bytes of request: binary\xf0\x9f\x98\x83' to ('10.0.2.2', 55320)
closing connection to ('10.0.2.2', 55320).Copy the code

troubleshooting

It’s common that something doesn’t work, and you may not know what to do with it, but don’t worry, it happens to all of us. Hopefully you can fix it with the help of this tutorial, the debugger, and the almighty search engine and keep going

If that doesn’t work, your first port of call should be the Python socket module documentation. Make sure you read every method and function we use in the documentation. Again, take a look at the references section, especially the errors section

Sometimes the problem is not caused by your source code, the source code may be correct. There may be different hosts, clients, and servers. It could also be network reasons, such as routers, firewalls, or other network devices acting as middlemen

For these types of problems, additional tools are necessary. The following tools or sets may help you or at least provide some clues

pin

The ping command sends an ICMP message to check whether the host is connected to the network. It communicates directly with the TCP/IP stack on the operating system, so it runs independently of any application on the host

Here is the result of the ping command on macOS

$ping -c 3 127.0.0.1 ping 127.0.0.1 (127.0.0.1): 56 data bytes 64 bytes from 127.0.0.1: Icmp_seq =0 TTL =64 time=0.058 ms 64 bytes from 127.0.0.1: ICmp_seq =1 TTL =64 time=0.165 ms 64 bytes from 127.0.0.1: Icmp_seq =2 TTL =64 time= 0.164ms -- ping statistics -- 3 packets transmitted, 3 packets received 0.0% packet loss round-trip min/avg/ Max /stddev = 0.058/0.129/0.165/0.050msCopy the code

Note the statistics output below, which will help you troubleshoot intermittent connection problems. For example, are packets missing? How about network latency (see round-trip times for messages)

If you have a firewall between you and the host, ping requests may be blocked. Firewall administrators define rules that enforce blocking requests, mainly because they don’t want their hosts to be discovered. If this happens to your machine, be sure to add a rule that allows ICMP packets to be sent

ICMP is the protocol used by the ping command, but it’s also the protocol used by TCP and other underlying error messages, which could be the reason if you’re experiencing strange behavior or slow connections

ICMP messages are defined by type and code. Here are some important things to consider:

The ICMP type The ICMP code instructions
8 0 Print the request
0 0 Print the reply
3 0 The destination network is unreachable
3 1 The destination host is unreachable
3 2 The destination protocol is unreachable
3 3 The destination port is unreachable
3 4 Fragmentation is required, but the DF identifier is set
11 0 Loops exist on the network

Check out Path MTU Discovery for more on sharding and ICMP messages, where the problem is some of the strange behavior I mentioned earlier

netstat

We’ve seen how to use netstat to view information about sockets and their status in the “Checking socket state” section. This command can be used on macOS, Linux, and Windows

I didn’t mention the RECV-q and Send-q columns in the previous example. These columns represent the number of bytes of network buffer data in the send or receive queue, but for some reason these bytes have not been read or written by remote or local applications

In other words, those bytes from the network are still in the operating system’s queue. One reason may be that the application is cpu-bound or unable to call socket.recv() or socket.send(), or it may be due to some other network problem, such as network congestion, failure, hardware or cable problems

To reproduce the problem, see how much data I should send before the error occurs. I wrote a test client that could connect to the test server and repeatedly call socket.send(). The test server never calls socket.recv() or socket.send() to process data sent by the client, it only accepts connection requests. This causes the network buffer on the server to fill up and eventually causes an error to be reported on the client

Run the server first:

$./app-server-test.py 127.0.0.1 65432 Listening on ('127.0.0.1', 65432).Copy the code

Then run the client and see what happens:

$./app-client-test.py 127.0.0.1 65432 binarytest
error: socket.send() blocking io exception for ('127.0.0.1', 65432):
BlockingIOError(35, 'Resource temporarily unavailable')
Copy the code

The following is the result of executing with the netstat command when an error occurs:

65432 $netstat - an | grep Proto Recv - Q Send - Q Local Address Foreign Address tcp4 408300 0 127.0.0.1.65432 (state) 127.0.0.1.53225 ESTABLISHED tcp4 0 269868 127.0.0.1.53225 127.0.0.1.65432 ESTABLISHED tcp4 0 0 127.0.0.1.65432 *.* LISTENCopy the code

The first line represents the server (local port 65432)

Proto Recv -q Send -q Local Address Foreign Address (state) tcp4 408300 0 127.0.0.1.65432 127.0.0.1.53225 ESTABLISHEDCopy the code

Pay attention to the Recv – Q: 408300

The second line represents the client (remote port is 65432)

Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)
tcp4       0 269868  127.0. 01.53225.        127.0. 01.65432.        ESTABLISHED
Copy the code

Pay attention to Send – Q: 269868

Obviously, the client is trying to write bytes, but the server is not reading them. This causes the data that should be stored in the server network buffer queue to be backlogged at the receiver, and the client network buffer queue to be backlogged at the sender

windows

If you are using a Windows computer, there is a suite of tools that is definitely worth installing Windows Sysinternals

There is a tool called tcpView.exe, which is a visual Netstat tool for Windows. In addition to the address, port number, and socket status, it also displays packets sent and received and the number of bytes. Just like the Unix toolset lsof command, you can also see the process name and ID, and see more options in the menu

Wireshark

Sometimes you might want to look at what’s going on underneath the network, ignore the output of your application or external library calls, and see what’s going on underneath the network, just like a debugger, and when you need to see that, what else do you have to do

Wireshark is a tool for analyzing network protocols and capturing traffic that runs on macOS, Linux, Windows, and other operating systems. The Wireshark is a GUI program and tshark is a command-line interface (CLI) program

Flow capture is a very good method, it allows you to see the behavior of the application on the network, to collect information about how to send and receive messages, frequency, etc, you can also see how the client or server shut down/cancel the connection, or to stop response, when you need these information is very useful when troubleshooting

There are also many tutorials on how to use Wireshark and TShark in general

Here is an example of using Wireshark to capture local network data:

Here’s another result from using the tshark command:

$ tshark -i lo0 'tcp port 65432'
Capturing on 'Loopback'TCP 68 53942 β†’ 65432 [SYN] Seq=0 Win=65535 Len=0 MSS=16344 WS=32 TSval=940533635 TSecr=0 SACK_PERM=1
    2   0.000057    127.0.0.1 β†’ 127.0.0.1    TCP 68 65432 β†’ 53942 [SYN, ACK] Seq=0 Ack=1 Win=65535 Len=0 MSS=16344 WS=32 TSval=940533635 TSecr=940533635 SACK_PERM=1 3 0.000068 127.0.0.1 β†’ 127.0.0.1 TCP 56 53942 β†’  65432 [ACK] Seq=1 Ack=1 WinLen=0 TSval=940533635 TSecr=940533635 4 0.000075 127.0.0.1 β†’ 127.0.0.1 TCP 56 [TCP Window Update] 65432 β†’ 53942 [ACK] Seq=1 Ack=1 WinLen=0 TSval=940533635 TSecr=940533635 5 0.000216 127.0.0.1 β†’ 127.0.0.1 TCP 202 53942 β†’ 65432 ACK] Seq=1 Ack=1 WinTSval=940533635 TSecr=940533635 6 0.000234 127.0.0.1 β†’ 127.0.0.1 TCP 56 65432 β†’ 53942 [ACK] Seq=1 Ack=147 WinLen=0 TSval=940533635 TSecr=940533635 7 0.000627 127.0.0.1 β†’ 127.0.0.1 ACK] Seq=1 Ack=147 WinLen=148 TSval=940533635 TSecr=940533635 8 0.000649 127.0.0.1 β†’ 127.0.0.1 TCP 56 53942 β†’ 65432 [ACK] Seq=147 Ack=149 WinLen=0 TSval=940533635 TSecr=940533635 9 0.000668 127.0.0.1 β†’ 127.0.0.1 TCP 56 65432 β†’ 53942 [FIN, ACK] Seq=149 Ack=147 WinTSval=940533635 TSecr=940533635 10 0.000682 127.0.0.1 β†’ 127.0.0.1 TCP 56 53942 β†’ 65432 [ACK] Seq=147 Ack=150 WinTSval=940533635 TSecr=940533635 11 0.000687 127.0.0.1 β†’ 127.0.0.1 TCP 56 [TCP Dup ACK 6[ACK] Seq=150 ACK =147 Win=408128 Len=0 TSval=940533635 TSecr=94053363512 0.0048 127.0.0.1 β†’ 127.0.0.1 TCP 56 53942 β†’ 65432 [FIN, ACK] Seq=147 ACK =150 WinTSval=940533635 TSecr=940533635 13 0.001004 127.0.0.1 β†’ 127.0.0.1 TCP 56 65432 β†’ 53942 [ACK] Seq=150 Ack=148 Win=408128 Len=0 TSval=940533635 TSecr=940533635
^C13 packets captured
Copy the code

reference

This section mainly refers to some additional information and links to external resources

Python documentation

  • Python’s socket module
  • Python’s Socket Programming HOWTO

The error message

The following is from the Python socket module documentation:

All errors trigger exceptions, common exceptions like invalid parameter types and out of memory can be thrown; Starting with Python 3.3, errors related to socket or address semantics raise abnormal references to OSError or one of its subclasses

Abnormal | | errno constants that BlockingIOError | EWOULDBLOCK | resources temporarily unavailable, such as in non-blocking mode by using the send () method, the other is too busy not read, send queue is full, Or there is something wrong with the network OSError | EADDRINUSE | port by war, to ensure that no other process with the current program is running on the same address/port, Your server Settings SO_REUSEADDR parameter ConnectionResetError | ECONNRESET | connection is reset, process on the far side of collapse, or socket closed unexpectedly, Or have a firewall or set on the link with TimeoutError | ETIMEDOUT | operation timed out, the other party no response ConnectionRefusedError | ECONNREFUSED | connection is rejected, no program to monitor the specified port

The family of the socket address

Socket.af_inet and socket.af_inet6 are the first arguments to the socket.socket() method call, representing the address protocol family. The API uses an address that is expected to pass in the specified format parameter, depending on whether it is AF_INET or AF_INET6

The family of address agreement Address a tuple instructions
socket.AF_INET IPv4 (host, port) The host argument is an examplewww.example.comThe host name of the10.1.2.3IPv4 address
socket.AF_INET6 IPv6 (host, port, flowinfo, scopeid) Host names are the same as above, and IPv6 addresses are as follows:fe80::6203:7ab:fe88:9c23, FlowInfo and Scopeid represent C language structures respectivelysockaddr_in6In thesin6_flowinfo ε’Œ sin6_scope_idMembers of the

Note the following python socket module documentation about the host value and address tuple

For IPv4 addresses, you can use the host address in either of the following ways: The ” empty string stands for INADDR_ANY and the ‘

‘ character stands for INADDR_BROADCAST. This behavior is not compatible with IPv6, so you should avoid it if you use IPv6. The source document

I used IPv4 addresses in this tutorial, but if your machine supports IPv6 addresses, you can also try IPv6 addresses. The socket.getaddrInfo () method returns a sequence of five tuples containing all the parameters necessary to create a socket connection. The socket.getaddrInfo () method understands and processes the incoming IPv6 address and host name

In the following example, the program returns an address on port example.org 80 via a TCP connection:

>>> socket.getaddrinfo("example.org".80, proto=socket.IPPROTO_TCP)
[(<AddressFamily.AF_INET6: 10>, <SocketType.SOCK_STREAM: 1>,
 6.' ', ('2606:2800:220:1:248:1893:25c8:1946'.80.0.0)),
 (<AddressFamily.AF_INET: 2>, <SocketType.SOCK_STREAM: 1>,
 6.' ', ('93.184.216.34'.80)))Copy the code

The results may be different if IPv6 is available, and the values returned above can be used as arguments to socket.socket() and socket.connect() method calls, as shown in the Examples section of the Python Socket module documentation for client and server programs

Using host names

This section focuses on how to use a host name when using bind() and connect() or connect_ex() methods, but when you use a loopback address as a host name, it will always resolve to the address you expect. This is just the opposite of a client using a host name, which requires a DNS resolution process, such as www.example.com

The following paragraph comes from the Python Socket module documentation

If your host name is used as the host part of the IPv4/v6 socket address, the program may have unexpected results. Since Python uses the first result of a DNS lookup, the socket address will be resolved to a different address than the real IPv4/v6 address. This depends on DNS resolution and your host file configuration. To get a definitive result, use the numeric address as the source document for the value of the host parameter

Usually the loopback address localhost will be resolved to 127.0.0.1 or ::1, which your system may or may not be set up to do. IT depends on your system configuration, and as with all IT related things, there are always exceptions and there is no guarantee that localhost will be resolved to the loopback address

For example, on Linux, check out the result of man nsswitch.conf, the domain name switch configuration file, and another common macOS and Linux configuration file address: /etc/hosts, which on Windows is C:\Windows\System32\drivers\etc\hosts. The hosts file contains a static mapping table of domain names and addresses in text format

Interestingly, at the time of writing this article (June 2018), there was a draft RFC for making Localhost a real Localhost, and the discussion revolved around the use of localhost

The most important thing to understand is that when you use a host name in your application, the return address can be anything. If you have a security-sensitive application, don’t use a host name. Depending on your application and environment, this can be confusing

Note: Security considerations and best practices are always good, even if your program is not a security-sensitive application. If your application has access to the network, it should be secure and stable. This means doing at least the following:

  • There are often system software upgrades and security patches, including Python. Are you using a third party library? If so, make sure they work and are updated to the new version
  • Use dedicated or host-based firewalls whenever possible to limit connections to trusted systems
  • How is the DNS service configured? Whether you trust configuration content and its configurator
  • Be sure to clean up and validate the request data as much as possible, add test cases, and run them often, before calling to process other code

Whether you use a host name or not, your application will need to support secure connections (encrypted authorization), and you will probably use TLS, a topic that is beyond the scope of this tutorial. See python’s SSL module documentation for how to get started. Is this protocol the same security protocol your browser uses

Given the “variables” of interfaces, IP addresses, and domain name resolution, what should you do? If you don’t already have a web application review process, use these tips:

The application use advice
The service side Loopback address Use the IP address 127.0.0.1 or ::1
The service side Ethernet address Use an IP address, for example, 10.1.2.3. Use an empty string to represent all IP addresses of the local host
The client Loopback address Use the IP address 127.0.0.1 or ::1
The client Ethernet address Use a unified IP address that does not depend on domain name resolution. The host address is used only in special cases. Check the preceding security tips

For clients or servers, if you need to authorize a connection to a host, see how to use TLS

Blocking calls

If a socket function or method suspends your program, it is a blocking call, such as Accept (), connect(), send(), and recv() are all blocked and do not return immediately. Blocking calls must wait for the system call (I/O) to complete before returning. So the caller, you, will be blocked until the system call ends or the delay expires or an error occurs

Blocking socket calls can be set to non-blocking mode so that they can be returned immediately. If you want to do this, you need to refactor and redesign your application

Since the call returned directly but the data was not ready, the caller is in a state waiting for a network response and cannot complete its work, in which case the current socket’s status code errno should be socket.ewouldblock. The setblocking() method supports non-blocking mode

By default, sockets are created in blocking mode. See the explanations of the three modes in socket latency

Close the connection

The interesting thing is that it is perfectly legal for a TCP connection to be open on one end and closed on the other. This is called a TCP “half-connection.” It is up to the application to decide whether or not this keepalive state is required, and generally not. In this state, the closing party cannot send any data, it can only receive data

I’m not advocating that you take this approach, but as an example, HTTP uses a header called “Connection” to standardize whether an application is closed or kept connected. See section 6.3 of RFC 7230, HTTP Protocol (HTTP/1.1) for more: Message syntax and routing

When you are in the design application and its application layer protocol, it is a good idea to learn how to close the connection, sometimes it’s very simple and very obvious, or take some of the prototype can implement, depending on your application and how to be processed into the desired data message loop, just make sure that the socket is always closed correctly after finish the work

Byte order

Check the Wikipedia bytecode for information on how different cpus store byte sequences in memory. There is no problem with processing a single byte, but when processing multiple bytes into a single value (a four-byte integer), the byte order needs to be reversed if the other end you are communicating with uses a different byte order

Byte order is also important for character texts, which are represented as multi-byte sequences, just like Unicode. Unless you use only true and ASCII characters to control client and server implementations, use UTF-8 or a Unicode character set that supports byte order identification (BOM)

It is important to specify the encoding format explicitly in the application layer protocol. You can specify utF-8 for all text or use the “Content-encoding” header. This will prevent your program from detecting the encoding, and should be avoided as much as possible

When the data is called and stored in a file or database and there is no meta information about the data, it will try to detect how the data is encoded when it is passed to another end. For a discussion, see Wikipedia’s Unicode article, which references RFC 3629:UTF-8, a Transformation Format of ISO 10646

The utF-8 standard, RFC 3629, recommends that utF-8 be prohibited from using the marked byte order (BOM). However, if this is not possible, the big question is how to use a pattern to distinguish UTF-8 from other encoding methods without relying on BOM

Way to avoid these problems is to always store data encoding to use, in other words, if you don’t use utf-8 code or other with BOM coding try somehow to encoding is stored as metadata, and then you can on the data of additional encoding header information, tell the receiver encoding

TCP/IP uses a big-endian byte order, known as the network order. The network order is used to represent integer numbers in the underlying protocol stack, such as IP addresses and port numbers. Python’s socket module has several functions to convert such integer numbers from the network order to the host order

function instructions
socket.ntohl(x) To convert a 32-bit integer from network to host bytes is a null operation on machines with the same network and host bytes, otherwise it would be a 4-byte swap operation
socket.ntohs(x) To convert a 16-bit integer from network to host bytes is a null operation on machines with the same network and host bytes, otherwise it would be a 2-byte swap operation
socket.htonl(x) To convert a 32-bit integer from host to network bytes is a null operation on machines with the same network and host bytes, otherwise it would be a 4-byte swap operation
socket.htons(x) To convert a 16-bit integer from host to network bytes is a null operation on machines with the same network and host bytes, otherwise it would be a 2-byte swap operation

You can also use the struct module to package or unpack binary data (using formatted strings) :

import struct
network_byteorder_int = struct.pack('>H'.256)
python_int = struct.unpack('>H', network_byteorder_int)[0]
Copy the code

conclusion

We’ve covered a lot of ground in this tutorial, and networking and sockets are a big topic, so if you’re new to them, don’t be intimidated by all the rules and capital letter jargon

There are many parts to understand in order to understand how everything works. But, like Python, it starts to make sense when you take the time to understand each individual part

We looked at some of the low-level apis in the Python Socket module and saw how to use them to create client server applications. We also created a custom class as an application-layer protocol that exchanges data between different endpoints. You can use this class to quickly and easily build your own socket application

You can find the source code on Github

Congratulations on making it to the end! You can now use sockets nicely in your programs

I hope this tutorial provides you with some information, examples, or inspiration as you begin your socket programming journey

Blog post: keelii.com/2018/09/24/…