大家好,欢迎来到 JS 脚本语言的第八课。上课之前,我们先花几分钟,把前两节课的内容串一下。
前面两节课我们讲了 HTTP 请求里常见的方法、头部以及一些附加参数。第七课说到 HTTP 请求的附加信息时,我们用了去餐厅点菜来打比方:你走进一家餐厅,服务员要知道你从哪来、用什么设备来、是不是本人,这里对应到网络请求里,就是各种请求头和校验信息。
Referer 有点像“你是从哪看到我们餐厅的”。比如你从某个网页点进来,请求里可能带上来源地址,服务端就能判断这张图片是不是被别的网站偷偷引用了,这就是常说的防盗链。
User Agent 像是一张“名片”,告诉对方你大概是用什么设备、什么浏览器来的。服务端会参考它做页面适配、兼容处理,或者做一些简单识别。不过这张名片是可以被改写的,所以不能只靠它判断一个人是不是真实用户。
CSRF token 则像一枚“防伪暗号”。它不是为了证明你是谁,而是防止别人冒用你的登录状态,伪造一个看起来合法的请求去点菜。服务端拿到这个暗号,再和之前给你的对一下,就能降低跨站请求伪造的风险。
接下来进入今天的主题。今天我们重点看 WebSocket 是怎么建立连接的,以及它建立之后怎么传参数。别看名字里有 Web,它和普通 HTTP 的一问一答并不完全一样。
先说“流式传输”。你可以把它理解成水管不是接满一桶再给你,而是边流边用。HTTP 里也有一些类似效果的做法,常见的有两种:一种是 SSE,一种是 Partial Content。
SSE 像收听广播电台。你打开收音机,和电台建立一条连接,之后电台可以持续把消息推给你。你这边主要是听,服务器单方向地发,直到连接断开。它很适合做通知、进度更新、简单推送这类场景。
Partial Content 更像看连载小说或者断点续传。客户端会告诉服务器:“我这次先要第几段到第几段。”服务器就把这一段发回来。下次想接着看,再按范围要下一段。它不一定要求一直占着同一条连接,重点在于按范围取一部分内容,所以下载大文件、视频拖动进度条、断点续传时经常会碰到它。
理解这两种之后,再看 WebSocket 就清楚了。WebSocket 像打电话或者用对讲机,双方可以随时说话。它经常被拿来和 SSE 比较,但它不是简单地在 HTTP 通道里一直塞数据,而是先借一次 HTTP 请求完成“协议升级”。也就是说,WebSocket 本身没有像 HTTP 那样的 GET、POST 方法。真正建立连接时,借用的是一次 HTTP GET 请求,核心在请求头里的 Upgrade 和 Connection 这些字段,意思是:“我想把这条连接换个聊天方式。”
一次典型的 WebSocket 握手,也就是拨号过程,大概像下面这样:
GET /chat?room=1001 HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://example.com
Sec-WebSocket-Protocol: chat, json
服务端如果同意升级,就会返回:
HTTP/1.1 101 Switching Protocols
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
这里的 101 Switching Protocols 很关键,它表示这条连接已经从 HTTP 协议切换成了 WebSocket 协议。就像双方对上了暗号,正式切到专属的电话频道。后面的通信就不再是 HTTP 那种一问一答的请求和响应,而是双向的 WebSocket 帧。
什么是帧?你可以把它理解成打电话时传递的一个个“语音包”。客户端发往服务端的帧,要按协议做一层掩码。别把它想成加密,它更像是给数据套了一层随机外衣,主要是避免中间代理把 WebSocket 数据误当成普通 HTTP 数据缓存下来。服务端发回客户端的帧,按协议则不需要做这层掩码。
语音包里面会有 opcode,也就是操作码,用来区分里面装的是什么东西:是文字,就是文本帧;是图片、文件等,就是二进制帧;是要挂电话了,就是关闭帧;还有“喂,还在吗”的 ping 和 pong 心跳帧。文本帧一般传 JSON 字符串,二进制帧可以传 ArrayBuffer、Blob,或者图片、音频等数据。
那 WebSocket 怎么传参呢?也就是电话接通后,怎么把具体信息告诉对方。浏览器里的 WebSocket API 非常简单:
const ws = new WebSocket(url, protocols);
它不像 fetch 或 XMLHttpRequest 那样能让你自由配置完整的请求头,所以浏览器端的传参方式主要有下面几种。
第一种,把参数放在 URL 查询字符串里:
const ws = new WebSocket('wss://example.com/ws?room=1001&token=abc');
这种方式最直接,就像把房间号和密码直接写在电话号码里。但要注意,URL 可能会进入服务端日志、代理日志、监控系统,所以不要长期把敏感 token 放在 URL 里,容易泄露。
第二种,通过子协议传参:
const ws = new WebSocket('wss://example.com/ws', ['chat.v1', 'json']);
服务端会在 Sec-WebSocket-Protocol 中选择一个它支持的协议返回。这个方式更适合做版本协商和消息格式约定,比如约定好:“我们接下来用 chat.v1 这套格式聊天。”它不适合塞敏感数据,也不适合把很长的 JSON 硬塞进去。
第三种,利用 Cookie。同源情况下,浏览器会在 WebSocket 握手请求中自动带上 Cookie,所以服务端可以用 Session 做身份识别。这就像你进了同一个小区的门,保安自动认出了你的业主卡。但跨站场景下要特别注意 SameSite、Origin 校验和 CSRF 风险,不能觉得有 Cookie 就一定安全。
第四种,也是最灵活、最推荐的一种,是在连接建立后发送第一条业务消息:
const ws = new WebSocket('wss://example.com/ws');
ws.onopen = () => {
ws.send(JSON.stringify({
type: 'auth',
token: 'abc',
room: 1001,
since: 0
}));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
console.log('收到消息:', msg);
ws.onclose = (event) => {
console.log('连接关闭:', event.code, event.reason);
这种方式可以把鉴权、订阅频道、同步历史消息等复杂参数一次性发给服务端,结构清晰,也方便扩展。就像电话接通后,你先开口说:“你好,我是某某,我的密码是 abc,我要进 1001 房间。”
如果你是在 Node.js 里使用 ws 这类库,那么还可以在握手阶段自定义请求头:
const WebSocket = require('ws');
const ws = new WebSocket('wss://example.com/ws', {
headers: {
Authorization: 'Bearer abc'
}
});
但要注意,不同 WebSocket 库的 API 不一样,浏览器原生的 WebSocket 构造函数是不支持自定义请求头的。所以实际项目里,常见的做法是:握手阶段用 Cookie 或子协议做粗粒度身份和版本识别,连接成功后再用第一条消息做详细鉴权和业务参数传递。
同时还要考虑心跳保活,也就是定期问一句“还在吗”;断线重连,也就是断了自动重拨;消息顺序和重复发送,尤其是在重连之后,要避免重复处理或者漏掉消息;还有鉴权过期、服务端主动踢下线等问题。只有把握手、帧和业务参数分层设计好,WebSocket 才不会变成一条只会“裸奔”的长连接。
今天的课程内容就是这些。下节课开始,我们就要进入抓包教程。我会教大家如何使用抓包工具抓包,抓出来的请求该如何分析,以及如何用同样的请求去发起请求。那今天的课程就到这里,下课。
目录
大家好,欢迎来到 JS 脚本语言的第八课。上课之前,我们先花几分钟,把前两节课的内容串一下。
前面两节课我们讲了 HTTP 请求里常见的方法、头部以及一些附加参数。第七课说到 HTTP 请求的附加信息时,我们用了去餐厅点菜来打比方:你走进一家餐厅,服务员要知道你从哪来、用什么设备来、是不是本人,这里对应到网络请求里,就是各种请求头和校验信息。
Referer 有点像“你是从哪看到我们餐厅的”。比如你从某个网页点进来,请求里可能带上来源地址,服务端就能判断这张图片是不是被别的网站偷偷引用了,这就是常说的防盗链。
User Agent 像是一张“名片”,告诉对方你大概是用什么设备、什么浏览器来的。服务端会参考它做页面适配、兼容处理,或者做一些简单识别。不过这张名片是可以被改写的,所以不能只靠它判断一个人是不是真实用户。
CSRF token 则像一枚“防伪暗号”。它不是为了证明你是谁,而是防止别人冒用你的登录状态,伪造一个看起来合法的请求去点菜。服务端拿到这个暗号,再和之前给你的对一下,就能降低跨站请求伪造的风险。
接下来进入今天的主题。今天我们重点看 WebSocket 是怎么建立连接的,以及它建立之后怎么传参数。别看名字里有 Web,它和普通 HTTP 的一问一答并不完全一样。
先说“流式传输”。你可以把它理解成水管不是接满一桶再给你,而是边流边用。HTTP 里也有一些类似效果的做法,常见的有两种:一种是 SSE,一种是 Partial Content。
SSE 像收听广播电台。你打开收音机,和电台建立一条连接,之后电台可以持续把消息推给你。你这边主要是听,服务器单方向地发,直到连接断开。它很适合做通知、进度更新、简单推送这类场景。
Partial Content 更像看连载小说或者断点续传。客户端会告诉服务器:“我这次先要第几段到第几段。”服务器就把这一段发回来。下次想接着看,再按范围要下一段。它不一定要求一直占着同一条连接,重点在于按范围取一部分内容,所以下载大文件、视频拖动进度条、断点续传时经常会碰到它。
理解这两种之后,再看 WebSocket 就清楚了。WebSocket 像打电话或者用对讲机,双方可以随时说话。它经常被拿来和 SSE 比较,但它不是简单地在 HTTP 通道里一直塞数据,而是先借一次 HTTP 请求完成“协议升级”。也就是说,WebSocket 本身没有像 HTTP 那样的 GET、POST 方法。真正建立连接时,借用的是一次 HTTP GET 请求,核心在请求头里的 Upgrade 和 Connection 这些字段,意思是:“我想把这条连接换个聊天方式。”
一次典型的 WebSocket 握手,也就是拨号过程,大概像下面这样:
GET /chat?room=1001 HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://example.com
Sec-WebSocket-Protocol: chat, json
服务端如果同意升级,就会返回:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
这里的 101 Switching Protocols 很关键,它表示这条连接已经从 HTTP 协议切换成了 WebSocket 协议。就像双方对上了暗号,正式切到专属的电话频道。后面的通信就不再是 HTTP 那种一问一答的请求和响应,而是双向的 WebSocket 帧。
什么是帧?你可以把它理解成打电话时传递的一个个“语音包”。客户端发往服务端的帧,要按协议做一层掩码。别把它想成加密,它更像是给数据套了一层随机外衣,主要是避免中间代理把 WebSocket 数据误当成普通 HTTP 数据缓存下来。服务端发回客户端的帧,按协议则不需要做这层掩码。
语音包里面会有 opcode,也就是操作码,用来区分里面装的是什么东西:是文字,就是文本帧;是图片、文件等,就是二进制帧;是要挂电话了,就是关闭帧;还有“喂,还在吗”的 ping 和 pong 心跳帧。文本帧一般传 JSON 字符串,二进制帧可以传 ArrayBuffer、Blob,或者图片、音频等数据。
那 WebSocket 怎么传参呢?也就是电话接通后,怎么把具体信息告诉对方。浏览器里的 WebSocket API 非常简单:
const ws = new WebSocket(url, protocols);
它不像 fetch 或 XMLHttpRequest 那样能让你自由配置完整的请求头,所以浏览器端的传参方式主要有下面几种。
第一种,把参数放在 URL 查询字符串里:
const ws = new WebSocket('wss://example.com/ws?room=1001&token=abc');
这种方式最直接,就像把房间号和密码直接写在电话号码里。但要注意,URL 可能会进入服务端日志、代理日志、监控系统,所以不要长期把敏感 token 放在 URL 里,容易泄露。
第二种,通过子协议传参:
const ws = new WebSocket('wss://example.com/ws', ['chat.v1', 'json']);
服务端会在 Sec-WebSocket-Protocol 中选择一个它支持的协议返回。这个方式更适合做版本协商和消息格式约定,比如约定好:“我们接下来用 chat.v1 这套格式聊天。”它不适合塞敏感数据,也不适合把很长的 JSON 硬塞进去。
第三种,利用 Cookie。同源情况下,浏览器会在 WebSocket 握手请求中自动带上 Cookie,所以服务端可以用 Session 做身份识别。这就像你进了同一个小区的门,保安自动认出了你的业主卡。但跨站场景下要特别注意 SameSite、Origin 校验和 CSRF 风险,不能觉得有 Cookie 就一定安全。
第四种,也是最灵活、最推荐的一种,是在连接建立后发送第一条业务消息:
const ws = new WebSocket('wss://example.com/ws');
ws.onopen = () => {
ws.send(JSON.stringify({
type: 'auth',
token: 'abc',
room: 1001,
since: 0
}));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
console.log('收到消息:', msg);
};
ws.onclose = (event) => {
console.log('连接关闭:', event.code, event.reason);
};
这种方式可以把鉴权、订阅频道、同步历史消息等复杂参数一次性发给服务端,结构清晰,也方便扩展。就像电话接通后,你先开口说:“你好,我是某某,我的密码是 abc,我要进 1001 房间。”
如果你是在 Node.js 里使用 ws 这类库,那么还可以在握手阶段自定义请求头:
const WebSocket = require('ws');
const ws = new WebSocket('wss://example.com/ws', {
headers: {
Authorization: 'Bearer abc'
}
});
但要注意,不同 WebSocket 库的 API 不一样,浏览器原生的 WebSocket 构造函数是不支持自定义请求头的。所以实际项目里,常见的做法是:握手阶段用 Cookie 或子协议做粗粒度身份和版本识别,连接成功后再用第一条消息做详细鉴权和业务参数传递。
同时还要考虑心跳保活,也就是定期问一句“还在吗”;断线重连,也就是断了自动重拨;消息顺序和重复发送,尤其是在重连之后,要避免重复处理或者漏掉消息;还有鉴权过期、服务端主动踢下线等问题。只有把握手、帧和业务参数分层设计好,WebSocket 才不会变成一条只会“裸奔”的长连接。
今天的课程内容就是这些。下节课开始,我们就要进入抓包教程。我会教大家如何使用抓包工具抓包,抓出来的请求该如何分析,以及如何用同样的请求去发起请求。那今天的课程就到这里,下课。