Socket Programming in Python (Guide) β Real Python
Sockets and the socket API are used to send messages across a network. They provide a form of inter-process communication (IPC). The network can be a logical, local network to the computer, or one that’s physically connected to an external network, with its own connections to other networks. The obvious example is the Internet, which you connect to via your ISP.
This tutorial has three different iterations of building a socket server and client with Python:
- We’ll start the tutorial by looking at a simple socket server and client.
- Once you’ve seen the API and how things work in this initial example, we’ll look at an improved version that handles multiple connections simultaneously.
- Finally, we’ll progress to building an example server and client that functions like a full-fledged socket application, complete with its own custom header and content.
By the end of this tutorial, you’ll understand how to use the main functions and methods in Python’s socket module to write your own client-server applications. This includes showing you how to use a custom class to send messages and data between endpoints that you can build upon and utilize for your own applications.
The examples in this tutorial use Python 3.6. You can find the source code on GitHub.
Networking and sockets are large subjects. Literal volumes have been written about them. If you’re new to sockets or networking, it’s completely normal if you feel overwhelmed with all of the terms and pieces. I know I did!
Don’t be discouraged though. I’ve written this tutorial for you. As we do with Python, we can learn a little bit at a time. Use your browser’s bookmark feature and come back when you’re ready for the next section.
Let’s get started!
Background
Sockets have a long history. Their use originated with ARPANET in 1971 and later became an API in the Berkeley Software Distribution (BSD) operating system released in 1983 called Berkeley sockets.
When the Internet took off in the 1990s with the World Wide Web, so did network programming. Web servers and browsers weren’t the only applications taking advantage of newly connected networks and using sockets. Client-server applications of all types and sizes came into widespread use.
Today, although the underlying protocols used by the socket API have evolved over the years, and we’ve seen new ones, the low-level API has remained the same.
The most common type of socket applications are client-server applications, where one side acts as the server and waits for connections from clients. This is the type of application that I’ll be covering in this tutorial. More specifically, we’ll look at the socket API for Internet sockets, sometimes called Berkeley or BSD sockets. There are also Unix domain sockets, which can only be used to communicate between processes on the same host.
Socket API Overview
Python’s socket module provides an interface to the Berkeley sockets API. This is the module that we’ll use and discuss in this tutorial.
The primary socket API functions and methods in this module are:
socket()
bind()
listen()
accept()
connect()
connect_ex()
send()
recv()
close()
Python provides a convenient and consistent API that maps directly to these system calls, their C counterparts. We’ll look at how these are used together in the next section.
As part of its standard library, Python also has classes that make using these low-level socket functions easier. Although it’s not covered in this tutorial, see the socketserver module, a framework for network servers. There are also many modules available that implement higher-level Internet protocols like HTTP and SMTP. For an overview, see Internet Protocols and Support.
TCP Sockets
As you’ll see shortly, we’ll create a socket object using socket.socket()
and specify the socket type as socket.SOCK_STREAM
. When you do that, the default protocol that’s used is the Transmission Control Protocol (TCP). This is a good default and probably what you want.
Why should you use TCP? The Transmission Control Protocol (TCP):
- Is reliable: packets dropped in the network are detected and retransmitted by the sender.
- Has in-order data delivery: data is read by your application in the order it was written by the sender.
In contrast, User Datagram Protocol (UDP) sockets created with socket.SOCK_DGRAM
aren’t reliable, and data read by the receiver can be out-of-order from the sender’s writes.
Why is this important? Networks are a best-effort delivery system. There’s no guarantee that your data will reach its destination or that you’ll receive what’s been sent to you.
Network devices (for example, routers and switches), have finite bandwidth available and their own inherent system limitations. They have CPUs, memory, buses, and interface packet buffers, just like our clients and servers. TCP relieves you from having to worry about packet loss, data arriving out-of-order, and many other things that invariably happen when you’re communicating across a network.
In the diagram below, let’s look at the sequence of socket API calls and data flow for TCP:
The left-hand column represents the server. On the right-hand side is the client.
Starting in the top left-hand column, note the API calls the server makes to setup a “listening” socket:
socket()
bind()
listen()
accept()
A listening socket does just what it sounds like. It listens for connections from clients. When a client connects, the server calls accept()
to accept, or complete, the connection.
The client calls connect()
to establish a connection to the server and initiate the three-way handshake. The handshake step is important since it ensures that each side of the connection is reachable in the network, in other words that the client can reach the server and vice-versa. It may be that only one host, client or server, can reach the other.
In the middle is the round-trip section, where data is exchanged between the client and server using calls to send()
and recv()
.
At the bottom, the client and server close()
their respective sockets.
Echo Client and Server
Now that you’ve seen an overview of the socket API and how the client and server communicate, let’s create our first client and server. We’ll begin with a simple implementation. The server will simply echo whatever it receives back to the client.
Echo Server
Here’s the server, echo-server.py
:
#!/usr/bin/env python3 import socket HOST = '127.0.0.1' # Standard loopback interface address (localhost) PORT = 65432 # Port to listen on (non-privileged ports are > 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)
Note: Don’t worry about understanding everything above right now. There’s a lot going on in these few lines of code. This is just a starting point so you can see a basic server in action.
There’s a reference section at the end of this tutorial that has more information and links to additional resources. I’ll link to these and other resources throughout the tutorial.
Let’s walk through each API call and see what’s happening.
socket.socket()
creates a socket object that supports the context manager type, so you can use it in a with
statement. There’s no need to call s.close()
:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: pass # Use the socket object without calling s.close().
The arguments passed to socket()
specify the address family and socket type. AF_INET
is the Internet address family for IPv4. SOCK_STREAM
is the socket type for TCP, the protocol that will be used to transport our messages in the network.
bind()
is used to associate the socket with a specific network interface and port number:
HOST = '127.0.0.1' # Standard loopback interface address (localhost) PORT = 65432 # Port to listen on (non-privileged ports are > 1023) # ... s.bind((HOST, PORT))
The values passed to bind()
depend on the address family of the socket. In this example, we’re using socket.AF_INET
(IPv4). So it expects a 2-tuple: (host, port)
.
host
can be a hostname, IP address, or empty string. If an IP address is used, host
should be an IPv4-formatted address string. The IP address 127.0.0.1
is the standard IPv4 address for the loopback interface, so only processes on the host will be able to connect to the server. If you pass an empty string, the server will accept connections on all available IPv4 interfaces.
port
should be an integer from 1
-65535
(0
is reserved). It’s the TCP port number to accept connections on from clients. Some systems may require superuser privileges if the port is < 1024
.
Here’s a note on using hostnames with bind()
:
“If you use a hostname in the host portion of IPv4/v6 socket address, the program may show a non-deterministic behavior, as Python uses the first address returned from the DNS resolution. The socket address will be resolved differently into an actual IPv4/v6 address, depending on the results from DNS resolution and/or the host configuration. For deterministic behavior use a numeric address in host portion.” (Source)
I’ll discuss this more later in Using Hostnames, but it’s worth mentioning here. For now, just understand that when using a hostname, you could see different results depending on what’s returned from the name resolution process.
It could be anything. The first time you run your application, it might be the address 10.1.2.3
. The next time it’s a different address, 192.168.0.1
. The third time, it could be 172.16.7.8
, and so on.
Continuing with the server example, listen()
enables a server to accept()
connections. It makes it a “listening” socket:
s.listen() conn, addr = s.accept()
listen()
has a backlog
parameter. It specifies the number of unaccepted connections that the system will allow before refusing new connections. Starting in Python 3.5, it’s optional. If not specified, a default backlog
value is chosen.
If your server receives a lot of connection requests simultaneously, increasing the backlog
value may help by setting the maximum length of the queue for pending connections. The maximum value is system dependent. For example, on Linux, see /proc/sys/net/core/somaxconn
.
accept()
blocks and waits for an incoming connection. When a client connects, it returns a new socket object representing the connection and a tuple holding the address of the client. The tuple will contain (host, port)
for IPv4 connections or (host, port, flowinfo, scopeid)
for IPv6. See Socket Address Families in the reference section for details on the tuple values.
One thing that’s imperative to understand is that we now have a new socket object from accept()
. This is important since it’s the socket that you’ll use to communicate with the client. It’s distinct from the listening socket that the server is using to accept new connections:
conn, addr = s.accept() with conn: print('Connected by', addr) while True: data = conn.recv(1024) if not data: break conn.sendall(data)
After getting the client socket object conn
from accept()
, an infinite while
loop is used to loop over blocking calls to conn.recv()
. This reads whatever data the client sends and echoes it back using conn.sendall()
.
If conn.recv()
returns an empty bytes
object, b''
, then the client closed the connection and the loop is terminated. The with
statement is used with conn
to automatically close the socket at the end of the block.
Echo Client
Now let’s look at the client, echo-client.py
:
#!/usr/bin/env python3 import socket HOST = '127.0.0.1' # The server's hostname or IP address PORT = 65432 # The 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))
In comparison to the server, the client is pretty simple. It creates a socket object, connects to the server and calls s.sendall()
to send its message. Lastly, it calls s.recv()
to read the server’s reply and then prints it.
Running the Echo Client and Server
Let’s run the client and server to see how they behave and inspect what’s happening.
Note: If you’re having trouble getting the examples or your own code to run from the command line, read How Do I Make My Own Command-Line Commands Using Python? If you’re on Windows, check the Python Windows FAQ.
Open a terminal or command prompt, navigate to the directory that contains your scripts, and run the server:
$ ./echo-server.py
Your terminal will appear to hang. That’s because the server is blocked (suspended) in a call:
conn, addr = s.accept()
It’s waiting for a client connection. Now open another terminal window or command prompt and run the client:
$ ./echo-client.py Received b'Hello, world'
In the server window, you should see:
$ ./echo-server.py Connected by ('127.0.0.1', 64623)
In the output above, the server printed the addr
tuple returned from s.accept()
. This is the client’s IP address and TCP port number. The port number, 64623
, will most likely be different when you run it on your machine.
Viewing Socket State
To see the current state of sockets on your host, use netstat
. It’s available by default on macOS, Linux, and Windows.
Here’s the netstat output from macOS after starting the server:
$ 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
Notice that Local Address
is 127.0.0.1.65432
. If echo-server.py
had used HOST = ''
instead of HOST = '127.0.0.1'
, netstat would show this:
$ netstat -an Active Internet connections (including servers) Proto Recv-Q Send-Q Local Address Foreign Address (state) tcp4 0 0 *.65432 *.* LISTEN
Local Address
is *.65432
, which means all available host interfaces that support the address family will be used to accept incoming connections. In this example, in the call to socket()
, socket.AF_INET
was used (IPv4). You can see this in the Proto
column: tcp4
.
I’ve trimmed the output above to show the echo server only. You’ll likely see much more output, depending on the system you’re running it on. The things to notice are the columns Proto
, Local Address
, and (state)
. In the last example above, netstat shows the echo server is using an IPv4 TCP socket (tcp4
), on port 65432 on all interfaces (*.65432
), and it’s in the listening state (LISTEN
).
Another way to see this, along with additional helpful information, is to use lsof
(list open files). It’s available by default on macOS and can be installed on Linux using your package manager, if it’s not already:
$ lsof -i -n COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME Python 67982 nathan 3u IPv4 0xecf272 0t0 TCP *:65432 (LISTEN)
lsof
gives you the COMMAND
, PID
(process id), and USER
(user id) of open Internet sockets when used with the -i
option. Above is the echo server process.
netstat
and lsof
have a lot of options available and differ depending on the OS you’re running them on. Check the man
page or documentation for both. They’re definitely worth spending a little time with and getting to know. You’ll be rewarded. On macOS and Linux, use man netstat
and man lsof
. For Windows, use netstat /?
.
Here’s a common error you’ll see when a connection attempt is made to a port with no listening socket:
$ ./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
Either the specified port number is wrong or the server isn’t running. Or maybe there’s a firewall in the path that’s blocking the connection, which can be easy to forget about. You may also see the error Connection timed out
. Get a firewall rule added that allows the client to connect to the TCP port!
There’s a list of common errors in the reference section.
Communication Breakdown
Let’s take a closer look at how the client and server communicated with each other:
When using the loopback interface (IPv4 address 127.0.0.1
or IPv6 address ::1
), data never leaves the host or touches the external network. In the diagram above, the loopback interface is contained inside the host. This represents the internal nature of the loopback interface and that connections and data that transit it are local to the host. This is why you’ll also hear the loopback interface and IP address 127.0.0.1
or ::1
referred to as “localhost.”
Applications use the loopback interface to communicate with other processes running on the host and for security and isolation from the external network. Since it’s internal and accessible only from within the host, it’s not exposed.
You can see this in action if you have an application server that uses its own private database. If it’s not a database used by other servers, it’s probably configured to listen for connections on the loopback interface only. If this is the case, other hosts on the network can’t connect to it.
When you use an IP address other than 127.0.0.1
or ::1
in your applications, it’s probably bound to an Ethernet interface that’s connected to an external network. This is your gateway to other hosts outside of your “localhost” kingdom:
Be careful out there. It’s a nasty, cruel world. Be sure to read the section Using Hostnames before venturing from the safe confines of “localhost.” There’s a security note that applies even if you’re not using hostnames and using IP addresses only.
Handling Multiple Connections
The echo server definitely has its limitations. The biggest being that it serves only one client and then exits. The echo client has this limitation too, but there’s an additional problem. When the client makes the following call, it’s possible that s.recv()
will return only one byte, b'H'
from b'Hello, world'
:
data = s.recv(1024)
The bufsize
argument of 1024
used above is the maximum amount of data to be received at once. It doesn’t mean that recv()
will return 1024
bytes.
send()
also behaves this way. send()
returns the number of bytes sent, which may be less than the size of the data passed in. You’re responsible for checking this and calling send()
as many times as needed to send all of the data:
“Applications are responsible for checking that all data has been sent; if only some of the data was transmitted, the application needs to attempt delivery of the remaining data.” (Source)
We avoided having to do this by using sendall()
:
“Unlike send(), this method continues to send data from bytes until either all data has been sent or an error occurs. None is returned on success.” (Source)
We have two problems at this point:
- How do we handle multiple connections concurrently?
- We need to call
send()
andrecv()
until all data is sent or received.
What do we do? There are many approaches to concurrency. More recently, a popular approach is to use Asynchronous I/O. asyncio
was introduced into the standard library in Python 3.4. The traditional choice is to use threads.
The trouble with concurrency is it’s hard to get right. There are many subtleties to consider and guard against. All it takes is for one of these to manifest itself and your application may suddenly fail in not-so-subtle ways.
I don’t say this to scare you away from learning and using concurrent programming. If your application needs to scale, it’s a necessity if you want to use more than one processor or one core. However, for this tutorial, we’ll use something that’s more traditional than threads and easier to reason about. We’re going to use the granddaddy of system calls: select()
.
select()
allows you to check for I/O completion on more than one socket. So you can call select()
to see which sockets have I/O ready for reading and/or writing. But this is Python, so there’s more. We’re going to use the selectors module in the standard library so the most efficient implementation is used, regardless of the operating system we happen to be running on:
“This module allows high-level and efficient I/O multiplexing, built upon the select module primitives. Users are encouraged to use this module instead, unless they want precise control over the OS-level primitives used.” (Source)
Even though, by using select()
, we’re not able to run concurrently, depending on your workload, this approach may still be plenty fast. It depends on what your application needs to do when it services a request and the number of clients it needs to support.
asyncio
uses single-threaded cooperative multitasking and an event loop to manage tasks. With select()
, we’ll be writing our own version of an event loop, albeit more simply and synchronously. When using multiple threads, even though you have concurrency, we currently have to use the GIL with CPython and PyPy. This effectively limits the amount of work we can do in parallel anyway.
I say all of this to explain that using select()
may be a perfectly fine choice. Don’t feel like you have to use asyncio
, threads, or the latest asynchronous library. Typically, in a network application, your application is I/O bound: it could be waiting on the local network, endpoints on the other side of the network, on a disk, and so forth.
If you’re getting requests from clients that initiate CPU bound work, look at the concurrent.futures module. It contains the class ProcessPoolExecutor that uses a pool of processes to execute calls asynchronously.
If you use multiple processes, the operating system is able to schedule your Python code to run in parallel on multiple processors or cores, without the GIL. For ideas and inspiration, see the PyCon talk John Reese - Thinking Outside the GIL with AsyncIO and Multiprocessing - PyCon 2018.
In the next section, we’ll look at examples of a server and client that address these problems. They use select()
to handle multiple connections simultaneously and call send()
and recv()
as many times as needed.
Multi-Connection Client and Server
In the next two sections, we’ll create a server and client that handles multiple connections using a selector
object created from the selectors module.
Multi-Connection Server
First, let’s look at the multi-connection server, multiconn-server.py
. Here’s the first part that sets up the listening socket:
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)
The biggest difference between this server and the echo server is the call to lsock.setblocking(False)
to configure the socket in non-blocking mode. Calls made to this socket will no longer block. When it’s used with sel.select()
, as you’ll see below, we can wait for events on one or more sockets and then read and write data when it’s ready.
sel.register()
registers the socket to be monitored with sel.select()
for the events you’re interested in. For the listening socket, we want read events: selectors.EVENT_READ
.
data
is used to store whatever arbitrary data you’d like along with the socket. It’s returned when select()
returns. We’ll use data
to keep track of what’s been sent and received on the socket.
Next 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)
sel.select(timeout=None)
blocks until there are sockets ready for I/O. It returns a list of (key, events) tuples, one for each socket. key
is a SelectorKey namedtuple
that contains a fileobj
attribute. key.fileobj
is the socket object, and mask
is an event mask of the operations that are ready.
If key.data
is None
, then we know it’s from the listening socket and we need to accept()
the connection. We’ll call our own accept()
wrapper function to get the new socket object and register it with the selector. We’ll look at it in a moment.
If key.data
is not None
, then we know it’s a client socket that’s already been accepted, and we need to service it. service_connection()
is then called and passed key
and mask
, which contains everything we need to operate on the socket.
Let’s look at what our accept_wrapper()
function 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)
Since the listening socket was registered for the event selectors.EVENT_READ
, it should be ready to read. We call sock.accept()
and then immediately call conn.setblocking(False)
to put the socket in non-blocking mode.
Remember, this is the main objective in this version of the server since we don’t want it to block. If it blocks, then the entire server is stalled until it returns. Which means other sockets are left waiting. This is the dreaded “hang” state that you don’t want your server to be in.
Next, we create an object to hold the data we want included along with the socket using the class types.SimpleNamespace
. Since we want to know when the client connection is ready for reading and writing, both of those events are set using the following:
events = selectors.EVENT_READ | selectors.EVENT_WRITE
The events
mask, socket, and data objects are then passed to sel.register()
.
Now let’s look at service_connection()
to see how a client connection is handled when it’s 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:]
This is the heart of the simple multi-connection server. key
is the namedtuple
returned from select()
that contains the socket object (fileobj
) and data object. mask
contains the events that are ready.
If the socket is ready for reading, then mask & selectors.EVENT_READ
is true, and sock.recv()
is called. Any data that’s read is appended to data.outb
so it can be sent later.
Note the else:
block if no data is received:
if recv_data: data.outb += recv_data else: print('closing connection to', data.addr) sel.unregister(sock) sock.close()
This means that the client has closed their socket, so the server should too. But don’t forget to first call sel.unregister()
so it’s no longer monitored by select()
.
When the socket is ready for writing, which should always be the case for a healthy socket, any received data stored in data.outb
is echoed to the client using sock.send()
. The bytes sent are then removed from the send buffer:
data.outb = data.outb[sent:]
Multi-Connection Client
Now let’s look at the multi-connection client, multiconn-client.py
. It’s very similar to the server, but instead of listening for connections, it starts by initiating connections via 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)
num_conns
is read from the command-line, which is the number of connections to create to the server. Just like the server, each socket is set to non-blocking mode.
connect_ex()
is used instead of connect()
since connect()
would immediately raise a BlockingIOError
exception. connect_ex()
initially returns an error indicator, errno.EINPROGRESS
, instead of raising an exception while the connection is in progress. Once the connection is completed, the socket is ready for reading and writing and is returned as such by select()
.
After the socket is setup, the data we want stored with the socket is created using the class types.SimpleNamespace
. The messages the client will send to the server are copied using list(messages)
since each connection will call socket.send()
and modify the list. Everything needed to keep track of what the client needs to send, has sent and received, and the total number of bytes in the messages is stored in the object data
.
Let’s look at service_connection()
. It’s fundamentally 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:]
There’s one important difference. It keeps track of the number of bytes it’s received from the server so it can close its side of the connection. When the server detects this, it closes its side of the connection too.
Note that by doing this, the server depends on the client being well-behaved: the server expects the client to close its side of the connection when it’s done sending messages. If the client doesn’t close, the server will leave the connection open. In a real application, you may want to guard against this in your server and prevent client connections from accumulating if they don’t send a request after a certain amount of time.
Running the Multi-Connection Client and Server
Now let’s run multiconn-server.py
and multiconn-client.py
. They both use command-line arguments. You can run them without arguments to see the options.
For the server, pass a host
and port
number:
$ ./multiconn-server.py usage: ./multiconn-server.py <host> <port>
For the client, also pass the number of connections to create to the server, num_connections
:
$ ./multiconn-client.py usage: ./multiconn-client.py <host> <port> <num_connections>
Below is the server output when listening on the loopback interface 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)
Below is the client output when it creates two connections 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