第 54 课 - 网络编程:Socket 基础与 TCP 通信
1. 学习目标
通过本节课的学习,你将能够:
- 理解 Socket(套接字)在网络编程中的基本概念和作用。
- 掌握 TCP 协议建立连接(三次握手)和断开连接(四次挥手)的基本流程。
- 使用 Python 的
socket模块编写一个基础的 TCP 服务器。 - 使用 Python 的
socket模块编写一个基础的 TCP 客户端,与服务器进行通信。 - 理解并应用 TCP 通信中可靠、有序的数据传输特性。
2. 核心概念
2.1 什么是 Socket?
想象一下,网络通信就像两个人打电话。Socket(套接字) 就像是电话机上的插口,它是网络通信的端点。你的程序通过这个"插口"与网络连接,进行数据的发送和接收。在 Python 中,socket 模块提供了对底层操作系统 Socket 接口的访问。
2.2 TCP 协议:可靠的数据传输
TCP(传输控制协议)是一种面向连接的、可靠的通信协议。它就像是一次有保证的电话通话:
- 面向连接:通信前必须先建立连接(三次握手),通信结束后要断开连接(四次挥手)。
- 可靠传输:数据按顺序到达,如果丢失会自动重传。
- 字节流:数据被看作一串无结构的字节流,而不是独立的消息包。
TCP 建立连接(三次握手)简化过程:
- 客户端向服务器发送:"我想连接你"(SYN)。
- 服务器回复:"好的,我同意,并且我也准备好了"(SYN-ACK)。
- 客户端再发送:"收到,我们开始通信吧"(ACK)。 这样,连接就建立了,双方可以开始互发数据。
3. 代码示例
我们将创建一个简单的 TCP 聊天程序。服务器可以接收客户端的消息并回复。
3.1 TCP 服务器代码 (tcp_server.py)
import socket
import threading
def handle_client(client_socket, address):
"""处理单个客户端连接的函数"""
print(f"[新连接] 来自 {address} 的客户端已连接。")
try:
while True:
# 接收客户端数据,最大 1024 字节
data = client_socket.recv(1024)
if not data:
# 如果没有数据,说明客户端断开连接
print(f"[断开] 客户端 {address} 已断开连接。")
break
# 打印接收到的消息
message = data.decode('utf-8')
print(f"[收到来自 {address} 的消息] {message}")
# 向客户端发送回复
reply = input("请输入服务器回复: ")
client_socket.send(reply.encode('utf-8'))
except ConnectionResetError:
print(f"[异常] 客户端 {address} 强制断开连接。")
finally:
# 关闭该客户端的套接字
client_socket.close()
def start_server(host='127.0.0.1', port=12345):
"""启动 TCP 服务器"""
# 1. 创建 Socket 对象
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2. 设置套接字选项,允许地址重用(避免"地址已被使用"错误)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 3. 绑定地址(IP 和端口)
server_socket.bind((host, port))
# 4. 开始监听,参数 5 表示最大排队连接数
server_socket.listen(5)
print(f"[启动] 服务器正在监听 {host}:{port}...")
try:
while True:
# 5. 接受客户端连接(阻塞式等待)
client_socket, address = server_socket.accept()
# 为每个客户端创建一个新线程来处理
client_thread = threading.Thread(
target=handle_client,
args=(client_socket, address)
)
client_thread.daemon = True # 设置为守护线程,主程序退出时线程自动结束
client_thread.start()
# 显示当前活动的连接数(不包括主线程)
print(f"[活动连接] {threading.active_count() - 1}")
except KeyboardInterrupt:
print("\n[关闭] 服务器正在关闭...")
finally:
# 6. 关闭服务器套接字
server_socket.close()
if __name__ == "__main__":
start_server()
3.2 TCP 客户端代码 (tcp_client.py)
import socket
def start_client(host='127.0.0.1', port=12345):
"""启动 TCP 客户端"""
# 1. 创建 Socket 对象 (AF_INET: IPv4, SOCK_STREAM: TCP)
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
# 2. 连接服务器
print(f"[连接] 正在连接服务器 {host}:{port}...")
client_socket.connect((host, port))
print("[连接成功]")
while True:
# 3. 用户输入要发送的消息
message = input("请输入要发送的消息 (输入 'quit' 退出): ")
# 4. 检查是否要退出
if message.lower() == 'quit':
print("[退出] 正在断开连接...")
break
# 5. 发送数据(将字符串编码为字节)
client_socket.send(message.encode('utf-8'))
# 6. 接收服务器回复
reply = client_socket.recv(1024).decode('utf-8')
print(f"[服务器回复] {reply}")
except ConnectionRefusedError:
print("[错误] 无法连接服务器,请确认服务器是否已启动。")
except Exception as e:
print(f"[错误] 发生异常: {e}")
finally:
# 7. 关闭套接字
client_socket.close()
print("[连接已断开]")
if __name__ == "__main__":
start_client()
运行步骤:
- 先运行
tcp_server.py,它将开始监听。 - 再运行一个或多个
tcp_client.py实例。 - 在客户端输入消息,服务器会收到并提示你输入回复。
- 客户端将收到服务器的回复。
4. 实践练习
练习 1:修改客户端功能
要求: 修改 tcp_client.py,使其在发送消息前自动在消息末尾添加客户端的昵称(例如:"Alice: 你好")。昵称在程序启动时由用户输入。
预期输出(客户端部分):
请输入你的昵称: Alice
[连接成功]
请输入要发送的消息 (输入 'quit' 退出): 你好
[服务器回复] 你好Alice!
练习 2:服务器广播消息
要求: 在 tcp_server.py 的 handle_client 函数中,当收到一个客户端的消息时,不仅回复发送者,还将此消息广播给所有其他已连接的客户端(除了发送者自己)。你需要维护一个已连接客户端的列表。
提示: 使用全局列表和线程锁 (threading.Lock) 来安全地管理客户端连接。
练习 3:实现"断开连接"命令
要求: 在客户端添加一个 /exit 命令。当用户输入 /exit 时,客户端向服务器发送一个特殊的字符串(如 "__EXIT__"),然后正常关闭。服务器收到该字符串后,打印日志并关闭与该客户端的连接,而不是等待用户手动输入回复。
5. 常见错误
-
Address already in use(地址已被使用):- 原因:服务器程序没有正确关闭,端口仍处于占用状态。
- 解决:在
server_socket.bind()前设置server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1),或在服务器代码退出时确保socket.close()被调用。也可以稍等片刻再重新启动服务器。
-
Connection refused(连接被拒绝):- 原因:客户端尝试连接时,服务器程序没有在运行,或者监听的
host/port不正确。 - 解决:确保服务器程序已启动,并且客户端连接的
(host, port)与服务器bind的参数一致。
- 原因:客户端尝试连接时,服务器程序没有在运行,或者监听的
-
BlockingIOError或数据粘包:- 原因:TCP 是字节流协议,没有消息边界。
recv(1024)可能一次收到不完整的消息,也可能一次收到多个消息粘在一起。 - 解决:对于简单应用,可以约定消息以特定字符(如换行符
\n)结尾,或在消息头部包含固定长度的字节数来指示消息体长度。本课示例用于教学,未处理此复杂问题。
- 原因:TCP 是字节流协议,没有消息边界。
-
缓冲区大小设置不当:
recv(1024)的 1024 并不是每次发送数据的上限,它只是告诉操作系统一次最多从缓冲区读取多少字节。如果你需要发送大于 1024 字节的数据,客户端可能需要多次send,服务器也需要循环recv直到收完整条消息。
6. 小结
- Socket 是网络通信的编程接口,是应用层与传输层之间的桥梁。
- TCP 是一种可靠的、面向连接的协议,适用于需要确保数据完整性和顺序的场景,如网页浏览、文件传输。
- Python 的
socket模块是进行网络编程的基础。 - 一个基本的 TCP 服务器流程:创建 Socket -> 绑定地址 -> 监听 -> 接受连接 -> 收发数据 -> 关闭连接。
- 一个基本的 TCP 客户端流程:创建 Socket -> 连接服务器 -> 收发数据 -> 关闭连接。
- 对于需要同时服务多个客户端的服务器,通常需要使用多线程 (
threading) 或异步编程(将在后续课程涉及)来处理并发连接。
下一课预告:我们将学习另一种常见的传输层协议——UDP。它无连接、不可靠,但在某些实时性要求高、允许少量丢包的场景下(如在线视频、语音、DNS查询)非常高效。