TCP真的可靠吗

TCP真的可靠吗

一、TCP的特性

TCP保证可靠性:

1.序列号、确认应答、超时重传

  • 数据到达接收方,接收方需要发出一个确认应答,表示已经收到该数据段,并且确认序号会说明了它下一次需要接收的数据序列号
  • 如果发送发迟迟未收到确认应答,那么可能是发送的数据丢失,也可能是确认应答丢失,这时发送方在等待一定时间后会进行重传。这个时间一般是2*RTT(报文段往返时间)+一个偏差值。

2.窗口控制与高速重发控制/快速重传(重复确认应答)

  • TCP会利用窗口控制来提高传输速度,意思是在一个窗口大小内,不用一定要等到应答才能发送下一段数据,窗口大小就是无需等待确认而可以继续发送数据的最大值。如果不使用窗口控制,每一个没收到确认应答的数据都要重发。
  • 使用窗口控制,如果数据段1001-2000丢失,后面数据每次传输,确认应答都会不停地发送序号为1001的应答,表示我要接收1001开始的数据,发送端如果收到3次相同应答,就会立刻进行重发;
  • 但还有种情况有可能是数据都收到了,但是有的应答丢失了,这种情况不会进行重发,因为发送端知道,如果是数据段丢失,接收端不会放过它的,会疯狂向它提醒…

3.拥塞控制

  • 如果把窗口定的很大,发送端连续发送大量的数据,可能会造成网络的拥堵(大家都在用网,你在这狂发,吞吐量就那么大,当然会堵),甚至造成网络的瘫痪。所以TCP在为了防止这种情况而进行了拥塞控制

4.慢启动:

  • 定义拥塞窗口,一开始将该窗口大小设为1,开始发送数据的时候以低速传输,只要能够得到对应报文的ACK,就以指数级的速度提高速率。当增长到一个阈值时,增长速度就变成线性增长,而不是指数级的。或者是丢包严重了,说明网络出现拥塞,要降低发送速率,进入拥塞避免阶段。

5.拥塞避免

  • 设置慢启动阈值,一般开始都设为65536。拥塞避免是指当拥塞窗口大小达到这个阈值,拥塞窗口的值不再指数上升,而是加法增加(每次确认应答/每个rtt,拥塞窗口大小+1),以此来避免拥塞。
  • 将报文段的超时重传看做拥塞,则一旦发生超时重传,我们需要先将阈值设为当前窗口大小的一半,并且将窗口大小设为初值1,然后重新进入慢启动过程。

7.快速重传

  • 在遇到3次重复确认应答(高速重发控制)时,代表收到了3个报文段,但是这之前的1个段丢失了,便对它进行立即重传。然后,先将阈值设为当前窗口大小的一半,然后将拥塞窗口大小设为慢启动阈值+3的大小。
  • 这样可以达到:在TCP通信时,网络吞吐量呈现逐渐的上升,并且随着拥堵来降低吞吐量,再进入慢慢上升的过程,网络不会轻易的发生瘫痪

8.链接机制

  • TCP建立连接和断开连接的过程:

在这里插入图片描述

  • 三次握手:

  • Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。

  • Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。

  • Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。

  • 四次挥手

  • 由于TCP连接时全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭。

  • 1.数据传输结束后,客户端的应用进程发出连接释放报文段FIN(FIN = 1),序列号为u(seq = u),并停止发送数据,客户端进入FIN_WAIT_1状态,此时客户端依然可以接收服务器发送来的数据。

  • 2.服务端发送ACK确认报文(ACK = 1),序列号为v(seq = v),确认报文u(ack = u + 1),进入CLOSE-WAIT状态,继续传送数据。,客户端收到上述报文进入FIN-WAIT2状态,继续接收服务端传输的数据

  • 3.当服务器没有数据要发送时,数据传输完毕后,发送FIN报文(FIN = 1,ACK = 1),序列号为w(seq = w),确认报文u(ack = u + 1),进入LAST-ACK状态,等待最后一个ACK。

  • 4.客户端发送ACK确认报文(ACK = 1),序列号为u+1(seq = u + 1),确认报文w(ack = w + 1),进入TIME-WAIT状态,等待2MSL(最长报文段寿命),客户端进入CLOSED状态,服务端收到后上述报文后进入CLOSED状态。

9.进行三次握手、四次挥手及timewait的原因

  • 1)三次握手原因:

  • 三次握手是为了防止,客户端的请求报文在网络滞留,客户端超时重传了请求报文,服务端建立连接,传输数据,释放连接之后,服务器又收到了客户端滞留的请求报文,建立连接一直等待客户端发送数据。

  • 服务器对客户端的请求进行回应(第二次握手)后,就会理所当然的认为连接已建立,而如果客户端并没有收到服务器的回应呢?此时,客户端仍认为连接未建立,服务器会对已建立的连接保存必要的资源,如果大量的这种情况,服务器会崩溃。

  • 因为TCP为保证可靠性,对传输对数据进行序列号,数据到达接收方,接收方需要发出一个确认应答,表示已经收到该数据段,并且确认序号会说明了它下一次需要接收的数据序列号,所以三次握手就是相互确认序号的,两次握手只能确认一方的序列号

  • 2)为什么TCP协议终止链接要四次?

  • 当客户端确认发送完数据且知道服务器已经接收完了,想要关闭发送数据口(当然确认信号还是可以发),就会发FIN给服务器。

  • 服务器收到客户端发送的FIN,表示收到了,就会发送ACK回复,这样关闭客户端到服务端的通信。

  • 但这时候服务器可能还在发送数据,没有想要关闭数据口的意思,所以服务器的FIN与ACK不是同时发送的,而是等到服务器数据发送完了,才会发送FIN给客户端。

  • 客户端收到服务器发来的FIN,知道服务器的数据也发送完了,回复ACK,客户端等待2MSL以后,没有收到服务器传来的任何消息,知道服务器已经收到自己的ACK了,客户端就关闭链接,服务器也关闭链接了

  • 3)2MSL意义:

  • 保证最后一次握手报文能到达服务端,可以进行超时重传

  • 2MSL后,双向连接产生的所有报文都会消失,不会影响下一次连接(msl是报文最大的生命周期)

  • 如果没有TIME_WAIT状态,主动请求关闭链接的一方就会直接进入关闭状态,如果因为网络原因最后一个ACK发生了丢包,服务端就会不断的请求FIN,等待最后一个ACK,链接并没有成功关闭,并且如果此时打开一个新的链接,那么服务器端就会把SYN请求当成ACK,因而发生请求码错误,服务端就会发送RET重置链接。而TIME_WAIT的作用就是让主动请求的一方进入TIME_WAIT状态后等待2MSL时间关闭链接,等待这段时间是为了让客户端收到服务器端的FIN后可以有充分的时间回复ACK,让网络中延迟的FIN/ACK失效

上述所有的TCP可靠机制只是针对端与端之间的传输

二、问题引入

  • 面试官经常会问的一个问题是,如果TCP服务器宕机了,会发生什么?换句话说,TCP真的可靠吗

  • 这个问题要从两个方面来回答:

  • 1.TCP是个可靠的协议,怎么保证它可靠的。

  • 2.TCP并不能保证它所发送数据的可靠传输。

三、TCP如何保证可靠性?

首先,我们看看数据报不可靠有哪些问题,以及TCP是怎么解决的?

  • 1.差错
  • TCP通过首部的校验和,可以校验首部和和数据。这是一种端到端的校验目的是检测数据在传输过程中的任何变化,如果收到对端的校验和有差错,TCP将这个包丢弃并且不确认。
  • 2.丢包
  • TCP发出一个数据包后,启动一个定时器,等待对端确认收到这个数据包,如果不能及时收到这个确认,将重发这个报文(超时重传机制)
  • 3.失序
  • TCP承载于IP数据包来传输,IP包的到达可能会失序,因此TCP数据包的到达也可能失序,TCP对收到的数据包按照首部的序列号进行重新排序
  • 4.重复
  • IP数据包会发生重复,TCP接收端根据TCP首部的序列号将重复的数据丢弃。
  • 此外,确认数据包,也不能是确认了一个数据包再发送下一个数据包,这不利于并行的批量发送,我们可以批量的发送,再批量的确认。
  • 这里有两个问题需要考虑:1.接收方的处理能力,2.网络的处理能力。
  • 1.首先来看看接收方的处理能力接收方的硬件能力不如发送方,或者是系统繁忙,那发送过去的报文只能丢弃。要限制发送方的发送速度,接收方就要告诉发送方它的处理能力,好让发送发方限制它的发送速度就可以了,这就是滑动窗口的由来。
  • 2.下面来看看网络处理能力如果发送TCP数据包的速度快于中间某个路由器的发速度,路由器就开始丢包。导致较高的丢包率,如果TCP继续保持这个速度送数据,那么网络的性能就会极大的降低。这就需要拥塞控制算法。它分为两分,一个是慢启动,一个是拥塞避免。
  • 慢启动指的就是TCP在一开始发送数据的时候以低速传输,只要能够得到对应报文的ACK,就以指数级的速度提高速率。当增长到一个阈值时,增长速度就变成线性增长,而不是指数级的。或者是丢包严重了,说明网络出现拥塞,要降低发送速率,进入拥塞避免阶段。

四、TCP并不能保证它所发送数据的可靠传输

可靠指的是什么,不可靠指的是什么?

  • 上面我们讨论了TCP通过很多机制保证可靠,这种可靠只是在端到端的通信上
  • 假设数据从A进程送到B进程,数据从A进程通过它所在主机TCP/IP协议栈向下传输,经过若干台路由器,通过进程B所在主机的TCP/IP协议栈向上传输,最后到达B进程。这些路由器没有TCP层,只是转发IP数据报,IP是个不可靠的协议
    在这里插入图片描述
  • TCP能够向进程B保证所有到达的数据是按序且未受损的。但有个问题, TCP已经ACK的数据包实际上不一定会抵达应用进程。比如,接收端TCP刚对数据进行ACK,但应用程序还没有读走,就崩溃了
  • 针对TCP的ACK的数据报不能抵达目的应用程序的解决方案
  • 我们的解决方案是应用层ACK。下面给一个简单的实现,我们采用停等的方式来实现回射客户服务器。
    在这里插入图片描述
  • 这里设计成客户端和服务器有两条通道,主要的原因是想让发送数据模块和接收网络数据模块都能够获取网络中对端的状态,而不是将状态混在一条通道上
  • 这里给大家实现向外发送数据模块。实现思路是:发送一条消息后,在定时器到之前必须接收对等实体发过来的应用层ACK,如果定时器时间到,我们就终止程序,当然,你可以实现的更复杂,比如重传。
#include <iostream>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>
#define APP_ACK 0x01

int main() 
{
    fd_set allset;
    fd_set readset;
    fd_set sockonly;

    struct timeval tv;
    struct timeval *tvp = NULL;
    const static struct timeval timeout = {2,0};

    char buf[1024] = NULL;
    int sfd = tcp_client(9999);

    FD_ZERO(&allset);
    FD_ZERO(&readset);
    FD_ZERO(&sockonly);
    FD_SET(sfd, &allset);
    sockonly = allset;
    FD_SET(fileno(stdin), &allset);
    readset = allset;

    for ( ; ; ) 
    {
        memset(buf, 0x00, sizeof(buf));
        /*
            select分两种情况:
                1.接收来自键盘和网络的事件。
                2.当向网络发送数据后,只能接收网络事件,不能接收键盘事件,这时想接收网络的对端应用ACK。
        */
        int ready = select(sfd + 1, &readset, NULL, NULL, tvp);
        if ( ready < 0 ) 
        	perror("select"),exit(1);

        // 超时,不能及时获取对端的应用ACK,打印超时并退出程序
        if ( ready == 0 ) 
        	printf("message time out\n"),exit(1);

        // 接收网络数据
        if ( FD_ISSET(sfd, &readset)) 
        {
            int r = read(sfd, buf, sizeof(buf));
            // 如果是错误或者EOF,终止程序
            if ( r == 0 ) 
            {
                printf("server close\n");
                break;
            } 
            else if ( r == -1 ) 
            {
                perror("read"),exit(1);
            } 
            else if ( r==1 && buf[0] != APP_ACK ) 
            {
                printf("没有收到对方的回应报文\n");
            }
            
            // 收到确认报文,关闭定时器
            tvp = NULL;
            // 允许接收来自网络和键盘事件
            readset = allset;
        }

        if ( FD_ISSET(fileno(stdin), &readset) ) 
        {
            // 获取键盘事件,发送网络,启动定时器,只接受网络事件
            int r = read(filene(stdin), buf, sizeof(buf));
            if ( r == -1 ) 
            	perror("read"),exit(1);
            
            write(sfd, buf, r);
            tv = timeout;
            tvp = &tv;
            readset = sockonly;
        }
    }
    return 0;
}

当然这里设计的是停等协议,如果需要做的更好,像TCP内核协议栈一样,可以考虑把应用程序做成事件驱动的,这是一个软件设计的问题

五、故障类型

  • 通过前面的讨论我们可以看到网络程序员不能认为TCP为我们做好了一切。 我们可以把故障分为两类:
  • 1.收不到FIN的故障,比如网络掉线,或者主机崩溃都是这种情况。
  • 2.能收到FIN的故障,比如对方应用程序崩溃。

1.收不到FIN的故障

  • 先来说说没有FIN的故障,分成四种情况:
  • 1.如果刚好阻塞在read函数上,这时没法恢复。可以通过设置读超时来解决
struct timeval tv;
tv.tv_sec = 2;
tv.tv_usec = 0;
setsockopt(cfd, SOL_SOCKET, SO_RCVTIMEO, (const char *) &tv, sizeof tv);
  • 2.如果是先write,再read,协议栈会持续重传。经过多次重传不成功,协议栈会标记连接异常,阻塞的read就会得到TIMEOUT错误。
  • 3.如果是阻塞在select或epoll上,建议做心跳包。下面是一个有心跳功能的回射客户服务器程序客户端程序。十秒中如果没有数据通信,就心跳,执行三次如果没有应答,就退出。
#include <iostream>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>
#define APP_ACK 0x01

int main() 
{
    int heartbeat = 0;
    fd_set allfd;
    fd_set readfd;
    struct timeval tv;
    msg_t msg;
    char buf[1024];
    int cfd = tcp_client();

    FD_ZERO(&allfd);
    FD_SET(cfd, &allfd);
    FD_SET(fileno(stdin), &allfd);
    tv.tv_sec  = 10;
    tv.tv_usec = 0;
    for ( ; ; ) 
    {
        readfd = allfd;
        memset(&msg, 0x00, sizeof(msg));
        int ready = select(cfd+1, &readfd, NULL, NULL, &tv);
        if ( ready == -1 ) 
        	perror("select"),exit(1);
        
        if ( ready == 0 ) 
        {
            printf("timeout %d\n", heartbeat);
            if ( ++heartbeat > 3 ) 
            {
                printf("connection dead\n");
                exit(0);
            }
            
            msg.type = htonl(MSG_HEARTBEAT);
            if ( write(cfd, (char*)&msg, sizeof(msg)) == -1 )
                perror("write"),exit(1);
            
            tv.tv_sec = 2;
            continue;
        }

        if ( FD_ISSET(cfd, &readfd) ) 
        {
            int ret = read(cfd, (char*)&msg, sizeof(msg));
            if ( ret == 0 ) 
            {
                printf("server close\n");
                break;
            }
            if ( ntohl(msg.type) == MSG_ECHO ) 
            {
                printf("=> %s\n", msg.data);
                heartbeat = 0;
                tv.tv_sec = 10;
                continue;
            }
        }

        if ( FD_ISSET(fileno(stdin), &readfd) ) 
        {
            msg.type = htonl(MSG_ECHO);
            if ( fgets(msg.data, 100, stdin) == NULL )
                break;
            if ( write(cfd, (char*)&msg, sizeof(msg)) == -1 )
            {
                perror("write");
                break;
            }
        }

    }
    close(cfd);
    return 0;
}
  • 4.还有一种特殊情况就是,如果是主机崩溃又重启了,这时对端主机得到RST错误

2.能收到FIN的故障

  • 再来看看能收到FIN的故障,这里要意识到,从一个用程序角度,对端进程崩溃还是调用了close以及exit是无法区分的,在这两种情况下TCP都会向我们发送一个FIN
  • 1.如果是read,直接得到FIN信息,返回0。
  • 2.如果是write,则第一次调用会得到RST。
  • 3.收到RST,再多次调用write就得到SIGPIPE信号。

3.总结

  • 1.TCP通过序号和超时重传保证了端到端的可靠。
  • 2.TCP并不能保证应用层的可靠。
  • 3.异常的情况分为,网络故障,主机崩溃和进程崩溃。网络故障和主机故障可以看作是一类故障,当然是指除了主机崩溃并在TCP放弃连接之前,就重启了的情况。

猜你喜欢

转载自blog.csdn.net/wolfGuiDao/article/details/107371664