HOWTO · Python

Python 中的套接字编程:初学者指南

本文讨论 Python 中的套接字编程。我们已经讨论了使用 Python 中的套接字编程使用 TCP 协议和 UDP 协议进行通信。

本页内容

Python 套接字编程让两个进程通过称为套接字的通信端点交换数据。本指南使用标准库中的 socket 模块构建本机 TCP 和 UDP 客户端/服务器通信,并说明各协议适用的情境。无需安装其他套件。

Python 套接字编程:概述与准备

服务器等待客户端连接;客户端则主动发起与服务器的通信。在以下示例中,两者都在同一台计算机上运行,并使用 127.0.0.1 和相同的端口。socket.socket() 建构函数会建立套接字。TCP 使用 socket.SOCK_STREAM,UDP 使用 socket.SOCK_DGRAM。发送文字前必须先将其编码为字节,接收后再解码。

请在同一个终端中,将下面每个完整代码区块各运行一次。每个脚本都会在背景线程中启动服务器,随后由同一脚本充当客户端,通过回送介面交换一则讯息,并关闭套接字。

建立 TCP 服务器与客户端

TCP 使用面向连接、可靠且有序的字节流。服务器会绑定地址和端口、调用 listen(),然后等待 accept() 接受连接。客户端连接到相同的地址和端口。连接建立后,双方都可以发送和接收字节。sendall() 会发送提供缓冲区中的所有字节,而 recv(1024) 最多读取 1,024 个字节。TCP 读取不会标记应用程式讯息的边界,因此较大型的通讯协议必须自行定义讯息框架,例如使用长度前缀或分隔符。

运行此示例:

本节中的代码以及显示的标签或输出仍为英文。

import socket
import threading

HOST = "127.0.0.1"
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
    probe.bind((HOST, 0))
    PORT = probe.getsockname()[1]
ready = threading.Event()


def serve():
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
        server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        server.bind((HOST, PORT))
        server.listen(1)
        ready.set()
        connection, _ = server.accept()
        with connection:
            message = connection.recv(1024).decode("utf-8")
            connection.sendall(("Received: " + message).encode("utf-8"))


thread = threading.Thread(target=serve)
thread.start()
ready.wait()
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client:
    client.connect((HOST, PORT))
    client.sendall(b"Hello over TCP")
    reply = client.recv(1024).decode("utf-8")
thread.join()
print(reply)

作业系统会分配一个可用的本机端口,让示例无需占用固定端口,也能避免端口冲突。背景线程让服务器可以在客户端连接时接受连接;交换完成后,两个套接字都会由各自的情境管理器关闭。

建立 UDP 服务器与客户端

UDP 以独立的数据报传送资料,不会建立 TCP 式的连接。服务器会绑定地址,并通过 recvfrom() 等待资料;该方法会同时返回收到的字节和发送方的地址。客户端使用服务器地址调用 sendto(),然后再用 recvfrom() 接收回复及发送方地址。以下本机示例使用相同的回送地址,并让作业系统选择服务器端口。

运行此示例:

本节中的代码以及显示的标签或输出仍为英文。

import socket
import threading

HOST = "127.0.0.1"
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as probe:
    probe.bind((HOST, 0))
    PORT = probe.getsockname()[1]
ready = threading.Event()


def serve():
    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as server:
        server.bind((HOST, PORT))
        ready.set()
        message, client_address = server.recvfrom(1024)
        reply = ("Received: " + message.decode("utf-8")).encode("utf-8")
        server.sendto(reply, client_address)


thread = threading.Thread(target=serve)
thread.start()
ready.wait()
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as client:
    client.sendto(b"Hello over UDP", (HOST, PORT))
    reply, server_address = client.recvfrom(1024)
thread.join()
print(reply.decode("utf-8"))

数据报会发送到指定的地址和端口,但 UDP 不保证资料一定抵达,也不保证多个数据报按发送顺序抵达。示例会先解码讯息字节再建立回复,因此回复包含可读文字,而不是 Python 的字节表示形式。

如何选择 TCP 或 UDP

当应用程式需要有序的字节流,以及传输层提供的递送和重传行为时,可选择 TCP。应用程式仍须处理连接失败并定义讯息框架;TCP 不保证对端的应用程式会处理收到的讯息。当独立数据报适合任务,而且应用程式可以容忍资料遗失或乱序,或能自行处理这些情况时,可选择 UDP。UDP 并非天生更快:实际效能取决于网络、工作负载,以及应用程式所需的可靠性功能。在套接字和网络设定允许时,UDP 也能用于广播,但它并不是可靠客户端/服务器通信的一般替代方案。

结论

socket 模块提供两种协议所需的基础功能。TCP 示例使用 SOCK_STREAM、accept()、sendall() 和 recv(),通过连接交换资料;UDP 示例使用 SOCK_DGRAM、sendto() 和 recvfrom(),交换指定地址的数据报。可以先从本机客户端和服务器开始练习,再根据应用程式所需的资料递送行为选择 TCP 或 UDP。