本文正在参加「Python主题月」,详情查看 活动链接
socket 是对 TCP/IP 协议族的封装,在网络中可以通过 socket 通信。Python 中也实现了对 socket 编程的支持,可以方便的开发网络应用
客户端-服务端通信应用
下面是一个简单的客户端和服务端通信的例子
通信流程
- 服务端启动并创建一个 socket
- 服务端将 socket 绑定到一个端口
- 服务端开始监听端口
- 客户端请求服务端监听的端口
- 服务端接收到客户端的连接请求,并与客户端建立连接
- 客户端与服务端相互发送数据
- 客户端向服务端发送关闭连接的请求,然后退出
- 服务端关闭连接
服务端实现
import socket
HOST = '127.0.0.1' # 将监听的地址设为本地的 127.0.0.1
PORT = 65432 # 将监听的端口设为 65432
# 使用 socket.socket 创建一个 socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
# 将 socket 绑定到指定的地址和端口
s.bind((HOST, PORT))
# 服务端开始监听
s.listen()
# 服务端等待客户端的连接请求
conn, addr = s.accept()
with conn:
print('Connected by', addr)
# 服务端循环接收客户端发来的数据
while True:
data = conn.recv(1024)
if not data:
break
# 接收到数据后将数据返回给客户端
conn.sendall(data)
复制代码
创建 socket 连接
服务端实现的开头,先创建了一个 socket
对象,使用了 socket.AF_INET
和 socket.SOCK_STREAM
这两个参数,分别表示使用 IPv4 的地址和 TCP 协议。
如果需要使用 IPv6 地址,那么第一个参数可以使用 socket.AF_INET6
,例如:
import socket
with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as s:
s.bind((HOST, PORT))
s.listen()
复制代码
如果基于 Unix 文件创建 socket,那么第一个参数可以使用 socket.AF_UNIX
,并且需要将绑定的地址和端口换成需要的文件路径,例如:
import socket
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s:
s.bind("/tmp/socket_test.s")
s.listen()
复制代码
如果不使用 TCP 协议,而是要使用 UDP 协议,那么需要将将第二个参数改成 socket.SOCK_DGRAM
并且去掉 listen()
,例如:
import socket
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.bind((HOST, PORT))
复制代码
s.listen()
表示开始监听 socket, listen()
有一个可选的参数 backlog
,用来设置等待连接的客户端的最大数量,等待连接的客户端数量超过这个值后,新的连接请求将被拒绝。
接收客户端连接
conn, addr = s.accept()
with conn:
print('Connected by', addr)
复制代码
accept()
用于接收客户端连接,它是一个阻塞方法,当有新客户端连接时会返回连接对象 conn
和 (Host, Port)
组成的表示客户端地址的 addr
。
获取 conn
后在 with conn
的作用域内操作 conn
对象,这样当离开 with conn
的作用域后会自动调用 conn.close()
,就不需要手动关闭连接了。
接收数据
conn.recv()
方法用来从连接对象中读取数据,1024 表示每次最多读取 1024 个字节。如果有数据接收到,就使用 sendall()
方法将数据再传回客户端。
while True:
data = conn.recv(1024)
if not data:
break
conn.sendall(data)
复制代码
send 和 sendall 的区别:
s.send() | 发送 TCP 数据,将 string 中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于 string 的字节大小。 |
---|---|
s.sendall() | 完整发送 TCP 数据。将 string 中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回 None,失败则抛出异常。 |
客户端实现
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)
复制代码
客户端实现和服务端实现类似,不过在创建了 socket 对象之后,客户端使用 connect()
向服务端发起连接请求。成功连上之后,客户端就可以使用 sendall()
方法向服务端发送数据,或者使用 recv()
方法从服务端接收数据了
I/O 多路复用
前面的例子中,服务端一次只能处理一个客户端的请求,等到这个客户端退出之后才能继续处理其他客户端。为了是服务端能够同时支持多客户端的连接,就需要用到 Python 的 selectors
库。
selectors
对多路复用的系统调用进行了封装,使用 selectors.DefaultSelector
就能根据代码的执行平台自动选择最高效的系统调用方法。
selectors 示例
首先创建一个 selector 对象
import selectors
sel = selectors.DefaultSelector()
复制代码
这里 DefaultSelector
会根据代码运行的平台自动从 kqueue, epoll, devpoll, poll, select 这几种 I/O 多路复用的方法中选择一个,优先顺序为 epoll|kqueue|devpoll > poll > select
。
接着同前面一样,创建一个 socket 对象,绑定到 65432 端口,然后开启监听:
host = '127.0.0.1'
port = 65432
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind((host, port))
sock.listen()
sock.setblocking(False)
print('listening on', (host, port))
复制代码
注意,这里需要使用 socket.setblocking(False)
将 socket 上执行的操作设为非阻塞模式,否则会因为执行 socket 的系统调用将应用阻塞。而非阻塞模式下,即使系统无法立即执行 send()
或 recv()
操作,也会立即返回,但是会抛出一个异常。
下面将 socket 注册到 selector 对象,这里只监听可读事件,因此第二个参数使用 selectors.EVENT_READ
sel.register(sock, selectors.EVENT_READ, data=None)
复制代码
接下来当有可读事件时就可以从 selector 对象获取发生事件的 socket,例如:
while True:
# 读取事件
events = sel.select(timeout=None)
for key, mask in events:
# 处理客户端连接
if key.data is None:
# 接收客户端连接
handle_conn(key)
# 处理客户端事件
else:
# 读写客户端数据
handle_rw(key, mask)
复制代码
sel.select()
会返回一个元组列表,每个元组由 (selectorKey, events)
组成。其中,可以通过 selectorKey.fileobj
获取发生事件的 socket,而 events 表示就绪事件的掩码。通过 selectorKey.data
可以获取客户端传来的数据,如果 selectorKey.data
为 None
,则表示这个 selectorKey 是监听 65432 端口的 socket,因此是一个新的连接请求,需要把它注册到 selector
中,以便接收后续传来的数据。否则表示这是一个已连接客户端,直接接收它传来的数据即可。
接收客户端连接
通过 key.data is None
可以判定这个 key 是监听 65432 端口的 socket,需要通过这个 socket 接收新的连接请求,然后注册到 selector 中。
def handle_conn(sock):
# 接收连接
conn, addr = sock.accept()
# 将操作设为非阻塞模式
conn.setblocking(False)
# 创建一个 data 对象用来存储这个连接相关的数据
data = types.SimpleNamespace(addr=addr, inb=b'', outb=b'')
# 对于新的连接,需要关注可读事件(客户端有数据传来)和可写事件(允许将服务端数据写回客户端)
events = selectors.EVENT_READ | selectors.EVENT_WRITE
# 在 selector 注册新连接对应的 socket
sel.register(conn, events, data=data)
复制代码
处理客户端读写
客户端触发的事件可以分为 可读事件 和 可写事件 两种情况:
- 可读事件,表示客户端有新的数据发到服务端,需要服务端调用
recv()
从客户端对应的 socket 读取数据。 - 可写事件,表示客户端对应的 socket 能够接收数据,此时如果服务端有数据需要传给客户端的话,就可以通过
send()
或者sendall()
方法向客户端发送数据。
def handle_data(key, mask):
# 获取触发事件的 socket
sock = key.fileobj
# 读取数据,得到的 data 和用 register 绑定的 data 参数一样,
# 是一个 types.SimpleNamespace 对象
data = key.data
# 判断是否有可读事件
if mask & selectors.EVENT_READ:
# 接收数据,添加到 data 的 outb 属性
recv_data = sock.recv(1024)
if recv_data:
data.outb += recv_data
# 客户端发来空数据,表示数据发送完了
else:
print('closing connection to', data.addr)
# socket 即将关闭,之后 selector 不需要再监听这个 socket,因此需要
# 注销 selector 中的对应的 socket
sel.unregister(sock)
# 关闭 socket 连接
sock.close()
# 判断是否有可写事件
if mask & selectors.EVENT_WRITE:
# 将客户端发来的数据原样返回给客户端
if data.outb:
print('echoing', repr(data.outb), 'to', data.addr)
sent = sock.sendall(data.outb)
复制代码