读书笔记_python网络编程3_(3)

时间:2023-03-08 22:34:21
读书笔记_python网络编程3_(3)

3.TCP:传输控制协议

第一个版本在1974年定义,建立在网际层协议(IP)提供的数据包传输技术之上。TCP使程序可以使用连续的数据流进行相互通信。

除非网络原因导致连接中断/冻结,TCP都能保证将数据流完好无缺地传输至接收方,不会发生丢包、重包、乱序问题。

传输文档/文件的协议都使用TCP,包括浏览网页、文件传输、电子邮件的所有主要机制,也是人机间进行长对话的协议的基础之一,如SSH/聊天协议

经过30年的改进,现代TCP相当精良,除了协议设计专家,很少有人能再改进现代TCP栈的性能,就算是消息队列这种对性能要求很高的程序,

也会选择TCP作为传输媒介。

3.1. TCP工作原理:数据包被隐藏在协议层之下,程序只需向目标机器发送流数据,TCP会将丢失的信息重传,保证信息成功到达目标机器

经典定义来自1981年的RFC 793,如何提供可靠连接,基本原理:

3.1.1. 每个TCP数据包都有一个序列号,接收方通过该序列号将响应数据包正确排序,也可通过该序列号发现传输序列中丢失的数据包,并请求进行重传。

3.1.2. TCP不使用顺序整数作为数据包的序列号,而是通过计数器来记录发送的字节数。如,包含1024字节的数据表的序列号为7200,

下一个数据包序列号就为8224。网络栈无需记录如何将数据流分割为数据包,需要重传,可以使用另一种分割方式,将数据流分为多个新数据包(需要传输更多

字节,可以将更多数据包装入一个数据包),接收方仍然能够正确接收数据包流。

3.1.3. 一个优秀的TCP实现中,初始序列号是随机选择的,如果TCP序列号易于猜测,伪造数据包就容易多了,可以将数据包伪造成一个会话的合法数据,

就有可能攻击这个会话了。

3.1.4. TCP不通过锁不方式进行通信,如果通过这种方式,必须等待每个数据包都被确认接收后,才能发送下一个数据包,速度非常慢。TCP无需等待响应,

就能一口气发送多个数据包。某一时刻,发送方希望同时传输的数据量叫做TCP窗口(Win)的大小。

3.1.5. 接收方的TCP实现,可以通过控制发送方的窗口大小,来减缓/暂停连接,叫做流量控制(flow control)。使接收方在输入缓冲区已满时,可以禁止

更多数据包的传输。如果还有数据到达,这些数据会被丢弃

3.1.6. TCP认为数据包被丢弃了,会嘉定网络正在变得拥挤,会减少每秒发送的数据量。对无线网络/简单噪声丢包的媒体是灾难,会破会运行良好的连接,

导致通信双方在一定时间(如20s)无法通信,直到路由器重启,才恢复正常。网络重连时,TCP通信双方认为网络负载过重,一开始就会拒绝向对方发送大型数据

对程序可见的只有数据流,实际的数据包和序列号都被OS的网络栈巧妙的隐藏了。

3.2. 何时使用TCP:使用Py进行的大多数网络通信都是基于TCP,可能在整个职业生涯都不会在代码中有意构造UDP数据包。(查询UDP主机名时,可能会在后台运行)

TCP几乎成为了普遍情况下两个互联网程序进行通信的默认选择,仍然有些情况,TCP并不是最适用的:

3.2.1. Cli只需向Serv发送单个较小的请求,且请求完成后,无后续通信,使用TCP来处理就有些复杂了。

3.2.2.两台主机间建立TCP连接需要3个数据包,组成了一个序列-SYN、SYN-ACK、ACK

3.2.2.1. SYN:“我想进行通信,这是数据包的初始序列号。”

3.2.2.2. SYN-ACK:“好的,这是我向你发送数据包的初始序列号。”

3.2.2.3. ACK:“好的!”

通信结束,还需要另外3个/4个数据包来关闭连接,可以只发送3个:FIN、FIN-ACK、ACK,比较快速。也可以是4个,每个方向都发送一对FIN和ACK。故,

完成一个请求总共需要最少6个数据包,这种情况下,协议设计者会很快考虑改用UDP。

需要考虑的问题:Cli是否想打开一个TCP,通过该连接在几min/几h内向同一台Serv发送许多单独的请求。三次握手的时间开销只需一次。一旦连接建立,

每个实际请求和响应都只需一个数据包,却能充分利用TCP在重传、指数退避、流量控制方面的智能支持。

Cli和Serv不存在长时间连接的情况下,UDP更合适,尤其客户端太多,一台典型的Serv如果要为每台相连的Cli保存单独的数据流,可能会内存溢出。

3.2.3. 当丢包现象发生,程序有比简单重传数据聪明的方法,不适用TCP。如,进行音频通话,有1s的数据由于丢包而丢失,只是简单不断重发这1s的数据,

直至成功送达是无济于事的。反之,Cli应该从传达的数据包中任意选择一些组合成一段音频(智能音频协议,会用前一段音频的高度压缩版本,作为数据包的开始

部分,同样将其后继音频压缩,作为数据包的结束部分),继续进行后续操作,就像没有发生丢包一样。如果使用TCP,这是不可能的,因为TCP会固执的重传丢失信息,

即使信息早已过时无用也不例外。UDP数据报通常是互联网实时多媒体流的基础。

3.3. TCP套接字的含义

TCP也使用port来区分同一IP上运行的不同程序,知名port和临时port的划分与UDP完全一致。

3.3.1. 使用UDP通信只需一个套接字。Serv可以打开一个UDP端口,然后从数千个不同的Cli接收数据报。也可以通过connect()将一个数据报socket与另一个连接。如此,该socket就只能使用send()想与之连接的socket发送数据包,recv()调用也只会接收来自特定socket的数据包。如果使用sendto(),由程序决定数据包的唯一目标IP并且忽略所有IP,效果与connect()完全相同。

3.3.2.使用TCP这样支持状态的流协议,connect()调用就是后续所有网络通信所依赖的首要步骤。只有OS的网络栈完成了协议握手,TCP流的双方才算做好了准备

这意味着TCP的connect()调用与UDP的connect()调用是不同的。TCP的connect()调用有可能是失败的。远程主机可能不应答、拒绝连接、协议错误,如立即收到一个RST('重置')数据包,因为TCP流连接,涉及两台主机间持续连接的建立。另一方的主机需要处于正在监听的状态,并做好连接请求的准备。

3.3.3. "Serv端"不进行connect()调用,而是接收Cli端connect()调用的初始SYN数据包。对于Py程序,Serv接受连接请求的过程中,还完成了新建套接字。

因为TCP的标准POSIX接口包含了两种截然不同的socket类型: “被动”监听socket和主动“连接”socket。

3.3.3.4. 被动套接字(passive socket)/监听套接字(listening packet),维护了“套接字名” -IP与port。Serv通过该socket来接受连接请求,但该socket

不能用于发送/接收任何数据,也不表示任何实际的网络会话。而是有Serv指示被动socket通知OS,首先使用哪个特定的TCP端口来接受连接请求。

3.3.3.5. 主动套接字(active socket)/连接套接字(connected socket),将一个特定的IP及port和某个与其进行远程会话的主机绑定。连接socket只与该特定

主机进行通信。可以通过该socket发送/接收数据,无需担心数据是如何划分为不同数据包的。该通信流就像是Unix系统的管道/文件,可将TCP的连接套接字传给另一个

接收普通文件作为输入的程序,该程序永远不会知道其实正在进行网络通信。

3.3.4. 被动socket由接口IP和正在监听的port来唯一标识,任何其他应用程序都无法再使用相同IP和port。多个主动socket是可以共享同一个本地socket名,如:

1000个Cli和1台繁忙的Serv都进行着HTTP链接,那么就会有1000个主动socket绑定到了Serv的公共IP和TCP的80端口,而唯一标识主动socket的是四元组:

(local_ip, local_port, remote_ip, remote_port)

OS是通过这个四元组来为主动TCP连接命名的。接收到TCP数据包时,OS会检查他们的源IP和目标IP是否与系统中的某一主动socket相符。

3.4. 一个简单的TCPServ和Cli

#3-1. 简单TCPServ和Cli
#chapter03/tcp_sixteen.py
import argparse, socket def recvall(sock, length):
data = b''
while len(data) < length:
more = sock.recv(length - len(data))
if not more:
raise EOFError('was expeting %d bytes but only received %d bytes before the socket closed' % (length, len(data)))
data += more
return data def server(interface, port):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((interface, port))
sock.listen(1)
print('Listening at', sock.getsockname())
while True:
sc, sockname = sock.accept()
print('We have accepted a connection from', sockname)
print(' Socket name:', sc.getsockname())
print(' Socket peer:', sc.getpeername())
message = recvall(sc, 16)
print(' Incoming sixteen-octet message:', repr(message))
sc.sendall(b'Farewell, client')
sc.close()
print(' Reply sent, socket closed') def client(host, port):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, port))
print('Client has ben assigned socket name', sock.getsockname())
sock.sendall(b'Hi there, server')
reply = recvall(sock, 16)
print('The server said', repr(reply))
sock.close() if __name__ == '__main__':
choices = {'client': client, 'server': client}
parser = argparse.ArgumentParser(description='Send and receive over TCP')
parser.add_argument('role', choices=choices, help='which roke to play')
parser.add_argument('host', help='interface the server listens at; host the client sends to')
parser.add_argument('-p', metavar='PORT', type=int, default=1060, help='TCP port (default 1060)')
args = parser.parse_args()
function = choices[args.role]
function(args.host, args.p)

3.4.0. UDP的connect()调用只是对绑定套接字进行了配置,设置后续send()或recv()调用所要使用的默认远程地址,不会导致任何错误,而TCP的connect()调用则是真实的网络操作,会在要通信的Sevr和Cli间进行三次握手,意味着connect()可能失败。如,Serv没有运行时执行Cli。

$ python tcp_deadlock.py client localhost
Sending 16 bytes of data, in chunks of 16 bytes
Traceback (most recent call last):
...
ConnectionRefusedError: [Errno 111] Connection refused
由于不需再程序中处理任何丢包 现象,TCP-Cli逼UDP-Cli简单多了。使用TCP的send()来发送数据时无需停下来检查远程主机是否接收到了数据,使用recv()接收数据时也无需考虑请求重传的情况。网络栈会保证进行必要的重传,Cli无需关心。

3.4.0.1. TCP把发送和接收的数据简单地看做流,流是没有开始或结束标记的,TCP会自行将流分为多个数据包。UDP要么是“发送这个数据报”,要么是“接收一个数据报”,每个数据报都是原子的。数据报中的数据是自包含的,只有成功或失败两种状态。接收到的只可能是完整无损的数据报。

3.4.0.2. TCP可能会在传输过程中,把数据流分为多个大小不同的数据包,然后在接收端将这些数据包逐步重组。

3.4.0.3. 进行一次TCP send()时,OS的网络栈会碰到三种情况之一:

3.4.0.3.1. 要发送的数据立即被本地系统的网络栈接收。可能是由于网卡正好空闲,可用于立即发送数据;也可能因为系统还有空间,可以将数据复制到临时的发送缓冲区,程序可以继续运行了。send()会立即返回。由于发送的是整个串,返回值就是数据串的长度。
3.4.0.3.2. 网卡正忙,该socket的发送缓冲区也已满,OS也无法/不愿为其分配更多空间。此时,send()默认情况会直接阻塞进程,暂停应用,知道本地网络栈能够接受并传输数据。
3.4.0.3.3. 发送缓冲区几乎满了,但尚有空间,想要发送的部分数据可以进入缓冲区的队列等待发送。但剩余的数据块则必须等待。send()会立即返回从数据串开始处起已经被接收的字节数,剩余数据则尚未被处理。
->在调用流socket的send()时需要检查返回值,还需要在一个循环内进行send()调用,这样程序就会不断发送剩余的数据,直至整个字节串均被发送。可能会看到如下形式的循环:
bytes_sent = 0
while bytes_send < len(message):
message_remaining = message[bytes_sent:]
bytes_send += s.send(message_remaining)

3.4.0.4. 每次需要传输数据块时,socket提供了一个sendall()方法,3-1就是用了该方法。sendall()是用C实现的,逼自己实现的循环要快。在循环中释放了GIL,其他py线程在所有数据发送完成前不会竞争资源。

3.4.0.5. 对于recv()调用,它同样可能遇到传输不完整的问题,但是并没有相应的标准库封装。OS的内部recv()实现所用的逻辑与发送数据包的逻辑类似。

3.4.0.6. 没有数据,recv()会阻塞程序,直到有数据传到。

3.4.0.7. 接收缓冲区内的数据完整就绪,recv()会接收所需所有数据

3.4.0.8. 接收缓冲区只有recv()需要返回的部分数据,即使并非所需全部内容,也会立即返回已经有的数据。

这就是必须在一个循环汇总调用recv()的原因。OS无法得知Cli和Serv之间传输的是16位固定宽度的数据,无法猜测传来的数据何时能组成程序所需的完整信息,因此受到数据时会立即返回。

3.4.0.9. Py中包含sendall(),没有相应的recv()?由于如今很少使用定长消息。多数洗衣对待部分到达流的处理方法很复杂。消息能够猜测还有多少行数据没有传达前,就需要读取并处理已传达的部分消息了。如,HTTP响应包含头信息,一行空行,然后在Content-Length头中给出的任意字节数据。至少在接收到头信息并解析得到响应内容的长度前,不知道要连续运行recv()次数,这种细节最好是在程序中处理,而不是在标准库中提供方法

3.4.1. 每个会话使用一个套接字

两种不同类型的流套接字,

1)监听套接字(listening packet),服务器通过监听套接字设定某个端口,用于监听连接请求。

2)连接套接字(connected socket),表示Serv与某一特定Cli正在进行会话

3.4.1.1. 监听socket调用accept()后实际会返回一个新建的连接socket。

3.4.1.2. Serv通过运行bind()来声明一个特定port,此时还没有决定该程序会被作为Serv还是Cli,还没有确定程序是主动发出连接请求,还是被动等待接收连接请求,只是简单声明一个用于本程序的特定port,该port可以用于某一特定IP/所有接口,如果Cli不想使用随机分配的任意临时port,而是特定port连接Serv,也可以使用该调用。

3.4.1.3. listen(),希望socket进行监听,才真正决定了程序作为服务器。TCP-socket上运行该调用,会彻底转变该socket的角色,改变后再也不能用于发送/接收数据。该socket只能通过accept()来接受连接请求(唯一目的就是用于支持TCP-socket的监听功能),每次调用accept()都会等待一个新的Cli连接Serv,返回一个全新的socket。新建的socket负责管理对应的新建socket。

3.4.1.4. getsockname()同时适用于监听socket+连接socket,获取socket正在使用的绑定TCP端口。获取连接socket对应的Cli-IP,运行getpeername(),也可存储accept()的第二个返回值(即Cli-socket名),两种方法返回的地址是一样的。

$ python tcp_sixteen.py
server ""
Listening at ('0.0.0.0', 1060)
We have accepted a connection from ('127.0.0.1', 52538)
Socket name: ('127.0.0.1', 1060)
Socket peer: ('127.0.0.1', 52538)
Incoming sixteen-octet message: b'Hi there, server'
Reply sent, socket closed

通过Clie向Serv发起一次连接,输出:

$ python tcp_sixteen.py
client 127.0.0.1
Client has ben assigned socket name ('127.0.0.1', 52538)
The server said b'Farewell, client'

从余下的Serv代码中,在accept()返回一个连接socket后,该连接socket与Cli-socket的通信模式是完全相同的。recv()调用会在接受完毕后返回数据。如果要保证数据全部发送,使用sendall()来发送整个数据块。

3.4.1.5. Serv-socket在调用listen()时,传入了一个int参数,指明了处于等待的连接的最大数目。如果Serv没有通过accept()调用为某连接创建socket,那么该连接就会被压栈等待。如果栈中等待的连接超过设置的MAX_waited,OS会忽略新的连接请求,推迟后续三次握手。

一旦Cli和Serv完成所有需要的通信,就调用close()方法关闭socket,通知OS将输出缓冲区中剩余的数据传输完成,使用FIN数据包的关闭流程来结束TCP会话。

3.4.2. 地址已被占用

Serv在绑定port前,为什么要设置socket的SO_REUSEADDR选项?

如果这行代码注释掉,运行Serv,就能看到不设置的结果。如果只是重启Serv,没有效果,但是先启动Serv,运行Cli,再重启Cli,会报错。

$ python tcp_sixteen.py server ""
Listening at ('127.0.0.1', 1060)
Waiting to accept a new connection
^C
Traceback (most recent call last):
...
KeyboardTnterrupt
$ python tcp_sixteen.py server ""
Listening at ('127.0.0.1', 1060)
Waiting to accept a new connection
$ python tcp_sixteen.py server
Traceback (most recent call last):
...
OSError: [Errno 98] Address alread in use

bind()应该是可以不断重复使用的,但是因为有一个Cli已经连接就不行了,不断尝试没有设置SO_REUSEADDR选项的情况下运行Serv的话,

该地址在上一次Cli连接几分钟后才变得可用。该限制是因为OS的网络栈需要非常谨慎地处理连接的关闭。仅用于监听的Serv-socket是可以立即关闭并被OS忽略的。

对于实际与Cli进行通信的连接socket就不行了。即使Cli和Serv都关闭了连接并向对方发送了FIN数据包,连接socket也无法立即消失,即使网络栈发送了最后一个数据包,

将socket关闭,还是无法确认该数据包是否可以被接收。如果该数据包正好被网络丢弃了,另一方就无法得知该数据包长时间无法传达的原因,会重新发送FIN数据包,希望最后能收到响应。

3.4.3. TCP这样可靠协议在停止通信时都会有类似的问题。逻辑上来讲,一些表示通信结束的数据包必须是无需接收响应的,否则OS在关机前都会无限等待结束消息。

然而就算是通信结束的数据包,自身也可能丢失,并需要重传多次,直至另一方最终接收。解决方案为,一旦应用程序认为某个TCP最终关闭了,OS的网络栈会在一个

等待状态中,将该连接的记录最多保存4分钟。RFC命名为CLOSE-WAIT和TIME-WAIT。当关闭的socket还处于其中某一状态时,最终的FIN数据包都是可以得到适当响应的。

如果TCP实现要忽略某个连接,就无法用某个适当的ACK为FIN做出响应了。

3.4.4. 当一个Serv试图声明某个几分钟前运行的连接所使用的port时,是在声明一个仍在使用的port。这就是bind()时返回错误的原因。通过设定socket选项SO_REUSEADDRSO,可以告诉程序,能够使用一些Cli之前的连接,正在关闭的port。

3.5. 绑定接口

进行bind()调用时,使用IP和port的二元组作为OS接收连接请求的网络接口。如果使用本地IP-127.0.0.1,不会接收来自其他机器的连接请求。
OS甚至都没有通知Serv收到的一个连接请求被拒绝(如果OS运行了Farewall,Cli在试图连接时,可能会不断等待,而不是‘拒绝连接’的异常信息)
如果使用空字符串作为hostname运行Serv,Py的bind()就知道,希望接收来自机器任意运行的网络接口的连接请求。这样Cli就能连接另一台主机了。
$ python tcp_sixteen.py server ""
Listening at ('0.0.0.0', 1060)
We have accepted a connection from ('127.0.0.1', 53641)
Socket name: ('127.0.0.1', 1060)
Socket peer: ('127.0.0.1', 53641)
Incoming sixteen-octet message: b'Hi there, server'
Reply sent, socket closed
OS使用特殊IP地址0.0.0.0表示“接收传至任意接口的连接请求”。Py隐藏了这一不同点,只需使用空字符即可。

3.6. 死锁: 当两个程序共享有限的资源时,由于糟糕的计划,只能一直等待对方结束资源占用的情况

3.6.1. 使用TCP时,死锁现象很容易发生,典型的TCP栈使用了缓冲区,就可以在应用程序准备好读取数据前,存放接收到的数据,也可在网络硬件准备好读取数据前,存放要发送的数据。缓冲区的大小通常有限制,OS不想让程序用未发送的数据将RAM填满。

3.6.2. 3-1的Serv-Cli模式中,双方总是读取另一方发来的完整数据后,才发送响应信息,如果Serv和Cli没有立即读取数据,导致有太多数据在等待处理,会遇到麻烦。

3-2中,Serv的任务是将任意数量的文本转换为大写形式。由于Cli的请求量可能非常大,试图读取整个输入流后再做处理的话,会耗尽OS的RAM。故,该服务器每次只读取并处理1024字节的小型数据块。
import argparse, socket, sys

def server(host, port, bytecount):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((host, port))
sock.listen(1)
print('Listening at', sock.getsockname())
while True:
sc, sockname = sock.accept()
print('Processing up to 1024 bytes at a time from', sockname)
n = 0
while True:
data = sc.recv(1024)
if not data:
break
output = data.decode('ascii').upper().encode('ascii')
sc.sendall(output) # send it back uppercase
n += len(data)
print('\r %d bytes processed so far' % (n, ), end='')
sys.stdout.flush()
print()
sc.close()
print(' Socket closed') def client(host, port, bytecount):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
bytecount = (bytecount + 15) // 16 * 16 # round up to a multiple of 16
message = b'captialize this!' # 16-byte message to repeat over and over
print('Sending', bytecount, 'bytes of data, in chunks of 16 bytes')
sock.connect((host, port))
sent = 0
while sent < bytecount:
sock.sendall(message)
sent += len(message)
print('\r %d byets sent' % (sent,), end=' ')
sys.stdout.flush() print()
sock.shutdown(socket.SHUT_WR) print('Receiving all the data the server sends back') received = 0
while True:
data = sock.recv(42)
if not received:
print(' The first data received says', repr(data))
if not data:
break
received += len(data)
print('\r %d bytes received' % (received, ), end=' ')
print()
sock.close() if __name__ == '__main__':
choices = {'server': server, 'client': client}
parser = argparse.ArgumentParser(description='Get deadlocked over TCP')
parser.add_argument('role', choices=choices, help='which role to play')
parser.add_argument('host', help='interface the server listens at; host the client sends to')
parser.add_argument('bytecount', type=int, nargs='?', default=16, help='number of bytes for client to send (default 16)')
parser.add_argument('-p', metavar='PORT', type=int, default=1060, help='TCP port (default 1060)')
args = parser.parse_args()
function = choices[args.role]
function(args.host, args.p, args.bytecount)
无需做什么构造或分析,就能很容易将任务分割---只要在原始ASCII字符上运行upper(),该操作可以在每个输入块上独立运行,无需担心之前或之后处理的数据块。

如果Serv要进行一个像title()这样更复杂的操作的话,就没有这么容易了。如,某个单词刚好由于数据块的边界从中间被一分为二,之后却没有进行适当的重组,该单词

中间的字母也有可能被转换为大写格式。如,一个特定的数据流被分割为大小为16字节的数据块,就会引入错误:

>>> message = 'the tragedy of macbeth'
>>> blocks = message[:16], message[16:]
>>> ''.join( b.upper() for b in blocks ) # works fine
'THE TRAGEDY OF MACBETH'
>>> ''.join( b.title() for b in blocks ) # whoops
'The Tragedy Of MAcbeth'
如果要处理UTF-8的Unicode数据,使用定长块来划分数据同样会产生问题。因为包含多个字节的字符可能会从中间被分成两个二进制块。这种情况下,

Serv处理问题时,就要更为小心仔细,还要维护两个连续数据块之间的一些状态。

3.6.3. 任何情况下,处理输入时,像这样每次只处理一个数据块对Serv来说都是较为明智的行为,即使1024字节的数据块对于如今的Serv和网络来说已经微不足道了。通过分块处理数据并及时发回响应,Serv限制了任意时刻需要保存在RAM中的数据量。如果Serv这样设计,即使每个Cli发送的数据流多达几M,Serv也能在同一时刻处理数百个Cli,且不会令RAM或其他硬件难堪重负。

3.6.4. 较小的数据流,3-2中的Serv和Cli可以正确工作。为了方便处理,不管提供的参数是多少字节,都会向上近似为16的倍数。

python tcp_deadlock.py client 127.0.0.1 32
Sending 32 bytes of data, in chunks of 16 bytes
32 byets sent
Receiving all the data the server sends back
The first data received says b'CAPTIALIZE THIS!CAPTIALIZE THIS!'
32 bytes received

3.6.5. 3-2的Serv和Cli是有可能发生死锁的。如果试图发送大到一定程度的数据,如总大小为1G的数据流:

python tcp_deadlock.py client 127.0.0.1 1073741824
Sending 1073741824 bytes of data, in chunks of 16 bytes
1315616 byets sent

Cli和Serv会疯狂刷新终端窗口,不断更新发送/接收的数据量大小。连接会突然中断,Serv会先停止,Cli也会停止,Serv输出如下:

python tcp_deadlock.py server ""
Listening at ('0.0.0.0', 1060)
Processing up to 1024 bytes at a time from ('127.0.0.1', 63235)
655680 bytes processed so far

Cli终止时发送的数据流要多655040B

$ python tcp_deadlock.py client 127.0.0.1 16000000
Sending 16000000 bytes of data, in chunks of 16 bytes
1310720 byets sent

3.6.6. 为什么Serv和Cli都会停止?

Serv的输出缓冲区和Cli的输入缓冲区最后都会被填满。然后,TCP就会使用滑动窗口协议来处理这种情况。socket会停止发送更多数据,因为即使发送,这些数据也会被丢弃,然后进行重传。

为什么会导致死锁?

考虑每个数据块的传输过程。Cli使用sendall()发送数据块,Serv使用recv()接收、处理,将数据转换为大写,再次使用sendall()将结果传回。由于还有数据需要发送,Cli此时没有运行任何recv()调用。故,越来越多的数据填满了OS的缓冲区,缓冲区无法再接收更多数据了。

OS在Cli的接收队列中缓冲了大约0.65M的数据,网络栈就任务客户端接收缓冲区已满。此时,Serv阻塞了sendall()调用,发送缓冲区被渐渐填满,Serv进程也被OS暂停,无法发送更多数据。此时Serv不再处理任何数据,没有运行recv()调用。Serv缓冲区的数据会不断增加,而OS对CLi发送缓冲区队列中数据量的限制可能在0.65M左右。当CLi产生的数据量达到这一值后,最终也会停止。

3.6.7. 网络Serv与Cli在处理大量数据时怎样避免进入死锁?

3.6.7.1. Serv和Cli可通过socket选项将阻塞关闭。像send()和recv()这样的调用在得知还不能发送数据时,就会立即返回。

3.6.7.2. 程序可以使用某种技术,同时处理来自多个输入的数据。可以采用多个线程/进程来处理(如,一个用来向socket发送数据,另一个负责从socket读取数据);也可运行select()或poll()等OS调用,使程序在发送socket和接收socket繁忙时等待,当中任何一个空闲时做出响应。

3.6.7.3. 使用UDP不会发生死锁,因为UDP并没有实现流量控制。如果传达的数据报数量超出了接收端的处理能力,那么UDP会直接将部分数据报丢弃,由应用程序来发现数据报的丢失。

3.7. 已关闭连接,半开连接。

3.7.1. 代码3-2展示了socket对象在遇到文件结束时的处理方法:在调用read()时,如果发现数据已读取结束,会返回一个空字符串。Py-socket对象与之相同,在socket关闭时会返回一个空字符串

3.7.2. 代码3-1中,严格规定了用于交换的信息大小为16B,故不用担心,无需将关闭socket作为通信完成的信号。socket的关闭是惰性的。Serv和Cli之间发送信息后,可以保持socket的打开状态,再关闭socket,无需担心一方会等待另一socket的关闭而阻塞

3.7.3. 3-2中,Cli发送任意数量的数据,数据长度仅由用户提供的参数决定。故Serv也会处理并发送回任意数量的数据。可以看到一个模式---一个在recv()返回空字符串前一直会运行的while循环。非阻塞socket中,这种—Py的模式就不适用了。因为只要没有数据可用,非阻塞socket的recv()调用就可能会抛出一个异常,需要使用其他技术来确定socket是否已经关闭。

3.7.4. Cli在socket完成发送后调用了shutdown(),解决了一个重要问题:

1)如果Serv在遇到文件结束符前,永远读取数据的话,Cli如何避免在socket上进行完整的close()操作?

2)Cli怎样防止很多recv()调用来接收Serv的响应?

解决方法:将socket"半关",即在一个方向上永久关闭通信连接,但不销毁socket。这种状态下,Serv再也不会读取任何数据,但它仍然能够想Serv发送剩余的响应,因为该方向的连接仍然是没有关闭的。

3.7.5. shutdown()调用可以用来关闭双向socket中任一方向的通信连接。3-2中的socket就是双向套接字。该调用的参数可以是下述3个符号之一:

3.7.5.1. SHUT_WR:最常用的参数值,表示不再向socket写入数据,对方也不会再读取任何数据,并认为遇到了文件结束符。

3.7.5.2. SHUT_RD:关闭接收方向的socket流。设置了该值,当对方尝试送发送更多数据时,会引发文件结束错误。

3.7.5.3. SHUT_RDWR:将socket两个方向的通信都关闭。如果OS允许多个程序共享同一个socket,则close()仅结束了调用它的进程与socket的关系,此时如果其他进程仍然在使用该socket,该socket仍然是可用的。而shutdown()就不同了,一旦shutdown()嗲用,该socket对于所有使用它的进程会立即变得不可用。

3.7.6. 由于无法通过标准socket()调用来创建单向socket,一般会先创建双向socket,然后在socket连接后立即运行shutdown(),来关闭不需要的连接方向。意味着,如果对方意外在不需要的方向上发送了数据,OS的缓冲区也不会被无意义的填充。

3.7.7. 在需要单向语义的socket上立即运行shutdown(),还能为对方提供清晰的错误信息,对方就不会混淆,也不会尝试在不需要的方向上发送数据。否则,意外数据会被直接忽略,甚至可能将缓冲区填满。这样,由于这些数据永远不会被读取,会导致死锁的发生。

3.8. 想使用文件一样使用TCP流

3.8.1. TCP对数据流的支持与普通文件的读取和写入顺序数据,Py进行了区分,文件对象可以read()和writer(),而socket只能send()和recv()。

3.8.2. 像操作Py文件对象一样操作socket,想把该socket传给希望执行此操作的代码,如pickle、json和zlib等很多Py模块,能直接从文件读取/写入数据。故,Py为每个socket提供了makefile()方法,该方法返回一个Py文件对象,该对象会在底层调用recv()和send()

>>> import socket
>>> sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
>>> hasattr(sock, 'read')
False
>>> f = sock.makefile()

在类Unix的OS上,socket与普通Py文件一样,都有一个fileno()方法,可以在需要时获取文件描述符编号,将其提供给底层调用,在select()和poll()时,很有帮助

3.9. 小结

3.9.1. TCP的“流"socket提供了所有必需的功能,包括重传丢失数据包、重新排列接收到的顺序错误的数据包,及将大型数据流分割为,针对特定网络的最优大小的数据包。这些功能提供了对网络上两个socket之间传输并接受数据流的支持。

3.9.2. TCP与UDP一样,使用port来区分同一台机器上可能存在的多个流断点。想要接受TCP连接请求的程序,需要通过bind()绑定到一个port,在socket上运行listen(),进入一个循环,不断运行accept(),为每个连接请求,新建一个socket(该socket用于与特定Cli进行通信).如果程序想要连接到已经存在的Serv-port,只需新建一个socket,调用connect()连接到一个地址。

3.9.3. Serv通常有要为绑定的socket设置SO_REUSEADDR选项,防止同一port最近运行的,正在关闭中的连接,阻止OS进行绑定

3.9.4. 数据是通过send()和recv()来发送和接收的。基于TCP的协议会对数据进行标记,Serv和Cli就能自动得知通信何时完成。其他协议吧TCP-socket看作真正的流,会不断发送和接收数据,知道文件传输结束。socket方法shutdown()用来为socket生成一个方向上的文件结束符(所有socket本质上都是双向的),同时保持另一方向的链接处于打开状态。

3.9.5. 通信双方都写数据,socket缓冲区被越来越多的数据填满,而这些数据从未被读取,就可能发生死锁。最终,某个方向上再也无法通过send()发送数据,可能会永远等待缓冲区清空,从而导致阻塞

3.9.6. 想把一个socket传递给一个支持读取/写入普通对象的Py模块,可使用makefile()方法,该方法返回一个Py对象。调用方需要读取及写入数据时,该对象会在底层调用recv()和send()