1 tcp的连接特性
对于tcp或者udp稍微了解一点的同学,肯定知道一句话,tcp是基于连接的,而udp是无连接的。怎么理解这句话呢?
无论tcp还是udp,都是用来传输数据的一种协议,都处于网络层之上的传输层。也都需要通信双方的重新向操作系统申请端口,这是通信的前提。
但不一样的是,udp发送方收到上层应用层程序(不一定非得是应用层协议,也可以自己写的网络程序)需要发送的数据后,直接发送出去,无论接收方是否收到,反正有数据给我,我就发送。
而tcp在发送数据之前,是需要先通过三次握手建立连接的。在发送过程中,要求收到数据后的一方回复ACK,以示收到数据。发送完成后,可以通过四次挥手关闭连接,也可以维持连接等待下一次数据的发送(也就是所谓的TCP长连接)。
还有一点要说明的是,udp跟ip协议是不区分客户端和服务器的,也就是说它提供的是P2P的通信。虽然我们平时所见的,基于udp上的dns、tftp、dhcp、radius等等应用层协议有客户端和服务器之分,那那也是应用层协议自己设计如此,udp本身是不区分的。
tcp是区分客户端和服务器的,三次握手建立连接的过程,一定是客户端主动发起的,而服务器是被动响应。四次挥手拆掉连接的过程,既可以是客户端主动发起,也可以是服务器主动发起。只要一方没有数据发送给对端,那么就可以主动关闭连接,而不管你是客户端还是服务器。
正是因为有连接的建立和关闭过程,因此tcp引入了状态机的概念,当客户端或者服务器发送或者接收到不同的报文时,就进入不同的状态。
2 三次握手
2.1 三次握手的状态机
我们先看三次握手建立连接时,报文的交互和状态机的变化,如下图:
2.2 python如何控制tcp三次握手
可以看到,对于客户端而言:
(1)当上层程序要通过tcp发送数据时,会调用一个方法【比如python的的socket.connect()】,通知tcp发送数据包【SYN】建立连接,并把状态机改为【SYN-SENT】状态;
(2)当客户端tcp收到服务端tcp的数据包【SYN/ACK】时,改变状态机到【ESTABLISHED】状态,表示从客户端到服务器的连接已经成功建立起来。
对于服务器而言:
(1)上层程序如果需要对外提供tcp服务时,会调用方法【比如python中的socket.bind((ip, port))】向tcp申请一个端口,ip是向操作系统申请,可以是一个网卡的ip,也可以是所有网卡的ip。申请成功后,继续调用方法【比如python中的socket.listen()】开始在tcp分配的端口上,监听客户端发来的连接建立请求,tcp会把状态机改为【Listen】状态;
(2)当服务器tcp收到客户端的数据包【SYN】后,就会切换状态机到【SYN-RCVD】状态,并立即回复【SYN/ACK】报文;
(3)当服务器tcp收到客户端的回复报文【ACK】后,改变状态机到【ESTABLISHED】状态,表示从服务器到客户端的连接已经成功建立起来。
2.3 三次握手时协商了哪些参数
如上图所示,为客户端【10.140.6.44】与服务器【10.74.97.122】三次握手的报文--【SYN】【SYN/ACK】【ACK】。三次握手除了建立双向的tcp连接以外,其实客户端和服务器还协商了各自支持的一些参数,参数的协商是通过【SYN】和【SYN/ACK】报文来完成的。
2.3.1 SYN报文中,【Flags】字段中的【SYN】标志位置为1,表示这是SYN报文。
在TCP固定头部里的【Window size value】,表示客户端通过此SYN报文告诉服务器,自己此刻接收窗口的大小。这是TCP的流控机制里面一个参数,后文会讲。另外TCP的【Options】字段出现了,一般都会携带【MSS】、【SACK】、【NOP】字段。
【MSS】 :占4个字节,客户端用此字段告诉服务器,自己发送的TCP数据段的最大长度,以字节为单位;
【SACK】:占2个字节,客户端用此字段告诉服务器,自己支持SACK,即选择确认机制,后文会讲;
【NOP】 :占1个字节,由于【Options】被定义为4字节的整数倍,因此如果出现非4字节的option时,用此option实现4字节对齐;
2.3.2 SYN/ACK报文中,它的【Flags】字段中的【SYN】和【ACK】标志位都被置为1,表示这是SYN/ACK报文。
在TCP固定头部里的【Window size value】,表示服务器通过此SYN/ACK报文告诉客户端,自己此刻接收窗口的大小,流控机制是双向的,任何一个TCP报文都会存在。
由于SYN报文出现了【Options】字段,因此SYN/ACK也会利用【Options】来与SYN协商参数,如果支持就回复自己对应字段的值,如果不支持此字段,就为空。
2.4 三次握手的python实现
我们可以通过python来调用tcp收发数据,即通过前文中的【socket.recv()】和【socket.sendall()】方法控制tcp收发数据。但是,这两个方法我们都没有提供目的ip和端口,数据往谁发送,又是从哪里接收数据?
其实这也说明了tcp的连接特性,我们在收发数据之前,是需要提前建立连接,在建立连接的过程中,我们会提供目的ip和端口。当然了,tcp是不会主动帮你应用程序建立连接的,需要你调用对应的方法,它才能帮你做事。
如下所示,为服务器向tcp申请端口,并监听连接的代码。
# !/usr/bin python3
import socket
import contextlib
import logging
@contextlib.contextmanager
def server_prepare_conn(ip, port, backlog=5):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as tcp_sk:
logging.debug("Try to apply for one port from os......")
try:
tcp_sk.bind((ip, port))
except Exception:
raise
else:
tcp_sk.listen(backlog)
logging.debug("Succeeded to apply one port from os")
try:
yield tcp_sk
except Exception:
raise
如下所示,为客户端要求tcp建立连接的代码。
# !/usr/bin python3
import socket
import contextlib
import logging
@contextlib.contextmanager
def client_setup_conn(ip, port):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as tcp_sk:
logging.debug("Try to setup a connection......")
try:
tcp_sk.connect((ip, port))
except Exception:
raise
else:
logging.debug("Succeeded to setup a connection")
try:
yield tcp_sk
except Exception:
raise