Accepting a Connection
Accept clients, read the raw bytes they send and keep your TCP server running.
Previously, our socket started listening, and the operating system began queueing connections for it. But our program never picked any of them up, so curl just timed out.
In this section, we will accept a connection, read the bytes the client sent and print them, so you can see a real HTTP request arrive at your own program. Along the way, we will also run into a classic problem with restarting servers, and fix it.
Accept one connection
We start from the file we ended with on the last page, and replace the input(...) placeholder with code that waits for a client. To accept an incoming connection, we use the accept() method on our listening socket.
Let's start by updating server.py:
import socket
HOST = "127.0.0.1"
PORT = 8000
def main() -> None:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
server.bind((HOST, PORT))
server.listen()
print(f"listening on http://{HOST}:{PORT}")
conn, addr = server.accept()
with conn:
print(f"connection from {addr}")
data = conn.recv(1024)
print(data)
if __name__ == "__main__":
main()We have introduced four new things here and removed the input() line:
server.accept()waits for an incoming connection and returns two values: a new socket for communicating with that client (conn) and the client's address (addr). If nobody connects, the program simply sits here. This is called blocking.addris a pair containing the client's IP address and port. The client's port is a temporary one that their own operating system picks for each connection, so it changes every time.conn.recv(1024)reads up to 1024 bytes that the client sent, and returns them as abytesobject.with conn:closes the client socket when execution leaves the block, just as the with statement closes our listening socket when its block ends.
Notice that we still have two separate sockets. server is our listening socket, which waits for new connections. conn is a new socket representing one particular connection.
Run the server python3 server.py and then in a second terminal use curl -v http://127.0.0.1:8000
Expected output in first terminal:
listening on http://127.0.0.1:8000
connection from ('127.0.0.1', 33952)
b'GET / HTTP/1.1\r\nHost: 127.0.0.1:8000\r\nUser-Agent: curl/8.22.0\r\nAccept: */*\r\n\r\n'That last line is a real HTTP request, received by your own code. It is the same text you saw on How a Browser Reaches Your Server, but now you can see how it really travels:
- The
b'...'means these are bytes, not a Python string. A socket only deals in bytes. - Each line ends with
\r\n, which Python shows as visible characters. - The request ends with
\r\n\r\n: a line ending, then an empty line. That is the blank line that marks the end of the headers.
Each time you use curl in the second terminal, you will see different client port for every new connection.
Curl may report an empty reply or a connection error. Don't worry about that just yet. We haven't implemented HTTP or sent any data back to the client. The server program also exits after this one connection.
Our goal here was simply to accept a TCP connection, and we have done that!
That last part reveals another problem, our server accepts only one connection and then stops. We need our server to keep running and accept more connections.
But before the last part there is another problem.
Restarting the server
Now run python3 server.py again, right away after the connection closes. You may see this:
OSError: [Errno 98] Address already in useYes, if you wait a minute, you will be able to restart the server again but not immediately.
To fix it, we are adding one more line before we bind a port:
import socket
HOST = "127.0.0.1"
PORT = 8000
def main() -> None:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((HOST, PORT))
server.listen()
print(f"listening on http://{HOST}:{PORT}")
conn, addr = server.accept()
with conn:
print(f"connection from {addr}")
data = conn.recv(1024)
print(data)
if __name__ == "__main__":
main()What does SO_REUSEADDR do?
SO_REUSEADDR tells the operating system that we are happy to bind to an address that is only being held in TIME_WAIT.
Accept Many Connections
Back to the last problem that our current program accepts only one connection and then exits. A real server needs to keep running and accept more clients. We can do that with an infinite loop.
So we are wrapping the accept() code inside an infinite loop, and catch Ctrl+C so that stopping the server is clean.
Let's modify our code:
import socket
HOST = "127.0.0.1"
PORT = 8000
def main() -> None:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((HOST, PORT))
server.listen()
print(f"listening on http://{HOST}:{PORT} (Press Ctrl+c to stop)")
try:
while True:
conn, addr = server.accept()
with conn:
print(f"connection from {addr}")
data = conn.recv(1024)
print(data)
except KeyboardInterrupt:
print("\nbye")
if __name__ == "__main__":
main()Run it again and send several requests using curl from another terminal.
Each connection will be accepted, logged, and closed. The loop then returns to accept() and waits for the next connection.
This server handles connections sequentially, it accepts one connection at a time and closes it before accepting the next. It is not yet a concurrent server.
Expected output in first terminal:
listening on http://127.0.0.1:8000 (Press Ctrl+c to stop)
connection from ('127.0.0.1', 54274)
b'GET / HTTP/1.1\r\nHost: 127.0.0.1:8000\r\nUser-Agent: curl/8.22.0\r\nAccept: */*\r\n\r\n'
connection from ('127.0.0.1', 54290)
b'GET / HTTP/1.1\r\nHost: 127.0.0.1:8000\r\nUser-Agent: curl/8.22.0\r\nAccept: */*\r\n\r\n'
connection from ('127.0.0.1', 54294)
b'GET / HTTP/1.1\r\nHost: 127.0.0.1:8000\r\nUser-Agent: curl/8.22.0\r\nAccept: */*\r\n\r\n'
^C
byeA Visit From Your Browser
With the server running, open http://127.0.0.1:8000 in your browser. The page never loads, and the browser eventually reports an error such as an empty response, because we read the request and never answered. But look at the server's terminal. A browser sends the same kind of request, with many more headers.
Expected output:
b'GET / HTTP/1.1\r\nHost: 127.0.0.1:8000\r\nUser-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:155.0) Gecko/20100101 Firefox/155.0\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Language: en-US,en;q=0.9\r\nAccept-Encoding: gzip, deflate, br, zstd\r\nSec-GPC: 1\r\nConnection: keep-alive\r\nUpgrade-Insecure-Requests: 1\r\nSec-Fetch-Dest: document\r\nSec-Fetch-Mode: navigate\r\nSec-Fetch-Site: none\r\nSec-Fetch-User: ?1\r\nPriority: u=0, i\r\n\r\n'Yours will differ. The shape is the same as curl's. A request line, headers, and the blank line at the end.
So far we have built a TCP server which can create a TCP socket, bind to a port, listen and accept connections.
We have the basic structure of a TCP server, but it still isn't an HTTP server. We can now see requests arrive, but only as a lump of bytes. It doesn't understand request methods such as GET, paths such as /, or HTTP headers.
In the next lesson, we'll start communicating over the accepted client socket: first by receiving bytes from the client, then by sending bytes back.
That's where our TCP server starts becoming an HTTP server.