Un cliente TCP en Python se conecta a un servidor, envía bytes y recibe una respuesta mediante el módulo estándar socket. Este proyecto ayuda a comprender redes, direcciones IP, puertos, codificación, timeouts, protocolos y manejo de errores.
TCP proporciona una conexión orientada a flujo: entrega los bytes en orden y vuelve a transmitir información perdida. No conserva automáticamente los límites entre mensajes, por lo que la aplicación debe definir cómo termina cada solicitud y respuesta.
Qué necesitas entender antes de comenzar
- Host: nombre de dominio o dirección IP del servidor.
- Puerto: número que identifica el servicio.
- Socket: punto de comunicación entre procesos.
- Protocolo de aplicación: reglas que definen el formato de los mensajes.
La documentación oficial del módulo socket describe familias de direcciones, tipos de socket y métodos disponibles.
Crear el cliente más pequeño
import socket
HOST = "127.0.0.1"
PORT = 5000
with socket.socket(
socket.AF_INET,
socket.SOCK_STREAM,
) as client:
client.connect((HOST, PORT))
client.sendall(b"Hello server\n")
response = client.recv(1024)
print(response.decode("utf-8"))AF_INET selecciona IPv4 y SOCK_STREAM selecciona TCP. El bloque with cierra el socket al finalizar.
La guía de with en Python explica cómo los context managers administran recursos.
Enviar bytes, no strings
Los sockets transmiten bytes. Convierte texto con encode() y la respuesta con decode():
message = "Hola desde Python\n"
client.sendall(message.encode("utf-8"))
response = client.recv(1024)
text = response.decode("utf-8")Ambos extremos deben utilizar la misma codificación y el mismo protocolo. La guía de strings en Python explica Unicode, bytes y UTF-8.
Por qué utilizar sendall()
send() puede enviar solamente una parte del buffer y devolver la cantidad transmitida. sendall() continúa hasta enviar todo o producir una excepción:
payload = b"important message\n"
client.sendall(payload)Para mensajes pequeños y completos, sendall() suele ser la opción más segura.
recv() puede devolver datos parciales
TCP es un flujo. Una llamada a recv(1024) puede devolver menos bytes que el mensaje completo. Debes definir un límite, una longitud o una condición de cierre.
def receive_until_newline(sock):
chunks = []
while True:
chunk = sock.recv(1024)
if not chunk:
raise ConnectionError(
"El servidor cerró la conexión"
)
chunks.append(chunk)
if b"\n" in chunk:
break
data = b"".join(chunks)
line, _, remaining = data.partition(b"\n")
return line, remainingUn protocolo basado en líneas funciona bien para ejemplos sencillos, pero no sirve para cualquier contenido binario.
Protocolo con longitud prefijada
Otra estrategia consiste en enviar primero el tamaño:
import struct
def send_message(sock, data):
header = struct.pack("!I", len(data))
sock.sendall(header + data)El receptor lee exactamente cuatro bytes para la longitud y después la cantidad indicada.
def receive_exactly(sock, size):
chunks = []
remaining = size
while remaining:
chunk = sock.recv(remaining)
if not chunk:
raise ConnectionError(
"Conexión cerrada antes del mensaje"
)
chunks.append(chunk)
remaining -= len(chunk)
return b"".join(chunks)
def receive_message(sock):
header = receive_exactly(sock, 4)
size = struct.unpack("!I", header)[0]
return receive_exactly(sock, size)Este diseño permite transmitir texto o datos binarios sin depender de un carácter delimitador.
Configurar un timeout
import socket
with socket.create_connection(
("127.0.0.1", 5000),
timeout=5,
) as client:
client.settimeout(10)
client.sendall(b"status\n")
response = client.recv(1024)El primer timeout limita el establecimiento de la conexión. El segundo limita operaciones posteriores. Sin límites, un cliente puede quedar esperando indefinidamente.
Utilizar create_connection()
socket.create_connection() simplifica la resolución del host y prueba direcciones compatibles:
import socket
with socket.create_connection(
("example.com", 80),
timeout=5,
) as client:
request = (
"HEAD / HTTP/1.1\r\n"
"Host: example.com\r\n"
"Connection: close\r\n"
"\r\n"
)
client.sendall(request.encode("ascii"))
response = client.recv(4096)
print(response.decode("iso-8859-1"))Este ejemplo ilustra el formato básico de HTTP, pero para aplicaciones reales utiliza una biblioteca como Requests. Implementar correctamente TLS, redirecciones, compresión y parsing HTTP es mucho más complejo.
Manejar errores de red
import socket
try:
with socket.create_connection(
("127.0.0.1", 5000),
timeout=5,
) as client:
client.sendall(b"ping\n")
response = client.recv(1024)
except socket.timeout:
print("La operación superó el tiempo máximo")
except ConnectionRefusedError:
print("El servidor rechazó la conexión")
except socket.gaierror:
print("No fue posible resolver el host")
except OSError as error:
print(f"Error de red: {error}")
else:
print(response.decode("utf-8"))La guía de try y except en Python explica captura específica, else y conservación del traceback.
Crear una clase de cliente
import socket
class TcpClient:
def __init__(self, host, port, timeout=5):
self.address = (host, port)
self.timeout = timeout
def request(self, message):
data = message.encode("utf-8")
with socket.create_connection(
self.address,
timeout=self.timeout,
) as client:
client.settimeout(self.timeout)
send_message(client, data)
response = receive_message(client)
return response.decode("utf-8")
client = TcpClient("127.0.0.1", 5000)
print(client.request("status"))La guía de programación orientada a objetos en Python ayuda a organizar estado y comportamiento en clases.
Reintentos con espera
No reintentes inmediatamente de forma infinita. Limita los intentos y aumenta la espera:
import socket
import time
def connect_with_retry(address, attempts=3):
delay = 1
for attempt in range(1, attempts + 1):
try:
return socket.create_connection(
address,
timeout=5,
)
except OSError:
if attempt == attempts:
raise
time.sleep(delay)
delay *= 2Los reintentos son apropiados para fallos transitorios, no para credenciales inválidas o protocolos incompatibles.
Registrar información útil
import logging
logger = logging.getLogger(__name__)
logger.info(
"Conectando con %s:%s",
host,
port,
)Registra host, puerto, duración, cantidad de bytes y tipo de error. Evita guardar contraseñas, tokens o contenido privado. La guía de logging en Python presenta niveles, archivos y excepciones.
TCP frente a UDP
TCP ofrece conexión, orden y retransmisión. UDP envía datagramas independientes sin garantizar entrega u orden. TCP resulta adecuado para protocolos donde la integridad y secuencia importan. UDP puede ser útil cuando la latencia es prioritaria y la aplicación tolera pérdidas.
La especificación RFC 9293 define el protocolo TCP moderno y sus conceptos fundamentales.
Seguridad y cifrado
TCP no cifra el tráfico. Para datos sensibles utiliza TLS mediante bibliotecas de alto nivel o el módulo ssl. No inventes tu propio cifrado ni envíes credenciales en texto plano.
También valida tamaños antes de reservar memoria. Un servidor malicioso podría anunciar un mensaje enorme:
MAX_MESSAGE_SIZE = 1_000_000
if size > MAX_MESSAGE_SIZE:
raise ValueError("Mensaje demasiado grande")Probar el cliente
Separa las funciones de codificación y framing para probarlas sin una red real. Para integración, inicia un servidor local controlado en un puerto temporal.
La guía de Pytest para principiantes explica fixtures, asserts y organización.
Errores frecuentes
- Enviar strings sin codificarlas.
- Suponer que una llamada a
recv()devuelve el mensaje completo. - No definir un protocolo de delimitación o longitud.
- Trabajar sin timeouts.
- Ignorar que
recv()devuelveb""cuando el otro extremo cierra. - Reintentar infinitamente.
- Enviar información sensible sin TLS.
- No limitar el tamaño de los mensajes.
Conclusión
Un cliente TCP fiable necesita más que connect(), sendall() y recv(). Define un protocolo, procesa datos parciales, utiliza timeouts, captura errores específicos y limita los tamaños. Después añade registros, reintentos controlados, pruebas y TLS cuando la información sea sensible.






