《网络是怎样连接的》笔记
date
Nov 9, 2022
slug
how-to-connect
status
Published
tags
计算机网络
summary
输入 URL 后到底发生了什么?
type
Post
简介大体流程浏览器 —— 输入URL,构造HTTP数据包客户端查询DNS服务器 —— 获取目标IP地址TCP/IP协议栈与socket —— 概念TCP/IP协议栈的工作流程构造数据包传输数据web服务器处理TCP/IP协议栈的具体细节TCP的控制字段三次握手信号传输消息的确认消息重传和滑动窗口拥塞控制四次挥手从socket的角度看TCP/IP的工作客户端的socket服务端的socket三次握手中的socket总结参考
简介
快速翻阅了一下这本书,内容主要是回答“浏览器输入URL到接收数据到底发生了什么”这个经典问题。
大体流程
- 浏览器输入URL,查询DNS服务器(基于UDP)获得目标IP地址,构造HTTP请求报文,将HTTP请求报文交给TCP/IP协议栈
DNS服务器,HTTP控制协议
- TCP模块收到HTTP报文后,按照TCP协议封装数据包,然后创建socket进行链接
socket通信,TCP协议(三次握手、四次挥手、可靠传输)
- TCP模块将TCP数据包交给IP模块,IP模块继续封装IP报文,然后通过网卡传输到路由器,由路由器进行转发
IP协议
- 路由器接力转发,直到进入服务器端的网络中(入口路由器)
- 防火墙过滤数据包,经过缓存服务器
这里缓存服务器的操作和HTTP字段有关
- 数据包进到服务器(实际上是通过socket拿到数据),先是暂存在网卡缓存里面,等到CPU进行读取
- CPU读取后,交给IP模块处理数据,此时根据协议号来决定交给UDP还是TCP(假设是TCP)
- TCP收到数据报文后,根据首部字段决定操作
如果SYN=1,则是建立连接的请求,进行三次握手 如果FIN=1,则是断开连接的请求,进行四次挥手 剩下的情况是数据传输过程的数据包,接收并处理
- TCP会将处理好的数据包交给HTTP模块,HTTP处理数据包,然后交给服务器程序进行处理
- 服务器处理完毕之后,将内容封装成HTTP数据包,然后用同样的方式返回给客户端
下面还是从浏览器输入URL开始,到接收数据结束,详细讲解一下每一步发生了什么。
浏览器 —— 输入URL,构造HTTP数据包
当我们输入URL之后,实际上构造的是HTTP数据包
URL是统一资源定位符的一种,它用来表示目标服务器上的一个资源,这个资源可能是网页,可能是图片、视频。
例如我们输入一个网址,实际上对应的是目标服务器中的file1.html这个网页
确定了URL之后,我们会根据用户发起的请求构造HTTP数据包。
HTTP数据包总的来说就是两部分:
- 控制信息 —— 比如头部字段、请求方法、状态码等这些描述数据包信息的字段
- 数据 —— 需要传输的数据
而根据包的传输方向,可以分为:
- 请求消息
- 响应消息
至此HTTP数据包构造完成,我们需要向目标URL的web服务器发送数据、
发送数据靠的是IP地址而不是URL,因此我们首先需要获取目标服务器的IP地址
客户端查询DNS服务器 —— 获取目标IP地址
当用户在浏览器输入网址时,浏览器首先会去DNS服务器查询域名对应的IP地址
DNS服务器就像在远端维护的一个hashmap,key是URL,value是IP地址
这一步可能涉及到多个DNS的接力查询。
(所谓DNS污染就是在这一步破坏查询结果,如果我们拿到一个不正确的IP地址,自然没有办法进行通信了)
这一步是通过UDP协议查询得到结果,当我们查询到IP地址的时候,我们就可以准备发送数据了,接下来我们需要委托协议栈帮助我们发送数据
TCP/IP协议栈与socket —— 概念
这里协议栈指的是TCP、UDP、IP等这些网络通信协议组成的模块。
协议栈定义了一系列的通信规则,比如TCP的超时重传,建立连接、断开链接等规定,IP的校验码等。
这些通信规则相当于约定,当我们双方都遵守约定的时,就能达到某种通信效果。
比如TCP可以在不可靠的网络环境下建立起可靠的通信信道。
表面上我们使用协议栈就进行通信,但是实际上我们还需要socket(套接字)的帮助。
形象一点的比喻是:TCP/IP协议是电,socket是插座。当一个管道两端都插上插座,电才能在管道中传输。
这个比喻背后蕴含的意思是:TCP/IP等协议是对数据内容的抽象,规定数据内容供双方通信;而socket是通信实际实现的工具,没有socket我们无法传播信号。
明白了协议栈和socket的概念后,我们来看看它们是怎么运行的。
TCP/IP协议栈的工作流程
TCP/IP协议栈的工作是复杂的,下面我从一个数据包的流动来描述整个过程,中途涉及到的知识点后面再具体展开。
构造数据包
首先我们介绍数据包如何构造:
当我们把HTTP数据包构造完成后,首先交给TCP模块。
TCP模块会根据实际情况将数据进行拆分,然后写入自己的头部控制信息,这一步是在构造TCP数据包,当TCP数据包构造完成后,会交给IP模块处理。
IP模块收到TCP数据包后,会添加IP协议的头部控制信息。
这里涉及到两个长度:
- MTU:IP数据包的极限大小,一般是1500字节
- MSS:IP数据包最大的数据量,一般是1460字节,40字节是IP头部信息的占用大小。
直到这里我们传输用的数据包就构造完成了。
传输数据
我们将数字信号的数据包交给网卡,然后网卡会将数据发送给路由器(中途可能有集线器和交换机,还有传输依赖的网线,这里忽略掉它们的工作),路由器多次转发后,可以找到服务器所在的网络,然后根据IP地址转发至web服务器
web服务器处理
这里web服务器整体收到数据包的过程是这样的:
- 防火墙过滤
- 如果有缓存服务器,经过缓存服务器
- web服务器网卡收到数据包,检查IP数据包是否出错,没有出错的话通知CPU读取
- CPU读取后,根据IP数据包的协议号交给TCP或者UDP模块处理,这里HTTP依赖TCP,所以交给TCP梳理
- TCP模块收到数据包后会检查头部字段是否正常,如果正常,交给web服务器软件进行处理。常见的web服务器软件有tomcat、jetty等。
- web服务器软件读取到头部信息和数据,针对要求执行具体操作,比如说HTTP GET操作或者HTTP POST操作,这一部分的处理方法是我们写的业务代码。
当我们处理完毕后,会按照反过来的顺序进行传输,最终就能传输到客户端的浏览器里面,然后浏览器会根据数据进行相应处理。
TCP/IP协议栈的具体细节
上面对TCP/IP协议栈的工作流程做了简要的叙述,这里我们拆开来仔细看每一步到底是怎么工作的。
我们都知道,TCP协议建立的传输有三个结果:
- 三次握手
- 信号传输
- 这里涉及到拥塞控制,可靠传输等知识
- 四次挥手
下面我们详细描述一下流程
TCP的控制字段
之前提到协议栈里面的数据包,其实都可以笼统的看为两部分的内容:
- 控制信息
- 数据
控制信息是对要传输的数据进行描述,TCP的控制信息如下图:
我们需要注意的地方是:
- 发送方端口号,接收方端口号
- 序号,ACK号
- 控制位
- 其中ACK、SYN、FIN是常见的控制位
- 窗口
三次握手
三次握手
三次握手大家都很熟悉了
- 客户端发送SYN = 1的TCP数据包,请求建立链接
- 服务器收到后,返回SYN=1,ACK=1的数据包,表示同意建立链接
- 客户端收到后,返回ACK=1的数据包,表示收到同意
经过三个步骤,一个可靠的传输信道就建立起来了,接下来就可以进行数据传输了。
然而这里面有几个问题
TCB是什么?为什么是三次握手?SYN-SENT和SYN-RCVD是什么?
TCB是什么?
TCB(Transmission Control Block,传输控制块)是TCP用来传输数据的数据结构,它的组成是两个部分:
- socket,读写数据,传输
- 缓冲区,存放数据
我们所谓的通信,实际上是TCB之间的通信。
为什么要三次握手?
这部分可以参考下面的文章,写得很好,这里不赘述了。简单来说有以下三个特点:
- 三次握手才可以阻止重复历史连接的初始化(主要原因)
- 三次握手才可以同步双方的初始序列号
- 三次握手才可以避免资源浪费
信号传输
这里有三大部分:
- 消息的确认
- 消息重传和滑动窗口
- 拥塞控制
消息的确认
我们都说TCP是可靠的连接,那么TCP到底如何实现可靠连接?
实际上这和序列号和TCP控制字段里面的ACK有关。
上面这张图描述了ACK和序列号到底如何工作
- 客户端 => 服务端
- 当我们传输一个序号为1,长度1460的数据包时。服务端如果收到,会返回一个ACK=1461的数据包给客户端,表示1461字节之前的数据都已经收到了
- 客户端 => 服务端
- 客户端收到ACK=1461的数据包后,继续传输接下来的字节
...
如此,我们就能通过ACK和序列号来进行消息传输的确认。
这里的序列号是从1开始, 这种实际上是不安全的,因为外界可以根据序列号和ACK号分析客户端和服务器的通信情况
所以我们回过头来看这幅图,注意里面的seq字段
这个字段是我们双方约定的随机开始的序列号,x是客户端约定的起点,y是服务端约定的起点,而ack是对起点的确认
这里我们就能避免上述不安全的情况。
消息重传和滑动窗口
上面提到的传输是理想的情况,实际传输的时候可能会发生丢失,这个时候就需要重传机制了。
实际上传输并不是一来一回,而是通过双方维护的一个滑动窗口进行传输
对于客户端:发送滑动窗口内的数据,当收到ACK=x的时候,滑动窗口的左端移动到x+1的位置
对于服务器:接收滑动窗口内的数据,当x之前的数据都已经收到的时候,返回ACK=x的数据包
注意我们服务器是收到x之前的所有数据才会返回ACK=x,这样能确保数据的连续性。
那么我们ACK丢失的情况呢?TCP应该怎么处理?
实际上这涉及到重传机制了,重传分为四种
- 超时重传
- 快速重传
什么是超时重传?
ACK数据包丢失了,此时客户端并不知道对方是否收到数据,所以客户端会等待一段时间,超过了这个时间后会重新发送数据包
那么等待多久比较合理呢?
这里涉及到两个概念:
RTO
(Retransmission Timeout 超时重传时间)RTT
就是数据从网络一端传送到另一端所需的时间,也就是包的往返时间。RTO的值会略大于RTT,而它们具体的值会由网络环境决定
什么是快速重传?
存在一种情况,ACK数据包没有丢失,但是服务器反复发送
当客户端收到3次相同ACK=x的时候,立即重传ACK=x+1的数据包
拥塞控制
拥塞控制是指我们应当有节制的发送数据包到网络环境中,因为网络环境是公用的,一个地方阻塞必然造成整个网络的阻塞,TCP针对这种情况设计了阻塞机制。
这里涉及到两个重要参数,一个是cwnd即滑动窗口大小,另一个是ssthresh窗口的临界值,分别有四个要点:
- 慢启动
- cwnd = 1,指数速度增长
- 拥塞避免
- cnwd >= ssthresh时,cwnd += 1
- 快重传
- 当客户端收到3次相同ACK=x的时候,立即重传ACK=x+1的数据包
- 快恢复
- 发送拥塞时,ssthresh /=2 ,cwnd /= ssthresh,然后执行拥塞避免算法
四次挥手
当我们数据传输完毕后,TCP将会断开连接。
这里断开链接的过程是四次挥手,过程以上图为例:
- 客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。
- 服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
- 客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
- 服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
- 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
- 服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。
MSL是Maximum Segment Lifetime,一个数据分片(报文)在网络中能够生存的最长时间,一个MSL大概2分钟的时间。
为什么客户端还需要等待2MSL的时间呢?
第一,保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失,站在服务器的角度看来,我已经发送了FIN+ACK报文请求断开了,客户端还没有给我回应,应该是我发送的请求断开报文它没有收到,于是服务器又会重新发送一次,而客户端就能在这个2MSL时间段内收到这个重传的报文,接着给出回应报文,并且会重启2MSL计时器。
第二,防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中。客户端发送完最后一个确认报文后,在这个2MSL时间中,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样新的连接中不会出现旧连接的请求报文。
从socket的角度看TCP/IP的工作
终于到了最后的部分,我们TCP/IP协议的工作是离不开socket的,之前提到的半连接、全连接也是和socket有关的状态。
回过头来看这张图,当我们申请进行TCP通信的时候,实际上背后的工作者是socket
我们一个tcp连接标识为
<clientIP, clientPort, serverIP, serverPort>
其中socket就是用来监听端口号和通信的。
客户端的socket
首先客户端通过
socket
函数创建一个socket,这个socket会随机分配一个端口号然后使用
connect
去连接web服务器,这里需要的参数是<serverIp, serverPort>,默认的HTTP端口号是80,而IP是我们从DNS服务器里面获取的如果建立完毕,那么就可以开始传输了
使用
write
写入数据,使用read
读取数据最后关闭连接时使用
close
断开服务端的socket
服务端的socket稍微有点不同
首先socket分为监听socket和连接socket,我们大家都知道socket是用来监听端口的。
比如说我们创建一个web服务,监听8080端口,那么此时web服务器是创建一个监听socket去监听8080端口
然后当请求进来的时候,监听socket收到请求申请,那么就会通知CPU,CPU就会分配新的连接socket和缓存(加一起就是TCB)去监听8080端口,真正实际通信的是该连接socket。
这样一个端口是可以被多个socket监听的,它们辨识的方法就是客户端的IP地址和端口号,用这两个信息来区分一个端口号到底是哪个socket来执行通信。
明白了上述概念,我们再来看服务器端的socket如何操作。
- 首先通过
socket
来创建一个socket
- 通过
bind
来绑定指定端口
- 使用
listent
来将socket转换为监听socket
- 调用
accpect
等待客户端发起连接请求,此时获得的描述符2是连接socket,我们交给通信模块处理
- 使用
write
和read
来通信
- 使用
close
来结束通信
三次握手中的socket
以下内容参考:
介绍了socket的基本使用后,我们来看三次握手中的socket和资源分配吧
半连接队列与全连接队列
- 首先服务端进行
bind
和listen
调用,创建监听socket
- 然后客户端创建socket后,执行
connect
建立连接,发送SYN=1的数据包
- 服务器收到SYN=1的数据包后,会将该连接加入到半连接队列(SYN队列),然会返回SYN+ACK给客户端
- 当服务器再次收到ACK的时候,会把连接从半连接队列里面取出来,加入到全连接队列
- 然后服务器调用
accept
可以从全连接队列中取出连接进行处理
总结
本文简单的回答了浏览器输入URL到返回数据到底发生了什么这个问题.
中间涉及到TCP/IP的通信过程和socket等内容。