2025-01-15🌱上海: ☀️ 🌡️+6°C 🌬️↓18km/h
# TCP/IP 的四层模型是是么?
# 什么是 TCP/IP 协议
TCP/IP 协议是一种网络体系模型的代名词,指的是多种协议的协议簇,即包含 TCP、IP、MAC、UDP、HTTP、FTP 等多种协议,它是四层网络模型,包含应用层、传输层、网络层、链路物理层(网络接口层),和 OSI 七层网络模型、五层网络模型略有区别,TCP/IP 四层模型可以说是 OSI 七层网络的简化版。如下图所示。
# 网络模型对应的设备及协议
# TCP/IP 分层讲解
TCP/IP 的四层结构:包含应用层、传输层、网络层、链路物理层(网络接口层)。
# 应用层(Application Layer)
TCP/IP 协议应用层整合了 OSI 的应用层、表示层和会话层,直接为用户应用程序服务,常见协议包括浏览器与客户端文本传输的 HTTP、FTP 协议,域名服务的 DNS 协议,电子邮件的 SMTP 协议,远程登录的 TELNET、SSL 协议,以及动态主机配置的 DHCP 协议等。
应用层位于传输层之上,主要提供两个终端设备上的应用程序之间信息交换的服务,它定义了信息交换的格式,消息会交给下一层传输层来传输。 我们把应用层交互的数据单元称为报文。
应用层协议定义了网络通信规则,对于不同的网络应用需要不同的应用层协议。在互联网中应用层协议很多,如支持 Web 应用的 HTTP 协议,支持电子邮件的 SMTP 协议等等。
应用 | 应用层协议 | 端口号 | 传输层协议 | 备注 |
---|---|---|---|---|
域名解析 | DNS | 53 | UDP/TCP | 长度超过 512 字节时使用 TCP |
动态主机配置协议 | DHCP | 67/68 | UDP | |
简单网络管理协议 | SNMP | 161/162 | UDP | |
文件传送协议 | FTP | 20/21 | TCP | 控制连接 21,数据连接 20 |
远程终端协议 | TELNET | 23 | TCP | |
超文本传送协议 | HTTP | 80 | TCP | |
简单邮件传送协议 | SMTP | 25 | TCP | |
邮件读取协议 | POP3 | 110 | TCP | |
网际报文存取协议 | IMAP | 143 | TCP |
# 传输层(Transport Layer)
传输层的主要任务就是负责向两台终端设备进程之间的通信提供通用的数据传输服务。 应用进程利用该服务传送应用层报文。“通用的” 是指并不针对某一个特定的网络应用,而是多种应用可以使用同一个运输层服务。
协议 | 特点 |
---|---|
TCP(传输控制协议) | 提供面向连接、可靠的数据传输服务 |
UDP(用户数据协议) | 提供无连接、尽最大努力(不保证可靠性)的数据传输服务 |
# 网络层
- 网络层封装运输层报文段或用户数据报为 IP 数据报传送
- 网络层负责选择合适的路由使分组找到目的主机
- 区分运输层的 “用户数据报 UDP” 和网络层的 “IP 数据报”
- 网络层的 “网络” 指计算机网络体系结构第三层
- 互联网由大量异构网络通过路由器连接,其网络层叫网际层或 IP 层,使用无连接网际协议和路由选择协议
协议名称 | 功能解析 |
---|---|
网际协议 (IP) | 核心协议,封装运输层数据为 IP 数据报,通过 IP 地址在不同网络间转发,提供无连接、不可靠的数据传输。 |
互联网组管理协议 (IGMP) | 用于多播通信,主机通过它告知路由器自己所属多播组,路由器借此管理多播数据转发。 |
互联网控制报文协议 (ICMP) | 用于 IP 主机和路由器间传递控制消息,报告错误和异常,也用于网络诊断,如 “ping” 命令基于此协议。 |
- 寻址:数据链路层中使用的物理地址(如 MAC 地址)仅解决网络内部的寻址问题。在不同子网之间通信时,为了识别和找到网络中的设备,每一子网中的设备都会被分配一个唯一的地址。由于各子网使用的物理技术可能不同,因此这个地址应当是逻辑地址(如 IP 地址)。
- 路由选择:当源节点和目的节点之间存在多条路径时,本层可以根据路由算法,通过网络为数据分组选择最佳路径,并将信息从最合适的路径由发送端传送到接收端。
# 网络接口层
- 网络接口层可看作数据链路层和物理层的合体
- 数据链路层(链路层)将网络层的 IP 数据报组装成帧,在相邻节点间链路传送,帧含数据和控制信息
- 物理层实现相邻计算机节点间比特流透明传送,屏蔽传输介质和物理设备差异
协议名称 | 功能解析 |
---|---|
CSMA/CD | 以太网的介质访问控制方法,检测网络传输避免数据冲突 |
MAC | 数据链路层子层,控制连接物理层的物理介质,含设备物理地址 |
差错检测 | 检测数据传输中是否出错,如奇偶校验、CRC 等 |
多路访问 | 多个设备共享同一通信信道传输数据的方式 |
以太网 | 广泛应用的计算机局域网技术,规定连线、信号和协议等 |
# TCP/IP 是如何进行数据通信的?
发送方:
- 应用层调用
Socket API
将数据包放入Socket
发送缓冲区。 - 网络协议栈从发送缓冲区取出数据包,按
TCP/IP
栈从上到下处理。 - 传输层增加
TCP
头。 - 网络层增加
IP
头、路由查找、按MTU
分片。 - 数据链路层进行物理地址寻址、添加帧头帧尾后放发包队列。
- 驱动程序通过
DMA
从发包队列读出网络帧并由网卡发送。
接收方:
- 网卡通过
DMA
将网络帧放收包队列,通过硬中断告知中断处理程序。 - 网卡中断处理程序为网络帧分配
sk_buff
并拷贝,通过软中断通知内核。 - 内核协议栈从缓冲区取出网络帧,从下到上处理。
- 数据链路层检查报文合法性、去帧头帧尾交网络层。
- 网络层取出
IP
头判断走向,确认发往本机则去IP
头交传输层。 - 传输层取出
TCP
或UDP
头,根据四元组找Socket
并拷贝数据到接收缓存。 - 应用层用
Socket
接口读取新接收的数据。
# 为什么要分层?
- 简化设计与实现:网络功能分层,各层负责特定任务,降低复杂性。
- 模块化:各层可独立发展优化,通过标准接口通信,方便更新替换。
- 互操作性:明确定义接口和协议,不同厂商设备软件兼容,提升网络兼容性。
- 故障隔离:各层有错误检测等机制,分层结构助于定位故障。
# 每层的数据信息?
层次 | 包头信息主要字段 | 数据单位 |
---|---|---|
应用层 | HTTP:Host(目标主机)、User - Agent(客户端类型)、Content - Length(内容长度)等 <br>DNS:Transaction ID(事务 ID)、Flags(标识符)、Query/Response(查询 / 响应标识)等 | 数据(Data) |
传输层 | TCP:Source Port(源端口)、Destination Port(目的端口)、Sequence Number(序列号)、Acknowledgment Number(确认号)、Flags(控制标志)等 <br>UDP:Source Port(源端口)、Destination Port(目的端口)、Length(数据包长度)、Checksum(校验和)等 | 报文段(Segment) |
网络层 | IP:Source IP Address(源 IP 地址)、Destination IP Address(目的 IP 地址)、TTL(生存时间)、Protocol(上层协议类型)等 | 数据包(Packet) |
网络接口层(数据链路层和物理层) | 以太网:Source MAC Address(源 MAC 地址)、Destination MAC Address(目的 MAC 地址)、Type(上层协议类型)等 | 帧(Frame) |
# Cookie、Session、Token 之间有什么区别?
名称 | 定义 | 存储位置 | 主要用途 | 使用场景区别 |
---|---|---|---|---|
Cookie | 存储在用户浏览器端的小型数据文件,用于跟踪和保存用户状态信息 | 浏览器端 | 保持用户登录状态、跟踪用户行为、存储用户偏好等 | 主要用于客户端状态的简单存储和追踪,适合单次会话的认证和状态管理 |
Session | 服务器端保存用户状态的机制,每个用户会话有唯一 Session ID | 服务器端(Session ID 通过 Cookie 保存在客户端浏览器) | 跟踪用户在服务器上的状态信息,如登录状态、购物车内容 | 用于服务器端的复杂状态管理,适合单次会话的认证和状态管理,尤其在需要存储大量会话数据时 |
Token | 本质为加密字符串,用于身份验证和授权,可包含用户信息和权限 | 客户端(浏览器或移动应用) | 认证后,客户端凭此访问服务端资源,用于验证用户身份或授权访问资源 | 用于无状态的认证和授权,尤其在分布式和跨域环境下,更适合跨会话的认证和状态管理 |
# Cookie
1991 年 HTTP 0.9 诞生,仅支持 GET 请求,用于浏览 web 文档,连接无关联,这是 HTTP 无状态的原因。
交互式 Web 兴起,单纯浏览无法满足需求,如网上购物需记录用户购物车等,于是 Cookie 诞生。
Cookie 是某些网站为辨别用户身份、进行 Session 跟踪,存储在用户本地终端的小型文本文件(常加密),由客户端计算机暂时或永久保存。
以加入购物车场景说明:每次浏览器向 server 发起请求,server 把本次商品 id 存于 Cookie 返回客户端,客户端本地保存该 Cookie。下次请求时,客户端将本地保存的 Cookie 传给 server,因每个 Cookie 保存着用户商品 id,所以购买记录不会丢失。
观察发现购物车内商品增多时,每次请求的 cookie 随之增大,给每个请求带来较大负担。例如添加一件商品,却需将历史商品记录随请求返回给 server,而购物车信息已记录在 server 端,浏览器此举看似多此一举。所以换一个思想就是把数据信息存储在服务端,那这就是 session 的实现方式了
# Session
因为用户购物车信息存于 Server,Cookie 只需保存识别用户身份的信息,明确加入购物车操作的发起者。每次请求时,Cookie 带上用户身份信息,请求体带上本次加入购物车的商品 id,以此大幅减小 Cookie 体积。这种识别请求所属用户的机制叫 Session(会话机制),其中生成的识别用户身份信息的字符串是 sessionId 。
- 用户登录,server 为用户生成一个 session 并分配唯一的 sessionId,该 sessionId 与特定用户绑定,可依据此 sessionId 查询对应的用户。随后,server 将此 sessionId 通过 cookie 传给浏览器。
- 之后浏览器每次发起添加购物车请求时,仅需在 cookie 里带上 “sessionId=abc” 这一键值对。
- server 接收到请求后,依据 sessionId 找到对应的用户,将传过来的商品 id 保存到 server 中该用户对应的购物车。
通过这种方式,可以大大减轻请求的负担,因为 cookie 中不需要保存所有购物车的商品 id 了,同时可以发现 cookie 是存储在客户端的,而 session 是保存在服务端的,但是 sessionid 也需要借助 cookie 传递。但是 session 就解决所有问题了么?
# Session 存在的问题
- cookie + session 方式看似解决问题,但该方式基于 server 单机工作的假设。
- 实际生产中,为保障高可用,服务器通常至少两台,通过负载均衡决定请求分配到哪台机器。
如图所示,根据现在网络业务量的增加,一般服务器都是集群部署,通过负载均衡配置来决定请求哪台服务器。
假设登录请求由 A 机器处理,A 机器生成 session 并通过在 cookie 中添加 sessionId 返回给浏览器。但在下次添加购物车时,若请求被 B 或 C 机器处理,由于 session 是 A 机器生成的,B、C 机器找不到 session,会出现无法添加购物车的错误,需要重新登录。这种情况下主要有三种解决方式。
# 1.Session 复制
通俗点来讲,就是把单点服务器上的 session 数据每个服务器都复制一份。
这样的话,无论请求到哪台服务器,由于服务器都存有 session 数据,就不会产生问题
虽然这样可以解决问题,但是同时也产生了额外的消耗
- 同一样的一份 session 保存了多份,数据冗余
- 如果节点少还好,但如果节点多的话,特别是像阿里,微信这种由于 DAU 上亿,可能需要部署成千上万台机器,这样节点增多复制造成的性能消耗也会很大。
# 2.Session 粘连
让客户端请求固定打到某一台机器,例如浏览器登录请求打到 A 机器后,后续添加购物车等所有请求也都打到此机器。Nginx 的 sticky 模块可支持此方式,包括按 ip 或 cookie 粘连等,像按 ip 粘连就是其中一种具体方式。配置如下
http { | |
upstream backend { | |
# 使用 ip_hash 方法实现按 IP 粘连 | |
ip_hash; | |
server 124.115.6.19; | |
server 124.115.6.20; | |
server 124.115.6.21; | |
} | |
server { | |
listen 80; | |
location / { | |
#配置请求转发到后端服务器组的名称 | |
proxy_pass ; | |
} | |
} | |
} |
在上述配置中:
upstream
块用于定义后端服务器组,ip_hash
指令告诉 Nginx 根据客户端的 IP 地址计算哈希值,将请求始终路由到同一台后端服务器。server
块中的listen 80
表示监听 80 端口。
虽然这样可以解决复制带来的额外的消耗,但是很明显可以看出一个问题就是加入一台服务器挂了怎么办,那不就还是会存在 sessionid 请求失败么。因为 session 本身不是去中心化的,所以我们可以通过中心化管理,也就是共享 session。
# 3. 共享 Session
这种方式也是目前各大公司普遍采用的方案,将 session 保存在 redis,memcached 等中间件中,请求到来时,各个机器去这些中间件取一下 session 即可。
该方案缺点在于每个请求都需到 redis 获取 session,增加一次内部连接从而消耗性能。此外,为保证 redis 高可用需搭建集群。不过正常来说 redis 集群化一般公司都会部署,所以这个方案也是目前很多公司的首选。那有没有不适用 session 实现去中心化管理的方案,有,就是接下来讲的 token。
# Token
token 把数据存储在服务端又改变为存储在客户端,只不过为了防止数据信息泄露,一般都是加密后的数据信息。具体流程如下
首先请求方输入自己的用户名,密码,然后 server 据此生成 token,客户端拿到 token 后会保存到本地,之后向 server 请求时在请求头带上此 token 即可。
为了防止 token 数据是随便输入的,服务端会有一套校验机制,校验 token 是否合法,同时 token 中携带了基本的数据信息。
哪如何校验 token 呢?
可以看到 token 主要由三部分组成
header:指定了签名算法
payload:可以指定用户 id,过期时间等非敏感数据
Signature: 签名,server 根据 header 知道它该用哪种签名算法,再用密钥根据此签名算法对 head + payload 生成签名,这样一个 token 就生成了。
当 server 收到浏览器传来的 token,会先提取 token 中的 header 和 payload,依据密钥生成签名并与 token 中的签名比对。若比对成功,表明签名及 token 合法。此外,payload 中存储有 userId,通过 token 能直接获取 userId,无需像 session 那样从 redis 获取,减少了开销。(这里需要注意,header, payload 实际上是以 base64 的形式存在的)
那么 token 就不存在问题了么?当然也会有问题
- 此方式有效避免 token 必须保存在 server 的弊端,实现分布式存储。
- token 由 server 生成后,直到过期都有效,难以直接让其失效。除非在 server 设黑名单,校验前检查,若在黑名单则失效,但这样黑名单需保存在 server,类似 session 模式。
- 一般做法是客户端登出时,在本地移除 token,下次登录重新生成。
- token 通常放在 header 的 Authorization 自定义头里,而非 Cookie,主要为解决跨域不能共享 Cookie 的问题。
通过上面可以了解到 token 生成后不能自主控制其有效期,因为其去中心化的特性,服务器难以管理其有效性,想要自主管理就需要在服务端添加黑名单校验,但这样就需要服务端存储黑名单,那不就又回到了 session 模式的方案了么?还有没有其他方案呢,那就是双 token 管理(后续讲解这个,我也在自己的项目实现过)。
# Cookie 的局限性
Cookie 跨站是不能共享的,这样的话如果你要实现多应用(多系统)的单点登录(SSO),使用 Cookie 来做需要的话就很困难了(要用比较复杂的 trick 来实现,有兴趣的话可以看文末参考链接)
所谓单点登录,是指在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
但如果用 token 来实现 SSO 会非常简单,如下
只要在 header 中的 authorize 字段(或其他自定义)加上 token 即可完成所有跨域站点的认证。
在移动端原生请求是没有 cookie 之说的,而 sessionid 依赖于 cookie,sessionid 就不能用 cookie 来传了,如果用 token 的话,由于它是随着 header 的 authoriize 传过来的,也就不存在此问题,换句话说 token 天生支持移动平台,可扩展性好。
# Token 的缺点
- token 长度问题:token 由 header、payload 编码构成,比 sessionId 长得多,易超出 cookie 大小限制(如 4kb)。token 内存储信息越多越长,每次请求都携带会给请求带来较大负担。
- 安全隐患:虽有观点认为 token 更安全,但实际并非如此。因太长不适合放 cookie,常存于 local storage,而本地存储可被 JS 直接读取,存在安全风险。且 token 生成后除非过期无法失效,服务端检测到安全威胁也无法使其失效。因此,token 更适用于一次性命令认证,并设置较短有效期。
# CSRF 攻击
这里扩展一下关于 Cookie 的一些不安全攻击,其实之前疫情期间,我也通过抓包自己给请求赋值 cookie 等方式模拟真实请求跳过登录验证持续进行抢菜。(这个不建议,小心喝茶),先通过开发者工具看下请求中的 cookie 样式了解一下。
- CSRF 攻击原理:攻击者欺骗用户浏览器访问用户已认证过的网站并执行操作。因浏览器保存有认证信息(如 cookie 中的 sessionId 等),被访问网站会误将其当作真实用户操作而执行。
- 举例说明:用户登录某银行网站(如
http://www.examplebank.com/
,转账地址为http://www.examplebank.com/withdraw?amount=1000&transferTo=PayeeName
),登录后 cookie 含 sessionid。攻击者在其他网站放置代码,如<img src="http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman">
,用户误点此图片,因同域名请求自动携带含 sessionid 的 cookie,server 会执行转账操作,带来严重安全风险。
- CSRF 攻击源于浏览器机制,相同域名请求会自动带上 cookie,因此有人认为 cookie 不安全。
- 使用 token 虽能避免 CSRF 问题,但 token 存于 local storage 可被 JS 读取,从存储角度也不安全,防护 CSRF 攻击应使用 CSRF token。
- 从存储角度看,cookie 和 token 都有暴露风险,所谓的安全更多强调传输安全,可通过 HTTPS 协议加密请求头,保证传输安全。
将 cookie 与 token 作比较不合理,应是 session 与 token 比较。
- session 和 token 本质均为用户身份认证机制,区别在于校验机制,session 保存在 server,通过 redis 等中间件获取校验;token 保存在 client,通过签名校验。
- 多数场景下 session 使用更合理,单点登录、一次性命令认证场景使用 token 更合适,应依不同业务场景合理选型。
# 双 token 方案
双 token 是为了解决 jwt 的续期问题的。由于 jwt 一颁布,就意味着在指定时间内能够通行。
- 如果给的有效期过长,风险是比较大的,服务器失去了掌控力。在这期间如果想让用户失效,或者是有人盗取了 token。都可以胡作非为好久。
- 如果给的有效期过短,用户经常需要重新登录,体验也很不好。
- 如果中心化管理用户状态,也就是每次解析 jwt token 之后,还需要去中心化比对能否通过。这样又违背了初衷。增加每次认证的耗时
双 token 分为 access_token
和 refresh_token
。一般 access_token
的有效期可以设置为 10 分钟, refresh_token
的有效期可以设置为 7 天。用户每次请求都用 access_token
,如果前端发现请求 401,也就是过期了,就用 refresh_token
去重新申请一个 access_token
。继续请求。
这里的关键在于, refresh_token
申请 access_token
的时候,用户是无感知的,前后端的框架自动去更新这个新的 access_token
。
还有一个点在申请 access_token
的时候,后端这时候会去校验用户的状态等问题,如果发现用户被禁用了,就申请不到 token
了。
# 网络角度来看,用户从输入网址到网页显示,期间发生了什么?
发送方:
- 应用层调用
Socket API
将数据包放入Socket
发送缓冲区。 - 网络协议栈从发送缓冲区取出数据包,按
TCP/IP
栈从上到下处理。 - 传输层增加
TCP
头。 - 网络层增加
IP
头、路由查找、按MTU
分片。 - 数据链路层进行物理地址寻址、添加帧头帧尾后放发包队列。
- 驱动程序通过
DMA
从发包队列读出网络帧并由网卡发送。
接收方:
- 网卡通过
DMA
将网络帧放收包队列,通过硬中断告知中断处理程序。 - 网卡中断处理程序为网络帧分配
sk_buff
并拷贝,通过软中断通知内核。 - 内核协议栈从缓冲区取出网络帧,从下到上处理。
- 数据链路层检查报文合法性、去帧头帧尾交网络层。
- 网络层取出
IP
头判断走向,确认发往本机则去IP
头交传输层。 - 传输层取出
TCP
或UDP
头,根据四元组找Socket
并拷贝数据到接收缓存。 - 应用层用
Socket
接口读取新接收的数据。