TCP 是一种面向连接的、可靠的协议,非常适合用于构建需要稳定数据传输的服务(如网页服务器、数据库连接等)。

我们将分步进行,从最基础的服务端开始,然后逐步改进,使其能够处理多个客户端的并发连接。
最简单的 TCP 服务端(单连接)
这个例子创建一个服务端,它会等待一个客户端连接,接收客户端发送的数据,打印出来,然后发送一个回复,最后关闭连接。
代码示例
# server_simple.py
import socket
# 1. 创建一个 socket 对象
# socket.AF_INET 表示使用 IPv4 地址
# socket.SOCK_STREAM 表示使用 TCP 协议
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2. 绑定 IP 地址和端口号
# '0.0.0.0' 表示监听本机所有可用的网络接口
# 8888 是我们选择的端口号(注意:1024以下的端口需要管理员权限)
host = '0.0.0.0'
port = 8888
server_socket.bind((host, port))
# 3. 开始监听连接
# 5 是连接队列的长度,表示最多有多少个等待连接的客户端
server_socket.listen(5)
print(f"服务器已启动,正在监听 {host}:{port}...")
# 4. 接受客户端连接
# accept() 会阻塞程序,直到有客户端连接上来
# 它返回一个 (client_socket, client_address) 元组
# client_socket 是一个用于与这个特定客户端通信的新的 socket 对象
# client_address 是客户端的 IP 地址和端口号
client_socket, client_address = server_socket.accept()
print(f"已接受来自 {client_address} 的连接!")
# 5. 与客户端进行通信
# 接收数据
# recv(1024) 表示每次最多接收 1024 字节的数据
# recv() 也会阻塞,直到收到数据
data = client_socket.recv(1024)
print(f"收到来自客户端的数据: {data.decode('utf-8')}")
# 发送数据
response = "你好,客户端!"
client_socket.sendall(response.encode('utf-8'))
print("已向客户端发送回复。")
# 6. 关闭连接
# 先关闭与客户端的通信 socket
client_socket.close()
# 再关闭服务端的主 socket
server_socket.close()
print("服务器已关闭。")
如何运行
-
保存代码:将上面的代码保存为
server_simple.py。 -
运行服务端:在终端中执行
python server_simple.py。
(图片来源网络,侵删)$ python server_simple.py 服务器已启动,正在监听 0.0.0.0:8888...
服务端会阻塞在
server_socket.accept()这一行,等待客户端连接。 -
使用客户端连接:打开另一个终端,可以使用
telnet或nc(netcat) 作为客户端来测试。- 使用 telnet:
$ telnet 127.0.0.1 8888 Trying 127.0.0.1... Connected to 127.0.0.1. Escape character is '^]'.
然后在 telnet 窗口中输入一些文字,
hello,然后按回车。 服务端终端会显示:已接受来自 ('127.0.0.1', 54321) 的连接! 收到来自客户端的数据: hello 已向客户端发送回复。telnet 客户端会收到回复:
(图片来源网络,侵删)hello 你好,客户端! - 使用 nc (netcat):
$ nc 127.0.0.1 8888 hello 你好,客户端!
- 使用 telnet:
-
关闭服务:在服务端终端按
Ctrl+C,程序会执行最后的close()操作并退出。
改进版:服务端持续运行并处理多个客户端
上面的例子有一个很大的问题:当一个客户端连接后,服务端处理完就退出了,无法为其他客户端服务。
我们可以使用一个 while True 循环来让服务端持续运行,不断接受新的连接。
代码示例
# server_loop.py
import socket
# 创建 socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(('0.0.0.0', 8888))
server_socket.listen(5)
print(f"服务器已启动,正在监听 0.0.0.0:8888...")
# 循环接受连接
while True:
# 接受新的客户端连接
client_socket, client_address = server_socket.accept()
print(f"已接受来自 {client_address} 的连接!")
try:
# 接收数据
data = client_socket.recv(1024)
if not data:
# recv() 返回空数据,表示客户端已关闭连接
print(f"{client_address} 已关闭连接。")
client_socket.close()
continue
print(f"收到来自 {client_address} 的数据: {data.decode('utf-8')}")
# 发送回复
response = f"你好,{client_address[0]}!你的消息已收到。"
client_socket.sendall(response.encode('utf-8'))
except ConnectionResetError:
print(f"客户端 {client_address} 强制关闭了连接。")
finally:
# 确保与当前客户端的连接被关闭
client_socket.close()
print(f"与 {client_address} 的连接已关闭。")
# 理论上这行代码不会被执行到,除非有 break 语句跳出 while True
server_socket.close()
这个版本的问题
虽然这个版本可以持续为多个客户端服务,但是它是串行处理的,当服务端在为一个客户端处理数据时(recv() 正在等待数据),它会阻塞,无法接受新的客户端连接,如果某个客户端发送了数据后很久才发送下一条数据,其他客户端就必须排队等待。
最终版:多线程服务端(处理并发)
为了解决串行处理的问题,我们可以使用多线程,当服务端接受到一个新的客户端连接时,就创建一个新的线程来专门处理这个客户端的通信,而主线程则继续返回 accept(),准备接受下一个连接。
这是生产环境中最常用和最简单的并发处理模型之一。
代码示例
# server_threaded.py
import socket
import threading
# 定义一个函数,用于处理每个客户端的连接
def handle_client(client_socket, client_address):
"""处理单个客户端连接的函数"""
print(f"[新线程] 已接受来自 {client_address} 的连接!")
try:
while True:
# 接收数据
data = client_socket.recv(1024)
if not data:
# recv() 返回空数据,表示客户端已关闭连接
print(f"[新线程] {client_address} 已关闭连接。")
break
print(f"[新线程] 收到来自 {client_address} 的数据: {data.decode('utf-8')}")
# 发送回复
response = f"你好,{client_address[0]}!你的消息已收到。"
client_socket.sendall(response.encode('utf-8'))
except ConnectionResetError:
print(f"[新线程] 客户端 {client_address} 强制关闭了连接。")
finally:
# 确保与当前客户端的连接被关闭
client_socket.close()
print(f"[新线程] 与 {client_address} 的连接已关闭。")
# --- 主程序 ---
# 创建 socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(('0.0.0.0', 8888))
server_socket.listen(5)
print(f"服务器已启动,正在监听 0.0.0.0:8888...")
# 循环接受连接
while True:
# 接受新的客户端连接
client_socket, client_address = server_socket.accept()
# 创建一个新线程来处理这个客户端
# target 指定线程要执行的函数
# args 指定传递给函数的参数
client_thread = threading.Thread(target=handle_client, args=(client_socket, client_address))
# 设置为守护线程(可选)
# 当主线程退出时,所有守护线程也会随之退出
# client_thread.daemon = True
# 启动线程
client_thread.start()
print(f"[主线程] 已为 {client_address} 创建线程,当前活动线程数: {threading.active_count() - 1}") # -1 是因为主线程本身
# 理论上这行代码不会被执行到
server_socket.close()
如何测试多线程
- 运行
server_threaded.py。 - 打开三个或更多个终端,分别用
telnet或nc连接到0.0.1 8888。 - 在每个客户端终端中输入不同的消息,"Client 1", "Client 2"。
- 观察服务端终端的输出,你会发现它能够同时打印出来自不同客户端的消息,并且每个客户端都收到了正确的回复,你会看到线程数在增加。
| 步骤 | 函数 | 说明 |
|---|---|---|
| 创建 | socket.socket(family, type) |
创建一个 socket 对象,指定地址族(如 AF_INET for IPv4)和类型(如 SOCK_STREAM for TCP)。 |
| 绑定 | socket.bind((host, port)) |
将 socket 绑定到一个具体的 IP 地址和端口号,服务端必须做这一步。 |
| 监听 | socket.listen(backlog) |
使 socket 进入监听状态,准备接受客户端连接。backlog 是连接队列的最大长度。 |
| 接受 | socket.accept() |
阻塞式地等待并接受一个客户端连接,成功后,返回一个新的 socket(用于与该客户端通信)和客户端的地址元组。 |
| 通信 | socket.recv(bufsize) |
从已连接的 socket 接收数据,bufsize 指定每次接收的最大字节数。 |
socket.sendall(data) |
发送数据,它会持续发送数据,直到所有数据都发送完毕,适用于确保数据完整发送。 | |
socket.send(data) |
发送数据,但不保证所有数据都能发送完。 | |
| 关闭 | socket.close() |
关闭 socket,释放资源。 |
注意事项
- 端口号:端口号范围是 0-65535,0-1023 是系统保留端口,通常需要管理员权限才能使用,建议使用 1024 以上的端口号。
- IP 地址:
'127.0.0.1'是本地回环地址,只在本地机器内有效。'0.0.0.0'表示监听本机所有网络接口,其他机器可以通过服务器的真实 IP 地址来连接。 - 阻塞:
accept()和recv()都是阻塞函数,在没有连接或数据时,程序会暂停在这些地方,直到有事件发生。 - 编码:
send()和recv()处理的是字节流(bytes),如果需要发送字符串,必须先将其编码(如encode('utf-8')),接收时再解码(如decode('utf-8'))。 - 并发模型:除了多线程,还有多进程、异步 I/O(
asyncio)等更高级的并发模型来处理大量客户端连接,对于初学者,多线程是最容易理解和实现的。
