Skip to content

Latest commit

 

History

History
218 lines (131 loc) · 21 KB

File metadata and controls

218 lines (131 loc) · 21 KB

什么是 Socket?

这是计算机网络连载系列的第十三篇文章,前十二篇文章见

计算机网络基础知识总结

TCP/IP 基础知识总结

计算机网络应用层

计算机网络传输层

计算机网络网络层

计算机网络链路层

计算机网络 ARP 协议

计算机网络 DNS 协议

计算机网络 ICMP 协议

计算机网络 DHCP 协议

计算机网络 NAT 协议

计算机网络 web 请求过程

之前的计算机网络系列文章中没有详细介绍 Socket ,这篇文章我们来聊一下 Socket。

关于对 Socket 的认识,大致分为下面几个主题,Socket 是什么,Socket 是如何创建的,Socket 是如何连接并收发数据的,Socket 套接字的删除等。

Socket 是什么以及创建过程

一个数据包经由应用程序产生,进入到协议栈中进行各种报文头的包装,然后操作系统调用网卡驱动程序指挥硬件,把数据发送到对端主机。整个过程的大体的图示如下。

Socket 相当于是应用程序的"大门",我们在网络中发送的报文都会经过这道大门才能够进入到应用程序中,让应用程序来使用报文中的数据。

如果你看我之前的文章你应该知道,协议栈其实是位于操作系统中一些协议的堆叠,这些协议包括 TCP、UDP、ARP、ICMP、IP 等。通常某个协议的设计都是为了解决某些问题,比如 TCP 协议的设计就解决了安全可靠的传输问题;UDP 协议的设计就是报文小,传输效率高,性能佳;ARP 协议的设计是通过 IP 地址查询物理(Mac)地址;ICMP 协议设计的目的是在 IP 报文无法发送到目标主机时,返回错误消息给发送方;IP 设计的目的是为了实现大规模主机的互联互通。

应用程序比如浏览器、电子邮件、文件传输服务器等产生的数据,会经过传输层协议作为载体进行传输。不过应用程序是不会和传输层直接建立联系的,而是有一个能够连接应用层和传输层之间的套件,这个套件就是 Socket

在上面这幅图中,应用程序包含 Socket 和解析器,解析器的作用就是向 DNS 服务器发起查询,查询目标 IP 地址。

应用程序的下面就是操作系统,操作系统内部包括协议栈,操作系统下面就是网卡驱动程序,网卡驱动程序负责控制网卡硬件,网卡驱动程序驱动网卡硬件完成底层收发工作。这一系列分层的逻辑就很像公司的管理模式,领导要开发一个 APP(应用层),会通过秘书(Socket)把它交给项目经理(操作系统),项目经理通过驱动程序(管理规范)使硬件(程序员)完成开发工作。

在操作系统内部有一块用于存放控制信息的存储空间,这块存储空间记录了用于控制通信的控制信息。其实这些控制信息就是 Socket 的实体,或者说存放控制信息的内存空间就是套接字的实体

很多资料上表示 Socket 就是几个描述符,是不存在实体的,其实这些描述符所占用的空间就是 Socket 的实体。

这里大家有可能不太清楚所以然,所以我用了一下 netstat 命令来给大伙看一下套接字是啥玩意。

我们在 Windows 的命令提示符中输入

netstat -ano

# netstat 用于显示套接字内容 , -ano 是可选选项
# a 不仅显示正在通信的套接字,还显示包括尚未开始通信等状态的所有套接字
# n 显示 IP 地址和端口号
# o 显示套接字的程序 PID

我的计算机会出现下面结果。

图中的每一行都是一个套接字,每一列是一个元组,所以一个套接字就是五元组(协议、本地地址、外部地址、状态、PID)。有的时候也被叫做四元组,四元组不包括协议字段。

比如图中的第一行,它的协议就是 TCP,本地地址和远程地址都是 0.0.0.0(0.0.0.0 表示通信还没有开始,IP 地址暂时还未确定),而本地端口已知是 135,但是远程端口还未知,此时的状态是 LISTENING,LISTENING 表示 TCP 正在等待与远程主机建立连接(关于各种状态之间的转换,大家可以阅读笔者的这篇文章 TCP ,丫的终于来了!!)最后一个元组是 PID,即进程标识符,PID 就像我们的身份证号码,能够精确定位唯一的进程。我们可以通过 Win 下的任务管理器和 Mac 下的活动监视器来查询 PID。

现在你应该大致清楚 Socket 是干什么的了,它就是一个描述符。那么,这个描述符是啥时候有的?或者说这个描述符是啥时候创建的呢?

先说结论:Socket 是和应用程序一起创建的。

在应用程序比如浏览器启动时,应用程序会申请创建套接字,协议栈会根据应用程序的申请创建套接字:首先分配一个套接字所需的内存空间,这一步相当于是准备一个容器,但是容器空着不行,浪费空间,所以你还需要向容器中放入信息;如果你不申请创建套接字所需要的内存空间,你创建的控制信息也没有地方存放,所以分配内存空间,放入控制信息缺一不可,这就是套接字的创建过程。

套接字创建完成后,会返回一个套接字描述符给应用程序,这个描述符相当于是区分不同套接字的号码牌。根据这个描述符,应用程序在委托协议栈收发数据时就需要提供这个描述符。

内核中有一套 Socket 库,这个库中有各种方法,比如 create() ,connect() ,listen() ,accept(),send() 等等,应用程序在通信过程中会调用库中的各种方法来完成数据的收发操作。

套接字连接

套接字创建完成后,就可以进行数据收发操作了,不过在数据收发之前,还需要建立连接这个过程。只不过这个连接并不是真实的连接:可以理解为把一根管子连在两个电脑之间。

套接字刚刚创建完成后,还没有数据,也不知道通信对象是谁。在这种状态下,即使你让客户端应用程序委托协议栈发送数据,它也不知道发送到哪里。所以浏览器需要根据网址来查询服务器的 IP 地址,这就是 DNS 解析器的工作,查询到目标主机后,再把目标主机的 IP 告诉协议栈,至此,客户端这边就准备好了。

在服务器上,与客户端一样也需要创建套接字,但是同样的它也不知道通信对象是谁,所以我们需要让客户端向服务器告知客户端的必要信息:IP 地址和端口号

现在通信双方建立连接的必要信息已经具备,只差一股东南风了。

再等这个东南风之前,让我们再检查一下是否万事俱备了,当通信双方正在收发数据的时候,此时 OS 发生中断,转而干别的事儿了,那没收发完的这些数据该放哪?这确实是个问题。

实际上,通信双方都有一块缓冲区,这个缓冲区就是用来存放通信数据的,收发数据时这些数据会优先存放在缓冲区中,然后应用程序再取缓冲区中的数据来使用,这就解决了上面这个问题。

OK,现在是真的万事俱备了。那么现在客户端想要给服务器发送一条数据,该进行哪些操作呢?

首先,客户端应用程序需要调用 Socket 库中的 connect 方法,提供 socket 描述符和服务器 IP 地址、端口号。

connect(<描述符><服务器IP地址和端口号>)

这些信息会传递给协议栈中的 TCP 模块,TCP 模块会对请求报文进行封装,再传递给 IP 模块,进行 IP 报文头的封装,然后传递给物理层,进行帧头封装,之后通过网络介质传递给服务器,服务器上会对帧头、IP 模块、TCP 模块的报文头进行解析,从而找到对应的套接字,套接字收到请求后,会写入相应的信息,并且把状态改为正在连接。请求过程完成后,服务器的 TCP 模块会返回响应,这个过程和客户端是一样的(如果大家不太清楚报文头的封装过程,可以阅读笔者的这篇文章 TCP/IP 基础知识总结

在一个完整的请求和响应过程中,控制信息起到非常关键的作用(具体的作用我们后面会说)。

  • SYN 就是同步的缩写,客户端会首先发送 SYN 数据包,请求服务端建立连接。
  • ACK 就是相应的意思,它是对发送 SYN 数据包的响应。
  • FIN 是终止的意思,它表示客户端/服务器想要终止连接。

由于网络环境的复杂多变,经常会存在数据包丢失的情况,所以双方通信时需要相互确认对方的数据包是否已经到达,而判断的标准就是 ACK 的值。

(通信双方连接的建立会经过三次握手流程,对三次握手详细的介绍可以阅读笔者的这篇文章 TCP 基础知识

当所有建立连接的报文都能够正常收发之后,此时套接字就已经进入可收发状态了,此时可以认为用一根管子把两个套接字连接了起来。当然,实际上并不存在这个管子。此时 connect 操作结束。

收发数据

当控制流程从 connect 回到应用程序之后,接下来就正式进入数据收发阶段,数据收发操作是从应用程序调用 write 将要发送的数据交给协议栈开始的,协议栈收到数据之后执行发送操作。

协议栈并不会关心应用程序传输过来的是什么数据,因为这些数据最终都会转换为二进制序列,协议栈在收到数据之后并不会马上把数据发送出去,而是会将数据放在发送缓冲区,再等待应用程序发送下一条数据。

为什么收到数据包不会直接发送出去,而是放在缓冲区中呢?

因为只要一旦收到数据就会发送,就有可能发送大量的小数据包,导致网络效率下降。所以协议栈需要将数据积攒到一定数量才能将其发送出去。至于协议栈会向缓冲区放多少数据,这个不同版本和种类的操作系统有不同的说法,不过,所有的操作系统和种类都会遵循下面这几个标准:

  • 第一个判断要素是每个网络包能够容纳的数据长度,判断的标准是 MTU,它表示的是一个网络包的最大长度。最大长度包含头部,所以如果单论数据区的话,就会用 MTU - 包头长度,由此的出来的最大数据长度被称为 MSS

  • 另一个判断标准是时间,当应用程序产生的数据比较少,协议栈向缓冲区放置数据效率不高时,如果每次都等到 MSS 再发送的话,可能因为等待时间太长造成延迟,在这种情况下,即使数据长度没有到达 MSS,也应该把数据发送出去。

所以现在客户端和服务器之间都各自有自己的发送缓冲区和接收缓冲区。

协议栈并没有告诉我们怎样平衡这两个因素,如果数据长度优先,那么效率有可能比较低;如果时间优先,那又会降低网络的效率。

假设我们使用的是长度有限法则,此时缓冲区已满,协议栈要发送数据了,协议栈刚要把数据发送出去,却发现无法一次性传输这么大数据量(相对的)的数据,那怎么办呢?

在这种情况下,发送缓冲区中的数据就会超过 MSS 的长度,发送缓冲区中的数据会以 MSS 大小为一个数据包进行拆分,拆分出来的每块数据都会加上 TCP,IP,以太网头部,然后被放进单独的网络包中。

到现在,网络包已经准备好发往服务器了,但是数据发送操作还没有结束,因为服务器还未确认是否已经收到网络包。因此在客户端发送数据包之后,还需要服务器进行确认。

TCP 模块在拆分数据时,会计算出网络包偏移量,这个偏移量就是相对于数据从头开始计算的第几个字节,并将算好的字节数写在 TCP 头部,TCP 模块还会生成一个网络包的序号(SYN),这个序号是唯一的,这个序号就是用来让服务器进行确认的。

服务器会对客户端发送过来的数据包进行确认,确认无误之后,服务器会生成一个序号和确认号(ACK)并一起发送给客户端,客户端确认之后再发送确认号给服务器。

我们来看一下实际的工作过程。

首先,客户端在连接时需要计算出序号初始值,并将这个值发送给服务器。接下来,服务器通过这个初始值计算出 确认号并返回给客户端。初始值在通信过程中有可能会丢弃,因此当服务器收到初始值后需要返回确认号用于确认。同时,服务器也需要计算出从服务器到客户端方向的序号初始值,并将这个值发送给客户端。然后,客户端也需要根据服务器发来的初始值计算出确认号发送给服务器,至此,连接建立完成,接下来就可以进入数据收发阶段了。

数据收发阶段中,通信双方可以同时发送请求和响应,双方也可以同时对请求进行确认。

请求 - 确认机制非常强大,通过这一机制,我们可以确认接收方有没有收到某个包,如果没有收到则重新发送,这样一来,但凡网络中出现的任何错误,我们都可以即使发现并补救。

网卡、集线器、路由器都没有错误补救机制,一旦检测到错误就会直接丢弃数据包,应用程序也没有这种机制,起作用的只是 TCP/IP 模块。

由于网络环境复杂多变,所以数据包会存在丢失情况,因此发送序号和确认号也存在一定规则,TCP 会通过窗口管理确认号,我们这篇文章不再赘述,大家可以阅读笔者的这篇文章 TCP 基础知识 来寻找答案。

断开连接

当通信双方不再需要收发数据时,需要断开连接。不同的应用程序断开连接的时机不同。以 Web 为例,浏览器向 Web 服务器发送请求消息,Web 服务器再返回响应消息,这时收发数据就全部结束了,服务器可能会首先发起断开响应,当然客户端也有可能会首先发起(谁先断开连接是应用程序做出的判断),与协议栈无关。

无论哪一方发起断开连接的请求,都会调用 Socket 库的 close 程序。我们以服务器断开连接为例,服务器发起断开连接请求,协议栈会生成断开连接的 TCP 头部,其实就是设置 FIN 位,然后委托 IP 模块向客户端发送数据,与此同时,服务器的套接字会记录下断开连接的相关信息

收到服务器发来 FIN 请求后,客户端协议栈会将套接字标记为断开连接状态,然后,客户端会向服务器返回一个确认号,这是断开连接的第一步,在这一步之后,应用程序还会调用 read 来读取数据。等到服务器数据发送完成后,协议栈会通知客户端应用程序数据已经接收完毕。

只要收到服务器返回的所有数据,客户端就会调用 close 程序来结束收发操作,这时客户端会生成一个 FIN 发送给服务器,一段时间后服务器返回 ACK 号,至此,客户端和服务器的通信就结束了。

删除套接字

通信完成后,用来通信的套接字就不再会使用了,此时我们就可以删除这个套接字了。不过,这时候套接字不会马上删除,而是等过一段时间再删除。

等待这段时间是为了防止误操作,最常见的误操作就是客户端返回的确认号丢失,至于等待多长时间,和数据包重传的方式有关。

数据收发阶段汇总

总得来说,两个端系统之间的数据收发操作中,需要调用 socket 的方法流程图如下所示:

  1. socket 中的 API 用于创建通信链路中的端点,创建完成后,会返回描述该套接字的套接字描述符。

  2. 当应用程序具有套接字描述符后,它可以将唯一的名称绑定在套接字上,服务器必须绑定一个名称才能在网络中访问。

  3. 在为服务端分配了 socket 并且将名称使用 bind 绑定到套接字上后,将会调用 listen api。listen 表示客户端愿意等待连接的意愿,listen 必须在 accept api 之前调用。

  4. 客户端应用程序在流套接字(基于 TCP)上调用 connect 发起与服务器的连接请求。

  5. 服务器应用程序使用 accept API 接受客户端连接请求,服务器必须先成功调用 bind 和 listen 后,再调用 accept api。

  6. 在流套接字之间建立连接后,客户端和服务器就可以发起 read/write api 调用了。

  7. 当服务器或客户端要停止操作时,就会调用 close API 释放套接字获取的所有系统资源。

总结

这篇文章我带你汇总了一下 Socket 的创建过程以及作用。

如果你在阅读文章的过程中发现错误和问题,请及时与我联系!

如果文章对你有帮助,希望小伙伴们三连走起!