Python实现TCP简单循环服务器实战Demo
本文还有配套的精品资源,点击获取
简介:TCP简单循环服务器是网络编程的入门实践项目,用于深入理解TCP协议通信机制和Socket编程基础。本Demo基于Python的socket库,实现了服务器监听、连接接受、数据收发及异常处理等核心功能,采用循环或并发方式处理多个客户端请求。通过该实例,学习者可掌握TCP三次握手、连接管理、字节流传输与消息边界处理等关键知识,为构建高可靠网络服务打下坚实基础。
TCP协议核心机制与Socket编程实战
在当今的分布式系统中,网络通信无处不在。从一个简单的网页请求到复杂的微服务调用,底层都依赖于可靠的传输协议。而 TCP(Transmission Control Protocol) 作为互联网的基石之一,正是我们构建稳定、高效应用的关键所在。
你有没有想过:当你打开浏览器访问某个网站时,数据是如何穿越层层网络最终抵达服务器并返回结果的?这背后,就是TCP在默默工作——它像一位经验丰富的快递调度员,确保每一个“包裹”都能准确送达,不丢失、不乱序、不错位。
但理解TCP并不仅仅是知道“三次握手四次挥手”这么简单。真正掌握它的精髓,意味着你能写出更健壮的服务端程序,能快速定位连接异常问题,甚至设计出高并发的通信架构。本文将带你深入这场旅程:从TCP状态机的本质讲起,剖析Socket API背后的系统行为,亲手实现一个可扩展的服务器框架,并逐步揭示如何突破性能瓶颈,迈向异步高并发时代。
准备好了吗?让我们从最基础却最容易被忽视的地方开始—— 为什么连接建立需要三次握手?
为什么是三次,而不是两次或四次?
想象一下,你在给朋友寄一封重要的信件。为了确保对方确实收到了,你会怎么做?
你可能会先发一条消息:“我要寄信了,准备好收。”
朋友回复:“我准备好了,请寄。”
然后你才真正把信寄出去。
这就是 三次交互 的意义:避免旧的、迟到的消息干扰当前会话。
在TCP中,如果只有两次握手:
- 客户端发送SYN → 服务器
- 服务器回应SYN+ACK → 客户端
此时客户端已经确认可以通信,但 服务器无法确定客户端是否真的收到了自己的响应 !万一这个ACK在网络中迷路了呢?服务器就会误以为连接已建立,但实际上客户端并没有进入ESTABLISHED状态。这种情况下,服务器可能一直在等待数据,造成资源浪费。
通过第三次握手(客户端回传ACK),服务器才能100%确认:“我的响应已经被对方收到”,从而安全地进入数据传输阶段。
🧠 小知识 :有一种叫做“SYN Flood”的攻击方式,就是利用这一点——只发SYN而不完成第三次握手,导致服务器维护大量半开连接,耗尽资源。
stateDiagram-v2
[*] --> CLOSED
CLOSED --> LISTEN : bind+listen(server)
LISTEN --> SYN_RCVD : recv SYN
SYN_RCVD --> ESTABLISHED : recv ACK
ESTABLISHED --> FIN_WAIT_1 : send FIN
FIN_WAIT_1 --> FIN_WAIT_2 : recv ACK
FIN_WAIT_2 --> TIME_WAIT : recv FIN + ACK
TIME_WAIT --> [*] : timeout
上面这张图展示了TCP连接的完整生命周期。你会发现,在断开连接时用了 四次挥手 ,比建立多了一次。这是因为在全双工通信中,每一方都需要独立关闭自己的发送通道。主动关闭方最后进入 TIME_WAIT 状态,持续一段时间(通常是2MSL,约60秒),是为了防止最后一个ACK丢失而导致对端重发FIN。如果此时立即释放端口,新的连接可能收到上一次的延迟报文,引发混乱。
所以, TIME_WAIT 不是bug,而是 一种自我保护机制 。
Socket到底是什么?别再把它当成黑盒了!
很多初学者认为Socket是一种协议,其实不然。Socket只是一个 编程接口(API) ,位于操作系统内核与应用程序之间,屏蔽了底层复杂的网络细节。
你可以把它想象成一扇门:
- 门里面是你写的代码(应用层)
- 门外是网络世界(IP/TCP层)
- 而Socket就是那扇门上的把手——你不需要知道门是怎么造的,只要学会拧把手就能进出。
graph TD
A[应用层] --> B(Socket API)
B --> C{传输层}
C --> D[TCP]
C --> E[UDP]
D --> F[网络层 - IP]
E --> F
F --> G[数据链路层]
G --> H[物理层]
如上图所示,无论是HTTP、FTP还是自定义协议,只要涉及网络通信,最终都会经过Socket进入传输层。这种设计实现了良好的解耦:你的业务逻辑只需要关注“发什么”,至于“怎么可靠送达”,交给TCP就行。
流式 vs 报文式:SOCK_STREAM 和 SOCK_DGRAM 的选择艺术
创建Socket时必须指定类型,最常见的两种是:
| 特性 | SOCK_STREAM (TCP) | SOCK_DGRAM (UDP) |
|---|---|---|
| 连接模式 | 面向连接 | 无连接 |
| 可靠性 | ✅ 保证顺序、不丢包 | ❌ 可能丢失、乱序 |
| 数据边界 | ❌ 字节流(无消息边界) | ✅ 报文边界明确 |
| 速度 | 较慢(握手+确认) | 快(直接发) |
| 典型场景 | 文件传输、数据库连接 | 视频直播、DNS查询 |
举个例子:如果你做的是实时语音通话,偶尔丢几个包没关系(反正听不清),但延迟一定要低,那就选UDP;但如果是银行转账,哪怕慢一点也绝不能出错,那必须用TCP。
Python中创建一个TCP套接字非常简单:
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
这里:
- AF_INET 表示IPv4地址族;
- SOCK_STREAM 指定使用流式套接字;
- 返回值是一个文件描述符对象,后续所有操作都基于它。
有趣的是,即使UDP是无连接的,也可以调用 connect() 来绑定远端地址。这样做之后,你就可以直接用 send() 代替 sendto() ,简化多次发送的目标指定工作。这也说明了Socket设计的高度灵活性—— 它提供了一种统一的方式来处理不同类型的通信需求 。
绑定IP和端口:五元组才是连接的唯一标识
每个TCP连接由一个 五元组 唯一确定:
(源IP, 源端口, 目标IP, 目标端口, 协议)
其中IP用来定位主机,端口号则用于区分同一台机器上的不同服务进程。比如80端口通常属于Web服务器,22端口归SSH使用。
服务器端必须显式调用 bind() 将自己的Socket与特定IP和端口关联起来:
sock.bind(('0.0.0.0', 8080))
这里的 '0.0.0.0' 表示监听所有可用网卡接口。如果你想限制只接收某个局域网内的请求,可以写成 '192.168.1.100' 。
⚠️ 注意:普通用户不能绑定<1024的端口(称为“特权端口”),除非以root权限运行。
客户端一般不需要手动绑定端口。当调用 connect() 时,操作系统会自动分配一个临时端口(ephemeral port),范围通常是49152–65535。这样多个客户端同时连接同一个服务器就不会冲突。
如果两个服务尝试绑定同一个端口,就会抛出错误:
OSError: [Errno 98] Address already in use
常见原因包括:
- 上次程序没完全退出,端口还在 TIME_WAIT 状态;
- 其他进程正在占用该端口。
解决办法是在绑定前设置 SO_REUSEADDR 选项:
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
这个选项允许立即重用处于 TIME_WAIT 的端口,非常适合开发调试时频繁重启服务的场景。
Python socket 库核心API详解:不只是函数列表
Python标准库中的 socket 模块是对Berkeley Sockets API的封装,虽然简洁,但每一步背后都有深刻的操作系统原理支撑。
socket() —— 创建套接字对象
起点永远是 socket.socket(family, type, proto) :
tcp_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
调用后,操作系统会在进程的文件描述符表中新增一项,指向内核中的网络资源结构体(如 struct socket )。此时它还不能通信,必须继续配置。
bind() —— 将套接字与本地地址绑定
只有服务器需要这一步:
server_socket.bind(('localhost', 8080))
参数可以是域名、IP字符串或空字符串。若为 '' 或 '0.0.0.0' ,表示监听所有接口;若为 '127.0.0.1' ,则仅接受本地回环连接。
失败常见原因:
- 端口被占;
- 权限不足;
- 地址格式错误。
建议加上异常捕获增强健壮性:
try:
server_socket.bind(('0.0.0.0', 8080))
except OSError as e:
print(f"Bind failed: {e}")
exit(1)
listen() —— 启动监听队列
对TCP服务器而言, listen() 标志着进入被动监听状态:
server_socket.listen(5)
这里的 backlog=5 指的是 已完成连接队列的最大长度 (不是总连接数!)
现代Linux系统中,实际生效值受限于 /proc/sys/net/core/somaxconn 。如果 backlog > somaxconn ,会被自动截断。
当客户端发起SYN请求时,连接首先进入“半打开”状态(SYN_RCVD),存入未完成连接队列;待三次握手完成后,移至已完成连接队列,等待 accept() 取出处理。
如果队列满了,新的SYN请求将被忽略,可能导致客户端超时。因此合理设置 backlog 对高并发服务至关重要。
accept() —— 阻塞等待客户端连接
accept() 从已完成连接队列中取出一个新连接,返回一个新的套接字对象:
client_conn, client_addr = server_socket.accept()
print(f"Accepted connection from {client_addr}")
关键特性:
- 返回的 client_conn 是全新的Socket,专用于与该客户端通信;
- 原始 server_socket 继续监听其他连接;
- 默认是 阻塞调用 ,直到有连接到达才返回。
这意味着单线程服务器一旦进入 accept() ,就暂停了其他任务的执行。这也是循环服务器性能瓶颈的根源。
此外, accept() 可能抛出多种异常:
- Too many open files :文件描述符耗尽;
- InterruptedError :被信号中断(可重试);
- ConnectionAbortedError :客户端在握手完成前断开。
生产环境中应结合 try-except 进行容错处理,并考虑非阻塞模式或多路复用来提升并发能力。
整个流程可以用一句话概括:
socket() → bind() → listen() → accept()
四步走完,才算真正准备好迎接第一个客户 😄
循环服务器:简单却不平凡的教学模型
现在我们来动手写一个最基本的TCP服务器—— 循环服务器(Iterative Server) 。
它的特点是“一次只处理一个客户端”。虽然效率低,但对于理解套接字编程的核心流程极具价值。
import socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(('localhost', 8080))
server_socket.listen(5)
while True:
client_conn, client_addr = server_socket.accept()
print(f"Connected by {client_addr}")
while True:
data = client_conn.recv(1024)
if not data:
break
client_conn.sendall(data.upper()) # 示例:转大写回传
client_conn.close()
逐行解读:
-
socket.socket(...):创建IPv4 TCP套接字; -
bind():绑定本地地址和端口; -
listen(5):启动监听,参数5表示最大未完成连接队列长度; -
accept():阻塞直到收到三次握手完成的连接; -
recv(1024):尝试接收最多1024字节数据;若返回空字符串(b''),表示对方已关闭连接; -
sendall():确保所有数据都被发送出去,内部会自动重试; -
close():关闭连接套接字,触发四次挥手。
这个结构体现了同步阻塞I/O的基本范式,适用于低负载或测试用途。
阻塞式I/O的表现形式
所谓“阻塞”,是指某个系统调用无法立即完成时,进程会被挂起,直到条件满足才继续执行。
在上述代码中,有两个关键阻塞点:
| 系统调用 | 阻塞原因 | 触发条件 |
|---|---|---|
accept() | 没有新连接 | 监听队列为空 |
recv() | 客户端未发数据 | 接收缓冲区为空且连接仍打开 |
这就决定了服务器在同一时间只能服务一个客户端。例如,若某客户端连接后长时间不发数据,则 recv() 一直阻塞,其他客户端无法接入。
graph TD
A[开始主循环] --> B{是否有新连接?}
B -- 否 --> B
B -- 是 --> C[accept()获取client_conn]
C --> D{是否收到数据?}
D -- 否 --> E[阻塞等待recv()]
D -- 是 --> F[处理数据并sendall()]
F --> G{客户端是否关闭?}
G -- 否 --> D
G -- 是 --> H[关闭client_conn]
H --> A
这张流程图清晰展示了服务器的状态迁移过程:连接建立 → 数据交互 → 连接终止 → 回到初始状态。
客户端串行处理的时间线分析
假设Client A在t=0s发起连接,持续发送数据到t=10s断开;Client B在t=5s发起连接。
| 时间 (秒) | 事件描述 | 服务器状态 |
|---|---|---|
| 0 | Client A 发起连接 | accept() 返回,开始处理A |
| 1~9 | Client A 持续发送数据 | recv() 正常读取, sendall() 回应 |
| 5 | Client B 发起连接 | 内核队列缓存B的连接,服务器仍在处理A |
| 10 | Client A 关闭连接 | recv() 返回0,关闭A连接 |
| 10+ε | accept() 返回Client B连接 | 开始处理B |
| 11 | Client B 发送数据 | 正常回应 |
尽管Client B的三次握手早已完成,但由于应用层是单线程串行处理,它必须等到A彻底结束才能被响应。
这种“先来先服务、全程独占”的特性使得循环服务器不适合生产环境,但它为我们后续引入多进程/多线程模型提供了绝佳的对比基准。
解决粘包问题:TCP字节流的边界困境
TCP是面向字节流的协议,没有天然的消息边界。这意味着:
发送端调用
send()多次的数据,接收端可能一次性收到,也可能分多次接收。
这就是著名的“ 粘包 ”问题。
举个例子:
# 客户端
sock.send(b"Hello")
time.sleep(0.1)
sock.send(b"World")
服务端调用一次 recv(1024) ,可能得到:
- b"HelloWorld" (粘在一起)
- b"H" (只收到部分)
- b"Hello" (刚好一次)
因为TCP只保证字节顺序,不保证每次 send 对应一次 recv 。
recv()返回值的三种情况
| 返回值 | 含义 | 应对策略 |
|---|---|---|
> 0 | 收到指定长度数据 | 继续处理或拼接 |
0 | 对端调用 close() ,连接关闭 | 停止读取,关闭本端套接字 |
-1 或抛出异常 | 出现错误(如EAGAIN/EWOULDBLOCK) | 根据错误类型决定重试或断开 |
正确处理逻辑如下:
while True:
try:
data = client_conn.recv(1024)
if len(data) == 0:
print("Client disconnected")
break
# 处理data...
except socket.error as e:
print(f"Recv error: {e}")
break
如何定义消息边界?三种主流方案
要解决粘包,必须在应用层定义消息格式。
方案一:定长消息
每条消息固定长度(如100字节),不足补空。
优点:实现简单;
缺点:浪费带宽。
适合实时音视频帧传输等固定大小场景。
方案二:分隔符分割
使用特殊字符(如
)标记消息结尾。
# 发送
sock.send(b"Hello
")
# 接收
buffer = b''
while True:
chunk = sock.recv(1024)
if not chunk:
break
buffer += chunk
if b'
' in buffer:
msg, _, buffer = buffer.partition(b'
')
print(msg.decode())
适用于文本协议(如HTTP、SMTP、Redis)。
方案三:长度前缀法(推荐)
在消息前加上4字节的长度字段(网络字节序):
import struct
# 发送
payload = b"Hello World"
header = struct.pack('!I', len(payload)) # 大端整数
sock.send(header + payload)
# 接收
def recv_exact(sock, n):
data = b''
while len(data) < n:
chunk = sock.recv(n - len(data))
if not chunk:
raise EOFError
data += chunk
return data
# 先读4字节头
header = recv_exact(sock, 4)
length = struct.unpack('!I', header)[0]
body = recv_exact(sock, length)
print(body)
此方法高效、通用,广泛应用于gRPC、Kafka、WebSocket等协议中。
| 方法 | 边界明确 | 带宽利用率 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|
| 定长 | ✅ | ❌(冗余填充) | ✅ | 实时音视频帧传输 |
| 分隔符 | ✅(依赖约定字符) | ✅ | ✅✅ | 文本协议(Telnet、Redis) |
| 长度前缀 | ✅✅ | ✅✅ | ✅✅✅ | 二进制协议(gRPC、Kafka) |
对于通用服务器框架, 强烈推荐优先实现长度前缀方案 。
异常处理与连接稳定性保障:别让一个小错误拖垮整个服务
你以为写了 recv() 和 send() 就万事大吉了吗?现实远比理想残酷得多。
网络环境复杂多变,客户端可能随时崩溃、断网、主动关闭或长时间无响应。若服务器缺乏完善的异常处理机制,轻则资源泄漏,重则进程崩溃。
客户端非正常断开的识别
当客户端意外终止时,服务器并不会立刻感知。只有当下次I/O操作发生时才会暴露问题:
- 写操作失败 :
send()抛出BrokenPipeError(errno=EPIPE) - 读操作返回0 :
recv()返回空bytes,表明对端关闭了写方向 - 读操作阻塞 :若对端既不发FIN也不发数据,且无keep-alive,则
recv()无限期等待
为此,需结合多种手段综合判断:
stateDiagram-v2
[*] --> Connected
Connected --> DataTransfer: send()/recv()
DataTransfer --> ClientCrash: 客户端崩溃/断网
ClientCrash --> ServerWriteAttempt: 服务器尝试写入
ServerWriteAttempt --> EPIPE_Error: write() → EPIPE
ClientCrash --> ServerReadAttempt: 服务器尝试读取
ServerReadAttempt --> HangIndefinitely: recv() 阻塞
ServerReadAttempt --> RSTReceived: 内核收到RST → ConnectionResetError
由此可见,被动等待读事件不足以及时发现连接失效,必须辅以主动探测机制。
设置超时防止长时间挂起
由于TCP默认不设读写超时, recv() 可能永远阻塞。解决办法是使用 settimeout() :
sock.settimeout(30.0) # 30秒超时
try:
data = sock.recv(1024)
except socket.timeout:
print("Receive timed out - possible network issue or idle client.")
这能让服务器主动判定客户端“失活”,从而释放资源或发起重连。
使用上下文管理器确保资源释放
每个Socket都关联一个文件描述符(fd),操作系统对每个进程的fd数量有限制(Linux默认1024)。若不及时关闭,会导致“Too many open files”错误。
最佳实践是使用 with 语句或 finally 块:
def handle_client(client_sock):
try:
while True:
data = client_sock.recv(1024)
if not data:
break
client_sock.send(data.upper())
except Exception as e:
print(f"Error: {e}")
finally:
client_sock.close() # 必须在这里关闭!
或者更优雅地使用上下文管理器:
from contextlib import closing
with closing(client_sock):
client_sock.settimeout(60)
try:
while True:
data = client_sock.recv(1024)
if not data:
break
client_sock.send(data)
except (ConnectionResetError, socket.timeout):
pass
closing() 确保无论是否异常, close() 都会被执行。
心跳机制:让死连接无所遁形
为了让服务器主动发现空闲无效连接,我们可以启用两种心跳机制。
TCP层keep-alive
内核内置的探测功能,默认2小时才开始探测,显然太久了。可通过修改以下参数调整:
| 参数 | 默认值 | 含义 |
|---|---|---|
tcp_keepalive_time | 7200秒 | 第一次探测前的空闲时间 |
tcp_keepalive_intvl | 75秒 | 探测间隔 |
tcp_keepalive_probes | 9 | 最大探测次数 |
修改方法(Linux):
echo 1200 > /proc/sys/net/ipv4/tcp_keepalive_time
echo 60 > /proc/sys/net/ipv4/tcp_keepalive_intvl
echo 3 > /proc/sys/net/ipv4/tcp_keepalive_probes
应用层启用:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
应用层PING/PONG协议(更灵活)
相比TCP层,应用层心跳更可控:
import json
import time
HEARTBEAT_INTERVAL = 30
INACTIVITY_TIMEOUT = 90
def is_heartbeat(data):
try:
msg = json.loads(data.decode())
return msg.get("type") in ("ping", "pong")
except:
return False
def handle_with_heartbeat(client_sock):
last_seen = time.time()
client_sock.settimeout(10)
while True:
try:
data = client_sock.recv(1024)
if not data:
break
last_seen = time.time()
if is_heartbeat(data):
if '"ping"' in data.decode():
client_sock.send(json.dumps({"type": "pong"}).encode())
else:
client_sock.send(b"Echo: " + data)
except socket.timeout:
if time.time() - last_seen > INACTIVITY_TIMEOUT:
print("Client inactive too long, closing.")
break
except:
break
client_sock.close()
客户端定期发送PING:
def start_heartbeat(sock, interval=30):
def heartbeat_loop():
while True:
try:
sock.send(json.dumps({"type": "ping"}).encode())
time.sleep(interval)
except:
break
thread = threading.Thread(target=heartbeat_loop, daemon=True)
thread.start()
从理论到实践:完整TCP服务器代码剖析
前面说了这么多,不如直接看一个完整的、可运行的服务器实现。
import socket
import argparse
import logging
import sys
from typing import Optional
# 全局常量
HOST = '0.0.0.0'
PORT = 8888
BUFFER_SIZE = 4096
LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s'
def parse_args():
parser = argparse.ArgumentParser(description="Simple TCP Echo Server")
parser.add_argument('--host', default=HOST, help='Bind host')
parser.add_argument('--port', type=int, default=PORT, help='Listen port')
parser.add_argument('--buffer-size', type=int, default=BUFFER_SIZE, help='Recv buffer size')
parser.add_argument('--log-level', default='INFO', choices=['DEBUG','INFO','WARNING','ERROR'])
return parser.parse_args()
def handle_client(client_socket: socket.socket, buffer_size: int):
client_addr = client_socket.getpeername()
try:
while True:
data = client_socket.recv(buffer_size)
if not data:
logging.info(f"Client {client_addr} disconnected.")
break
response = data.upper()
client_socket.sendall(response)
logging.debug(f"Echoed {len(data)} bytes to {client_addr}")
except ConnectionResetError:
logging.warning(f"Connection reset by peer: {client_addr}")
except BrokenPipeError:
logging.warning(f"Broken pipe when sending to {client_addr}")
except Exception as e:
logging.error(f"Unexpected error with {client_addr}: {e}")
finally:
client_socket.close()
def start_server(host: str, port: int, buffer_size: int):
try:
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind((host, port))
server_socket.listen(5)
logging.info(f"Server started on {host}:{port}")
while True:
try:
client_socket, client_addr = server_socket.accept()
logging.info(f"Accepted connection from {client_addr}")
handle_client(client_socket, buffer_size)
except KeyboardInterrupt:
logging.info("Shutting down...")
break
except Exception as e:
logging.error(f"Accept error: {e}")
continue
except Exception as e:
logging.critical(f"Failed to start server: {e}")
sys.exit(1)
finally:
server_socket.close()
def main():
args = parse_args()
logging.basicConfig(level=args.log_level, format=LOG_FORMAT)
start_server(args.host, args.port, args.buffer_size)
if __name__ == '__main__':
main()
这个 normalserver.py 具备以下特点:
- 支持命令行参数配置;
- 使用标准日志模块分级输出;
- 包含完整的异常捕获和资源释放;
- 结构清晰,易于扩展。
启动方式:
python normalserver.py --port 9999 --log-level DEBUG
性能瓶颈与并发演进路径:别再让单线程拖累你!
循环服务器最大的问题是: 一个客户端的耗时操作会让所有人排队等待 。
假设处理每个请求需要1秒,那么每秒最多处理1个客户端。如果有100个并发连接?不好意思,第100个要等99秒……
多进程模型:fork的力量
思路很简单:每当有新连接到来,就 fork() 一个子进程去处理。
优点:
- 利用多核CPU;
- 内存隔离,安全性高;
- 子进程崩溃不影响主服务。
缺点:
- 进程创建开销大;
- 内存占用随连接数线性增长;
- 进程间通信复杂。
多线程模型:更轻量的选择
import threading
def thread_handle_client(client_sock, addr):
try:
while True:
data = client_sock.recv(1024)
if not data:
break
client_sock.send(data[::-1])
finally:
client_sock.close()
# 主循环
while True:
client_sock, addr = server.accept()
t = threading.Thread(target=thread_handle_client, args=(client_sock, addr))
t.daemon = True
t.start()
优点:
- 创建成本低;
- 易于共享内存数据。
⚠️ 缺点:Python的GIL锁导致CPU密集型任务无法真正并行。
高并发未来之路:I/O多路复用 + 异步编程
| 技术 | 平台支持 | 性能特点 |
|---|---|---|
| select | 跨平台 | O(n),FD上限1024 |
| poll | Linux/Unix | O(n),无硬限制 |
| epoll | Linux专有 | O(1),事件驱动高效通知 |
以及现代Python推崇的 asyncio :
import asyncio
async def echo_handler(reader, writer):
while True:
data = await reader.read(1024)
if not data:
break
writer.write(data)
await writer.drain()
writer.close()
async def main():
server = await asyncio.start_server(echo_handler, 'localhost', 8888)
async with server:
await server.serve_forever()
asyncio.run(main())
这才是应对海量并发的正确姿势!
写在最后:从循环服务器出发,走向高并发架构
回顾全文,我们从TCP协议的基本原理入手,一步步实现了从零搭建一个TCP服务器的过程。你现在已经掌握了:
✅ TCP三次握手与四次挥手的本质意义
✅ Socket API的正确使用方式
✅ 粘包问题的解决方案
✅ 异常处理与资源管理的最佳实践
✅ 心跳机制的设计与实现
✅ 从循环服务器到多线程/异步的演进路径
这些知识不仅是面试常考点,更是你在实际工作中排查网络问题、优化服务性能的底气所在。
下一步,不妨试试将这个echo server升级为支持JSON协议的聊天室,或是集成TLS加密,又或者用 uvloop 替换默认事件循环,看看性能能提升多少倍。
记住: 每一个伟大的系统,都是从一个最简单的循环服务器开始的。 💪
本文还有配套的精品资源,点击获取
简介:TCP简单循环服务器是网络编程的入门实践项目,用于深入理解TCP协议通信机制和Socket编程基础。本Demo基于Python的socket库,实现了服务器监听、连接接受、数据收发及异常处理等核心功能,采用循环或并发方式处理多个客户端请求。通过该实例,学习者可掌握TCP三次握手、连接管理、字节流传输与消息边界处理等关键知识,为构建高可靠网络服务打下坚实基础。
本文还有配套的精品资源,点击获取







