交互过程如下图所示:
套接字如何创建的
协议栈内部结构
如上图所示,整个请求交互过程分为了几个部分,首先最上层就是应用程序,接着往下是 Socket 库。
再下面就是操作系统的内部了,这里面就包括了协议栈,协议栈上半部分为 TCP 和 UDP ,它们都是负责数据的收发。
只是一个需要 连接,一个不需要连接可以直接收发数据,这两者的详细区别我会在后期文章单独讲解,这里大家先了解下就行。
协议栈的下半部分是 IP 协议,用来真正将数据转变为网络包进行实际数据传送的媒介。
IP 下面就是网卡驱动程序,用来控制网卡硬件。
认识套接字
在协议栈内部有一块用来存放控制信息的内存空间,这里面记录了需要连接的对象 IP 地址、端口号、进行状态等信息。
而套接字本身其实只是一个概念,实际并没有这样一个东西,这个概念如果非要赋予它一个实体,那控制信息可以认为就是它的实体。
在发送数据时,我们需要看下套接字要进行连接的对象 IP 地址和端口号;发送数据之后,套接字里面会记录发送数据经过了多长时间,如果发送收到响应,也会进行记录。
我们来实际看下 套接字 都有哪些信息,可以在你电脑的控制台输入 netstat 命令进行查询:
- Proto: 表示协议类型。这里是 tcp ,如果用到了 udp 就会显示为 udp。
- Local Address : 本机的 IP 地址。
- Foreign Address : 通信对象的 IP 地址
- state : 通信状态。ESTABLISHED 表示完成连接 ,CLOSE_WAIT 表示等待关闭,还有一个状态也很常见,LISTENING:等待对方连接。
当浏览器通过 Socket 库向协议栈发出 socket 调用时,协议栈就会根据申请执行创建套接字的操作。
协议栈首先会分配一个存放套接字的内存空间,然后往里面存入控制信息,这样套接字就创建好了。
连接服务器
创建好套接字后,浏览器会调用 connect ,协议栈就会将本地的套接字和服务器的套接字进行连接。
连接就是通信双方互相交换控制信息,连接操作所交换的控制信息是根据通信规则来确定的,只要双方根据规则进行连接,就能建立起连接关系,完成数据收发的准备。
控制信息
控制信息一般可以分为两类,一类是客户端和服务器相互联系时交换的控制信息,这个信息是两者建立连接、数据收发、断开连接整个通信过程都需要的信息。
一般这些内容是通过 TCP 协议进行定义的。这些信息会被添加进网络包的开头,因此也叫作头部,以太网和 IP 协议也有自己的控制信息,这个信息也叫头部,为了进行区分,我们分别叫作 TCP 头部、以太网头部、IP 头部。
这里罗列了部分 TCP 头部的信息,仅供参考。
控制信息还有一类,是保存在套接字里的,应用程序传递的信息和从通信对象接受的信息都会保存在这里,以及数据收发操作的执行状态也会在这里面。
连接操作的实际过程
连接操作的第一步就是在 TCP 模块处创建表示连接控制信息的头部。当 TCP 头部创建好后,TCP 模块会将信息传递给 IP 模块委托其进行发送。
IP 执行发送后,网络包会通过网络到达服务器,服务器上的 IP 模块将接收到的数据传递给 TCP 模块,TCP 模块根据头部信息找到对应的套接字,套接字中会写入相应的信息,然后将状态改为正在连接。
于此同时,在返回响应时,会将 ACK 控制位设为 1,代表已接收到网络包。服务器 TCP 模块会将响应消息通过 IP 模块向客户端做出响应。
客户端接收到响应后,其 IP 模块将信息传递给 TCP 模块,然后通过 TCP 头部信息确认连接是否成功,SYN 等于 1 就代表成功,客户端还会将 ACK 设置为1 并发回给服务器,服务器收到这个包后才算连接操作真正的完成。
建立连接后,就可以随时进行收发数据了,在调用 close 之前,连接会一直存在。
收发数据
收发数据的触发操作是应用程序发起的,通过调研 write,指定发送数据的长度。
一般当协议栈接受到数据时可能并不会马上发出去,而是放在发送缓冲区中,为什么要这样做呢?
有些程序可能一次性会传所有数据,但有些程序会逐行传递,在这种情况下,如果收到数据就发送,可能会造成发送大量小包数据,导致效率低下。
至于需要积累多少数据才发送一般是根据两方面因素来考量,一个是每个网络包的数据长度,还有一个纬度是处理时间。
网络包容纳的数据长度
首先介绍下两个名词:
MTU: 一个网络包的最大长度,以太网中一般是1500字节,是包含头部的总长度。
MSS: 除去头部后,一个网络包所有容纳的数据最大长度。
处理时间
当一个应用程序发送数据的频率不高时,如果每次都需要等到长度达到 MSS 才发送,就会造成等待时间过长。
为了解决这种情况,协议栈会有一个计时器,如果达到一定时间,即使还远未达到 MSS 长度,也会把网络包发送出去。
ACK 机制确认网络包接收情况
当客户端向服务端发送数据时,TCP会将数据的字节数算好写在 TCP 头部,同时会生成一个随机数 当作 ACK 一并发送给服务端,服务端接受后就会根据实际收到的长度和TCP头部给的长度做对比,来确保数据没有遗漏。
同时客户端还需要告知服务端是从哪个字节开始发送的,而我们的 ACK是个随机值,这时候我们就需要通过 SYN 控制位设置为1发送给服务器,这样服务器就知道其初始是从哪个字节开始发送的。
接受方收到数据后,如果数据没问题,就需要告知发送方收到了多少数据,也是通过 ACK 号的操作来返回的,这个 ACK 的值就是一共接收了多少字节。
通过这种机制,我们就可以确认接收方是否正确收到数据,如果没有准确收到,就可以重新发送网络包。
无论网络发生何种错误,我们就都可以发现并采取补救措施。
窗口滑动
一般如果我们每发送一个网络包就等待 ACK 返回确认后再发送下一个包,这个等待 ACK 的时间啥都不做就会很浪费。
窗口滑动的概念就是每次发送一个网络包,不会等 ACK 返回就会继续发送下一个包,减少等待时间的浪费。
但这种方式也会存在问题,假如发送方不断发送数据给接收方,接收方第一个数据还没处理完,第二个数据就来了,这些来不及处理的数据会进入接收缓冲区,数据会不断增多,就会造成溢出。避免这种方式的处理是通过接收方告知发送方自己最大能接收多少数据,发送方会根据这个值对发送的数据进行控制。
删除套接字
当我们数据收发完成后,就会启动断开机制,以 Web 为例,收发数据结束时,服务器会发起断开过程,会调用 Socket 库的 close 程序,服务器协议栈会生成一个包含断开信息的 TCP 头部,就是将 FIN 比特设置为1。协议栈会委托 IP 模块向客户端发送数据。
当客户端接收到 FIN 为 1 的 TCP 头部时,客户端协议栈会将自己的套接字标记为进入断开操作状态,然后告知服务器已经收到 FIN 为 1的包,客户端会向服务器返回一个 ACK 号。
UDP 协议收发操作
之前我们都是以 TCP 协议讲解的数据收发操作,可以看出整个流程下来其实是挺复杂的,但是有时候可能我们并不需要这么复杂的安全校验,UDP 就可以满足一些简单的数据收发。例如像我们之前提到的 向 DNS 服务器查询 IP 地址,我们就是用的 UDP 协议。
UDP 没有 TCP 的接收确认、窗口等机制,在收发数据之前是不需要进行交换控制信息,不需要进行连接操作。
接收数据也很简单,只需要根据 IP 头部中的接收方和发送方 IP 地址,以及 UDP 头部中的接收方和发送方端口号,找到对应的套接字然后将数据交给相应的应用程序即可。