聊一聊关于 Socket 缓冲区的那些事

楔子

本文来自于公众号《小白debug》

先上这篇文章的目录。

代码执行send成功后,数据就发出去了吗?回答这个问题之前,需要了解什么是Socket 缓冲区。

什么是 Socket 缓冲区?

编程的时候,如果要跟某个IP建立连接,我们需要调用操作系统提供的 Socket API。Socket 在操作系统层面可以理解为一个文件,我们对这个文件可以执行一些方法操作。比如用 listen 方法可以让程序作为服务器端监听来自客户端的连接,用 connect 可以作为客户端连接服务器,用 send 或 write 发送数据、recv 或 read 接收数据。

在建立好连接之后,这个 socket 文件就像是远端机器的 "代理人" 一样,如果我们想给远端服务发点什么东西,那就只需要对这个文件执行写操作就行了。

而写到了这个文件之后,剩下的发送工作自然就是由操作系统「内核」来完成了,不过既然是写给操作系统,那操作系统就需要提供一个地方给用户写,当然接收消息也是一样的。而这个地方,就叫做「Socket」缓冲区。

  • 用户发送消息的时候写给 send buffer(发送缓冲区)
  • 用户接收消息的时候写给 recv buffer(接收缓冲区)

也就是说一个 Socket 会带有两个缓冲区,一个用于发送、另一个用于接收。因为这是个先进先出的结构,所以有时也叫它们发送、接收队列

而写到了这个文件之后,剩下的发送工作自然就是由操作系统「内核」来完成了,不过既然是写给操作系统,那操作系统就需要提供一个地方给用户写,当然接收消息也是一样的。而这个地方,就叫做「Socket」缓冲区。

在 Linux环境下我们可以执行 netstat -nt 命令来查看 Socket 缓冲区:

[root@satori-1 ~]# netstat -nt
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State      
tcp        0      0 10.0.24.11:38414        169.254.0.55:5574       ESTABLISHED
tcp        0     28 10.0.24.11:22           123.125.8.130:61846     ESTABLISHED
tcp        0      0 10.0.24.11:41856        169.254.0.55:8080       TIME_WAIT  
tcp        0      0 10.0.24.11:41004        169.254.0.138:8086      ESTABLISHED
[root@satori-1 ~]# 

Proto 表示协议类型,这里都是 TCP,Recv-Q 和 Send-Q 表示接收缓冲区和发送缓冲区,Local Address  和 Foreign Address 表示本地和远端的 IP 的信息,State 表示连接状态。

我们看到 Recv-Q 这一列全部是 0,表示接收缓冲区中的数组都被应用进程取走了,而 Send-Q 有一列是 28,表示缓冲区中还有 28 字节的数据没有发送。

执行 send 发送的字节,会立马发送吗?

我们在使用 Socket 建立 TCP 连接之后,一般会使用 send 发送数据。

int main(int argc, char *argv[])
{
    // 创建 Socket
    sockfd = socket(AF_INET,SOCK_STREAM, 0))

    // 建立连接  
    connect(sockfd, 服务器ip信息, sizeof(server))  

    // 执行 send 发送消息
    send(sockfd, str, sizeof(str), 0))  

    // 关闭 socket
    close(sockfd);

    return 0;
}

上面是一段伪代码,仅用于展示大概逻辑,我们在建立好连接后,一般会在代码中执行 send 方法。那么此时,消息就会被立刻发到对端机器吗?

答案是不确定!执行 send 之后,数据只是拷贝到了socket 缓冲区。至于什么时候会发数据,发多少数据,全听操作系统安排。

在用户进程中,程序通过操作 socket 会从用户态进入内核态,而 send 方法会将数据一路传到传输层。在识别到是 TCP协议后,会调用 tcp_sendmsg 方法。

// net/ipv4/tcp.c
// 以下省略了大量逻辑
int tcp_sendmsg()
{  
  // 如果还有可以放数据的空间
  if (skb_availroom(skb) > 0) {
    // 尝试拷贝待发送数据到发送缓冲区
    err = skb_add_data_nocache(sk, skb, from, copy);
  }  
  // 下面是尝试发送的逻辑代码,先省略     
}

在 tcp_sendmsg 中, 核心工作就是将待发送的数据组织按照先后顺序放入到发送缓冲区中, 然后根据实际情况(比如拥塞窗口等)判断是否要发数据。如果不发送数据,那么此时直接返回。

如果执行 send 时发送缓冲区满了会怎么办?

前面提到的情况里是,发送缓冲区有足够的空间,可以用于拷贝待发送数据。但如果发送缓冲区空间不足,或者满了,执行发送,会怎么样?

这里需要分两种情况讨论,首先 Socket 在创建的时候,是可以设置为「阻塞」或「非阻塞」。

int s = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP);

比如通过上面的代码,就可以将 Socket 设置为「非阻塞」(SOCK_NONBLOCK)。

当发送缓冲区「满了」,如果还向 Socket 执行 send:

  • 如果此时 socket 是阻塞的,那么程序会在那干等、死等,直到释放出新的缓存空间,就继续把数据拷进去,然后返回:

  • 如果此时 socket 是非阻塞的,程序就会立刻返回一个 EAGAIN 错误信息,意思是 Try again ,表示现在缓冲区满了,你也别等了,待会再试一次。

我们可以简单看下源码是怎么实现的。还是回到刚才的 tcp_sendmsg 发送方法中。

int tcp_sendmsg()
{  
  if (skb_availroom(skb) > 0) {
    // ..如果有足够缓冲区就执行 balabla
  } else {
    // 如果发送缓冲区没空间了,那就等到有空间,至于等的方式,分阻塞和非阻塞
    if ((err = sk_stream_wait_memory(sk, &timeo)) != 0)
        goto do_error;
  }   
}        

里面提到的 sk_stream_wait_memory 会根据 socket 是否阻塞来决定是一直等等一会就返回。

int sk_stream_wait_memory(struct sock *sk, long *timeo_p)
{
    while (1) {
    // 非阻塞模式时,会等到超时返回 EAGAIN
        if (等待超时))
            return -EAGAIN;     
     // 阻塞等待时,会等到发送缓冲区有足够的空间了,才跳出
        if (sk_stream_memory_free(sk) && !vm_wait)
            break;
    }
    return err;
}

 

如果执行 recv 时接收缓冲区满了会怎么办?

接收缓冲区也是类似的情况,当接收缓冲区「为空」,如果还向 Socket 执行 recv:

  • 如果此时 socket 是阻塞的,那么程序会在那「干等」,直到接收缓冲区有数据,就会把数据从接收缓冲区拷贝到用户缓冲区,然后「返回」

  • 如果此时 Socket 是非阻塞的,程序就会立刻返回一个 EAGAIN 错误信息。

下面用一张图汇总一下:

如果 Socket 缓冲区还有数据,执行 close 了,会怎么样?

首先我们要知道,一般正常情况下,发送缓冲区和接收缓冲区都应该是空的。如果发送、接收缓冲区长时间非空,说明有数据堆积,这往往是由于一些网络问题或用户应用层问题,导致数据没有正常处理。那么正常情况下,如果 socket 缓冲区为空,执行 close,就会触发四次挥手。

而如果缓冲区非空,那么分两种情况:

接收缓冲区非空时执行 Close

Socket Close 时,主要的逻辑在 tcp_close() 里实现。先说结论,关闭过程主要有两种情况:

  • 如果接收缓冲区还有数据未读,会先把接收缓冲区的数据清空,然后给对端发一个 RST
  • 如果接收缓冲区是空的,那么就调用 tcp_send_fin() 开始进行四次挥手过程的第一次挥手
void tcp_close(struct sock *sk, long timeout)
{
  // 如果接收缓冲区有数据,那么清空数据
    while ((skb = __skb_dequeue(&sk->sk_receive_queue)) != NULL) {
        u32 len = TCP_SKB_CB(skb)->end_seq - TCP_SKB_CB(skb)->seq -
              tcp_hdr(skb)->fin;
        data_was_unread += len;
        __kfree_skb(skb);
    }

   if (data_was_unread) {
    // 如果接收缓冲区的数据被清空了,发 RST
        tcp_send_active_reset(sk, sk->sk_allocation);
     } else if (tcp_close_state(sk)) {
    // 正常四次挥手, 发 FIN
        tcp_send_fin(sk);
    }
    // 等待关闭
    sk_stream_wait_close(sk, timeout);
}

发送缓冲区非空时执行 Close

以前以为在这种情况下内核会把发送缓冲区数据清空,然后四次挥手,但是发现源码中不是这样的。

void tcp_send_fin(struct sock *sk)
{
  // 获得发送缓冲区的最后一块数据
    struct sk_buff *skb, *tskb = tcp_write_queue_tail(sk);
    struct tcp_sock *tp = tcp_sk(sk);

  // 如果发送缓冲区还有数据
    if (tskb && (tcp_send_head(sk) || sk_under_memory_pressure(sk))) {
        TCP_SKB_CB(tskb)->tcp_flags |= TCPHDR_FIN; // 把最后一块数据值为 FIN 
        TCP_SKB_CB(tskb)->end_seq++;
        tp->write_seq++;
    }  else {
    // 发送缓冲区没有数据,就造一个FIN包
  }
  // 发送数据
    __tcp_push_pending_frames(sk, tcp_current_mss(sk), TCP_NAGLE_OFF);
}

此时,还有些数据没发出去,内核会把发送缓冲区最后一个数据块拿出来,然后置为 FIN。Socket 缓冲区是个「先进先出」的队列,这种情况是指内核会等待 TCP 层安静把发送缓冲区数据都发完,最后再执行四次挥手的第一次挥手(FIN包)。

有一点需要注意的是,只有在「接收缓冲区为空的前提下」,我们才有可能走到 tcp_send_fin() 。而只有在进入了这个方法之后,我们才有可能考虑发送缓冲区是否为空的场景。

UDP 的缓冲区

说完 TCP 了,我们聊聊 UDP。这对好基友,同时都是传输层里的重要协议。既然前面提到 TCP 有发送、接收缓冲区,那 UDP 有吗?

有人觉得:每个UDP Socket 都有一个接收缓冲区,没有发送缓冲区,从概念上来说就是只要有数据就发,不管对方是否可以正确接收,所以不缓冲,不需要发送缓冲区。

但事实上 UDP Socket 也是 Socket,一个 Socket 就是会有收和发两个缓冲区,跟用什么协议关系不大。所以 UDP 不仅有发送缓冲区,也用发送缓冲区。

一般正常情况下,会把数据直接拷到发送缓冲区后直接发送;但还有一种情况,是在发送数据的时候,设置一个 MSG_MORE 的标记。

ssize_t send(int sock, const void *buf, size_t len, int flags); // flag 置为 MSG_MORE

大概的意思是告诉内核,待会还有其他「更多消息」要一起发,先别着急发出去。此时内核就会把这份数据先用「发送缓冲区」缓存起来,待会应用层说ok了,再一起发。

int udp_sendmsg()
{
    // corkreq 为 true 表示是 MSG_MORE 的方式,仅仅组织报文,不发送;
    int corkreq = up->corkflag || msg->msg_flags&MSG_MORE;

    //  将要发送的数据,按照MTU大小分割,每个片段一个skb;并且这些
    //  skb会放入到套接字的发送缓冲区中;该函数只是组织数据包,并不执行发送动作。
    err = ip_append_data(sk, fl4, getfrag, msg->msg_iov, ulen,
                 sizeof(struct udphdr), &ipc, &rt,
                 corkreq ? msg->msg_flags|MSG_MORE : msg->msg_flags);

    // 没有启用 MSG_MORE 特性,那么直接将发送队列中的数据发送给IP。 
    if (!corkreq)
        err = udp_push_pending_frames(sk);

}

因此,不管是不是 MSG_MORE, IP 都会先把数据放到发送队列中,然后根据实际情况再考虑是不是立刻发送。而我们大部分情况下,都不会用 MSG_MORE,也就是来一个数据包就直接发一个数据包。从这个行为上来说,虽然 UDP 用上了发送缓冲区,但实际上并没有起到 "缓冲" 的作用。

posted @ 2019-11-06 17:01  古明地盆  阅读(3193)  评论(0编辑  收藏  举报