套接字(Sockets )和套接字 API 用于在网络上传递消息,它们提供了一种进程间通信 (inter-process communication,IPC) 的形式。网络可以是计算机上的一个逻辑本地网络,也可以是一个物理上连接到外部网络的网络,并通过该外部网络连接到其他网络。
套接字(Sockets )有着悠久的历史。它们的使用起源于1971年的ARPAnet,后来在1983年发布的伯克利软件发布版(Berkeley Software Distribution,BSD)操作系统中成为了一种API,被称为伯克利套接字(Berkeley sockets)。
在90年代,随着万维网(World Wide Web)的兴起,网络编程也迅速发展。利用新连接的网络并使用套接字的不仅仅是Web服务器和浏览器。各种类型和规模的客户端-服务器应用程序也得到了广泛应用。
今天,尽管套接字API使用的底层协议多年来有所演变,并且出现了新协议,但底层API本身保持不变。
最常见的套接字应用程序类型是客户端-服务器应用程序,其中一方充当服务器并等待来自客户端的连接,这是主要的网络模式。
Python Socket API 概述
Python 的
socket
模块提供了一组API接口,用于访问套接字 API(the Berkeley sockets API)。该模块中的主要API 函数和方法包括:
-
socket()
-
.bind()
-
.listen()
-
.accept()
-
.connect()
-
.connect_ex()
-
.send()
-
.recv()
-
.close()
Python 提供了一个方便且一致的 API,它直接映射到系统调用及其对应的 C 函数。作为其标准库的一部分,Python 还提供了一些类,使得使用这些底层套接字函数更加简单,比如
socketserver
模块,这是一个用于网络服务器的框架;此外,还有许多模块实现了更高级的互联网协议,如 HTTP 和 SMTP。
TCP 套接字
使用
socket.socket()
创建一个套接字对象,并将套接字类型指定为
socket.SOCK_STREAM
。默认使用的协议是传输控制协议 ( Transmission Control Protocol ,TCP)。
传输控制协议 (TCP) 具有以下特点:
相比之下,也可以使用
socket.SOCK_DGRAM
创建的用户数据报协议 (User Datagram Protocol,UDP) 套接字不具备可靠性,接收方读取的数据可能会与发送方写入的数据顺序不一致。TCP 让您无需担心数据包丢失、数据到达顺序混乱以及其他在网络通信中不可避免的陷阱。下图是 TCP 的套接字 API 调用顺序和数据流:
左侧列表示服务器。右侧列表示客户端。从左上角开始,注意服务器为设置“监听”套接字所进行的 API 调用:
-
socket()
-
.bind()
-
.listen()
-
.accept()
监听套接字的作用正如其名称所示:它监听来自客户端的连接。当客户端连接时,服务器调用
.accept()
来接受或完成连接。客户端调用
.connect()
来建立与服务器的连接,并启动三次握手。握手步骤很重要,因为它确保连接的每一端在网络中是可达的,换句话说,客户端可以到达服务器,反之亦然。有时,可能只有一个主机、客户端或服务器可以到达另一个。在中间部分是往返通信阶段,客户端和服务器通过调用
.send()
和
.recv()
来交换数据。最后,客户端和服务器关闭各自的套接字。
Echo Client and Server
上面介绍了套接字 API 以及客户端和服务器如何通信,下面是一个最为简单的第一个客户端和服务器。将从一个简单的实现开始。服务器将简单地将接收到的内容原样返回给客户端。
以下是服务器的代码:
import socket
HOST = "127.0.0.1"
PORT = 65432
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind((HOST, PORT))
s.listen()
while True:
conn, addr = s.accept()
with conn:
print(f"Connected by {addr}")
while True:
data = conn.recv(1024)
if not data:
break
conn.sendall(data)
print(data)
这段代码实现了一个简单的回显服务器,功能如下:
-
导入模块
:使用
socket
模块来进行网络编程。
-
定义地址和端口
:服务器监听本地主机 (
127.0.0.1
) 和端口
65432
。
-
创建套接字
:使用 IPv4 和 TCP 协议创建一个套接字。
-
绑定和监听
:将套接字绑定到指定的地址和端口,然后开始监听连接请求。
-
处理连接
:接受客户端连接并打印客户端地址。
-
数据接收与回显
:接收客户端发送的数据并将其回显给客户端,直到客户端断开连接。
以下是client 代码:
import socket
HOST = "127.0.0.1"
PORT = 65432
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
s.sendall(b"Hello, world")
data = s.recv(1024)
print(f"Received {data!r}")
这段代码实现了一个简单的客户端,功能如下:
-
导入模块
:使用
socket
模块进行网络编程。
-
定义服务器地址和端口
:客户端连接到本地主机 (
127.0.0.1
) 的端口
65432
。
-
创建并连接套接字
:使用 IPv4 和 TCP 协议创建套接字,并连接到指定的服务器地址和端口。
-
发送数据
:向服务器发送字节数据
b"Hello, world"
。
-
接收数据
:从服务器接收最多 1024 字节的数据。
-
打印接收到的数据
:将接收到的数据以原始格式输出。
通信过程解析
现在,您将更详细地了解客户端和服务器之间是如何进行通信的:
使用回环接口( loopback interface )(IPv4 地址 127.0.0.1 或 IPv6 地址 ::1)时,数据不会离开主机或接触到外部网络。在上面的示意图中,回环接口( loopback interface )位于主机内部。这代表了回环接口的内部特性,显示了穿越它的连接和数据仅在主机内部。这也是为什么回环接口和 IP 地址 127.0.0.1 或 ::1 被称为“localhost”。
应用程序使用回环接口( loopback interface )与在主机上运行的其他进程进行通信,同时确保安全性和与外部网络的隔离。因为它是内部的,仅从主机内部可以访问,所以不会暴露在外部。
当您在应用程序中使用 127.0.0.1 或 ::1 以外的 IP 地址时,它通常绑定到连接到外部网络的以太网接口。这是通向“localhost”之外的其他主机的网关。
多连接客户端和服务器
在接下来的两个部分中,您将创建一个服务器和客户端,使用来自
selectors
模块的选择器对象来处理多个连接。服务器示例代码如下:
import sys
import socket
import selectors
import types
sel = selectors.DefaultSelector()
host = "127.0.0.1"
port = 65432
lsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
lsock.bind((host, port))
lsock.listen()
print(f"Listening on {(host, port)}")
lsock.setblocking(False)
sel.register(lsock, selectors.EVENT_READ, data=None)
def accept_wrapper(sock):
conn, addr = sock.accept()
print(f"Accepted connection from {addr}")
conn.setblocking(False)
data = types.SimpleNamespace(addr=addr, inb=b"", outb=b"")
events = selectors.EVENT_READ | selectors.EVENT_WRITE
sel.register(conn, events, data=data)
def service_connection(key, mask):
sock = key.fileobj
data = key.data
if mask & selectors.EVENT_READ:
recv_data = sock.recv(1024)
if recv_data:
data.outb += recv_data
else:
print(f"Closing connection to {data.addr}")
sel.unregister(sock)
sock.close()
if mask & selectors.EVENT_WRITE:
if data.outb:
print(f"Echoing {data.outb!r} to {data.addr}")
sent = sock.send(data.outb)
data.outb = data.outb[sent:]
try:
while True:
events = sel.select(timeout=None)
for key, mask in events:
if key.data is None:
accept_wrapper(key.fileobj)
else:
service_connection(key, mask)
except KeyboardInterrupt:
print("Caught keyboard interrupt, exiting")
finally:
sel.close()
这段代码实现了一个多连接的服务器,能够同时处理多个客户端的连接。
导入和初始化
-
socket
和
selectors
模块用于创建和管理网络连接。
-
sel
是选择器对象,用于管理多个套接字的 I/O 事件。
-
服务器设置
-
创建一个监听套接字
lsock
,绑定到
127.0.0.1
地址和端口
65432
。
-
设置套接字为非阻塞模式,并使用选择器
sel
注册监听套接字,监控其
EVENT_READ
事件(即有新的连接请求)。
-
当有新连接到来时,接受连接,并为每个连接创建一个新的非阻塞套接字。
-
为新连接创建一个数据对象
data
,包含连接的地址以及输入和输出缓冲区。
-
使用选择器注册新连接,监控其
EVENT_READ
和
EVENT_WRITE
事件。
service_connection
函数:
处理现有连接的 I/O 操作:
-
在
try
块中,持续监听和处理所有注册的套接字的 I/O 事件。
-
如果有新的连接请求,调用
accept_wrapper
处理。
-
如果有现有连接的读写事件,调用
service_connection
处理。
-
使用
except KeyboardInterrupt
捕获键盘中断,安全地关闭选择器并退出。
该服务器可以同时处理多个客户端连接,接收并回显客户端发送的数据。通过使用
selectors
模块,服务器能够高效地管理多个非阻塞套接字,从而实现多连接处理。
client示例代码如下: