# 【NO.609】深入操作系统，一文搞懂Socket到底是什么

## **1.引言**

我相信大家刚开始学网络编程中socket的时候，都跟我一样对书上所讲的socket概念云里雾里的、似懂非懂，很是困扰。

这篇文章我打算从初学者的角度，用通俗易懂的文字，跟大家分享下我所理解的socket是什么，并由浅入深从操作系统内核实现来透视socket的原理。

## **2.初识socket**

故事要从一个插头说起。

![img](https://pic3.zhimg.com/80/v2-08c006d2aa2acca9c68cfad57c44acfe_720w.webp)

▲ 插头与插座

当我将插头插入插座，那看起来就像是将两者连起来了。

![img](https://pic4.zhimg.com/80/v2-b5b9ade20ef32b7e699b36d6efaa0073_720w.webp)

▲风扇与电力系统建立"连接"

而插座的英文，又叫socket。巧了，我们程序员搞网络编程时也会用到一个叫socket的东西。

其实两者非常相似。通过socket，我们可以与某台机子建立"连接"，建立"连接"的过程，就像是将插口插入插槽一样。

![img](https://pic3.zhimg.com/80/v2-9fc86dc1cf1d34e9c9ed181e7bd1e9ba_720w.webp)

大概概念是了解了，但我相信各位对socket其实还是很模糊。接下来我们从大家最熟悉的使用场景开始说起。

## **3.socket的典型使用场景**

我们想要将数据从A电脑的某个进程发到B电脑的某个进程。

这时候我们需要选择将数据发过去的方式，如果需要确保数据要能发给对方，那就选可靠的TCP协议，如果数据丢了也没关系，看天意，那就选择不可靠的UDP协议。

初学者毫无疑问，首选TCP。

![img](https://pic1.zhimg.com/80/v2-da2cca74e1715819e10747fc1f41e814_720w.webp)

▲TCP是什么

那这时候就需要用socket进行编程。

于是第一步就是创建个关于TCP的socket，就像下面这样：

```text
sock_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
```

上面这个方法会返回socket_fd，它是socket文件的句柄，是个数字，相当于socket的身份证号。

得到了socket_fd之后，对于服务端，就可以依次执行bind(), listen(), accept()方法，然后坐等客户端的连接请求。

对于客户端，得到socket_fd之后，你就可以执行connect()方法向服务端发起建立连接的请求，此时就会发生TCP三次握手（如下图所示）。

![动图封面](https://pic4.zhimg.com/v2-98d0b7d3af9149dd03c36d447eca9f43_b.jpg)



▲握手建立连接流程

连接建立完成后，客户端可以执行send() 方法发送消息，服务端可以执行recv()方法接收消息，反过来，服务器也可以执行send()，客户端执行recv()方法。

到这里为止，就是我们大部分程序员最熟悉的使用场景。

PS：限于篇幅，本篇不展开TCP协议的3次握手原理，有兴趣可以详读：《理论经典：TCP协议的3次握手与4次挥手过程详解》、《跟着动画来学TCP三次握手和四次挥手》。

## **4.socket该怎么设计？**

### 4.1 基本认知


现在，socket我们见过，也用过，但对大部分程序员来说，它是个黑盒。

那既然是黑盒，我们索性假设我们忘了socket。重新设计一个内核网络传输功能。

网络传输，从操作上来看，无非就是发数据和远端之间互相收发数据（也就是对应着写数据和读数据）。

![img](https://pic4.zhimg.com/80/v2-e41f84ac547b8d4ae0800fb14b9b012b_720w.webp)

▲ 读写收发

但显然，事情没那么简单。

这里有两个问题：

- ***1）***接收端和发送端可能不止一个，因此我们需要一些信息做下区分，这个大家肯定很熟悉，可以用IP和端口。IP用来定位是哪台电脑，端口用来定位是这台电脑上的哪个进程；
- ***2）***发送端和接收端的传输方式有很多区别，可以是可靠的TCP协议，也可以是不可靠的UDP协议，甚至还需要支持基于icmp协议的ping命令。

### 4.2 sock的基本定义


写过代码的都知道，为了支持这些功能，我们需要定义一个数据结构去支持这些功能。这个数据结构，叫sock。

为了解决上面的第一个问题，我们可以在sock里加入IP和端口字段：

![img](https://pic3.zhimg.com/80/v2-6dc4b1fb4d4f45ee8e4485a5e023367e_720w.webp)

▲ sock加入IP和端口字段

而第二个问题：我们会发现这些协议虽然各不相同，但还是有一些功能相似的地方，比如收发数据时的一些逻辑完全可以复用。按面向对象编程的思想，我们可以将不同的协议当成是不同的对象类（或结构体），将公共的部分提取出来，通过"继承"的方式，复用功能。

### 4.3 基于各种sock实现网络传输功能

于是，我们将功能重新划分下，定义了一些数据结构：

![img](https://pic3.zhimg.com/80/v2-1056ca3697522ee95190f93d974f7b62_720w.webp)

▲ 继承sock的各类sock

sock是最基础的结构，维护一些任何协议都有可能会用到的收发数据缓冲区。

inet_sock特指用了网络传输功能的sock，在sock的基础上还加入了TTL，端口，IP地址这些跟网络传输相关的字段信息。说到这里大家就懵了，难道还有不是用网络传输的？有，比如Unix domain socket，用于本机进程之间的通信，直接读写文件，不需要经过网络协议栈。这是个非常有用的东西，我以后一定讲讲（画饼）。

inet_connection_sock 是指面向连接的sock，在inet_sock的基础上加入面向连接的协议里相关字段，比如accept队列，数据包分片大小，握手失败重试次数等。虽然我们现在提到面向连接的协议就是指TCP，但设计上linux需要支持扩展其他面向连接的新协议，

tcp_sock 就是正儿八经的tcp协议专用的sock结构了，在inet_connection_sock基础上还加入了tcp特有的滑动窗口、拥塞避免等功能。同样udp协议也会有一个专用的数据结构，叫udp_sock。

好了，现在有了这套数据结构，我们将它们跟硬件网卡对接一下，就实现了网络传输的功能。

### 4.4 提供socket层


可以想象得到，这里面的代码肯定非常复杂，同时还操作了网卡硬件，需要比较高的操作系统权限，再考虑到性能和安全，于是决定将它放在操作系统内核里。

既然网络传输功能做在内核里，那用户空间的应用程序想要用这部分功能的话，该怎么办呢？

这个好办，本着不重复造轮子的原则，我们将这部分功能抽象成一个个简单的接口。以后别人只需要调用这些接口，就可以驱动我们写好的这一大堆复杂的数据结构去发送数据。

那么问题来了，怎么样将这部分功能暴露出去呢？让其他程序员更方便的使用呢？

既然跟远端服务端进程收发数据可以抽象为“读和写”，操作文件也可以抽象为"读和写"，正好有句话叫，"linux里一切皆是文件"，那我们索性，将内核的sock封装成文件就好了。创建sock的同时也创建一个文件，文件有个句柄fd，说白了就是个文件系统里的身份证号码，通过它可以唯一确定是哪个sock。

```text
这个文件句柄fd其实就是 sock_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) 里的sock_fd。
```

将句柄暴露给用户，之后用户就可以像操作文件句柄那样去操作这个sock句柄。在用户空间里操作这个句柄，文件系统就会将操作指向内核sock结构。

是的，操作这个特殊的文件就相当于操作内核里对应的sock：

![img](https://pic2.zhimg.com/80/v2-de1b770efc257f5faf4a69a83fc89415_720w.webp)

▲ 通过文件找到sock

有了sock_fd句柄之后，我们就需要提供一些接口方法，让用户更方便的实现特定的网络编程功能。这些接口，我们列了一下，发现需要有send()，recv()，bind(), listen()，connect()这些。

到这里，我们的内核网络传输功能就算设计完成了。

现在是不是眼熟了，上面这些接口方法其实就是socket提供出来的接口。

所以说：socket其实就是个代码库 or 接口层，它介于内核和应用程序之间，提供了一些高度封装过的接口，让我们去使用内核网络传输功能。

![img](https://pic4.zhimg.com/80/v2-e97d660d25ba35aa8627ea364067687b_720w.webp)

▲ 基于sock实现网络传输功能

到这里，我们应该明白了。我们平时写的应用程序里代码里虽然用了socket实现了收发数据包的功能，但其实真正执行网络通信功能的，不是应用程序，而是linux内核。相当于应用程序通过socket提供的接口，将网络传输的这部分工作外包给了linux内核。

这听起来像不像我们最熟悉的前后端分离的服务架构，虽然这么说不太严谨，但看上去linux就像是被分成了应用程序和内核两个服务。内核就像是后端，暴露了好多个api接口，其中一类就是socket的send()和recv()这些方法。应用程序就像是前端，负责调用内核提供的接口来实现想要的功能。

![img](https://pic1.zhimg.com/80/v2-c9cfcff5288cc1549104419424a90a08_720w.webp)

▲ 进程通过socket调用内核功能

看到这里，我担心大家会有点混乱，下面来做个小的总结。

### 4.5 小结一下

在操作系统内核空间里，实现网络传输功能的结构是sock，基于不同的协议和应用场景，会被泛化为各种类型的xx_sock，它们结合硬件，共同实现了网络传输功能。

为了将这部分功能暴露给用户空间的应用程序使用，于是引入了socket层，同时将sock嵌入到文件系统的框架里，sock就变成了一个特殊的文件，用户就可以在用户空间使用文件句柄，也就是socket_fd来操作内核sock的网络传输能力。

这个socket_fd是一个int类型的数字。

现在回去看socket的中文翻译，套接字，我将它理解为一套用于连接的数字，是不是就觉得特别合理了。

![img](https://pic3.zhimg.com/80/v2-eff2cdc9fceb9aa24e513f8969b5cc3a_720w.webp)

▲网络分层与基于sock实现网络传输功能

原文地址：https://zhuanlan.zhihu.com/p/610147885

作者：cpp后端技术