# socket缓冲区

## socket 缓冲区

**socket** 在操作系统层面，可以理解为一个**文件**。我们可以对这个文件进行一些**方法操作**。

用 `listen` 方法，可以让程序作为服务器**监听**其他客户端的连接。

用 `connect` ，可以作为客户端**连接**服务器。

用 `send` 或 `write` 可以**发送**数据，`recv` 或 `read` 可以**接收**数据。

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

那写到了这个文件之后，剩下的发送工作自然就是由操作系统**内核**来完成了。

既然是写给操作系统，那操作系统就需要**提供一个地方给用户写**。同理，接收消息也是一样。

这个地方就是 **socket 缓冲区**，实际是个环形缓冲队列 `ring buffer`。

用户**发送**消息的时候写给 `send buffer`（发送缓冲区）

用户**接收**消息的时候写给 `recv buffer`（接收缓冲区）

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

**怎么观察 socket 缓冲区**

在 linux 环境下执行 `netstat -nt` 命令。

```bash
# netstat -nt
Active Internet connections (w/o servers)
Proto     Recv-Q     Send-Q        Local Address            Foreign Address         State      
  tcp          0         60      172.22.66.69:22       122.14.220.252:59889    ESTABLISHED
#协议   接收缓冲区  发送缓冲区             本机地址                    远端地址       连接状态
```

## TCP 部分

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

```c
 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` 方法。

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

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

### 缓冲区满的情况

这里分两种情况。

首先，`socket` 在创建的时候，是可以设置是**阻塞**的还是**非阻塞**的。

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

比如通过上面的代码，就可以将 `socket` 设置为**非阻塞** （`SOCK_NONBLOCK`）。

* 如果 `socket` 是阻塞的，那么程序会在那**干等**，直到释放出新的缓存空间，就继续把数据拷进去，然后**返回**。
  * 如果缓冲区没满，但剩余空间不够了，那么程序会先将缓冲区写满，再干等
  * 这种情况会在客户端故障拒收数据的情况下产生。应用程序可以设置超时时间，超过时长后断开和客户端的连接。
* 如果 `socket` 是非阻塞的，程序就会**立刻返回**一个 `EAGAIN` 错误信息，意思是 `Try again` , 现在缓冲区满了，你也别等了，待会再试一次。

### 缓冲区 close

首先我们要知道，\*\*一般正常情况下，发送缓冲区和接收缓冲区 都应该是空的。\*\*如果发送、接收缓冲区长时间非空，说明有数据堆积，这往往是由于一些网络问题或用户应用层问题，导致数据没有正常处理。

正常情况下，如果 `socket` 缓冲区**为空**，执行 `close`。就会触发四次挥手。

这个也是面试老八股文内容了，**这里我们只需要关注第一次挥手，发的是 `FIN` 就够了**。

#### 缓冲区有数据时，执行 close 了，会怎么样？

**接收缓冲区**

* 如果接收缓冲区还有数据未读，会先把接收缓冲区的数据清空，然后给对端发一个 `RST`。（对端就会收到 `connection reset by peer`）

**发送缓冲区**

* 内核会等待 `TCP` 层把发送缓冲区数据都发完，最后再执行 四次挥手的第一次挥手（`FIN`包）

#### 既然如此，如何判断数据发送成功？

通过 `send()` 函数无法判断，只能通过 `TCP`协议的 ACK确认机制，由接收端发送 ACK才能判断数据发送成功

## UDP 部分

`UDP socket` 也是 `socket`，一个 `socket` 就是会有收和发两个缓冲区。跟用什么协议关系不大。因此 `UDP` 也是有两个缓冲区的。

使用 `UDP` 发送数据的时候，可以设置一个 `MSG_MORE` 的标记。

系统会先把数据放到发送队列中，然后根据这个标记再考虑是不是立刻发送。

不过我们大部分情况下，都不会用 `MSG_MORE`，也就是来一个数据包就立即发一个数据包。从这个行为上来说，**虽然UDP用上了发送缓冲区，但实际上并没有起到"缓冲"的作用。**

**参考**

[小白debug - 代码执行send成功后，数据就发出去了吗](https://mp.weixin.qq.com/s/SRzJ4ABooeJ-Lnmotuu60w)
