Socket Programming in Python

·

4 min read

Python is a versatile programming language that can be used for various projects. In this blog post, we will discuss socket programming in python. We will cover the basics of sockets, and how to use them to create network connections between devices. We will also provide some example code for those who want to try out socket programming in python. So, if you are interested in learning more about sockets, or want to start using them in your Python projects, keep reading!

The Python socket module provides a straightforward interface to the Berkeley sockets API. But it uses Python’s object-oriented style. The socket() function returns a socket object whose methods implement the various socket system calls.

The primary socket API functions are:

  • socket()

  • .bind()

  • .listen()

  • .accept()

  • .connect()

  • .send()

  • .recv()

  • .close()

Server socket vs client socket

Sockets can mean subtly different things depending on context. There is a difference between a client socket and a server socket.

A client socket is an endpoint of a conversation. It usually is only used for one exchange or a small set of sequential exchanges, and that is it.

A server socket on the other hand is more like a switchboard operator. It doesn't send or receive any data. It only produces client sockets. Each client socket is created in response to some other client socket doing connect() to the server. Once a client socket is created, the two clients are free to communicate with each other.

An example of this distinction can be found on your browser. The web browser you operate uses client sockets exclusively. The web server that handles requests from the client uses both server sockets and client sockets.

Echo client and server

Below is the minimum example code. A server that echoes all data it receives back (servicing only one client) and a client that sends data.

Echo Server

import socket

QUEUE_SIZE = 5
BUFFER_SIZE = 4096

def serve(host, port):
    # Create a server socket
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # Bind socket to host and port
    sock.bind((host, port))

    # Officially become a server socket
    sock.listen(QUEUE_SIZE)

    # Accept connection from outside
    client_socket, addr = sock.accept()

    while True:
        data = client_socket.recv(BUFFER_SIZE)
        if not data:
            break
        client_socket.sendall(data)

    # Close the client socket
    client_socket.close()    

    # Close the server socket
    sock.close()

A server must perform the sequence socket(), bind(), listen(), accept(). The server does not send(), sendall() or recv() on the socket it is listening on but on the new socket returned by accept().

The socket() function takes two parameters: address family and socket types. The address family is usually IPv4 (socket.AF_INET) or IPv6 (socket.AF_INET6). Socket types are usually streaming sockets (socket.SOCK_STREAM) or datagram sockets (socket.SOCK_DGRAM).

The value passed to bind() depends on the socket's address family. For socket.AF_INET (IPv4), it expects a (host, port) tuple. host can be a hostname, IP address, or an empty 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.

Socket listen() can take a parameter called backlog. When a backlog is specified, it tells the system to allow a number of unaccepted connections before refusing new connections. A good number for a backlog is 5 (the typical max). If you write your code correctly, five should be plenty.

Echo Client

import socket

BUFFER_SIZE = 4096


def connect(host, port, message):
    # Create a client socket
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # Connect to a server
    sock.connect((host, port))

    # Send message to the server
    sock.sendall(bytes(message, "utf-8"))

    # Capture any information back 
    # from the server
    data = sock.recv(BUFFER_SIZE)

    # Close the connection
    sock.close()

Notes on recv() and send()

send and recv operate on network buffers. They do NOT necessarily handle all the bytes you hand them (or expect from them). They return when the network buffers have been filled (send) or emptied (recv). They then tell you how many bytes they handled. It is your responsibility to call them again until your message has been completely dealt with.

The recv() method receive data from the socket. It returns the number of bytes received, which may be less than the size of the data. It requires the bufsize parameters to be specified. It is the maximum amount of data to be received at once. As for the value of the bufsize, the documentation recommended the following:

For best match with hardware and network realities, the value of bufsize should be a relatively small power of 2, for example, 4096.

Typically we set this to 4096 or 8192.

The other side has closed the connection when a recv returns 0 bytes. You will NOT receive any more data from this connection.

The send() method also sends the number of bytes sent, which may be less than the size of the data passed in. To ensure you send all the data, use the sendall() method instead.

Conclusion

As we have seen, sockets are the fundamental building blocks of network communications. By learning how to use them in Python, we can easily add this powerful tool to our arsenal. With a little practice, you'll be able to build robust client-server applications that can communicate across networks. Thanks for reading!