Sockets
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.
Inter-process communication (IPC) refers to OS mechanism where it allows processes to manage shared data. A common example is clients and servers where clients request data and the server responds to client requests.
Communication Breakdown
When we use the loopback interface, data never leaves the host or touches the external network. The internal nature of the loopback interface shows that connections and data that transit it are local to the host. This is why we say localhost
.
Localhost IP address is probably bound on an Ethernet interface that’s connected to an external network. This is the way to connect to other hosts from localhost
.
Handling Multiple Connections
asyncio
was introduced into the standard libary of Python 3.4+. The traditional choice is to use threads
.
Concurrency is a necessity if we want to scale and use more than one processor or one core.
.select()
is even more traditional than threads
. It allows you to check for I/O completion on more than one socket. Using .select()
doesn’t allow you to run concurrently but it may be fast enough. 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. When using multiple threads, although you have concurrency, you have to use the GIL (Global Interpreter Lock) with CPython and PyPy. This effectively limits the amount of work we can do in parallel anyway.
If you use multiple processes, the OS can schedule your Python code to run in parallel on multiple processors or cores without GIL.
Error Handling
Since Python 3.3, all errors related to socket or address semantics raise OSError
. Therefore, we can catch OSError
or use timeouts.
Since we’re using TCP (socket.SOCK_STREAM
), we read from a continuous stream of bytes from the network. Unlike files, there is no f.seek()
and we cannot reposition the socket pointer. When bytes arrive at our socket, we’ll need to save them somewhere, or else we’ll have dropped them. We’ll need to read from the socket in chucks by calling .recv()
and save the data in a buffer until we’ve read enough bytes to have a complete message that makes sense to our application. TCP socket is just sending and receiving raw bytes to and from the network.
Application Layer Protocol
The format of the messages that your application will send and receive is from the application’s protocol. This way, when we read bytes with .recv()
, we need to keep up with the number of bytes that were read and message boundaries.
Option 1: Always send fixed-length messages
- Pros:
- Simple and easy
- Cons:
- Inefficient for small messages as we’d need to pad them
- Large messages don’t fit into the fixed length
Option 2: Use header
We can prefix messages with a header that includes the content length and other fields
- Pros:
- Only need to keep up with the header
- Once we know header, we can determine the length of message’s content and decide the number of bytes to read
Note on sending and receiving data via sockets
Data are sent and received in raw bytes. It could be possible that the format of data may not be native to your machine’s CPU. In this case, you’ll need to convert it to your machine’s native byte order before using it.
The byte order is referred to as a CPU’s endianness. We can avoid this issue by taking advantage of Unicode for message header and using the encoding UTF-8. Since UTF-8 uses an 8-bit encoding, there are no byte ordering issue. This applies to text header only.
How to determine the byte order of your machine
We can use sys.byteorder
in Python.
$ python -c 'import sys; print(repr(sys.byteorder))'
'little'
Application Protocol Header
The protocol header includes such as:
byteorder
: the byte order of the machinecontent-length
: the length of the content in bytescontent-type
: the type of content in the payload. Ex:text/json
orbinary/my-binary-type
content-encoding
: the encoding used by the content. Ex:uft-8
for Unicode,binary
for binary data
The headers inform the receiver about the content, and allows you to send arbitrary data while providing enough information so that the content can be decoded and interpreted correctly by the receiver.
References
I have largely referred to the following references for this topic: