第一个版本在1974年定义,建立在网际层协议(IP)提供的数据包传输技术之上。TCP使程序可以使用连续的数据流进行相互通信。
除非网络原因导致连接中断/冻结,TCP都能保证将数据流完好无缺地传输至接收方,不会发生丢包、重包、乱序问题。
传输文档/文件的协议都使用TCP,包括浏览网页、文件传输、电子邮件的所有主要机制,也是人机间进行长对话的协议的基础之一,如SSH/聊天协议
经过30年的改进,现代TCP相当精良,除了协议设计专家,很少有人能再改进现代TCP栈的性能,就算是消息队列这种对性能要求很高的程序,
也会选择TCP作为传输媒介。
经典定义来自1981年的RFC 793,如何提供可靠连接,基本原理:
下一个数据包序列号就为8224。网络栈无需记录如何将数据流分割为数据包,需要重传,可以使用另一种分割方式,将数据流分为多个新数据包(需要传输更多
字节,可以将更多数据包装入一个数据包),接收方仍然能够正确接收数据包流。
就有可能攻击这个会话了。
就能一口气发送多个数据包。某一时刻,发送方希望同时传输的数据量叫做TCP窗口(Win)的大小。
更多数据包的传输。如果还有数据到达,这些数据会被丢弃
导致通信双方在一定时间(如20s)无法通信,直到路由器重启,才恢复正常。网络重连时,TCP通信双方认为网络负载过重,一开始就会拒绝向对方发送大型数据
对程序可见的只有数据流,实际的数据包和序列号都被OS的网络栈巧妙的隐藏了。
TCP几乎成为了普遍情况下两个互联网程序进行通信的默认选择,仍然有些情况,TCP并不是最适用的:
通信结束,还需要另外3个/4个数据包来关闭连接,可以只发送3个:FIN、FIN-ACK、ACK,比较快速。也可以是4个,每个方向都发送一对FIN和ACK。故,
完成一个请求总共需要最少6个数据包,这种情况下,协议设计者会很快考虑改用UDP。
需要考虑的问题:Cli是否想打开一个TCP,通过该连接在几min/几h内向同一台Serv发送许多单独的请求。三次握手的时间开销只需一次。一旦连接建立,
每个实际请求和响应都只需一个数据包,却能充分利用TCP在重传、指数退避、流量控制方面的智能支持。
Cli和Serv不存在长时间连接的情况下,UDP更合适,尤其客户端太多,一台典型的Serv如果要为每台相连的Cli保存单独的数据流,可能会内存溢出。
直至成功送达是无济于事的。反之,Cli应该从传达的数据包中任意选择一些组合成一段音频(智能音频协议,会用前一段音频的高度压缩版本,作为数据包的开始
部分,同样将其后继音频压缩,作为数据包的结束部分),继续进行后续操作,就像没有发生丢包一样。如果使用TCP,这是不可能的,因为TCP会固执的重传丢失信息,
即使信息早已过时无用也不例外。UDP数据报通常是互联网实时多媒体流的基础。
TCP也使用port来区分同一IP上运行的不同程序,知名port和临时port的划分与UDP完全一致。
这意味着TCP的connect()调用与UDP的connect()调用是不同的。TCP的connect()调用有可能是失败的。远程主机可能不应答、拒绝连接、协议错误,如立即收到一个RST('重置')数据包,因为TCP流连接,涉及两台主机间持续连接的建立。另一方的主机需要处于正在监听的状态,并做好连接请求的准备。
因为TCP的标准POSIX接口包含了两种截然不同的socket类型: “被动”监听socket和主动“连接”socket。
不能用于发送/接收任何数据,也不表示任何实际的网络会话。而是有Serv指示被动socket通知OS,首先使用哪个特定的TCP端口来接受连接请求。
主机进行通信。可以通过该socket发送/接收数据,无需担心数据是如何划分为不同数据包的。该通信流就像是Unix系统的管道/文件,可将TCP的连接套接字传给另一个
接收普通文件作为输入的程序,该程序永远不会知道其实正在进行网络通信。
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-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)
$ 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无需关心。
->在调用流socket的send()时需要检查返回值,还需要在一个循环内进行send()调用,这样程序就会不断发送剩余的数据,直至整个字节串均被发送。可能会看到如下形式的循环:
bytes_sent = 0
while bytes_send < len(message):
message_remaining = message[bytes_sent:]
bytes_send += s.send(message_remaining)
这就是必须在一个循环汇总调用recv()的原因。OS无法得知Cli和Serv之间传输的是16位固定宽度的数据,无法猜测传来的数据何时能组成程序所需的完整信息,因此受到数据时会立即返回。
两种不同类型的流套接字,
1)监听套接字(listening packet),服务器通过监听套接字设定某个端口,用于监听连接请求。
2)连接套接字(connected socket),表示Serv与某一特定Cli正在进行会话
$ 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()来发送整个数据块。
一旦Cli和Serv完成所有需要的通信,就调用close()方法关闭socket,通知OS将输出缓冲区中剩余的数据传输完成,使用FIN数据包的关闭流程来结束TCP会话。
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数据包,希望最后能收到响应。
然而就算是通信结束的数据包,自身也可能丢失,并需要重传多次,直至另一方最终接收。解决方案为,一旦应用程序认为某个TCP最终关闭了,OS的网络栈会在一个
等待状态中,将该连接的记录最多保存4分钟。RFC命名为CLOSE-WAIT和TIME-WAIT。当关闭的socket还处于其中某一状态时,最终的FIN数据包都是可以得到适当响应的。
如果TCP实现要忽略某个连接,就无法用某个适当的ACK为FIN做出响应了。
进行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-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处理问题时,就要更为小心仔细,还要维护两个连续数据块之间的一些状态。
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
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
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产生的数据量达到这一值后,最终也会停止。
1)如果Serv在遇到文件结束符前,永远读取数据的话,Cli如何避免在socket上进行完整的close()操作?
2)Cli怎样防止很多recv()调用来接收Serv的响应?
解决方法:将socket"半关",即在一个方向上永久关闭通信连接,但不销毁socket。这种状态下,Serv再也不会读取任何数据,但它仍然能够想Serv发送剩余的响应,因为该方向的连接仍然是没有关闭的。
>>> 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()时,很有帮助
原文:https://www.cnblogs.com/wangxue533/p/11986645.html