Socket API 的调用顺序和 TCP 的数据流:
左边表示服务器, 右边则是客户端
服务器创建「监听」 Socket 的 API 调用:
- socket()
- bind()
- listen()
- accept()
「监听」 Socket 做的事情就像它的名字一样。 它会监听客户端的连接, 当一个客户端连接进
来的时候, 服务器将调用 accept()
来「接受」 或者「完成」 此连接
客户端调用 connect() 方法
来建立与服务器的链接, 并开始三次握手
。 握手很重要是因为它
保证了网络的通信的双方可以到达, 也就是说客户端可以正常连接到服务器, 反之亦然
中间部分往返部分表示客户端和服务器的数据交换过程, 调用了send() 和 recv() 方法
客户端 / 服务器打印程序
打印程序的服务端
import socket
HOST = '127.0.0.1' # 标准的回环地址 (localhost)
PORT = 65432 # 监听的端口 ( 非系统级的端口: 大于 1023)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
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 对象, 并且支持 上下文管理器, 你可以使用 with 语
句, 这样你就不用再手动调用 s.close() 来关闭 socket 了
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
调用 socket() 时传入的 socket 地址族参数socket.AF_INET
表示因特网 IPv4 地址族, SOCK_STREAM
表示使用 TCP 的 socket 类型, 协议将被用来在网络中传输消息
bind()
用来关联 socket 到指定的网络接口( IP 地址) 和端口号:
HOST = '127.0.0.1'
PORT = 65432
…
s.bind((HOST, PORT))
bind() 方法
的入参取决于 socket 的地址族
, 在这个例子中我们使用了 socket.AF_INET
(IPv4), 它将返回两个元素的元组: (host, port)
-
host
可以是主机名称、 IP 地址、 空字符串, 如果使用 IP 地址, host 就应该是 IPv4 格式的字符串, 127.0.0.1 是标准的 IPv4 回环地址, 只有主机上的进程可以连接到服务器, 如果你传了空字符串, 服务器将接受本机所有可用的 IPv4 地址 -
端口号
应该是 1-65535 之间的整数( 0 是保留的) , 这个整数就是用来接受客户端链接的
TCP 端口号, 如果端口号小于 1024, 有的操作系统会要求管理员权限
listen() 方法
调用使服务器可以接受连接请求, 这使它成为一个「监听中」 的 socket
s.listen()
conn, addr = s.accept()
listen() 方法
有一个 backlog 参数。 它指定在拒绝新的连接之前系统将允许使用的 未接受的连接 数量。 从 Python 3.5 开始, 这是可选参数。 如果不指定, Python 将取一个默认值
accept() 方法
阻塞并等待传入连接。 当一个客户端连接时, 它将返回一个新的 socket 对象, 对象中有表示当前连接的 conn 和一个由主机、 端口号组成的 IPv4/v6 连接的元组
这里必须要明白我们通过调用 accept() 方法拥有了一个新的 socket 对象
。 这非常重要, 因为你将用这个 socket 对象和客户端进行通信。 和监听一个 socket 不同的是后者只用来授受新的连接请求
with conn:
print('Connected by', addr)
while True:
data = conn.recv(1024)
if not data:
break
conn.sendall(data)
从 accept()
获取客户端 socket 连接对象 conn 后, 使用一个无限 while 循环来阻塞调用
conn.recv()
, 无论客户端传过来什么数据都会使用 conn.sendall()
打印出来
如果 conn.recv() 方法返回一个空 byte 对象( b'' )
, 然后客户端关闭连接, 循环结束,with 语句和 conn 一起使用时, 通信结束的时候会自动关闭 socket 链接
打印程序的客户端
import socket
HOST = '127.0.0.1' # 服务器的主机名或者 IP 地址
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('Received', repr(data))
与服务器程序相比, 客户端程序简单很多。 它创建了一个 socket 对象, 连接到服务器并且调用 s.sendall() 方法
发送消息, 然后再调用 s.recv() 方法
读取服务器返回的内容并打印出来
socket传输的数据为二进制数据(字节)
处理多个连接
打印程序的服务端肯定有它自己的一些局限。 这个程序只能服务于一个客户端然后结束。 打印程序的客户端也有它自己的局限, 但是还有一个问题, 如果客户端调用了下面的方法 s.recv() 方法
将返回 b’Hello, world’ 中的一个字节 b’H’
data = s.recv(1024)
1024 是缓冲区数据大小限制最大值参数 bufsize , 并不是说 recv() 方法只返回 1024 个字节的内容
send() 方法也是这个原理, 它返回发送内容的字节数, 结果可能小于传入的发送内容, 你得处理这处情况, 按需多次调用 send() 方法来发送完整的数据
应用程序负责检查是否已发送所有数据; 如果仅传输了一些数据, 则应用程序需要尝试
传递剩余数据 引用
可以使用 sendall() 方法
来回避这个过程
和 send() 方法不一样的是, sendall() 方法会一直发送字节, 只到所有的数据传输完成
或者中途出现错误。 成功的话会返回 None 引用
有两个问题:
- 如何同时处理多个连接请求
- 我们需要一直调用 send() 或者 recv() 直到所有数据传输完成
应该怎么做呢, 有很多方式可以实现并发。 最近, 有一个非常流程的库叫做 Asynchronous
I/O 可以实现, asyncio 库在 Python 3.4 后默认添加到了标准库里面。
用一个非常古老的系统调用:select()
select() 允许你检查多个 socket 的 I/O 完成情况, 所以你可以使用它来检测哪个 socket I/O
是就绪状态从而执行读取或写入操作, 但是这是 Python, 总会有更多其它的选择, 我们将使
用标准库中的selectors 模块
, 所以我们使用了最有效的实现, 不用在意你使用的操作系统:
asyncio 使用单线程来处理多任务, 使用事件循环来管理任务。 通过使用 select() , 我们可
以创建自己的事件循环, 更简单且同步化。