跳到主要内容

4 篇博文 含有标签「http」

查看所有标签

· 阅读需 9 分钟

参考文章:WebSockets vs Server-Sent-Events vs Long-Polling vs WebRTC vs WebTransport

0 轮询​

数据流向:client <= server

基于传统的 HTTP 请求-响应方案,定时频繁地向服务器请求最新数据,实现伪实时通信

优点:

  • 实现简单,不需要特殊的服务器端支持。

  • 兼容性好,几乎所有的环境都支持。

缺点:

  • 高频请求会增加服务器和网络的负载。

  • 存在明显的延迟,无法真正实现实时通信。

适用场景:数据更新频率低且实时性要求不高的场景,如定期数据同步。

1 长轮询(Long-Polling)​

数据流向:client <= server

客户端建立与服务器的连接,该连接在新数据可用之前保持打开状态。一旦服务器获得新信息,它就会将响应发送到客户端,并关闭连接。在收到服务器的响应后,客户端会立即启动一个新请求,并重复该过程

优点:

  • 比传统轮询更高效,减少不必要的网络流量和服务器负载。

  • 实时性更高,能够更及时地获取数据更新。

缺点:

  • 仍然会导致一定的通信延迟。效率低于 WebSockets 等其他实时技术

  • 在高并发情况下,服务器需要处理大量的打开和关闭连接的操作。

示例:

// js 客户端长轮询
function longPoll() {
fetch('http://example.com/poll')
.then(response => response.json())
.then(data => {
console.log("Received data:", data);
longPoll(); // 马上建立一个新的长轮询
})
.catch(error => {
/**
* 当连接超时或客户端离线,请求会抛出异常,此时可以尝试一段时间后再次发起长轮询
*/
setTimeout(longPoll, 10000);
});
}
longPoll(); // 执行长轮询,建立初始连接

适用场景:中等实时性要求的场景,如通知系统、消息推送等。

2 WebSocket​

数据流向:client <=> server

WebSocket 为客户端和服务器之间的单一长期连接提供全双工通信通道,允许双方在建立连接后独立互相发送数据

示例:

// js 客户端 websocket 连接
const socket = new WebSocket('ws://example.com');

socket.onopen = function(event) {
console.log('Connection established');
// 向服务器发送一条消息
socket.send('Hello Server!');
};

// 接收服务器响应的消息
socket.onmessage = function(event) {
console.log('Message from server:', event.data);
};

优点:

  • 实时性高,低延迟。
  • 全双工通信,支持双向数据传输。
  • WebSocket API 简单易用。

缺点:

  • 连接不稳定时需要处理重新连接和断线重连。
  • 需要额外的心跳检测(ping-pong)来保持连接的活跃性。

适用场景:

  • 需要低延迟和高频数据更新的场景,如实时聊天、游戏、金融交易平台等。

推荐使用 Socket.IO 这样的 WebSockets 之上的库,不仅可以处理以上这些复杂情况,甚至在需要时提供对长轮询的回退。

3 Server-Sent-Events​

数据流向:client <= server

服务器发送事件(SSE)提供了一种通过 HTTP 将服务器更新推送到客户端的标准方法,保持连接并持续发送数据。可以将 SSE 视为单个 HTTP 请求,其中服务端不会一次发送整个正文,而是保持连接,然后像水流一样一点一点返回响应数据

优点:

  • 简单易用,浏览器原生支持。
  • 自动处理重新连接。
  • 适用于单向数据流的场景。

缺点:

  • 仅支持服务器到客户端的单向通信。
  • 不如 WebSocket 灵活,不能进行双向通信。

适用场景:

  • 需要实时更新客户端而无需向服务器发送数据的场景,如实时新闻提要、体育比分等。
  • 与 WebSocket 不同,SSE 专为服务器到客户端的单向通信而设计,非常适合实时新闻提要、体育比分、AI 聊天等需要实时更新客户端而无需向服务器发送数据的场景。

示例:

  1. EventSource

在浏览器上可以使用 URL 初始化 EventSource 实例

// 连接服务端 sse 接口
const evtSource = new EventSource("https://example.com/events");

// 处理数据
evtSource.onmessage = event => {
console.log('got message: ' + event.data);
};

与 WebSocket 不同,EventSource 将在连接丢失时自动重新连接。

  1. fetch
async function getAiResponse() {
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
content: '这是我的问题'
})
})

const reader = res.body.getReader()
const textDecoder = new TextDecoder()
while(1) {
const {done, value} = await reader.read()
if(done) {
break
}
const str = textDecoder.decode(value)
console.log(str)
}
}
  1. 服务端

在服务器端,您的脚本必须将 Content-Type 标头设置为 text/event-stream,并根据 SSE 规范设置每条消息的格式。这包括指定事件类型、数据有效负载和可选字段,如事件 ID 和重试时间。

下面以 Express 为例,介绍如何设置简单的 SSE 接口:

import express from 'express';
const app = express();
const PORT = process.env.PORT || 3000;

app.get('/events', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});

const sendEvent = (data) => {
// 全部 message 必须加上 'data:'
const formattedData = `data: ${JSON.stringify(data)}\n\n`;
res.write(formattedData);
};

// 没两秒发送一次事件,模拟流式数据
const intervalId = setInterval(() => {
const message = {
time: new Date().toTimeString(),
message: 'Hello from the server!',
};
sendEvent(message);
}, 2000);

// 当连接关闭后,做些清理工作
req.on('close', () => {
clearInterval(intervalId);
res.end();
});
});
app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`));

4 WebTransport API​

数据流向:client <=> server

WebTransport 利用 HTTP/3 QUIC 协议实现 Web 客户端和服务器之间的高效、低延迟通信,例如以可靠和不可靠的方式通过多个流发送数据,甚至允许数据无序发送。

优点:

  • 高效的低延迟通信。
  • 支持多流和无序数据传输。

缺点:

  • API 较复杂,开发难度较大。可以等第三方库
  • 浏览器支持不广泛,目前 Safari 和 Node.js 不支持。

适用场景:

  • 实时游戏、直播、协作平台等需要高效低延迟通信的场景。

5 WebRTC​

数据流向:client <=> client

WebRTC(Web Real-Time Communication)是一个开源项目和 API 标准,可在 Web 浏览器和移动应用程序中启用实时通信(RTC)功能,支持点对点连接

优点:

  • 支持音频、视频和数据流的点对点通信。
  • 实现低延迟的实时通信。

缺点:

  • 需要信令服务器来建立连接。
  • 复杂度高,需要处理 NAT 穿透和防火墙问题。

适用场景:

  • 视频会议、实时音视频聊天、P2P 文件传输等需要实时点对点通信的场景。

· 阅读需 3 分钟

在更新接口联调时,前端还在纠结入参是传全部还是只传改动过的值?

后端服务还在 get post 一把梭?

http 早就为开发者设计好更新接口的策略了:

  • patch 是部分更新,没带的参数就忽略

  • put 是全量覆盖,没带的参数就删除或置空

理论上增删改查应该去根据 http 动词选择更新策略,但是目前大多数的后端设计仍然是一个接口一个接口手写

再次夸赞 Rails,非常舒服的 RESTful 接口路由定义

Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resource :session, only: [:create]
resources :items, only: [:create, :index, :destroy]
resource :me, only: [:show]
# 也支持单独定义接口与方法的映射
# post :validation_codes, to: 'validation_codes#create'
end
end
end

在 Rails 中,路由资源提供了 HTTP 动词和 URL 到控制器操作之间的映射。按照惯例,每个操作还映射到数据库中的特定 CRUD 操作。路由文件中的单个条目,例如我们在 routes 中定义了下面的路由:

resources :photos

Rails 会在应用程序中创建如下 7 种不同的路由,所有路由都映射到 Photos 控制器:

HTTP VerbPathController#ActionUsed for
GET/photosphotos#indexdisplay a list of all photos
GET/photos/newphotos#newreturn an HTML form for creating a new photo
POST/photosphotos#createcreate a new photo
GET/photos/:idphotos#showdisplay a specific photo
GET/photos/:id/editphotos#editreturn an HTML form for editing a photo
PATCH/PUT/photos/:idphotos#updateupdate a specific photo
DELETE/photos/:idphotos#destroydelete a specific photo

· 阅读需 8 分钟

本文讨论浏览器的同源策略,跨域的常见解决方案及其原理

浏览器的同源策略​

同源策略只存在于浏览器环境中,是浏览器的重要安全策略之一

其中,源 = 协议 + 主机 + 端口,两个源相同,称之为同源,反之则称为跨域或跨源

同源策略是指若页面的源(地址栏)和页面运行过程中加载的源(link、script、ajax)不一致时,处于安全考虑,浏览器会对跨域的资源访问进行一些限制

主要通过以下三种方式解决跨域的问题

  • 代理
  • CORS
  • JSONP

无论哪种方式,都是在想办法让浏览器知道,我这次跨域的请求是自己人,就不要拦截了

对于具体方案的选择,参考下图

image-20240515160743964

代理​

代理适用的场景是:生产环境不发生跨域(前端服务和后端服务在一起),但开发环境发生跨域(前后端分离开发),主要解决前端开发环境的跨域问题

思路:将后端服务改成自己的开发服务器,这样一来浏览器就会允许这个 ajax 请求。然后开发服务器再将这个请求转发给后端服务器即可

比如浏览器打开 localhost:8080 页面,接口请求了 localhost:8080/api,满足同源策略,浏览器会成功(向本地开发服务器)发出这个请求。当开发服务器收到请求后,会帮我们改成正确的地址去访问

即让(开发)服务器与(后端)服务器交互,从而绕开浏览器的同源限制

打包工具都自带这个功能,配置一下即可(devServer.proxy/server.proxy)

// webpack
module.exports = {
devServer: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8000', // 你想要代理到的目标 API 地址
pathRewrite: {'^/api': ''}, // 可选,重写路径
secure: false, // 如果是https接口,需要配置这个参数
changeOrigin: true, // 如果接口跨域,需要进行这个参数配置
}
}
}
};

// vite
export default defineConfig({
server: {
port: 3000,
proxy: {
// 选项写法
'/api': {
target: 'http://localhost:8000', // 后端服务实际地址
changeOrigin: true, // 必须设置为true
rewrite: (path) => path.replace(/^\/api/, '') // 重写请求路径
},
// 简写写法
'/foo': 'http://localhost:4567/foo',
// 正则表达式写法
'^/fallback/.*': {
target: 'http://jsonplaceholder.typicode.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/fallback/, '')
},
}
}
})

CORS​

跟前端没关系,主要是后端的处理

CORS 是基于 http 1.1 的一种跨域解决方案,它的全称是 Cross-Origin Resource Sharing 跨域资源共享

如果浏览器要跨域访问服务器的资源,需要获得服务器的允许

针对不同的请求,CORS 规定了三种不同的交互模式,分别是

  • 简单请求
  • 需要预检的请求
  • 附带身份凭证的请求

简单请求​

当请求同时满足以下条件时,浏览器会认为它是一个简单请求

  • 请求方法属于 GET、POST 或 HEAD

  • 请求头仅包含安全的字段(无自定义),例如 Accept、Content-Type、Accept-Language 等

  • 请求头如果包含 Content-Type,仅限如下值之一:text/plain、multipart/form-data 或者 application/x-www-form-urlencoded

浏览器对于简单请求,会做如下处理

  1. 给请求头自动添加 Origin 字段
  2. 服务器响应头应包含 Access-Control-Allow-Origin 字段,该字段包括如下的值
    1. * 表示全部源都可以访问
    2. 具体的且可以访问此服务器的源

需要预检的请求​

如果浏览器识别出该请求不属于简单请求,就会先发送 OPTIONS 预检请求

  1. 浏览器发送预检请求,询问服务器是否允许
OPTIONS /api/user HTTP/1.1
Host: crossdomian.com
...
Origin: http://my.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: a, b, content-type

预检请求有如下特征:

  • 请求方法为 OPTIONS
  • 没有请求体
  • 请求头包含
    • Origin:请求的源
    • Access-Control-Request-Method:后续真实请求将使用的请求方法
    • Access-Control-Request-Headers:后续真实请求会改动的请求头
  1. 服务器允许
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://my.com
Access-Control-Allow-Method: POST
Access-Control-Allow-Headers: a, b, content-type
Access-Control-Max-Age: 86400

服务器允许的情况下,只需要在响应头中添加:

  • Access-Control-Allow-Origin: 和简单请求一样,表示允许的源
  • Access-Control-Allow-Method: 表示允许的后续真实的请求方法
  • Access-Control-Allow-Headers: 表示允许改动的请求头
  • Access-Control-Max-Age: 告诉浏览器,多少秒内,对于同样的请求源、方法、头,都不需要再发送预检请求了

后续发送真实请求的处理与简单请求相同

附带身份凭证的请求​

默认情况下,ajax 的跨域请求并不会附带 cookie,某些需要权限的操作就无法进行

这个问题可以通过简单的配置来解决

// xhr
xhr.withCredentials = true
// fetch
fetch(url, { credentials: 'include' })

此时,该请求就被浏览器识别为一个附带身份凭证的请求,会在预检请求头中的 Access-Control-Request-Headers 添加 cookie 或者 Authorization 字段

当服务器响应附带身份凭证的请求时,需要明确告知客户端,服务器允许这样的凭据

告知的方式是在预检响应头中添加 Access-Control-Allow-Credentials: true 即可

需要注意的是,对于这种请求,服务器不得设置 Access-Control-Allow-Origin: *

补充​

在跨域访问时,JS 只能拿到一些最基本的响应头,例如 Cache-Control,Content-Type,Expires 等

服务器可以通过设置 Access-Control-Expost-Headers: authorization, a, b 响应头使 JS 能够访问这些特殊响应头

JSONP​

当需要跨域请求时,不使用 AJAX,转而生成一个 script 元素去请求服务器,由于浏览器并不阻止 script 元素的请求,这样请求可以到达服务器

  1. 本地准备好一个 callback 函数,相当于 ajax 的回调函数

  2. 生成一个 script 标签,该 src 为服务器的地址

  3. 服务器响应一段 js 代码,将数据作为参数传入回调中,相当于执行了 callback 函数

  4. 相当于间接的把数据传递给客户端

JSONP 只支持 GET 请求

· 阅读需 5 分钟

TCP 三次握手​

  • 第一次 客户端向服务端发送连接请求

客户端向服务端发送连接请求报文段。该报文段中包含自身的数据通讯初始序号。请求发送后,客户端便进入 SYN-SENT 状态。

  • 第二次 服务端确认接收客户端的连接请求,并向客户端发送连接请求

服务端收到连接请求报文段后,如果同意连接,则会发送一个应答,该应答中也会包含自身的数据通讯初始序号,发送完成后便进入 SYN-RECEIVED 状态。

  • 第三次 客户端确认接收服务端的连接请求,连接建立成功

当客户端收到连接同意的应答后,还要向服务端发送一个确认报文。客户端发完这个报文段后便进入 ESTABLISHED 状态,服务端收到这个应答后也进入 ESTABLISHED 状态,此时连接建立成功。

image-20231219170511166

TCP 四次挥手​

  • 第一次

若客户端 A 认为数据发送完成,则它需要向服务端 B 发送连接释放请求。

  • 第二次

B 收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,此时表明 A 到 B 的连接已经释放,不再接收 A 发的数据了。但是因为 TCP 连接是双向的,所以 B 仍旧可以发送数据给 A。

  • 第三次

B 如果此时还有没发完的数据会继续发送,完毕后会向 A 发送连接释放请求,然后 B 便进入 LAST-ACK 状态。

PS:通过延迟确认的技术(通常有时间限制,否则对方会误认为需要重传),可以将第二次和第三次握手合并,延迟 ACK 包的发送。

  • 第四次

A 收到释放请求后,向 B 发送确认应答,此时 A 进入 TIME-WAIT 状态。该状态会持续 2MSL(最长报文段寿命,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有 B 的重发请求的话,就进入 CLOSED 状态。当 B 收到确认应答后,也便进入 CLOSED 状态。

image-20231219170654861

TCP 泛洪攻击​

我们已经知道,TCP 只有经过三次握手才能连接,而 SYN 泛洪攻击就是针对 TCP 握手过程进行攻击:

  • 攻击者发送大量的 SYN 包给服务器(第一次握手成功)
  • 服务器回应(SYN + ACK)包(第二次握手成功)
  • 但是攻击者不回应 ACK 包(第三次握手不进行)

导致服务器存在大量的半开连接,这些半连接可以耗尽服务器资源,使被攻击服务器无法再响应正常 TCP 连接,从而达到攻击的目的

幸运的是,一种称为 SYN cookie 的有效防御现在已部署在大多数主要的操作系统中:

  • 在客户端发送 SYN 报文给服务器(第一次握手),服务端收到连接请求报文段后,服务器不会为此SYN创建半开连接,而是生成一个序列号(所谓的 cookie)一起发送给客户端(第二次握手),在这个阶段,服务器不会为该连接分配任何资源
  • 客户端返回 ACK 报文给服务器(第三次握手),服务器会验证这个 cookie 值,只有验证成功才创建 TCP 连接,分配资源
  • 如果客户端没有返回 ACK 报文给服务器,也不会对服务器造成任何的伤害,因为服务器没有分配任何资源给它