跳到主要内容

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 规定了三种不同的交互模式,分别是

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

简单请求

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

  • 请求方法属于 GETPOSTHEAD

  • 请求头仅包含安全的字段(无自定义),例如 AcceptContent-TypeAccept-Language

  • 请求头如果包含 Content-Type,仅限如下值之一:text/plainmultipart/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 报文给服务器,也不会对服务器造成任何的伤害,因为服务器没有分配任何资源给它