HTTP , Websocket

218 阅读10分钟

Http历史

http 0.9

这个版本的http协议只有请求行。 接着服务器返回一个HTML文件。采用ASCII编码

  • 极其简单:只支持GET方法
  • 只有请求行:GET /index.html(没有HTTP版本号)
  • 没有请求头:不能指定接收格式等信息
  • 只返回HTML:无法传输其他类型文件

http 1.0

这时候开始,协议加入了请求头请求体,例如可以在请求头中指定发送文件的格式,接收文件的格式。并且引入了状态码。

  • 新增方法:增加了POST和HEAD方法
  • 引入状态码:如200(成功)、404(未找到)等
  • 支持多种文件类型:通过Content-Type指定

http1.1

这时用户量激增,每次请求都需要建立握手挥手的过程,太麻烦了。于是引入了长连接机制,能够多次请求复用同一个连接。

还引入了管线化机制,多次请求可以同时按顺序发送,不用等前一个请求的响应完成,但是接收响应必须按照发送请求的顺序来接收,这样就会导致队头堵塞问题。

流式输出:设置 'Content-Type': 'text/plain', 'Transfer-Encoding': 'chunked'

HTTP2.0

采用了二进制分帧,将请求分成很多个二进制帧,分为首部帧和数据帧。这样虽然传输的总时长不变,但是更小的请求能更快到达。每个帧有一个并且多个请求的请求头往往高度一致,采用了头部压缩算法,减少传输体积。

多路复用机制:

单一的TCP连接被分为多条数据流,每个帧有标识。接收方根据表示重新组装成整的数据。

  • 一条高速公路有多个双向车道
  • 每个车道都被分成两部分:上行道和下行道
  • 上行道的车流不会影响下行道的车流
  • 不同车道之间也互不影响

还有一个服务器主动推送机制。服务器可以主动推送多个文件给客户端。

HTTP 3.0

  • 基于UDP的QUIC协议,而非TCP
  • 改进的多路复用,彻底解决队头阻塞
  • 更快的连接建立(0-RTT)
  • 内置加密(TLS 1.3)

常用HTTP响应码

  • 100 用于接收到请求头,客户端可以继续发送请求体。

  • 101 协议升级 用于websocket或HTTP/2升级

  • 102 接收到了正在处理,防止客户端超时

  • 200 成功

  • 204 请求成功,没有返回体,如预检请求。

  • 301 资源移动到新的url

  • 304 资源未更改

  • 307 临时重定向

  • 308 永久重定向

  • 400 请求参数错误

  • 401 未授权

  • 404 notFound

  • 500 服务器代码异常

  • 505 客户端使用过旧或过新HTTP版本

TLS

最开始内容是明文处理,这时候任何人都能对内容进行处理。后来就引入了对称加密。我们先把密钥发出去,接着密钥给内容上锁,服务器用密钥解锁。但是中间人也能拦截密钥,用密钥解锁,整个过程如同虚设。

非对称加密: 服务器先把公钥发过去。这时候客户端使用公钥对内容加密,再发送给服务器。中间人用公钥打不开,服务端有私钥可以解密内容。

但是攻击者可以掉包服务端的公钥,把自己的公钥替换成服务器的,这时候客户端以为是服务端的公钥,毫不犹豫的加密,攻击者就可以用自己的私钥解密,同时用服务端的公钥加密内容返回给服务器,这就是中间人攻击。

服务端把自己的域名,公钥等信息发送给CA机构,CA审核后用非对称加密的方式加密,加密的内容就变成了证书,之后肉身送给服务端(是通过电话,电子邮件,物理邮件等多种信道降低中间人攻击的风险),又把CA公钥运送给客户端。

HTTP缓存

浏览器首先会去检查有没有强缓存,没有再去检查协商缓存。

强缓存

当服务器返回响应头 Expires,Cache-Control中的一个,会触发强缓存。ctrl + f5 强制刷新。 Expires是设置缓存日期,cache-Control 可以设置缓存时间max-age ,no-cache 等。因为可能会出现服务器与客户端时间不一致的情况。 paragma: 设置no-cache 跟上面的效果一致,不过优先级最高。

image.png

协商缓存

就是客户端去询问一下资源有没有变化。服务器对比,不变则返回304状态码更改则返回更改后的资源。 ETags(优先级高) 根据文件内容生成hash值。 Last-modified 文件最后修改时间

当文件修改频率为秒级别,这样会出现漏判,并且当文件修改但是内容不发生变化,也会失去缓存。

  1. ETag类型:
  • 强ETag:ETag: "abc123"(字节级完全匹配)
  • 弱ETag:ETag: W/"abc123"(语义上相同即可)
  1. 请求头匹配:
  • ETag对应If-None-Match请求头
  • Last-Modified对应If-Modified-Since请求头

Websocket

websocket 是全双工通信协议,他是基于基于TCP协议的。协议标识符为ws,如果加密就为wss。

大家可能会想,http2也能进行主动推送啊,有什么区别吗?

  1. 服务器只能在收到客户端初始请求后才能推送相关资源,不能完全主动发起通信。
  1. 推送内容有限:主要用于推送静态资源(如CSS、JS文件),不适合推送动态生成的数据
  1. 缺乏真正的实时性:不是为持续的实时通信设计的

连接过程

客户端发起http请求,进行三次握手。

发送的信息中存在协议升级的信息。例如connection:upgrade upgrade:websocket websocket-version

客户端收到信息后返回http协议:101状态码表示协议升级,

心跳检测

步骤: 客户端他会去向服务端发送一个数据包,同时设置一个定时器 服务器向客户端返回一个数据包。 假如客户端到定时器到时了还没收到数据包,就证明连接断开了。 假如用户收到了消息,就把旧的定时器清除。

  • 建议值:20-60秒,视应用场景而定
  • 尽量小:减少网络负担

已经有了onclose和onerror 事件,仍然需要心跳检测,例如防止网络突然中断:双方无法进行tcp关闭。

断线重连

当监听到onclose事件onerror或者心跳检测不到,这时候就会进行断线重连。我们通常会设置重连的次数,以及重连时间间隔(退避指数算法)。接着自己去进行数据的恢复,例如给每条信息设置ID,接着根据ID继续上传,确认消息的幂等性,安全性。

SSE

SSE本质上是HTTP1.1流式输出的一种标准化格式。他规定了我们的消息格式,响应头,客户端处理方式。他有点像是HTTP规定了TCP传输内容的格式。

服务器设置一下响应头即可:

     'Content-Type': 'text/event-stream',
     'Cache-Control': 'no-cache',
     'Connection': 'keep-alive'

客户端使用 EventSource来进行进行监听。 EventSource 内置了自动重连机制。 例如监听onMessage消息。

AI流式输出

接口调用中开启stream,这时候openAI服务器给你返回的数据就会带上响应头'Transfer-Encoding': 'chunked'。这时候服务器会创建一个异步迭代器,我们使用await for of 来获取到每个块数据,将数据发送给客户端。可以通过SSE,websocket。

import OpenAI from 'openai';

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

async function streamCompletion(req, res) {
  // 设置正确的响应头以支持流式输出
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  try {
    const stream = await openai.chat.completions.create({
      model: 'gpt-3.5-turbo',
      messages: [{ role: 'user', content: '讲个故事' }],
      stream: true, // 开启流式输出
    });

    for await (const chunk of stream) {
      // 将每个块发送到客户端
      const content = chunk.choices[0]?.delta?.content || '';
      if (content) {
        res.write(`data: ${JSON.stringify({ content })}\n\n`);
      }
    }
    
    // 结束流
    res.write('data: [DONE]\n\n');
    res.end();
  } catch (error) {
    console.error('流式输出出错:', error);
    res.status(500).send('服务器错误');
  }
}

问题:

  1. WebSocket的关闭握手(Close Handshake)是如何工作的?不同的关闭码(Close Code)代表什么?

任一方(客户端或服务器)可以通过发送包含关闭码和可选关闭原因的关闭帧来启动关闭

确认关闭:接收方收到关闭帧后,会回复一个关闭帧作为确认

终止TCP连接:完成关闭帧交换后,发起方负责关闭底层TCP连接

完成关闭:接收方检测到TCP连接关闭后,也关闭自己的连接

  1. 如何实现WebSocket的身份验证?

websocket只有在最开始握手使用http协议,有请求头。后续是没有请求头的。我们可以通过先使用http进行身份验证,然后进行协议升级。或者URL直接传递token过去。

  1. 跨域WebSocket连接是如何处理的?与HTTP CORS有何不同?

在握手阶段跟HTTP请求完全一致,

利用这个时间进行检查。 但是后续的消息发送不会跨域。

  1. WebSocket连接和代理服务器。

代理服务器确实同时与客户端和服务器建立两个TCP连接,然后在这两个连接之间转发数

  1. WebSocket是全双工通信,这和HTTP/2的多路复用有什么本质区别?

全双工通信是同一个tcp连接有来回两条信道,请求和响应互不影响,就像汽车的双行道。多路复用是一个tcp连接创建多个数据流,实现并行通信,就像有多条赛道,每条赛道有着双行道。但是仍然遵守着请求响应模式。

  1. 请详细解释SSE和WebSocket的工作原理以及它们在流式输出场景下的主要区别。

SSE的数据格式以特定格式的文本进行发送,websocket的数据是以二进制帧发送,支持文本和二进制数据。

  1. 在什么场景下选择SSE,什么场景下选择WebSocket?你如何理解这两种技术在HTTP协议层面的本质区别?

websockt协议是不同于HTTP协议的一种协议,最开始通过HTTP协议进行握手,然后升级到websocket协议。而SSE本质上还是HTTP协议,它相当于是HTTP1.1流式输出的一种格式协议,特殊应用模式。

  1. 在使用SSE实现流式输出时,你是如何处理连接断开重连的?客户端如何恢复之前的会话状态

SSE最强大的特性是通过Last-Event-ID头实现状态恢复,这允许客户端告诉服务器上次接收到的最后一个事件,服务器可以从该点继续发送。

  1. SSE是单向通信,WebSocket是双向通信,在AI对话这种看似单向输出的场景中,为什么有时仍需要WebSocket?

用户可能在看到不满意要求时立即停止生成内容,可能需要实时修改内容。

  1. WebTransport作为新兴技术可能替代WebSocket,你对此有何看法?在你的场景中是否适用? webTransport基于 http3和QUIC协议。提供了更低的延迟和更高的并发能力,还支持不可靠传输。

  2. 针对以下状态码,解释它们的具体含义和使用场景:401 vs 403、307 vs 308。

401(未授权) vs 403(无权访问)、307(内容暂时迁移) vs 308(内容永久迁移)。

  1. 在实际项目中,你如何优化缓存策略来提高性能同时确保内容及时更新?
静态资源(图片,css,js)通过长期的强缓存来设置,HTML文档通过协商缓存,API响应根据数据变化合理设置强缓存。