杰瑞科技汇

Python socket服务器端如何实现?

核心概念回顾

在开始编码前,我们先快速回顾一下网络编程的核心概念:

  • 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("连接已关闭,服务器退出。")

如何运行

  1. 将上述代码保存为 server_simple.py
  2. 在终端中运行它:python server_simple.py,你会看到 服务器正在监听 0.0.0.0:8080...
  3. 打开另一个终端,使用 telnetnc (netcat) 作为客户端来测试:
    • 使用 telnet: telnet 127.0.0.1 8080
    • 使用 nc (netcat): nc 127.0.0.1 8080
  4. 连接成功后,你会看到 已接受来自... 的消息。
  5. 在客户端终端输入一些文字,然后按回车,服务器端会打印出你输入的消息。
  6. 客户端断开连接后,服务器脚本会执行完毕并退出。

持续监听的服务器(处理多个客户端)

上面的服务器只能处理一个客户端,然后就退出了,真实的服务器需要能够持续运行,并为多个客户端提供服务,我们可以使用 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}") # 减去主线程本身

如何测试多线程服务器

  1. 运行 python server_threaded.py
  2. 打开两个或更多个终端窗口,分别运行 telnet 127.0.0.1 8080
  3. 在不同的客户端窗口输入不同的消息,你会发现服务器可以同时响应所有客户端,而不会互相阻塞,你可以在服务器端看到来自不同线程的日志输出。

更健壮的服务器(添加异常处理和资源管理)

一个好的服务器应该能够优雅地处理各种错误,并确保资源(如 socket)被正确关闭,我们可以使用 try...finallywith 语句(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()

改进点:

  1. with 语句:确保 server_socketclient_socket 在使用完毕后能被自动关闭,即使发生异常。
  2. setsockopt(socket.SO_REUSEADDR, 1):这是一个非常实用的设置,它允许你立即重用之前处于 TIME_WAIT 状态的地址和端口,这在开发调试时重启服务器非常有用,可以避免 "Address already in use" 错误。
  3. KeyboardInterrupt:捕获 Ctrl+C 信号,让服务器可以优雅地退出。
  4. 线程 ID:在日志中打印线程 ID,可以更清晰地追踪哪个线程在处理哪个客户端。
类型 特点 适用场景
简单服务器 一次连接,即用即关。 快速测试、学习基本概念。
持续监听服务器 循环接受连接,但顺序处理。 简单的、单任务的服务(如聊天室服务器,但性能差)。
多线程服务器 为每个客户端创建一个线程,可并发处理。 最常用的模型,适用于大多数需要同时处理多个客户端请求的场景。
健壮服务器 结合了异常处理、资源管理和优雅退出。 生产环境,需要高可靠性和稳定性的应用。

从上面的例子可以看出,构建一个功能强大的服务器是一个渐进的过程,从最简单的 socket 基础开始,逐步加入循环、多线程和健壮性处理,最终形成一个可以投入生产使用的服务端程序。

分享:
扫描分享到社交APP
上一篇
下一篇