TT Lab
Get started
Learn Learning paths Courses

My TCP Parcel Arrived in Pieces

Return the Last Reply after a Half-Close

Continue in TT Lab

In one line

A successful send call, the fact that the other side received the message, and the fact that the other side's work was completed are three different things.

Why this was needed

send returned 3, but the frame you meant to send was 20 bytes. If you record it as complete because there was no exception, the remaining 17 bytes are dropped. When you repeat the call yourself, you must advance only by the returned size. If send returns 0, there is no progress, so you must treat it as a termination error, and if you keep repeating, it becomes an infinite loop. If you use Python's sendall, you can hand this repetition to an API that means either everything is sent or an exception is raised.

Even so, sendall does not mean "the other side finished processing the order". Handing bytes over on the local transmission path is different from the remote program interpreting the frame. In this course, you check whether a real echo response comes back. Even an echo does not mean that work such as payment or storage has completed. Payment retries and idempotency need a separate contract.

How it works

A listening socket accepts connections. The connection socket that accept returns reads and writes data. The final checker creates a listening socket on 127.0.0.1 with port 0 and passes the port chosen by the operating system to the client. It does not assume that any particular port is free, so it is unlikely to collide with other tests. The loopback is inside this Pod, and no external internet access is needed.

The learner's handle_connection takes over the connection socket. It repeats reading one frame and replying with the same bytes. None means a normal close with no new frame. b"" is a valid message of length 0, so it builds a frame from it as is and sends it back. If you write if not payload, you confuse these two and lose the data after the empty message as well. Compare against the termination marker the API defines instead of the truthiness of the value.

The client sends several frames and then calls shutdown(SHUT_WR). It sends nothing more now, but it can keep receiving responses. The server sends the responses for the frames it already received, then meets a normal EOF and closes. Because a socket is two-way communication, "the other side has finished sending" and "I cannot respond" are not the same. If you do not understand half-close, you get a bug that drops the last response.

with sock closes the connection socket on both success and exception. It does not close the listening socket the caller owned. That is why you write into the interface which function owns the lifetime of which socket. Open file descriptors are a limited resource. Code that leaks only on exceptions looks fine in normal request tests but drains resources when failures repeat.

Send time matters too. In this course, send_frame uses the socket timeout that the caller set. send_frame itself does not set a new send budget. recv_frame restores the existing value when its own work is finished, so the server's caller must set a default timeout that fits the overall policy before handing over the connection. Implementing a receive time limit does not mean you have limited every send wait as well. The slow-consumer problem with many clients and the queue limit are covered separately in the next stage.

What it looks like in the field

A unit test can feed the parser exactly cut pieces. A real TCP test checks connect, accept, half-close, and socket reclamation, but it cannot force the size of the recv pieces. You have to keep both tests to prove both the boundary calculation and the real communication. You do not claim that this local echo lab verified TLS, authentication, internet latency, packet loss recovery, or large-scale throughput.

If you split the client's observations into "finished sending the request", "received the response header", "the response body is complete", and "received a normal EOF", you can narrow down where the failure happened. If you leave only a single log line saying the connection succeeded, you cannot tell whether the server processed the request. It is also important to get into the habit of logging only the necessary information, such as a message ID or byte length, and not unconditionally printing the actual user body. The test data in this course is our own example with no sensitive information.

If the connection drops before a response arrives, it is hard for the client to tell whether the server never did the work or did the work and only the response was lost. That is why retries in a real order system need a key that identifies the same operation and a rule for looking up the result. Sending the bytes again is simple, but not processing the work twice is a separate design. That is why, once you finish the echo program, you can connect to the API idempotency course.

The final checker runs a handler that takes care of one connection. This is not the design of a server with unlimited concurrent connections. Whether to split connections across threads or handle them with an event loop, and how to limit the wait queue and the number of connections, will be compared with real load in the next course. If you increase concurrency while the boundaries and cleanup in a single connection are wrong, the same errors show up in more complicated forms, so first get the small contract right.

What you will do in the next lab

The checker sends a small frame, an empty frame, and a UTF-8 body on one connection and half-closes the sending side. It checks that the response bytes match exactly and that the socket was closed after the server ended. The learner's functions are actually executed, and you cannot pass with a file that merely writes plausible-looking output. If the test time is exceeded, first check for an infinite wait. Files do not persist when the temporary lab session ends, so keep any code you need separately before it ends.