网络是怎样连接的--客户端

1,017 阅读46分钟

指南

文章是对整个网络交互的总结(客户端部分),本篇会以客户端为侧重点,比较详细的讲解客户端发起网络请求到服务端返回数据这一个交互过程中客户端主要做了哪些工作以及经历了什么。文内理论知识偏多,不会讲得特别深入,但是看完有一个全面了解是足够的。

在此之前我先提几个会在文章内有解释的问题,可以先思考一下,题目比较基础,懂的童鞋可以在文内看看写的是否正确,不是太懂的童鞋可以通过阅读本文掌握他们,我也是抱着加深自己的记忆来写这篇文章的:

  • 1、如何正确分析一个网址URL,如:https://github.com/ChavezChen/CWLateralSlide 和 https://www.baidu.com
  • 2、什么是DNS服务器,以及它是如何工作的?
  • 3、IP地址知道是用来干嘛的,那端口号呢?子网掩码又是个什么东东?
  • 4、套接字、Socket与socket是什么东西?这三者之间有什么关系?
  • 5、连接服务器的连接是什么意思?TCP三次握手中常说的SYN,ACK是啥?
  • 6、MAC地址是用来干嘛的?
  • 7、协议栈的工作内容是什么?

可以先尝试作答一下。当然文内的知识一定不止于回答这几个问题而已。

网络的全貌

首先我们以web浏览器为例先来大概了解一下网络的全貌(当然移动应用也是类似的):

  • 1、用户在浏览器输入一个网址如:www.baidu.com,浏览器会按照一定的规则去分析这个网址,然后生成请求消息,浏览器会委托搬运数字信息的机制将数据发出去。
  • 2、搬运数字信息机制最先出场的是协议栈(网络控制软件),协议栈会将从浏览器接收到的消息打包,加上目的地址等控制信息,然后将包交给网卡(负责以太网或无线网络通信的硬件)。网卡会将包转换为电信号,并通过网线或者无线网信号发送到网络当中。
  • 3、以最典型的场景为例,网卡发送的包会经过集线器等设备,到达用来接入互联网的路由器,路由器后面就是互联网了。
  • 4、通过路由器进入互联网内部,互联网的入口线路称为接入网,比如电话线、光线、专线等多种通信线路统称为接入网,接入网连接到签约的网络运营商并接入,然后进入互联网的骨干部分,我们称之为骨干网,骨干网有很多运营商和大量路由器,这些路由器相互连接,组成一张巨大的网,而我们的网络包经过若干个路由器的接力,最终发送到服务器所在的局域网中。
  • 5、到了服务器所在的局域网,这个数据包会遇到防火墙,防火墙会对进入的包进行检查,接下来网络包可能还会遇到缓存服务器,如果在缓存内找到需要的数据就直接返回了,没找到再经过一些机制比如分布在多台服务器上的负载均衡器等,之后网络包就到达了服务器。
  • 6、服务器会将网络包解包还原为原始的请求消息,然后交给服务器程序分析请求消息的含义,并按照其中的指示将数据装入响应消息,然后打包发送回客户端,响应消息回到客户端的过程刚好和前面介绍的相反,客户端拿到数据后会将数据展示到屏幕上。

到这里,一次网络的交互也到达了终点。上面有很多词不理解没关系,接下来文内都会有讲解,然后我们就开始尽量往详细的讲每个环节。

每个环节的详细介绍

1、生成HTTP请求消息

1.1 第一步工作就是对网址进行解析

于是我们先来解释网址到底是什么?网址,准确的来说应该叫URL(Uniform Resource Locator,统一资源定位符)或者也可以理解为http开头的那个东西,下图上一张完整的URL图:

  • 协议:客户端是具备多种功能的,因此需要一些东西来判断应该使用哪种功能来访问对应的数据,比如访问访问http服务器用http协议,而访问FTP服务器时用ftp协议等等。可以称它为访问时的协议类型,但是访问本地的时候用file是不需要使用网络的,因此也可以理解为访问方法。
  • 用户名密码:这些我们一般省略或者不写在这,所以先不用管。
  • 服务器域名:我们可以理解为服务器的名称
  • 端口号:会在后面详细讲解
  • 文件路径:要访问的文件的路径

所以这个URL可以理解为:user使用https协议访问www.baidu.com这个服务器上/dir/目录下file.html这个文件

当然我们上面说的是一个非常典型的URL,当然也有一些不一样的,比如文章开头的第一个问题,我们在此多加两个类型进行分析:

https://github.com/ChavezChen/ 
https://github.com/ 
https://github.com
https://github.com/ChavezChen/CWLateralSlide

首先我们在前面已经知道了域名之后的“/”后面这部分是代表要访问的文件路径

  • 第一种可以理解为/ChavezChen/后面本来应该有的文件名被省略了,不过,没有文件名服务器怎么知道要访问哪个文件呢?其实服务器上会实现设置好文件名被省略时要访问的默认文件,这个看服务器怎么设置了,大多数情况下是index.html或者default.html之类的文件名。
  • 第二种以/结尾,也就是说要访问目录层级中最顶层的根目录,由于省略了文件名,所以结果就是访问/index.html这样的文件了
  • 第三种连/都省略的这种写法也是非常见到的,它其实和第二种一样,同样是访问根目录下面的默认文件
  • 第四种比较诡异,由于末尾没有/,所以CWLateralSlide应该理解为文件名才对,但实际上我们不应该总是将CWLateralSlide按照文件名来处理,一般来说会按照以下惯例进行处理:如果存在CWLateralSlide文件,则直接作为文件名来处理,如果存在名为CWLateralSlide目录,则将CWLateralSlide作为目录处理。

1.2 使用HTTP协议访问服务器

在此之前,我们先来讲一讲HTTP协议到底是怎么回事:首先,客户端会向服务器发送请求消息,请求消息中包含了“对什么”和“进行怎样的操作”两部分,其中相当于“对什么”的部分称为URI(Uniform Resource Identifer,统一资源标志符),“进行怎样的操作”的部分称为方法,方法表示需要让服务器完成怎样的工作,也就是我们常见的“GET、POST、PUT、DELETE”等......我们拿几个常见的来举例说明一下:

  • GET:获取URI指定的信息,如果URI指定的是文件,返回文件。如果URI指定的是程序,返回程序的输出数据。比如我们在请求消息中写上GET方法,然后在URI中写上存放网页数据的文件名“/dirl/file.html”,就表示我们要获取“file.html”文件中的数据,服务器会读取该文件中的数据放到响应中返回。
  • POST:客户端向服务器提交数据。使用POST方法时,URI会指向服务器中运行的一个应用程序的文件名,如:“index.cgi”等。然后服务器会将请求消息中的数据发给指定的程序,最后服务器从指定程序中接收输出结果返回到客户端。

其他的一些方法我们就不一一举例了,面试时候GET和POST的区别在网上有非常多的答案,想了解的可以自行搜索以下。

对URL进行解析之后,会根据解析出来的服务器和文件名信息生成HTTP请求消息,HTTP请求消息在格式上是有严格规定的,所以客户端都会按照规定的格式来生成请求消息:

1、<方法><空格><URI><空格><HTTP版本>   
2、<头字段名>:<头字段值>   头字段名如:Content-Type,表示消息体的数据类型.等
3、...
4、...
5、<空行>   
6、<消息体>

第1行为请求行,通过这一行大致了解请求内容。
2-5行成为消息头,每行包含一个头字段,用于表示请求消息的附加信息。消息头的行数一直
延伸到空行为止,比如iOS中可以通过AFN的requestSerializer来设置头信息
第6行消息体,包含客户端向服务器发送的数据。

发送请求后会收到响应,这个后面在说,简单解释一下响应的各个状态码的含义:

1xx         告知请求的处理进度情况。临时。
2xx         成功
3xx         表示需要进一步操作,重定向
4xx         客户端/请求错误
5xx         服务器错误

1.3 向DNS服务器查询web服务器的IP地址

在消息发送之前,我们还有一个工作需要完成,就是查询网址中服务器域名对应的IP地址,在委托操作系统发送消息时,必须提供的是IP地址而不是域名。

有童鞋肯定会有疑问,既然还要去查询ip地址那我干脆在网址中不写域名写IP地址不就好了?实际上这样也能正常工作,但是就像你容易记名字难记电话号码一样,要记住一串IP地址也是非常困难的,因此相对IP地址来说,网址中使用域名更好。

那有有童鞋要发问了,那干脆不用IP地址直接用域名来确认不就好了么?IP地址的长度是32bit,也就是4字节,而域名最短的也要几十个字节,换句话说,使用IP地址只要处理4字节的数字,而域名则要处理几十到255个字符,这增加了路由器的负担,传送数据也会花费更长时间。

1.3.1 IP地址的基本知识

在网络中,所有的设备都会分配一个地址,这个地址相当于现实中的“xx号xx室”,其中“号”是分配给整个子网的,称为网络号,“室”是分配给子网中的计算机的,称为主机号,这个地址整体称为IP地址。什么是子网呢?子网可以理解为用集线器连接起来的几台计算机,比如,一个房间有几台计算器,他们都连接到一个集线器上,那么他们就形成一个子网,然后多个子网在通过路由器连接起来就形成一个网络。然后你又想到上现实中,啥?没见过集线器这种东西,家里都是多台电脑用网线连到一个路由器上的啊!因为现在很多家用路由器中已经内置了集线器功能,所以你也可以把一个路由器连接的多台电脑,统称为一个子网。

通过IP地址我们可以判断访问对象服务器的位置,从而将消息发送到服务器。消息传送的具体过程后面再讲,不过我们先简单的了解一下:发送者发出的消息首先经过子网的集线器转发到发送者最近的路由器(家用的因为集线器和路由器二合一了,可以理解为从网线到了路由器上面),接下来路由器会根据消息的目的地判断下一个路由器的位置,将消息发到下一个路由器,不断重复之后消息就到了服务器最近的路由器,最终消息就被传送到了目的地。整个过程与现实中快递类似,经过一个一个中转站,到达离你最近的中转站,然后送到你房间。

然后我们来看一看实际的IP地址,实际的IP地址(IPV4)是一串32比特的数字,按照8比特(一个字节)为一组分成4组,分别用十进制表示,然后再用圆点隔开。但是我们要明确知道目的地,必须先知道网络号,确定哪一个子网,然后再通过主机号确定哪一台计算机,比如一个网吧有100台计算机,首先我们得通过网络号确定到底是哪个网吧,然后再通过主机号确定是哪台机器,但是IP地址有4段,到底哪几段代表网络号,哪几段代表主机号呢

于是我们需要另外一些附加信息来确认IP地址的哪个部分是网络号,哪个部分是主机号,这个附加信息就是 子网掩码 子网掩码的格式和IP一样,其中每段全部为1的为网络号,为0的表示主机号,子网掩码的每一段要不全部为1要不全部为0,也就是十进制的255和0,用十进制的来表示子网掩码和IP地址的关系:

IP地址  :      10 . 1.  2.3            10 .  1.  2.3
子网掩码:      255.255.255.0            255.255.  0.0
第一个IP地址的网络号为10.1.2 主机号为3
第二个IP地址的网络号为12.1 主机号为2.3    
总结:子网掩码的255对应的IP地址部分就是网络号,0对应的就是主机号
当然有两种特殊情况:IP地址的主机号为255表示向子网上的所有设备发送包,即“广播”。
IP地址的主机号为0代表整个子网,其他的情况一概视为某个子网的某一台主机。

1.3.2 通过解析器向DNS发出查询

查询IP地址的方法非常简单,只要询问最近的DNS服务器“www.baidu.com”的IP地址是什么就可以了,DNS服务器会回答说“该域名的IP地址为xxx.xxx.xxx.xxx”。-----DNS:Domain Name System,域名服务系统

那客户端是通过什么向DNS查询IP的呢?我们将负责域名解析这一操作的叫做解析器,解析器实际上是一段程序,它包含在操作系统的Socket库中。

我们先来简单了解以下Socket库。首先,库就是一堆通用程序组件的集合,iOS中的UIKIT也是一个库。Socket库也是一种库,其中包含的程序可以让其他的应用程序调用操作系统的网络功能(还包含很多发送和接收数据的程序组件)。就像iOS使用AFN框架就可以发起网络请求类似的。

解析器的用法非常简单,只要写上解析器的程序名称(OC中称为一个方法或者一个函数)“gethostbyname”然后参数带上对应的域名就可以了,比如:

IP = gethostbyname("www.baidu.com"); 

顺带一提,向DNS服务器发送查询消息是,我们当然也需要知道DNS服务器的IP地址。这个地址是我们事先就设置好的,windows电脑在IPV4\IPV6的属性里面设置,mac电脑在网络的高级里面设置。如下图:

1.3.3 DNS服务器的工作原理

解析器向DNS服务器发送的查询消息包含以下3种信息:

  • 域名:web服务器或邮件服务器的名称
  • Class: 用来识别网络的,不过如今除了互联网没有其他网络了。因此这个值永远是代表互联网的IN
  • 记录类型:表示域名对应何种类型。例如当类型为A时,表示域名对应的是IP地址;当类型为MX时,表示域名对应的是邮件服务器。

DNS服务器上事先就保存了前面这三种信息对应的记录数据,当收到客户端的查询消息时,DNS服务器会从已有的记录种查找域名、Class、类型全部匹配的记录并向客户端返回响应消息(IP地址)。

看起来挺简单的,但是实际上还有互联网中存在不计其数的域名,将这些信息全部保存在一台DNS服务器中是不可能的,因此一定会出现在某台DNS查询不到信息的情况,于是我们来看一看此时DNS服务器是如何工作的

答案很简单,就是将信息分布到多台DNS服务器中,然后这些DNS相互配合接力,从而查询到需要的信息。首先DNS服务器中的所有信息都是按照域名以分层次的结构来保存的,可能不是很好理解,于是我们先来简单介绍域名的层次,域名的层次越靠近右边层次越高,如:www.baidu.com,这个如果按照公司的的组织结构来说,大概就是com公司的baidu部门的www。以域来说顶层是com域,然后是baidu域,然后是www。com还有上一层域,我们叫根域,根域一般省略,用.号代替如:“www.baidu.com.”。

于是根据域名的层次,我们来分析DNS服务器的层次。按照一个原则:负责管理下级域的DNS服务器的IP地址保存在它的上级DNS服务器中。也就是说,负责管理www.baidu.com这个域的DNS服务器的IP地址保存在管理baidu.com这个DNS服务器中,而管理baidu.com域的DNS服务器IP地址保存在管理com域的服务器中。注意⚠️这里的IP地址是指DNS服务器的IP地址,不是域名的IP地址。这样我们就可以通过上级DNS查询下级DNS的IP地址,也就可以向下级DNS服务器发送查询请求了。还有一项工作就是根域的DNS服务器信息保存在互联网中所有DNS服务器中,这样一来任何DNS服务器都可以查询根域的DNS服务器。

客户端会先访问离得最近的DNS服务器(也就是客户端在TCP/IP设置中填写的DNS服务器IP地址),如果这台DNS服务器中没有存放我们需要的域名的IP地址,最近的这台DNS服务器会直接去根域的DNS查找,根域中也没有则向下查询,例如www.baidu.com,先向跟域查询,没找到则向com的DNS服务器查询,然后是baidu.com,一直向下知道查询到了为止。

但是每次都查询会很麻烦且耗时,于是DNS服务器有一个缓存功能,可以记住每次查询的域名,缓存中的信息为了防止有改变而查询到老的数据,会设置一个有效期,过期则会删除。当然客户端也可以在hosts配置域名对应的IP地址

于是通过DNS服务器查询IP地址的步骤为:首先在客户端的hosts配置中去取域名对应的IP地址,没取到则向最近的DNS服务器查询IP地址,查询时会先从DNS服务器缓存中取,没取到再到DNS服务器的记录表中查找,如果最近的DNS服务器没有找到,则直接到根域的DNS服务器查找,没找到再根据前面域的层级所述一级一级向下查找。

2、数据的传输

通过调用gethostbyname的程序组件(也就是解析器)去DNS获取IP地址之后,则进入了数据的收发(也就是传输)阶段,数据的收发大致总结为以下4个阶段:

  • 1、创建套接字(创建套接字阶段)
  • 2、将管道连接到服务器端的套接字上(连接阶段)
  • 3、收发数据(通信阶段)
  • 4、断开管道并删除套接字(断开阶段)

用伪代码的形式来表述一下对应阶段的情形:

发送阶段
...
<内存地址> = gethostbyname("www.baidu.com"); // 向DNS查询IP地址
...
<描述符> = socket(<使用IPv4>, <流模式>, ...); // 1、创建套接字
...
connect(<描述符>, <服务器的IP地址以及端口号>, ...) // 2、连接
...
write(<描述符>, <发送数据>, <发送的数据长度>); // 3、发送
...

接收阶段
<接收数据长度> = read(<描述符>, <接收缓冲区>, ...); // 3、接收
...
close(<描述符>); // 4、断开

2.1、创建套接字

2.1.1 协议栈

首先了解一下协议栈是什么!协议栈与浏览器不同,它是属于操作系统的部分,工作内容也是从表面上看不到的。先分析一下协议栈的结构:

最上层是网络应用程序,也就是浏览器之类的,接下来是Socket库,Socket库中包含解析器,也就是用来向DNS查询IP地址的,在上面已经说过了。

再下来是操作系统内部,其中包括协议栈:

  • 协议栈上半部分分成两块,分别是负责用TCP协议收发数据部分以及负责用UDP协议收发数据部分,关于TCP和UDP后面再详细讲解。
  • 下半部分是用IP协议控制网络包收发操作部分,在传送数据时,数据会被分成一个一个网络包,而将网络包发送给通信对象的操作就是IP协议负责的。此外IP中还包含ICMP协议和ARP协议。ICMP用于告知网络包传送过程中产生的错误以及各种控制信息,ARP用于根据IP地址查询对应的以太网MAC地址(后面会绝体讲解)。

再下面就是网卡驱动程序负责控制网卡硬件,最下面的网卡则负责完成实际的收发操作,也就是对网线中的信号执行发送和接收操作。

2.1.2 套接字

在协议栈内部有一块用于存放控制信息的内存空间,这里记录了用于控制通信操作的控制信息,例如通信对象的IP地址、端口号、通信操作的进行状态等。本来套接字就只是一个概念,并不存在实体,如果一定要赋予它一个实体,那么存放这些控制信息的内存空间就是套接字的实体。

协议栈在执行操作时需要参阅这些控制信息。例如,在发送数据时,需要看一看套接字中的通信对象IP地址和端口号。或者在发送数据之后,在等待响应消息时,不能一直等,于是一段时间后需要重新发送丢失的数据。为此,套接字中必须要记录是否已经收发响应以及发送数据后经过了多长时间。

上面讲的太抽象了,但是我们知道套接字就是一些控制信息,所以接下来来看看实际的套接字,在终端使用netstat命令显示套接字的内容

  • Proto:协议类型,tcp4与tcp6区别:tcp6支持操作IPV6/IPV4 而tcp4只能操作IPV4
  • Recv-Q:表示程序总共还有多少字节的数据没有从内存空间的套接字缓存拷贝到用户空间
  • Send-Q:对方没有确认收到的字节数。Q表示Queue队列
  • Local Address:本地IP地址和端口号
  • Foreign Address:远程端的IP地址和端口号
  • state:通信状态,ESTABLISHEND表示连接完成正在进行数据通信

总结:套接字的实体就是通信控制信息,协议栈需要根据这些信息判断下一步行动,这就是套接字的作用。

2.1.3 调用socket时的操作

首先,应用程序会调用Socket库中的socket程序申请创建套接字,协议栈根据根据申请执行创建套接字操作。创建套接字时,首先分配一个套接字所需的内存空间,然后向写入初始状态。

接下来,需要将表示这个套接字的描述符告知应用程序。描述符相当于用来区分协议栈中多个套接字的号码牌,也可以理解为某个套接字的编号,在创建套接字时会返回这个描述符。也可以看一下第二节开头的伪代码找到描述符。

收到描述符后,应用程序在向协议栈进行收发数据委托时就要提供这个描述符,到这里,套接字就已经创建完成了

2.2、连接服务器

首先我们要明白“连接”是什么意思,我们不可能说拿个网线连起来就是连接,因为网线一直都是连接的。那么“连接”到底是什么意思呢?连接实际上就是通信双方交换控制信息,在套接字中记录这些必要信息并准备数据收发的一连串操作。是不是感觉很抽象?接下来详细说说“连接”到底是什么意思!

套接字刚创建完成的时候,里面没有存放任何数据,也不知道通信对象是谁,这时候就算应用程序要发消息,协议栈也不知道发给谁,因此我们需要把IP地址和端口号等信息告诉协议栈,这是连接操作之一。

在服务器端也会创建套接字,服务器程序一般会在系统启动时就创建套接字,并等待客户端连接。但是服务器也不知道通信对象是谁,于是我们需要让客户端向服务器告知必要的信息,比如客户端的IP地址和端口号。可见客户端向服务器传达通信的请求也是连接的操作之一。

此外,当执行数据收发操作时,我们还需要一块用来临时存放要收发数据的缓冲区,它也是在连接操作的过程中分配的。这些就是“连接”这个词代码的具体含义。

或者你也可以理解为:连接就是客户端和服务器确认好通信双方身份的过程,其中IP地址确认通信双方的地址,端口号确认IP地址对应的计算机中的哪一个套接字

2.2.1 控制信息补充

之前我们所说的控制信息,大概可以分为两类:

第一类是客户端和服务器联络时交换的控制信息。这些信息不仅连接时需要,包括数据收发和断开连接在内,整个通信过程都需要。那么这些控制信息会通过什么样的形式传输呢?它会放在TCP头部,一个数据包的表现形式大概是这样:

在连接过程中,由于数据收发还没开始,上图中的最后“数据”部分是没有数据的,因此只包含了前面两部分的控制信息。上面这个图我们当前只关心TCP头部,那么TCP头部究竟定义了哪些控制信息呢?

字段名称 长度(比特) 含义
发送方端口号 16 发送网络包的程序的端口号
接收方的端口号 16 网络包的接收方程序端口号
序号 32 发送方告知接收方该网络发送的数据相当于所有发送数据的第几个字节
ACK号 32 接收方告知发送方接收方已经收到所有数据的第几个字节。ACK是acknowledge的缩写
数据偏移量 4 表示数据部分的起始位置,也可以认为表示头部的长度
保留 6 该字段保留,现在未使用
控制位 6 该字段中的每个比特分别表示以下通信控制含义:ACK:表示接收数据序号(ACK号)字段有效,一般表示数据已经被接收方收到;SYN:发送方和接收方互相确认的序号,表示连接操作;FIN:表示断开连接;还有3个不常用先省略
窗口 16 接收方告知发送方窗口大小(即无须等待确认可一起发送的数据量)比较抽象没事,后面会讲解
校验和 16 用来检查是否出错
紧急指针 16 表示应紧急处理的数据位置
可选字段 可变长度 可以添加的字段,但除了连接一般很少使用

以上的控制信息我们见得比较多且很少解释的应该就是控制位,因为我们经常在TCP3次握手中看到,现在你知道他们是什么意思了么?

第二类就是保存在套接字中,用来控制协议栈操作的信息。之前已经讲过了这些信息保存在协议栈中的套接字内存空间内。应用程序传递来的信息以及从通信对象接收到的信息都会保存在这里,还有收发操作的执行状态等信息也会保存在这里,协议栈会根据这些信息来执行每一步操作。创建套接字时创建的缓冲区就是临时保存数据的。不好理解可以再回头再看看我们输入netstat命令时输出的结果。

2.2.2 连接操作的实际过程(三次握手)

这个过程是从应用程序调用Socket库中的connect开始。

connect(<描述符>, <服务器的IP地址以及端口号>, ...)

首先,客户端先创建一个包含表示开始数据收发操作的控制信息头部,如上的TCP头部表格所示,包含了很多字段。我们首先关注端口号,通过端口号,客户端的套接字就准确找到服务端的套接字,也就是搞清了哪两个套接字进行通信。然后我们将头部中控制位的SYN设置为1,可以认为这样就代表连接。此外还需要设置适当的序号(假设设置为X)和端口号,当TCP头部创建好后,TCP模块会将信息传递给IP模块委托它进行发送(因为这是连接的过程,数据块是没有实际数据的)。

然后包通过网络到达服务器,服务器的IP模块会将包传给服务器的TCP模块,然后TCP模块通过收到的包的TCP头部的信息找到端口号对应的套接字,然后套接字会写入相应的信息并且将状态设置为正在连接。然后服务器的TCP模块会返回响应消息,响应消息和客户端一样会将控制位的SYN设置为1,同时会将控制位的ACK也设置为1,并将头部字段的ACK号设置为X+1,然后再将头部的序号设置为Y。ACK号表示已经收到客户端发过来的数据包。

然后,网络包会返回到客户端,通过客户端的IP模块到达TCP模块,并通过TCP头部信息确认连接操作是否成功。如果控制位的SYN为1则表示连接成功,这时会向套接字中写入服务器的IP地址、端口号等信息,同时还会将状态改为连接完毕。到这里,客户端的操作就已经完成了。

但其实还剩下最后一个步骤,刚才服务器响应时控制位的ACK比特设置为1并将ACK号返回给客户端确认包已经收到,相应的客户端也需要将控制位的ACK比特设置为1并将ACK号设置为Y+1发回服务器,告诉刚刚服务器发的相应包已经收到,到这里连接操作才算完成。ACK号与控制位的ACK比特是两个东西,详情看以下上面TCP头部表格,关于ACK号和序号不是太理解的没关系,在收发数据的时候会有详细的解释。

2.3、收发数据

连接之后进入数据收发操作,数据收发操作是从应用程序调用Socket库中的write将要发送的数据交给协议栈开始的。

write(<描述符>, <发送数据>, <发送的数据长度>);

首先,协议栈并不关心应用程序传来的数据是什么内容。其次,协议栈并不是一收到数据就马上发送出去,而是会将数据存放在内部的发送缓冲区中,并等待应用程序的下一段数据。因为一次性将多少数据委托给协议栈是应用程序自行决定的,协议栈并不控制这一行为,在这种情况下,如果马上发送出去就可能会发送大量的小包,导致网络效率下降,因此需要在数据积累到一定量再发送,至于要积累多少数据再发送,不同种类的版本和操作系统会有所不同,但是能根据以下两要素来判断:

  • 第一判断要素是网络包能容纳的最大长度,协议栈里由MTU参数来表示这一最大长度,MTU是总的长度,因为数据包的数据的长度需要由MTU减去头部的长度,这一长度叫MSS,当协议栈从应用程序收到数据长度达到或者超过MSS时则会将数据发送出去。
  • 另一判断要素是时间,当应用程序发送的数据不是很多事,如果一直等数据可能会造成发送延迟,为此,协议栈内部有一个计时器,经过一定时间后不管数据长度如何都会把网络包发出去,这个时间很短,以毫秒计。

进行发送操作时会综合考虑以上两个要素以达到平衡。当然应用程序发送数据时也可以指定一些选项,如:直接发送不等待。

对较大的数据进行拆分

如果一个网络包的数据非常大,发送缓冲区的数据会被以MSS为长度进行拆分,拆分出来的每块数据会被单独放进单独的网络包内。

使用ACK号确认网络包已收到

网络包发送出去之后,需要进行确认操作。---这个ACK和控制位的ACK是两个东西。

首先TCP模块在拆分数据时会先算好每一块数据相当于从头开始的第几个字节,接下来发送这个数据时,将算好的字节写在TCP头部的序号中,然后,发送数据的长度也要告知对方,不过这个在接收方可以自己算出来,使用接收到的数据的长度减去头的长度就能得到数据的长度。有了这两个数值我们就可以知道发送的数据是从整个数据的第几个字节开始,长度是多少了。(序号的初始值是在连接的时候产生一个随机数告知对方的)

通过这些信息,接收方还能检测网络包有没有遗落。例如,上一次接收到第1000字节,那么接下来如果收到序号为1001的包,说明中间没有遗落;但如果收到的包序号为2000,则说明遗漏了1000个字节的数据。如果确认没有遗漏,接收方会将目前为止接收到的数据长度加起来,将这个数值写入TCP头部的ACK号中发送给对方,发送方就能确认接收方到底收到多少数据。例如:发送方发送的包序号为1,长度为1000,当接收方接收到这个数据之后,会返回一个ACK号为10001的包给发送方,发送方收到这个包就知道我之前发的哪个包你已经确认收到了。

除了在客户端发送数据给服务端之外,服务端也会发送数据给客户端,因此在实际发送中会增加一种与之前相反的情形,服务端发送数据时也需要先计算一个序号,然后将序号的数据一起发送给客户端,客户端收到后计算ACK号返回给服务器。

TCP采用ACK号这种方式确认对方是否收到了数据,在得到对方的确认之前,发送过的包都会保存在发送缓冲区。如果对方没有返回某些包对应的ACK号,那么就重新发送这些包。

当客户端在委托协议栈发送请求消息之后,客户端会调用Socket库的read程序来接收相应消息。

<接收数据长度> = read(<描述符>, <接收缓冲区>, ...);

然后会通过read转移到协议栈,然后协议栈会检查收到的数据和TCP头部的内容判断数据是否有丢失,如果没问题则返回ACK号给服务区确认收到数据,同时会把数据保存在接收缓冲区中,并将数据按照序号的顺序连接起来还原出原始数据,最后将数据返回给应用程序。

2.4、从服务器断开并删除套接字

在返回相应消息之后,客户端还可以继续发起下一个请求,这样可以省去再次连接的操作,如果接下来没有请求发送,客户端会调用Socket库中的close程序发起断开操作,断开的操作顺序如下:

  • 客户端将TCP头部中控制位的FIN设置为1,发送出去
  • 服务器将TCP头部的ACK号改成对应数值发给给客户端,确认收到客户端的断开请求
  • 服务器再发一个将TCP头的控制位FIN设置为1的包给客户端
  • 客户端返回ACK号确认收到

这就是断开链接所谓的四次挥手。断开之后就会删除套接字,一般来说会等待几分钟再删除套接字,为什么要等待几分钟呢?留给大家自己思考一下🤔。

2.5、IP模块的工作内容

在前面讲到TCP模块在执行连接、收发、断开等各阶段时,都需要委托IP模块将数据封包发送给通信对象。接下来我们要看看IP模块是如何将包发送给对方的

之前在2.2.1已经说过,包是由头部和数据两部分构成,头部包含了目的地址等控制信息,在前面我们讲了TCP头部的内容,从2.2.1的图上可以看到在TCP头的前面还有一个IP头部,在那张图里面将IP头部表述为以太网和IP的控制信息,实际上他们并不是一个头而是两个不同的头,分别为MAC头部与IP头部。而IP模块则负责给TCP委托过来的数据包添加MAC头以及IP头

  • MAC头部:以太网用的头部,包含MAC地址
  • IP头部:IP用的头部,包含IP地址

于是实际上一个数据包的正确表达形式是这样的:

2.5.1、生成IP头部

IP模块接受TCP模块的委托负责包的收发工作,它会生成IP头部并附加在TCP头部前面。下表表示IP头部的字段

字段名称 长度(比特) 含义
版本号 4 IP协议版本号,4或6
头部长度 4 IP头部长度。可选字段可导致长度变化,因此这里需要指定长度
服务类型(ToS) 8 表示包传输的优先级
总长度 16 表示IP消息的总长度
ID号 16 用于识别包的编号,一般为包的序列号。如果一个包被IP分片,则所有分片都拥有相同ID
标识(Flag) 3 该字段有3各比特,其中2各比特有效,分别代表是否允许分片以及当前包是否为分片包
分片偏移量 13 表示当前包的内容为整个IP消息的第几个字节开始的内容
生存时间(TTL) 8 表示包生存的时间,这是为了避免网络出现回环时一个包永远在网络中打转。每经过一个路由器,这个值就会减1,减到0时这个包就会被丢弃
协议号 8 协议号表示的协议类型:TCP:06;UDP:11;ICMP:01
头部校验和 16 用于检查错误,现在已不使用
发送方IP地址 32 网络包发送方的IP地址
接收方IP地址 32 网络包接收方的IP地址
可选字段 可变长度 可以添加的字段,一般很少使用

表中最重要的就是IP地址,接收方的IP表示这个包应该发到哪里去。这个地址是应用程序在执行连接操作时传给TCP模块然后再由TCP模块传给IP模块的。发送方的IP地址填写使用网卡中的IP地址

2.5.2、生成以太网用的MAC头部

生成IP头部之后,接下来IP模块还需要在IP头部前加上MAC头部。MAC头部字段:

字段名称 长度(比特) 含义
接收方MAC地址 48 网络包接收方的MAC地址,在局域网中使用这一地址来传输网络包
发送方的MAC地址 48 网络包发送方的MAC地址,接收方通过它来判断是谁发送了这个包
以太类型 16 使用的协议类型。一般在TCP/Ip通信中只使用0800(IP协议)和0806(ARP协议--前面有讲这个哦,在2.1.1)

IP头部的IP地址可以用于判断包发送到哪里。那MAC地址是用来干嘛的呢?

MAC地址是在以太网中使用的,以太网在判断目的地时和TCP/IP的方式不同,因此必须采用相匹配的方式才能在以太网中将包发送到目的地,MAC头部就是干这个用的。那什么是以太网呢?

以太网是一种为多台计算机能够彼此自由和廉价地相互通信而设计的通信技术,或者可以说以太网是互联网的一个子集,以太网可以理解为是一种局域网,只能连接附近的设备,这种网络的本质实际上就是一根网线。生活化一点,以太网就是把你家的电脑,笔记本连接到猫上,然后再通过猫连接到因特网上去,这样你才能和国外的朋友乔布斯聊天。因此,你家的电脑,笔记本和猫就组成了一个以太网。所以你知道为什么接收方的IP地址不能在以太网使用了吗?因为这个IP地址是针对整个互联网的,而以太网只是互联网的一部分,所以这个IP地址在以太网不适用

当一台计算机发送信号时,信号就会通过网线流过整个以太网,最终到达所有设备。不过我们无法判断信号是发给谁的,因此要在信号开头加上接受者的地址,也就是MAC头部的接收方MAC地址。比如计算机发一个请求,这个请求首先要到达路由器,那么到达哪个路由器呢?于是MAC地址就是告诉网卡消息应该到达这个路由器。相对的MAC头部发送方的MAC地址是从哪来的?这个MAC地址是在网卡生产时写入ROM里的,只要读取这个值就可以了。

那么问题又来了,接收方的MAC地址我们又是怎么知道的?

这里就需要使用到之前所说的ARP地址解析协议。在以太网中,有一种叫做广播的方法,可以把包发给同一以太网中的所有设备。ARP就是用广播的方式实现的。ARP发送一个广播问“XX这个IP地址是谁的?请把MAC地址告诉我”,然后就有设备会回答“这个IP地址是我的,我的MAC地址是XXX”,于是你就把这个XXX写到MAC头部发送!当然为了节省每次都查询的时间,ARP也有相应的缓存,首先会从缓存中取,为了防止地址变换,缓存一般会在几分钟后删除。

那么问题又来了,ARP发广播时问“XX这个IP地址是谁的”中的IP地址是哪来的?这个IP地址一定不是我们发送消息时填的服务器的IP地址吧,因为ARP查询的只是子网内的目的地MAC地址啊。

首先,发送方将包的目的地也就是要访问的服务器的IP地址写入IP头,协议栈IP模块有一张IP协议的表,在这个表中找到相匹配的条目的IP地址就可以了,然后通过这个IP地址通过ARP查询路由器的MAC地址写入MAC头部,网络包在传输过程中会经过集线器,集线器是根据以太网协议工作的设备。为了判断包接下来应该向什么地方传输,集线器里有一张表(用于以太网协议的表),可以根据MAC头部记录的目的地查找对应的传输方向,将包发送到路由器,然后路由器中又有一张IP协议的表(路由表),可根据这张表以及IP头部中记录的目的地信息查找出接下来应该发往哪个路由器(IP地址),接下来又通过ARP查询MAC地址.....一直进行下去....到达目的地

2.6、网卡的工作内容

IP模块生成的网络包只是存放在内存中的一串数组信息,没有办法直接发送给对方,因此我们需要将数字信息转换为电信号或光信号才能在网线上传输,这才是真正的数据收发过程,负责这一执行操作的是网卡,这个操作需要网卡驱动程序和网卡一起完成。

网卡驱动从IP模块获取包之后,会将其复制到网卡内的缓冲区,然后会传给网卡内部的MAC模块,MAC模块会在包的开头加上报头和起始帧分界符,在末尾加上用于检测错误的FCS(帧校验序列),网卡发出去的包就是这个形式:

网卡中的MAC模块从报头开始将数字信息按每个比特转换为电信号,然后又信号收发模块发送出去,在这里,将数字信息转换为电信号的速率就是网络的传输速率。(网卡的细节并没有讲的很详细,了解大概就差不多了吧)

2.7、接收数据小结

发送包之后,我们假设web服务器返回了一个网络包,到达了网卡,网卡首先会根据报头和起始帧分界符开始提取网络包的数据,并使用末尾的FCS来检查包传输过程中是否有数据错误。然后根据MAC头部的以太类型来交给对应的协议栈,比如这里的以太类型会是0800(IP协议,这个在2.5.2MAC头的表里面我们有说过的)。

接下来IP模块会进行工作,首先是检查IP头部确认格式是否正确,格式没问题下一步就是查看接收方的IP地址是否是自己的地址,如果不是自己的地址则会通过ICMP消息将错误告知发送方(ICMP在文章的2.1.1有简单说明)。

如果IP地址正确,这个包将会被接收下来,这时候还需要完成另一项工作。IP协议有一个叫分片的功能(2.5.1的表格内的标识字段)。简单来说,网线和局域网只能传输小包,因此需要将大的包切分成多个小包。如果包是经过分片的,IP模块会将其暂存在内部的内存空间中,等待IP头部中具有相同ID所谓包全部到达(借助分片偏移量字段),然后IP模块会将他们还原成原始的包,这个操作叫分片重组

到这里IP模块工作就结束了,接下来包会交给TCP模块。TCP会根据IP头部中的接收方和发送方的IP地址,以及TCP头部中的接收方和发送方的端口号来查找对应的套接字(2.1.2有说明)。找到对应的套接字后,就可以根据套接字中记录的通信状态,执行相应的操作了。例如,如果包的内容是应用程序数据,则设置ACK号返回确认接收的包(2.3有说明),并将数据放入缓冲区,等待应用程序来读取;如果是建立连接或者断开连接的控制包,则返回相应的响应控制包,并告知应用程序建立和断开连接(2.2.2有说明)。

然后响应包就到达了应用程序,这个过程也就告一段落了

2.8、UDP协议的收发操作

大多数的应用程序都相之前介绍的一样使用TCP协议来收发数据,当然也有例外,向NDS服务器查询IP地址的时候就是使用的UDP协议。下面简单介绍一下UDP协议

先来理解以下为什么TCP要设计得如此复杂?因为我们需要将数据高效且可靠的发送给对方,为了实现可靠性,我们就需要确认对方是否收到我们发送的数据,如果没有收到还需要再发一遍。为了实现高效的传输,我们要避免重发已经送达的包,而是只重发那些出错的或者未送达的包。TCP之所以复杂就是为了实现这一点。

UDP没有TCP的接收确认、窗口等机制,因此在收发数据之前也不需要交换控制信息,也就是说不需要建立和断开连接的步骤,只要在从应用程序获取的数据前面加上UDP头部,然后交给IP进行发送就可以了。接收也很简单,只要根据IP头部中接收方和发送方的IP地址,以及UDP头部中的端口号找到对应的套接字并将数据交给相应的应用程序即可,除此之外UDP协议没有其他功能了,遇到错误和丢包也一概不管。以下是UDP头部的控制信息:

字段名称 长度(比特) 含义
发送方端口号 16 网络包发送的端口号
接收方端口号 16 网络包接收的端口号
数据长度 16 UDP头部后面数据的长度
校验和 16 用于校验错误

另外一个常见的UDP使用场景就是发送音频和视频数据的时候。音视频数据必须在规定的时间送达,一旦晚了,就会错过播放的时机,就会造成声音和图像卡顿。一旦错过了播放时机,使用TCP重发包也是没用的,因为已经卡顿了,这是无法挽回的。此外,音视频数据中缺少了某些包并不会产生严重问题2,只会产生一些失帧或者卡顿而已,一般都可以接受。所以使用UDP发送数据效率会更高。

结语

至此,我们探索网络是怎样连接的客户端部分说得差不多了,可能后面有时间会再总结一下网络包如何经过集线器、交换机、路由器等设备最终到达互联网,也可能会总结一下服务器端的接收情况(作为客户端开发也可以适当的了解一下)。本篇文章的内容,绝大部分来自同名书本《网络是怎样连接的》,想更加详细了解可以阅读此书。

附上文章前面几个问题的解答:

1、如何正确分析一个URL: 文内1.1
2、DNS如何工作的:文内1.3
3、IP地址、子网掩码:文内1.3.1
4、套接字、端口号:文内2.2; 文内2.1.2 
5、Socket、socket:文内1.3.2 ;文内2.1.3
6、MAC地址:文内2.5.2
7、协议栈:文内2.1.1

再留几个小问题给大家思考(书本上的题),文内都讲解过的:

1、表示网络包收件人的接收方IP地址是位于IP头部还是TCP头部中呢?
2、端口号用来指定服务器程序的种类,它位于TCP头部还是IP头部中呢?
3、对包是否正确送达进行确认的是TCP还是IP?
4、根据IP地址查询MAC地址的机制叫什么?

马上就要放假回家过年了。最后祝大家假期愉快~新年快乐😄新年一起学习、一起成长、一起进步!