跳至主要内容

Python 套接字编程完整指南

从零构建 TCP 客户端与服务器,使用线程处理多连接,并了解何时应选择 asyncio。
· 15分钟 阅读

使用 AI 探索

网络的核心在于让设备彼此连接以交换信息。套接字(Socket)是高效网络通信的关键组成部分,它作为底层概念,用于在本地或广域网络上的设备之间以及同一台机器上的不同进程之间传输消息。它提供了一个低级接口,使我们能够对发送或接收的流量进行细粒度控制。

这种低级特性让我们能够为特定场景创建高性能的通信通道(或自定义协议),避免了建立在套接字之上的传统协议可能带来的开销。

因此,套接字在依赖即时消息交换或处理海量数据的实时客户端-服务器应用中格外实用。

本文将介绍套接字编程基础,并提供使用 Python 创建基于套接字的客户端与服务器应用的分步指南。

网络基础

网络使各种形式的通信与信息共享成为可能。这些互联设备的集合被称为网络。

在物理世界中,我们能看到许多网络:航空网、电力网,或通过高速公路互联的城市,都是很好的例子。

同样,在信息技术领域也存在大量网络;其中最著名的就是互联网——将无数设备相连的全球网络。您现在很可能正通过它阅读这篇文章。

网络类型

互联网内部还包含规模或属性各异的众多网络:例如局域网(LAN),通常连接彼此距离很近的计算机。公司或机构(银行、大学等)中的机器,甚至您家中通过路由器连接的设备,都构成这样的网络。

还有更大或更小的网络类型,如 PAN(个人区域网),可以只是您的智能手机通过蓝牙连接笔记本;MAN(城域网),可连接整座城市的设备;以及 WAN(广域网),覆盖整个国家甚至全世界。没错,最大的 WAN 就是互联网本身。

不言而喻,计算机网络可能非常复杂,由许多要素组成。其中最基础且至关重要的原语之一是通信协议。

网络通信协议类型

通信协议规定了信息发送与接收的规则与格式。这些协议被组织成分层结构,以管理网络通信涉及的各类任务。

一些常见且广为人知的网络通信协议包括:

Wi‑Fi

链路层协议的一个例子,它非常接近硬件,负责在无线环境中在设备之间进行数据的物理传输。

IP(Internet Protocol)

IP 是网络层协议,主要负责分组路由与 IP 编址。

TCP(传输控制协议)

一种可靠的、面向连接的协议,提供全双工通信并确保数据完整性与可达性。它属于传输层协议,管理连接、检测错误并控制信息流。

UDP(用户数据报协议)

与 TCP 属于同一协议族。主要区别在于 UDP 更简单、更快,但不可靠且无连接,不执行投递校验,遵循“发出即忘”的范式。与 TCP 一样,UDP 也位于传输层。

HTTP(超文本传输协议)

一种应用层协议,是 Web 上浏览器与服务器通信最常用的协议,尤其用于提供网站内容。您正在阅读的这篇文章也通过 HTTP 提供。HTTP 构建在 TCP 之上,管理并传输与 Web 应用相关的信息,如用于传递元数据与 Cookie 的头信息,不同的 HTTP 方法(GET、POST、DELETE、UPDATE)等。

MQTT(消息队列遥测传输)

另一种应用层协议,适用于处理能力与电量受限、且网络状况不稳定的设备(例如矿区的气体传感器,或您家中的智能灯泡)。MQTT 是物联网(IoT)中的标准消息协议,既轻量又易用,设计了内置的重传机制以增强可靠性。如果您有兴趣在 Python 中使用该协议,可以阅读这篇 Python MQTT 指南,详细介绍了 Paho MQTT 客户端。

需要特别指出的是,上述所有协议底层都使用套接字,但在其之上添加了自己的逻辑与数据处理。这是因为在现代设备中,套接字是任何网络通信的低级接口,我们将在下一节讨论这一点。

关键概念与术语

当然,在网络语境中还有许多重要概念与术语。下面快速回顾一些在后续教程中可能出现的重点:

  • 数据包(Packet):计算机网络中的标准数据传输单元(口语中可类比“消息”)。
  • 端点(Endpoint):数据包到达的目的地。
  • IP 地址:在网络中唯一标识设备的数值标识符。例如:192.168.0.0
  • 端口(Ports):在设备上唯一标识某个进程的数值标识符,该进程处理特定的网络通信,例如通过 HTTP 提供网站。IP 地址标识设备,端口标识应用(每个应用是一个或多个进程)。一些知名端口示例:80 端口通常由服务器应用用于处理 HTTP 流量,443 用于 HTTPS(安全的 HTTP)。
  • 网关(Gateway):一种特殊的网络节点(设备),作为从一个网络访问另一个网络的入口。这些网络可能使用不同协议,因此网关可能需要进行协议转换。常见示例是将家庭局域网连接至互联网的路由器。

理解套接字

什么是套接字?

套接字是用于同一台或不同机器上不同进程之间通信的接口(“门”)。在后一种情况下,我们称之为网络套接字。

网络套接字抽象了连接管理。特别是在 Unix 系统中,套接字本质上就是文件,支持与普通文件相同的写读操作,但会将所有数据通过网络发送。

当套接字处于监听或连接状态时,它总是绑定到一个 IP 地址与端口号的组合上,用以标识主机(机器/设备)与进程。

套接字连接如何工作

套接字可以监听传入连接,也可以主动发起出站连接。连接建立后,监听套接字(服务器套接字)会额外绑定到对端的 IP 与端口。

或者,系统会创建一个新套接字,现在它绑定了监听端与请求端的两对 IP 地址与端口号。这样,不同机器上的两个已连接套接字就能彼此标识,并共享一个数据传输连接,而不会阻塞监听套接字,后者可继续监听其他连接。

对于连接方套接字(客户端套接字),在发起连接时,它会隐式绑定到设备的 IP 地址和一个随机可用端口。随后连接建立时,会以与监听套接字类似的方式绑定到对端的 IP 与端口,但不会创建新套接字。

套接字编程

谈到套接字编程,我们会在代码中实例化套接字对象并对其执行操作(监听、连接、接收、发送等)。在此语境中,套接字就是程序中我们创建的特殊对象,拥有用于网络连接与流量处理的专用方法。

在底层,这些方法会调用操作系统内核,更具体地说是网络栈,这是内核中负责管理网络操作的特殊部分。

套接字与客户端-服务器通信

套接字与连接相关;它们是连接处理器。在 Web 上,每当您要发送或接收数据,您就会发起一个连接(通过称为套接字的接口来发起)。

接着,您或对端一方充当服务器,另一方充当客户端。服务器为客户端提供数据,客户端主动连接并向服务器请求数据。服务器通过监听套接字监听新连接,建立连接,接收客户端请求,并在响应中向客户端返回所请求的数据。

另一方面,客户端使用要连接的服务器的 IP 地址与端口创建一个套接字,发起连接,向服务器发送请求,并接收其响应。客户端与服务器套接字之间这种顺畅的信息交换构成了各种网络应用的基础。

作为网络协议基础的套接字

既然套接字构成了基础,就有许多协议构建并运行在其之上。我们已简要提到的 UDP 与 TCP 就是非常常见的两种。使用这些传输协议之一的套接字被称为 UDP 或 TCP 套接字。

IPC 套接字

除了网络套接字,还有其他类型。例如 IPC(进程间通信)套接字。IPC 套接字用于在同一台机器上的进程间传递数据,而网络套接字则可在网络上完成同样的事情。

IPC 套接字的好处在于,它们避免了构建数据包与路由解析的诸多开销。由于 IPC 的收发双方都是本地进程,通过 IPC 套接字通信通常具有更低的延迟。

Unix 套接字

Unix 套接字是 IPC 套接字的一个很好示例。与 Unix 中的一切一样,它们只是文件系统中的文件。它们不是通过 IP 地址与端口识别,而是通过文件系统中的文件路径识别。

将网络套接字用于 IPC

请注意,如果服务器与接收方都在本地主机(即 IP 地址为 127.0.0.1),您同样可以使用网络套接字实现进程间通信。

当然,这一做法一方面会因网络栈处理数据的开销而增加额外延迟;但另一方面,这让我们无需关心底层操作系统,因为网络套接字在所有系统上都可用并工作良好,而 IPC 套接字通常依赖于特定的操作系统或家族。

Python 套接字库

在 Python 中进行套接字编程,我们使用官方内置的 socket 库,其中包含用于创建、管理与操作套接字的函数、常量和类。常用函数包括:

  • socket():创建新套接字。
  • bind():将套接字绑定到特定地址与端口。
  • listen():开始在套接字上监听传入连接。
  • accept():接受来自客户端的连接,返回用于通信的新套接字。
  • connect():与远程服务器建立连接。
  • send():通过套接字发送数据。
  • recv():从套接字接收数据。
  • close():关闭套接字连接。

Python 套接字示例

下面通过一个 Python 实例来看看套接字编程。我们的目标是让两个应用相互连接并通信。我们将使用 Python 的 socket 库创建一个服务器套接字应用,用于与网络上的客户端通信与信息交换。

注意事项与限制

请注意,为教学目的,示例做了简化,应用将在本地运行而不通过实际网络——我们将使用回环的 localhost 地址将客户端连接到服务器。

这意味着客户端与服务器都运行在同一台机器上,客户端会连接到它所在的同一机器,只是连接到代表服务器的另一个进程。

在不同机器上运行

或者,您也可以将应用部署在两台不同设备上,并让它们连接到同一个 Wi‑Fi 路由器,从而形成一个局域网。此时,一台设备上的客户端可以连接到另一台机器上的服务器。

不过这种情况下,您需要知道路由器为设备分配的 IP 地址,并用它们替代 localhost(127.0.0.1)回环 IP 地址(查看 IP 地址:类 Unix 系统使用 ifconfig,Windows 使用 ipconfig)。获得应用的 IP 地址后,在代码中相应修改,示例依然可运行。

在 Python 中创建套接字服务器

我们先创建一个套接字服务器(特别是 Python TCP 服务器,因为它将使用 TCP 套接字,如下所示),用于与客户端交换消息。

请按照以下步骤操作:

创建包含样板代码的 Python 文件

  • 创建名为 server.py 的文件

  • 在脚本中导入 socket 模块。

import socket
  • 添加名为 run_server 的函数。我们的大部分代码都将写在这里。向函数添加代码时,请注意正确缩进:
def run_server():
    # your code will go here

实例化套接字对象

接下来,在 run_server 中,使用 socket.socket() 创建套接字对象。

第一个参数(socket.AF_INET)指定 IPv4 地址族(其他选项包括:IPv6 的 AF_INET6 与 Unix 套接字的 AF_UNIX)。

第二个参数(socket.SOCK_STREAM) 表示我们使用 TCP 套接字。

使用 TCP 时,操作系统会创建一个可靠的连接,提供有序的数据投递、错误发现与重传,以及流量控制。您无需自己实现这些细节。

也可以指定 UDP 套接字:socket.SOCK_DGRAM。它会创建一个在底层实现 UDP 特性的套接字。

如果您想更进一步下沉,基于套接字所用的 TCP/IP 网络层协议自行构建传输层协议,可以将第二个参数设为 socket.SOCK_RAW。此时操作系统不会为您处理任何更高层协议功能,若需要,您必须自行实现所有头部、连接确认与重传等功能。还有其他取值可参考文档。

# create a socket object
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

将服务器套接字绑定到 IP 与端口

定义主机名或服务器 IP 与端口,指明服务器可达的地址以及监听传入连接的位置。本例中,服务器监听本机——由 server_ip 变量设为 127.0.0.1(即 localhost)。

port 变量设为 8000,这是服务器应用在操作系统中被识别的端口号(建议将端口号设为 1023 以上,避免与系统进程使用的端口冲突)。

server_ip = "127.0.0.1"
    port = 8000

通过将套接字绑定到之前定义的 IP 与端口,为其接收连接做好准备。

# bind the socket to a specific address and port
    server.bind((server_ip, port))

监听传入连接

使用 listen 函数让服务器套接字进入监听状态。其 backlog 参数用于设置在被接受前,允许有多少个传入连接排队等待。

在我们的示例中传入 0,这在大多数系统上实际上会将服务器限制为同一时间仅服务一个客户端——当它忙时,额外的连接尝试会被拒绝。更高的值(比如 1 或更多)允许相应数量的客户端在队列中等待;一旦对某个客户端调用 accept,它就会离开队列并释放名额。

如果省略该参数,将回退到系统默认值(在 Unix 上可在 /proc/sys/net/core/somaxconn 查看)。

# listen for incoming connections
    server.listen(0)
    print(f"Listening on {server_ip}:{port}")

接受传入连接

接下来等待并接受来自客户端的连接。accept 方法会阻塞执行线程,直到有客户端连接。随后它返回一个 (conn, address) 的元组,其中 address 是客户端 IP 与端口的二元组,conn 是一个与该客户端共享连接、可用于与其通信的新套接字对象。

accept 会创建一个新套接字与客户端通信,而不是把监听套接字(在本例中名为 server)绑定到客户端地址并用于通信,这是因为监听套接字需要继续监听其他客户端的连接,否则会被阻塞。当然,在我们的示例中,我们一次只处理一个客户端,并在此期间拒绝其他连接,但在稍后的多线程服务器示例中,这一点会更加重要。

# accept incoming connections
    client_socket, client_address = server.accept()
    print(f"Accepted connection from {client_address[0]}:{client_address[1]}")

创建通信循环

与客户端建立连接后(调用 accept 之后),我们启动一个无限循环进行通信。在循环中,我们调用 client_socket 对象的 recv 方法。该方法从客户端接收指定数量的字节——此处为 1024。

将 1024 字节作为载荷大小只是一个常见约定,因为它是 2 的幂,相比其他任意值在优化上可能更有利。当然,您可以按需修改。

由于从客户端接收到的 request 数据是原始二进制形式,我们使用 decode 将其从字节序列转换为字符串。

接着我们使用 if 判断:当收到 ”close” 消息时跳出通信循环。这意味着一旦服务器在请求中收到 ”close” 字符串,就会将确认信息发回客户端并终止与其的连接。否则,我们将收到的消息打印到控制台。确认在此处仅是向客户端发送一个 ”closed” 字符串。

注意,if 判断中对 request 字符串使用的 lower 方法会将其转换为小写。这样我们就不必关心 close 原始大小写。

# receive data from the client
    while True:
        request = client_socket.recv(1024)
        request = request.decode("utf-8") # convert bytes to string
        
        # if we receive "close" from the client, then we break
        # out of the loop and close the connection
        if request.lower() == "close":
            # send response to the client which acknowledges that the
            # connection should be closed and break out of the loop
            client_socket.send("closed".encode("utf-8"))
            break

        print(f"Received: {request}")

向客户端发送响应

现在我们应处理服务器对客户端的常规响应(即客户端不希望关闭连接时)。在 while 循环中,紧接 print(f"Received: {request}") 之后,添加以下代码,将响应字符串(此处为 ”accepted”)转换为字节并发送给客户端。这样,只要服务器收到的消息不是 ”close”,就会发送 ”accepted” 作为响应:

response = "accepted".encode("utf-8") # convert string to bytes
# convert and send accept response to the client
client_socket.send(response)

释放资源

跳出无限循环后,与客户端的通信即告完成,因此我们使用 close 方法关闭客户端套接字以释放系统资源。随后也使用相同方法关闭服务器套接字,从而实际关闭服务器。在真实场景中,我们当然更希望服务器继续监听其他客户端,而非与一个客户端通信后就关闭;别担心,稍后会有新的示例。

目前,请在无限循环之后添加如下代码:

# close connection socket with the client
    client_socket.close()
    print("Connection to client closed")
    # close server socket
    server.close()

注意:别忘了在 server.py 文件末尾调用 run_server 函数。使用如下代码行即可:

run_server()

完整的服务器套接字代码示例

以下是完整的 server.py 源码:

import socket

def run_server():
    # create a socket object
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    server_ip = "127.0.0.1"
    port = 8000

    # bind the socket to a specific address and port
    server.bind((server_ip, port))
    # listen for incoming connections
    server.listen(0)
    print(f"Listening on {server_ip}:{port}")

    # accept incoming connections
    client_socket, client_address = server.accept()
    print(f"Accepted connection from {client_address[0]}:{client_address[1]}")

    # receive data from the client
    while True:
        request = client_socket.recv(1024)
        request = request.decode("utf-8") # convert bytes to string
        
        # if we receive "close" from the client, then we break
        # out of the loop and close the conneciton
        if request.lower() == "close":
            # send response to the client which acknowledges that the
            # connection should be closed and break out of the loop
            client_socket.send("closed".encode("utf-8"))
            break

        print(f"Received: {request}")

        response = "accepted".encode("utf-8") # convert string to bytes
        # convert and send accept response to the client
        client_socket.send(response)

    # close connection socket with the client
    client_socket.close()
    print("Connection to client closed")
    # close server socket
    server.close()

run_server()

请注意,为避免使这个基础示例变得复杂,我们省略了错误处理。当然,您应添加 try-except 块,并确保始终在 finally 中关闭套接字。继续阅读,我们将看到更高级的示例。

在 Python 中创建客户端套接字

设置好服务器后,下一步是设置一个客户端,用于连接并向服务器发送请求。请从以下步骤开始:

创建包含样板代码的 Python 文件

  • 创建名为 client.py 的新文件
  • 导入 socket 库:
import socket
  • 定义 run_client 函数,我们将把所有代码写在其中:
def run_client():
    # your code will go here

实例化套接字对象

接下来,使用 socket.socket() 创建一个 TCP 套接字对象,作为客户端与服务器通信的入口。

# create a socket object
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

连接到服务器套接字

指定服务器的 IP 地址与端口,以便连接。它们应与您此前在 server.py 中设置的 IP 与端口一致。

server_ip = "127.0.0.1"  # replace with the server's IP address
server_port = 8000  # replace with the server's port number

使用客户端套接字的 connect 方法与服务器建立连接。请注意,我们没有将客户端套接字绑到任何 IP 或端口。这对客户端而言是正常的,因为 connect 会自动选择一个空闲端口,并从系统的网络接口中选择到服务器的最佳路由所对应的 IP 地址(本例为 127.0.0.1),并将客户端套接字绑定到这些地址。

# establish connection with server
client.connect((server_ip, server_port))

创建通信循环

建立连接后,我们启动一个无限通信循环,向服务器发送多条消息。我们通过 Python 内置的 input 函数获取用户输入,然后将其编码为字节并截断为最多 1024 字节。之后使用 client.send 向服务器发送消息。

while True:
   # input message and send it to the server
   msg = input("Enter message: ")
   client.send(msg.encode("utf-8")[:1024])

处理服务器响应

服务器收到来自客户端的消息后会进行响应。现在在客户端代码中,我们要接收服务器的响应。为此,在通信循环中,使用 recv 读取最多 1024 字节。然后通过 decode 将字节转换为字符串,并检查其是否等于 ”closed”。若是,则跳出循环,稍后我们将看到这会终止客户端连接。否则,将服务器响应打印到控制台。

# receive message from the server
response = client.recv(1024)
response = response.decode("utf-8")

# if server sent us "closed" in the payload, we break out of the loop and close our socket
if response.lower() == "closed":
   break

print(f"Received: {response}")

释放资源

最后,在 while 循环之后,使用 close 关闭客户端套接字连接。这确保资源得到正确释放,连接被终止(即当接收到 “closed” 消息并跳出循环时)。

# close client socket (connection to the server)
client.close()
print("Connection to server closed")

注意:同样别忘了在文件末尾调用我们实现的 run_client 函数,如下所示:

run_client()

完整的客户端套接字代码示例

以下是完整的 client.py 代码:

import socket

def run_client():
    # create a socket object
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    server_ip = "127.0.0.1"  # replace with the server's IP address
    server_port = 8000  # replace with the server's port number
    # establish connection with server
    client.connect((server_ip, server_port))

    while True:
        # input message and send it to the server
        msg = input("Enter message: ")
        client.send(msg.encode("utf-8")[:1024])

        # receive message from the server
        response = client.recv(1024)
        response = response.decode("utf-8")

        # if server sent us "closed" in the payload, we break out of the loop and close our socket
        if response.lower() == "closed":
            break

        print(f"Received: {response}")

    # close client socket (connection to the server)
    client.close()
    print("Connection to server closed")

run_client()

测试客户端与服务器

要测试我们上面编写的服务器与客户端实现,请执行以下步骤:

  • 同时打开两个终端窗口。
  • 在一个终端窗口中,进入 server.py 所在目录,运行以下命令启动服务器:
python server.py

这会将服务器套接字绑定到本地主机地址(127.0.0.1)的 8000 端口,并开始监听传入连接。

  • 在另一个终端中,进入 client.py 所在目录,运行以下命令启动客户端:
python client.py

随后会提示输入。您可以输入消息并按回车,这将把输入传输到服务器,并显示在服务器终端窗口。服务器会将响应发送给客户端,客户端随后会再次提示输入。此过程将持续,直至您向服务器发送 ”close” 字符串。

处理多个客户端——多线程

我们在上一个示例中看到服务器如何响应单个客户端的请求,但在许多实际情况下,会有大量客户端需要同时连接到同一台服务器。这就是多线程的用武之地。多线程用于需要并发(同时)处理多个任务(例如执行多个函数)的场景。

其思路是生成线程,即一组可由处理器处理的独立指令。线程比进程轻量得多,因为它们实际存在于进程内部,无需为其分配大量资源。

Python 中多线程的限制

在默认的 CPython 构建中,由于全局解释器锁(GIL),一次只有一个线程执行 Python 字节码。对于套接字编程而言,这没有问题——在网络 I/O 期间 GIL 会释放,因此线程可以真正并行地等待连接。截至 Python 3.13(2024 年 10 月),CPython 还提供移除了 GIL 的自由线程构建(PEP 703),而 asyncio 则是高并发套接字代码更常见的现代选择。

多线程服务器示例

下面的示例展示如何为服务器添加多线程能力以处理大量客户端。请注意,这次我们还会使用 try-except-finally 块加入一些基础错误处理。开始前,请按照以下步骤操作:

创建生成线程的服务器函数

在 Python 文件中导入 socket 与 threading 模块,以便同时处理套接字与线程:

import socket
import threading

定义 run_server 函数,与上例类似:创建服务器套接字、绑定并监听传入连接。然后在无限循环中调用 accept。它会持续监听新连接。accept 收到传入连接并返回后,使用 threading.Thread 构造线程。该线程将执行稍后定义的 handle_client 函数,并将 client_socket 与 addr 作为参数传入(addr 元组包含已连接客户端的 IP 与端口)。创建线程后,调用 start 启动其执行。

请记住,accept 是阻塞调用,因此在 while 循环的首次迭代中,当执行到 accept 这一行时,我们会停止并等待客户端连接,不会执行其他操作。一旦客户端连接,accept 返回,我们继续执行:生成一个线程来处理该客户端,然后进入下一次循环,在那里我们会再次在 accept 调用处等待另一个客户端连接。

函数末尾添加错误处理,确保在出现意外情况时始终关闭服务器套接字。

def run_server():
    server_ip = "127.0.0.1"  # server hostname or IP address
    port = 8000  # server port number
    # create a socket object
    try:
        server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # bind the socket to the host and port
        server.bind((server_ip, port))
        # listen for incoming connections
        server.listen()
        print(f"Listening on {server_ip}:{port}")

        while True:
            # accept a client connection
            client_socket, addr = server.accept()
            print(f"Accepted connection from {addr[0]}:{addr[1]}")
            # start a new thread to handle the client
            thread = threading.Thread(target=handle_client, args=(client_socket, addr,))
            thread.start()
    except Exception as e:
        print(f"Error: {e}")
    finally:
        server.close()

请注意,我们的示例服务器仅在发生意外错误时才会停止。否则它会无限期地监听客户端,若要停止,您需要手动关闭终端进程。

创建在独立线程中运行的客户端处理函数

现在在 run_server 函数上方定义另一个函数 handle_client。该函数会在每个客户端连接上作为独立线程执行。它接收客户端的套接字对象与 addr 元组作为参数。

在该函数中,我们与单线程示例类似地处理请求,并加入一些错误处理:启动循环,通过 recv 获取客户端消息。

然后检查是否收到关闭消息。若收到,则返回 ”closed” 并通过跳出循环关闭连接。否则打印客户端请求字符串到控制台,并进入下一次循环接收后续消息。

函数末尾包含异常情况下的错误处理(except 子句),以及 finally 子句通过 close 释放 client_socket。无论怎样,finally 都会执行,确保客户端套接字总能被正确释放。

def handle_client(client_socket, addr):
    try:
        while True:
            # receive and print client messages
            request = client_socket.recv(1024).decode("utf-8")
            if request.lower() == "close":
                client_socket.send("closed".encode("utf-8"))
                break
            print(f"Received: {request}")
            # convert and send accept response to the client
            response = "accepted"
            client_socket.send(response.encode("utf-8"))
    except Exception as e:
        print(f"Error when handling client: {e}")
    finally:
        client_socket.close()
        print(f"Connection to client ({addr[0]}:{addr[1]}) closed")

当 handle_client 返回时,执行它的线程也会自动结束并释放。

注意:别忘了在文件末尾调用 run_server 函数。

完整的多线程服务器代码示例

现在我们把完整的多线程服务器代码组合起来:

import socket
import threading

def handle_client(client_socket, addr):
    try:
        while True:
            # receive and print client messages
            request = client_socket.recv(1024).decode("utf-8")
            if request.lower() == "close":
                client_socket.send("closed".encode("utf-8"))
                break
            print(f"Received: {request}")
            # convert and send accept response to the client
            response = "accepted"
            client_socket.send(response.encode("utf-8"))
    except Exception as e:
        print(f"Error when hanlding client: {e}")
    finally:
        client_socket.close()
        print(f"Connection to client ({addr[0]}:{addr[1]}) closed")

def run_server():
    server_ip = "127.0.0.1"  # server hostname or IP address
    port = 8000  # server port number
    # create a socket object
    try:
        server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # bind the socket to the host and port
        server.bind((server_ip, port))
        # listen for incoming connections
        server.listen()
        print(f"Listening on {server_ip}:{port}")

        while True:
            # accept a client connection
            client_socket, addr = server.accept()
            print(f"Accepted connection from {addr[0]}:{addr[1]}")
            # start a new thread to handle the client
            thread = threading.Thread(target=handle_client, args=(client_socket, addr,))
            thread.start()
    except Exception as e:
        print(f"Error: {e}")
    finally:
        server.close()

run_server()

注意:在真实项目中,为避免在处理多线程服务器时出现竞争条件或数据不一致等问题,务必考虑线程安全与同步技术。在我们的简单示例中暂不成问题。

带基础错误处理的客户端示例

现在我们已有可并发处理多个客户端的服务器实现,您可以继续使用前面基础示例中的客户端来发起连接,也可以稍作更新并添加一些错误处理。下面的代码与前面的客户端示例基本相同,只是增加了 try-except 块:

import socket

def run_client():
    # create a socket object
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    server_ip = "127.0.0.1"  # replace with the server's IP address
    server_port = 8000  # replace with the server's port number
    # establish connection with server
    client.connect((server_ip, server_port))

    try:
        while True:
            # get input message from user and send it to the server
            msg = input("Enter message: ")
            client.send(msg.encode("utf-8")[:1024])

            # receive message from the server
            response = client.recv(1024)
            response = response.decode("utf-8")

            # if server sent us "closed" in the payload, we break out of
            # the loop and close our socket
            if response.lower() == "closed":
                break

            print(f"Received: {response}")
    except Exception as e:
        print(f"Error: {e}")
    finally:
        # close client socket (connection to the server)
        client.close()
        print("Connection to server closed")

run_client()

测试多线程示例

若要测试多客户端实现,请为多个客户端打开若干终端窗口,并为服务器打开一个。先用 python server.py 启动服务器。之后使用 python client.py 启动多个客户端。在服务器终端窗口中,您会看到新客户端如何连接到服务器。现在您可以在不同客户端的终端中输入文本发送消息,服务器端会处理并将它们全部打印到控制台。

套接字编程在数据科学中的应用

虽然每个网络应用在底层都使用操作系统创建的套接字,但也有大量系统会特别依赖套接字编程,要么出于特定用例,要么为了提升性能。那么,套接字编程在数据科学语境下有何用处?当需要快速接收或发送海量数据时,它就显得尤为重要。因此,套接字编程主要用于数据采集与实时处理、分布式计算以及进程间通信。下面更具体地看看数据科学领域中的一些应用。

实时数据采集

套接字被广泛用于从不同来源采集实时数据,以便进一步处理、写入数据库或进入分析管道等。例如,可用套接字即时接收来自金融系统或社交媒体 API 的数据,供数据科学家后续处理。

分布式计算

数据科学家可以通过套接字连接,将对海量数据集的处理与计算分布到多台机器上。Apache Spark 及其他分布式计算框架在节点间通信时通常使用套接字编程。

模型部署

在向用户提供机器学习模型服务时,可以使用套接字编程实现即时的预测与推荐。为支持实时决策,数据科学家可以使用高性能的基于套接字的服务器应用,接收大量数据,使用训练好的模型进行处理并给出预测,然后迅速将结果返回给客户端。

进程间通信(IPC)

套接字可用于 IPC,使运行在同一台机器上的不同进程彼此通信与交换数据。在数据科学中,这有助于将复杂且资源密集型的计算分散到多个进程。事实上,Python 的子进程库常用于此目的:它会生成多个进程以利用多核并在重计算时提升性能。此类进程间的通信可通过 IPC 套接字实现。

协作与沟通

套接字编程支持数据科学家之间的实时沟通与协作。基于套接字的聊天应用或协作式数据分析平台可用于促进高效协作与知识共享。

值得一提的是,在上述许多应用中,数据科学家未必直接与套接字打交道。他们通常会使用库、框架与系统来抽象套接字编程的所有底层细节。然而,这些解决方案的底层都基于套接字通信并使用套接字编程。

套接字编程的挑战与最佳实践

由于套接字是管理连接的低级概念,开发者需要在其之上实现一整套基础设施,以构建健壮可靠的应用。这当然会带来许多挑战。不过,有一些最佳实践与通用准则可用于应对这些问题。下面列出套接字编程中最常见的一些问题及通用建议:

连接管理

同时处理大量连接、管理多客户端并确保高效处理并发请求,确实具有挑战且并非易事。这需要仔细的资源管理与协调,以避免瓶颈。

最佳实践

  • 使用列表或字典等数据结构跟踪活动连接;或使用连接池等高级技术来提升可扩展性。
  • 使用线程或异步编程技术同时处理多个客户端连接。
  • 正确关闭连接以释放资源并避免内存泄漏。

错误处理

处理连接失败、超时与数据传输问题等错误至关重要。在进行低级套接字编程时,处理这些错误并向客户端提供恰当反馈可能颇具挑战。

最佳实践

  • 使用 try-except-finally 捕获并处理特定类型的错误。
  • 提供信息充分的错误消息,并考虑使用日志以便排查问题。

可扩展性与性能

在处理高吞吐数据流或实时应用时,确保最佳性能并尽量降低延迟是关键关注点。

最佳实践

  • 通过最小化不必要的数据处理与网络开销来优化代码性能。
  • 实现缓冲技术以高效处理大数据传输。
  • 考虑使用负载均衡技术,将客户端请求分发到多个服务器实例。

安全与认证

保护基于套接字的通信并实现合适的认证机制并不容易。确保数据隐私、防止未授权访问、抵御恶意活动,需要谨慎考量与安全协议的正确实现。

最佳实践

  • 使用 SSL/TLS 安全协议对信息进行加密,确保数据传输安全。
  • 通过令牌认证、非对称加密或用户名/密码等方法实现安全的客户端身份认证。
  • 确保密码或 API 密钥等敏感数据得到保护并加密,或理想情况下不直接存储(如需,仅存其哈希)。

网络可靠性与韧性

处理网络中断、带宽波动与不稳定连接可能带来挑战。为构建健壮的网络应用,维持稳定连接、优雅地处理断连并实现重连机制至关重要。

最佳实践

  • 使用 keep‑alive 消息检测不活跃或掉线的连接。
  • 实现超时以避免无限期阻塞,并确保及时处理响应。
  • 实现指数退避的重连逻辑,以在连接丢失时重新建立。

代码可维护性

最后但同样重要的是代码的可维护性。由于套接字编程的低级特性,开发者往往需要编写更多代码,这很快可能演变为难以维护的“意大利面条式”代码。因此,务必要尽早组织与结构化代码,并在架构设计上投入额外精力。

最佳实践

  • 将代码拆分为类或函数,且函数体宜精炼。
  • 尽早编写单元测试,可通过模拟客户端与服务器实现进行测试。
  • 除非确有必要使用套接字编程,否则考虑使用更高层的连接处理库。

总结:Python 中的套接字编程

套接字是所有网络应用不可或缺的一部分。本文我们在 Python 语境下讨论了套接字编程。要点如下:

  • 套接字是抽象连接管理的接口。
  • 套接字使不同进程(通常是客户端与服务器)在本地或网络上进行通信。
  • 在 Python 中,通过 socket 库与套接字交互;其中提供的套接字对象包含 recv、send、listen、close 等方法。
  • 套接字编程在数据科学中的应用包括数据采集、进程间通信与分布式计算等。
  • 套接字编程的挑战包括连接管理、数据完整性、可扩展性、错误处理、安全与代码可维护性。

掌握套接字编程技能,开发者可以创建高效的实时网络应用。通过理解相关概念与最佳实践,便能充分发挥套接字编程的潜力,构建可靠且可扩展的解决方案。

然而,套接字编程是一种非常底层的技术,使用起来较为困难,因为应用工程师需要考虑到应用通信的每个细节。

如今,我们往往不需要直接操作套接字,因为通常由更高层的库与框架来处理,除非确实需要从应用中挤出极致性能或实现大规模扩展。

不过,理解套接字并了解底层工作原理,有助于提升作为开发者或数据科学家的整体认知,这始终是明智之举。

想进一步了解 Python 在网络分析中的角色,请查看我们的 Intermediate Network Analysis in Python 课程。您也可以学习我们的 Python Programming 技能路径,提升您的 Python 编程能力。

Serhii Orlivskyi

作者

Serhii Orlivskyi

Serhii 是一名软件工程师,拥有 3 年从业经验,主要专注于后端技术,如 DBMS、数据库相关语言、Python、Node.js 等。他也具备使用前端框架(如 Vue.js 和 React.js)的经验。

FAQs

Python 的套接字编程用于什么?

Python 中的套接字编程让应用能够在网络上或本地进程之间交换数据。常见用例包括聊天应用、多玩家游戏、IoT 通信、分布式计算以及为机器学习模型提供服务。内置的 socket 库同时支持客户端与服务器端代码。

Python 中的 TCP 与 UDP 套接字有什么区别?

TCP 套接字(socket.SOCK_STREAM)提供可靠、有序并具备错误校验的传输——适用于聊天应用、文件传输和 Web 服务器。UDP 套接字(socket.SOCK_DGRAM)更快但不可靠,属于“发出即忘”,适合视频流、游戏与 IoT 传感器。

我应该使用 Python 套接字还是 asyncio 进行网络编程?

当您需要精细控制或想学习网络工作原理时使用原始套接字。大多数生产环境中使用 asyncio——它比线程更高效地处理成千上万的并发连接。FastAPI 与 aiohttp 都基于 asyncio。

如何用 Python 套接字处理多个客户端?

最简单的方法是多线程——为每个连接生成一个 threading.Thread。更高并发场景下,使用 asyncio 在单线程中处理大量客户端。生产环境中,可使用标准库的 socketserver 模块中的现成类 ThreadingTCPServer 与 ForkingTCPServer。

Python 的套接字连接安全吗?

原生 Python 套接字默认不加密——任何拦截流量的人都能读取内容。要实现安全通信,请使用内置的 ssl 模块将套接字包装为 TLS(ssl.SSLContext.wrap_socket())。在生产环境中,凡涉及敏感数据务必使用 TLS。

主题