A reusable interactive TCP client that connects to a listening service, sends data, and prints the reply — the counterpart to [[TCP-Server]].
A TCP client establishes a reliable, ordered byte stream to a server via the three-way handshake, then exchanges data. This project builds a small command-line client you can point at your own [[TCP-Server]] (or any authorized service) to send a line and read the response, or to run an interactive session. It is the building block behind banner grabbing, custom protocol testing, and simple network debugging.
[!warning] Authorized use only Connect only to services you own or are permitted to test. Examples target
127.0.0.1and a local test server.
- Establish a TCP connection and exchange data with a server.
- Handle partial sends and partial receives correctly.
- Apply timeouts so the client never blocks indefinitely.
- Close connections deterministically with a context manager.
- Optionally wrap the connection in TLS.
| Item | Detail |
|---|---|
| Python | 3.9 or newer |
| Dependencies | socket, ssl, argparse (standard library) |
| Privileges | None |
| Target | A server you control — pair this with the [[TCP-Server]] project |
| Network | 127.0.0.1 or a private lab network |
- CLI —
argparsesupplies host, port, timeout, and an optional one-shot message. - Connector —
socket.create_connection()resolves the host and connects with a timeout in one call. - Send/receive loop —
sendall()guarantees the full payload goes out;recv()reads the reply in chunks until the peer stops or a delimiter is seen. - Modes — one-shot (
--message) sends a single line and exits; interactive mode reads from stdin until EOF.
argparse ─▶ create_connection((host, port)) ─▶ sendall(data)
│
▼
recv() loop ─▶ stdout
tcp-client/
├── tcp_client.py # CLI client
└── tcp_server.py # local echo server for testing (see TCP-Server note)
- Connect. Create a socket, set a timeout, and connect to host and port.
- Send. Use
sendall()rather thansend()so partial writes are handled for you. - Receive. Loop on
recv()until the expected amount arrives or the peer closes — a singlerecv()is not guaranteed to return everything. - Frame messages. TCP is a byte stream with no message boundaries. Add newline delimiting or a length prefix.
- Use a context manager.
with socket.socket(...) as sock:guarantees closure on error. - Add TLS. Wrap with
ssl.create_default_context()for an encrypted variant. - CLI. Expose host, port, timeout, and message as arguments.
#!/usr/bin/env python3
"""tcp_client.py - Minimal interactive TCP client (educational).
Connect only to servers you own or are authorized to test.
"""
import argparse
import socket
import sys
def recv_all(sock: socket.socket, bufsize: int = 4096) -> bytes:
"""Read one response chunk (blocks until data or timeout)."""
return sock.recv(bufsize)
def one_shot(sock: socket.socket, message: str) -> None:
"""Send a single line and print the reply."""
sock.sendall(message.encode() + b"\n")
reply = recv_all(sock)
sys.stdout.write(reply.decode(errors="replace"))
def interactive(sock: socket.socket) -> None:
"""Read lines from stdin, send them, and print each reply until EOF."""
print("[*] Interactive mode - type a line, Ctrl-D to quit.")
for line in sys.stdin:
sock.sendall(line.encode())
reply = recv_all(sock)
if not reply:
print("[*] Server closed the connection.")
break
sys.stdout.write(reply.decode(errors="replace"))
def main() -> int:
parser = argparse.ArgumentParser(description="Interactive TCP client.")
parser.add_argument("host", help="server host (must be authorized)")
parser.add_argument("port", type=int, help="server port")
parser.add_argument("-m", "--message", help="one-shot message then exit")
parser.add_argument("--timeout", type=float, default=5.0,
help="socket timeout seconds (default: 5.0)")
args = parser.parse_args()
try:
with socket.create_connection((args.host, args.port), timeout=args.timeout) as sock:
print(f"[+] Connected to {args.host}:{args.port}")
if args.message is not None:
one_shot(sock, args.message)
else:
interactive(sock)
except (ConnectionRefusedError, socket.timeout, OSError) as exc:
print(f"[!] Connection failed: {exc}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())# In one terminal start the test server from the TCP-Server note, then:
python tcp_client.py 127.0.0.1 9000 -m "ping"[+] Connected to 127.0.0.1:9000
echo: ping
# Send a line to a local server
python3 tcp_client.py 127.0.0.1 9009 --message "hello"
# Interactive mode
python3 tcp_client.py 127.0.0.1 9009 --interactive
# TLS-wrapped connection
python3 tcp_client.py 127.0.0.1 9443 --tls$ python3 tcp_client.py 127.0.0.1 9009 --message "hello"
[*] Connecting to 127.0.0.1:9009
[+] Connected
[>] hello
[<] HELLO
[*] Connection closed cleanly
$ python3 tcp_client.py 127.0.0.1 9999 --message "hello"
[!] Connection refused by 127.0.0.1:9999
| Condition | Exception | Response |
|---|---|---|
| Nothing listening | ConnectionRefusedError |
Clear message, exit 1 |
| Host unreachable or filtered | socket.timeout |
Report the timeout value used, exit 1 |
| Peer closes mid-exchange | ConnectionResetError |
Report partial data received, exit 1 |
| Hostname does not resolve | socket.gaierror |
Report and exit 1 |
| TLS certificate invalid | ssl.SSLCertVerificationError |
Report the reason and exit — never fall back to unverified |
| User interrupt | KeyboardInterrupt |
Close the socket and exit 130 |
[!warning] Authorized use only Connect only to servers you own or are explicitly authorized to interact with.
- Plain TCP is unencrypted and unauthenticated. Anything sent is readable by anyone on the path — never send credentials over a plain socket.
- Always verify TLS certificates. Use
ssl.create_default_context(), which verifies by default. Disabling verification defeats the entire purpose of TLS. - Bound every read. An unbounded
recv()loop into a growing buffer is a memory-exhaustion denial of service if the peer is hostile. - Set a timeout on every operation — connect, send, and receive — so a malicious or broken server cannot hang your client indefinitely.
- Treat all received data as untrusted. Validate it before parsing, and never
eval()orpickle.loads()it.
# Round-trip against a local echo server
python3 -c "
import socket
s = socket.socket(); s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('127.0.0.1', 9009)); s.listen(1)
c, _ = s.accept(); c.sendall(c.recv(1024).upper()); c.close(); s.close()
" &
sleep 0.5
python3 tcp_client.py 127.0.0.1 9009 --message "hello" | grep -q HELLO && echo PASSfrom unittest.mock import MagicMock, patch
import tcp_client
def test_send_and_receive():
fake = MagicMock()
fake.recv.side_effect = [b"HELLO\n", b""]
with patch("socket.socket") as mock_socket:
mock_socket.return_value.__enter__.return_value = fake
assert tcp_client.exchange("127.0.0.1", 9009, "hello") == "HELLO"
fake.sendall.assert_called_once()Cover: a successful round trip, a refused connection, a timeout, and a peer that closes early.
- TLS — wrap the socket with
ssl.create_default_context()to speak to HTTPS/TLS services. - Non-blocking I/O — use
selectorsso you can read server data while typing in interactive mode. - Delimiter framing — read until a newline or length prefix instead of a single
recv(). - Reconnect/retry — add exponential backoff for flaky links.
- Hex/raw mode — send and display raw bytes for binary protocol testing.
| Symptom | Likely cause | Fix |
|---|---|---|
ConnectionRefusedError |
No listener on that port | Start the server first; confirm with ss -tlnp |
| Client hangs after sending | Waiting for data the server never sends | Set a receive timeout and define a framing protocol |
| Only part of the message arrives | Single recv() assumed to return everything |
Loop until the delimiter or expected length is reached |
| Only part of the message sent | send() used instead of sendall() |
send() may write fewer bytes than requested |
SSLCertVerificationError |
Self-signed certificate in the lab | Pass the lab CA via cafile= — do not disable verification |
| Works locally, fails remotely | Firewall or NAT between client and server | Test connectivity with nc -vz host port |
- [[TCP-Server]]
- [[Banner-Grabber]]
- [[Port-Scanner]]
- [[Socket-Programming-for-Networking]]
- [[Argparse-Module]]
- [[Mini-Projects/Readme|Mini-Projects]] — module index
- [[Readme|Python for Security Professionals]] — course home