面试八股之网络

面试八股之网络

1.基本概念

TCP连接中间会有什么操作

在TCP连接中,客户端和服务器之间会进行以下操作:

  • 握手阶段:客户端向服务器发送SYN包(同步包),请求建立连接。服务器收到SYN包后,向客户端发送SYN+ACK包(同步确认包),表示可以建立连接。客户端收到SYN+ACK包后,再向服务器发送ACK包(确认包),表示连接建立成功。

  • 数据传输阶段:连接建立成功后,客户端和服务器之间可以进行数据的传输。客户端向服务器发送数据包,服务器接收数据包并进行处理,然后向客户端发送响应包。客户端收到响应包后,可以再次向服务器发送数据包,以此类推。

  • 断开连接阶段:当客户端或服务器不再需要连接时,可以发送FIN包(结束包)来请求断开连接。对方收到FIN包后,也发送FIN包进行响应,表示同意断开连接。当两端都收到对方的FIN包后,连接才真正关闭。

需要注意的是,在TCP连接中可能会出现丢包、拥塞等情况,需要进行相应的处理,例如重传丢失的数据包、调整发送窗口大小等

TCP 三次握手

  • TCP 三次握手中,客户端收到的第二次握手中 ack 确认号不是自己期望的,会发生什么?是直接丢弃 or 回 RST 报文?
  • 什么情况下会收到不正确的 ack(第二次握手中的 ack) 呢?
    不卖关子,直接说这个问题,是回 RST 报文。过程如下图:

tcp handshak

三次握手避免历史连接

当客户端连续发送多次建立连接的 SYN 报文,然后在网络拥堵的情况,就会发生客户端收到不正确的 ack 的情况。具体过程如下:

  • 客户端先发送了 SYN(seq = 90) 报文,但是被网络阻塞了,服务端并没有收到,接着客户端又重新发送了 SYN(seq = 100) 报文,注意不是重传 SYN,重传的 SYN 的序列号是一样的。

  • 「旧 SYN 报文」比「最新的 SYN 」 报文早到达了服务端,那么此时服务端就会回一个 SYN + ACK 报文给客户端,此报文的确认号是 91(90+1)。

  • 客户端收到后,发行自己期望收到的确认号应该是 100+1,而不是 90 + 1,于是就会回 RST 报文。

  • 服务端收到 RST 报文后,就会中止连接。

  • 后续最新的 SYN 抵达了服务端后,客户端与服务端就可以正常的完成三次握手了。

上述中的「旧 SYN 报文」称为历史连接,TCP 使用三次握手建立连接的最主要原因就是防止「历史连接」初始化了连接

我们也可以从 RFC 793 知道 TCP 连接使用三次握手的首要原因:

The principle reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion.

简单来说,三次握手的首要原因是为了防止旧的重复连接初始化造成混乱。RFC 给出的三次握手防止历史连接的案例图如下:

tcp_handshake_reason

RFC 793

如果是两次握手连接,就无法阻止历史连接,那为什么 TCP 两次握手为什么无法阻止历史连接呢?

我先直接说结论,主要是因为在两次握手的情况下,「被动发起方」没有中间状态给「主动发起方」来阻止历史连接,导致「被动发起方」可能建立一个历史连接,造成资源浪费

你想想,两次握手的情况下,「被动发起方」在收到 SYN 报文后,就进入 ESTABLISHED 状态,意味着这时可以给对方发送数据给,但是「主动发」起方此时还没有进入 ESTABLISHED 状态,假设这次是历史连接,主动发起方判断到此次连接为历史连接,那么就会回 RST 报文来断开连接,而「被动发起方」在第一次握手的时候就进入 ESTABLISHED 状态,所以它可以发送数据的,但是它并不知道这个是历史连接,它只有在收到 RST 报文后,才会断开连接。

liangci_handshake

两次握手无法阻止历史连接

可以看到,上面这种场景下,「被动发起方」在向「主动发起方」发送数据前,并没有阻止掉历史连接,导致「被动发起方」建立了一个历史连接,又白白发送了数据,妥妥地浪费了「被动发起方」的资源。

因此,要解决这种现象,最好就是在「被动发起方」发送数据前,也就是建立连接之前,要阻止掉历史连接,这样就不会造成资源浪费,而要实现这个功能,就需要三次握手

我说回 RST 就回 RST 吗?当然不是了,肯定得用源码证明我说的这个结论。

听到要源码分析,可能有的同学就怂了。

其实要分析我们今天这个问题,只要懂 if else 就行了,我也会用中文来表述代码的逻辑,所以单纯看我的文字也是可以的。

这次我们重点分析的是,在 SYN_SENT 状态下,收到不正确的确认号的 syn+ack 报文是如何处理的。

处于 SYN_SENT 状态下的客户端,在收到服务端的  syn+ack 报文后,最终会调用  tcp_rcv_state_process,在这里会根据 TCP 状态做对应的处理,这里我们只关注 SYN_SENT 状态。

// net/ipv4/tcp_ipv4.c int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb) {  ...     int queued = 0;      ...     switch (sk->sk_state) {  case TCP_CLOSE:   ...  case TCP_LISTEN:   ...  case TCP_SYN_SENT:     ....   queued = tcp_rcv_synsent_state_process(sk, skb, th);   if (queued >= 0)    return queued;     ...  }

可以看到,接下来,会继续调用 tcp_rcv_synsent_state_process 函数。

static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,       const struct tcphdr *th) {  ....  if (th->ack) {   /* rfc793:    * "If the state is SYN-SENT then    *    first check the ACK bit    *      If the ACK bit is set    *   If SEG.ACK =< ISS, or SEG.ACK > SND.NXT, send    *        a reset (unless the RST bit is set, if so drop    *        the segment and return)"    */     // ack 的确认号不是预期的   if (!after(TCP_SKB_CB(skb)->ack_seq, tp->snd_una) ||       after(TCP_SKB_CB(skb)->ack_seq, tp->snd_nxt))       //回 RST 报文    goto reset_and_undo;   ... }

从上面的函数,就可以得知了,客户端在 SYN_SENT 状态下,收到不正确的确认号的 syn+ack 报文会回 RST 报文。

小结

TCP 三次握手中,客户端收到的第二次握手中 ack 确认号不是自己期望的,会发生什么?是直接丢弃 or 回 RST 报文?

回 RST 报文。

什么情况下会收到不正确的 ack(第二次握手中的 ack) 呢?

当客户端发起多次 SYN 报文,然后网络拥堵的情况下,「旧的 SYN 报文」比「新的 SYN 报文」早抵达服务端,此时服务端就会按照收到的「旧的 SYN 报文」回复 syn+ack 报文,而此报文的确认号并不是客户端期望收到的,于是客户端就会回 RST 报文。

四次挥手

虽然我们在学习 TCP 挥手时,学到的是需要四次来完成 TCP 挥手,但是在一些情况下, TCP 四次挥手是可以变成 TCP 三次挥手的

tcp_goodbye

而且在用 wireshark 工具抓包的时候,我们也会常看到 TCP 挥手过程是三次,而不是四次,如下图:

tcp_goodbye_3

先来回答为什么 RFC 文档里定义 TCP 挥手过程是要四次?

再来回答什么情况下,什么情况会出现三次挥手?

为什么 TCP 挥手需要四次?

TCP 四次挥手的过程如下:
tcp_goodbye_4

具体过程:

  • 客户端主动调用关闭连接的函数,于是就会发送 FIN 报文,这个  FIN 报文代表客户端不会再发送数据了,进入 FIN_WAIT_1 状态;

  • 服务端收到了 FIN 报文,然后马上回复一个 ACK 确认报文,此时服务端进入 CLOSE_WAIT 状态。在收到 FIN 报文的时候,TCP 协议栈会为 FIN 包插入一个文件结束符 EOF 到接收缓冲区中,服务端应用程序可以通过 read 调用来感知这个 FIN 包,这个 EOF 会被放在已排队等候的其他已接收的数据之后,所以必须要得继续 read 接收缓冲区已接收的数据;

  • 接着,当服务端在 read 数据的时候,最后自然就会读到 EOF,接着 read() 就会返回 0,这时服务端应用程序如果有数据要发送的话,就发完数据后才调用关闭连接的函数,如果服务端应用程序没有数据要发送的话,可以直接调用关闭连接的函数,这时服务端就会发一个 FIN 包,这个  FIN 报文代表服务端不会再发送数据了,之后处于 LAST_ACK 状态;

  • 客户端接收到服务端的 FIN 包,并发送 ACK 确认包给服务端,此时客户端将进入 TIME_WAIT 状态;

  • 服务端收到 ACK 确认包后,就进入了最后的 CLOSE 状态;

  • 客户端经过 2MSL 时间之后,也进入 CLOSE 状态;

你可以看到,每个方向都需要一个 FIN 和一个 ACK,因此通常被称为四次挥手

为什么 TCP 挥手需要四次呢?

服务器收到客户端的 FIN 报文时,内核会马上回一个 ACK 应答报文,但是服务端应用程序可能还有数据要发送,所以并不能马上发送 FIN 报文,而是将发送 FIN 报文的控制权交给服务端应用程序

  • 如果服务端应用程序有数据要发送的话,就发完数据后,才调用关闭连接的函数;

  • 如果服务端应用程序没有数据要发送的话,可以直接调用关闭连接的函数,

从上面过程可知,是否要发送第三次挥手的控制权不在内核,而是在被动关闭方(上图的服务端)的应用程序,因为应用程序可能还有数据要发送,由应用程序决定什么时候调用关闭连接的函数,当调用了关闭连接的函数,内核就会发送 FIN 报文了,所以服务端的 ACK 和 FIN 一般都会分开发送。

FIN 报文一定得调用关闭连接的函数,才会发送吗?

不一定。

如果进程退出了,不管是不是正常退出,还是异常退出(如进程崩溃),内核都会发送 FIN 报文,与对方完成四次挥手。

粗暴关闭 vs 优雅关闭

前面介绍 TCP 四次挥手的时候,并没有详细介绍关闭连接的函数,其实关闭的连接的函数有两种函数:

  • close 函数,同时 socket 关闭发送方向和读取方向,也就是 socket 不再有发送和接收数据的能力。如果有多进程/多线程共享同一个 socket,如果有一个进程调用了 close 关闭只是让 socket 引用计数 -1,并不会导致 socket 不可用,同时也不会发出 FIN 报文,其他进程还是可以正常读写该 socket,直到引用计数变为 0,才会发出 FIN 报文。

  • shutdown 函数,可以指定 socket 只关闭发送方向而不关闭读取方向,也就是 socket 不再有发送数据的能力,但是还是具有接收数据的能力。如果有多进程/多线程共享同一个 socket,shutdown 则不管引用计数,直接使得该 socket 不可用,然后发出 FIN 报文,如果有别的进程企图使用该 socket,将会受到影响。

如果客户端是用 close 函数来关闭连接,那么在 TCP 四次挥手过程中,如果收到了服务端发送的数据,由于客户端已经不再具有发送和接收数据的能力,所以客户端的内核会回 RST 报文给服务端,然后内核会释放连接,这时就不会经历完成的 TCP 四次挥手,所以我们常说,调用 close 是粗暴的关闭。

tcp_close

当服务端收到 RST 后,内核就会释放连接,当服务端应用程序再次发起读操作或者写操作时,就能感知到连接已经被释放了:

  • 如果是读操作,则会返回 RST 的报错,也就是我们常见的Connection reset by peer。

  • 如果是写操作,那么程序会产生 SIGPIPE 信号,应用层代码可以捕获并处理信号,如果不处理,则默认情况下进程会终止,异常退出。

相对的,shutdown 函数因为可以指定只关闭发送方向而不关闭读取方向,所以即使在 TCP 四次挥手过程中,如果收到了服务端发送的数据,客户端也是可以正常读取到该数据的,然后就会经历完整的 TCP 四次挥手,所以我们常说,调用 shutdown 是优雅的关闭。

tcp_goodbye_shutdown

但是注意,shutdown 函数也可以指定「只关闭读取方向,而不关闭发送方向」,但是这时候内核是不会发送 FIN 报文的,因为发送 FIN 报文是意味着我方将不再发送任何数据,而 shutdown 如果指定「不关闭发送方向」,就意味着 socket 还有发送数据的能力,所以内核就不会发送 FIN

什么情况会出现三次挥手?

当被动关闭方(上图的服务端)在 TCP 挥手过程中,「没有数据要发送」并且「开启了 TCP 延迟确认机制」,那么第二和第三次挥手就会合并传输,这样就出现了三次挥手。

tcp_goodbye_33

然后因为 TCP 延迟确认机制是默认开启的,所以导致我们抓包时,看见三次挥手的次数比四次挥手还多。

什么是  TCP 延迟确认机制?

当发送没有携带数据的 ACK,它的网络效率也是很低的,因为它也有 40 个字节的 IP 头 和 TCP 头,但却没有携带数据报文。

为了解决 ACK 传输效率低问题,所以就衍生出了 TCP 延迟确认

TCP 延迟确认的策略:

  • 当有响应数据要发送时,ACK 会随着响应数据一起立刻发送给对方

  • 当没有响应数据要发送时,ACK 将会延迟一段时间,以等待是否有响应数据可以一起发送

  • 如果在延迟等待发送 ACK 期间,对方的第二个数据报文又到达了,这时就会立刻发送 ACK

tcp_delay_rsp

延迟等待的时间是在 Linux 内核中定义的,如下图:

tcp_delack_time

关键就需要 HZ 这个数值大小,HZ 是跟系统的时钟频率有关,每个操作系统都不一样,在我的 Linux 系统中 HZ 大小是 1000,如下图:

tcp_delack_hz

知道了 HZ 的大小,那么就可以算出:

  • 最大延迟确认时间是 200 ms (1000/5)

  • 最短延迟确认时间是 40 ms (1000/25)

怎么关闭 TCP 延迟确认机制?

如果要关闭 TCP 延迟确认机制,可以在 Socket 设置里启用 TCP_QUICKACK,启用 TCP_QUICKACK,就相当于关闭 TCP 延迟确认机制。

// 1 表示开启 TCP_QUICKACK,即关闭 TCP 延迟确认机制
int value = 1;
setsockopt(socketfd, IPPROTO_TCP, TCP_QUICKACK, (char*)& value, sizeof(int));

实验验证

实验一

接下来,来给大家做个实验,验证这个结论:

当被动关闭方(上图的服务端)在 TCP 挥手过程中,「没有数据要发送」并且「开启了 TCP 延迟确认机制」,那么第二和第三次挥手就会合并传输,这样就出现了三次挥手。

服务端的代码如下,做的事情很简单,就读取数据,然后当 read 返回 0 的时候,就马上调用 close 关闭连接。因为 TCP 延迟确认机制是默认开启的,所以不需要特殊设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#include <stdlib.h>  
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <netinet/tcp.h>

#define MAXLINE 1024

int main(int argc, char *argv[])
{

    // 1. 创建一个监听 socket
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if(listenfd < 0)
    {
        fprintf(stderr, "socket error : %s\n", strerror(errno));
        return -1;
    }

    // 2. 初始化服务器地址和端口
    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(struct sockaddr_in));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(8888);

    // 3. 绑定地址+端口
    if(bind(listenfd, (struct sockaddr *)(&server_addr), sizeof(struct sockaddr)) < 0)
    {
        fprintf(stderr,"bind error:%s\n", strerror(errno));
        return -1;
    }

    printf("begin listen....\n");

    // 4. 开始监听
    if(listen(listenfd, 128))
    {
        fprintf(stderr, "listen error:%s\n\a", strerror(errno));
        exit(1);
    }


    // 5. 获取已连接的socket
    struct sockaddr_in client_addr;
    socklen_t client_addrlen = sizeof(client_addr);
    int clientfd = accept(listenfd, (struct sockaddr *)&client_addr, &client_addrlen);
    if(clientfd < 0) {
        fprintf(stderr, "accept error:%s\n\a", strerror(errno));
        exit(1);
    }

    printf("accept success\n");

    char message[MAXLINE] = {0};
    
    while(1) {
        //6. 读取客户端发送的数据
        int n = read(clientfd, message, MAXLINE);
        if(n < 0) { // 读取错误
            fprintf(stderr, "read error:%s\n\a", strerror(errno));
            break;
        } else if(n == 0) {  // 返回 0 ,代表读到 FIN 报文
            fprintf(stderr, "client closed \n");
            close(clientfd); // 没有数据要发送,立马关闭连接
            break;
        }

        message[n] = 0; 
        printf("received %d bytes: %s\n", n, message);
    }
 
    close(listenfd);
    return 0;
}

客户端代码如下,做的事情也很简单,与服务端连接成功后,就发送数据给服务端,然后睡眠一秒后,就调用 close 关闭连接,所以客户端是主动关闭方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <stdlib.h>  
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>

int main(int argc, char *argv[])
{

    // 1. 创建一个监听 socket
    int connectfd = socket(AF_INET, SOCK_STREAM, 0);
    if(connectfd < 0)
    {
        fprintf(stderr, "socket error : %s\n", strerror(errno));
        return -1;
    }

    // 2. 初始化服务器地址和端口
    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(struct sockaddr_in));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    server_addr.sin_port = htons(8888);
    
    // 3. 连接服务器
    if(connect(connectfd, (struct sockaddr *)(&server_addr), sizeof(server_addr)) < 0)
    {
        fprintf(stderr,"connect error:%s\n", strerror(errno));
        return -1;
    }

    printf("connect success\n");


    char sendline[64] = "hello, i am xiaolin";

    //4. 发送数据
    int ret = send(connectfd, sendline, strlen(sendline), 0);
    if(ret != strlen(sendline)) {
        fprintf(stderr,"send data error:%s\n", strerror(errno));
        return -1;
    }

    printf("already send %d bytes\n", ret);

    sleep(1);

    //5. 关闭连接
    close(connectfd);
    return 0;
}

编译服务端和客户端的代码:

tcp_compile

先启用服务端:

tcp_server_start

然后用 tcpdump 工具开始抓包,命令如下:

tcpdump -i lo tcp and port 8888 -s0 -w /home/tcp_close.pcap

然后启用客户端,可以看到,与服务端连接成功后,发完数据就退出了。

tcp_client_start

此时,服务端的输出:

tcp_server_output

接下来,我们来看看抓包的结果。

tcp_pack

可以看到,TCP 挥手次数是 3 次。

所以,下面这个结论是没问题的。

结论:当被动关闭方(上图的服务端)在 TCP 挥手过程中,「没有数据要发送」并且「开启了 TCP 延迟确认机制(默认会开启)」,那么第二和第三次挥手就会合并传输,这样就出现了三次挥手。

实验二

我们再做一次实验,来看看关闭 TCP 延迟确认机制,会出现四次挥手吗?

客户端代码保持不变,服务端代码需要增加一点东西。

在上面服务端代码中,增加了打开了 TCP_QUICKACK (快速应答)机制的代码,如下:

tcp_quick_ack

编译好服务端代码后,就开始运行服务端和客户端的代码,同时用 tcpdump 进行抓包。

抓包的结果如下,可以看到是四次挥手。

tcp_goodbye_pack

所以,当被动关闭方(上图的服务端)在 TCP 挥手过程中,「没有数据要发送」,同时「关闭了 TCP 延迟确认机制」,那么就会是四次挥手。

设置 TCP_QUICKACK 的代码,为什么要放在 read 返回 0 之后?

我也是多次实验才发现,在 bind 之前设置 TCP_QUICKACK 是不生效的,只有在 read 返回 0 的时候,设置 TCP_QUICKACK 才会出现四次挥手。

网上查了下资料说,设置 TCP_QUICKACK 并不是永久的,所以每次读取数据的时候,如果想要立刻回 ACK,那就得在每次读取数据之后,重新设置 TCP_QUICKACK。

而我这里的实验,目的是为了当收到客户端的 FIN 报文(第一次挥手)后,立马回 ACK 报文,所以就在 read 返回 0 的时候,设置 TCP_QUICKACK。

当然,实际应用中,没人会在我这个位置设置 TCP_QUICKACK,因为操作系统都通过 TCP 延迟确认机制帮我们把四次挥手优化成了三次挥手了,这本来就是一件好事呀。

总结

当被动关闭方在 TCP 挥手过程中,如果「没有数据要发送」,同时「没有开启 TCP_QUICKACK(默认情况就是没有开启,没有开启 TCP_QUICKACK,等于就是在使用 TCP 延迟确认机制)」,那么第二和第三次挥手就会合并传输,这样就出现了三次挥手。

所以,出现三次挥手现象,是因为 TCP 延迟确认机制导致的。

TCP 拥塞控制

TCP如何保证可靠性

TCP 拆包沾包原因

TCP 拆包和沾包现象是由于 TCP 协议的特性以及网络传输过程中的各种因素所导致的。
TCP 协议是基于字节流的传输层协议,没有固定的分包边界。发送方将数据分成多个小的数据包进行传输,接收方再将这些数据包组合成完整的数据。在这个过程中,可能会出现拆包和沾包现象。
网络传输中的延迟和拥塞会影响数据包发送的速度和到达接收方的顺序。这可能导致数据包的拆分和组合不规律,从而出现拆包和沾包现象。
接收方的缓冲区大小限制。当接收方的缓冲区不足以容纳一个完整的数据包时,可能会将数据包拆分成多个部分,导致拆包现象。
为了解决 TCP 拆包和沾包的问题,可以采用以下方法:
在应用层实现数据包的边界识别,例如通过添加包头,包头中包含数据包长度等信息,使得接收方能够准确地将数据包进行拼接。
使用固定长度的数据包或者特殊的分隔符,以便于接收方识别数据包的边界。
使用更高级的传输层协议,如 WebSocket,它在 TCP 基础上增加了数据帧的概念,可以更好地解决拆包和沾包问题。

TCP粘包问题怎么解决

  • 答:特殊标记

  • 追问:打断,如果使用特殊标记解决会遇到什么问题

  • 答:正文转义字符

补充:

1、固定长度的消息

这种是最简单方法,即每个用户消息都是固定长度的,比如规定一个消息的长度是 64 个字节,当接收方接满 64 个字节,就认为这个内容是一个完整且有效的消息。

但是这种方式灵活性不高,实际中很少用。

2、特殊字符作为边界

我们可以在两个用户消息之间插入一个特殊的字符串,这样接收方在接收数据时,读到了这个特殊字符,就把认为已经读完一个完整的消息。

HTTP 是一个非常好的例子。

http_header

HTTP 通过设置回车符、换行符作为 HTTP 报文协议的边界。

有一点要注意,这个作为边界点的特殊字符,如果刚好消息内容里有这个特殊字符,我们要对这个字符转义,避免被接收方当作消息的边界点而解析到无效的数据。

3、自定义消息结构

我们可以自定义一个消息结构,由包头和数据组成,其中包头包是固定大小的,而且包头里有一个字段来说明紧随其后的数据有多大。

比如这个消息结构体,首先 4 个字节大小的变量来表示数据长度,真正的数据则在后面。

struct {      u_int32_t message_length;      char message_data[];  } message;

当接收方接收到包头的大小(比如 4 个字节)后,就解析包头的内容,于是就可以知道数据的长度,然后接下来就继续读取数据,直到读满数据的长度,就可以组装成一个完整到用户消息来处理了。

输入网址后发生了什么?

应用层 DNS 解析,传输层 TCP 连接,网络层 IP,数据链路 MAC,真实物理层,接收到之后再一层层扒皮。

http报文长度判断

udp为啥不可靠性。

服务端挂了,客户端的 TCP 连接会发生什么?

如果「服务端挂掉」指的是「服务端进程崩溃」,那么这个读者猜的想法是对的,服务端的进程在发生崩溃的时候,内核会发送 FIN 报文,与客户端进行四次挥手。

但是,如果「服务端挂掉」指的是「服务端主机宕机」,那么是不会发生四次挥手的,具体后续会发生什么?还要看客户端会不会发送数据?

  • 如果客户端会发送数据,由于服务端已经不存在,客户端的数据报文会超时重传,当重传次数达到一定阈值后,会断开 TCP 连接;

  • 如果客户端一直不会发送数据,再看客户端有没有开启 TCP keepalive 机制?

  • 如果有开启,客户端在一段时间后,检测到服务端的 TCP 连接已经不存在,则会断开自身的 TCP 连接;

  • 如果没有开启,客户端的 TCP 连接会一直存在,并不会断开。

详细介绍一下TCP的四次挥手机制,为什么要有TIME_WAIT状态,为什么需要四次握手?服务器出现了大量CLOSE_WAIT状态如何解决

当客户端要服务器断开连接时,客户端 TCP 会向服务器发送一个特殊的报文段,该报文段的 FIN 标志位会被置1,接着服务器会向客户端发送一个确认报文段。然后服务器也会客户端发送一个 FIN 标志位为1的终止报文段,随后客户端回送一个确认报文段,服务器立即断开连接。客户端等待一段时间后也断开连接。

其实四次挥手的过程是很容易理解的,由于 TCP 协议是全双工的,也就是说客户端和服务端都可以发起断开连接。两边各发起一次断开连接的申请,加上各自的两次确认,看起来就像执行了四次挥手。

为什么要有 TIME_WAIT 状态?因为客户端最后向服务器发送的确认 ACK 是有可能丢失的,当出现超时,服务端会再次发送 FIN 报文段,如果客户端已经关闭了就收不到了。还有一点是避免新旧连接混杂。

大量 CLOSE_WAIT 表示程序出现了问题,对方的 socket 已经关闭连接,而我方忙于读或写没有及时关闭连接,需要检查代码,特别是释放资源的代码,或者是处理请求的线程配置

\3. TIME_WAIT状态
经过前面的铺垫,终于要讲到与本文主题相关的内容了。 ^_^
从TCP状态迁移图可知,只有首先调用close()发起主动关闭的一方才会进入TIME_WAIT状态,而且是必须进入(图中左下角所示的3条状态迁移线最终均要进入该状态才能回到初始的CLOSED状态)。
从图中还可看到,进入TIME_WAIT状态的TCP连接需要经过2MSL才能回到初始状态,其中,MSL是指Max
Segment Lifetime,即数据包在网络中的最大生存时间。每种TCP协议的实现方法均要指定一个合适的MSL值,如RFC1122给出的建议值为2分钟,又如Berkeley体系的TCP实现通常选择30秒作为MSL值。这意味着TIME_WAIT的典型持续时间为1-4分钟。
TIME_WAIT状态存在的原因主要有两点:
1)为实现TCP这种全双工(full-duplex)连接的可靠释放
参考本文前面给出的TCP释放连接4次挥手示意图,假设发起active close的一方(图中为client)发送的ACK(4次交互的最后一个包)在网络中丢失,那么由于TCP的重传机制,执行passiveclose的一方(图中为server)需要重发其FIN,在该FIN到达client(client是active close发起方)之前,client必须维护这条连接的状态(尽管它已调用过close),具体而言,就是这条TCP连接对应的(local_ip, local_port)资源不能被立即释放或重新分配。直到romete peer重发的FIN达到,client也重发ACK后,该TCP连接才能恢复初始的CLOSED状态。如果activeclose方不进入TIME_WAIT以维护其连接状态,则当passive close方重发的FIN达到时,active close方的TCP传输层会以RST包响应对方,这会被对方认为有错误发生(而事实上,这是正常的关闭连接过程,并非异常)。
2)为使旧的数据包在网络因过期而消失
为说明这个问题,我们先假设TCP协议中不存在TIME_WAIT状态的限制,再假设当前有一条TCP连接:(local_ip, local_port, remote_ip,remote_port),因某些原因,我们先关闭,接着很快以相同的四元组建立一条新连接。本文前面介绍过,TCP连接由四元组唯一标识,因此,在我们假设的情况中,TCP协议栈是无法区分前后两条TCP连接的不同的,在它看来,这根本就是同一条连接,中间先释放再建立的过程对其来说是“感知”不到的。这样就可能发生这样的情况:前一条TCP连接由local peer发送的数据到达remote peer后,会被该remot peer的TCP传输层当做当前TCP连接的正常数据接收并向上传递至应用层(而事实上,在我们假设的场景下,这些旧数据到达remote peer前,旧连接已断开且一条由相同四元组构成的新TCP连接已建立,因此,这些旧数据是不应该被向上传递至应用层的),从而引起数据错乱进而导致各种无法预知的诡异现象。作为一种可靠的传输协议,TCP必须在协议层面考虑并避免这种情况的发生,这正是TIME_WAIT状态存在的第2个原因。
具体而言,local peer主动调用close后,此时的TCP连接进入TIME_WAIT状态,处于该状态下的TCP连接不能立即以同样的四元组建立新连接,即发起active close的那方占用的local port在TIME_WAIT期间不能再被重新分配。由于TIME_WAIT状态持续时间为2MSL,这样保证了旧TCP连接双工链路中的旧数据包均因过期(超过MSL)而消失,此后,就可以用相同的四元组建立一条新连接而不会发生前后两次连接数据错乱的情况。

另一比较深入的说法

TIME_WAIT状态的存在有两个理由:(1)让4次握手关闭流程更加可靠;4次握手的最后一个ACK是是由主动关闭方发送出去的,若这个ACK丢失,被动关闭方会再次发一个FIN过来。若主动关闭方能够保持一个2MSL的TIME_WAIT状态,则有更大的机会让丢失的ACK被再次发送出去。(2)防止lost duplicate对后续新建正常链接的传输造成破坏。lost duplicate在实际的网络中非常常见,经常是由于路由器产生故障,路径无法收敛,导致一个packet在路由器A,B,C之间做类似死循环的跳转。IP头部有个TTL,限制了一个包在网络中的最大跳数,因此这个包有两种命运,要么最后TTL变为0,在网络中消失;要么TTL在变为0之前路由器路径收敛,它凭借剩余的TTL跳数终于到达目的地。但非常可惜的是TCP通过超时重传机制在早些时候发送了一个跟它一模一样的包,并先于它达到了目的地,因此它的命运也就注定被TCP协议栈抛弃。另外一个概念叫做incarnation connection,指跟上次的socket pair一摸一样的新连接,叫做incarnation of previous connection。lost duplicate加上incarnation connection,则会对我们的传输造成致命的错误。大家都知道TCP是流式的,所有包到达的顺序是不一致的,依靠序列号由TCP协议栈做顺序的拼接;假设一个incarnation connection这时收到的seq=1000, 来了一个lost duplicate为seq=1000, len=1000, 则tcp认为这个lost duplicate合法,并存放入了receive buffer,导致传输出现错误。通过一个2MSL TIME_WAIT状态,确保所有的lost duplicate都会消失掉,避免对新连接造成错误。

滑动窗口

从上面的图可以看到滑动窗口左边的是已发送并且被确认的分组,滑动窗口右边是还没有轮到的分组。滑动窗口里面也分为两块,一块是已经发送但是未被确认的分组,另一块是窗口内等待发送的分组。随着已发送的分组不断被确认,窗口内等待发送的分组也会不断被发送。整个窗口就会往右移动,让还没轮到的分组进入窗口内。

可以看到滑动窗口起到了一个限流的作用,也就是说当前滑动窗口的大小决定了当前 TCP 发送包的速率,而滑动窗口的大小取决于拥塞控制窗口和流量控制窗口的两者间的最小值。

接着就讲讲什么是流量控制窗口,什么是拥塞控制窗口。

先讲流量控制:

TCP 是全双工的,客户端和服务器均可作为发送方或接收方,我们现在假设一个发送方向接收方发送数据的场景来讲解流量控制。首先我们的接收方有一块接收缓存,当数据来到时会先把数据放到缓存中,上层应用等缓存中有数据时就会到缓存中取数据。假如发送方没有限制地不断地向接收方发送数据,接收方的应用程序又没有及时把接收缓存中的数据读走,就会出现缓存溢出,数据丢失的现象,为了解决这个问题,我们引入流量控制窗口。

假设应用程序最后读走的数据序号是 lastByteRead,接收缓存中接收到的最后一个数据序号是 lastByteRcv,接收缓存的大小为 RcvSize,那么必须要满足 lastByteRcv - lastByteRead <= RcvSize 才能保证接收缓存不会溢出,所以我们定义流量窗口为接收缓存剩余的空间,也就是Rcv = RcvSize - (lastByteRcv - lastByteRead)。只要接收方在响应 ACK 的时候把这个窗口的值带给发送方,发送方就能知道接收方的接收缓存还有多大的空间,进而设置滑动窗口的大小。

接着讲解拥塞控制:

拥塞控制是指发送方先设置一个小的窗口值作为发送速率,当成功发包并接收到ACK时,便以指数速率增大发送窗口的大小,直到遇到丢包(超时/三个冗余ACK),才停止并调整窗口的大小。这么做能最大限度地利用带宽,又不至于让网络环境变得太过拥挤。

最终滑动窗口的值将设置为流量控制窗口和拥塞控制窗口中的较小值。

Q: 编写 TCP/SOCK_STREAM 服务程序时,SO_REUSEADDR到底什么意思?

A: 这个套接字选项通知内核,如果端口忙,但TCP状态位于 TIME_WAIT ,可以重用端口。如果端口忙,而TCP状态位于其他状态,重用端口时依旧得到一个错误信息, 指明”地址已经使用中”。如果你的服务程序停止后想立即重启,而新套接字依旧 使用同一端口,此时 SO_REUSEADDR 选项非常有用。必须意识到,此时任何非期 望数据到达,都可能导致服务程序反应混乱,不过这只是一种可能,事实上很不可能。

 CLOSE_WAIT: 这种状态的含义其实是表示在等待关闭。怎么理解呢?当对方close一个SOCKET后发送FIN报文给自己,你系统毫无疑问地会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,实际上你真正需要考虑的事情是察看你是否还有数据发送给对方,如果没有的话,那么你也就可以close这个SOCKET,发送FIN报文给对方,也即关闭连接。所以你在CLOSE_WAIT状态下,需要完成的事情是等待你去关闭连接。

UDP客户端可以CONNECT不,那CONNECT和不CONNECT有啥区别

在UDP协议中,没有真正的连接(connection)的概念,因此UDP客户端不能像TCP客户端那样通过connect()函数来建立连接。

使用connect()函数的主要目的是为了简化代码和提高安全性。当TCP客户端调用connect()函数时,操作系统会自动为其分配一个socket文件描述符,并将该socket与远程服务器的IP地址和端口号绑定起来。这样,在之后的数据通信中,客户端只需要向该socket发送数据即可,而不必每次都指定远程服务器的IP地址和端口号。另外,connect()函数还可以对发送的数据进行一些验证和过滤,以提高数据传输的安全性。

在UDP协议中,由于不存在真正的连接,因此也就不需要使用connect()函数来建立连接。UDP客户端每次发送数据时,都需要指定远程服务器的IP地址和端口号,这样才能确保数据能够正确地被发送到目标地址。因此,在UDP协议中,没有connect()函数的概念。

总之,UDP客户端没有连接(connection)的概念,因此不能像TCP客户端那样使用connect()函数来建立连接。每次发送数据时,UDP客户端需要显式地指定远程服务器的IP地址和端口号。如果需要简化代码和提高安全性,可以使用其他技术手段,例如使用加密算法和数字证书来保证数据传输的安全性。

5. 讲一下HTTP与HTTPS的区别

HTTP和HTTPS的主要区别在于HTTP协议传递的是明文数据,而HTTPS传递的是加密过的数据,也就是说HTTPS更具有安全性。也正由HTTPS需要保证安全性,所以它的性能要比HTTP差一点。

单说安全性肯定是不够的,我打算扩展讲一下HTTPS是怎么解决安全性问题的,通过这些HTTP没有机制,反映出HTTPS与HTTP的区别。下面尝试把HTTPS加密的过程推导出来。推导过程不涉及复杂的实现细节:

如何安全地进行数据传输?

假设现在A和B要进行安全的通信,那么究竟怎样才算是安全的通信?很自然地会想到:A和B之间传递数据,这些数据只有A和B才看得懂,中间人就算截取了信息但也看不懂,这才算得上安全。

安全通信的处理手段:

为了能让A和B才能看懂,就必须要对数据进行加密,而且首先想到的就是对称加密。对称加密的意思是A和B各持有一个相同的密钥,它们传递信息时会用密钥给信息加密,在消息到达端给消息解密,完成安全通信。

在对称加密中又会涉及到加密算法的选择问题。现实世界中,通常是多个客户端面向一个服务器的情况,不可能让每个客户端和服务器之间都采用相同的加密算法,如果是这样那和没加密差不多。所以注定每个客户端和服务器之间都会采用不同的加密方式。

如何让每个客户端与服务器之间都采用不同的加密方式?

要想对不同的机器使用不同的加密方式,最直接想到的就是使用随机数。也就说客户端和服务器之间每次都基于一个随机数产生加密算法。(具体实现时为了保证随机,用到还不止一个随机数)

这个产生加密算法的过程称之为协商,现在问题是协商的过程是透明的,也就是说中间人可以截获协商的过程,从而知道我们的加密方式。为了解决这个问题,我们需要对协商的过程进行加密。

如何对协商的过程进行加密?

之所以能来到这一步,是因为我们一开始就选择使用了对称加密,也就说一开始的对称加密导致了现在的问题,所以这时我们不能再使用对称加密了,否则会陷入死循环。

在密码学领域,还有一种加密过程叫非对称加密,它的逻辑是这样的:通信双方一方持有私钥,一方持有公钥,经过私钥加密的信息,都能通过公钥进行解密。但是经过公钥加密的数据,只有私钥可以解密。

按照非对称加密的规则,我们让服务器持有私钥,让客户端持有公钥。这样就能保证客户端给服务器发送消息的时候是安全的(相反,服务器给客户端发送消息就是不安全的),我们可以把协商时重要的逻辑安排在客户端给服务器发送信息的过程中,从而保证了协商过程的安全性。

客户端如何获得公钥?

现在用非对称加密算法解决了协商的安全问题,但是非对称加密的前提是客户端需要获得公钥,这又是一个问题了,客户端与服务器打交道之前是互不知道双方身份的,怎么才能让客户端获得公钥呢?

也就只有两种办法:

  1. 客户端向服务器要公钥
  2. 客户端向一个远程的公共服务器获取公钥

方法2显然是不行的,尚且不说多了一个访问节点,如何找到公共服务器的地址也是一个待解决的问题,所以还是使用方法1。

但是方法1存在一个问题:如果中间人把服务器发送给客户端的公钥调包了怎么办?也就是说客户端无法知道发送公钥的是否是正真的服务器。

引入第三方机构解决问题

客户端无法辨识服务端和中间人的问题称为“身份验证”问题,也就是说我们需要为服务器向客户端发送公钥的过程进行加密。

这下完了,之前我们因遇到对称加密的瓶颈选择了非对称加密,现在使用非对称加密也遇到了瓶颈。显然这两种加密方式都是不可用的了,否则会再次陷入死循环。

接下来我们只好通过第三方机构的介入,解决这个问题。首先我们自己保存有第三方权威机构的公钥,然后第三方机构使用私钥对服务器将要发送给客户端的公钥进行加密,客户端接收到这个经加密的公钥后(数字证书),就能通过自己保存的第三方机构公钥进行解密。

到这里为止,我们解释了HTTPS中使用到的对称加密,非对称加密,CA,数字证书的概念,但是还差一个叫数字签名的概念没有解释。

在现实生活中,CA不单止会给我们正常公司发放证书,还会给中间人的坏公司发放证书,如果中间人把发放的证书调包了怎么办?这时我们仍能用CA的私钥进行解密,但是证书已经被调包了。

那么客户端怎样验证证书的真伪呢?答案是证书本身会告诉客户端如何辨认真伪。比方说证书上面有一个证书编号,还有一个如何计算证书编号的方法,客户端可以根据计算证书编号的方法计算出自己要获得的证书的编号,然后把这个编号和证书上的编号进行比对,如果一样证明没有被调包。

这里的证书编号指的就是数字签名,证书指的就是数字证书。

总结一下HTTPS:HTTPS想要保证客户端与服务器之间的通信安全,就得使用对称加密算法进行加密。协商对称加密算法的过程通过非对称加密算法来保证。在非对称加密算法中,客户端获得公钥的过程需要第三方机构(CA)通过颁发数字证书保证安全性。

总得来说通过这一系列机制协商出了一个对称加密算法后,客户端与服务器之间就能通过该算法进行安全的通信了

  • TCP消息和UDP的主要区别

  • Epoll比起select/poll,主要优化点在哪里?

  • Post和Get有什么区别?

  • 接收时,如何确定一个HTTP消息已经读取完毕

  • 从一个命令行下载一个网页,会涉及哪些网络协议?(DNS,TCP,HTTP,如果有ARP和SSL更好)

  • HTTP/2主要优化点

  • \1. C++中具体是怎么实现多态的?构造函数可以是虚函数吗?构造函数可以调用虚函数吗?
    编译器发现一个类中有虚函数,便会立即为此类生成虚函数表 vtable。虚函数表的各表项为指向对应虚函数的指针。
    构造函数不可以是虚函数,也不能调用虚函数,因为此时虚函数表还未生成。

    \2. 操作系统中进程、线程的区别,进程间通信通信的方式?
    进程: 进程是资源(CPU、内存等)分配的基本单位,具有一定功能的程序关于某个数据集合上的一次运行活动,是系统进行资源分配和调度的一个独立单位。
    线程:线程是进程的一个实体,是独立运行和独立调度的基本单位(CPU上真正运行的是线程)。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
    进程通信的方式:管道,有名管道,信号量,消息队列,信号,共享内存,套接字.

    \3. python中的多线程能充分利用多核CPU吗?如果要充分利用的话应该怎么做?
    python的多线程不能充分利用多CPU,因为python解释器有一个全局锁;
    要想利用多CPU只能使用多进程模型,或者使用C++

    \4. http属于OSI分层协议中的哪一层?TCP/UDP是属于哪一层?TCP的三次握手的过程是怎样的?为什么要有三次握手?
    http属于应用层,TCP/UDP属于传输层;

    假设 A 为客户端,B 为服务器端。

    首先 B 处于 LISTEN(监听)状态,等待客户的连接请求。

    A 向 B 发送连接请求报文,SYN=1,ACK=0,选择一个初始的序号 x。

    B 收到连接请求报文,如果同意建立连接,则向 A 发送连接确认报文,SYN=1,ACK=1,确认号为 x+1,同时也选择一个初始的序号 y。

    A 收到 B 的连接确认报文后,还要向 B 发出确认,确认号为 y+1,序号为 x+1。

    B 收到 A 的确认后,连接建立。

    客户端发送的连接请求如果在网络中滞留,那么就会隔很长一段时间才能收到服务器端发回的连接确认。客户端等待一个超时重传时间之后,就会重新请求连接。但是这个滞留的连接请求最后还是会到达服务器,如果不进行三次握手,那么服务器就会打开两个连接。如果有第三次握手,客户端会忽略服务器之后发送的对滞留连接请求的连接确认,不进行第三次握手,因此就不会再次打开连接。

    \5. TCP怎么实现可靠传输的?

    TCP滑动窗口是缓存的一部分,用来暂时存放字节流。发送方和接收方各有一个窗口,接收方通过 TCP 报文段中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小。
    发送窗口内的字节都允许被发送,接收窗口内的字节都允许被接收。如果发送窗口左部的字节已经发送并且收到了确认,那么就将发送窗口向右滑动一定距离,直到左部第一个字节不是已发送并且已确认的状态;接收窗口的滑动类似,接收窗口左部字节已经发送确认并交付主机,就向右滑动接收窗口。
    接收窗口只会对窗口内最后一个按序到达的字节进行确认,例如接收窗口已经收到的字节为 {31, 34, 35},其中 {31} 按序到达,而 {34, 35} 就不是,因此只对字节 31 进行确认。发送方得到一个字节的确认之后,就知道这个字节之前的所有字节都已经被接收。

不同地区的用户的请求怎么打到附近的地区呢?

答:讲了CDN

补充:

CDN 将内容资源分发到位于多个地理位置机房中的服务器上,这样我们在访问内容资源的时候,不用访问源服务器。而是直接访问离我们最近的 CDN 节点 ,这样一来就省去了长途跋涉的时间成本,从而实现了网络加速。

找到离用户最近的 CDN 节点是由 CDN 的全局负载均衡器(Global Sever Load Balance,GSLB负责的。

那 GSLB 是在什么时候起作用的呢?在回答这个问题前,我们先来看看在没有 CDN 的情况下,访问域名时发生的事情。

在没有 CDN 的情况下,当我们访问域名时,DNS 服务器最终会返回源服务器的地址。

比如,当我们在浏览器输入 www.xiaolin.com 域名后,在本地 host 文件找不到域名时,客户端就会访问本地 DNS 服务器。

dns

这时候:

  • 如果本地 DNS 服务器有缓存该网站的地址,则直接返回网站的地址;

  • 如果没有就通过递归查询的方式,先请求根 DNS,根 DNS 返回顶级 DNS(.com)的地址;再请求 .com 顶级 DNS 得到 xiaolin.com 的域名服务器地址,再从 xiaolin.com 的域名服务器中查询到 www.xiaolin.com 对应的 IP 地址,然后返回这个 IP 地址,同时本地 DNS 缓存该 IP 地址,这样下一次的解析同一个域名就不需要做 DNS 的迭代查询了。

但加入 CDN 后就不一样了

dns_cdn

会在 xiaolin.com 这个 DNS 服务器上,设置一个 CNAME 别名,指向另外一个域名 www.xiaolin.cdn.com,返回给本地 DNS 服务器。

接着继续解析该域名,这个时候访问的就是 xiaolin.cdn.com 这台 CDN 专用的 DNS 服务器,在这个服务器上,又会设置一个 CNAME,指向另外一个域名,这次指向的就是 CDN 的 GSLB。

接着,本地 DNS 服务器去请求 CDN 的 GSLB 的域名,GSLB 就会为用户选择一台合适的 CDN 节点提供服务,选择的依据主要有以下几点:

  • 看用户的 IP 地址,查表得知地理位置,找相对最近的 CDN 节点;

  • 看用户所在的运营商网络,找相同网络的 CDN 节点;

  • 看用户请求 URL,判断哪一台服务器上有用户所请求的资源;

  • 查询 CDN 节点的负载情况,找负载较轻的节点;

GSLB 会基于以上的条件进行综合分析后,找出一台最合适的 CDN 节点,并返回该 CDN 节点的 IP 地址给本地 DNS 服务器,然后本地 DNS 服务器缓存该 IP 地址,并将 IP 返回给客户端,客户端去访问这个 CDN 节点,下载资源。

TCP的close_wait在哪端,如果我们场景中出现了大量的close_wait,你觉得要怎么排查

答:被动方,代码逻辑有问题,没close

补充:

CLOSE_WAIT 状态是「被动关闭方」才会有的状态,而且如果「被动关闭方」没有调用 close 函数关闭连接,那么就无法发出 FIN 报文,从而无法使得 CLOSE_WAIT 状态的连接转变为 LAST_ACK 状态。

所以,当服务端出现大量 CLOSE_WAIT 状态的连接的时候,说明服务端的程序没有调用 close 函数关闭连接

那什么情况会导致服务端的程序没有调用 close 函数关闭连接?这时候通常需要排查代码。

我们先来分析一个普通的 TCP 服务端的流程:

  1. 创建服务端 socket,bind 绑定端口、listen 监听端口

  2. 将服务端 socket 注册到 epoll

  3. epoll_wait 等待连接到来,连接到来时,调用 accpet 获取已连接的 socket

  4. 将已连接的 socket 注册到 epoll

  5. epoll_wait 等待事件发生

  6. 对方连接关闭时,我方调用 close

可能导致服务端没有调用 close 函数的原因,如下。

第一个原因:第 2 步没有做,没有将服务端 socket 注册到 epoll,这样有新连接到来时,服务端没办法感知这个事件,也就无法获取到已连接的 socket,那服务端自然就没机会对 socket 调用 close 函数了。

不过这种原因发生的概率比较小,这种属于明显的代码逻辑 bug,在前期 read view 阶段就能发现的了。

第二个原因:第 3 步没有做,有新连接到来时没有调用 accpet 获取该连接的 socket,导致当有大量的客户端主动断开了连接,而服务端没机会对这些 socket 调用 close 函数,从而导致服务端出现大量 CLOSE_WAIT 状态的连接。

发生这种情况可能是因为服务端在执行 accpet 函数之前,代码卡在某一个逻辑或者提前抛出了异常。

第三个原因:第 4 步没有做,通过 accpet 获取已连接的 socket 后,没有将其注册到 epoll,导致后续收到 FIN 报文的时候,服务端没办法感知这个事件,那服务端就没机会调用 close 函数了。

发生这种情况可能是因为服务端在将已连接的 socket 注册到 epoll 之前,代码卡在某一个逻辑或者提前抛出了异常。之前看到过别人解决 close_wait 问题的实践文章,感兴趣的可以看看:一次 Netty 代码不健壮导致的大量 CLOSE_WAIT 连接原因分析

第四个原因:第 6 步没有做,当发现客户端关闭连接后,服务端没有执行 close 函数,可能是因为代码漏处理,或者是在执行 close 函数之前,代码卡在某一个逻辑,比如发生死锁等等。

可以发现,当服务端出现大量 CLOSE_WAIT 状态的连接的时候,通常都是代码的问题,这时候我们需要针对具体的代码一步一步的进行排查和定位,主要分析的方向就是服务端为什么没有调用 close

服务端出现大量 TIME_WAIT 状态的原因有哪些?

我们先来看一下 TCP 四次挥手的流程吧,看看 TIME_WAIT 状态发生在哪一个阶段。

下面这个图,是由「客户端」作为「主动关闭方」的 TCP 四次挥手的流程。

tcp_timewait

TCP 四次挥手的流程

从上面我们可以知道,TIME_WAIT 状态是「主动关闭连接方」才会出现的状态。而且 TIME_WAIT 状态会持续 2MSL 时间才会进入到 close 状态。在 Linux 上 2MSL 的时长是 60 秒,也就是说停留在 TIME_WAIT 的时间为固定的 60 秒

为什么需要 TIME_WAIT 状态?(老八股文了,帮大家复习一波)主要有两个原因:

  • 保证「被动关闭连接」的一方,能被正确的关闭。TCP 协议在关闭连接的四次挥手中,在主动关闭方发送的最后一个 ACK 报文,有可能丢失,这时被动方会重新发 FIN 报文, 如果这时主动方处于 CLOSE 状态 ,就会响应 RST 报文而不是 ACK 报文。所以主动方要处于 TIME_WAIT 状态,而不能是 CLOSE。

  • 防止历史连接中的数据,被后面相同四元组的连接错误的接收。TCP 报文可能由于路由器异常而 “迷路”,在迷途期间,TCP 发送端可能因确认超时而重发这个报文,迷途的报文在路由器修复后也会被送到最终目的地,这个原来的迷途报文就称为 lost duplicate。在关闭一个 TCP 连接后,马上又重新建立起一个相同的 IP 地址和端口之间的 TCP 连接,后一个连接被称为前一个连接的化身,那么有可能出现这种情况,前一个连接的迷途重复报文在前一个连接终止后出现,从而被误解成从属于新的化身。为了避免这个情 况, TIME_WAIT 状态需要持续 2MSL,因为这样就可以保证当成功建立一个 TCP 连接的时候,来自连接先前化身的重复报文已经在网络中消逝。

很多人误解以为只有客户端才会有 TIME_WAIT 状态,这是不对的。TCP 是全双工协议,哪一方都可以先关闭连接,所以哪一方都可能会有 TIME_WAIT 状态。

总之记住,谁先关闭连接的,它就是主动关闭方,那么 TIME_WAIT 就会出现在主动关闭方

什么场景下服务端会主动断开连接呢?

如果服务端出现大量的 TIME_WAIT 状态的 TCP 连接,就是说明服务端主动断开了很多 TCP 连接

问题来了,什么场景下服务端会主动断开连接呢?

  • 第一个场景:HTTP 没有使用长连接

  • 第二个场景:HTTP 长连接超时

  • 第三个场景:HTTP 长连接的请求数量达到上限

接下来,分别介绍下。

第一个场景:HTTP 没有使用长连接

我们先来看看 HTTP 长连接(Keep-Alive)机制是怎么开启的。

在 HTTP/1.0 中默认是关闭的,如果浏览器要开启 Keep-Alive,它必须在请求的 header 中添加:

Connection: Keep-Alive

然后当服务器收到请求,作出回应的时候,它也被添加到响应中 header 里:

Connection: Keep-Alive

这样做,TCP 连接就不会中断,而是保持连接。当客户端发送另一个请求时,它会使用同一个 TCP 连接。这一直继续到客户端或服务器端提出断开连接。

从 HTTP/1.1 开始, 就默认是开启了 Keep-Alive,现在大多数浏览器都默认是使用 HTTP/1.1,所以 Keep-Alive 都是默认打开的。一旦客户端和服务端达成协议,那么长连接就建立好了。

如果要关闭 HTTP Keep-Alive,需要在 HTTP 请求或者响应的 header 里添加 Connection:close 信息,也就是说,只要客户端和服务端任意一方的 HTTP header 中有 Connection:close 信息,那么就无法使用 HTTP 长连接的机制

关闭 HTTP 长连接机制后,每次请求都要经历这样的过程:建立 TCP -> 请求资源 -> 响应资源 -> 释放连接,那么此方式就是 HTTP 短连接,如下图:

tcp_syn_fin

HTTP 短连接

在前面我们知道,只要任意一方的 HTTP header 中有 Connection:close 信息,就无法使用 HTTP 长连接机制,这样在完成一次 HTTP 请求/处理后,就会关闭连接。

问题来了,这时候是客户端还是服务端主动关闭连接呢?

在 RFC 文档中,并没有明确由谁来关闭连接,请求和响应的双方都可以主动关闭 TCP 连接。

不过,根据大多数 Web 服务的实现,不管哪一方禁用了 HTTP Keep-Alive,都是由服务端主动关闭连接,那么此时服务端上就会出现 TIME_WAIT 状态的连接。

客户端禁用了 HTTP Keep-Alive,服务端开启 HTTP Keep-Alive,谁是主动关闭方?

当客户端禁用了 HTTP Keep-Alive,这时候 HTTP 请求的 header 就会有 Connection:close 信息,这时服务端在发完 HTTP 响应后,就会主动关闭连接。

为什么要这么设计呢?HTTP 是请求-响应模型,发起方一直是客户端,HTTP Keep-Alive 的初衷是为客户端后续的请求重用连接,如果我们在某次 HTTP 请求-响应模型中,请求的 header 定义了 connection:close 信息,那不再重用这个连接的时机就只有在服务端了,所以我们在 HTTP 请求-响应这个周期的「末端」关闭连接是合理的。

客户端开启了 HTTP Keep-Alive,服务端禁用了 HTTP Keep-Alive,谁是主动关闭方?

当客户端开启了 HTTP Keep-Alive,而服务端禁用了 HTTP Keep-Alive,这时服务端在发完 HTTP 响应后,服务端也会主动关闭连接。

为什么要这么设计呢?在服务端主动关闭连接的情况下,只要调用一次 close() 就可以释放连接,剩下的工作由内核 TCP 栈直接进行了处理,整个过程只有一次 syscall;如果是要求 客户端关闭,则服务端在写完最后一个 response 之后需要把这个 socket 放入 readable 队列,调用 select / epoll 去等待事件;然后调用一次 read() 才能知道连接已经被关闭,这其中是两次 syscall,多一次用户态程序被激活执行,而且 socket 保持时间也会更长。

因此,当服务端出现大量的 TIME_WAIT 状态连接的时候,可以排查下是否客户端和服务端都开启了 HTTP Keep-Alive,因为任意一方没有开启  HTTP Keep-Alive,都会导致服务端在处理完一个 HTTP 请求后,就主动关闭连接,此时服务端上就会出现大量的 TIME_WAIT 状态的连接。

针对这个场景下,解决的方式也很简单,让客户端和服务端都开启 HTTP Keep-Alive 机制。

第二个场景:HTTP 长连接超时

HTTP 长连接的特点是,只要任意一端没有明确提出断开连接,则保持 TCP 连接状态。

HTTP 长连接可以在同一个 TCP 连接上接收和发送多个 HTTP 请求/应答,避免了连接建立和释放的开销。

http_keepalive

可能有的同学会问,如果使用了 HTTP 长连接,如果客户端完成一个 HTTP 请求后,就不再发起新的请求,此时这个 TCP 连接一直占用着不是挺浪费资源的吗?

对没错,所以为了避免资源浪费的情况,web 服务软件一般都会提供一个参数,用来指定 HTTP 长连接的超时时间,比如 nginx 提供的 keepalive_timeout 参数。

假设设置了 HTTP 长连接的超时时间是 60 秒,nginx 就会启动一个「定时器」,如果客户端在完后一个 HTTP 请求后,在 60 秒内都没有再发起新的请求,定时器的时间一到,nginx 就会触发回调函数来关闭该连接,那么此时服务端上就会出现 TIME_WAIT 状态的连接

http_timeout

HTTP 长连接超时

当服务端出现大量 TIME_WAIT 状态的连接时,如果现象是有大量的客户端建立完 TCP 连接后,很长一段时间没有发送数据,那么大概率就是因为 HTTP 长连接超时,导致服务端主动关闭连接,产生大量处于 TIME_WAIT 状态的连接。

可以往网络问题的方向排查,比如是否是因为网络问题,导致客户端发送的数据一直没有被服务端接收到,以至于 HTTP 长连接超时。

第三个场景:HTTP 长连接的请求数量达到上限

Web 服务端通常会有个参数,来定义一条 HTTP 长连接上最大能处理的请求数量,当超过最大限制时,就会主动关闭连接。

比如 nginx 的 keepalive_requests 这个参数,这个参数是指一个 HTTP 长连接建立之后,nginx 就会为这个连接设置一个计数器,记录这个 HTTP 长连接上已经接收并处理的客户端请求的数量。如果达到这个参数设置的最大值时,则 nginx 会主动关闭这个长连接,那么此时服务端上就会出现 TIME_WAIT 状态的连接。

keepalive_requests 参数的默认值是 100 ,意味着每个 HTTP 长连接最多只能跑 100  次请求,这个参数往往被大多数人忽略,因为当 QPS (每秒请求数) 不是很高时,默认值 100 凑合够用。

但是,对于一些 QPS 比较高的场景,比如超过 10000 QPS,甚至达到 30000 , 50000 甚至更高,如果 keepalive_requests 参数值是 100,这时候就 nginx 就会很频繁地关闭连接,那么此时服务端上就会出大量的 TIME_WAIT 状态

针对这个场景下,解决的方式也很简单,调大 nginx 的 keepalive_requests 参数就行。

TIME_WAIT 状态过多有什么危害?

过多的 TIME-WAIT 状态主要的危害有两种:

  • 第一是占用系统资源,比如文件描述符、内存资源、CPU 资源等;

  • 第二是占用端口资源,端口资源也是有限的,一般可以开启的端口为 32768~61000,也可以通过 net.ipv4.ip_local_port_range参数指定范围。

客户端和服务端 TIME_WAIT 过多,造成的影响是不同的。

如果客户端(主动发起关闭连接方)的 TIME_WAIT 状态过多,占满了所有端口资源,那么就无法对「目的 IP+ 目的 PORT」都一样的服务端发起连接了,但是被使用的端口,还是可以继续对另外一个服务端发起连接的。具体可以看我这篇文章:客户端的端口可以重复使用吗?

因此,客户端(发起连接方)都是和「目的 IP+ 目的 PORT 」都一样的服务端建立连接的话,当客户端的 TIME_WAIT 状态连接过多的话,就会受端口资源限制,如果占满了所有端口资源,那么就无法再跟「目的 IP+ 目的 PORT」都一样的服务端建立连接了。

不过,即使是在这种场景下,只要连接的是不同的服务端,端口是可以重复使用的,所以客户端还是可以向其他服务端发起连接的,这是因为内核在定位一个连接的时候,是通过四元组(源IP、源端口、目的IP、目的端口)信息来定位的,并不会因为客户端的端口一样,而导致连接冲突。

如果服务端(主动发起关闭连接方)的 TIME_WAIT 状态过多,并不会导致端口资源受限,因为服务端只监听一个端口,而且由于一个四元组唯一确定一个 TCP 连接,因此理论上服务端可以建立很多连接,但是 TCP 连接过多,会占用系统资源,比如文件描述符、内存资源、CPU 资源等。

如何优化 TIME_WAIT 状态?

这里给出优化 TIME-WAIT 的几个方式,都是有利有弊:

  • 打开 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps 选项;

  • net.ipv4.tcp_max_tw_buckets

  • 程序中使用 SO_LINGER ,应用强制使用 RST 关闭。

方式一:net.ipv4.tcp_tw_reuse 和 tcp_timestamps

开启 tcp_tw_reuse,则可以复用处于 TIME_WAIT 的 socket 为新的连接所用

有一点需要注意的是,tcp_tw_reuse 功能只能用客户端(连接发起方),因为开启了该功能,在调用 connect() 函数时,内核会随机找一个 time_wait 状态超过 1 秒的连接给新的连接复用。

net.ipv4.tcp_tw_reuse = 1

使用这个选项,还有一个前提,需要打开对 TCP 时间戳的支持,即

net.ipv4.tcp_timestamps=1(默认即为 1)

这个时间戳的字段是在 TCP 头部的「选项」里,它由一共 8 个字节表示时间戳,其中第一个 4 字节字段用来保存发送该数据包的时间,第二个 4 字节字段用来保存最近一次接收对方发送到达数据的时间。

由于引入了时间戳,可以使得重复的数据包会因为时间戳过期被自然丢弃,因此 TIME_WAIT 状态才可以被复用。

方式二:net.ipv4.tcp_max_tw_buckets

这个值默认为 18000,当系统中处于 TIME_WAIT 的连接一旦超过这个值时,系统就会将后面的 TIME_WAIT 连接状态重置,这个方法比较暴力。

net.ipv4.tcp_max_tw_buckets = 18000

方式三:程序中使用 SO_LINGER

我们可以通过设置 socket 选项,来设置调用 close 关闭连接行为。

struct linger so_linger; so_linger.l_onoff = 1; so_linger.l_linger = 0; setsockopt(s, SOL_SOCKET, SO_LINGER, &so_linger,sizeof(so_linger));

如果l_onoff为非 0, 且l_linger值为 0,那么调用close后,会立该发送一个RST标志给对端,该 TCP 连接将跳过四次挥手,也就跳过了TIME_WAIT状态,直接关闭。

但这为跨越TIME_WAIT状态提供了一个可能,不过是一个非常危险的行为,不值得提倡。

前面介绍的方法都是试图越过 TIME_WAIT状态的,这样其实不太好。虽然 TIME_WAIT 状态持续的时间是有一点长,显得很不友好,但是它被设计来就是用来避免发生乱七八糟的事情。

《UNIX网络编程》一书中却说道:TIME_WAIT 是我们的朋友,它是有助于我们的,不要试图避免这个状态,而是应该弄清楚它

如果服务端要避免过多的 TIME_WAIT 状态的连接,就永远不要主动断开连接,让客户端去断开,由分布在各处的客户端去承受 TIME_WAIT

服务端出现大量 CLOSE_WAIT 状态的原因有哪些?

还是拿这张图:

close_wait_more

TCP 四次挥手的流程

从上面这张图我们可以得知,CLOSE_WAIT 状态是「被动关闭方」才会有的状态,而且如果「被动关闭方」没有调用 close 函数关闭连接,那么就无法发出 FIN 报文,从而无法使得 CLOSE_WAIT 状态的连接转变为 LAST_ACK 状态。

所以,当服务端出现大量 CLOSE_WAIT 状态的连接的时候,说明服务端的程序没有调用 close 函数关闭连接

那什么情况会导致服务端的程序没有调用 close 函数关闭连接?这时候通常需要排查代码。

我们先来分析一个普通的 TCP 服务端的流程:

  1. 创建服务端 socket,bind 绑定端口、listen 监听端口

  2. 将服务端 socket 注册到 epoll

  3. epoll_wait 等待连接到来,连接到来时,调用 accpet 获取已连接的 socket

  4. 将已连接的 socket 注册到 epoll

  5. epoll_wait 等待事件发生

  6. 对方连接关闭时,我方调用 close

可能导致服务端没有调用 close 函数的原因,如下。

第一个原因:第 2 步没有做,没有将服务端 socket 注册到 epoll,这样有新连接到来时,服务端没办法感知这个事件,也就无法获取到已连接的 socket,那服务端自然就没机会对 socket 调用 close 函数了。

不过这种原因发生的概率比较小,这种属于明显的代码逻辑 bug,在前期 read view 阶段就能发现的了。

第二个原因:第 3 步没有做,有新连接到来时没有调用 accpet 获取该连接的 socket,导致当有大量的客户端主动断开了连接,而服务端没机会对这些 socket 调用 close 函数,从而导致服务端出现大量 CLOSE_WAIT 状态的连接。

发生这种情况可能是因为服务端在执行 accpet  函数之前,代码卡在某一个逻辑或者提前抛出了异常。

第三个原因:第 4 步没有做,通过 accpet 获取已连接的 socket 后,没有将其注册到 epoll,导致后续收到 FIN 报文的时候,服务端没办法感知这个事件,那服务端就没机会调用 close 函数了。

发生这种情况可能是因为服务端在将已连接的 socket 注册到 epoll 之前,代码卡在某一个逻辑或者提前抛出了异常。之前看到过别人解决 close_wait 问题的实践文章,感兴趣的可以看看:一次 Netty 代码不健壮导致的大量 CLOSE_WAIT 连接原因分析

第四个原因:第 6 步没有做,当发现客户端关闭连接后,服务端没有执行 close 函数,可能是因为代码漏处理,或者是在执行 close 函数之前,代码卡在某一个逻辑,比如发生死锁等等。

可以发现,当服务端出现大量 CLOSE_WAIT 状态的连接的时候,通常都是代码的问题,这时候我们需要针对具体的代码一步一步的进行排查和定位,主要分析的方向就是服务端为什么没有调用 close
面试问题

1)TCP/IP协议
1)三次握手/四次挥手过程
2)TIME_WAIT状态
1)主动关闭/被动关闭
2)需要的原因
3)缓解措施
4)有没有方式不出现TIME_WAIT状态
3)RST出现的场景
4)滑动窗口
2)网络编程
1)EPOLL/SELECT的区别
2)边沿触发/水平触发
3)事件触发的场景
1)读事件 有哪些场景
2)写事件 有哪些场景
4)READ调用返回值场景
1)0
2)-1
3)>0
5)UDP客户端可以CONNECT不,那CONNECT和不CONNECT有啥区别
3)后台编程
1)FORK的用途,FORK区分父子进程方式
2)进程间通信方式
3)僵尸进程
4)内存模型,有哪些段构成
5)进程/线程/协程的区别
4)常见中间件
1)MYSQL
1)为啥不建议SELECT *
2)覆盖索引
3)分页优化
2)ZOOKEEPER
1)有哪些WATCH以及对应唤醒事件
3)KAFKA
1)分区分服
2)生产者
3)消费者
5)语言
1)JAVA语言
1)内存管理
6)设计模式
1)单例模式
7)项目中的难点、挑战点

22989-腾讯云网络后台开发工程师(CSIG全资子公司)(西安)

建议用online ddl(网上可以查)修改,就是逗号后面的参数
另外,添加字段要加上after,根据表结构看看fromWanIp适合在哪个字段后
ALTER TABLE cEip ALTER COLUMN ispId SET DEFAULT -1, ALGORITHM=INPLACE, LOCK=NONE;

HTTP 长连接和 TCP 长连接有什么区别?

其实就是HTTP 的 Keep-Alive 和 TCP 的 Keepalive 有什么区别?
事实上,这两个完全是两样不同东西,实现的层面也不同:

  • HTTP 的 Keep-Alive,是由应用层(用户态) 实现的,称为 HTTP 长连接;

  • TCP 的 Keepalive,是由 TCP 层(内核态) 实现的,称为 TCP 保活机制;

接下来,分别说说它们。

HTTP 的 Keep-Alive

HTTP 协议采用的是「请求-应答」的模式,也就是客户端发起了请求,服务端才会返回响应,一来一回这样子。

http_process

请求-应答

由于 HTTP 是基于 TCP 传输协议实现的,客户端与服务端要进行 HTTP 通信前,需要先建立 TCP 连接,然后客户端发送 HTTP  请求,服务端收到后就返回响应,至此「请求-应答」的模式就完成了,随后就会释放 TCP 连接。

http_end

一个 HTTP 请求

如果每次请求都要经历这样的过程:建立 TCP -> 请求资源 -> 响应资源 -> 释放连接,那么此方式就是 HTTP 短连接,如下图:

http_short

HTTP 短连接

这样实在太累人了,一次连接只能请求一次资源。

能不能在第一个 HTTP 请求完后,先不断开 TCP 连接,让后续的 HTTP 请求继续使用此连接?

当然可以,HTTP 的 Keep-Alive 就是实现了这个功能,可以使用同一个 TCP 连接来发送和接收多个 HTTP 请求/应答,避免了连接建立和释放的开销,这个方法称为 HTTP 长连接

http_long

HTTP 长连接

HTTP 长连接的特点是,只要任意一端没有明确提出断开连接,则保持 TCP 连接状态。

怎么才能使用 HTTP 的 Keep-Alive 功能?

在 HTTP 1.0 中默认是关闭的,如果浏览器要开启 Keep-Alive,它必须在请求的包头中添加:

Connection: Keep-Alive

然后当服务器收到请求,作出回应的时候,它也添加一个头在响应中:

Connection: Keep-Alive

这样做,连接就不会中断,而是保持连接。当客户端发送另一个请求时,它会使用同一个连接。这一直继续到客户端或服务器端提出断开连接。

从 HTTP 1.1 开始, 就默认是开启了 Keep-Alive,如果要关闭 Keep-Alive,需要在 HTTP 请求的包头里添加:

Connection:close

现在大多数浏览器都默认是使用 HTTP/1.1,所以 Keep-Alive 都是默认打开的。一旦客户端和服务端达成协议,那么长连接就建立好了。

HTTP 长连接不仅仅减少了 TCP 连接资源的开销,而且这给 HTTP 流水线技术提供了可实现的基础。

所谓的 HTTP 流水线,是客户端可以先一次性发送多个请求,而在发送过程中不需先等待服务器的回应,可以减少整体的响应时间。

举例来说,客户端需要请求两个资源。以前的做法是,在同一个 TCP 连接里面,先发送 A 请求,然后等待服务器做出回应,收到后再发出 B 请求。HTTP 流水线机制则允许客户端同时发出 A 请求和 B 请求。

http_pipline

右边为 HTTP 流水线机制

但是服务器还是按照顺序响应,先回应 A 请求,完成后再回应 B 请求。

而且要等服务器响应完客户端第一批发送的请求后,客户端才能发出下一批的请求,也就说如果服务器响应的过程发生了阻塞,那么客户端就无法发出下一批的请求,此时就造成了「队头阻塞」的问题。

可能有的同学会问,如果使用了 HTTP 长连接,如果客户端完成一个 HTTP 请求后,就不再发起新的请求,此时这个 TCP 连接一直占用着不是挺浪费资源的吗?

对没错,所以为了避免资源浪费的情况,web 服务软件一般都会提供 keepalive_timeout 参数,用来指定 HTTP 长连接的超时时间。

比如设置了 HTTP 长连接的超时时间是 60 秒,web 服务软件就会启动一个定时器,如果客户端在完后一个 HTTP 请求后,在 60 秒内都没有再发起新的请求,定时器的时间一到,就会触发回调函数来释放该连接。

http_timeout_1

HTTP 长连接超时

TCP 的 Keepalive

TCP 的 Keepalive 这东西其实就是 TCP 的保活机制,它的工作原理我之前的文章写过,这里就直接贴下以前的内容。

如果两端的 TCP 连接一直没有数据交互,达到了触发 TCP 保活机制的条件,那么内核里的 TCP 协议栈就会发送探测报文。

  • 如果对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。

  • 如果对端主机崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡

所以,TCP 保活机制可以在双方没有数据交互的情况,通过探测报文,来确定对方的 TCP 连接是否存活,这个工作是在内核完成的。

tcp_alive

TCP 保活机制

注意,应用程序若想使用 TCP 保活机制需要通过 socket 接口设置 SO_KEEPALIVE 选项才能够生效,如果没有设置,那么就无法使用 TCP 保活机制。

总结

HTTP 的 Keep-Alive 也叫 HTTP 长连接,该功能是由「应用程序」实现的,可以使得用同一个 TCP 连接来发送和接收多个 HTTP 请求/应答,减少了 HTTP 短连接带来的多次 TCP 连接建立和释放的开销。

TCP 的 Keepalive 也叫 TCP 保活机制,该功能是由「内核」实现的,当客户端和服务端长达一定时间没有进行数据交互时,内核为了确保该连接是否还有效,就会发送探测报文,来检测对方是否还在线,然后来决定是否要关闭该连接。

HTTPS 一定安全可靠吗?

客户端通过浏览器向服务端发起 HTTPS 请求时,被「假基站」转发到了一个「中间人服务器」,于是客户端是和「中间人服务器」完成了 TLS 握手,然后这个「中间人服务器」再与真正的服务端完成 TLS 握手。

具体过程如下:
https_anquan

  • 客户端向服务端发起 HTTPS 建立连接请求时,然后被「假基站」转发到了一个「中间人服务器」,接着中间人向服务端发起 HTTPS 建立连接请求,此时客户端与中间人进行 TLS 握手,中间人与服务端进行 TLS 握手;

  • 在客户端与中间人进行 TLS 握手过程中,中间人会发送自己的公钥证书给客户端,客户端验证证书的真伪,然后从证书拿到公钥,并生成一个随机数,用公钥加密随机数发送给中间人,中间人使用私钥解密,得到随机数,此时双方都有随机数,然后通过算法生成对称加密密钥(A),后续客户端与中间人通信就用这个对称加密密钥来加密数据了。

  • 在中间人与服务端进行 TLS 握手过程中,服务端会发送从 CA 机构签发的公钥证书给中间人,从证书拿到公钥,并生成一个随机数,用公钥加密随机数发送给服务端,服务端使用私钥解密,得到随机数,此时双方都有随机数,然后通过算法生成对称加密密钥(B),后续中间人与服务端通信就用这个对称加密密钥来加密数据了。

  • 后续的通信过程中,中间人用对称加密密钥(A)解密客户端的 HTTPS 请求的数据,然后用对称加密密钥(B)加密 HTTPS 请求后,转发给服务端,接着服务端发送 HTTPS 响应数据给中间人,中间人用对称加密密钥(B)解密 HTTPS 响应数据,然后再用对称加密密钥(A)加密后,转发给客户端。

从客户端的角度看,其实并不知道网络中存在中间人服务器这个角色。

那么中间人就可以解开浏览器发起的 HTTPS 请求里的数据,也可以解开服务端响应给浏览器的 HTTPS 响应数据。相当于,中间人能够 “偷看” 浏览器与服务端之间的 HTTPS 请求和响应的数据。

但是要发生这种场景是有前提的,前提是用户点击接受了中间人服务器的证书。

中间人服务器与客户端在 TLS 握手过程中,实际上发送了自己伪造的证书给浏览器,而这个伪造的证书是能被浏览器(客户端)识别出是非法的,于是就会提醒用户该证书存在问题。

website_alert

如果用户执意点击「继续浏览此网站」,相当于用户接受了中间人伪造的证书,那么后续整个 HTTPS 通信都能被中间人监听了。

所以,这其实并不能说 HTTPS 不够安全,毕竟浏览器都已经提示证书有问题了,如果用户坚决要访问,那不能怪 HTTPS ,得怪自己手贱

客户端是如何验证证书的?

接下来,详细说一下实际中数字证书签发和验证流程。

如下图图所示,为数字证书签发和验证流程:

website_cert当服务端向 CA 机构申请证书的时候,CA 签发证书的过程,如上图左边部分:

  • 首先 CA 会把持有者的公钥、用途、颁发者、有效时间等信息打成一个包,然后对这些信息进行 Hash 计算,得到一个 Hash 值;

  • 然后 CA 会使用自己的私钥将该 Hash 值加密,生成 Certificate Signature,也就是 CA 对证书做了签名;

  • 最后将 Certificate Signature 添加在文件证书上,形成数字证书;

客户端校验服务端的数字证书的过程,如上图右边部分:

  • 首先客户端会使用同样的 Hash 算法获取该证书的 Hash 值 H1;

  • 通常浏览器和操作系统中集成了 CA 的公钥信息,浏览器收到证书后可以使用 CA 的公钥解密 Certificate Signature 内容,得到一个 Hash 值 H2 ;

  • 最后比较 H1 和 H2,如果值相同,则为可信赖的证书,否则则认为证书不可信。

但事实上,证书的验证过程中还存在一个证书信任链的问题,因为我们向 CA 申请的证书一般不是根证书签发的,而是由中间证书签发的,比如百度的证书,从下图你可以看到,证书的层级有三级:

website_cert_1

对于这种三级层级关系的证书的验证过程如下:

  • 客户端收到 baidu.com 的证书后,发现这个证书的签发者不是根证书,就无法根据本地已有的根证书中的公钥去验证 baidu.com 证书是否可信。于是,客户端根据 baidu.com 证书中的签发者,找到该证书的颁发机构是 “GlobalSign Organization Validation CA - SHA256 - G2”,然后向 CA 请求该中间证书。

  • 请求到证书后发现 “GlobalSign Organization Validation CA - SHA256 - G2” 证书是由 “GlobalSign Root CA” 签发的,由于 “GlobalSign Root CA” 没有再上级签发机构,说明它是根证书,也就是自签证书。应用软件会检查此证书有否已预载于根证书清单上,如果有,则可以利用根证书中的公钥去验证 “GlobalSign Organization Validation CA - SHA256 - G2” 证书,如果发现验证通过,就认为该中间证书是可信的。

  • “GlobalSign Organization Validation CA - SHA256 - G2” 证书被信任后,可以使用 “GlobalSign Organization Validation CA - SHA256 - G2” 证书中的公钥去验证 baidu.com 证书的可信性,如果验证通过,就可以信任 baidu.com 证书。

在这四个步骤中,最开始客户端只信任根证书 GlobalSign Root CA 证书的,然后 “GlobalSign Root CA” 证书信任 “GlobalSign Organization Validation CA - SHA256 - G2” 证书,而 “GlobalSign Organization Validation CA - SHA256 - G2” 证书又信任 baidu.com 证书,于是客户端也信任 baidu.com 证书。总括来说,由于用户信任 GlobalSign,所以由 GlobalSign 所担保的 baidu.com 可以被信任,另外由于用户信任操作系统或浏览器的软件商,所以由软件商预载了根证书的 GlobalSign 都可被信任。

browser_neizhi

操作系统里一般都会内置一些根证书,比如我的 MAC 电脑里内置的根证书有这么多:

computer_neizhi

这样的一层层地验证就构成了一条信任链路,整个证书信任链验证流程如下图所示:

cert_line

如果你的电脑中毒了,被恶意导入了中间人的根证书,那么在验证中间人的证书的时候,由于你操作系统信任了中间人的根证书,那么等同于中间人的证书是合法的。

这种情况下,浏览器是不会弹出证书存在问题的风险提醒的。

这其实也不关 HTTPS 的事情,是你电脑中毒了才导致 HTTPS 数据被中间人劫持的。

所以,HTTPS 协议本身到目前为止还是没有任何漏洞的,即使你成功进行中间人攻击,本质上是利用了客户端的漏洞(用户点击继续访问或者被恶意导入伪造的根证书),并不是 HTTPS 不够安全。

为什么抓包工具能截取 HTTPS 数据?

抓包工具 Fiddler 之所以可以明文看到 HTTPS 数据,工作原理与中间人一致的。

对于 HTTPS 连接来说,中间人要满足以下两点,才能实现真正的明文代理:

  1. 中间人,作为客户端与真实服务端建立连接这一步不会有问题,因为服务端不会校验客户端的身份;

  2. 中间人,作为服务端与真实客户端建立连接,这里会有客户端信任服务端的问题,也就是服务端必须有对应域名的私钥;

中间人要拿到私钥只能通过如下方式:

  1. 去网站服务端拿到私钥;

  2. 去CA处拿域名签发私钥;

  3. 自己签发证书,且被浏览器信任;

不用解释,抓包工具只能使用第三种方式取得中间人的身份。

使用抓包工具进行 HTTPS 抓包的时候,需要在客户端安装 Fiddler 的根证书,这里实际上起认证中心(CA)的作用。

Fiddler 能够抓包的关键是客户端会往系统受信任的根证书列表中导入 Fiddler 生成的证书,而这个证书会被浏览器信任,也就是 Fiddler 给自己创建了一个认证中心 CA。

客户端拿着中间人签发的证书去中间人自己的 CA 去认证,当然认为这个证书是有效的。

如何避免被中间人抓取数据?

我们要保证自己电脑的安全,不要被病毒乘虚而入,而且也不要点击任何证书非法的网站,这样 HTTPS 数据就不会被中间人截取到了。

当然,我们还可以通过 HTTPS 双向认证来避免这种问题。

一般我们的 HTTPS 是单向认证,客户端只会验证了服务端的身份,但是服务端并不会验证客户端的身份。

如果用了双向认证方式,不仅客户端会验证服务端的身份,而且服务端也会验证客户端的身份。

ssl_shuangxiang

服务端一旦验证到请求自己的客户端为不可信任的,服务端就拒绝继续通信,客户端如果发现服务端为不可信任的,那么也中止通信。

客户端连接一个不存在的 IP 地址,会发生什么?客户端连接一个存在的 IP 地址但是端口不存在,会发生什么?

PS:这里的「连接」指的是 TCP 连接。

Q1:客户端连接一个不存在的 IP 地址,会发生什么?

这个问题要分两种情况来思考,不同的情况得到的结论是不同的。

第一个情况:目标 IP 地址和客户端的 IP 地址是同一个局域网(网络号相同)。

第一种情况,客户端无法发出 SYN 报文,主要卡在数据链路层。

因为目标地址不存在 IP 地址,客户端的内核在发 arp 请求的时候,广播询问这个目标 IP 地址是谁的,由于网络中不存在该目标 IP 地址,所以没有设备应答客户端的 arp 请求。

由于客户端无法拿到目标设备的 MAC,这样就没办法组装 MAC 头的信息,所以 SYN 报文无法发送出去

第二个情况:目标 IP 地址和客户端的 IP 地址不在同一个局域网(网络号不同)。

第二种情况,客户端会先将 SYN 报文发给路由器,然后路由器会继续转发。

由于目标 IP 地址是不存在的,该 SYN 报文会在网络中消亡,因此客户端是不会收到对 SYN 报文的确认报文的,接着客户端会触发超时重传,重传 SYN 报文,直到重传的次数达到最大次数后,客户端的连接就会被释放

可能有的同学好奇,为什么这种情况客户端的 SYN 报文可以发出来?

因为当目标 IP 地址和客户端 IP 地址不在同一个局域网时,客户端客通过路由表的判断,判断到下一步是要将网络报文发送给路由器。

route_table

这时候数据链路层的 arp 请求,会广播询问 IP 地址(路由器 IP 地址)是谁的,路由器发现是自己的 IP 地址,于是就会将自己的 MAC 地址告诉客户端。

然后客户端的网络报文中 MAC 头的「目标 MAC 地址」填入的就是路由器的 MAC 地址,于是 SYN 报文就可以发送出去了。

由于目标 MAC 地址是路由器的,所以就会被路由器接收,然后路由器继续通过路由表的判断,转发给下一个路由器,直到找到目标设备。

Q2:客户端连接一个存在的 IP 地址但是端口不存在,会发生什么?

客户端连接的目标 IP 地址是存在的,那么 SYN 报文就能正确的抵达到目标设备。

目标设备收到 SYN 报文后,发现端口号并没有被进程监听,这时候目标设备的内核就会回 RST 报文。

客户端收到 RST 报文后,就会释放连接。

…….

至此,结论已说完。

不知道你们会不会觉得信息量很大,如果你觉得信息量大,那么你该补补网络知识啦。

第二题不难,难在的是第一题,如果你没有把两台电脑之间是怎么通信的搞清楚,那么你是无从下手回答的,所以建立好体系化的网络知识,面对这类的场景题目,就能做到举一反三了。

最近比较忙,偷懒了,没有画图,如果哪里没理解的,可以先看我以前写的这篇文章:探究!一个数据包在网络中的心路历程,有详细讲解每一层是怎么封装头部的,以及路由表是怎么判断的。

最后再提一个问题:****客户端发送了一个目标 IP 地址存在但是端口不存在的 UDP 报文,UDP 没有像 TCP 那样的 RST 报文,此时会发生什么?

网络

  • TCP消息和UDP的主要区别

  • Epoll比起select/poll,主要优化点在哪里?

  • Post和Get有什么区别?

  • 接收时,如何确定一个HTTP消息已经读取完毕

  • 从一个命令行下载一个网页,会涉及哪些网络协议?(DNS,TCP,HTTP,如果有ARP和SSL更好)

  • HTTP/2主要优化点

  • \1. C++中具体是怎么实现多态的?构造函数可以是虚函数吗?构造函数可以调用虚函数吗?
    编译器发现一个类中有虚函数,便会立即为此类生成虚函数表 vtable。虚函数表的各表项为指向对应虚函数的指针。
    构造函数不可以是虚函数,也不能调用虚函数,因为此时虚函数表还未生成。

    \2. 操作系统中进程、线程的区别,进程间通信通信的方式?
    进程: 进程是资源(CPU、内存等)分配的基本单位,具有一定功能的程序关于某个数据集合上的一次运行活动,是系统进行资源分配和调度的一个独立单位。
    线程:线程是进程的一个实体,是独立运行和独立调度的基本单位(CPU上真正运行的是线程)。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
    进程通信的方式:管道,有名管道,信号量,消息队列,信号,共享内存,套接字.

    1. python中的多线程能充分利用多核CPU吗?如果要充分利用的话应该怎么做?
      python的多线程不能充分利用多CPU,因为python解释器有一个全局锁;
      要想利用多CPU只能使用多进程模型,或者使用C++

    2. http属于OSI分层协议中的哪一层?TCP/UDP是属于哪一层?TCP的三次握手的过程是怎样的?为什么要有三次握手?
      http属于应用层,TCP/UDP属于传输层;

    假设 A 为客户端,B 为服务器端。

    首先 B 处于 LISTEN(监听)状态,等待客户的连接请求。

    A 向 B 发送连接请求报文,SYN=1,ACK=0,选择一个初始的序号 x。

    B 收到连接请求报文,如果同意建立连接,则向 A 发送连接确认报文,SYN=1,ACK=1,确认号为 x+1,同时也选择一个初始的序号 y。

    A 收到 B 的连接确认报文后,还要向 B 发出确认,确认号为 y+1,序号为 x+1。

    B 收到 A 的确认后,连接建立。

    客户端发送的连接请求如果在网络中滞留,那么就会隔很长一段时间才能收到服务器端发回的连接确认。客户端等待一个超时重传时间之后,就会重新请求连接。但是这个滞留的连接请求最后还是会到达服务器,如果不进行三次握手,那么服务器就会打开两个连接。如果有第三次握手,客户端会忽略服务器之后发送的对滞留连接请求的连接确认,不进行第三次握手,因此就不会再次打开连接。

    1. TCP怎么实现可靠传输的?

    TCP滑动窗口是缓存的一部分,用来暂时存放字节流。发送方和接收方各有一个窗口,接收方通过 TCP 报文段中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小。
    发送窗口内的字节都允许被发送,接收窗口内的字节都允许被接收。如果发送窗口左部的字节已经发送并且收到了确认,那么就将发送窗口向右滑动一定距离,直到左部第一个字节不是已发送并且已确认的状态;接收窗口的滑动类似,接收窗口左部字节已经发送确认并交付主机,就向右滑动接收窗口。
    接收窗口只会对窗口内最后一个按序到达的字节进行确认,例如接收窗口已经收到的字节为 {31, 34, 35},其中 {31} 按序到达,而 {34, 35} 就不是,因此只对字节 31 进行确认。发送方得到一个字节的确认之后,就知道这个字节之前的所有字节都已经被接收。

作者

Amazing Coder

发布于

2022-10-08

更新于

2022-10-08

许可协议

评论