对于常见的网络协议来说,TCP是最为人知晓的,即便是一些业外人士只要对电脑网络有一定的了解也能说上几句,那么广为人知的TCP协议到底是什么呢,下边作为一个初学者的视角来浅谈一下
TCP(Transmission Control Protocol 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议 。常用于一次传输要交换大量报文的情形,如文件传输,远程登陆等。为了实现这种端到端的可靠传输,TCP必须规定传输层的连接建立与拆除的方式,数据传输格式,确认的方式,目标应用进程的识别以及差错控制和流量控制机制等。
TCP的协议数据单元被称为分段,TCP通过分段的交互来建立连接,传输数据,发出确认,进行差错控制,流量控制及关闭连接。
TCP分段格式
-
TCP协议段格式
-
源/目的端口号:表示数据从哪个进程来,到哪个进程去;
-
32位序号:该分段在发送方的数据流中的位置,用来保证到达数据顺序的序号
-
32位确认序号:下一个期望接收的分段序号,相当于是对方发送的并已经被本方正确接收的分段的确认
-
4位首部长度:TCP头长,以32位字长为单位。表示该TCP头部有多少个4字节,所以TCP头部最大长度为15*4=60,实际相当于给出数据在数据段中的开始位置
-
保留:占6比特,目前置为“0”
-
六位标志位:
URG:紧急指针是否有效,即解决报文插队的问题
ACK:确认号是否有效
PSH:提示接收端应用程序立刻从TCP缓冲区将数据读走(接收方的上层尽快取走)
RST:对方要求重新建立连接(复位报文段)
SYN:请求建立连接,与ACK合用以建立TCP连接
FIN:通知对方,本端要关闭了。称携带FIN标识的为结束报文段 -
16位窗口大小:可以理解成自己所能提供的缓冲区大小(填上自己的,给对方看,让对方根据这个数值来设置要发给自己的被滑动窗口传输的数据量)。由于窗口由16位bit所定义,所以接收端TCP 能最大提供65535个字节的缓冲
-
16位校验和:CRC校验。接收端不通过则认为数据有问题。此处的校验和不光包括TCP首部,也包括TCP数据部分
-
16位紧急指针:标识哪部分数据是紧急数据
-
40字节头部选项:暂时忽略
连接管理机制
TCP连接包括建立和拆除两个过程。TCP使用3次握手协议来建立连接。连接可以由任何一方发起,也可以由双方同时发起。一旦一台主机上的TCP软件已经主动发起连接请求,运行在另一台主机上的TCP软件就被动地等待握手。
建立连接(三次握手)
设主机B运行一个服务器进程,它先发出一个被动打开命令,告诉它的TCP要准备接收客户进程的连续请求,然后服务进程就处于听的状态。不断检测是否有客户进程发起连续请求,如有,作出响应。 设客户进程运行在主机A中,他先向自己的TCP发出主动打开的命令,表明要向某个IP地址的某个端口建立运输连接,过程如下:
1)主机A的TCP向主机B的TCP发出连接请求报文段,其首部中的同步比特SYN应置1,同时选择一个序号x,表明在后面传送数据时的第一个数据字节的序号是x。
2)主机B的TCP收到连接请求报文段后,如同意,则发挥确认。在确认报文段中应将SYN置为1,确认号应为x+1,同时也为自己选择一个序号y
3)主机A的TCP收到此报文段后,还要向B给出确认,其确认号为y+1
4)主机A的TCP通知上层应用进程,连接已经建立,当主机B的TCP收到主机A的确认后,也通知上层应用进程,连接建立。
释放连接(四次挥手)
在数据传输完毕之后,通信双方都可以发出释放连接的请求。释放连接的过程为如上图所示:
1)数据传输结束后,主机A的应用进程先向其TCP发出释放连接请求,不在发送数据。TCP通知对方要释放从A到B的连接,将发往主机B的TCP报文段首部的终止比特FIN置为1,序号u等于已传送数据的最后一个字节的序号加1。
2)主机B的TCP收到释放连接通知后发出确认,其序号为u+1,同时通知应用进程,这样A到B的连接就释放了,连接处于半关闭状态。主机B不在接受主机A发来的数据;但主机B还向A发送数据,主机A若正确接收数据仍需要发送确认。
3)在主机B向主机A的数据发送结束后,其应用进程就通知TCP释放连接。主机B发出的连接释放报文段必须将终止比特置为1,并使其序号w等于前面已经传送过的数据的最后一个字节的序号加 1,还必须重复上次已发送过的ACK=u+1。
4)主机A对主机B的连接释放报文段发出确认,将ACK置为1。这样才把从B到A的反方向连接释放掉,主机A的TCP再向其应用进程报告,整个连接已经全部释放。
可以根据上图看到客户端以及服务端状态变化:
客户端状态变化:
CLOSED->SYN_SENT:客户端调用connect,发送同步报文段
SYN_SENT->ESTABLISHED:connect调用成功,则进入ESTABLISHED状态,开始读写数据
ESTABLISHED->FIN_WAIT_1:客户端主动调用close时,向服务器发送结束报文段,同时进入FIN_WAIT_1
FIN_WAIT_1->FIN_WAIT_2:客户端收到服务器对结束报文段的确认就进入FIN_WAIT_2,开始等待服务器的结束报文段
FIN_WAIT_2->TIME_WAIT:客户端收到服务器发来的结束报文段,进入TIME_WAIT,并发出LAST_ACK
TIME_WAIT->CLOSED:客户端要等待一个2MSL(Max Segment Life,报文最大生存时间)的时间才进入CLOSE状态
服务端状态变化:
CLOSED->LISTEN:服务端调用listen后进入LISTEN状态,等待客户端连接
LISTEN->SYN_RCVD:一旦监听到客户端连接请求(同步报文段),就将该连接放入内核等待队列中,并向客户端发送 SYN确认报文
SYN_RCVD->ESTABLISHED:服务端一旦收到客户端的确认报文,就进入ESTABLISHED状态,可以进行读写数据了
ESTABLISHED->CLOSE_WAIT:当客户端主动关闭连接(调用close),服务器会收到结束报文段,服务器返回确认报文 段并进入CLOSE_WAIT
CLOSE_WAIT->LAST_ACK:进入CLOSE_WAIT后说明服务器准备关闭连接(处理完之前的数据);当服务端真正调用close 关闭连接时,会向客户端发送FIN,此时服务器进入LAST_ACK状态,等待最后一个ACK到来(这个 ACK是客户端确认收到了FIN)
LAST_ACK->CLOSED:服务器收到了对FIN的ACK,彻底关闭连接
服务拒绝式攻击
在连接管理机制这里,我们可以发现如果想要黑掉一台服务器是很容易的。因为TCP是面向连接的,也就是说当服务器端与客户端要进行数据通信时首先要建立连接,而在建立连接时服务器端与客户端的关系本身是一对多的,当一个服务器被多个客户端访问时就有可能在服务器端建立很多的连接,此时我们首先需要明确一点——服务器端管理连接是要耗费成本的——空间成本和时间成本。那么也就是说服务器端操作系统通过先描述再组织的方式要把连接管理起来,即在系统层面上创建一个结构体对象,而创建一个结构体对象是需要成本的(包括内存资源),连接越多所需要的资源越多,换而言之,要想黑掉一个服务器只需要使服务器挂上大量不做任何操作的非法连接,这样就会导致正常客户想要和服务器无法正常通信——操作系统因为资源问题不可能使连接创建好。
为什么是三次握手?
在了解了服务拒绝式攻击后,我们现在可以思考一个问题,建立连接为什么是三次握手而不是两次,抑或四次?
为什么不是两次握手?
两次握手会产生一个问题——最后一个响应报文(ACK)丢失,从而导致服务器端对客户端的请求进行回应(第二次握手)后,就会认为连接已经建立好。而如果客户端没有收到服务器端对其做出的回应呢?此时,客户端认为连接尚未建立,而服务器端会对已经建立的连接保存必要的资源,如果出现大量的这种情况,服务器端会崩溃。
为什么是三次握手?
我们看了上边的分析又会有一个疑问:既然没法确认第二次的握手,客户端是否可以收到,那么怎么确定第三次握手服务器端就可以收到呢?
这根本没法确定,因为完全可靠的通信协议是根本不存在的,我们任何的通信协议都是在接受这样的现实情况之上进行的。此时我们假设三次握手的最后一个ACK丢失,这时客户端认为连接已经建立好而服务器端认为没有,连接挂在了客户端上,服务器端就不用耗费资源去管理连接,有效的规避了服务器受到攻击的可能。
所以使用三次握手可以较为可靠的建立连接!
理解TIME_WAIT
释放连接时,提到一个名词–>TIME_WAIT,那么什么是TIME_WAIT?
首先我们可以通过如下测试:启动server,再启动一个client,然后使用Ctrl+C使server终止,这时立马运行server结果是:
这是因为,虽然server的应用程序终止了,但是TCP协议层的连接还没有完全断开,因此不能监听到同样的server端口。
-
TCP协议规定,主动关闭的一方要处于TIME_WAIT状态,等待两个MSL的时间才能回到CLOSED状态。 我们使用Ctrl+C终止了server,所以server是主动关闭连接的一方,在TIME_WAIT期间仍然不能再次监听同样的server端口。
TIME_WAIT为什么是2MSL呢
MSL是TCP报文的最大生存时间,因此TIME_WAIT持续在2MSL时间的话:
1.保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立即重启就会收到来自上一个进程迟到的数据,而这个数据可能是错误的)
2.在理论上保证最后一个报文可靠到达(假设最后一个ACK丢失了,那么服务器会重发一个FIN。这时候虽然客户端进程不在了,但是TCP连接还在,仍然可以重发LAST_ACK)
此时会出现一个新的问题:如果服务器需要处理大量的客户端连接,但是这些连接的生存周期很短,这个时候如果由服务器来主动关闭连接清除不活跃连接就会导致服务器上产生大量TIME_WAIT连接,从而导致服务器的端口不够用无法处理新的连接。
如何解决这个问题呢?
在server代码的socket()和bind()调用之间插入如下代码:
int opt = 1;
setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
引入了setsockopt()函数。使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符