看懂TCP协议
Posted 李嘉图杂文
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了看懂TCP协议相关的知识,希望对你有一定的参考价值。
预备知识
-
TCP提供可靠连接的原理:
-
每个TCP包都有一个序列号,接收方通过该序列号将响应数据包正确排序;也通过该序列号发现传输序列中丢失的数据包,并请求重传。 -
TCP并不使用顺序的整数作为数据包的序列号,而是通过一个计数器来记录发送的字节数。以字节数+上一个数据包的序列号的方式生成序列号。这样,需要重传的时候,只要把数据流用另一种方式分割成新的数据包,就可以让接收方重新接收正确的数据包流。 -
TCP不通过锁步(等待上一个接受被确认以后才发送下一个数据包)的方式进行通信。而是一次发送多个数据包。在某一时刻,发送方希望同时传输的数据量叫做 TCP窗口。 -
接收方的TCP实现可以通过控制发送方的窗口大小来减缓或暂停连接。这叫做 流量控制。 -
最后,如果TCP认为数据包被丢弃了,它会减少每秒发送的数据量,并假定网络正在变得拥堵。 -
大名鼎鼎的三次握手,四次挥手:
(这个是网图,我不小心加了水印,不知道怎么去掉)下图是TCP建立连接的过程。
SYN:“我想进行通信,这是数据包和初始序列号。”
SYN-ACK:“好的,这是我向你发送数据包的初始序列号。”
ACK:“好的。”
结束过程,在图里看起来也很简单:
FIN:“我们结束吧。”
ACK:“好的。”
FIN:“我们结束吧。”
ACK:“好的。”
(有些情况下,其实只需要三次挥手,即FIN,ACK-FIN,ACK)
但是,我初学到这里的时候有个疑惑,以上图为例:
服务端最后为什么接收客户端的ACK呢?这不是已经结束了吗,他怎么接受这个ACK的呢?
解答在后面。
-
TCP套接字
和UDP的套接字的概念是一样的,前一篇文章讲过,这里就不赘述了。
但是,TCP套接字有一个特殊的机制:
这个机制可以解决上面的问题。(客户端的TCP套接字和UDP没什么差别)
-
完整的服务端TCP套接字其实是两个套接字:
被动套接字(监听套接字)+ 主动套接字(连接套接字)
-
监听套接字(被动套接字):
-
连接套接字(主动套接字):
-
非常有助理解的一个细节:
而连接套接字是可以多个套接字共享同一个本地套接字名的。
一个监听套接字可以指挥操作系统生成多个同名的连接套接字,来和不同的客户端进行通信。
TCP通信的基本过程:
# Simple TCP client and server that send and receive 16 octets(字节)
import argparse, socket
# 如果接收的字节长度小于length,报错。
# recv必须在循环里实现recvall,否则可能会因为机器的缓存溢出问题导致数据传输不全的情况发生。
def recvall(sock, length):
data = b''
while len(data) < length:
more = sock.recv(length - len(data))
if not more:
raise EOFError('was excepting %d bytes but only received '
'%d bytes before the socket closed'
% (length, len(data)))
data += more
return data
def server(interface, port):
# 指定协议族,指定TCP协议
# 这里创建的套接字就是被动套接字(监听套接字)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 下面用setsockopt对sock进行额外的配置,这样的配置是对sock.sock的一种自定义写法,很多情况下是等价的。
# SOL_SOCKET是指原始的协议的级别 ;
# the SO_REUSEADDR flag tells the kernel to reuse a local socket in TIME_WAIT state,
# without waiting for its natural timeout to expire.
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((interface, port))
# listen(x)表示它程序希望套接字进行监听,x表示处于等待的连接的最大数目。
sock.listen(1)
print('Listing at', sock.getsockname())
while True:
# 套接字的监听调用实际上会返回一个新的套接字!就是下面的sc
# 监听套接字调用accept方法,产生连接套接字(主动套接字),然后才能真正的和客户端进行通信。
sc, sockname = sock.accept()
print('We have accepted a connection from', sockname)
print(sc)
print(' Socket name:', sc.getsockname())
print('Socket peer:', sc.getpeername())
message = recvall(sc, 16)
# repr的作用是,返回一个对象的 string 格式。
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 been 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': server}
parser = argparse.ArgumentParser(description='Send and receive over TCP')
parser.add_argument('role', choices=choices, help='选择要运行的程序(server or client)')
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)
上面的例子中,有一个细节片段:
# the SO_REUSEADDR flag tells the kernel to reuse a local socket in TIME_WAIT state,
# without waiting for its natural timeout to expire.
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
你也许会看不太懂第二个参数 _
这个参数设置涉及到TCP的关闭机制,不指定这个参数的话,在会话结束后,操作系统会保留已经不用了的连接套接字几分钟的时间(==监听套接字会被立刻关闭==)。
TCP显然是没有这样的逻辑问题的。
一旦应用程序认为某个TCP连接关闭了,操作系统实际上会在一个等待的状态中,把这个连接的记录最多保持4分钟。套接字处于这种状态时,任何最终的FIN数据包都是可以得到适当响应的。
但是很多程序中,这样的保留机制会造成端口的浪费,所以指定这个套接字选项, _ ,让处于这种半关闭状态的端口,不仅仅为刚结束的会话保留,其他程序也可以使用这个端口。
TCP的其他问题
你可能会好奇上面的程序中,为什么有 这样一个函数,没有它,为什么就会有缓存溢出问题?缓存溢出有什么影响吗?
-
缓存溢出问题是怎么产生的?
以数据流的形式发送/接收数据时,数据可能处于以下的三种状态之一:
下面以数据发送过程为例:
由于第三种情况可能出现,我们调用send()函数的时候,要检查返回值,需要在一个循环内进行send()调用。
bytes_sent = 0
while bytes_sent < len(message):
message_remaining = message[bytes_sent:]
bytes_sent += s.send(message_remaining)解决发送数据时的这种问题很简单,python为我们封装了sendall()方法,用就完事了。
recv没有这样的函数(这是因为recv应用场景更灵活多变),我们只能自己写一个recvall()函数。
但是,真正的问题来了!
我最多就是缓存溢出以后,等一会儿嘛,阻塞起来,等有资源之后再继续重传不就好了,看起来也没啥大问题吧?
学过操作系统的同学,必然会突然想起来一个事情:
有阻塞,就会有死锁!!!!!!
缓存溢出问题的致命之处在于它可能造成的死锁问题!!!
-
要发送的数据可能立即被本地接收,或者存储到缓存区。这可能是因为网卡或者系统正好空闲。 -
网卡正忙,缓存区也满。那么本进程会被阻塞,直到本地系统可以接受并传输数据。 -
最后一种情况介于两者之间,即一部分被接收了,但是接收这一部分数据之后,进程被阻塞了。这种情况下,我们发送数据调用的send()方法,会返回从数据串开始处已经被接收的字节数,剩余的数据则尚未被处理。 -
缓存溢出会导致死锁问题!
死锁的概念:
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
在我们的TCP通信问题中,当服务端和客户端的缓存区都被填满的时候,两个进程都被阻塞,就会陷入死锁的状态。
下面写一对可能产生死锁问题的server-client
# TCP client and server that leave too much data waiting
import socket, sys
# 服务端的功能是把客户端发送来的数据都转换成大写。
def server(host, port):
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('Listing 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)
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)
message = b'capitalize this!'
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 bytes 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()这个示例里,死锁是这样产生的:
客户端一次发送巨量的数据(比如1G),我们直接用sendall()发送数据块,会占满缓存区。客户端不再会调用recv()。于是客户端进入阻塞状态。
由于客户端不再接收数据,所以服务端要发送的数据会在缓存区排队,积累,知道占满缓存区,此时服务端的recv()还是没有结束,于是服务端的进程也进入阻塞状态。
死锁就这样产生了。
死锁的解决方案:
-
客户端和服务端通过套接字选项的设置,将阻塞关闭。像send(),recv()这样的方法在得知不能再发送数据的时候,会立刻返回。 -
多线程运行程序,比如把send(),和recv()分别运行在两个线程。
以上是关于看懂TCP协议的主要内容,如果未能解决你的问题,请参考以下文章