网络套接字是用于在计算机网络上的节点内发送或接收数据的内部端点。具体来说,它是一个网络软件端点的代表(协议栈),例如表中的条目(列出通信协议、目标、状态等),是系统资源的一种形式。
术语套接字类似于物理母连接器,两个节点之间通过通道进行通信,通道被可视化为电缆,每个节点上都有两个公连接器插入套接字。类似地,术语port(女性连接器的另一个术语)用于节点上的外部端点,术语socket也用于本地进程间通信(IPC)的内部端点(不通过网络)。但是,这种类比有些牵强,因为网络通信不需要是一对一的,也不需要有专用的通信通道。
进程可以使用套接字描述符(句柄的一种类型)引用套接字。流程首先请求协议堆栈创建套接字,堆栈将向流程返回描述符,以便能够识别套接字。然后,当进程希望使用此套接字发送或接收数据时,将描述符传递回协议堆栈。
与端口不同,套接字是特定于一个节点的;它们是本地资源,不能被其他节点直接引用。此外,套接字不一定与用于两个节点之间通信的持久连接(通道)相关联,也不一定存在某个单独的其他端点。例如,数据报套接字可用于无连接通信,多播套接字可用于发送到多个节点。然而,在internet通信的实践中,套接字通常用于连接到特定的端点,并且通常使用持久连接。
在实践中,套接字通常指Internet协议(IP)网络中的套接字(其中套接字可以称为Internet套接字),特别是传输控制协议(TCP),它是一对一连接的协议。在这个上下文中,套接字被假定与一个特定的套接字地址相关联,即本地节点的IP地址和端口号,并且在外部节点(其他节点)上有一个对应的套接字地址,它本身有一个关联的套接字,由外部进程使用。将套接字与套接字地址关联称为绑定。
注意,虽然当地的过程可以与外国交流过程通过发送或接收数据从外国或套接字地址,它没有访问外国插座本身,也不能使用外国套接字描述符,因为这些都是外国内部的节点。例如,在10.20.30.40:4444和50.60.70.80:8888(本地IP地址:本地端口,外部IP地址:外部端口)之间的连接中,每一端都有一个关联的套接字,对应于该节点上协议栈对连接的内部表示。这些由数值套接字描述符局部引用,比如一边是317,另一边是922。10.20.30.40可以请求流程节点与节点通信50.60.70.80在端口8888(请求的协议栈创建一个套接字与该目的地),一旦创建了一个套接字和接收一个套接字描述符(317),它可以通过这个套接字通信使用描述符(317)。然后协议栈将数据转发到端口8888上的节点50.60.70.80,并从节点50.60.70.80转发数据。但是,节点10.20.30.40上的进程不能请求基于外部套接字描述符进行通信(例如。“套接字922”或“节点50.60.70.80上的套接字922”),因为这些是外部节点内部的,并且不能被节点10.20.30.40上的协议栈使用。
协议栈(现在通常由操作系统提供,而不是作为一个单独的库)是一组服务,允许进程使用堆栈实现的协议在网络上通信。程序使用网络套接字与协议栈通信所用的应用程序编程接口(API)称为套接字API。开发利用这个API的应用程序称为套接字编程或网络编程。
Internet套接字api通常基于Berkeley套接字标准。在Berkeley sockets标准中,套接字是文件描述符(文件句柄)的一种形式,这是由于Unix哲学“一切都是文件”,以及套接字与文件之间的类比。它们都具有读取、写入、打开和关闭功能。实际上,这种差异意味着这种类比是牵强的,而在套接字上使用不同的接口(发送和接收)。在进程间通信中,每个端通常都有自己的套接字,但这些套接字可能使用不同的api:它们由网络协议抽象。
在标准的Internet协议TCP和UDP中,套接字地址是IP地址和端口号的组合,很像电话连接的一端是电话号码和特定扩展名的组合。套接字不需要有源地址,例如,仅用于发送数据,但如果程序将套接字绑定到源地址,则套接字可用于接收发送到该地址的数据。基于这个地址,Internet套接字将传入的数据包发送到适当的应用程序进程。
与端口不同,套接字是特定于一个节点的;它们是本地资源,不能被其他节点直接引用。此外,套接字不一定与用于两个节点之间通信的持久连接(通道)相关联,也不一定存在某个单独的其他端点。例如,数据报套接字可用于无连接通信,多播套接字可用于发送到多个节点。然而,在internet通信的实践中,套接字通常用于连接到特定的端点,并且通常使用持久连接。
套接字(内部表示)、套接字描述符(抽象标识符)和套接字地址(公共地址)之间的区别很细微,在日常使用中没有仔细区分它们。此外,“套接字”的特定定义在不同的作者之间有所不同,而且常常特别指internet套接字或TCP套接字。
Internet套接字至少具有以下特征:
这个示例根据Berkeley套接字接口建模,通过TCP将字符串“Hello, world!”通过地址为1.2.3.4的主机端口80发送。它演示了一个套接字(getSocket)的创建,将它连接到远程主机,发送字符串,最后关闭套接字:
Socket socket = getSocket(type = "TCP")
connect(socket, address = "1.2.3.4", port = "80")
send(socket, "Hello, world!")
close(socket)
有几种类型的互联网套接字:
数据报套接字,也称为无连接套接字,它使用用户数据报协议(UDP)。
流套接字,也称为面向连接的套接字,它使用传输控制协议(TCP)、流控制传输协议(SCTP)或数据报拥塞控制协议(DCCP)。
原始套接字(或原始IP套接字),通常在路由器和其他网络设备中可用。在这里,传输层被绕过,包头被应用程序访问,地址中没有端口号,只有IP地址。
数据报套接字
数据报套接字是一种网络套接字,它为发送或接收数据包提供无连接点。在数据报套接字上发送或接收的每个数据包都被单独寻址和路由。数据报套接字不能保证顺序和可靠性,因此从一台机器或进程发送到另一台机器或进程的多个数据包可能以任意顺序到达,也可能根本没有到达。
在网络上发送UDP广播总是在数据报套接字上启用。为了接收广播数据包,应该将数据报套接字绑定到通配符地址。当数据报套接字绑定到更特定的地址时,还可以接收广播数据包。
流套接字
流套接字是一种网络套接字,它提供面向连接的、有序的、惟一的数据流,没有记录边界,具有定义良好的机制来创建和销毁连接以及检测错误。
流套接字可靠地、有序地、带外地传输数据。
在Internet上,流套接字通常是在TCP之上实现的,因此应用程序可以使用TCP/IP协议在任何网络上运行。SCTP也可以用于流套接字。
原始套接字
原始套接字是一种网络套接字,它允许直接发送和接收IP数据包,而不需要任何特定于协议的传输层格式化。
对于其他类型的套接字,有效负载将根据所选的传输层协议(例如TCP、UDP)自动封装,套接字用户不知道使用有效负载广播的协议头的存在。当从原始套接字读取时,通常包括头。当从原始套接字传输数据包时,自动添加头是可选的。
原始套接字用于与安全性相关的应用程序,如Nmap。原始套接字的一个可能用例是在用户空间中实现新的传输层协议。[3]原始套接字通常在网络设备中可用,用于路由协议,如Internet Group Management Protocol (IGMPv4)和Open short Path First (OSPF),以及ping实用程序使用的Internet Control Message Protocol (ICMP)
大多数套接字应用程序编程接口(api),例如基于Berkeley套接字的api,都支持原始套接字。Windows XP于2001年发布,在Winsock接口中实现了原始套接字支持,但三年后,出于安全考虑,微软限制了Winsock的原始套接字支持
其他
其他套接字类型是通过其他传输协议实现的,比如系统网络体系结构(SNA)。有关内部进程间通信,请参阅Unix域套接字(UDS)。
Socket和Sokcet的api都被用来网络传递信息。它们提供了一种进程间的通信(IPC)。网络可以是一个逻辑的、局域的网络对于计算机来说,或者一个物理上链接上的额外网络,有着自己对于其他网络的链接。一个明显的例子的就是因特网,你链接它通过你的网络供应商。
我们来学习以下如何去使用socket编程去构建服务器和客户端:
我们最终能理解怎么使用主要地功能和python socket模块中的方法去写一个自己的客户端-服务器应用。这包括展示给你看如何使用一个自定义的类去发送信息和数据在您可以为自己的应用程序构建和利用的端点之间。
相关的学习例子
Socket 应用最常见的类型就是 客户端/服务器 应用,服务器用来等待客户端的链接。我们教程中涉及到的就是这类应用。更明确地说,我们将看到用于 InternetSocket 的 Socket API,有时称为 Berkeley 或 BSD Socket。当然也有 Unix domain sockets —— 一种用于 同一主机 进程间的通信。
本模块主要的socket API函数和方法有:
稍后您将看到,我们将使用socket.socket()创建一个套接字对象,并将套接字类型指定为socket. sock_stream。当您这样做时,使用的默认协议是传输控制协议(TCP)。这是一个很好的默认值,可能也是您想要的。
为什么选择TCP:
相反,用户数据报协议(UDP)套接字是用套接字创建的。SOCK_DGRAM不可靠,接收方读取的数据可能与发送方的写入顺序不一致。
网络设备(例如路由器和交换机)具有有限的可用带宽和它们自身固有的系统限制。它们有cpu、内存、总线和接口包缓冲区,就像我们的客户机和服务器一样。TCP使您不必担心包丢失、数据无序到达以及在跨网络通信时经常发生的许多其他事情。
一个监听的套接字,只做监听,响应客户端的信息。调用accept()完成链接。
客户端调用connect()去创建链接开始三方握手。握手步骤很重要,因为它确保连接的每一端在网络中都是可访问的,换句话说,客户机可以到达服务器,反之亦然。可能只有一台主机、客户机或服务器可以到达另一台主机。
中间是往返部分,在这里,客户机和服务器之间使用send()和recv()调用交换数据。
在底部,客户机和服务器关闭各自的套接字。
#!/usr/bin/env python3
import socket
HOST = '127.0.0.1' # Standard loopback interface address (localhost)
PORT = 65432 # Port to listen on (non-privileged ports are > 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)
**注意:**现在不要担心理解上面的所有内容。在这几行代码中发生了很多事情。这只是一个起点,因此您可以看到一个基本的服务器正在运行。
我们会展开每个api调用,然后看一看发生了什么。
socket()创建一个套接字对象,该对象支持上下文管理器类型,因此可以在with语句中使用它。没有必要调用s.close():
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
pass # Use the socket object without calling s.close().
传递给套接字()的参数指定地址族和套接字类型。AF_INET是IPv4的Internet地址家族。SOCK_STREAM是TCP的套接字类型,该协议将用于在网络中传输消息。
bind()用于将套接字与特定的网络接口和端口号关联起来:
HOST = '127.0.0.1' # Standard loopback interface address (localhost)
PORT = 65432 # Port to listen on (non-privileged ports are > 1023)
# ...
s.bind((HOST, PORT))
bind()函数传递的值依赖于套接字的地址家族,我们使用了AF_INET(IPv4)家族。
所以它需要一个2位的元组(host,port)
如果传递一个空字符串,服务器将接受所有可用IPv4接口上的连接。
端口应该是1-65535之间的整数(保留0)。它是接收来自客户机的连接的TCP端口号。如果端口小于1024,一些系统可能需要root权限。
下面是使用bind()使用主机名的注意事项:
如果在IPv4/v6套接字地址的主机部分使用主机名,程序可能会显示不确定的行为,因为Python使用DNS解析返回的第一个地址。套接字地址将根据DNS解析和/或主机配置的结果以不同的方式解析为实际的IPv4/v6地址。对于确定性行为,在主机部分使用一个数字地址。
你使用主机名的话,有可能出现不同的结果,取决于域名解析过程。
调用listen()使我们的服务器能用accept()去接受一个链接。这让使套接字成为监听的。
s.listen()
conn, addr = s.accept()
listen()有一个backlog参数。它指定系统在拒绝新连接之前允许的未接受连接的数量。从Python 3.5开始,它是可选的。如果没有指定,则选择默认的backlog值。
如果你的服务器自发接受了一系列的链接请求,增加backlog的值可能会有助于设置最大队列中等待链接的最大长度。最大值是系统以来的,举个例子,看/proc/sys/net/core/somaxconn。
accept()阻塞而且等待即将来临的链接。当一个客户端链接时,它返回一个新的套接字对象代表了链接和一个元组拥有客户端的地址。元组会包含(host,port)对于ipv4链接。
ipv6(host,port,flowinfo,scopeid)对于ipv6.有关元组值的详细信息,请参阅参考部分中的套接字地址族。
一个事情对我们来说很必要去理解就是我们现在得到了一个套接字对象从accept()。这很重要因为它是套接字用来链接你的客户端。他跟你服务器用来监听新连接的套接字是不同的。
conn, addr = s.accept()
with conn:
print('Connected by', addr)
while True:
data = conn.recv(1024)
if not data:
break
conn.sendall(data)
在从accept()中获得了一个套接字对象之后,一个无限while循环用于循环对conn.recv()的阻塞调用。这将读取客户机发送的任何数据,并使用conn.sendall()进行回显。
如果 conn.recv()返回了一个空字节对象,b’ ’,然后客户端会关闭链接然后循环停止。这个状态用于conn去自动关闭套接字在阻塞结束后。
现在让我们看看客户端echo-client。
#!/usr/bin/env python3
import socket
HOST = '127.0.0.1' # The server's hostname or IP address
PORT = 65432 # The port used by the server
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() 去发送它素有的信息,然后使用recv()去接受服务器的回应,然后显示出来。
$ ./echo-server.py
此时,服务器被阻塞了。
$ ./echo-client.py
Received b'Hello, world'
$ ./echo-server.py
Connected by ('127.0.0.1', 64623)
一个socket的函数或者方法会暂时的延迟你的程序那就是阻塞调用。举个例子,accept(),connect(),send(),和recv()块。 它们不立即返回。阻塞调用得等待系统调用去完成(I/O)它们才能返回一个值。
阻塞的套接字调用可以被设置为非阻塞的模式,所以它们能够立即返回。如果你这么做,你必须至少去重构/重设计你的应用去操作你的套接字当它准备好的时候。
默认的,套接字总是创建于阻塞态。有关这三种模式的描述,请参阅有关套接字超时的说明。
去查看你的socket的状态,使用netstat。它在macOS、Linux和Windows上都是默认可用的。
这是从macos系统上启动了server之后显示的状态。
$ netstat -an
Active Internet connections (including servers)
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp4 0 0 127.0.0.1.65432 *.*
注意,本地地址是127.0.0.1.65432。如果echo-server.py使用HOST = "而不是HOST = ‘127.0.0.1’, netstat将显示如下:
$ netstat -an
Active Internet connections (including servers)
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp4 0 0 *.65432 *.*
本地地址是*65432。这意味着所有支持address家族的可用主机接口都将用于接收传入的连接。在本例中,在对socket()的调用中,套接字。使用了AF_INET (IPv4)。您可以在Proto列中看到:tcp4。
我已经修剪了上面的输出,只显示echo服务器。根据运行它的系统,您可能会看到更多的输出。需要注意的是Proto、Local Address和(state)列。在上面的最后一个例子中,netstat显示echo服务器使用IPv4 TCP套接字(tcp4),在所有接口上的端口65432上(*.65432),并且处于监听状态(LISTEN)。
另一种查看方法是使用lsof(列出打开的文件),以及其他有用信息。它在macOS上是默认可用的,如果还没有安装,可以使用包管理器安装在Linux上:
$ lsof -i -n
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
Python 67982 nathan 3u IPv4 0xecf272 0t0 TCP *:65432 (LISTEN)
当与-i选项一起使用时,lsof为您提供打开Internet套接字的命令、PID(进程id)和用户(用户id)。上面是echo服务器进程。
当没有服务器启动的时候:
$ ./echo-client.py
Traceback (most recent call last):
File "./echo-client.py", line 9, in <module>
s.connect((HOST, PORT))
ConnectionRefusedError: [Errno 61] Connection refused
有三种可能,防火墙阻塞。服务器未开启,输错端口号。
让我进一步看一看我们的服务器和客户端是如何链接的。
当使用了回环地址的时候(ipv4地址127.0.0.1 或者 IPv6 地址:1),data不会去离开我们的主机或者触及外部网络。有时候也使用localhost。
echo服务器肯定有其局限性。最大的问题是它只服务一个客户端,然后退出。echo客户机也有这个限制,但是还有一个额外的问题。当客户端执行以下调用时,s.recv()可能只返回一个字节,从b’ hello, world’返回b’ h ':
使用recv意味着它一次最大能够接受1024个字节,但不意味它会返回1024个字节。
send()也有这种行为。send()返回发送的字节数,它可能小于传入的数据的大小。您负责检查这一点,并调用send()发送所有数据所需的次数:
如果它没有一次性被发送,这是需要我们的程序来处理的。
我们使用sendall()避免了这样做:
与send()不同,此方法继续从字节发送数据,直到发送完所有数据或发生错误。成功返回None。
我们到现在有两个问题亟待解决:
我们应该这么做?有很多的方法去并发。最近的,一个方法就是使用异步i/o。asyncio 被引入到标准python库中。以往的方法是使用线程。
并发性的问题是很难正确处理。有许多微妙之处需要考虑和防范。只需要其中一项就可以显示出来,您的应用程序可能会突然以不那么微妙的方式失败。
我这样说并不是为了吓唬您不要学习和使用并发编程。如果您的应用程序需要扩展,如果您想要使用多个处理器或一个核心,那么这是必要的。然而,在本教程中,我们将使用一些比线程更传统、更容易理解的东西。我们将使用系统调用的祖辈:select()。
解决方法:使用selectors
select()允许您检查多个套接字上的I/O完成情况。因此,您可以调用select()来查看哪些套接字I/O已经准备好用于读取和/或写入。但这是Python,所以还有更多。我们将使用标准库中的selectors模块,因此使用最有效的实现,无论我们运行的操作系统是什么:
这个模块允许了高等级和有效的i/o多路共用,构建在select模块基元之上。我们鼓励用户使用这个模块,除非他们希望对所使用的os级原语进行精确控制。
即使这样,通过使用 select(),我们还不能并发运行,依赖于你的工作量,这个方法也可能很快。依赖于你的应用程序迅速要做什么当它服务于一个请求和很多的客户端需要支持的时候。
asyncio使用单线程协作多任务和事件循环来管理任务。用select(),我们能够写出自己版本的事件循环,虽然更加简单和同步。当使用多线程的时候,即使您具有并发性,我们目前也必须在CPython和PyPy中使用GIL。他有效地限制了我们并行工作的数量。
我想说这一切都解释了使用select()可能是一个特别好的选择。不要觉得你一定得用asyncio,西安测绘给你或者最新的asynchronous 库。通常,在网络应用程序中,应用程序是I/O绑定的:它可以在本地网络、网络另一端的端点、磁盘上等待,等等。
如果您从启动CPU绑定工作的客户端收到请求,请查看并发。期货模块。它包含使用进程池异步执行调用的ProcessPoolExecutor类。
如果您使用多个进程,那么操作系统可以安排您的Python代码在多个处理器或核心上并行运行,而不需要GIL。
我们将查看解决这些问题的服务器和客户机示例。它们使用select()同时处理多个连接,并根据需要多次调用send()和recv()。
import selectors
sel = selectors.DefaultSelector()
# ...
lsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
lsock.bind((host, port))
lsock.listen()
print('listening on', (host, port))
lsock.setblocking(False)
sel.register(lsock, selectors.EVENT_READ, data=None)
最大的不同在这个服务器和之前的服务器之间就是掉欧阳那个了lsock.setblocking(False) 去配置socket在一个非阻塞状态。调用使得整个socket不会再被阻塞。当使用sel.select(),就像你之后看到得,我们能够等待一个事件在一个或多个套接字,然后等他们准备好了,我们再进行写/读。
sel.register()为您感兴趣的事件注册要用sel .select()监挺的套接字。对于监听套接字,我们需要读取事件:selector . event_read。
data用于存储随套接字一起存储的任意数据。当select()返回时返回。我们将使用数据来跟踪套接字上发送和接收的内容。
接下来是事件循环:
import selectors
sel = selectors.DefaultSelector()
# ...
while True:
events = sel.select(timeout=None)
for key, mask in events: # key是fileobj ,mask是事件掩码。
if key.data is None:
accept_wrapper(key.fileobj)# 自己写得
else:
service_connection(key, mask)
select(timeout=None)阻塞,直到为I/O准备好套接字为止。它返回一个(键、事件)元组列表,每个套接字对应一个元组。key是一个SelectorKey namedtuple,它包含一个fileobj属性。关键。fileobj是套接字对象,mask是准备好的操作的事件掩码。
如果key.data是None,然后我们知道它来自监听套接字,我们需要accept()连接。我们将调用自己的accept()包装器函数来获取新的套接字对象并将其注册到选择器中。我们待会再看。
如果key.data不为None,然后我们知道这是一个已经被接受的客户机套接字,我们需要对它进行服务。然后调用service_connection()并传递key和掩码,其中包含对套接字进行操作所需的所有内容。
下面是 accept_wrapper() :
def accept_wrapper(sock):
conn, addr = sock.accept() # Should be ready to read
print('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)
因为监听套接字是为事件选择器注册的。EVENT_READ,它应该可以读取了。我们调用sock.accept(),然后立即调用conn.setblocking(False),使套接字处于非阻塞模式。
记住,这是这个版本服务器的主要目标,因为我们不希望它阻塞。如果阻塞,则整个服务器将停止,直到它返回。这意味着其他套接字还在等待。这是您不希望服务器处于的可怕的“挂起”状态。
接下来,我们使用class types.SimpleNamespace创建一个对象来保存我们希望包含的数据和套接字。因为我们想知道客户端连接什么时候可以读写,所以这两个事件都是使用以下设置的:
events = selectors.EVENT_READ | selectors.EVENT_WRITE
如之前的语句。
sel.register(lsock, selectors.EVENT_READ, data=None)
然后将事件掩码、套接字和数据对象传递给sell .register()。
下面service_connection():
def service_connection(key, mask):
sock = key.fileobj
data = key.data
if mask & selectors.EVENT_READ:
recv_data = sock.recv(1024) # Should be ready to read
if recv_data:
data.outb += recv_data
else:
print('closing connection to', data.addr)
sel.unregister(sock)
sock.close()
if mask & selectors.EVENT_WRITE:
if data.outb:
print('echoing', repr(data.outb), 'to', data.addr)
sent = sock.send(data.outb) # Should be ready to write
data.outb = data.outb[sent:] #删除
这是简单多连接服务器的核心。key 是一个named元组 从select()中返回,它包含了一个fileojb(socket)和data对象。
mask包含了准备状态的事件。
如果socket是可以读取的,然后掩码判断为True。然后调用recv(),所有的无数据都被添加到data.outb然后它一会可以被发送。
注意:没有数据接受到的话,会被阻塞。
if recv_data:
data.outb += recv_data
else:
print('closing connection to', data.addr)
sel.unregister(sock)
sock.close()
这意味着客户端已经关闭了它们的套接字,那么服务器也得关闭。但是不要忘记首先调用sel.unregister(),这样select()就不再监视它了。
当socket等待读取的时候,它必须是一个健康的socket,所有收到的数据都应该被存储到data.outb 显示到客户端通过使用sock.send().然后删除我们发送缓冲区。
通过start_connections():来初始化我们的链接:
messages = [b'Message 1 from client.', b'Message 2 from client.']
def start_connections(host, port, num_conns):
server_addr = (host, port)
for i in range(0, num_conns):
connid = i + 1
print('starting connection', connid, 'to', server_addr)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setblocking(False)
sock.connect_ex(server_addr)
events = selectors.EVENT_READ | selectors.EVENT_WRITE
data = types.SimpleNamespace(connid=connid,
msg_total=sum(len(m) for m in messages),
recv_total=0,
messages=list(messages),
outb=b'')
sel.register(sock, events, data=data)
num_conns从命令行读取,命令行是要创建到服务器的连接数。就像服务器一样,每个套接字都被设置为非阻塞模式。
使用connect_ex()代替connect(),因为connect()会立即引发BlockingIOError异常。使用connect_ex()代替connect(),因为connect()会立即引发BlockingIOError异常。connect_ex()最初返回一个错误指示器errno。当连接位于progressconnect_ex()中时,EINPROGRESS不会引发异常,而是最初返回一个错误指示器errno。EINPROGRESS,而不是在连接正在进行时引发异常。连接完成后,套接字就可以读写了,并通过select()返回套接字。一旦链接完成了,套接字等待读/写并且select返回。
等到socket被安装,我们希望与套接字一起存储的数据是使用类types.SimpleNamespace创建的。客户端会发送被客户端会使用list(messages)复制,因为每个链接会调用socket.send()然后修改list。每个事情需要被追踪什么是客户端需要发送的,已经发送的和接受的,总共的字节的信息都被存储在data对象中。
让我们看看 service_connection()。它与服务器基本相同:
def service_connection(key, mask):
sock = key.fileobj
data = key.data
if mask & selectors.EVENT_READ:
recv_data = sock.recv(1024) # Should be ready to read
if recv_data:
print('received', repr(recv_data), 'from connection', data.connid)
data.recv_total += len(recv_data)
if not recv_data or data.recv_total == data.msg_total:
print('closing connection', data.connid)
sel.unregister(sock)
sock.close()
if mask & selectors.EVENT_WRITE:
if not data.outb and data.messages:
data.outb = data.messages.pop(0)
if data.outb:
print('sending', repr(data.outb), 'to connection', data.connid)
sent = sock.send(data.outb) # Should be ready to write
data.outb = data.outb[sent:]
有一个重要的区别。它跟踪从服务器接收到的字节数,以便能够关闭连接的这一侧。当服务器检测到这一点时,它也会关闭连接的另一端。
注意,通过这样做,服务器依赖于客户端是否表现良好:服务器希望客户端在发送消息后关闭其连接端。如果客户机没有关闭,服务器将保持连接打开。在实际应用程序中,您可能希望在服务器中防止这种情况发生,并防止客户端连接累积,如果它们在一定时间之后没有发送请求。
与我们开始时相比,多连接客户机和服务器示例无疑是一个改进。但是,让我们再走一步,在最后的实现中解决前面的“multiconn”示例的缺点:应用程序客户机和服务器。
我们想要的是一个客户端和服务器可以正确处理错误所以其他的链接不能影响。显然,如果没有捕捉到异常,我们的客户机或服务器不应该在愤怒中崩溃。这是我们到目前为止还没有讨论过的事情。为了使示例简洁明了,我有意省略了错误处理。
现在你熟悉了基本的api,没有阻塞的socket,和select(),我们可以加入一些错误处理并讨论一下我一直瞒着你藏在那边那块大窗帘后面的“房间里的大象”。
是的,我说的是我在介绍中提到的定制类。我就知道你不会忘记。
首先,让我们来解决这些错误:
所有错误都会引发异常。可以引发无效参数类型和内存不足条件的正常异常;从Python 3.3开始,与套接字或地址语义相关的错误会引发OSError或其子类之一。
我们需要抓住OSError。关于错误,我还没有提到的另一件事是超时。您将在文档的许多地方看到对它们的讨论。超时会发生,并且是一个“正常”错误。主机和路由器重启,交换机端口坏了,电缆坏了,电缆被拔掉了,你能想到的都有。您应该为这些错误和其他错误做好准备,并在代码中处理它们。
那“房间里的大象”呢?正如socket类型中的socket.SOCK_STREAM所暗示的。SOCK_STREAM,当使用TCP时,您从一个连续的字节流中读取数据。这就像从磁盘上的文件中读取数据,但实际上是从网络中读取字节。
但是,与读取文件不同,没有f.seek()。换句话说,如果有socket指针,就不能重新定位它,也不能随意移动读取数据的位置。
当字节到达套接字时,涉及到网络缓冲区。一旦你读了它们,它们就需要保存在某个地方。再次调用recv()将从套接字读取下一个可用的字节流。
这意味着您将以块的形式从套接字中读取数据。您需要调用recv()并将数据保存在缓冲区中,直到您读取了足够的字节,从而获得对您的应用程序有意义的完整消息为止。
由您来定义和跟踪消息边界的位置。就TCP套接字而言,它只是向网络发送和接收原始字节。它不知道这些原始字节是什么意思。
这就要求我们定义一个应用层协议。什么是应用层协议?简单地说,您的应用程序将发送和接收消息。这些消息是应用程序的协议。
换句话说,为这些消息选择的长度和格式定义了应用程序的语义和行为。这与我在上一段关于从套接字读取字节的解释直接相关。当您使用recv()读取字节时,您需要了解读取了多少字节,并计算出消息边界的位置。
这是怎么做到的?一种方法是始终发送固定长度的消息。如果它们总是一样的大小,那就很简单了。当您将这个字节数读入缓冲区时,您就知道您有了一个完整的消息。
然而,对于需要使用填充填充的小消息,使用固定长度的消息效率很低。此外,您仍然面临一个问题,即如何处理不适合一个消息的数据。
我们将采用一种通用方法。许多协议(包括HTTP)都使用这种方法。我们将在消息前面加上一个头,其中包括内容长度以及我们需要的任何其他字段。通过这样做,我们只需要跟上标题。一旦我们读取了报头,我们就可以处理它来确定消息内容的长度,然后读取这个字节数来使用它。
我们将通过创建一个自定义类来实现这一点,该类可以发送和接收包含文本或二进制数据的消息。您可以为自己的应用程序改进和扩展它。最重要的是,您将能够看到这是如何实现的一个例子。
我需要提到一些关于套接字和字节的事情,它们可能会影响您。如前所述,当通过套接字发送和接收数据时,您是在发送和接收原始字节。
如果您接收数据并希望在将其解释为多个字节(例如4字节整数)的上下文中使用它,则需要考虑它可能不是本机CPU的格式。另一端的客户机或服务器的CPU使用的字节顺序可能与您自己的不同。如果是这种情况,则需要在使用它之前将其转换为主机的本机字节顺序。
这个字节顺序称为CPU的endianness。有关详细信息,请参阅参考部分中的字节Endianness。我们将通过对消息头使用Unicode并使用编码UTF-8来避免这个问题。由于UTF-8使用8位编码,因此不存在字节排序问题。