Saltar al contenido
ES EN

Solved exercise: a TCP text protocol with a receive buffer in Python

Simulate a TCP server in Python that reassembles fragmented messages in a buffer, parses them and replies, with tested code, hints and typical mistakes.

Updated on

Exam Code Python Networking Exam level Tested code

Exam Code: Solved exercise: a TCP text protocol with a receive buffer in Python
Computer Networks · Final examQuestion 4 · 2.5 points · 30 min

A company wants a small configuration service called KVT (key-value over text). The client opens a TCP connection and sends text commands. The server keeps key-value pairs in memory and answers every command. You will not use sockets. Your program receives on standard input the segments exactly as successive calls to recv() would deliver them, and it must behave exactly as the real server would.

Remember that TCP is a stream of bytes, not of messages. One segment may hold half a command, several commands, or the end of one command and the start of the next. In KVT every message ends with the delimiter \r\n. To keep the input readable, in this exercise the delimiter is written as the character |. Everything between two delimiters, or between the start of the stream and the first delimiter, is one message.

Input format. The first line holds an integer N (0 ≤ N ≤ 1000), the number of segments. Then come N lines, each with the exact content of one segment (1 to 200 characters, excluding the newline). Spaces inside a segment, including leading ones, are part of the data and must not be removed. If the input is empty, treat it as N = 0.

Processing a message. First strip the spaces at both ends. If nothing is left, ignore the message and send no reply. Otherwise the first word is the command (case-insensitive), while keys are case-sensitive. The commands are: SET key value, where the value is everything after the key once the separating spaces are skipped, and may contain internal spaces (reply OK); GET key (reply VAL, a space and the value); DEL key (delete it and reply OK); COUNT (reply VAL, a space and the number of stored keys); QUIT (reply BYE; the server closes the connection).

Errors. A command that is none of the above gets ERR 400 unknown command. A known command with the wrong number of arguments gets ERR 400 arguments: SET needs a key and a value, GET and DEL exactly one key, COUNT and QUIT none. A GET or DEL on a missing key gets ERR 404.

Output format. One line per reply, in order. Finally print SUMMARY processed=P pending=Q, where P is the number of non-empty messages handled and Q the number of received characters that were never processed. If a QUIT is handled, Q counts everything after its delimiter, including any later segments, which are discarded. If the stream ends without QUIT, Q is the length of the undelimited fragment left in the buffer.

Sample input

4
SET color bl
ue|GET col
or|GET size|COUNT|DEL size|SET size
 M|GET size|QUIT|GET color|

Expected output

OK
VAL blue
ERR 404
VAL 1
ERR 404
OK
VAL M
BYE
SUMMARY processed=8 pending=10
Correct stream reassembly: a buffer kept across segments, extraction of every complete message and preservation of the trailing fragment
1.0
Parsing of each message and the semantics of SET, GET, DEL, COUNT and QUIT, including SET values with internal spaces
0.8
Error handling: unknown command, wrong number of arguments, missing key, and empty messages ignored
0.4
Exact SUMMARY line, with pending characters computed correctly both when QUIT closes the connection and when the stream simply ends
0.3

Hints

Hint 1 · Why can't I treat each line as one command?

Look at the example: the very first command arrives split across two segments. You need a variable that survives from one segment to the next, a buffer, to which you append whatever arrives. Only when the buffer contains a | do you hold a complete message you can process.

Hint 2 · How do I get several messages out of one segment?

After appending a segment to the buffer, loop while a delimiter remains: cut at the first one, process the left part and keep the right part in the buffer. str.split("|", 1) or str.partition("|") do exactly that cut. Whatever is left when the loop ends is an incomplete fragment that must wait for the next segment.

Hint 3 · How do I split off the command without breaking the SET value?

A plain split() also chops the value and loses its internal spaces. message.split(None, 2) gives you at most three pieces: command, key and the untouched rest. The number of pieces then tells you directly whether each command has the right number of arguments.

Solution

Explained solution

The solution separates two responsibilities, just as a real server would. The framing layer (function simulate) only knows how to accumulate segments and cut out complete messages. The application layer (function handle) receives one complete message and decides the reply. Each part can be reasoned about and tested on its own, and changing the delimiter does not touch the command logic.

import sys

DELIMITER = "|"


def handle(message, store):
    """Process one complete, non-empty message. Returns (reply, close)."""
    parts = message.split(None, 2)
    command = parts[0].upper()
    args = parts[1:]
    if command == "SET":
        if len(args) != 2:
            return "ERR 400 arguments", False
        store[args[0]] = args[1]
        return "OK", False
    if command in ("GET", "DEL"):
        if len(args) != 1:
            return "ERR 400 arguments", False
        key = args[0]
        if key not in store:
            return "ERR 404", False
        if command == "GET":
            return "VAL " + store[key], False
        del store[key]
        return "OK", False
    if command == "COUNT":
        if args:
            return "ERR 400 arguments", False
        return "VAL " + str(len(store)), False
    if command == "QUIT":
        if args:
            return "ERR 400 arguments", False
        return "BYE", True
    return "ERR 400 unknown command", False


def simulate(segments):
    """Reassemble the TCP stream. Returns (replies, processed, pending)."""
    store = {}
    buffer = ""
    replies = []
    processed = 0
    for i, segment in enumerate(segments):
        buffer += segment
        while DELIMITER in buffer:
            message, buffer = buffer.split(DELIMITER, 1)
            message = message.strip()
            if not message:
                continue
            reply, close = handle(message, store)
            replies.append(reply)
            processed += 1
            if close:
                discarded = sum(len(s) for s in segments[i + 1:])
                return replies, processed, len(buffer) + discarded
    return replies, processed, len(buffer)


def main():
    lines = sys.stdin.read().split("\n")
    n = int(lines[0]) if lines[0].strip() else 0
    segments = [line.rstrip("\r") for line in lines[1:1 + n]]
    replies, processed, pending = simulate(segments)
    for reply in replies:
        print(reply)
    print(f"SUMMARY processed={processed} pending={pending}")


if __name__ == "__main__":
    main()

Reading. main reads the whole input with sys.stdin.read() and splits it on newlines. It deliberately does not call strip() on the segments; only a possible trailing \r is removed. In the example, the fourth segment begins with a space that separates the key size from the value M. Stripping it would leave SET sizeM in the buffer, and the server would answer ERR 400 arguments instead of OK. If the first line is empty, n is 0, which is how the empty-input test works.

Framing. In simulate, the variable buffer is the connection state. Each segment is appended and, while a DELIMITER is present, buffer.split(DELIMITER, 1) separates the first complete message from the rest. In the example, after the first segment the buffer holds SET color bl and nothing is answered. The second segment completes SET color blue, the server replies OK, and GET col stays waiting. The third segment finishes that GET and brings three more commands, which the while loop serves without leaving the segment.

Empty messages. Here strip() is applied, to the message already cut out, because the statement requires it. A || or a message made only of spaces becomes an empty string, and continue skips it without incrementing processed. The mixed-commands test contains both cases, which is why it reports 9 processed messages even though the stream holds 11 delimiters.

Application layer. In handle, message.split(None, 2) splits on any run of whitespace into at most three pieces. For set greeting hello world it yields the command, the key and hello world with both internal spaces, which is exactly what the later GET returns. Only the command goes through upper(); the key is used as is, because keys are case-sensitive. The list args drives validation: GET a b leaves two arguments and is rejected, as are Count extra and quit now.

Closing and summary. handle returns a tuple whose second element says whether to close. When QUIT returns True, simulate computes the pending count as what remains in buffer plus the length of every later segment (segments[i + 1:]). In the example GET color| remains, 10 characters, and there are no more segments. In the early-QUIT test, SET a (5) remains plus two segments of 3 and 6 characters: 14. Without QUIT, as in the trailing-fragment test, the buffer length is returned: SET k3, 6.

Code and tests on GitLab

Step-by-step trace · generated by running the code

Stepsegmentmessagereplybuffer
10SET color bl
21SET color blue|GET col
31SET color blueOKGET col
42GET color|GET size|COUNT|DEL size|SET size
52GET colorVAL blueGET size|COUNT|DEL size|SET size
62GET sizeERR 404COUNT|DEL size|SET size
72COUNTVAL 1DEL size|SET size
82DEL sizeERR 404SET size
93SET size M|GET size|QUIT|GET color|
103SET size MOKGET size|QUIT|GET color|
113GET sizeVAL MQUIT|GET color|
123QUITBYEGET color|

Test cases

CaseInputExpected outputActual outputResult
Example: split commands and QUIT with trailing data4 SET color bl ue|GET col or|GET size|COUNT|DEL size|SET size M|GET size|QUIT|GET color|OK VAL blue ERR 404 VAL 1 ERR 404 OK VAL M BYE SUMMARY processed=8 pending=10OK VAL blue ERR 404 VAL 1 ERR 404 OK VAL M BYE SUMMARY processed=8 pending=10OK
Values with spaces, lowercase, errors and empty messages3 set greeting hello world |get greeting|PING|GET|GET a b|| | Count extra|del greeting|COUNT| QUIT|OK VAL hello world ERR 400 unknown command ERR 400 arguments ERR 400 arguments ERR 400 arguments OK VAL 0 BYE SUMMARY processed=9 pending=0OK VAL hello world ERR 400 unknown command ERR 400 arguments ERR 400 arguments ERR 400 arguments OK VAL 0 BYE SUMMARY processed=9 pending=0OK
Empty input(empty)SUMMARY processed=0 pending=0SUMMARY processed=0 pending=0OK
Stream ends with an incomplete fragment2 SET k1 v1|SET k2 v2|SET k1 new|GET k1|COU NT|DEL k2|GET k2|SET k3OK OK OK VAL new VAL 2 OK ERR 404 SUMMARY processed=7 pending=6OK OK OK VAL new VAL 2 OK ERR 404 SUMMARY processed=7 pending=6OK
Early QUIT with unread segments3 GET x|quit now|QUIT|SET a b| COUNT|ERR 404 ERR 400 arguments BYE SUMMARY processed=3 pending=14ERR 404 ERR 400 arguments BYE SUMMARY processed=3 pending=14OK

Actual outputs: code compiled with Python 3.13.16 and run in an isolated container on 30 October 2026.

Complexity

Let L be the total number of characters received and M the number of messages. Each command in the application layer costs O(message length) for the split plus O(1) on average for the dictionary access. The framing layer searches for the delimiter once per message and cuts once per message; since each split copies the rest of the buffer, a segment containing k messages costs O(k · 200) in the worst case. With segments of at most 200 characters that is a constant per segment, so the total is O(L).

There is a degenerate case worth knowing. A very long message spread over many segments makes buffer += segment copy an ever-growing string, which is O(L²) in the worst case. A real server avoids this with a bytearray and a read index, or by searching for the delimiter only in the newly arrived bytes. Memory is O(L) in the worst case for the buffer, plus O(C) for the store, where C is the total size of the stored pairs.

Common mistakes

  • Treating each segment as one complete message. That assumes TCP preserves message boundaries, and it breaks as soon as a command arrives split, like SET color bl in the example.
  • Calling segment.split("|") on each segment independently and dropping the last piece, which loses the fragment that should have been completed by the next segment.
  • Reading segments with input().strip(). Leading spaces are data: the segment  M|... in the example would glue the value onto the previous key.
  • Using an unlimited split() to parse SET, which chops the value and forces a join that loses the double internal spaces.
  • Upper-casing the whole message instead of only the command, so keys stop being case-sensitive and stored values are altered.
  • On QUIT, counting as pending only what is left in the buffer and forgetting the segments that had not been read yet.

Variants

Length-prefixed framing instead of a delimiter

The examiner may change the protocol so that each message is preceded by its length in decimal and a colon, for example 5:COUNT. The function handle does not change. In simulate, the loop gets two states: while the length is unknown, look for the : and convert what precedes it to an integer; once it is known, wait until the buffer holds at least that many characters and cut exactly there. The advantage is that the payload may contain any character, including the old delimiter.

A cap on the buffer size

Another common follow-up: if the buffer exceeds, say, 512 characters without containing a delimiter, the server replies ERR 413 and closes. It is enough to check len(buffer) after the while loop of each segment and treat the overflow as a close, exactly like QUIT, computing the pending count the same way. A client that never sends the delimiter can then no longer make the server's memory grow without bound.

On the syllabus

The relationship between the transport layer and the application layer that this exercise practises appears in the syllabus of several networking courses. At the Universidad de los Andes, ISIS3204 Infraestructura de Comunicaciones, the course description puts special emphasis on the TCP/IP stack as a real implementation of core networking concepts. The course plan for Redes de Datos at the Universidad Tecnológica Nacional (FRC) lists TCP/IP, the transport layer and the application layer among its contents.

Networking also features in the computer engineering course summaries of the Pontificia Universidad Católica del Perú, which list a Computer Networks course alongside interprocess communication, and in the programme of IIC2333 Operating Systems and Networks at the Pontificia Universidad Católica de Chile.

Article generated with AI.larebelion

Byline

· Chief editor · English edition · London

“Framing gets you the marks; deciding how much buffer you accept per connection is what keeps the server alive.”

Comentarios