在开始之前,我们需要了解一些基础信息
1. 协议栈
以下是操作系统中的网络控制软件,也就是我们说的协议栈的内部示意图。这张图只是显示总体规则,并不涉及细节,仅供参考。
我们从上往下看,最上层的网络应用程序就是我们日常使用的比如浏览器,电子邮件等程序。这些应用程序会将收发数据的操作交给下层处理。
应用程序下层就是socket库,这是一种链接库。在linux下,socket库有动态库也有静态库。库中包含之前提到过的解析器(用于向DNS服务器发出查询)。
然后就到操作系统内部了,这里也是协议栈的位置。其中TCP协议程序部分和UDP协议程序部分都依赖于下层的IP协议程序控制网络包收发操作。IP协议程序就是负责将网络包发送给通信对象。IP协议程序部分还包括ICMP协议程序和ARP协议程序。ICMP用于告知网络包传送过程中产生的错误信息以及各种控制信息。ARP用于根据IP地址查询相应的MAC地址。
2. 套接字
那么套接字究竟是什么?先看百科给出的定义
所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口
实际上,在协议栈内部有一块用于存放具体控制信息的内存空间,这里记录了用于控制网络通信操作的内部信息。例如通信对象的IP地址,端口号等等。这块存放着控制信息的内存空间就是套接字的实体。
协议栈在执行操作的时候就需要参阅套接字里的控制信息。例如协议栈要发送数据时,就需要看一眼套接字里写的通信对象IP和端口号是多少,才能够成功将信息发送出去。
想看看套接字长什么样?直接在控制行输入netstat就可以查看:
协议栈中可以存在多个套接字,他们拥有自己的独一无二的套接字描述符,协议栈就通过描述符区分不同的套接字。
3. 浏览器使用TCP进行消息收发时的具体过程
如图,调用解析器解析DNS的过程我们在第一章已经讲过了,我们直接来看浏览器在解析完成得知服务器的IP地址后所做的操作。
第一步:创建套接字
首先,浏览器调用Socket库申请创建套接字,协议栈根据申请执行创建套接字的操作,其会分配一个内存空间给所创建的套接字,然后将套接字的描述符告诉给浏览器,浏览器收到描述符后,就算创建完成了。
在一次连接中,浏览器要委托协议栈进行收发信息的时候就只要提供这个套接字描述符,协议栈就知道要参阅哪个套接字内的信息,向哪里发送信息了。
第二步:建立连接(TCP三次握手)
到目前为止,浏览器才刚刚委托协议栈创建了一个套接字,里边还没有内容,所以浏览器会首先将服务器的IP地址和端口号等告知协议栈,让协议栈往里边填写。但是套接字里远远不止存储目标地址和IP地址和端口号的信息,还有一些别的信息比如规定的序列号等信息必须从服务器那边获得。
于此同时,服务器那边的套接字还是空白的(服务器在启动的时候就已经创建了套接字等待连接了),里边根本没有任何关于目前浏览器的必要信息。这很正常,毕竟这俩还没有正式交流过。
所以在正式传输数据之前,客户端和服务器的套接字必须先拥有对方的信息,不然一会传数据根本不知道传给谁,怎么传。这就是连接的意义:通信双方互相交换控制信息。
连接的实际过程如下所示,同时这也是TCP三次握手的过程,建议配合着TCP三次握手一起看
连接后的样子
第三步:开始数据传输
建立好连接后,就可以进行收发数据了。应用程序会调用write指令将要发送的数据交给协议栈,在这里是浏览器将http请求报文交给协议栈进行发送。
协议栈才不关心你发的是什么,在协议栈的眼里这就是一堆二进制数据而已。为了控制传输效率,一般协议栈会将这些数据放在内部的发送缓冲区中,然后按照自己的节奏发送数据(当然你也可以让协议栈马上发送某数据)。对于超出包最大大小的数据,在协议栈传输的时候也会进行拆分成合适的长度发送。
在发送的时候,协议栈中的TCP模块会将数据加上TCP头部,并根据套接字中记录的控制信息标记发送方和接收方的端口号。IP模块也会在前边加上IP头部和MAC头部。如图
现在来看看TCP头部的样子
我们知道TCP协议是可靠传输协议,这究竟是什么意思呢?实际上TCP报文头的ACK和序列号SYN就是完成这个的关键。ACK则用来表示当前收到的字节是所接收的字节中的第几个。SYN表示当前网络包是从头开始的第几个字节。
传输的时候就依靠这俩东西来确认数据有没有被正确得接收到。在得到对方确认之前,发送过得包都还会保存在发送缓冲区中,如果对方没有返回网络包对应的ACK号,那么就重新发送这些包。
注意,为了防止网络攻击,SYN往往不是从1开始的,而是由发送方随机指定一个数据作为SYN的初始值,并且在连接阶段告知接收方。由于TCP中数据收发是双向的,所以客户端和服务端都需要指定一个初始值并且在连接阶段告知对方。
除此之外,在数据传输过程中会使用滑动窗口的方式,即不等待一个个ACK数据包响应确认,就继续发送网络包。这样可以提高传输效率。
第四步:接收HTTP响应报文
浏览器发出数据了,自然也要等待服务器发送回来的数据。这个阶段浏览器会调用read,然后控制流程就转移到了协议栈内部,等到协议栈收到了来自服务器的响应数据,就会做如下操作。
首先协议栈会检查收到的数据包的TCP头部内容,通过SYN判断是否有数据丢失,如果没有就返回对应的ACK号,然后将包内的数据按顺序连接起来,还原成原始数据,复制到应用程序指定的内存地址中,最后将流程交换给应用程序让其继续执行。
第五步:断开连接(TCP四次挥手)
所有的数据收发结束,那么就是时候说再见了。服务端和客户端任意一方都可以发起断开连接,协议栈的设计也允许应用程序发起断开连接。
以下是断开连接的具体过程:
第六步:删除套接字
通信结束后,客户端那边的套接字就不再使用,需要删除。但套接字不会被立即删除,而是等待几分钟后删除。这是为了防止一些意外情况出现。比如客户端最后返回的ACK号码中途丢失了,那么服务端就会重发一次FIN,此时若套接字已经删除,就会出现误操作,使得连接没有真正断开。