Socket连接
套接字链接在表面上看就是建立连接,交换数据,断开连接,虽然实际上细节肯定没有那么简单,但是大体上的思路基本不变。
协议栈建立连接
这里记住一个前提:向操作系统内部的协议栈发出委托时,需要按照指定的顺序来调用 Socket 库中的程序组件。
建立Socket的协议大部分情况都是Tcp/ip协议,Socket收发数据类似在两个主机之间建立一个无形的管道,Socket建立的关键是要按照指定顺序调用Socket程序组件,大致的构建顺序如下:
- 创建Socket(Socket类似管道两边的出入口)
- 绑定客户端的套接字到服务端(类似接管道)
- 交换数据。
- 断开Socket连接,解除绑定。
转化为具体的流程图如下:
创建Socket
过程大致为应用程序会把控制流程会转移到 socket 内部并执行创建套接字的操作,完成之后控制流程又会被移交回应用程序。
创建完套接字之后,协议栈需要返回标识符号也就是描述符用于标识是哪一个套接字在进行传数据,因为我们可能打开很多套接字连接访问不同的网站,具体的效果是我们浏览器会打开很多个页面,这时候每一个页面都可能需要创建套接字,此时就需要识别和区分这些套接字依赖描述符。
绑定客户端的套接字到服务端
连接操作核心是调用Socket的connect连接方法,此方法需要指定描述符、 服务器 IP 地址和端口号这 3 个参数。
connect看上去挺复杂,其实本质上就是完成连接动作而已,连接成功会把IP地址和端口号记录到套接字上面。
描述符在创建Socket的时候已经拿到了,IP地址则是在DNS解析的步骤完成,拿到IP之后会放入到应用程序的某个位置替换保存,而端口号则是需要应用程序事先提供。
端口可以简单看作应用程序的入口,DNS解析的IP只能知道主机在哪但是本身发往哪个应用程序是不清楚的,我们可以想象DNS解析类似地图上告诉我们高速的收费站坐标,但是他并不知道对应数据送往那个闸口)。
这里可以理解为端口就是收费站过站口,计算机会要求程序对待应用程序预设明确的端口参与网络交互。
传递消息
接下来的操作是调用read和write函数完成消息传递动作,这一步就是底层的流读写操作。
断开连接
这一步需要简单理解为需要一方主动发起断开申请浏览器调用read收发数据同时会收到关闭请求,此时客户端确认请求之后将会停止请求并且开始释放Socket连接。
为什么不能用描述符标识应用程序的入口?
- 描述符是和委托创建套接字的 应用程序进行交互时使用的,并不是用来告诉网络连接的另一方。
- 客户端也无法知道服务器上的描述符,客户端也无法通过服务器端的描述符去确定位于服务器上的某 一个套接字。〉
Socket连接中大致介绍了协议栈是如何通过网卡完成和目标服务器的连接、断开、收发数据的过程下面按照顺序讲述各个步骤的细节。
下面我们根据上面所讲的各个步骤按顺序进行详细介绍。
创建套接字
首先来看一下创建套接字的情况,下面是协议栈的内容。
委托分发被拆分为好几个部分,最上面可以看作浏览器,协议栈中主要有两张协议 TCP和UDP, TCP主要是用于和服务器交互收发数据的,UDP则用于较短的控制数据。
IP协议主要控制网络收发操作,主要工作是把一个个拆分的网络包发给通信的目标对象,IP协议包括 ICMP和 ARP协议,前者告知传输过程的错误和控制信息,后者传递以太网MAC地址。
MAC 地址:符合 IEEE 规格的局域网设备都使用同一格式的地址,这种地址被称为 MAC 地址
驱动部分是为了让操作系统能正常使用硬件进行网络收发的一个“适配器”,而所有的电信号最终要通过网卡完成。
套接字和协议栈
协议栈实际上是根据套接字传递的信息来决定做什么操作的,比如发数据要看IP和端口号。
以Windows的套接字为例,直接在CMD
中使用 netstat
操作即可:
C:\Users\Xander>netstat -ano 协议 本地地址 外部地址 状态 PID TCP 0.0.0.0:49666 0.0.0.0:0 LISTENING 604 TCP 0.0.0.0:49667 0.0.0.0:0 LISTENING 1892 TCP 0.0.0.0:49668 0.0.0.0:0 LISTENING 4508 TCP 0.0.0.0:57621 0.0.0.0:0 LISTENING 22748 TCP 127.0.0.1:1001 0.0.0.0:0 LISTENING 4 TCP 127.0.0.1:1043 127.0.0.1:1061 ESTABLISHED 8452 TCP 127.0.0.1:1043 127.0.0.1:1063 ESTABLISHED 8452 UDP 192.168.159.1:1900 *:* 3060 UDP 192.168.159.1:5353 *:* 5248 UDP 192.168.159.1:58085 *:* 3060
netstat 命令 的 ano 三个参数主要用于扩展IP地址端口以及PID的显示,以及一些隐式的可能存在的通信也会被记录。
LISTENING:表示等待对方连接 ESTABLISHED :表示完成连接并且进行数据通信操作
套接字和协议栈和应用程序的交互流程如下:
- 协议栈在操作套接字之前,需要事先开辟一块空间来存放用于操作套接字的必要信息。
- 协议栈需要向应用程序返回描述符表示当前连接的是哪一个“管道”。
- 之后应用程序需要和协议栈交互就必须要携带描述符,不过这样也节省了协议栈了解应用程序要和哪一个套接字交互。
连接服务器
连接的目的是为了让两台不再同一个地方的主机能够相互认识对方,这时候不可避免的需要互相提供自己的信息,这样才能正确的建立连接然后使用套接字传输数据。
连接的含义
人和人之间的沟通有时候可以不使用一个语言,只要双方都听懂就行,但是对于计算机是行不通的。
所以连接操作的控制信息要根据通信规则确定,协议栈在通信之前需要依靠一块空间来存放必要数据,这块内存空间称为缓冲区。
连接需要双方各自告知自己的信息,所以连接最开始的时候是没有任何数据交互的,由于是TCP是全双工的协议客户端和服务器都需要建立套接字,不过双方不知道和谁连接,所以需要在客户端和服务端各自开辟一块空间来存放对方的IP和端口等必要的传输信息。
为了让双方既可以正常通信,又可以根据自己的系统设计协议栈和套接字的控制信息处理方式,网络通信设计采用了 控制信息的的方式让不同计算机和系统能相互认识。
所谓的控制信息可以认为是一种 通用语言,只要是符合这个控制信息规范的头部信息就可以被其他的计算机认识。
控制信息分为两类:
- 客户端和服务器的交换的控制信息,主要用于整个通信过程,这些内容在TCP协议进行规定。生活的例子理解是我们和别人通话之前,两边都得知道对方的电话号码和基本身份。
- 保存在套接字中用来控制协议栈操作的信息,这些信息主要用来传输数据,通常需要包括通控制信息和数据块,套接字需要通过控制信息了解到发来的是什么类型的数据,然后协议栈才能配合处理数据。
由于在一开始传输的时候是没有具体数据的,通常是一个空的报文头,所以这个控制信息也被叫做 协议头部, 比如下面提到的TCP头部,IP头部。
第一类:TCP 头部格式
第二类:套接字中的信息
连接的实际操作
连接的实际操作主要是调用CONNECT
函数,协议首先会传递给TCP模块,通过TCP模块交换获取控制信息的头部,以此了解具体要连接的套接字信息,然后把头部的SYN比特设置为1,表示可以连接。
TCP 模块处创建表示连接控制信息的头部,接着便把信息传递给IP模块进行委托发送。
三次握手
交换头部信息之后,接着便是常见的TCP三次握手的过程:
- 第一步:客户端主动打开TCB端口,服务器被动打开TCB端口。发起方携带一个SYN标志,并且携带一个ISN序号Seq=x,但是需要注意的是第一步的过程这个ISN序号是隐藏传递的(因为没有传递数据),因为如果请求不存在数据的交换则不会被显示。客户端发送SYN命令之后进入设置SYN=1,并且设置SYN-SENT(同步-已发送状态)。
- 第二步:服务器收到客户端TCP报文之后,也将SYN=1,并且回送一个新的ISN序号ack=x+1,并且将ACK=1表示自己收到了,然后在返回参数回送自己新的序列号表示自己的确认请求Seq=y,将状态设置为SYN-RCVD(同步收到)状态,(表示希望收到的序号为xxxx1522),最后也是指定MSS。
- 第三步:客户端收到服务器的确认报文之后,还需要向服务端返回确认报文,确认报文的ACK=1,并且回传服务器传递的ISN序号+1(ack = y+1),以及自己的ISN序号+1(Seq = x+1),此时TCP连接进入已连接状态,ACK是可以携带数据的,但是如果不携带数据则不消耗序列号。
- 最后一步:当服务器收到客户端的确认,也进入已连接状态。
经过三次握手连接建立,直到断开连接之前都可以传递数据。
收发数据
收发数据有两个重点:
- 第一点是收发数据并不关心数据的格式,而是根据头部信息来辨别是什么类型的数据,对于协议栈来说接收的的内容都是二进制的数据。
- 第二点是利用缓冲区减少频繁的数据传输提高传输效率。
缓冲区的大小如何控制?
- 每个数据包的数据长度,协议栈会根据一个叫作 MTU的参数来进行判断,但是MTU指的是总长度,除开头部信息之后获得真实的数据长度MSS。
- 时间,这个时间指的是固定的时间内容不管缓冲区有有没有达到MSS长度必须发送数据的时间,目的是防止等待时间过长造成请求延迟。
名词解释:
MTU:一个网络包的最大长度,以太网中一般为 1500 字节。 MSS:除去头部之后,一个网络包所能容纳的 TCP 数据的最大长度。
但这两个因素实际上并不能完全决定收发数据的效率平衡,TCP协议没有规定协议栈如何平衡,具体需要看操作系统如何决定。
实际上协议栈收发数据是有所保留的,并不是强制按照协议的规定处理,而是给了应用程序一些可控选项,比如浏览器这种要求实时性的应用程序通常不使用缓冲区。Http请求拆分
通常情况http的请求响应内容可以通过一个网络包完成,但是针对POST请求等大表单的数据提交则通常会触发TCP拆包操作。
拆包是根据MSS的参数确定的,发送缓冲区会根据这个参数把一个超过一次请求长度的数据拆分为多个包,但是因为实际上同属一份数据,拆分之后所有的数据包都需要添加相同的头部。
注意:TCP是面向字节流的协议,就是没有界限的一串数据,本没有“包”的概念,“粘包”和“拆包”一说是为了有助于形象地理解这两种现象。
TCP粘包
TCP除了拆包动作之外还包含粘包的操作,所谓粘包是指TCP协议中发送方发送的若干包数据到接收方接收时粘成一个包,从接收缓冲区角度来看后一个数据的头紧接着前一包数据的尾部。
解决粘包、拆包问题策略?
粘包和拆包需要解决容易造成半包读写的根本问题,解决办法也有很多种,主要的策略基本很多网上资料都有讲到,这里直接搬运结论了:
- 请求消息定长,如果缓冲区不满,则通过补0的方式达到长度,防止粘包和拆包。
- 在包尾增加回车换行符进行分割,例如FTP协议;
- 将消息分为头部和消息体,头部中保存整个消息的长度,只有读取到足够长度的消息之后才算是读到了一个完整的消息;
- 通过自定义协议进行粘包和拆包的处理。(几乎不用)
ACK号确认网络包收发
ACK号码除了在三次握手的过程中确认对方是否有收到请求之外,还能作为判断接收的数据包是否完整的依据,在进行数据传输的时候,接收方会将到目前为止接收到的数据长度加起来,计算出一共已经收到了多少个字节,然后将这个数值写入 TCP头部的 ACK 号中发送给发送方,以表示自己到底受到了多少数据,如果中间存在缺少数据则服务端重新传输即可。
当然仅靠ACK号不能完全作为参考依据,并且只使用ACK号是只考虑 单向传输的情况,但是TCP是全双工协议,无法确定数据接收方来自哪一方。
解决这个问题也很简单,实际在进行双向数据传输的时候双方各自会额外计算一个序号,序号其实就是一组随机数,在接收方收到数据之后每次都需要把序号+1回传给发送方表示自己接收到哪一个序号之前的所有数据。
通过ACK+序号的方式确保数据正确传输,这样可以使得其他网络通信组件不需要额外的失败补偿机制,如果发现丢包或者数据不完整的情况,直接根据序号进行重传重发的操作即可。
影响数据传输的因素
主要影响因素是返回ACK号的等待时间。
如果ACK号迟迟没有响应给对方服务器,势必会影响整个网络传输的效率,如果下一个数据已经准备好上一个返回包却没有发回去,很容易造成网络的堵塞,对方迟迟拿不到正确结果。
网络环境的复杂多变,这个等待时间不可能是固定的,所以TCP使用了动态时间的方法进行调整,具体的调整方法就是使用滑动窗口。
滑动窗口
滑动窗口:指的是在不等待ACK返回结果的情况下直接双方互相不间断的发送数据。
双方需要通过各自的缓冲区顺序返回ACK信息,但是如果无限制的发送数据会导致数据无法处理出现丢包,所以滑动窗口的关键是接收方需要告诉发送方自己最多能接收多少数据。
滑动窗口的细节通过一张图更好理解:
关于接收方的接收量,最大能承受处理多少数据是通过缓冲区大小确定的。另外需要注意下面的图只有单向的部分,实际上对于双向来说都是类似的处理。
影响数据传输的次要因素:返回 ACK号和更新窗口的时机。
关于这一点直接记住一个结论,接收方在发送 ACK 号和窗口更新时,并不会马上把包发送出去,而是会等待一段时间,等到其他的通知合并到一起处理,因为ACK号体现的是已经收到的包的数据量,使用这样延迟发送的方式也可以防止过多的更新数据包出现。
最终协议栈收发数据的细节如下:
- 协议栈会根据收到的数据块和TCP或者IP头部解析内容,如果确认收到数据则返回ACK + 序号。
- 协议栈会把数据块放到缓冲区进行存储,利用滑动窗口的特性按照顺序处理数据交给应用程序处理。
- 协议栈会将接收到的数据复制到应用程序指定的内存地址中,然后将控制流程交回应用程序
断开连接
断开连接的部分包含断开连接和删除套接字的操作,断开连接也就是经典的四次挥手的操作,而删除套接字则需要注意在协议栈中并没有规定关闭的时间,但是通常情况下过几分钟之后会删除套接字。
四次挥手端口tcp连接
- 第一步(客户端):TCP发送释放连接的报文,停止发送数据,释放报文首部,把FIN=1,同时发送序列号,根据上一次传送的序列号+1传送Seq = t + 1(由于下图是在连接之后立马进行四次挥手,所以序列号没有变),此时客户端进行终止等待1的状态。注意FIN不携带数据也需要消耗序列号。
- 第二步(服务端):服务器回送确认报文,发出确认报文,ACK=1,并且把回传序列号+1回传(ack = t + 1),然后再带上自己的序列号Seq = y,此时服务端进入CLOSE-WAIT状态(关闭等待状态),TCP服务器此时需要停止上层应用客户端向服务端请求释放,处于 半关闭 阶段,此时服务端依然可以向客户端发数据并且客户端需要接收并处理,关闭等待状态意味着整个状态还需要持续一段时间。
- 第三步(客户端):客户端接收到服务端确认请求,此时客户端进入到FIN-WAIT-2终止等待2的阶段,等待服务器的释放报文。(还有一部分服务器没有发送完的数据需要处理)
- 第四步(服务端):服务器把最后的数据处理完毕,向客户端发送释放报文,FIN=1,ack=t + 1,由于需要把剩下的数据发送完成,假设处理完成之后需要带上自己的序列号Seq=w,服务器进入最后确认状态,等待客户端确认。
- 第五步(客户端):客户端收到报文之后,发出确认 ACK=1,ack=w+1,自己的序列号为Seq = t + 1,此时客户端进入到了TIME-WAIT(时间等待状态),此时客户端还是没有释放,必须经过**2 * MSL(最长报文寿命)**之后,客户端撤掉TCB之后才进入CLOSED状态。
- 第六步(服务端):服务器收到客户端的请求立马进入CLOSE状态,同时撤销TCB,结束此次TCP的连接。(服务端结束TCP连接要比客户端早一些)
套接字和协议栈和对方服务器的交互流程细节还是比较多的,这里可以发现实际上三次握手和四次挥手实际上只是网络连接当中很小的一部分,最后是从连接服务到数据收发到断开连接的一张简单总结图,建议当作一个大概的流程参考: