计算机网络的重要程度不言而言,也是非常的复杂。今天我将从输入URL这个简单例子开始,一起探索数据包的心路历程。先看文章的大纲。
1 源头------网址
网址即平时所说的URL。就是经常使用的以“Http://”开头的那一串东东,其实常用的还有很多,比如 "FTP" , "FILE"等,我们所访问的目标网站不同,网址开头的写法也就不同,下面列出常见的几种URL。
URL基本格式
从上图可知,URL 中可以包含服务器的域名,文件的路径,收件人邮件地址,用户名,密码等信息。总之URL想表达的是:
- 访问时所使用的协议。"HTTP" , "FTP" , "FILE"等
- 用户名/密码可选
- 所需访问或下载文件的路径
URL的相貌我们已经铭记于心,而且对于 URL 各个子模块也有了基本的认识,可别小看这几个小模块,慢工出细活。我们拆分后仔细看看
- URL拆分
- 理解URL个元素的含义
URL的拆分
从上面的结果我们可以得出,Web 服务器名称为 www.xiaolan.com ,文件路径名为 /dir1/index.html。所以这个URL表示我要访问www.xiaolan.com这个web服务器上路径为/dir/index.html的文件。
下面我们对这个URL稍微改动:
(a)http://www.xiaolan.com/dir/
这里注意,dir 后面的文件名被省略了,这样的话服务器会使用默认的文件名,就反复咱们定义变量的时候,如果没有赋初值,通常会给默认值。同样的道理,服务器也会给一个默认的文件名,不同的服务器默认的文件会不一样,通常会是 Index.html。
(c)http://www.xiaolan.com
这个就比较狠了,后面的"/"直接没有,那该访问啥呢?如果没有路径名,则代表访问根目录下面设置的默认文件。
(d)http://www.xiaolan.com/whatisthis
这末尾的 whatisthis 是什么呢?在这种情况,如果服务器中存在 whatisthis 的文件,则按照文件处理。如果是 wahtsthis 为目录,则按照目录进行处理。
2 HTTP初探
通过第一步对URL的解析,知道了我们所访问的目标是什么,接下来是不是就要请求数据了呢?在做请求之前,我们一起回忆一下HTTP的基础知识
首先HTTP协议定义了客户端和服务器之间交互的消息内容和步骤。简单的说呢即请求的信息包括了"请求啥"以及"你要进行什么操作",和我们面试的时候一样,简历上面写了XX项目,我们是不是也需要清楚自己的项目是什么,你在项目中什么角色一样且做了哪些部分,别写上去的东西一问三不知就比较尴尬了
在HTTP中请求啥这部分叫做 "URI",URI主要存放网页数据的文件名或者是CGI程序如"/Manage/index.html"等。
“进行啥操作”统称为方法。希望服务器能完成什么工作,比如读取URI中表示的数据。哪都有哪些方法可以使用呢,这张图总结常用的几种方法以及含义
这里提一下比较常用且面试常问的两个方法
- GET
当访问 Web 服务器获取网页数据的时候,使用的几乎都是 Get 方法。在请求消息中表明使用Get方法,然后在URI 中表明文件名,比如是 /manage/index.html。服务器收到消息后,会打开/manage/index.html并读取里面的数据,然后存放于相应消息中并返回给客户端,最后在屏幕中完成呈现。
- POST
当我们在购物填写地址信息,或者填写问卷信息的时候,将内容填写到表格中,然后点击提交这个过程,实际上通常就是采用的POST方式。这样看来,采用POST的方式提交数据,我们需要准备三样东西,分别为:所提供的方法,URL 和服务端。服务器收到请求数据后发送给 URI 所指定的应用程序,然后服务端获取应用程序的执行结果并在响应信息中返回给客户端。
OK,现在我们目标基本上明确了,将各个需要发送的内容组合并发给服务器。服务器进行解析,根据客户端的需求完成使命后将需要反馈的信息存放在响应消息中,那么对于客户端而言,也不知道到底是不是想要的结果。所以,服务端会在响应头中用一个状态码表示操作的结果是成功还是失败,比如 200 表示成功,404可能为没找到文件。
此时客户端收到了服务端的响应信息,浏览器觉得这太 lowb 了,给你渲染下并完美的呈现在我们眼前。HTTP的使命就此完成。
3 HTTP请求头------保命天子
看到这里,我相信大家应该了解了 HTTP 的大概样貌。万事儿都是有原则的,那么请求的也是有格式的,不听话就要被打屁屁。
先写方法,加上空格,然后写上 URI(文件或者程序的路径名),行末尾协商HTTP版本号即完成第一行的任务。
第二行为消息头。这一行主要是对第一行内容的进一步补充。比如会告知客户端支持的数据类型、压缩格式,数据有效期等,具体的我放张图,需要的可以去了解下。
第三行为空行,然后加上需要发送的数据,这为消息体。整个消息也就结束
4 HTTP响应-----我行我素
响应的内容和请求信息的内容类似。只是响应中的第一行内容为状态码,表示执行结果是否成功。常见的HTTP状态码如下图所示
响应信息返回后显示在屏幕中,如果为纯文字,到此就结束了。但是大部分时候都会有图片,视频,音频等信息,这个时候怎么办?
浏览器会从响应信息中的文字搜索相应的标签,如果有图片等其他信息,则再次请求服务器,按照相应的文件名向服务器发送请求并显示在刚才预留的空间中。至此,我们访问网页的初级过程版本就差不多结束了。下面用一个案例加深下印象。
上图是简化版,在这里再稳固几点
- Get和Post哪些区别
- 请求头和响应头哪些位置是需要空格或者空行
- 常用响应状态码和请求方法
到此,我们从表面上知道,从敲入网址,构造请求消息,收到响应,并能将美女图片给呈现在眼前,这样就完事了?不好意思,我们时刻都有一颗的去大厂的心,意味着我们不能只知道表面现象还要适当去了解更多的细节。
5 刨根
虽然浏览器能够解析我们的网址,但是它并不具备将消息发送到网络中的能力,那是谁打的辅助?当然是操作系统大哥,为了让操作系统大哥帮忙,我们得先拜访下操作系统大哥,问问需要我们提供哪些资源,需要什么,我们就全力配合它。
- IP地址
我们在浏览器输入的是网址,但是操作系统需要的是IP地址,所以我们需要想办法进行转换。转换的方法就需要请教 DNS 了。很简单,我们告诉DNS,"我的域名是www.xiaolan.com,请告诉我的 IP 地址",OK,DNS服务器很爽快,回复"你的IP地址是xxx.xxx.xxx.xxx"。那么问题来了,我们是如何向 DNS 发送的这个查询呢?我们先来复习DNS
DNS
有些小伙伴说 Mac 地址不能作为标识吗?可是太不容易记忆了,从而出现了简化了 IP 形式,可以它被直接暴露给外网不说,还让人类觉得比较麻烦,干脆用几个字母算了,也就是域名了。域名不仅仅能够代替 IP,还有很多其他的用途比如在 Web 应用中用来标识虚拟主机。
说了这么多,协议头部,到底有哪些字段,其含义是什么都还不知道,那怎么去分析报文,下面我们一起再看看报文什么样子
DNS报文结构
基础结构部分
DNS报文基础部分为DNS首部。其中包含了事务ID,标志,问题计数,回答资源计数,回答计数,权威名称服务器计数和附加资源记录数。
- 事务ID:报文标识,用来区分 DNS 应答报文是对哪个请求进行响应
- 标志:DNS 报文中标志字段
- 问题计数:DNS 查询请求了多少次
- 回答资源记录数:DNS 响应了多少次
- 权威名称服务器计数: 权威名称服务器数目
- 附加资源记录数: 权威名称服务器对应 IP 地址的数目
重点!!!!基础结构中的标志字段细分如下:
标志字段
- QR(Response):查询请求,值为0;响应为1
- Opcode:操作码。0表示标准查询;1表示反向查询;2服务器状态请求
- AA(Authoritative):授权应答,该字段在响应报文中有效。通过0,1区分是否为权威服务器。如果值为 1 时,表示名称服务器是权威服务器;值为 0 时,表示不是权威服务器。
- TC(Truncated):表示是否被截断。当值为1的时候时,说明响应超过了 512字节并已被截断,此时只返回前512个字节。
- RD(Recursion Desired):期望递归。该字段能在一个查询中设置,并在响应中返回。该标志告诉名称服务器必须处理这个查询,这种方式被称为一个递归查询。如果该位为 0,且被请求的名称服务器没有一个授权回答,它将返回一个能解答该查询的其他名称服务器列表。这种方式被称为迭代查询。
- RA(Recursion Available):可用递归。该字段只出现在响应报文中。当值为 1 时,表示服务器支持递归查询。
- Z:保留字段,在所有的请求和应答报文中,它的值必须为 0。
- rcode(Reply code):通过返回只判断相应的状态。
当值为0时,表示没有错误;当值为1时,表示报文格式错误(Format error),服务器不能理解请求的报文;当值为 2 时,表示域名服务器失败(Server failure),因为服务器的原因导致没办法处理这个请求;当值为 3 时,表示名字错误(Name Error),只有对授权域名解析服务器有意义,指出解析的域名不存在;当值为 4 时,表示查询类型不支持(Not Implemented),即域名服务器不支持查询类型;当值为 5 时,表示拒绝(Refused),一般是服务器由于设置的策略拒绝给出应答,如服务器不希望对某些请求者给出应答。
问题部分
该部分是用来显示DNS查询请求的问题,其中包含正在进行的查询信息,包含查询名(被查询主机名字)、查询类型、查询类。
- 查询名:一般为查询的域名,也可能是通过IP地址进行反向查询
- 查询类型:查询请求的资源类型。常见的如果为A类型,表示通过域名获取IP。具体如下图所示
- 查询类:地址类型,通常为互联网地址为1
资源记录部分
资源记录部分包含回答问题区域,权威名称服务器区域字段、附加信息区域字段,格式如下
资源记录部分
资源记录部分
- 域名:所请求的域名
- 类型:与问题部分查询类型值一直
- 类:地址类型,和问题部分查询类值一样
- 生存时间:以秒为单位,表示资源记录的生命周期
- 资源数据长度:资源数据的长度
- 资源数据:按照查询要求返回的相关资源数据
DNS解析详解
知道了DNS大概是什么,它的域名结构和报文结构,是时候看看到底怎么解析的以及如何保证域名的解析比较稳定和可靠
DNS核心系统
根域名服务器(Root DNS Server),大哥,管理顶级域名服务并放回顶级域名服务器IP,比如"com","cn"
顶级域名服务器(Top-level DNS Server),每个顶级域名服务器管理各自下属,比如com可以返回baidu.com域名服务器的IP
权威域名服务器(Authoritative DNS Server),管理当前域名下的IP地址,比如Tencent.com可以返回www.tencent.com的 IP 地址
域名结构
核心系统
举个例子,假设我们访问"www.google.com"
- 访问根域名服务器,这样我们就会知道"com"顶级域名的地址
- 访问"com"顶级域名服务器,可知道"google.com"域名服务器的地址
- 最后方位"google.com"域名服务器,就可知道"www.google.com"的IP地址
嘿嘿,目前全世界13组根域名服务器还有上百太镜像,但是为了让它能力更强,处理任务效率更高,尽量减少域名解析的压力,通常会加一层"缓存",意思是如果访问过了,就缓存,下一次再访问就直接取出,也就是咱么经常配置的"8.8.8.8"等
操作系统中同样也对DNS解析做缓存,比如说曾访问过"www.google.com",
其次,还有我们熟知的hosts文件,当在操作系统中没有命中则会在hosts中寻找。
这样依赖,相当于有了 DNS 服务器,操作系统的缓存和 hosts 文件,能就近(缓存)完成解析就好,不用每次都跑到很远的地方去解析,这样大大减轻的 DNS 服务器的压力。画了一个图,加深印象
域名解析
DNS解析过程
嗯?想必应该知道这个过程了,我们再举个例子,假设我们访问www.qq.com
- 客户端发送一个 DNS 请求,请问 qq 你的IP的什么啊,同时会在本地域名服务器(一般是网络服务是临近机房)打声招呼
- 本地收到请求以后,服务器会有个域名与IP的映射表。如果存在,则会告诉你,如果想访问qq,那么你就访问XX地址。不存在则会去问上级(根域服务器):"老铁,你能告诉我 www.qq.com"的IP么
- 根 DNS 收到本地 DNS 请求后,发现是.com,"www.qq.com哟,这个由.com大哥管理,我马上给你它的顶级域名地址,你去问问它就好了"
- 这个时候,本地 DNS 跑去问顶级域名服务器,"老哥,能告诉下www.qq.com"的ip地址码",这些顶级域名负责二级域名比如qq.com
- 顶级域名回复:"小本本记好,我给你 www.qq.com 区域的权威 DNS 服务器地址",它会告诉你
- 本地DNS问权威DNS服务器:"兄弟,能不能告诉我 www.qq.com 对应IP是啥"
- 权威DNS服务器查询后将响应的IP地址告诉了本地 DNS,本地服务器将 IP 地址返回给客户端,从而建立连接。
那如果我们写段cs程序都得这么麻烦的?不不,上面的是大佬们做好,我们只需要使用相关库就好了,这里就得说说Socket库了。
Socket库
实际上,这是一段程序包含在操作系统的 Socket 库中,我们只需要调用相关的库就可以获得IP。那 Socket 库又是个什么东西?
库,文库, Github 仓库,总之一定是 xxx 的集合。为了简便开发,大佬们会将很多方法封装为库,开发人员直接调用即可,这样不仅节省编程的工作量,也提高开发的工作效率,但是如果库出了问题,你就可能不是 GG 半会儿了。Socket 亦是如此,提供了一些网络编程相关的库,方便开发人员调用操作系统的网络功能。如下图,当我们调用 gethostbyname 的时候,就会向 DNS 服务器发送查询消息,然后 DNS 服务器进行响应。响应的信息就会包含查询到的IP地址,解析器取出IP地址并写入指定的内存中,浏览器只需要从内存地址中取出 IP 地址然后加上HTTP请求信息交给操作系统大哥即可
现在我们拿到了 IP 地址,就可以委托协议栈向这个目标 IP 发送信息了,下面我看看使用Socket库发送数据的过程
理解下上图,服务端创建套接字,我们可以想象为一个水管,当服务端监听进入等待状态后,客户端就可以连接服务端并塞数据到管子中,进行数据的收发。当然,如果不想聊天了,任何一方都可以断开,套接字随机也就断开,通信结束。总结为这几个阶段
- 创建套接字阶段
- 管子连接到服务端套接字
- 收发数据
- 断开并删除套接字
那么再具体的实现中是怎样的呢?
创建套接字,调用socket函数会返回一个描述符,这个描述符类似于门牌号,通过门牌号就可知道你住在那一房间。随后的通信直接关联此描述符即可
连接
创建完套接字,我们就得开始建立连接了,可是还是需要协议栈的帮忙,那么协议栈都干了啥呢?
我们从上到下来刮一遍
- 最上面是网络应用程序,其中包含了浏览器,邮件客户端等,紧接着是Socket库,其中一个功能就是向 DNS服务器发出请求获取IP。
- 往下是操作系统大哥内脏,其中包含了协议栈。上面是传输层常见的TCP和UDP,分别负责 TCP 协议的收发数据和 UD P的首发数据。
- 往下是IP,控制网络数据包的收发操作。主要负责将网络数据包发送给通信对象。其中包含ICMP,ARP等协议。其中ICMP主要负责告知网络数据包在发送的过程中产生的错误信息,ARP负责根据IP地质查询MAC地质
- 再往下就是网卡驱动负责的硬件网卡了。直白点说是对网线的信号执行发送接收操作
将刚才我们创建的客户端套接字与服务器那边的套接字连接上。使用的函数为connect,其中需要三个参数:
- 描述符
connnet会将描述符告诉协议栈,协议栈知道描述符后就来判断到底使用哪个套接字去连接服务端
- 地址
这个IP地址即使刚才我们通过DNS获取的IP地址,并将IP地址告知协议栈
- 端口
IP地址是用来区分网络中各个计算机而分配的数值。可以理解为公安局的公用电话,我们打电话过去找某人还需要知道名字吧,不然打过去找谁?这个某某人就类似端口号,根据这个端口号我们能找到具体的联系人。所以通过IP 端口的方式确定具体的套接字。端口号那么多,到底指定多少端口?不慌,其实服务器上面使用的大部分端口都事先定义好了,比如HTTP多为80,SMPT通常为35端口。这样子就可以正儿八经的通信了
通信
一旦套接字建立连接,随着就可以委托协议栈完成数据的发送操作。具体流程
- 应用程序准备好需要发送的数据
- 构造HTTP请求信息
- 调用write委托协议栈发送数据
那连接的真正含义是什么?
在真正的实体情况下,所谓连接通常是网线的连接,网线确实一直连接着,在这里,连接的意思是通信的双方能够交换控制信息,并在套接字中记录这些信息。
- 连接意义之一是告知协议栈IP和端口
当创建完套接字以后,并没有存放任何的数据,自然也就不知道和谁说话。这个时候,如果应用程序要求发送数据,对于协议栈而言还是一脸懵逼。只有将IP和端口告知协议栈,他才会开始干活
服务端通过Socket库中的read接收消息,这里注意,调用read的时候需要制定用于存放响应消息的内存地址,也叫做接收缓存区。
- 连接意义二:
服务端创建套接字,但是不知道和谁通信。所以等待客户端告知"我是XX,我的IP是xxx,端口号是XXX"
具体操作步骤
- 通过connect将IP地址和端口信息传递给协议栈的TCP模块,它会和服务端的TCP模块交换信息。具体交换哪些信息呢。客户端准确找到服务端以后,会将头部控制位中的SYN置为1。TCP 模块将信息传递给IP模块并委托它进行发送,服务端将接收到的IP模块传送给TCP模块 ,TCP模块根据控制信息找到端口号相同的套接字并将状态修改为正在连接。此时将会进行响应,响应的过程中将ACK控制位设置为1表示已经收到对应的网络包。TCP属于全双工通信,为了尽全力保证网络传输信息的不丢失,会进行双方确认机制。
- 此时网络包到达客户端,通过IP模块到达TCP模块,TCP模块通过头部信息确认连接服务器的这个操作是否成果。如果此时SYN为1则表示连接成功。然后将响应中的ACK设置1告诉服务器你的响应我收到了。这样连接操作完成。控制流程交给应用程序
6 应用阶段
当连接后到达应用程序后,此时将决定我们需要发送什么数据 ,怎么发数据,是按照流的方式还是逐字节发送,以及发什么内容,这样的多样性对于协议栈而言是不怎么关心的。对于协议栈,它不会是收到什么数据就马上发送,它会将数据先暂存缓冲区,如果收到数据就发送,难免会出现大量的小包,这样会让网络效率下降。那对于协议栈而言,到底一次满足多少才进行发送呢?
- 根据MTU判断
MTU是一个网络的最大长度,以太网中为1500字节,减去MTU的头部长度,所能容纳的最大数据长度为1460即MSS。这样就可避免出现大量的小包问题
- 根据时间。
协议栈内部有个计时器,到达时间就将网络包发送出去。
仔细理解这两点,你会发现两者冲突了。因为如果考虑长度的优先级更高,那么网络效率高,但是可能等待缓冲区的时间比较长。如果时间优先级更高,延迟时间就短,但是降低了网络效率。所以在应用程序中提供了选项,在开发的过程中可以根据实际情况进行设置。
如果HTTP请求消息太长了怎么办呢?
数据大了则进行拆分,拆分后为了能完整组装,每个小块提前做好标识。当判断需要发送这些数据的时候,就在每一块的数据前面加上TCP头部,然后交给IP模块进行数据的发送 。
ACK确认机制
如果能发出数据,但是我们发了数据却不知道是否已经收到,或者中途有没有出现损失数据却不知情。所以,引入ACK的确认机制进行可靠的传输。
我们客户端在发送数据的时候,会告知对方发送的数据从第几个字节开始且长度是多少,对于接收方而言也是能很好地清楚是否完整的接收。比如上次接收到的是520字节,那么接下来收到的包是521,说明中间没什么问题。如果收到的包是1314,中间这段时间可能就出轨了。这样子,如果没有遗漏,接收方就会将一共接收到了多少字节写到ACK中并发送给对方。不知道大家理解没有,我再换个方式说一遍。发送电报:“我现在发送的数据是从XX字节开始的部分,一共有XX字节哈”,接收端:“到XX字节之前的数据我都接收完了",这就是确认机制。在此跑一个面试题,为什么序号不是从"1"开始?
TCP正是采用这样的确认机制,数据在传输过程中,在诸如网络集线器等设备就不在有错误补偿机制,这些设备检测到错误就直接丢弃相应的包。TCP采用ACK的确认机制,这个确认的回复时间是根据什么来定?是固定时间内必须返回ACK呢,还是会根据距离远近等动态调整呢?
通常来说,在局域网中ACK的返回相对会比互联网返回所需时间更短。TCP采用动态调整等待时间的方法。这里所说的等待时间是根据ACK返回所需时间来判断的。也就是说TCP在发送数据后就会持续观测ACK返回时间,如果发现慢了则会延长等待的时间。
我们每发一个包,等待确认后再发送另一个包。那么在等待的这个过程是不是就浪费了时间呢。为了改变这样的情况,TCP采用了滑动窗口的方式管理数据发送和ACK号的操作。
滑动窗口
发送一个包后,不傻等ACK的返回,而是继续发送后续的包,这样就充分的利用这段空闲时间。但是这样也出现了一个问题,可能出现发送包的频率太快以致于接收方处理不过来出现堆积。
首先,TCP接收方收到包以后,并不是马上处理交给应用程序,而是先存在暂存区,但是发送方实在是太快了,接收方处理不过来,暂存区也满了。怎么解决?我们希望发送方能够随时知道接收方的接收数据能力,这样就不会无脑的扔数据过去了。ok,TCP 就是这样处理的,它会告诉发送方自己最多还能处理多少数据,然后发送方就会根据接收方的大小进行数据发送控制,这也就是滑动窗口的精髓所在。
通过这样长途跋涉终于发送了HTTP请求信息,等待着响应信息,客户端通过read获取响应信息,和发送数据时协议栈工作类似,从接收缓冲区中取出数据并传递给应用程序
断开连接
在 Web 使用的HTTP协议规定,如果web服务器发送完消息后,就应该主动的断开操作。客户端知道断开后,就当再执行read调用时就会被提醒收发数据已结束,随即也调用 close 进行断开操作。前面我们说过,每获取一次数据就会执行一次连接,这样的效率是非常低的,所以在 HTTP1.1 中就可以一次连接多次请求和响应。·
假设服务器端调用close程序,此时协议栈会生成断开信息的TCP头部,也就是将控制位中的FIN置为1,然后委托给IP模块向客户端发送数据
客户端收到服务端的 Fin 为1的包后,为了告知服务端已经收到了这个 Fin 包,会返回一个ACK号,等待应用程序来处理数据。当应用程序调用read的时候,发现服务端告诉它的是数据已经全部收到,所以客户端随即开始关闭操作,生成FIN比特为1的TCP包,然后交给IP模块发送给服务器,然后服务端段返回 ACK 表示收到。这样客户端与服务端全部关闭结束。
7 IP
上面讲述了想要实现通信,在TCP连接挥手时需要请IP模块帮忙并封装为包发送给就近的网络设备,网络设备根据头部控制信息确定目的地址,如何确定的呢?转发设备中有一张映射表,其中表中能表示"你可以将包发送到XX目的地",此时IP协议再委托以太网协议,寻找路由器的以太网地址(mac地址),如果有多个转发设备,原理过程一样,最终到达接收方的网络设备。
整个流程算是了解了,我们继续深究下IP模板到底是如何完成收发操作的。当TCP委托IP模块进行数据包传送的时候,告诉了目的地址是在哪里,然后经过一系列的中间网络设备寻找以太网地址也就是mac地址,所以现在拥有了IP头部和mac头部,发送给网卡等硬件设备,网卡将数字信息转换为电信号或光信号并发送出去。
当接收方收到数据包会做出响应,其路线相反。数据包以电信号的方式从网线发出,传递给IP模块,IP模块将MAC头部、IP头部后面数据传递给TCP模块
IP地址通过TCP模块获取目的地址,而TCP模块是从应用程序中获取IP地址,对于IP模块而言,只是乖乖的将包发往应用程序指定的接收方,那假设这个IP地址是错误的怎么办呢,IP模块不管,他只是负责打个包发出去,因为这个事儿是应用程序的任务。现在我们已经知道IP模块中有填写目的IP地址,还有哪些重要的控制信息呢
从上图我们发现还需要32字节的发送方IP地址,如果当前计算机只有一张网卡,那就是计算机的IP地址。
- 协议号:代表包从哪个模块来。如果是TCP模块则填写06,如果是UDP模块填写17。
MAC
生成了IP头部后,需要在IP头部加上MAC头部,其中包含了接收方和发送方的MAC地址信息,因为在以太网的世界里需要按照以太网的规则办事儿
- 以太网类型
以太网类型代表后面内容的类型,比如如果是IP地址相关则为0800
- 发送方MAC地址
MAC地址在网卡生产时就放入ROM中,取出存放于MAC头部即可。
- 接收方MAC地址
要知道接收方的MAC地址,又需要找帮手了(ARP),在局域网中大喊一声“xx这个IP地址是哪个?麻烦把你的MAC地址告诉我”,此时就有人给予回应"这是我的IP地址,我的MAC地址是XX",但是我们不可能每次都一顿喊,所以就有杀手锏"ARP缓存",一次询问后就会保存于缓存表中,下次再来如果能匹配到表就可直接获取MAC地址。
此时IP模块完成所有任务。下面就到网卡
8 网卡
上面辛辛苦苦的将包组装完成,但都是数字信息,我们需要转换为电信号或者光信号才能在网络上传输,这就网卡的作用。但是就当当的一块网卡能干啥,啥也干不了,他需要插上去并装上网卡驱动,计算机开机启动之时对网卡进行初始化才能开始使用。
网卡驱动从IP模块获取包之后,复制到网卡缓冲区,然后告知MAC层,MAC模块从缓冲区取出包并加上头部和起始帧,末尾加上帧校验序列
发送信号分为两种方式,一种是集线器方式,一种是交换机的全双工模式。
集线器方式
发送信号之前需要先检查线路中是否存在其他信号,以免造成冲突。MAC模块从头部开始逐比特转换为电信号,然后交给 PHY 模块发送出去,PHY模块将信号转换为可以在网线上传输的格式并通过网线发送出去。但是我们知道,由于电磁波接触到金属等半导体后会产生电流,与信号掺杂在一起,这样势必就会对原有的信号造成影响,为了尽量的避免这种影响,使用了双绞线的方式来抑制噪声。为什么双绞线就可以抑制噪声嘞,因为当电磁波接触到信号线时,假设电流方向为右,当使用双绞线的方式螺旋缠绕后,两个信号线所产生的的电流方向就会相反,从而相当于负负得正低效,不的不说阔学家们牛掰
全双工模式
全双工模式可以让发送和接收操作同时进行且不产生碰撞,因为在全双工模式下,无需等待其他信号就可发送信号,所以比半双工更快
接收方
在半双工的通信过程中,发送信号到达结合搜模块,信号的开头是报头,从起始帧分隔符开始将后面的信号转换为数字信息,即 PHY模块 先开始工作,将信号转换为通用格式并交付给MAC模块,MAC模块从头开始将信号转换为数字信号并存放缓冲区,这里注意,到达信号末尾的时候需要检查FCS,检查方法是通过响应算法计算出结果并和包末尾比较,如果不一致则会当做错误包丢弃。FCS没问题,再通过MAC头部接收方的地址查看是否给自己的包,如果不是也就没必要乱收,直接丢弃,如果MAC地址一致则将包存放缓冲区,此时MAC模块完成任务。
我们知道计算机会执行千万种任务,它不会随时监控网卡的行踪,所以需要打断计算机当前执行的任务,告诉它网卡现在发生的事情,这就是中断。网卡驱动被中断处理程序调用后,会从网卡的缓冲区中取出收到的包,并通过 MAC 头部中的以太类型字段判断协议的类型,如果是0080则代表IP协议,那么网卡驱动就讲这样包给TCP/IP协议栈。此时IP模块开始工作
- 检查IP头部,保证格式正确
- 查看接收方IP,如果接收的IP地址与客户端发送过来IP一致则接受这个包,否则就很可能除了问题,此时IP模块会通过ICMP将错误告知发送方,ICMP包含了哪些错误提示呢,总结如下
此时IP模块交给TCP模块,TCP模块根据IP头部的接收方和发送方IP地址,以及TCP头部的的发送,接收端口信息,组成<发送地址,接收地址,源端口,目的端口>四元组信息查找对应的套接字,从而可查看通信的状态并执行相关的通信。
9 防火墙
看似一切到达服务器还比较顺利,顺利归顺利,但是我们的大部分项目中不得不考虑安全因素,不是什么数据包都可以随便进来,所以必须使用某种手段过滤掉一部分数据包,这就是防火墙
不知道大家用过Tcpdump、Wireshark等工具没,它的过滤机制类似于防火墙的原理,那么为了实现过滤,我们就需要深刻了解各层协议的头部构造,只有熟悉其头部字段,才能在过滤表达式中施展魔法。
通过IP 端口等过滤
比如常见明文协议HTTP使用的80端口,我们可以通过设置IP 端口的方式限制其他数据包的通行。
设置控制位的方式
比如在TCP三次握手的时候会交换或者更新ack syn等信息,我们则可以通过设置相应位置来达到我们过滤的目的
随着系统越来与牛逼,收益越来越来,老板跑来:“小伙计,用户反映请求后半天收不到消息诶”。岂不是废话么,系统做得好,跑路少不了,钱不到手,怎敢跑路,成,一顿性能测试猛如虎,哎呀,加个负载均衡试试?
负载均衡
随着用户访问量的剧增,单台服务器明显感觉到了压力,再这样下去用户可能直接要干我,同事小A牛逼啊,上来就是:"上性能高一点的服务器啊",小B也不赖:“多买几台服务器不就完事了?” 好,我们就听听小B的方案
从曾经的一台服务器,增加到现在到五台服务器,相当于每台服务器分担1/5,这样压力自然小了很多,那问题来了,怎么才能将请求分散到各台服务器呢?哪都有哪些负载均衡的方案?
砸钱
最初实现负载均衡采取的方案很直接,直接上硬件,当然也就比较贵,互联网的普及,和各位科学家的无私奉献,各个企业开始部署自己的方案,从而出现负载均衡服务器
HTTP重定向负载均衡
也属于比较直接,当HTTP请求到达负载均衡服务器后,使用一套负载均衡算法计算到后端服务器的地址,然后将新的地址给用户浏览器,浏览器收到重定向响应后发送请求到新的应用服务器从而实现负载均衡,如下图所示
优点:
- 简单,如果是java开发工程师,只需要servlet中几句代码即可
缺点:
- 加大请求的工作量。第一次请求给负载均衡服务器,第二次请求给应用服务器
- 因为要先计算到应用服务器的IP地址,所以IP地址可能暴露在公网,既然暴露在了公网还有什么安全可言
DNS负载均衡
了解计算机网络的你应该很清楚如何获取IP地址,其中比较常见的就是DNS解析获取IP地址。用户通过浏览器发起HTTP请求的时候,DNS通过对域名进行即系得到IP地址,用户委托协议栈的IP地址简历HTTP连接访问真正的服务器。这样不同的用户进行域名解析将会获取不同的IP地址从而实现负载均衡
乍一看,和HTTP重定向的方案不是很相似吗而且还有DNS解析这一步骤,也会解析出IP地址,不一样的暴露?每次都需要解析吗,当然不,通常本机就会有缓存,在实际的工程项目中通常是怎么样的呢
- 通过DNS解析获取负载均衡集群某台服务器的地址
- 负载均衡服务器再一次获取某台应用服务器,这样子就不会将应用服务器的IP地址暴露在官网了
反向代理负载均衡
这里典型的就是Nginx提供的反向代理和负载均衡功能。用户的请求直接叨叨反向代理服务器,服务器先看本地是缓存过,有直接返回,没有则发送给后台的应用服务器处理。
IP负载均衡
上面一种方案是基于应用层的,IP很明显是从网络层进行负载均衡。TCP./IP协议栈是需要上下层结合的方式达到目标,当请求到达网络层的时候。负载均衡服务器对数据包中的IP地址进行转换,从而发送给应用服务器
注意,这种方案通常属于内核级别,如果数据比较小还好,但是大部分情况是图片等资源文件,这样负载均衡服务器会出现响应或者请求过大所带来的瓶颈
数据链路负载均衡
它可以解决因为数据量他打而导致负载均衡服务器带宽不足这个问题。怎么实现的呢。它不修改数据包的IP地址,而是更改mac地址。应用服务器和负载均衡服务器使用相同的虚拟IP
以上介绍了几种负载均衡的方式,但是很重要的负载均衡算法却没有设计,其中包含了轮询,随机,最少连接,下面分别对此进行介绍(假设以Nginx为例)
轮询
代码语言:javascript复制轮询是Nginx中默认的处理负载的方式,从方式名称应该可以猜出轮询即轮流的分配到后端的服务上。举个例子来说,假设目前后端有4台服务器,此时过来6个连接,如果采用轮询的方式,他就是这样工作A->1,B->2,C->3,D->4,A->5,B->6
upstream XXX{
server localhost:8081;
server localhost:8082;
server localhost:8083;
}
server {
listen 80;
server_name www.xiaolan.com;
location /{
proxy_pass http://xxx;
}
}
Hash方式*处理公式:abs(客户端ip.hash())%服务器数量*
代码语言:javascript复制因为客户端的ip地址是唯一不变的,所以,通过hash算法计算出ip地址对应的哈希码值,通过哈希码值对服务器的数量进行一个求模运算。这样就可以保证每个客户端访问的服务器都是保持不变的,因为hash算法的散列特点,也可以近似的当作平均分配。
upstream H_xx{
ip_hash;
server localhost:8081;
server localhost:8082;
server localhost:8083;
}
server {
listen 80;
server_name www.xiaolan.com;
location /{
proxy_pass http://H_xx;
}
出现的问题
Hash算法中的散列特点,会导致某台服务器请求量过高,其他服务器请求却很少的情况。比如A服务器处理请求1000,而B服务器请求只有80,C服务器请求为20。我们希望后面的请求尽量来C服务器,所以出现了下面的方案
最小连接方式
代码语言:javascript复制采用这种方式,Nginx会将请求发送给当前处理请求数量最少的服务器从而缓解集群的压力
upstream XXX{
leash_conn;
server localhost:8081;
server localhost:8082;
server localhost:8083;
}
server {
listen 80;
server_name www.xiaolan.com;
location /{
proxy_pass http://XXX;
}
}
既然是将请求分给目前连接数最少的服务器,那好,我们看看这种情况。A服务器买的比较早,承受的并发数为200,B服务器稍微能承受的服务器并发数高一点500,C服务器能承受的并发数为1000。目前各个服务器情况如何呢?此时A服务器已经处理了199个连接,B服务器处理了499个连接,C服务器处理了500个连接,我们当然希望接下来的请求交给C服务器处理,不然对于AB而言岂不是压死了最后一根稻草,所以出现下面这种方式
基于权重的方式
代码语言:javascript复制通过设置权重的方式合理分配请求连接数
upstream XXX{
server localhost:8081 weight=6;
server localhost:8082 weight=2;
server localhost:8083 down;
}
server {
listen 80;
server_name www.xiaolan.com;
location /{
proxy_pass http://xxx;
}
}
此时通过weight权重进行资源的分配。down表示当前服务器不参加负载均衡。