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

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.
Step-by-step trace · generated by running the code
| Step | segment | message | reply | buffer |
|---|---|---|---|---|
| 1 | 0 | SET color bl | ||
| 2 | 1 | SET color blue|GET col | ||
| 3 | 1 | SET color blue | OK | GET col |
| 4 | 2 | GET color|GET size|COUNT|DEL size|SET size | ||
| 5 | 2 | GET color | VAL blue | GET size|COUNT|DEL size|SET size |
| 6 | 2 | GET size | ERR 404 | COUNT|DEL size|SET size |
| 7 | 2 | COUNT | VAL 1 | DEL size|SET size |
| 8 | 2 | DEL size | ERR 404 | SET size |
| 9 | 3 | SET size M|GET size|QUIT|GET color| | ||
| 10 | 3 | SET size M | OK | GET size|QUIT|GET color| |
| 11 | 3 | GET size | VAL M | QUIT|GET color| |
| 12 | 3 | QUIT | BYE | GET color| |
Test cases
| Case | Input | Expected output | Actual output | Result |
|---|---|---|---|---|
| Example: split commands and QUIT with trailing data | 4
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=10 | OK
VAL blue
ERR 404
VAL 1
ERR 404
OK
VAL M
BYE
SUMMARY processed=8 pending=10 | OK |
| Values with spaces, lowercase, errors and empty messages | 3
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=0 | 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=0 | OK |
| Empty input | (empty) | SUMMARY processed=0 pending=0 | SUMMARY processed=0 pending=0 | OK |
| Stream ends with an incomplete fragment | 2
SET k1 v1|SET k2 v2|SET k1 new|GET k1|COU
NT|DEL k2|GET k2|SET k3 | OK
OK
OK
VAL new
VAL 2
OK
ERR 404
SUMMARY processed=7 pending=6 | OK
OK
OK
VAL new
VAL 2
OK
ERR 404
SUMMARY processed=7 pending=6 | OK |
| Early QUIT with unread segments | 3
GET x|quit now|QUIT|SET a
b|
COUNT| | ERR 404
ERR 400 arguments
BYE
SUMMARY processed=3 pending=14 | ERR 404
ERR 400 arguments
BYE
SUMMARY processed=3 pending=14 | OK |
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 blin 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 segmentM|...in the example would glue the value onto the previous key. - Using an unlimited
split()to parse SET, which chops the value and forces ajointhat 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



Publicar un comentario