The article describes:
- Examples of using blocking sockets
- Benefits of asynchronous sockets and linux epoll
- Examples of asynchronous use of sockets through epoll
- Performance issues
- Source
Introduction
Since version 2.6, Python includes an API for working with the
epoll Linux library. This article briefly demonstrates this API with Python 3 code examples.
From the translator.
I tried not to abuse English terms as much as possible. So “register / unregister” became “subscription / unsubscribe”, “print to the console” - “console output”. “Production server” decided to translate as “loaded server”, since nothing better than “production server” does not occur to me. “Thread” translated as “stream”, not “thread”.
The names of events, modes and flags decided not to translate at all, giving only a one-time example of a possible translation.
Although it is written that the code is for Python 3, everything works fine on Python 2.6.Examples of using blocking sockets
The first example is a simple Python 3.0 server that listens on port 8080 for incoming HTTP requests, displays them on the console, and sends an HTTP response message to the client.
- Line 9: Create a server socket.
- Line 10: We allow bind () to be executed on line 11 even if another program has recently listened to the same port. Without this, the program will not be able to work with the port for 1-2 minutes after the end of work with the same port in the previously launched program.
- Line 11: We hang (bind'im) server socket on port 8080 for all available IPv4 addresses of this machine.
- Line 12: We tell the server socket to start accepting incoming connections from clients.
- Line 14: The program will stop at this point until an incoming connection is received. When this happens, the server socket will create a new socket that will be used on this machine to communicate with the client. This new socket is represented by a clientconnection object, which is returned by a call to accept (). The address object contains the IP address and port number of the remote machine.
- Lines 15-17: Generate the data that will be sent to the client to complete the HTTP request. HTTP protocol is described here .
- Line 18: Display the request in the console as a validation action.
- Line 19: Sending response to client.
- Lines 20-22: Close the connection to the client as well as the listening server socket.
The official
HOWTO contains a more detailed description of socket programming in Python.
')
Example 1
Copy Source | Copy HTML- import socket
- EOL1 = b '\ n \ n'
- EOL2 = b '\ n \ r \ n'
- response = b 'HTTP / 1.0 200 OK \ r \ nDate: Mon, 1 Jan 1996 01:01:01 GMT \ r \ n'
- response + = b 'Content-Type: text / plain \ r \ nContent-Length: 13 \ r \ n \ r \ n'
- response + = b 'Hello, world!'
- serversocket = socket . socket ( socket .AF_INET, socket .SOCK_STREAM)
- serversocket.setsockopt ( socket .SOL_SOCKET, socket .SO_REUSEADDR, 1 )
- serversocket.bind (( '0.0.0.0' , 8080 ))
- serversocket.listen ( 1 )
- connectiontoclient, address = serversocket.accept ()
- request = b ''
- while EOL1 is not in request and EOL2 is not in request:
- request + = connectiontoclient.recv ( 1024 )
- print (request.decode ())
- connectiontoclient.send (response)
- connectiontoclient.close ()
- serversocket.close ()
Example 2 adds a loop in line 15 to re-process client connections that are performed before a user interrupt (for example, from the keyboard). This more clearly shows that the server socket is never used to exchange data with the client. Rather, it only accepts the connection from the client and creates a new socket, which is already used for communication.
The
finally block in lines 23-24 is needed so that the listening server socket is closed in any case, even if errors occur.
Example 2
Copy Source | Copy HTML- import socket
- EOL1 = b '\ n \ n'
- EOL2 = b '\ n \ r \ n'
- response = b 'HTTP / 1.0 200 OK \ r \ nDate: Mon, 1 Jan 1996 01:01:01 GMT \ r \ n'
- response + = b 'Content-Type: text / plain \ r \ nContent-Length: 13 \ r \ n \ r \ n'
- response + = b 'Hello, world!'
- serversocket = socket . socket ( socket .AF_INET, socket .SOCK_STREAM)
- serversocket.setsockopt ( socket .SOL_SOCKET, socket .SO_REUSEADDR, 1 )
- serversocket.bind (( '0.0.0.0' , 8080 ))
- serversocket.listen ( 1 )
- try :
- while true:
- connectiontoclient, address = serversocket.accept ()
- request = b ''
- while EOL1 is not in request and EOL2 is not in request:
- request + = connectiontoclient.recv ( 1024 )
- print ( '-' * 40 + '\ n' + request.decode () [: - 2 ])
- connectiontoclient.send (response)
- connectiontoclient.close ()
- finally :
- serversocket.close ()
Benefits of asynchronous sockets and linux epoll
The sockets shown in Example 2 are called blocking sockets, because the Python program pauses its execution until the event arrives. The
accept () call on line 16 is blocked until the connection is received from the client. The
recv () call on line 19 is blocked until receiving data from the client (or until there is no data to receive). The
send () call on line 21 is blocked before all data sent to the client is added to the Linux send queue.
When a program uses blocking sockets, it often uses a separate thread (or even process) to perform interaction with each of these sockets. The main program flow contains a listening server socket, which accepts incoming connections from clients. It accepts these connections one at a time, passing the newly created client socket to a separate thread that will interact with the client. Since each of these flows is associated with only one client, it is acceptable that network locks occur in some places. These locks do not prevent other threads from performing their tasks.
Using blocking sockets with multiple threads leads to simple code, but is associated with a
series of flaws . It is difficult to be sure of the correct sharing of streams to shared resources. And this programming style is not very effective on computers with a single CPU.
Issue C10K discusses alternative processing options for multiple concurrent sockets. One of them is to use asynchronous sockets. Such sockets are not blocked until the event arrives. On the contrary, the program performs an action on an asynchronous socket and immediately receives a notification of success or error. This information allows the program to decide how to proceed. Since asynchronous sockets are non-blocking, there is no need for multiple execution threads. All work can be done in a single thread. Such a single-threaded approach has its own problems and is not a good choice for many programs. But it can be combined with a multi-threaded approach: asynchronous sockets applied in a single stream can be used for the network component of the server, and streams can be used to access external blocking resources, such as databases.
Linux 2.6 has a number of mechanisms for managing asynchronous sockets, three of which are represented in the Python API through
select ,
poll and
epoll. epoll and
poll are better than
select , because the Python program does not need to keep track of all events of interest in the socket. Instead, you can rely on the operating system to report which events occurred on which sockets. And
epoll, in turn, is better than
poll , because it does not require the operating system to check all sockets for events of interest every time it is requested by the Python program. Rather, when requesting from Python, Linux checks if these events have occurred, and returns a list of events. So,
epoll is a more efficient and scalable mechanism for a large number (thousands) of simultaneous connections, as shown in
these graphs .
Examples of asynchronous use of sockets through epoll
Programs using epoll often work on the following principle:
- An epoll object is created.
- The epoll object is indicated to monitor certain events on certain sockets.
- The epoll object is requested on which sockets the specified events occurred since the previous poll
- Some actions are performed on these sockets.
- The epoll object is indicated to change the list of sockets and / or observable events.
- Repeat steps 3 through 5 to completion.
- The epoll object is destroyed.
Example 3 repeats the functionality of Example 2 using asynchronous sockets. The program is more complicated because one thread interacts with multiple clients in turn.
- Line 1: The select module contains epoll functionality.
- Line 13: Default blocking sockets should be used in non-blocking (asynchronous) mode.
- Line 15: Create an epoll object.
- Line 16: Subscribe to read events on the server socket. A read event occurs when the server socket accepts a connection.
- Line 19: The connection dictionary maps file descriptors (integers) to their corresponding network connection objects.
- Line 21: Query the epoll object to find out if any of the expected events have occurred. The parameter “1” indicates that we are ready to wait for events up to 1 second. If any of the events of interest occur earlier, the request will immediately return a list of these events.
- Line 22: Events are returned in a sequence of tuples ( fileno , event code ). fileno is a synonym for a file descriptor and is always an integer.
- Line 23: If a read event occurs on the server socket, then you can create a new client socket.
- Line 25: Set the new socket to non-blocking mode.
- Line 26: Subscribe to read events ( EPOLLIN ) on a new socket.
- Line 31: If a read event occurs on the client socket, then we read the new data that came from the client.
- Line 33: After receiving the request, we unsubscribe from reading events and subscribe to write events ( EPOLLOUT ). These events occur when you can send response data to a client.
- Line 34: We print the request, indicating that despite the switch between clients, the data can be gathered together and processed as a single message.
- Line 35: If a write event has occurred on the client socket, then you can try to send new data to the client.
- Lines 36-38: Sending response data in chunks at a time until the entire response is transferred to the operating system for sending.
- Line 39: After the response has been completely sent, we unsubscribe from further read or write events.
- Line 40: A shutdown call to the socket is not necessary for closing the connection explicitly. This example uses it to force the client to terminate the connection first. The shutdown call informs the client that it will no longer send or receive data and that it should close the socket for its part.
- Line 41: The HUP (hang-up, hang) event reports that the client socket has disconnected (was closed), that is, it should be closed. No need to subscribe to HUP events. They always occur on sockets that are signed in the epoll object.
- Line 42: Unsubscribe from events in this socket.
- Line 43: Close the socket.
- Lines 18-45: The try-catch block is used in this example because the program can be interrupted by the keyboard.
- Lines 46-48: Open sockets do not need to be closed, because Python closes them when the program ends. However, explicit closure is a good practice.
Example 3
Copy Source | Copy HTML- import socket, select
- EOL1 = b '\ n \ n'
- EOL2 = b '\ n \ r \ n'
- response = b 'HTTP / 1.0 200 OK \ r \ nDate: Mon, 1 Jan 1996 01:01:01 GMT \ r \ n'
- response + = b 'Content-Type: text / plain \ r \ nContent-Length: 13 \ r \ n \ r \ n'
- response + = b 'Hello, world!'
- serversocket = socket . socket ( socket .AF_INET, socket .SOCK_STREAM)
- serversocket.setsockopt ( socket .SOL_SOCKET, socket .SO_REUSEADDR, 1 )
- serversocket.bind (( '0.0.0.0' , 8080 ))
- serversocket.listen ( 1 )
- serversocket.setblocking ( 0 )
- epoll = select .epoll ()
- epoll.register (serversocket.fileno (), select .EPOLLIN)
- try :
- connections = {}; requests = {}; responses = {}
- while true:
- events = epoll.poll ( 1 )
- for fileno, event in events:
- if fileno == serversocket.fileno ():
- connection, address = serversocket.accept ()
- connection.setblocking ( 0 )
- epoll.register (connection.fileno (), select .EPOLLIN)
- connections [connection.fileno ()] = connection
- requests [connection.fileno ()] = b ''
- responses [connection.fileno ()] = response
- elif event & select .EPOLLIN:
- requests [fileno] + = connections [fileno] .recv ( 1024 )
- if EOL1 in requests [fileno] or EOL2 in requests [fileno]:
- epoll.modify (fileno, select .EPOLLOUT)
- print ( '-' * 40 + '\ n' + requests [fileno] .decode () [: - 2 ])
- elif event & select .EPOLLOUT:
- byteswritten = connections [fileno] .send (responses [fileno])
- responses [fileno] = responses [fileno] [byteswritten:]
- if len (responses [fileno]) == 0 :
- epoll.modify (fileno, 0 )
- connections [fileno] .shutdown ( socket .SHUT_RDWR)
- elif event & select .EPOLLHUP:
- epoll.unregister (fileno)
- connections [fileno] .close ()
- del connections [fileno]
- finally :
- epoll.unregister (serversocket.fileno ())
- epoll.close ()
- serversocket.close ()
epoll has two modes of operation, called edge-triggered and level-triggered. In the edge-triggered mode, a call to
epoll.poll () will return an event only after a read or write event occurs on the socket. The caller must process all the data associated with this event without having to call
epoll.poll ()
again . When data from a particular event is exhausted, additional attempts to work with the socket will lead to exceptions. On the contrary, in the level-triggered mode, repeated calls to
epoll.poll () will give repeated notifications about the events of interest until all the data associated with the events has been processed. No exceptions occur during normal operation in the level-triggered mode.
For example, suppose a server socket was signed in the
epoll object for read events. In the edge-triggered mode, the program should call
accept () to accept new connections until a
socket.error exception
occurs . In level-triggered mode, a single
accept () call can be made, and then an epoll object can be requested again for the next event in the queue.
Example 3 uses level-triggered mode, which is the default mode. Example 4 demonstrates how to use edge-triggered mode. In lines 25, 36 and 45, cycles are entered that I work until an exception occurs (or it becomes known that all data has been processed). Lines 32, 38 and 48 catch exceptions. Finally, lines 16, 28, 41, and 51 add an
EPOLLET mask, which defines an edge-triggered mode.
Example 4
Copy Source | Copy HTML- import socket, select
- EOL1 = b '\ n \ n'
- EOL2 = b '\ n \ r \ n'
- response = b 'HTTP / 1.0 200 OK \ r \ nDate: Mon, 1 Jan 1996 01:01:01 GMT \ r \ n'
- response + = b 'Content-Type: text / plain \ r \ nContent-Length: 13 \ r \ n \ r \ n'
- response + = b 'Hello, world!'
- serversocket = socket . socket ( socket .AF_INET, socket .SOCK_STREAM)
- serversocket.setsockopt ( socket .SOL_SOCKET, socket .SO_REUSEADDR, 1 )
- serversocket.bind (( '0.0.0.0' , 8080 ))
- serversocket.listen ( 1 )
- serversocket.setblocking ( 0 )
- epoll = select .epoll ()
- epoll.register (serversocket.fileno (), select .EPOLLIN | select .EPOLLET)
- try :
- connections = {}; requests = {}; responses = {}
- while true:
- events = epoll.poll ( 1 )
- for fileno, event in events:
- if fileno == serversocket.fileno ():
- try :
- while true:
- connection, address = serversocket.accept ()
- connection.setblocking ( 0 )
- epoll.register (connection.fileno (), select .EPOLLIN | select .EPOLLET)
- connections [connection.fileno ()] = connection
- requests [connection.fileno ()] = b ''
- responses [connection.fileno ()] = response
- except socket .error:
- pass
- elif event & select .EPOLLIN:
- try :
- while true:
- requests [fileno] + = connections [fileno] .recv ( 1024 )
- except socket .error:
- pass
- if EOL1 in requests [fileno] or EOL2 in requests [fileno]:
- epoll.modify (fileno, select .EPOLLOUT | select .EPOLLET)
- print ( '-' * 40 + '\ n' + requests [fileno] .decode () [: - 2 ])
- elif event & select .EPOLLOUT:
- try :
- while len (responses [fileno])> 0 :
- byteswritten = connections [fileno] .send (responses [fileno])
- responses [fileno] = responses [fileno] [byteswritten:]
- except socket .error:
- pass
- if len (responses [fileno]) == 0 :
- epoll.modify (fileno, select .EPOLLET)
- connections [fileno] .shutdown ( socket .SHUT_RDWR)
- elif event & select .EPOLLHUP:
- epoll.unregister (fileno)
- connections [fileno] .close ()
- del connections [fileno]
- finally :
- epoll.unregister (serversocket.fileno ())
- epoll.close ()
- serversocket.close ()
For all the similarities, the level-triggered mode is often used when porting applications using
select or
poll mechanisms, while the edge-triggered mode can be used by the programmer in the case when there is no need for such support for managing the state of events from the operating system.
In addition to these two modes, sockets can also be signed in the
epoll to an
EPOLLONESHOT event. When using this option, the event is correct only for a single call to
epoll.poll (), after which it is automatically removed from the list of observed events.
Performance issues
The length of the server connection queue
The 12th line of all examples shows the call to the
serversocket.listen () method. The parameter for this method is the length of the server connection queue (listen backlog). It tells the operating system about the maximum accepted number of TCP / IP connections that can be placed in the system queue before the Python program accepts them. Each time a Python program calls
accept () on a server socket, one of the connections is removed from the queue and the space freed up can be used for another incoming connection. When the queue is full, new incoming connections are silently ignored, resulting in unnecessary delays on the client side. A busy server typically handles hundreds and thousands of simultaneous connections, so a value of 1 will be inadequate. As an example, using ab for load testing the above examples with hundreds of simultaneous HTTP 1.0 clients, the queue length of less than 50 can sometimes lead to a dramatic drop in performance.
TCP settings
The
TCP_CORK option can block (bottle up) sending data until it is ready. This option, illustrated in lines 34 and 40 of Example 5, can be useful for an HTTP server using the HTTP / 1.1 pipeline.
Example 5
Copy Source | Copy HTML- import socket, select
- EOL1 = b '\ n \ n'
- EOL2 = b '\ n \ r \ n'
- response = b 'HTTP / 1.0 200 OK \ r \ nDate: Mon, 1 Jan 1996 01:01:01 GMT \ r \ n'
- response + = b 'Content-Type: text / plain \ r \ nContent-Length: 13 \ r \ n \ r \ n'
- response + = b 'Hello, world!'
- serversocket = socket . socket ( socket .AF_INET, socket .SOCK_STREAM)
- serversocket.setsockopt ( socket .SOL_SOCKET, socket .SO_REUSEADDR, 1 )
- serversocket.bind (( '0.0.0.0' , 8080 ))
- serversocket.listen ( 1 )
- serversocket.setblocking ( 0 )
- epoll = select .epoll ()
- epoll.register (serversocket.fileno (), select .EPOLLIN)
- try :
- connections = {}; requests = {}; responses = {}
- while true:
- events = epoll.poll ( 1 )
- for fileno, event in events:
- if fileno == serversocket.fileno ():
- connection, address = serversocket.accept ()
- connection.setblocking ( 0 )
- epoll.register (connection.fileno (), select .EPOLLIN)
- connections [connection.fileno ()] = connection
- requests [connection.fileno ()] = b ''
- responses [connection.fileno ()] = response
- elif event & select .EPOLLIN:
- requests [fileno] + = connections [fileno] .recv ( 1024 )
- if EOL1 in requests [fileno] or EOL2 in requests [fileno]:
- epoll.modify (fileno, select .EPOLLOUT)
- connections [fileno] .setsockopt ( socket .IPPROTO_TCP, socket .TCP_CORK, 1 )
- print ( '-' * 40 + '\ n' + requests [fileno] .decode () [: - 2 ])
- elif event & select .EPOLLOUT:
- byteswritten = connections [fileno] .send (responses [fileno])
- responses [fileno] = responses [fileno] [byteswritten:]
- if len (responses [fileno]) == 0 :
- connections [fileno] .setsockopt ( socket .IPPROTO_TCP, socket .TCP_CORK, 0 )
- epoll.modify (fileno, 0 )
- connections [fileno] .shutdown ( socket .SHUT_RDWR)
- elif event & select .EPOLLHUP:
- epoll.unregister (fileno)
- connections [fileno] .close ()
- del connections [fileno]
- finally :
- epoll.unregister (serversocket.fileno ())
- epoll.close ()
- serversocket.close ()
On the other hand, the
TCP_NODELAY option tells the system that any data passed to
socket.send () should immediately be sent to the client without buffering by the operating system. This option, illustrated in line 14 of Example 6, can be useful for SSH clients and other real-time applications.
Example 6
Copy Source | Copy HTML- import socket, select
- EOL1 = b '\ n \ n'
- EOL2 = b '\ n \ r \ n'
- response = b 'HTTP / 1.0 200 OK \ r \ nDate: Mon, 1 Jan 1996 01:01:01 GMT \ r \ n'
- response + = b 'Content-Type: text / plain \ r \ nContent-Length: 13 \ r \ n \ r \ n'
- response + = b 'Hello, world!'
- serversocket = socket . socket ( socket .AF_INET, socket .SOCK_STREAM)
- serversocket.setsockopt ( socket .SOL_SOCKET, socket .SO_REUSEADDR, 1 )
- serversocket.bind (( '0.0.0.0' , 8080 ))
- serversocket.listen ( 1 )
- serversocket.setblocking ( 0 )
- serversocket.setsockopt ( socket .IPPROTO_TCP, socket .TCP_NODELAY, 1 )
- epoll = select .epoll ()
- epoll.register (serversocket.fileno (), select .EPOLLIN)
- try :
- connections = {}; requests = {}; responses = {}
- while true:
- events = epoll.poll ( 1 )
- for fileno, event in events:
- if fileno == serversocket.fileno ():
- connection, address = serversocket.accept ()
- connection.setblocking ( 0 )
- epoll.register (connection.fileno (), select .EPOLLIN)
- connections [connection.fileno ()] = connection
- requests [connection.fileno ()] = b ''
- responses [connection.fileno ()] = response
- elif event & select .EPOLLIN:
- requests [fileno] + = connections [fileno] .recv ( 1024 )
- if EOL1 in requests [fileno] or EOL2 in requests [fileno]:
- epoll.modify (fileno, select .EPOLLOUT)
- print ( '-' * 40 + '\ n' + requests [fileno] .decode () [: - 2 ])
- elif event & select .EPOLLOUT:
- byteswritten = connections [fileno] .send (responses [fileno])
- responses [fileno] = responses [fileno] [byteswritten:]
- if len (responses [fileno]) == 0 :
- epoll.modify (fileno, 0 )
- connections [fileno] .shutdown ( socket .SHUT_RDWR)
- elif event & select .EPOLLHUP:
- epoll.unregister (fileno)
- connections [fileno] .close ()
- del connections [fileno]
- finally :
- epoll.unregister (serversocket.fileno ())
- epoll.close ()
- serversocket.close ()
Source
The examples on this page are publicly available and can be downloaded
here .
From translator
When a remote client
closes a socket, an
EPOLLIN event
arrives on the local socket, but nothing will be received when
recv is read. So moment
Copy Source | Copy HTML
elif event & select .EPOLLIN:
try :
while true:
requests [fileno] + = connections [fileno] .recv ( 1024 )
You can write this:
Copy Source | Copy HTML
elif event & select .EPOLLIN:
try :
while true:
data = connections [fileno] .recv ( 1024 )
if not data:
epoll.modify (fileno, select .EPOLLET)
connections [fileno] .shutdown ( socket .SHUT_RDWR)
else :
requests [fileno] + = data
In this case, there will be no looping when the connection is broken. Encountered a code where the gap does not occur immediately, but after several consecutive such idle positives, in order to eliminate the possibility of erroneous determination.