核心概念回顾
在开始编码前,我们先快速回顾一下网络编程的核心概念:
- Socket (套接字):是网络编程的 API,它代表了一个网络连接的端点,你可以把它想象成一个电话插孔,通过它,你的程序可以发送和接收数据。
- IP 地址:网络中设备的唯一地址,
0.0.1(本地回环地址) 或168.1.100(局域网地址)。 - 端口号:设备上应用程序的标识,一个 IP 地址可以同时运行多个网络服务,端口号用来区分它们,Web 服务通常使用 80 端口,HTTPS 使用 443 端口,注意:端口号范围是 0-65535,1024 以下的端口通常需要管理员权限才能使用。
- TCP (传输控制协议):一种面向连接的、可靠的协议,它像打电话一样,需要先“拨号”(建立连接),然后才能进行全双工通信,数据传输顺序有保障,不会丢失或重复。
- UDP (用户数据报协议):一种无连接的、不可靠的协议,它像寄明信片一样,发送方直接把数据包发出去,不保证对方一定能收到,也不保证顺序,速度快,开销小。
本教程主要讲解 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' 表示监听本机所有可用的网络接口
# 8080 是我们选择的端口号
host = '0.0.0.0'
port = 8080
server_socket.bind((host, port))
# 3. 开始监听 incoming connections
# 5 是连接队列的最大长度
server_socket.listen(5)
print(f"服务器正在监听 {host}:{port}...")
# 4. 接受客户端连接
# accept() 会阻塞程序,直到有客户端连接
# 返回一个新的 socket 对象 (client_socket) 和客户端的地址 (client_address)
client_socket, client_address = server_socket.accept()
# 5. 与客户端通信
print(f"已接受来自 {client_address} 的连接!")
# 发送欢迎消息给客户端 (需要编码为 bytes)
welcome_message = "欢迎连接到简单服务器!"
client_socket.send(welcome_message.encode('utf-8'))
# 接收客户端发来的数据 (最多接收 1024 字节)
data = client_socket.recv(1024)
print(f"收到来自客户端的消息: {data.decode('utf-8')}")
# 6. 关闭连接
client_socket.close()
server_socket.close()
print("连接已关闭,服务器退出。")
如何运行
- 将上述代码保存为
server_simple.py。 - 在终端中运行它:
python server_simple.py,你会看到服务器正在监听 0.0.0.0:8080...。 - 打开另一个终端,使用
telnet或nc(netcat) 作为客户端来测试:- 使用 telnet:
telnet 127.0.0.1 8080 - 使用 nc (netcat):
nc 127.0.0.1 8080
- 使用 telnet:
- 连接成功后,你会看到
已接受来自...的消息。 - 在客户端终端输入一些文字,然后按回车,服务器端会打印出你输入的消息。
- 客户端断开连接后,服务器脚本会执行完毕并退出。
持续监听的服务器(处理多个客户端)
上面的服务器只能处理一个客户端,然后就退出了,真实的服务器需要能够持续运行,并为多个客户端提供服务,我们可以使用 while True 循环来实现。
代码示例
# server_persistent.py
import socket
# 1. 创建 socket 对象
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2. 绑定地址和端口
host = '0.0.0.0'
port = 8080
server_socket.bind((host, port))
# 3. 开始监听
server_socket.listen(5)
print(f"服务器正在监听 {host}:{port}...")
# 4. 持续监听,处理多个客户端
while True:
# 接受新的客户端连接
client_socket, client_address = server_socket.accept()
print(f"已接受来自 {client_address} 的连接!")
# 获取客户端数据
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_message = f"你好,你的消息 '{data.decode('utf-8')}' 已收到!"
client_socket.send(response_message.encode('utf-8'))
# 关闭与当前客户端的连接
client_socket.close()
print(f"与 {client_address} 的连接已关闭。")
# (由于是 while True,这里的代码不会被执行)
# server_socket.close()
这个版本的会为每一个新连接创建一个处理流程,处理完后继续等待下一个。
多线程服务器(同时处理多个客户端)
上面的持续监听服务器仍然是顺序处理的,如果一个客户端连接后长时间不发送数据,那么新的客户端就必须等待,为了解决这个问题,我们可以使用多线程,让每个客户端连接都在一个独立的线程中处理。
代码示例
# 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:
# 如果没有数据,说明客户端已断开连接
print(f"[{client_address}] 客户端已断开连接。")
break
print(f"[{client_address}] 收到消息: {data.decode('utf-8')}")
# 发送响应
response = f"服务器回复: 你说 '{data.decode('utf-8')}'"
client_socket.send(response.encode('utf-8'))
except ConnectionResetError:
print(f"[{client_address}] 客户端异常断开。")
finally:
# 确保连接被关闭
client_socket.close()
print(f"[{client_address}] 连接已关闭。")
# --- 主服务器程序 ---
if __name__ == "__main__":
# 1. 创建 socket 对象
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2. 绑定地址和端口
host = '0.0.0.0'
port = 8080
server_socket.bind((host, port))
# 3. 开始监听
server_socket.listen(5)
print(f"服务器正在监听 {host}:{port}...")
# 4. 持续接受新连接
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}") # 减去主线程本身
如何测试多线程服务器
- 运行
python server_threaded.py。 - 打开两个或更多个终端窗口,分别运行
telnet 127.0.0.1 8080。 - 在不同的客户端窗口输入不同的消息,你会发现服务器可以同时响应所有客户端,而不会互相阻塞,你可以在服务器端看到来自不同线程的日志输出。
更健壮的服务器(添加异常处理和资源管理)
一个好的服务器应该能够优雅地处理各种错误,并确保资源(如 socket)被正确关闭,我们可以使用 try...finally 或 with 语句(with 语句在 Python 3.10+ 的 socket 模块中得到了更好的支持)。
代码示例(结合了所有最佳实践)
# server_robust.py
import socket
import threading
def handle_client(client_socket, client_address):
"""健壮的客户端处理函数"""
print(f"[线程 {threading.get_ident()}] 处理来自 {client_address} 的连接。")
try:
with client_socket: # 使用 with 语句确保 client_socket 自动关闭
while True:
data = client_socket.recv(1024)
if not data:
print(f"[线程 {threading.get_ident()}] {client_address} 断开连接。")
break
print(f"[线程 {threading.get_ident()}] 收到来自 {client_address}: {data.decode('utf-8')}")
response = f"消息已确认。"
client_socket.send(response.encode('utf-8'))
except Exception as e:
print(f"[线程 {threading.get_ident()}] 处理 {client_address} 时发生错误: {e}")
def main():
host = '0.0.0.0'
port = 8080
# 使用 with 语句确保 server_socket 自动关闭
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 允许地址重用,方便快速重启
server_socket.bind((host, port))
server_socket.listen(5)
print(f"服务器启动,正在监听 {host}:{port}...")
try:
while True:
client_socket, client_address = server_socket.accept()
print(f"接受新连接: {client_address}")
# 为新客户端创建线程
thread = threading.Thread(target=handle_client, args=(client_socket, client_address))
thread.daemon = True
thread.start()
print(f"活动线程数: {threading.active_count() - 1}")
except KeyboardInterrupt:
print("\n服务器正在关闭...")
finally:
print("服务器已关闭。")
if __name__ == "__main__":
main()
改进点:
with语句:确保server_socket和client_socket在使用完毕后能被自动关闭,即使发生异常。setsockopt(socket.SO_REUSEADDR, 1):这是一个非常实用的设置,它允许你立即重用之前处于TIME_WAIT状态的地址和端口,这在开发调试时重启服务器非常有用,可以避免 "Address already in use" 错误。KeyboardInterrupt:捕获Ctrl+C信号,让服务器可以优雅地退出。- 线程 ID:在日志中打印线程 ID,可以更清晰地追踪哪个线程在处理哪个客户端。
| 类型 | 特点 | 适用场景 |
|---|---|---|
| 简单服务器 | 一次连接,即用即关。 | 快速测试、学习基本概念。 |
| 持续监听服务器 | 循环接受连接,但顺序处理。 | 简单的、单任务的服务(如聊天室服务器,但性能差)。 |
| 多线程服务器 | 为每个客户端创建一个线程,可并发处理。 | 最常用的模型,适用于大多数需要同时处理多个客户端请求的场景。 |
| 健壮服务器 | 结合了异常处理、资源管理和优雅退出。 | 生产环境,需要高可靠性和稳定性的应用。 |
从上面的例子可以看出,构建一个功能强大的服务器是一个渐进的过程,从最简单的 socket 基础开始,逐步加入循环、多线程和健壮性处理,最终形成一个可以投入生产使用的服务端程序。
