一文带你走入计算机网络核心知识

87 阅读1小时+

前言

计算机网络这部分知识属于是非常非常重要的,科班的同学应该是在《计算机网络》专业课上已经学习过了。

不过作为专业课上的知识,并没有做到和我们实际应用(比如面试)非常的吻合,例如对于HTTPS的讲解,大部分都是一笔带过,这也是本文着重讲解的地方。

以及专业课上的知识大多偏向底层和专业概念的解释,并不是十分的通俗易懂(特别是对于一些量法指标的讲解,对于我这种资质愚笨的学生,属实有点强人所难)。

因此本人在完成专业课知识点的学习之后(课程 湖科大教书匠 + 书籍 计算机网络自顶向下方法),再通过学习计算机基础系列八股文的集大成者(小林coding),配合(B站 技术蛋老师)对于密码学和计算机网络知识的详细讲解,结合本人3年后端开发的学习经验,总结出本文。

强调一下,本文不太适合0基础,即完全不了解计算机网络的同学,阅读本文你至少得知道TCP、HTTP、IP这些协议之间的关系,本文只是对计算机网络的部分知识做比较深入的学习,适合想要学习应用于面试但又不局限于面试的同学。

接下来我将和大家,自顶向下的,学习计算机网络相关知识。

概念理解

与我们常规开发使用的Controller-Service-Mapper的三层架构相似,计算机网络也有多层架构,其主要目的也是为了 单一职责解耦

核心在于:每一层都对自己负责的功能进行封装,上层是对下层的进一步的具体功能封装。

通过上层调用下层,来实现上层功能,上层并不需要关心下层的实现。

例如:在后端开发中,我们常常需要根据不同的业务需求给出相应的接口controller,这个接口controller就需要通过调用service来实现他的具体功能,而对于service具体如何实现,以及service如何去调用mapper,controller并不需要关心。

计算机网络架构也同样如此,接下来我将和大家一起简单了解。

对于TCP/IP 网络模型,他的四层架构与架构常用的协议如下:

  • 应用层 HTTP/FTP/DNS
  • 传输层 TCP/UDP
  • 网络层 IP
  • 网络接口层

应用程序在应用层,使用应用层协议发送请求,其中,应用层类似为我们的controller,应用层协议可以类比为我们controller的具体的接口。

应用层协议基于传输层协议实现,传输层协议又基于网络层协议实现,网络层协议又基于网络接口层实现,网络接口层最后将数据交给以太网,完成交换。

这就像我们在实际开发中,前端访问指定接口,controller调用service,service进行数据处理后调用mapper,mapper进行数据库操作。

其中,不同的协议基于的实现也会不同,在不同的情况下,相同的协议可能采用不同的底层实现。这个也好理解,if-else判断不同的status执行不同的操作嘛。

这样以来,我们就能回答为什么计算机网络要采用分层架构了。(八股一大堆。。我个人觉得就是 分层、解耦、单一职责)

但是,对于每一层是干什么的,每一层是具体有哪些协议,各个协议的具体用法和之间的区别,我们还需要接着学习

应用层

计算机网络体系架构的最顶层是应用层。

应用层只需要专注于为用户提供应用功能,比如 HTTP、FTP、Telnet、DNS、SMTP等。

对于数据如何传输、如何通信,应用层都不需要关心。

以下是官方一点的解释,大致看一眼即可(可jump

应用层直接与用户应用程序进行交互,提供用户与网络之间的接口和服务

应用协议定义:

  • 数据格式化和编码:
    • 应用层负责将用户数据转换为网络可传输的格式。
    • 它可以对数据进行分割、封装、压缩和加密等操作,以确保数据在网络中的可靠传输和安全性。
  • 用户身份验证和授权:
    • 应用层提供用户身份验证和授权的机制,以保护网络资源和用户隐私。
    • 这可以包括用户账号和密码验证、数字证书认证、令牌验证等方式。
  • 文件传输和共享:
    • 应用层提供了文件传输和共享的功能。
    • 它可以通过协议如FTP来支持文件的上传、下载和管理
    • 通过协议如NFS(网络文件系统)来实现分布式文件共享。
  • 邮件和消息传递:
    • 应用层支持电子邮件和消息传递服务。
    • 它使用协议如SMTP和POP3(邮局协议版本3)来发送、接收和管理电子邮件
    • 使用协议如XMPP(可扩展通讯和存在协议)来实现实时消息传递。
  • 远程访问和远程控制:
    • 应用层提供远程访问和远程控制的功能使用户能够远程管理和操作其他计算机或设备。
    • 协议如SSH(安全外壳协议)和RDP(远程桌面协议)用于远程访问和远程控制。

HTTP协议

HTTP,全称超文本传输协议,基于传输层TCP协议实现。

HTTP定义了报文的结构以及客户和服务器进行报文交换的方式。(BS架构的服务,大部分请求-响应都是使用HTTP协议)

因为HTTP服务器并不保存关于客户的状态信息,所以我们说HTTP是一个无状态协议

HTTP支持两种连接方式

  • 持续连接
    • 一个TCP连接内可以发送多个HTTP请求和响应报文。
    • 一般来说,如果一条连接经过一定时间间隔(一个可配置的超时间隔)仍未被使用,HTTP服务器就关闭该连接。
    • HTTP的默认模式是使用带流水线的持续连接。(一个完整的HTTP请求响应完成再发下一个),不过是在一个tcp连接内完成的,还有一种是管道网络传输,这个在后面HTTP1.1的特性中会讲解。
  • 非持续连接
    • 每个TCP连接只传输一个请求和响应报文

在我们常规开发任务中,使用的最多的便是HTTP协议。

HTTP 报文格式

HTTP报文有两种:请求报文和响应报文。

请求报文

HTTP请求报文由:请求行、请求头、空行、请求体组成。

请求行

第一行为请求行,请求行有三个字段:方法字段、URL字段、HTTP版本字段。

  1. 方法字段 其中,方法字段可以取以下几种不同的值:
  • GET
  • POST
  • HEAD
  • PUT
  • DELETE

注意,从规范性而言,GET应该用于获取资源、DELETE应该用于删除资源,但是,HTTP本身并没有强制的规定性,在开发者进行实现的时候,同样也可以使用GET来实现删除资源功能。

以及,大家都知道,在开发WEB应用的时候,一次POST类型的HTTP请求,会先发送一次OPTION的HTTP请求,用于校验跨域,这属于浏览器实现的规范,并不属于HTTP协议规定的内容。

所以从本质上而言,各种方法仅仅是定义了规范性的东西,你可以把他当作常量名和浏览器基于常量名做了进一步规范,单独从计算机网络的角度上来说,HTTP请求报文的各个方法并没有不同。

  1. URL字段 URL的基础概念不做解释,URL字段就是请求的完整的URL路径。

  2. HTTP版本字段 现在主流的HTTP版本为1.0、1.1、2.0、3.0。我们最常使用的是1.1。

请求头

从第二行开始,直到遇到空行,即为连续的请求头。

请求头的格式为:头部字段名:值

HTTP自定义了一部分规范的请求头,在实际使用中,我们也可以自定义请求头。

请求体

空行之后即为请求体,请求体的格式有非常多,目前最常用的为json和formdata(传文件)。

注意,请求体任然为HTTP所定义的,在实际的浏览器操作中,会发现GET请求无法发送携带请求体,这是由于浏览器将GET请求的请求体给拦截了,并不是说GET请求就无法发请求体了,我们用postman等调试工具的时候就会发现,其实是可以携带的。

响应报文

HTTP响应报文由状态行、响应头部、空行、响应包体组成。

状态行包含了协议版本、状态码、状态描述

响应头部、响应包体格式与请求报文的相同。

常见的状态码

常见的状态码有:

  • 1XX 信息状态码
  • 2XX 成功
  • 3XX 重定向状态码
  • 4XX 客户端错误
  • 5XX 服务器错误

以下的大家看一眼即可,不需要死记硬背。。

  • 1xx 类状态码属于提示信息,是协议处理中的一种中间状态,实际用到的比较少。
  • 2xx 类状态码表示服务器成功处理了客户端的请求,也是我们最愿意看到的状态。
    • 「200 OK」是最常见的成功状态码,表示一切正常。如果是非 HEAD 请求,服务器返回的响应头都会有 body 数据。
    • 「204 No Content」也是常见的成功状态码,与 200 OK 基本相同,但响应头没有 body 数据。
    • 「206 Partial Content」是应用于 HTTP 分块下载或断点续传,表示响应返回的 body 数据并不是资源的全部,而是其中的一部分,也是服务器处理成功的状态。
  • 3xx 类状态码表示客户端请求的资源发生了变动,需要客户端用新的 URL 重新发送请求获取资源,也就是重定向。
    • 「301 Moved Permanently」表示永久重定向,说明请求的资源已经不存在了,需改用新的 URL 再次访问。
    • 「302 Found」表示临时重定向,说明请求的资源还在,但暂时需要用另一个 URL 来访问。
    • 301 和 302 都会在响应头里使用字段 Location,指明后续要跳转的 URL,浏览器会自动重定向新的 URL。
    • 「304 Not Modified」不具有跳转的含义,表示资源未修改,重定向已存在的缓冲文件,也称缓存重定向,也就是告诉客户端可以继续使用缓存资源,用于缓存控制。
  • 4xx 类状态码表示客户端发送的报文有误,服务器无法处理,也就是错误码的含义。
    • 「400 Bad Request」表示客户端请求的报文有错误,但只是个笼统的错误。
    • 「403 Forbidden」表示服务器禁止访问资源,并不是客户端的请求出错。
    • 「404 Not Found」表示请求的资源在服务器上不存在或未找到,所以无法提供给客户端。
  • 5xx 类状态码表示客户端请求报文正确,但是服务器处理时内部发生了错误,属于服务器端的错误码。
    • 「500 Internal Server Error」与 400 类型,是个笼统通用的错误码,服务器发生了什么错误,我们并不知道。
    • 「501 Not Implemented」表示客户端请求的功能还不支持,类似“即将开业,敬请期待”的意思。
    • 「502 Bad Gateway」通常是服务器作为网关或代理时返回的错误码,表示服务器自身工作正常,访问后端服务器发生了错误。
    • 「503 Service Unavailable」表示服务器当前很忙,暂时无法响应客户端,类似“网络服务正忙,请稍后重试”的意思。

HTTP 缓存技术

HTTP 缓存有两种实现方式,分别是强制缓存和协商缓存。

先做简单概述:

  • 强制缓存 即为 客户端判断是否需要访问服务器 客户端在发起网络请求之前,校验本地缓存的有效时间,如果本地缓存过期,才访问服务器
  • 协商缓存 即为 客户端访问服务器,服务器根据修改时间,来决定是否走缓存

并且 只有开启强制缓存 才会判断是否要走 协商缓存 逻辑

强制缓存

强缓存指的是只要浏览器判断缓存没有过期,则直接使用浏览器的本地缓存,决定是否使用缓存的 主动性在于浏览器 这边。

强缓存是利用下面这两个 HTTP 响应头部(Response Header)字段实现的,它们都用来表示资源在客户端缓存的有效期:

  • Cache-Control, 是一个相对时间;
  • Expires,是一个绝对时间;

Cache-Control的优先级更高。

Cache-control 选项更多一些,设置更加精细,所以建议使用 Cache-Control 来实现强缓存。具体的实现流程如下:

  • 当浏览器第一次请求访问服务器资源时,服务器会在返回这个资源的同时,在 Response 头部加上 Cache-Control,Cache-Control 中设置了过期时间大小;
  • 浏览器再次请求访问服务器中的该资源时,会先通过请求资源的时间与 Cache-Control 中设置的过期时间大小,来计算出该资源是否过期,如果没有,则使用该缓存,否则重新请求服务器;
  • 服务器再次收到请求后,会再次更新 Response 头部的 Cache-Control。

协商缓存

当我们在浏览器使用开发者工具的时候,你可能会看到过某些请求的响应码是 304,这个是告诉浏览器可以使用本地缓存的资源,通常这种通过服务端告知客户端是否可以使用缓存的方式被称为协商缓存

协商缓存可以基于两种头部来实现

  • If-Modified-Since + Last-Modified 通过修改时间判断
  • If-None-Match + ETag 通过是否存在版本变化判断

其中ETag的优先级更高。

第一种:请求头部中的 If-Modified-Since 字段与响应头部中的 Last-Modified 字段实现,这两个字段的意思是:

  • 响应头部中的 Last-Modified:标示这个响应资源的最后修改时间;
  • 请求头部中的 If-Modified-Since:
    • 当资源过期了,发现响应头中具有 Last-Modified 声明,则再次发起请求的时候带上 Last-Modified 的时间
    • 服务器收到请求后发现有 If-Modified-Since 则与被请求资源的最后修改时间进行对比(Last-Modified)
      • 如果最后修改时间较新(大),说明资源又被改过,则返回最新资源,HTTP 200 OK;
      • 如果最后修改时间较旧(小),说明资源无新修改,响应 HTTP 304 走缓存。

第二种:请求头部中的 If-None-Match 字段与响应头部中的 ETag 字段,这两个字段的意思是:

  • 响应头部中 Etag:唯一标识响应资源;
  • 请求头部中的 If-None-Match:
    • 当资源过期时,浏览器发现响应头里有 Etag,则再次向服务器发起请求时
    • 会将请求头 If-None-Match 值设置为 Etag 的值
    • 服务器收到请求后进行比对
      • 如果资源没有变化返回 304
      • 如果资源变化了返回 200

第一种实现方式是基于时间实现的,第二种实现方式是基于一个唯一标识实现的,相对来说后者可以更加准确地判断文件内容是否被修改,避免由于时间篡改导致的不可靠问题。

**为什么 ETag 的优先级更高?这是因为 ETag 主要能解决 Last-Modified 几个比较难以解决的问题:

  • 在没有修改文件内容情况下文件的最后修改时间可能也会改变,这会导致客户端认为这文件被改动了,从而重新请求;
  • 可能有些文件是在秒级以内修改的,If-Modified-Since 能检查到的粒度是秒级的,使用 Etag就能够保证这种需求下客户端在 1 秒内能刷新多次;
  • 有些服务器不能精确获取文件的最后修改时间。

注意,协商缓存这两个字段都需要配合强制缓存中 Cache-Control 字段来使用,只有在未能命中强制缓存的时候,才能发起带有协商缓存字段的请求。

HTTP版本特性与演变

HTTP 常见到版本有 HTTP/1.1,HTTP/2.0,HTTP/3.0,不同版本的 HTTP 特性是不一样的。

主要还是着重于HTTP1.1,对于2.0和3.0只做了解,以后有机会再补。

HTTP/1.1

  • 无状态
    • HTTP协议是无状态的,服务端无需做额外的保存状态来浪费服务端性能,但这也代表是不是每次请求都要进行身份验证呢?
    • 目前主要的解决方案有:
      • cookie-session
      • jwt
      • redis+token
  • 不安全
    • 明文
      • 利于调试,但是信息裸奔,非常的不安全,容易被截获
    • 不验证身份
      • 不对通信方进行身份验证
      • 如果第三方进行了拦截和伪造响应,容易受到中间人攻击等威胁
    • 无法验证报文完整性
      • 在请求和响应到达之前,其内容可能遭到修改
  • 支持长连接和短连接
    • 短连接
      • 一次tcp连接一个http请求
    • 长连接
      • 一次tcp连接多个http请求
      • 流水线传输
        • 虽然一个tcp连接包含多个http请求
        • 但是必须执行完一次http请求-响应的流程,才会继续发送下一次请求
      • 管道网络传输
        • 可以连续发送多个http请求,并不需要等待执行完一次http请求-响应的流程
        • 但是响应的顺序必须按照请求的顺序返回,因此,如果某个请求阻塞,便会造成 队头阻塞
        • 这个模式默认状态没有使用

HTTP/2.0

HTTP2.0的出现主要是解决HTTP1.1出现的性能问题,且HTTP2.0是基于HTTPS的,保证了安全性。

主要新出了以下四个特性:

  • 头部压缩
  • 二进制帧
  • 并发传输
  • 服务器主动推送

HTTP/3.0

HTTP/2 虽然具有多个流并发传输的能力,但是传输层是 TCP 协议,于是存在以下缺陷:

  • 队头阻塞,HTTP/2 多个请求跑在一个 TCP 连接中,如果序列号较低的 TCP 段在网络传输中丢失了,即使序列号较高的 TCP 段已经被接收了,应用层也无法从内核中读取到这部分数据,从 HTTP 视角看,就是多个请求被阻塞了;
  • TCP 和 TLS 握手时延,TCP 三次握手和 TLS 四次握手,共有 3-RTT 的时延;
  • 连接迁移需要重新连接,移动设备从 4G 网络环境切换到 WiFi 时,由于 TCP 是基于四元组来确认一条 TCP 连接的,那么网络环境变化后,就会导致 IP 地址或端口变化,于是 TCP 只能断开连接,然后再重新建立连接,切换网络环境的成本高;

HTTP/3 就将传输层从 TCP 替换成了 UDP,并在 UDP 协议上开发了 QUIC 协议,来保证数据的可靠传输。

QUIC 协议的特点:

  • 无队头阻塞,QUIC 连接上的多个 Stream 之间并没有依赖,都是独立的,也不会有底层协议限制,某个流发生丢包了,只会影响该流,其他流不受影响;
  • 建立连接速度快,因为 QUIC 内部包含 TLS 1.3,因此仅需 1 个 RTT 就可以「同时」完成建立连接与 TLS 密钥协商,甚至在第二次连接的时候,应用数据包可以和 QUIC 握手信息(连接信息 + TLS 信息)一起发送,达到 0-RTT 的效果。
  • 连接迁移,QUIC 协议没有用四元组的方式来“绑定”连接,而是通过「连接 ID 」来标记通信的两个端点,客户端和服务器可以各自选择一组 ID 来标记自己,因此即使移动设备的网络变化后,导致 IP 地址变化了,只要仍保有上下文信息(比如连接 ID、TLS 密钥等),就可以“无缝”地复用原连接,消除重连的成本;

另外 HTTP/3 的 QPACK 通过两个特殊的单向流来同步双方的动态表,解决了 HTTP/2 的 HPACK 队头阻塞问题。

HTTPS协议

概念

HTTPS 的出现是为了解决 HTTP 不安全的缺陷,在 TCP 和 HTTP 网络层之间加入了 SSL/TLS 安全协议,使得报文能够加密传输。

SSL是TLS的前身,现在大部分都是使用TLS1.2了。 HTTPS默认端口号443。

HTTP 由于是明文传输,所以安全上存在以下三个风险

  • 窃听风险,比如通信链路上可以获取通信内容。
  • 篡改风险,比如强制植入垃圾广告,视觉污染。
  • 冒充风险,比如冒充网站。

HTTPS 在 HTTP 与 TCP 层之间加入了 SSL/TLS 协议,可以很好的解决了上述的风险

  • 信息加密:交互信息无法被窃取
  • 校验机制:无法篡改通信内容,篡改了就不能正常显示
  • 身份证书:证明网站真实性,防止中间人攻击

HTTPS 是如何解决上面的三个风险的?

  • 混合加密的方式实现信息的机密性,解决了窃听的风险。(这不准确,大家往后看,这里看个概念就好了)
  • 摘要算法的方式来实现完整性,它能够为数据生成独一无二的「指纹」,指纹用于校验数据的完整性,解决了篡改的风险。
  • 将服务器公钥放入到数字证书中,解决了冒充的风险。

接下来简单介绍 混合加密、摘要算法、数字证书 三者的概念,不过在具体进入https的学习之前,需要先学习对称加密与非对称加密的概念

对称加密、非对称加密结合

  • 对称加密
    • 使用相同的私钥进行加密和解密
    • 速度快。
  • 非对称加密
    • 使用公钥加密后,只能用私钥解密
    • 使用私钥加密后,只能用公钥解密
    • 速度慢
    • 非对称算法有两种实现模式:
      • 公钥加密,私钥解密。
        • 这个目的是为了保证内容传输的安全
        • 因为被公钥加密的内容,其他人是无法解密的,只有持有私钥的人,才能解密出实际的内容;
        • 一般用于客户端向服务端发送不能被破除的信息
      • 私钥加密,公钥解密。
        • 这个目的是为了保证消息不会被冒充
        • 因为私钥是不可泄露的,如果公钥能正常解密出私钥加密的内容,就能证明这个消息是来源于持有私钥身份的人发送的
        • 一般用于服务器向客户端发送信息

理解非对称加密才能理解好https。

HTTPS加密核心理念

加密密文内容

我们知道,HTTP 的数据是明文传送的,为了数据的安全性,我们需要对 HTTP 的报文数据进行加密,数据要能加密,同样也要能解密。

而数据的加密与解密,有两种算法,一种是对称加密算法,一种是非对称加密算法,由于非对称加密算法的效率很低,所以在HTTPS的加密实现中,我们是使用对称加密的方法来加密数据。

而 对称加密算法 要求客户端-服务端 使用相同的 加密密钥。如何安全的生成相同的加密密钥,是HTTPS主要讨论并解决的问题

  1. 客户端与服务端同时将 加密密钥 写死于静态代码?很明显的不行,源代码反编译并不困难,写于静态常量代码泄露风险过大。
  2. 由 客户端或服务端生成 在连接建立后传递给对方?很明显的同样不行,因为在交换加密密钥的时候,这个加密密钥又怎么去加密解密呢?如果不加密,那么这个加密密钥也是一个明文数据,同样也不安全,那样就陷入死循环的境地了。
  3. 从配置中心获取也同理,获取的过程任然是不安全的。

通过分析上述情景,我们可以知道,目前主要的难点问题在于,交换 加密密钥 这个过程,是并不安全的。

于是,HTTPS引入 非对称加密算法 用于安全的传输 加密密钥

服务器 掌握有 公钥与私钥,服务器将 公钥 发送给客户端,客户端生成 加密密钥 后用 公钥 进行加密,被公钥加密的数据,只能被私钥解密,这样,即使公钥是明文传输也无所谓了。

因此,加密密钥的生成与交换的问题便解决了。

身份信息验证

虽然,数据加密的问题是解决了,但是HTTP还存在中间人代理问题,即形成客户端-中间人-服务端这样的三层架构。

中间人将客户端与服务端的网络请求进行拦截,在服务端发送公钥的时候,中间人将公钥拦截,然后把自己的公钥发送给客户端,这样,就相当于在客户端-中间人、中间人-服务器,建立了两个HTTPS连接,由于客户端使用的是中间人的公钥加密,因此中间人可以完全解密客户端信息。

由此可见,问题的关键点就在于判断 公钥是否在自服务器

这里就要引出数字证书和数字证书签发与验证流程的知识了。

数字证书

数字证书通常包含了:

  • 公钥;
  • 持有者信息;
  • 证书认证机构(CA)的信息;
  • CA 对这份文件的数字签名及使用的算法
  • 证书有效期;
  • 还有一些其他额外信息;

那数字证书的作用,是用来认证公钥持有者的身份,以防止第三方进行冒充。

数字证书签发和验证流程

CA 签发证书的过程:

  • 首先 CA 会把持有者的公钥、用途、颁发者、有效时间等信息打成一个包,然后对这些信息进行 Hash 计算,得到一个 Hash 值
  • 然后 CA 会使用自己的私钥将该 Hash 值加密,生成 Certificate Signature,也就是 CA 对证书做了签名;
  • 最后将 Certificate Signature 添加在文件证书上,形成数字证书;

客户端校验服务端的数字证书的过程:

  • 首先客户端会使用同样的 Hash 算法获取该证书的 Hash 值 H1;
  • 通常浏览器和操作系统中集成了 CA 的公钥信息,浏览器收到证书后可以使用 CA 的公钥解密 Certificate Signature 内容,得到一个 Hash 值 H2 ;
  • 最后比较 H1 和 H2,如果值相同,则为可信赖的证书,否则则认为证书不可信。

所以,只要客户端的主机没有去信任一些伪造的证书,那么https就是安全的。

因为中间人是无法拿到ca私钥的,而中间人自身的证书是不会被信任的。

强调一个点,在我的学习中,摘要算法,是为了降低数据的复杂度,将服务器公钥摘要成固定长度的hash值,可以加快非对称加密的加密和解密速度。

建立连接的流程

SSL/TLS 协议基本流程:

  • 客户端向服务器索要并验证服务器的公钥。
  • 双方协商生产「会话秘钥」。
  • 双方采用「会话秘钥」进行加密通信。

前两步也就是 SSL/TLS 的建立过程,也就是 TLS 握手阶段。

TLS 的「握手阶段」涉及四次通信,使用不同的密钥交换算法,TLS 握手流程也会不一样的,现在常用的密钥交换算法有两种:RSA 算法和 ECDHE 算法。

大致流程总结如下:

  1. 客户端和服务端先商量好 TLS信息
    1. 例如 TLS 版本 加密算法等等
  2. 服务端 向 客户端 传递 公钥
    1. 公钥 是藏匿于 数字证书中
    2. 客户端通过CA 验证数字证书真实性
    3. 确保 公钥 来自 服务端
  3. 服务端 和 客户端 通过 非对称加密 交换随机数
    1. 客户端 用 公钥 加密随机数
    2. 服务端 用 私钥 解密随机数
  4. 交换随机数后就可以通过算法生成加密密钥,后续通信即可通过加密方式进行了。

基于RSA算法

传统的 TLS 握手基本都是使用 RSA 算法来实现密钥交换的。

RSA算法概述

RSA算法是一种经典的非对称加密算法,它的数学原理是 大数分解的困难性。(数学原理大家有兴趣自己看吧)。

通过RSA算法我们可以生成一对公钥和私钥,用于加密通信、数字签名等等。

一、第一次握手
  1. 建立tcp连接
  2. 客户端向服务器发起加密通信请求,也就是 ClientHello 请求(第一次握手
    1. 携带 TLS 协议版本
    2. 客户端生成随机数 客户端随机数
    3. 客户端支持的密码套件列表 如RSA加密算法等等,来让服务端选一个进行交互
  3. 服务器返回ack表示接收成功
二、第二次握手
  1. 服务器收到客户端请求后,向客户端发出响应,也就是 SeverHello(第二次握手
    1. 确认TLS协议版本 如果浏览器不支持 关闭加密通信
    2. 服务器生成随机数 用于生成 会话密匙 服务端随机数
    3. 确认的密码套件列表
  2. 服务端发送 Server Certificate
    1. 服务器的数字证书
  3. 服务端发送server hello done表示发送完毕
  4. 客户端发送ack表示接收成功
  5. 客户端收到服务器的回应后
    1. 通过浏览器或者操作系统中的CA公钥,确认服务器数字证书真实性
    2. 从数字证书取出服务器公钥

客户端拿到了服务端的数字证书后,要怎么校验该数字证书是真实有效的呢?

三、第三次握手
  1. 客户端发送 client key exchange
    1. 生成一个新的随机数 pre-master随机数
    2. 提取的服务器公钥 加密随机数
  2. 服务端收到后
    1. 使用服务端私钥 解密
    2. 提取出随机数
  3. 客户端和服务端 根据三个随机数 生成 会话密钥
    1. 三个随机数为
    2. 第一次握手时 客户端生成的随机数
    3. 第二次握手时 服务端生成的随机数
    4. 第三次握手时 客户端又一次生成的随机数,这个随机数通过公钥加密,服务端私钥解密,所以这个随机数是无法被中间人所获取的。
  4. 客户端发送 change cipher spec
    1. 告诉服务端开始使用 会话密匙 通过对称加密方式 发送消息
  5. 客户端发送 Finished
    1. 把之前所有发送的数据做个摘要
    2. 再用 会话密钥 加密一下
    3. 让服务器做个验证
    4. 验证加密通信「是否可用」和「之前握手信息是否有被中途篡改过」。
  6. 服务端 返回 ack
四、第四次握手
  1. 服务端也进行 change cipher spec 和 finished 客户端返回ack
  2. 之后就是正常的http请求了 只是使用 会话密钥 加密

基于ECDHE算法

ECDHE 密钥协商算法是 DH 算法演进过来的,所以我们先从 DH 算法说起。

DH算法

DH 算法是非对称加密算法,它可以用于密钥交换,核心思想是生成一个通信双方共享的私钥

该算法的核心数学思想是 离散对数

离散对数的公式:

a的i次方 mod p = b

i 称为 b 以 a 为底的 mod p的 离散对数。

底数 a 和模数 p 是离散对数的公共参数,也就说是公开的,b 是真数,i 是对数

知道了对数,就可以用上面的公式计算出真数(b)。但反过来,知道真数却很难推算出对数(i)。

特别是当模数 p 是一个很大的质数,即使知道底数 a 和真数 b ,在现有的计算机的计算水平是几乎无法算出离散对数(i)的,这就是 DH 算法的数学基础。

因此,根据此数学问题,定义如下:

通信的双方,首先公共约定P和G作为mod和底数。(P G 暴露)

然后,通信的双方定义自身私钥a和b。

通信的双方定义公钥A和B。公钥的生成为 G的a或b次方 mod P (A和B 暴露)

通信双方交换公钥,继续代入公式 即 A或B的b或a次方 mod P 得到的结果是相同的,这便是后续用于加密的密匙。

DHE算法

根据私钥生成的方式,DH 算法分为两种实现:

  • static DH 算法,这个是已经被废弃了;
  • DHE 算法,现在常用的;

static DH 算法里有一方的私钥是静态的,也就说每次密钥协商的时候有一方的私钥都是一样的,一般是服务器方固定,即 a 不变,客户端的私钥则是随机生成的。

于是,DH 交换密钥时就只有客户端的公钥是变化,而服务端公钥是不变的,那么随着时间延长,黑客就会截获海量的密钥协商过程的数据,因为密钥协商的过程有些数据是公开的,黑客就可以依据这些数据暴力破解出服务器的私钥,然后就可以计算出会话密钥了,于是之前截获的加密数据会被破解,所以 static DH 算法不具备前向安全性。

既然固定一方的私钥有被破解的风险,那么干脆就让双方的私钥在每次密钥交换通信时,都是随机生成的、临时的,这个方式也就是 DHE 算法,E 全称是 ephemeral(临时性的)。

所以,即使有个牛逼的黑客破解了某一次通信过程的私钥,其他通信过程的私钥仍然是安全的,因为每个通信过程的私钥都是没有任何关系的,都是独立的,这样就保证了「前向安全」。

ECDHE算法

DHE 算法由于计算性能不佳,因为需要做大量的乘法,为了提升 DHE 算法的性能,所以就出现了现在广泛用于密钥交换算法 —— ECDHE 算法。

ECDHE 算法是在 DHE 算法的基础上利用了 ECC 椭圆曲线特性,可以用更少的计算量计算出公钥,以及最终的会话密钥。

在之前介绍DH算法的时候介绍过,DH算法的核心就是对于 离散对数 的使用,而离散对数的性能还是较差,因为要进行多次幂运算,因此 ECDHE算法 使用 ECC椭圆曲线特性 来优化性能

反正核心都是一样的,定义公开的信息,然后双方持有私有的信息,通过公开的信息与私有的信息加密,最后形成双方的加密密钥。

关于ECC椭圆曲线特性的内容,本来想挖坑的,但是挖坑后面的ECDHE握手过程就讲不明白了,想了一下还是来补上了。

ECC椭圆曲线

对于椭圆曲线,非数学专业应该也不知道他长什么样,没关系我也不知道。。。 我们只需要知道,对于椭圆曲线,有这么一个类似于定义的概念,给定两个点A和B,那么可以算出一个点C的位置,C的位置为A.B的结果。 也就是说,通过给定的两点,通过点运算,是可以得到确定的第三点的。 而且,A和B不一定要是两个不同的点,因此也可以是A.A=C,并且在定义上可以认定为,C=2A。 那么同样可以算出D=4A,E=6A等等这样的点。 但是E=6A就很暧昧了,他可以是2A.3A,也可以是2A.2A.2A来的。 想象一下,在以下伪代码情况下:

int G = 1;//定义点G
int privatekey = 100;//我自己的私人定制
Integer result;
for(int i =0 ;i<100;i++){
	if(result == null){
		result = G.privateKey;
	}else{
		result = result.privateKey;
	}
}

这样执行完成后,我们就得到了确认的result点,但是要逆向知道这个result是经过什么过程来的,那几乎是无法做到的事情(因为执行多少次也是随机的)。 这样我们可以用来做什么事情呢? 我们定义通信的双方客户端-服务端。 客户端和服务端统一声明G点。 客户端生成 私钥1 然后通过私钥1和G点,生成result1。 服务端生成 私钥2 然后通过私钥2和G点,生成result2。 然后客户端和服务端交换result1和result2。 客户端根据 私钥1 result2 再进行生成操作 生成 加密密钥。 服务端根据 私钥2 result1 再进行生成操作 生成 加密密钥。 这个 加密密钥 一定是相同的。 为什么一定是相同的呢? 其实核心原理就是 G.私钥1*n.私钥2*n = G.私钥2*n.私钥1*n 这样是不是清晰多了。。 那有些,观察力好的同学就能发现,那这个ECC,不还是有幂次计算吗,但是相较于DH算法的幂次,这个点运算要快很多。。所以是这样的个优化流程

ECDHE握手过程

ECDHE可以解决RSA算法的向前安全性问题。

接下来,分析每一个 ECDHE 握手过程。

第一次握手

和RSA一样。

  1. 建立tcp连接
  2. 客户端向服务器发起加密通信请求,也就是 ClientHello 请求(第一次握手
    1. 携带 TLS 协议版本
    2. 客户端生成随机数 客户端随机数(这个随机数仅是用于扩大复杂度的)
    3. 客户端支持的密码套件列表
  3. 服务器返回ack表示接收成功
第二次握手

这里开始就和RSA不一样了。

  1. 服务器收到客户端请求后,向客户端发出响应,也就是 SeverHello(第二次握手
    1. 确认TLS协议版本 如果浏览器不支持 关闭加密通信
    2. 服务器生成随机数 用于生成 会话密匙 服务端随机数
    3. 确认的密码套件列表
  2. 服务端发送 Server Certificate
    1. 服务器的数字证书
  3. 服务端 生成随机数作为 服务端私钥 保存本地
  4. 服务端发送 Server Key Exchange
    1. 选择的椭圆曲线 这样 基点G 就定好了
    2. 根据 G和私钥 计算ECC公钥 (这就是服务端的result1)
    3. 注意 这个ECC公钥,必须是被服务器的数字证书所加密过的,由此来抵制中间人修改ECC公钥,造成的攻击。
  5. 服务端发送server hello done表示发送完毕
  6. 客户端发送ack表示接收成功
  7. 客户端收到服务器的回应后
    1. 通过浏览器或者操作系统中的CA公钥,确认服务器数字证书真实性
    2. 从数字证书取出 服务器公钥
第三次握手
  1. 客户端 生成随机数作为 客户端椭圆曲线的 私钥
  2. 客户端 根据私钥+G 生成 客户端的椭圆曲线 公钥
  3. 客户端 Client Key Exchange
    1. 发送客户端公钥
    2. 客户端公钥不需要进行加密了
  4. 客户端 服务端 根据对方公钥 自己私钥 基点 计算出 共享密钥
  5. 使用 客户端随机数+服务端随机数+共享密钥 生成最终 会话密钥
  6. 算好会话密钥后,客户端会发一个「Change Cipher Spec」消息,告诉服务端后续改用对称算法加密通信。
  7. 接着,客户端会发「Encrypted Handshake Message」消息,把之前发送的数据做一个摘要,再用对称密钥加密一下,让服务端做个验证,验证下本次生成的对称密钥是否可以正常使用。
第四次握手

最后,服务端也会有一个同样的操作,发「Change Cipher Spec」和「Encrypted Handshake Message」消息,如果双方都验证加密和解密没问题,那么握手正式完成。于是,就可以正常收发加密的 HTTP 请求和响应了。

总结

总结一下两个算法的区别

  • RSA算法,主要是通过给客户端 数字证书,客户端保证数字证书正确的情况下,解析获取到正确的 服务端公钥 ,用公钥来加密随机数 发送给服务端,服务端用私钥解密随机数,这样就不会被破解获取了。
  • ECDHE算法,也是通过数字证书,将服务端给的result1进行解密获取,防止被中间人修改,这样才能进行正常的交互。

RPC协议

RPC,又叫做远程过程调用。它本身并不是一个具体的协议,而是一种调用方式。

用于调用一些远程方法,什么叫远程方法?比如微服务系统中,微服务A调用微服务B的接口,如果使用HTTP框架进行调用,那就写的很繁琐很麻烦,使用RPC框架的架构去调用,就像调用本地方法一样,非常的方便。

注意,TCP出身于70年代,RPC出身于80年代,HTTP出身于90年代。

现在电脑上装的各种联网软件,比如 xx管家,xx卫士,它们都作为客户端(Client)需要跟服务端(Server)建立连接收发消息,此时都会用到应用层协议,在这种 Client/Server (C/S) 架构下,它们可以使用自家造的 RPC 协议,因为它只管连自己公司的服务器就 ok 了。

但有个软件不同,浏览器(Browser),不管是 Chrome 还是 IE,它们不仅要能访问自家公司的服务器(Server),还需要访问其他公司的网站服务器,因此它们需要有个统一的标准,不然大家没法交流。于是,HTTP 就是那个时代用于统一 Browser/Server (B/S) 的协议。

因此现在RPC协议主要是用于微服务之间的消息通讯,而且各大框架对于RPC协议的实现也各不相同。

想要细致了解RPC,那肯定要涉及到分布式理论的东西,与本文偏差过远,以后有机会再新出文章讲解。

WebSocket协议

我们知道 TCP 连接的两端,同一时间里,双方都可以主动向对方发送数据。这就是所谓的全双工(后面讲TCP协议的时候会解释)。

而现在使用最广泛的HTTP/1.1,也是基于TCP协议的,同一时间里,客户端和服务器只能有一方主动发数据,这就是所谓的半双工。且在web浏览器的实现中,不支持服务器主动推送。

为了解决HTTP半双工的问题,因此推出了新的应用层协议WebSocket,它支持全双工通信,且支持服务器主动推送。

建立连接的流程

在TCP三次握手结束之后,浏览器会发送一次HTTP请求。

  • 如果此时是普通的 HTTP 请求,那后续双方就还是老样子继续用普通 HTTP 协议进行交互,这点没啥疑问。
  • 如果这时候是想建立 WebSocket 连接,就会在 HTTP 请求里带上一些特殊的header 头

携带的特殊header头如下:

Connection: Upgrade
Upgrade: WebSocket
Sec-WebSocket-Key: T2a6wZlAwhgQNqruZ2YUyg==\r\n

这些 header 头的意思是,浏览器想升级协议(Connection: Upgrade),并且想升级成 WebSocket 协议(Upgrade: WebSocket)。同时带上一段随机生成的 base64 码(Sec-WebSocket-Key),发给服务器。

如果服务器正好支持升级成 WebSocket 协议。就会走 WebSocket 握手流程,同时根据客户端生成的 base64 码,用某个公开的算法变成另一段字符串,放在 HTTP 响应的 Sec-WebSocket-Accept 头里,同时带上101状态码,发回给浏览器。

HTTP 的响应如下:

HTTP/1.1 101 Switching Protocols\r\n
Sec-WebSocket-Accept: iBJKv/ALIW2DobfoA4dmr3JHBCY=\r\n
Upgrade: WebSocket\r\n
Connection: Upgrade\r\n

之后,浏览器也用同样的公开算法将base64码转成另一段字符串,如果这段字符串跟服务器传回来的字符串一致,那验证通过。

这样WebSocket连接就建立完成了。

WebSocket的消息格式

数据包在WebSocket中被叫做帧

WebSocket的消息格式也是 消息头+消息体组成

消息头:

  • FIN: 表示这是一条完整的消息,一般情况下都是1。
  • RSV1、RSV2、RSV3: 暂时没有使用,一般都是0。
  • Opcode: 表示消息的类型,包括文本消息、二进制消息等。
    • 等于 1 ,是指text类型(string)的数据包
    • 等于 2 ,是二进制数据类型([]byte)的数据包
    • 等于 8 ,是关闭连接的信号
  • Mask: 表示消息是否加密。
  • Payload length: 表示消息体的长,单位是字节。
    • 长度分了多个块去存,分别是:7、16、32、16位
    • 无论如何,先读取前7位,根据前七位的值来判断
    • 0-125 只读这7位就可以了
    • 126 还要再读16位
    • 127 还要再读64位
  • Masking key: 仅在消息需要加密时出现,用于对消息进行解密。

消息体单位是字节。

DHCP协议

动态主机配置协议(Dynamic Host Configuration Protocol,简称DHCP)是一种网络协议,用于自动分配IP地址和其他网络配置参数给网络中的设备。

以下是DHCP协议的基本工作过程:

  • DHCP发现:
    • 当设备加入网络时,它会广播一个DHCP发现消息,寻找可用的DHCP服务器。
  • DHCP提供:
    • DHCP服务器接收到DHCP发现消息后,会广播一个DHCP提供消息,其中包含IP地址和其他网络配置参数。多个DHCP服务器存在时,设备可能会收到多个提供消息。
  • DHCP请求:
    • 设备选择其中一个DHCP服务器提供的IP地址和配置参数,并向该服务器发送一个DHCP请求消息,请求使用提供的配置。
  • DHCP确认:
    • DHCP服务器收到DHCP请求消息后,会确认设备的请求,并回复一个DHCP确认消息,其中包含设备所请求的IP地址和其他配置参数。
  • IP地址租约:
    • DHCP协议使用IP地址租约的概念。IP地址租约是指DHCP服务器为设备提供的IP地址和配置参数的使用期限。设备在租约到期前需要更新租约,否则会失去IP地址。
  • 配置更新:
    • 在租约到期前,设备可以向DHCP服务器发送配置更新请求,以延长租约的有效期或请求新的配置参数。

DHCP协议的优点包括:

  • 自动化配置:DHCP协议自动为设备分配IP地址和配置参数,无需手动配置,简化了网络管理和维护的工作。
  • 灵活性:DHCP协议支持动态分配IP地址,可以根据网络中设备的数量和需求进行灵活管理。
  • 集中管理:通过DHCP服务器,可以集中管理网络中的IP地址分配和配置信息,简化了网络管理工作。

DHCP协议在大多数局域网中被广泛应用,使得设备可以方便地连接到网络并获取必要的网络配置。

DHCP客户使用的UDP端口号为68,服务器使用UDP端口号为67

DNS

DNS(Domain Name System,域名系统)是计算机网络中的应用层协议,用于将域名(例如www.example.com)转换为对应的IP地址(例如192.0.2.1),以实现互联网上的主机之间的通信。

层次结构

域名系统采用层次化的树状结构,以域名的顶级域(例如.com、.net)开始,然后依次向下划分为二级域、三级域,以此类推。 最低级的域名单元是主机名,例如www.example.com中的www。

DNS查询过程

当用户在浏览器中输入一个域名时,操作系统会首先检查本地的DNS缓存,如果找到了对应的IP地址,则直接返回结果,否则进行下一步查询。

  1. 如果本地缓存中没有找到对应的IP地址,操作系统会向本地DNS服务器发起递归查询请求。
    1. 本地DNS服务器一般由互联网服务提供商(ISP)或企业内部配置,它会首先查询自己的缓存,如果找到了对应的IP地址,则返回结果给操作系统,否则进行下一步查询。
  2. 如果本地DNS服务器也没有找到对应的IP地址,它会向根DNS服务器发起请求,具体分为两种情况。
  3. 两种查询情况
    1. 递归查询
      1. 递归查询是一种查询方式,其中DNS服务器在查询过程中负责向其他DNS服务器发送查询请求,并在收到结果后直接返回给客户端。
      2. 当本地DNS服务器向根DNS服务器发起递归查询时,根DNS服务器会根据自己的缓存或其他配置向下级DNS服务器查询,并将结果返回给本地DNS服务器。
      3. 本地DNS服务器继续向下级DNS服务器发起递归查询,直到找到负责解析目标域名的DNS服务器,并将结果返回给客户端。
      4. 递归查询方式相比迭代查询方式减少了查询的次数,提高了查询效率。
    2. 迭代查询
      1. 迭代查询是一种查询方式,其中DNS服务器在查询过程中负责向其他DNS服务器发送查询请求,并将结果逐级返回给客户端。
      2. 根DNS服务器是域名系统的顶级服务器,它存储了顶级域的DNS服务器信息。本地DNS服务器会向根DNS服务器查询顶级域的DNS服务器地址。
      3. 根DNS服务器返回顶级域的DNS服务器地址给本地DNS服务器,然后本地DNS服务器继续向顶级域的DNS服务器查询下一级域的DNS服务器地址。
      4. 这个过程会一直迭代下去,直到找到负责解析目标域名的DNS服务器。

使用UDP协议封装,运输层端口号为53。

FTP

FTP(File Transfer Protocol,文件传输协议)是一种用于在计算机网络中传输文件的标准协议。它提供了一种简单的方式,使用户能够上传和下载文件到远程服务器或从远程服务器获取文件。

FTP协议涉及两个主要角色:客户端和服务器。

  • 客户端是发起文件传输请求的计算机,通常是用户所在的计算机。
  • 服务器是存储文件并提供文件传输服务的计算机。

FTP使用两个连接来完成文件传输:控制连接和数据连接。

  • 控制连接用于发送控制命令和传输命令参数,如登录、列出文件、改变目录等。 21端口
  • 数据连接用于实际的文件传输,包括上传和下载文件。

FTP提供了两种工作模式:主动模式和被动模式。

  • 在主动模式下,客户端在数据连接建立前会主动向服务器发起连接请求。 20端口
  • 在被动模式下,服务器被动等待FTP客户的连接。

FTP协议支持多种用户身份验证方式,例如基于用户名和密码的验证,以及匿名访问。

  • 在基于用户名和密码的验证中,客户端需要提供有效的用户名和密码才能登录到服务器。
  • 匿名访问允许用户以匿名身份登录到服务器,通常使用预定义的用户名(如"anonymous")和空密码。

原始的FTP协议是明文传输的,因此在传输过程中可能存在安全风险。 为了增加安全性,可以使用安全的FTP协议(FTPS)或通过VPN等其他安全通道来保护FTP传输。

邮件协议

分为传统和新型两类:

  • 传统
    • 使用SMTP传送
      • 用户代理使用SMTP传送到邮件服务器
      • 邮件服务器之间使用SMTP进行发送
    • 使用POP3接收
      • 接收方使用POP3从邮件服务器获取
  • 新型
    • 用户与邮件服务器之间采用HTTP协议
    • 邮件服务器之间使用SMTP协议

SMTP

基于TCP 端口号25; 只传送ASCII文本。 如果要传送非ASCII文本,需要使用MIME进行包装。

SMTP(Simple Mail Transfer Protocol)是一种用于电子邮件传输的标准协议。下面是SMTP协议的基本流程:

  1. TCP连接建立:客户端(邮件发送方)通过TCP与邮件服务器(邮件接收方的SMTP服务器)建立连接,默认端口号是25。
  2. 握手阶段:建立TCP连接后,客户端和服务器之间进行握手。客户端发送一个“HELO”或“EHLO”命令给服务器,服务器响应并确认连接。
  3. 发送邮件:客户端发送一个“MAIL FROM”命令,指定邮件的发件人。然后发送一个或多个“RCPT TO”命令,指定邮件的收件人。每个“RCPT TO”命令只能指定一个收件人。
  4. 邮件数据:客户端发送一个“DATA”命令,表示接下来要发送邮件的内容。客户端发送邮件头部信息,包括发件人、收件人、主题等。然后发送邮件正文内容。
  5. 结束数据:当邮件正文发送完毕后,客户端发送一个“.”命令表示邮件数据结束。
  6. 服务器响应:服务器接收到邮件数据后,返回一个响应码,表示邮件是否接收成功。
  7. 断开连接:客户端发送一个“QUIT”命令给服务器,表示结束邮件传输。服务器确认断开连接,并关闭与客户端的TCP连接。

以上是SMTP协议的基本流程。需要注意的是,实际的SMTP流程可能还包括身份验证、加密等步骤,以确保邮件传输的安全性和可靠性。另外,SMTP协议只负责邮件的传输,不涉及邮件的读取和存储,这些功能由其他协议(如POP3、IMAP)来实现。

POP3

邮件读取协议 用户只可以下载删除或下载保留 不能管理邮件服务器上的邮件 TCP 段偶110

IMAP

邮件读取协议 用户可操作邮件服务器上的邮件 TCP 端口143

传输层

应用层关注的是为应用程序提供封装好的应用层协议,提供功能,并不关注具体的网络传输方面的内容。

传输层是为应用层提供网络支持的,他为运行在不同主机上的应用进程提供直接的通信服务起着至关重要的作用。

因特网为应用层提供了两种截然不同的可用运输层协议:

  • UDP(用户数据报协议),它为调用它的应用程序提供了一种不可靠、无连接的服务。
  • TCP(传输控制协议),它为调用它的应用程序提供了一种可靠的、面向连接的服务。

在正式进入UDP和TCP协议的学习之前,我们先学习 多路复用与多路分解技术

多路复用与多路分解

多路复用与多路分解技术 是 运输层 能够实现分解服务到同一台计算机的不同服务的基础。

多路复用与多路分解服务是所有计算机网络都需要的。

  • 多路复用:主机 从不同的 套接字(socket) 中收集数据块,为每个数据块封装首部信息形成报文段,再传递给网络层。
  • 多路分解:运输层 将报文段的数据 交付给正确的套接字

这里了解端口与socket概念的话,应该是很好理解的。

现在应该清楚运输层是怎样能够实现分解服务的了:在主机上的每个套接字能够分配一个端口号,当报文段到达主机时,运输层检查报文段中的目的端口号,并将其定向到相应的套接字。然后报文段中的数据通过套接字进入其所连接的进程。如我们将看到的那样,UDP 大体上是这样做的。然而,也将如我们所见,TCP 中的多路复用与多路分解更为复杂。

这里也可以理解到,tcp和udp的端口是互不影响的

UDP

UDP只是做了运输协议能够做的最少工作。除了 复用/分解功能 及少量的差错检测外,它几乎没有对 IP 增加别的东西。

使用 UDP 时,在发送报文段之前,发送方和接收方的运输层实体之间没有握手。正因为如此,UDP 被称为是无连接的。

与TCP对比,UDP具有以下特点:

  • 无需连接建立
  • 无连接状态
  • 分组首部开销小

UDP报文格式

由报文头部和数据组成。

报文头部:

  • 源端口号、目的端口号
  • UDP长度,UDP校验和

UDP差错检测

UDP检验和提供了差错检测功能。

检验和用于确定当UDP 报文段从源到达目的地移动时,其中的比特是否发生了改变。

虽然 UDP 提供差错检测,但它对差错恢复无能为力。UDP 的某种实现只是丢弃受损的报文段;其他实现是将受损的报文段交给应用程序并给出警告。

TCP

如果要用一句话来描述 TCP 协议,我想应该是,TCP 是:

  • 可靠的(reliable)
  • 面向连接的(connection-oriented)
  • 基于字节流(byte-stream)
  • 全双工的(full-duplex)

接下来我将一步一步讲解

TCP报文格式

TCP报文由 TCP报文首部 + 传输字节组成

TCP报文首部:(示例32位一行)

  • 源端口号和目的端口号
    • 源端口号(Source Port):16位字段,指示发送方的端口号。
    • 目标端口号(Destination Port):16位字段,指示接收方的端口号。
  • 序列号
    • Seq(Sequence Number):32位字段,用于对TCP报文段进行排序和重组。
    • 它指示发送方发送的第一个数据字节的序号。
  • 确认号
    • Ack(Acknowledgment Number):32位字段,只有当ACK标志位被设置时才有效。
    • 它指示期望接收的下一个字节的序号。
    • 同时也是对之前收到的所有数据的确认。
  • 数据偏移、保留、Flag、窗口
    • 数据偏移
      • 也叫首部长度 占4位(表示数0-15),每一位4字节
      • 用于标识首部长度 或 数据的起始位置
    • 保留
      • 占 6位 保留给将来使用 目前为0
    • Flag
      • URG 表示紧急指针字段的值是否有效 指示需要优先处理的数据
      • ACK 确认号有效
      • PSH 表示发送方希望接收方尽快将数据交给应用层进行处理
      • RST 用于重置TCP连接
      • SYN 发起一个新连接
      • FIN 连接终止
    • 窗口
      • 16位 指示接收方的接收窗口大小 流量控制
      • 可能 TCP 协议设计者们认为 16 位的窗口大小已经够用了,也就是最大窗口大小是 65535 字节(64KB)。
      • 自己挖的坑当然要自己填,因此TCP 协议引入了「TCP 窗口缩放」选项 作为窗口缩放的比例因子,比例因子值的范围是 0 ~ 14,其中最小值 0 表示不缩放,最大值 14。比例因子可以将窗口扩大到原来的 2 的 n 次方
  • 校验和、紧急指针
    • 校验和
      • 16位字段,用于检验TCP报文段的完整性。
    • 紧急指针
      • 只有当URG标志位被设置时才有效。它指示紧急数据的字节偏移量
  • 选项、填充
    • 选项
      • 可变长度字段,用于在TCP头部中添加一些可选的功能和参数。
      • MSS 最大段大小选项
      • Window Scale 窗口缩放
    • 填充
      • 可变长度字段,用于填充TCP头部,以使其长度为32位的整数倍。

TCP协议的可靠性

IP 是一种无连接、不可靠的协议:它尽最大可能将数据报从发送者传输给接收者,但并不保证包到达的顺序会与它们被传输的顺序一致,也不保证包是否重复,甚至都不保证包是否会达到接收者,更不好保证传输的内容是否出错。

TCP 要想在 IP 基础上构建可靠的传输层协议,必须有一个复杂的机制来保障可靠性。 主要有下面几个方面:

  • 对每个包提供校验和
  • 包的序列号解决了接收数据的乱序、重复问题
  • 超时重传
  • 流量控制、拥塞控制

保证传输内容正确

每个 TCP 包首部中都有两字节用来表示校验和,防止在传输过程中有损坏。如果收到一个校验和有差错的报文,TCP 不会发送任何确认直接丢弃它,等待发送端重传。

解决乱序、重复问题

包的序列号保证了接收数据的乱序和重复问题

重传机制

TCP 针对数据包丢失的情况,会用重传机制解决。

接下来说说常见的重传机制:

  • 超时重传
  • 快速重传
  • SACK
  • D-SACK
超时重传

重传机制的其中一个方式,就是在发送数据时,设定一个定时器,当超过指定的时间后,没有收到对方的 ACK 确认应答报文,就会重发该数据,也就是我们常说的超时重传。

这个超时的时间是由操作系统内核自己计算的,他是动态变化的,因为不能太大(等待耗时)不能太小(重发过多,拥塞)

如果超时重发的数据,再次超时的时候,又需要重传的时候,TCP 的策略是超时间隔加倍。

也就是每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。

超时触发重传存在的问题是,超时周期可能相对较长。

快速重传

TCP 还有另外一种快速重传(Fast Retransmit)机制,它不以时间为驱动,而是以数据驱动重传。

例如,客户端依次发送 Seq(请求)1-6 ,服务端响应多次 ACK2 表示,2以前的数据都接收了,这时,客户端重发Seq2,服务端响应ACK7,表示1-6都接收了。

但是这样也有一个问题,万一2-6都丢失了,那么客户端只能一次一次发2-6,而不能直到2-6都丢失了那么就2-6全部都重发一遍,这样也很费时。

SACK

还有一种实现重传机制的方式叫:SACK( Selective Acknowledgment), 选择性确认。

这种方式需要在 TCP 头部「选项」字段里加一个 SACK 的东西,它可以将已收到的数据的信息发送给「发送方」,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据。

如果要支持 SACK,必须双方都要支持。在 Linux 下,可以通过 net.ipv4.tcp_sack 参数打开这个功能(Linux 2.4 后默认打开)

D-SACK

Duplicate SACK 又称 D-SACK,其主要使用了 SACK 来告诉「发送方」有哪些数据被重复接收了。

可见,D-SACK 有这么几个好处:

  • 可以让「发送方」知道,是发出去的包丢了,还是接收方回应的 ACK 包丢了;
  • 可以知道是不是「发送方」的数据包被网络延迟了;
  • 可以知道网络中是不是把「发送方」的数据包给复制了;

在 Linux 下可以通过 net.ipv4.tcp_dsack 参数开启/关闭这个功能(Linux 2.4 后默认打开)。

流量控制

发送方不能无脑的发数据给接收方,要考虑接收方处理能力。

如果一直无脑的发数据给对方,但对方处理不过来,那么就会导致触发重发机制,从而导致网络流量的无端的浪费。

为了解决这种现象发生,TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,这就是所谓的流量控制。

为了防止这种情况发生,TCP 规定是不允许同时减少缓存又收缩窗口的,而是采用先收缩窗口,过段时间再减少缓存,这样就可以避免了丢包情况。

拥塞控制

拥塞控制,即当tcp发送方检测到网络出现拥塞时,自调节减少发送窗口大小。

其实只要「发送方」没有在规定时间内接收到 ACK 应答报文,也就是发生了超时重传,就会认为网络出现了拥塞。

拥塞控制主要是四个算法:

  • 慢启动
  • 拥塞避免
  • 拥塞发生
  • 快速恢复

定义拥塞窗口大小为 cwnd 。发送窗口大小=min(拥塞窗口大小,发送缓存大小)

慢启动

TCP 在刚建立连接完成后,首先是有个慢启动的过程,这个慢启动的意思就是一点一点的提高发送数据包的数量。

即,每次成功收到ack,都会使发送窗口大小+1。

也是是说,每一轮请求完成,都会使下一次能发送的请求数加倍。

这很明显是一个指数级别的增长,肯定不能让他无限的增长下去的。

有一个叫慢启动门限 ssthresh (slow start threshold)状态变量来控制这个增长状态。

  • 当 cwnd < ssthresh 时,使用慢启动算法。
  • 当 cwnd >= ssthresh 时,就会使用「拥塞避免算法」。
拥塞避免

前面说道,当拥塞窗口 cwnd 「超过」慢启动门限 ssthresh 就会进入拥塞避免算法。

拥塞避免算法即:每收到目前 cwnd 大小个数的ack后,cwnd+1;

也就是说,每轮只+1个了,但是哪怕这样,最终也还是会一直变大,直到拥塞,这时候就使用拥塞发生算法。

拥塞发生

当网络出现拥塞,也就是会发生数据包重传,重传机制主要有两种:

  • 超时重传
  • 快速重传
超时重传引起

当发生了「超时重传」,则就会使用拥塞发生算法。

这个时候,ssthresh 和 cwnd 的值会发生变化:

  • ssthresh 设为 cwnd/2,
  • cwnd 重置为 1 (是恢复为 cwnd 初始化值,我这里假定 cwnd 初始化值 1)

重新进行慢启动。

快速重传引起

还有更好的方式,前面我们讲过「快速重传算法」。当接收方发现丢了一个中间包的时候,发送三次前一个包的 ACK,于是发送端就会快速地重传,不必等待超时再重传。

TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,则 ssthresh 和 cwnd 变化如下:

  • cwnd = cwnd/2 ,也就是设置为原来的一半;
  • ssthresh = cwnd;
  • 进入快速恢复算法
快速恢复

快速重传和快速恢复算法一般同时使用,快速恢复算法是认为,你还能收到 3 个重复 ACK 说明网络也不那么糟糕,所以没有必要像 RTO 超时那么强烈。

正如前面所说,进入快速恢复之前,cwnd 和 ssthresh 已被更新了:

cwnd = cwnd/2 ,也就是设置为原来的一半; ssthresh = cwnd; 然后,进入快速恢复算法如下:

  • 拥塞窗口 cwnd = ssthresh + 3 ( 3 的意思是确认有 3 个数据包被收到了);
  • 重传丢失的数据包;
  • 如果再收到重复的 ACK,那么 cwnd 增加 1;
  • 如果收到新数据的 ACK 后,把 cwnd 设置为第一步中的 ssthresh 的值,原因是该 ACK 确认了新的数据,说明从 duplicated ACK 时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态;

TCP连接的创建(三次握手)

建立TCP连接主要是解决以下三个问题:

  • 确认对方存在
  • 协商参数(最大报文长度、最大窗口大小、时间戳选项)
  • 对运输实体资源进行分配和初始化(缓存、状态变量、连接表)

连接创建的过程

  1. 一开始,客户端和服务端都处于 CLOSE 状态。
  2. 服务端主动监听某个端口,处于 LISTEN 状态
  3. 客户端发送第一次TCP请求
    1. 随机初始化序号 x ,将此序号置于 TCP 首部的序列号(Seq)字段中
    2. 同时把 SYN 标志位置为 1,表示 SYN 报文。
    3. TCP规定,SYN设置为1的报文段,不能携带数据,但要消耗一个序号
  4. 客户端处于 SYN-SENT 状态(同步已发送)
  5. 服务端发送第二次TCP请求
    1. 随机初始化序号 y ,将此序号置于 TCP 首部的序列号(Seq)字段中
    2. TCP 首部的确认号(ack)字段填入 x + 1,
    3. SYN 和 ACK 标志位置为 1。
  6. 服务端处于 SYN-RCVD 状态(同步已接收)
  7. 客户端发送第三次TCP请求
    1. 应答报文 TCP 首部 ACK 标志位置为 1 ,
    2. TCP 首部的序列号(seq)字段填入 x + 1
    3. TCP 首部的确认号(ack)字段填入 y + 1
    4. 本次可传递数据,如果没传递数据,则不消耗seq序列号
  8. 客户端处于 ESTABLISHED 状态(连接已建立)
  9. 服务端处于 ESTABLISHED 状态(连接已建立)

总结:每一边都要经历一轮 SYN和ACK 不过第二次握手包含了SYN和ACK 所以只有三次握手。

为什么是三次握手

关于为什么是三次握手才能建立连接,而不是两次或四次?理性来讲,下意思就能说出来的便是,三次握手才能建立连接,而且不导致资源浪费,但是这都是浮于表面的讲解。

重要的是为什么三次握手才可以初始化 Socket、序列号和窗口大小并建立 TCP 连接。

接下来,以三个方面分析三次握手的原因:

  • 三次握手才可以阻止重复历史连接的初始化(主要原因)
  • 三次握手才可以同步双方的初始序列号
  • 三次握手才可以避免资源浪费
原因一:避免历史连接

简单来说,三次握手的首要原因是为了防止旧的重复连接初始化造成混乱。

客户端发送tcp连接请求 seq为100

  • 如果客户端宕机
    • 客户端重新发起tcp请求 seq为200
    • 旧请求先到
      • 此时客户端需要的ack为201 但是旧请求会返回的ack为101
      • 就会RST设置为1 这次响应就不会建立连接
      • 如果是两次握手,那么此时就错误的建立连接了
    • 新请求先到
      • 此时客户端能正常建立tcp连接
      • 如果旧请求延迟了很久,在新tcp连接结束后到,那么如果是二次握手会错误的开启连接
  • 如果网络延迟
    • 客户端触发超时重传机制
    • 旧请求先到
      • 此时客户端需要的ack为201 但是旧请求会返回的ack为101
      • 就会RST设置为1 这次响应就不会建立连接
      • 如果是两次握手,那么此时就错误的建立连接了
    • 新请求先到
      • 此时客户端能正常建立tcp连接
      • 如果旧请求延迟了很久,在新tcp连接结束后到,那么如果是二次握手会错误的开启连接

仔细观察下可知,如果是两次握手,会导致在一定情况下错误的开启服务器的tcp连接,导致资源浪费。

主要是因为在两次握手的情况下,服务端没有中间状态给客户端来阻止历史连接,导致服务端可能建立一个历史连接,造成资源浪费。

原因二:同步双方初始序列号

TCP 协议的通信双方,都必须维护一个 序列号,序列号是可靠传输的一个关键因素,它的作用:

  • 接收方可以去除重复的数据;
  • 接收方可以根据数据包的序列号按序接收;
  • 可以标识发送出去的数据包中,哪些是已经被对方收到的(通过 ACK 报文中的序列号知道);

所以当客户端发送携带「初始序列号」的 SYN 报文的时候,需要服务端回一个 ACK 应答报文,表示客户端的 SYN 报文已被服务端成功接收,那当服务端发送初始序列号给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。

而两次握手的情况下,服务端收不到来自客户端的应答回应。

避免资源浪费

即两次握手会造成消息滞留情况下,服务端重复接受无用的连接请求 SYN 报文,而造成重复分配资源。

握手数据丢失会发生什么?

第一次

当客户端想和服务端建立 TCP 连接的时候,首先第一个发的就是 SYN 报文,然后进入到 SYN_SENT 状态。

在这之后,如果客户端迟迟收不到服务端的 SYN-ACK 报文(第二次握手),就会触发「超时重传」机制,重传 SYN 报文,而且重传的 SYN 报文的序列号都是一样的。

超时重传机制受操作系统底层限制。

例如在Linux操作系统中:客户端的 SYN 报文最大重传次数由 tcp_syn_retries内核参数控制,这个参数是可以自定义的,默认值一般是 5。

通常,第一次超时重传是在 1 秒后,第二次超时重传是在 2 秒,第三次超时重传是在 4 秒后,第四次超时重传是在 8 秒后,第五次是在超时重传 16 秒后。没错,每次超时的时间是上一次的 2 倍。

当第五次超时重传后,会继续等待 32 秒,如果服务端仍然没有回应 ACK,客户端就不再发送 SYN 包,然后断开 TCP 连接。

所以,总耗时是 1+2+4+8+16+32=63 秒,大约 1 分钟左右。

第二次

当服务端收到客户端的第一次握手后,就会回 SYN-ACK 报文给客户端,这个就是第二次握手,此时服务端会进入 SYN_RCVD 状态。

第二次握手的 SYN-ACK 报文其实有两个目的 :

第二次握手里的 ACK, 是对第一次握手的确认报文; 第二次握手里的 SYN,是服务端发起建立 TCP 连接的报文; 所以,如果第二次握手丢了,就会发生比较有意思的事情,具体会怎么样呢?

因为第二次握手报文里是包含对客户端的第一次握手的 ACK 确认报文,所以,如果客户端迟迟没有收到第二次握手,那么客户端就觉得可能自己的 SYN 报文(第一次握手)丢失了,于是客户端就会触发超时重传机制,重传 SYN 报文。

然后,因为第二次握手中包含服务端的 SYN 报文,所以当客户端收到后,需要给服务端发送 ACK 确认报文(第三次握手),服务端才会认为该 SYN 报文被客户端收到了。

那么,如果第二次握手丢失了,服务端就收不到第三次握手,于是服务端这边会触发超时重传机制,重传 SYN-ACK 报文。

在 Linux 下,SYN-ACK 报文的最大重传次数由 tcp_synack_retries内核参数决定,默认值是 5。

因此,当第二次握手丢失了,客户端和服务端都会重传:

客户端会重传 SYN 报文,也就是第一次握手,最大重传次数由 tcp_syn_retries内核参数决定; 服务端会重传 SYN-ACK 报文,也就是第二次握手,最大重传次数由 tcp_synack_retries 内核参数决定。

第三次

客户端收到服务端的 SYN-ACK 报文后,就会给服务端回一个 ACK 报文,也就是第三次握手,此时客户端状态进入到 ESTABLISH 状态。

因为这个第三次握手的 ACK 是对第二次握手的 SYN 的确认报文,所以当第三次握手丢失了,如果服务端那一方迟迟收不到这个确认报文,就会触发超时重传机制,重传 SYN-ACK 报文,直到收到第三次握手,或者达到最大重传次数。

注意,ACK 报文是不会有重传的,当 ACK 丢失了,就由对方重传对应的报文。

TCP连接的销毁(四次挥手)

TCP 断开连接是通过四次挥手方式,双方都可以主动断开连接

连接销毁的过程

  • 一开始双方都处于 ESTABLISHED 连接已建立阶段
  • 假如 客户端想要断开连接
  • 第一次挥手
    • 客户端调用关闭连接的函数,发送FIN报文
    • FIN和ACK=1
    • 这个报文代表客户端不会再发送数据
    • 客户端进入 FIN-WAIT-1 终止等待状态1
  • 第二次挥手
    • 服务端收到FIN报文后 马上回复ACK确认报文
    • ACK=1
    • 服务端进入 CLOSE-WAIT 关闭等待状态
    • TCP协议栈为收到的FIN包 插入一个文件结束符EOF 放入接收缓冲区
    • 服务端通过read调用感知FIN包 EOF再以排队等候的其他已接收的数据之后
  • 客户端收到ACK报文后,进入 FIN-WAIT-2 终止等待状态2
  • 第三次挥手
    • 服务端不断的read数据,读到EOF后,服务端如果有数据要发送,那就发送数据,直到没有数据需要发送,则调用关闭连接的函数
    • 此时服务端发送FIN包
    • FIN和ACK=1
    • 代表服务端不会再发送数据了
    • 服务端进入 LAST-ACK 最终确认状态
  • 第四次挥手
    • 客户端接收到FIN包后,发送ACK包给服务端
    • ACK=1
    • 客户端进入 TIME-WAIT 时间等待状态(等待2MSL)
  • 服务端接收到ACK包后,进入 LAST-ACK CLOSED 关闭状态
  • 服务端等待时间结束后,进入CLOSED关闭状态

总结:每一个方向都要经历一轮FIN和ACK

为什么要四次挥手

服务器收到客户端的 FIN 报文时,内核会马上回一个 ACK 应答报文,但是服务端应用程序可能还有数据要发送,所以并不能马上发送 FIN 报文,而是将发送 FIN 报文的控制权交给服务端应用程序:

如果服务端应用程序有数据要发送的话,就发完数据后,才调用关闭连接的函数;

如果服务端应用程序没有数据要发送的话,可以直接调用关闭连接的函数,

**是否要发送第三次挥手的控制权不在内核,而是在被动关闭方(上图的服务端)的应用程序,因为应用程序可能还有数据要发送,由应用程序决定什么时候调用关闭连接的函数,当调用了关闭连接的函数,内核就会发送 FIN 报文了,所以服务端的 ACK 和 FIN 一般都会分开发送。

注意:FIN 报文不是一定得调用关闭连接的函数,才会发送

如果进程退出了,不管是不是正常退出,还是异常退出(如进程崩溃),内核都会发送 FIN 报文,与对方完成四次挥手

粗暴关闭 vs 优雅关闭

前面介绍 TCP 四次挥手的时候,并没有详细介绍关闭连接的函数,其实关闭的连接的函数有两种函数:

  • close 函数

    • socket 同时关闭发送方向和读取方向,也就是 socket 不再有发送和接收数据的能力。
    • 如果有多进程/多线程共享同一个 socket,如果有一个进程调用了 close 关闭只是让 socket 引用计数 -1,并不会导致 socket 不可用,同时也不会发出 FIN 报文,其他进程还是可以正常读写该 socket,直到引用计数变为 0,才会发出 FIN 报文
  • shutdown 函数

    • 可以指定 socket 只关闭发送方向而不关闭读取方向,也就是 socket 不再有发送数据的能力,但是还是具有接收数据的能力。
    • 如果有多进程/多线程共享同一个 socket,shutdown 则不管引用计数,直接使得该 socket 不可用,然后发出 FIN 报文,如果有别的进程企图使用该 socket,将会受到影响。
  • 客户端

    • 使用close函数
      • 收到服务端发送的数据 客户端已经不能发送和接收了
      • 客户端内核便会返回 RST报文
      • 服务端收到RST报文 内核会直接释放连接
      • 这样就不会经历完整的四次挥手
      • 所以说close函数是粗暴关闭
      • 服务端断开tcp连接,但是服务端应用程的程序不知道
      • 这时候应用层程序进行操作
        • 如果是读 返回RST报错 Connection reset by peer
        • 如果是写 程序会产生 SIGPIPE 信号 默认情况下进程会终止,异常退出
    • 使用shutdown函数
      • 只关闭发送不关闭读取
      • 客户端任然能收到服务端的数据
      • 能经历完整的 四次挥手
      • 所以说shutdown是优雅关闭
      • 注意 shutdown也可以指定 只关闭读取 不关闭发送
      • 如果不关闭发送 内核是不会发送FIN报文的(毕竟FIN代表我方不再发送数据)
什么时候会只发送三次挥手

当被动关闭方在 TCP 挥手过程中,「没有数据要发送」并且「开启了 TCP 延迟确认机制」,那么第二和第三次挥手就会合并传输,这样就出现了三次挥手。

然后因为 TCP 延迟确认机制是默认开启的,所以导致我们抓包时,看见三次挥手的次数比四次挥手还多。

TCP 延迟确认机制

当发送没有携带数据的 ACK,它的网络效率也是很低的,因为它也有 40 个字节的 IP 头 和 TCP 头,但却没有携带数据报文。 为了解决 ACK 传输效率低问题,所以就衍生出了 TCP 延迟确认。 TCP 延迟确认的策略:

  • 当有响应数据要发送时,ACK 会随着响应数据一起立刻发送给对方
  • 当没有响应数据要发送时,ACK 将会延迟一段时间,以等待是否有响应数据可以一起发送
  • 如果在延迟等待发送 ACK 期间,对方的第二个数据报文又到达了,这时就会立刻发

挥手数据丢失会发送什么

第一次

第一次挥手为客户端发送的FIN报文,如果FIN报文丢失,客户端无法接收到服务端的ACK报文,会触发超时重传机制。

重发次数由 tcp_orphan_retries 参数控制。

每次重传后,等待上一次超时的两倍时间,如果没收到ACK,则根据是否达到最大次数,来决定是继续重传还是关闭连接。

第二次

第二次挥手报文为服务端发送的ACK报文,如果ACK报文丢失,客户端无法直到FIN报文抵达,会触发客户端ACK报文的超时重传机制,ACK报文不会重传

服务端收到第一次挥手的 FIN 报文后,内核会自动回复 ACK,同时连接处于 CLOSE_WAIT 状态,顾名思义,它表示等待应用进程调用 close 函数关闭连接。

第三次

内核是没有权利替代进程关闭连接,必须由进程主动调用 close 函数来触发服务端发送 FIN 报文。

服务端处于 CLOSE_WAIT 状态时,调用了 close 函数,内核就会发出 FIN 报文,同时连接进入 LAST_ACK 状态,等待客户端返回 ACK 来确认连接关闭。

第三次挥手为服务端发送FIN报文,也就是说 客户端无法接收到FIN报文服务端无法接收到客户端的ACK报文

  • 客户端无法接收FIN报文
    • 客户端使用 close 关闭
      • 因为无法 接收与发送 所以这个状态不能很久
      • 默认60
      • tcp_fin_timeout 控制了这个状态下连接的持续时间
      • 如果超时 客户端直接关闭
    • 客户端使用 shutdown 关闭
      • 只关闭了发送 没关闭接收
      • 如果 一直没收到FIN 会一直开启
      • tcp_fin_timeout 无法控制
  • 服务端无法接收到客户端的ACK报文、
    • 服务端就会重发 FIN 报文
    • 重发次数仍然由 tcp_orphan_retries 参数控制
    • 和第一次挥手重发机制一样
第四次

第四次挥手为客户端发送的ACK报文,ACK报文丢失,会导致服务端无法直到FIN报文是否抵达,触发服务端的超时重传机制,ACK报文不会重传。

当客户端收到服务端的第三次挥手的 FIN 报文后,就会回 ACK 报文,也就是第四次挥手,此时客户端连接进入 TIME_WAIT 状态。

在 Linux 系统,TIME_WAIT 状态会持续 2MSL 后才会进入关闭状态。

然后,服务端(被动关闭方)没有收到 ACK 报文前,还是处于 LAST_ACK 状态。

如果第四次挥手的 ACK 报文没有到达服务端,服务端就会重发 FIN 报文,重发次数仍然由前面介绍过的 tcp_orphan_retries 参数控制。

客户端TIME_WAIT机制

为什么 TIME_WAIT 等待的时间是 2MSL?

MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。

TIME_WAIT 等待 2 倍的 MSL,比较合理的解释是: 网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间。

比如,如果被动关闭方没有收到断开连接的最后的 ACK 报文,就会触发超时重发 FIN 报文,另一方接收到 FIN 后,会重发 ACK 给被动关闭方, 一来一去正好 2 个 MSL。

可以看到 2MSL时长 这其实是相当于至少允许报文丢失一次。

如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时。

在 Linux 系统里 2MSL 默认是 60 秒,那么一个 MSL 也就是 30 秒。Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒。

为什么需要 TIME_WAIT 状态?

需要 TIME-WAIT 状态,主要是两个原因:

  • 防止历史连接中的数据,被后面相同四元组的连接错误的接收;
    • 这个时间足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。
  • 保证「被动关闭连接」的一方,能被正确的关闭;
    • 如果没有 TIME_WAIT 那么客户端收到FIN后,返回ACK,直接关闭
    • ACK报文被丢失
    • 服务端 未收到ACK报文 重传FIN 客户端此时以关闭连接,返回RST报错,这并不优雅。

TCP滑动窗口

TCP 引入了窗口这个概念。即使在往返时间较长的情况下,它也不会降低网络通信的效率。

那么有了窗口,就可以指定窗口大小,窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值

窗口的实现实际上是操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。

通常窗口的大小是由接收方的窗口大小来决定的。

发送方发送的数据大小不能超过接收方的窗口大小,否则接收方就无法正常接收到数据。

网络层

网络层的主要作用是:实现主机与主机之间的通信,也叫点对点(end to end)通信。

这其中就涉及到:如何去标记网络中的主机、如何去寻找网络中的主机、寻找的路径的选择等等方面。

网络层最常使用的是 IP 协议(Internet Protocol),IP 协议会将传输层的报文作为数据部分,再加上 IP 包头组装成 IP 报文。

IP协议

IP地址由32位正整数表示,采用了点分十进制的标记方式,也就是将 32 位 IP 地址以每 8 位为组,共分为 4 组,每组以「.」隔开,再将每组转换成十进制。

简介

IP地址由32位正整数表示,采用了点分十进制的标记方式,也就是将 32 位 IP 地址以每 8 位为组,共分为 4 组,每组以 . 隔开,再将每组转换成十进制。

例如 255.255.255.255 最大格式。

IP地址的分类

IP地址的分类有一个明显的演进阶段

  1. 分类地址 网络号+主机号
  2. 划分子网 网络号+子网掩码+主机号
  3. CIDR 网络号+主机号+网络号占位

分类地址

IP 地址分类成了 5 种类型,分别是 A 类、B 类、C 类、D 类、E 类,其中ABC为单播地址,D为多播地址,E为保留地址

这个分类,是通过将IP地址的32为整数,划分为 网络号 + 主机号。不同类型的IP地址,网络号占的位数不同。

  • 网络号,负责标识该 IP 地址是属于哪个「子网」的;

  • 主机号,负责标识同一「子网」下的不同主机; 先看一下具体分类类型的定义,再举例解释网络号和主机号的概念。

  • A类(0-127)

    • 网络号占8位 主机号24位
    • 网络号第一位固定为0
  • B类(128-191)

    • 网络号占16位 主机号占16位
    • 网络号前二位固定为10
  • C类(192-254)

    • 网络号占24位 主机号占8位
    • 网络号前三位固定为110
  • D(224-239)

    • 前四位固定为 1110
  • E(240-255)

    • 前四位固定为 1111

其中IP 地址分类成了 5 种类型,分别是 A 类、B 类、C 类、D 类、E类

广播地址分为本地广播和网络广播,本地广播不会被路由器转发,网络广播会被路由器转发,但是由于安全问题,大部分路由器会屏蔽转发。

这种分类地址的优点就是简单明了、选路(基于网络地址)简单。

网络号和主机号
  • 网络号,负责标识该 IP 地址是属于哪个「子网」的;
  • 主机号,负责标识同一「子网」下的不同主机;

注意,网络号是向机构申请的,主机号是自己去分配的(这和我们目前实际使用的不一样,不要混淆了)。例如 组织1 向 机构 申请了一个A类地址 1.12 那么1.12下的主机号地址都由这个组织1来掌管分配。

这样他的缺点就也非常明显了:

  • 同一网络下没有地址层次(没办法做到隔离性)缺乏灵活性
  • 不能很好的与现实网络匹配 地址都或多或少 造成浪费

划分子网

因为原本的分类地址并不灵活,对于A类地址来说,他可分配的主机数量实在是太多了,因此引入划分子网的概念,通过占用主机号位数,对IP地址再进行一次分类,使IP地址划分更灵活,且主机数量更可控。

这样,IP地址的分层就由之前的 网络号+主机号 进一步划分为 网络号+子网号+主机号

子网划分分为两种形式

  • 定长子网掩码 FLSM
  • 变长子网掩码 VLSM

通过子网掩码,可以得知划分子网后的IP地址的网络号和子网号占用。

子网掩码由连续的1和0组成,起头的 网络号+子网号 部分是连续的1,到达 主机号 部分即为连续的0。

因此,在给定 划分子网后的IP地址 和 子网掩码的情况下,我们可以先通过IP地址,得知IP地址的类型 即得知 网络号长度,再根据子网掩码,即可知道 子网号长度

这样也能清楚该IP地址所属的子网与主机号。

定长子网掩码 FLSM

定长子网掩码,便是在该分配的ip网络下,划分的各个子网的子网掩码相同,各个子网的最大主机数量也相同。

变长子网掩码 VLSM

因为定长子网掩码下,资源分配还是不够灵活,有的子网可能只需要很少,有的却需要很多,此时就会导致严重的资源损耗。

变长子网掩码,便是在该分配的ip网络下,划分的各个子网的子网掩码可能不同,最大主机数也可能不同。

例如:在给定的C类地址(201.201.156.0)下,C类地址的主机位为8,如果要划分为3个子网,并且子网1的主机数为100台,子网2和子网3的主机数为50台。 此时使用定长子网掩码的话,每个子网都要划分100台,很明显无法达到要求。 变长子网掩码划分是有公式计算的,大家感兴趣可以自己搜,我这边不讲公式只讲案例: 在这种情况下,我们先把C类地址分为两半,即子网掩码为 255.255.255.128 此时 子网1使用的子网掩码便为 255.255.255.128 然后将剩下来的另一半 地址,继续分一半出来 即子网掩码为 255.255.255.192 此时 子网2和子网3使用的子网掩码便为 255.255.255.192

无分类域间路由选择

CIDR(Classless Inter-Domain Routing,无类域间路由选择)它消除了传统的A类、B类和C类地址以及划分子网的概念,因而可以更加有效地分配IPv4的地址空间。它可以将好几个IP网络结合在一起,使用一种无类别的域际路由选择算法,使它们合并成一条路由从而较少路由表中的路由条目减轻路由器的负担。

简单来说,他做了两件事:

  • 消除传统的IP网络划分(ABCDE),取消子网掩码概念
  • 能更有效的分配IP地址

他可以任意的指定网络号长度,例如 a.b.c.d/x 后面的x即为网络号长度。

参考

小林coding xiaolincoding.com/ 蛋老师 space.bilibili.com/327247876?s…