面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 06月1日 09:19

i18next 国际化怎么测试?翻译缺失和语言切换如何验证?

测试 i18next 分三层:翻译函数本身、React 组件集成、边界情况(缺失 key、懒加载)。翻译函数测试最简单——初始化一个独立的 i18next 实例,注入 mock 翻译资源,断言 t('key') 的返回值,覆盖简单 key、插值({{name}})、复数形式。React 组件测试需要用 I18nextProvider 包裹被测组件,传入配置好的 i18n 实例,这样组件内的 useTranslation 和 Trans 才能正常工作。语言切换测试调用 i18n.changeLanguage() 后用 waitFor 断言 UI 文案变化。缺失 key 测试开启 saveMissing: true,配合 missingKeyHandler 断言回调被调用,或直接断言 t('nonexistent') 返回 key 本身作为 fallback。追问为什么不用真实的翻译文件跑测试,而要 mock 资源?真实翻译文件会变——翻译团队随时可能改文案,如果测试断言了具体翻译文本,每次文案调整测试就挂了。mock 资源让测试只关心"key 能正确解析"这个逻辑,不绑定具体文案内容。另外 mock 资源体积小、加载快,避免测试里引入大量 JSON 文件。最佳实践:mock 资源只放测试需要的 key,用 i18next.createInstance() 隔离,避免测试间互相污染。Trans 组件和 t() 函数测试有什么区别?t() 是纯函数,输入 key 和参数返回字符串,测试最简单。Trans 组件渲染包含 HTML 标签的翻译内容(如 Welcome <1>{{name}}</1>),需要验证组件嵌套是否正确,不能只断言文本内容——Trans 可能渲染出 <strong>John</strong> 也可能渲染出纯文本 John,取决于翻译 key 的定义。测试 Trans 时要用 container.innerHTML 或 within 查询,确认标签结构正确,而不仅是文本存在。命名空间怎么测试?懒加载命名空间呢?命名空间测试关键是每个命名空间独立初始化资源,断言 t('ns:key') 能正确解析跨命名空间 key。懒加载命名空间测试用 useSuspense: false 模式——组件先渲染 loading 状态(ready 为 false),然后通过 addResourceBundle 手动注入资源,用 waitFor 断言组件渲染出翻译内容。注意懒加载测试不要用 Suspense 包裹,否则 React Testing Library 无法捕获 loading 态。快照测试适合 i18next 吗?有什么坑?不太适合。快照会把翻译文案写死到 .snap 文件里,翻译改了快照就挂,维护成本高。而且快照只能告诉你"渲染结果和之前一样",不能告诉你翻译是否正确。如果一定要用,只对组件结构做快照(用 render 后取 asFragment()),不要断言具体翻译文本。更好的替代方案是用 t() 的 mock 验证调用参数是否正确,而不是验证返回值。// 独立实例 + mock 资源的翻译测试import i18next from 'i18next';describe('i18next translations', () => { let i18n; beforeEach(() => { i18n = i18next.createInstance(); i18n.init({ lng: 'en', resources: { en: { translation: { hello: 'Hello', greet: 'Hi {{name}}' } }, zh: { translation: { hello: '你好', greet: '你好 {{name}}' } } } }); }); test('简单 key', () => expect(i18n.t('hello')).toBe('Hello')); test('插值', () => expect(i18n.t('greet', { name: 'Li' })).toBe('Hi Li')); test('缺失 key 返回 key 本身', () => expect(i18n.t('missing')).toBe('missing'));});// React 组件测试(I18nextProvider 包裹)import { I18nextProvider } from 'react-i18next';import { render, screen, waitFor } from '@testing-library/react';function Greeting() { const { t, i18n } = useTranslation(); return <> <span>{t('hello')}</span> <button onClick={() => i18n.changeLanguage('zh')}>切换</button> </>;}test('语言切换', async () => { render(<I18nextProvider i18n={i18n}><Greeting /></I18nextProvider>); expect(screen.getByText('Hello')).toBeInTheDocument(); fireEvent.click(screen.getByText('切换')); await waitFor(() => expect(screen.getByText('你好')).toBeInTheDocument());});
服务端阅读 06月1日 09:19

WebSocket 连接管理有哪些关键实践?断线重连怎么做?

WebSocket 连接管理围绕生命周期展开:建立、保活、重连、清理。建立时客户端发起连接,服务端做鉴权和元数据登记(用户 ID、设备信息),然后双方进入通信状态。保活用心跳机制——客户端定时发 ping,服务端回 pong,超时未收到 pong 则认为连接已死,这是检测"半开连接"(对端已断开但本端不知道)的唯一可靠手段。重连是客户端的职责,核心策略是指数退避:首次 1 秒后重试,之后 2s、4s、8s,上限 30s,避免服务端刚恢复就被雪崩式重连打垮。加随机抖动(jitter)防止大量客户端同时重连。清理是关闭连接后释放定时器、事件监听器、内存中的消息队列等资源,避免泄漏。追问指数退避为什么要加随机抖动?如果 1000 个客户端同时断线,都在 1s、2s、4s 后重连,会出现"惊群效应"——服务端刚恢复就被第一波重连打垮,然后再次宕机,下一波重连又打垮,形成恶性循环。加 jitter(如 delay = baseDelay * 2^retry + Math.random() * 1000)让各客户端的重连时间错开,将瞬间冲击分散到时间窗口内。AWS 和 Google 的重连库都默认启用 jitter。心跳间隔设多少合适?太短和太长各有什么问题?通常 30 秒。太短(如 5 秒)会浪费带宽和服务端资源,10 万连接每 5 秒 ping 一次就是 2 万次/秒;太长(如 120 秒)无法及时发现半开连接,用户可能等 2 分钟才知道消息收不到了。移动端建议 30-45 秒,服务端 idle timeout 设为心跳间隔的 2-3 倍(如 90 秒),给网络波动留余量。Nginx 代理 WebSocket 时也要配置 proxy_read_timeout 与之一致。连接池在什么场景下需要?单连接不够用吗?浏览器对同一域名的 WebSocket 连接数没有硬限制(不像 HTTP/1.1 的 6 个),所以大多数场景单连接就够了,在应用层做消息多路复用(通过 type 字段区分业务)。需要连接池的场景:不同业务域隔离(聊天和行情走不同连接,避免互相阻塞);大规模数据传输(单连接带宽不够时分流);高可用(主连接断开时备用连接无缝切换)。不要为了"看起来专业"而引入连接池,多连接意味着多倍的心跳和内存开销。页面切换或组件卸载时 WebSocket 连接怎么处理?SPA 路由切换时不要断开重连,把 WebSocket 实例提升到全局(Redux store 或 Context),路由组件只订阅/取消订阅消息,连接本身持续存在。组件卸载时必须移除事件监听器,否则闭包引用的旧 state 会导致内存泄漏和僵尸回调。页面真正关闭(beforeunload)时发关闭帧优雅断开。React 项目推荐用自定义 Hook 封装,useEffect 的 cleanup 中移除 listener。// 指数退避 + 随机抖动重连function createReconnectingWebSocket(url) { let ws; let retryCount = 0; const maxRetries = 10; const baseDelay = 1000; const maxDelay = 30000; function connect() { ws = new WebSocket(url); ws.onopen = () => { retryCount = 0; // 连接成功,重置计数 }; ws.onclose = (event) => { if (event.code === 1000 || retryCount >= maxRetries) return; const delay = Math.min(baseDelay * Math.pow(2, retryCount), maxDelay) + Math.random() * 1000; // 随机抖动 retryCount++; setTimeout(connect, delay); }; } connect(); return ws;}// 心跳机制function startHeartbeat(ws, interval = 30000) { const timer = setInterval(() => { if (ws.readyState === WebSocket.OPEN) { ws.ping(); // 或 ws.send(JSON.stringify({ type: 'ping' })) } }, interval); return timer; // 组件卸载时 clearInterval(timer)}
服务端阅读 06月1日 09:19

WebSocket 连接有哪些安全风险?如何防护?

WebSocket 的安全威胁主要来自四方面:窃听、劫持、伪造和洪泛。防护的核心是强制 WSS(TLS 加密)防止窃听,验证 Origin 头防止跨站 WebSocket 劫持(CSWSH),在握手阶段或首条消息中完成身份认证防止伪造,速率限制和消息校验防止洪泛和注入攻击。WSS 是底线——ws:// 明文传输下,中间人可以读取甚至篡改所有消息。Origin 验证同样关键:浏览器发起 WebSocket 连接时会自动携带 Cookie,如果服务端不校验 Origin,恶意页面可以伪造用户身份建立连接(即 CSWSH 攻击)。认证方面,URL 查询参数传 token 会泄露到日志,更安全的做法是连接建立后通过首条消息发送 JWT,服务端验证失败立即关闭连接(code=1008)。追问CSWSH 攻击是怎么发生的?和 CSRF 有什么区别?CSWSH 利用浏览器自动携带 Cookie 的特性:恶意网站用 JS 创建 new WebSocket('wss://victim.com/socket'),浏览器会带上 victim.com 的 Cookie,服务端如果只看 Cookie 不看 Origin,就会把连接当作合法用户。和 CSRF 的区别在于:CSRF 只能触发请求不能读取响应(同源策略限制),而 WebSocket 连接建立后恶意页面可以双向通信、读取服务端推送的敏感数据,危害更大。防护就是服务端必须校验 Origin 头。URL 参数传 Token 和连接后发认证消息,哪个更安全?连接后发认证消息更安全。URL 参数中的 token 会被记录在服务端访问日志、浏览器历史、代理日志中,且很难清除。连接后发消息的方式 token 只在 WebSocket 帧中传输,WSS 加密后中间人看不到。但连接后认证有一个窗口期:连接已建立但未认证,这段时间内服务端不能处理任何业务消息。实践中两者可以组合——URL 传短期一次性 ticket,连接后再用 JWT 换取长期会话。WebSocket 上的 XSS 攻击怎么防?WebSocket 传输的文本如果直接插入 DOM(innerHTML),和 HTTP 一样存在 XSS 风险。防护手段:消息内容做 HTML 转义后再渲染,或用 textContent 替代 innerHTML;服务端过滤消息中的 <script>、javascript:、onerror= 等危险模式;消息大小限制(如 10KB)防止超长 payload;消息结构白名单校验,只允许预定义的 type 字段。记住 XSS 的根源是渲染层,不是传输层。怎么防止 WebSocket 连接被用于 DDoS?服务端侧:限制单 IP 最大连接数(如 50),超出拒绝握手;单连接消息速率限制(如每分钟 100 条),超限断开;消息体积上限(如 64KB),防止内存炸弹;空闲连接超时断开(idleTimeout)。架构层:前端用 Nginx 做 WebSocket 代理,配置 limit_conn 和 limit_req;启用 DDoS 防护服务(Cloudflare 等);连接鉴权增加攻击成本,未认证的连接快速关闭不占资源。// 服务端安全校验示例wss.on('connection', (ws, req) => { // 1. Origin 校验 const origin = req.headers.origin; if (!['https://yourdomain.com'].includes(origin)) { return ws.close(1003, 'Forbidden origin'); } // 2. 身份认证(首条消息) ws.authenticated = false; ws.once('message', (msg) => { const { type, token } = JSON.parse(msg); if (type !== 'auth') return ws.close(1008, 'Auth required'); try { const user = jwt.verify(token, SECRET); ws.authenticated = true; ws.userId = user.id; } catch { return ws.close(1008, 'Invalid token'); } }); // 3. 速率限制 ws.messageCount = 0; ws.on('message', () => { ws.messageCount++; if (ws.messageCount > 100) ws.close(1008, 'Rate limit'); });});
服务端阅读 06月1日 09:19

WebSocket 性能瓶颈在哪?如何优化?

WebSocket 性能优化集中在三个层面:协议层压缩与传输格式、应用层消息策略、架构层连接管理。协议层最有效的是启用 permessage-deflate 扩展,文本消息可压缩 60-80%,但小消息(<1KB)压缩反而增加开销,建议设 threshold=1024,压缩级别用 3(速度与压缩率平衡)。传输格式上,大数据场景用二进制(ArrayBuffer/TypedArray)替代 JSON 字符串,解析更快、体积更小;高频结构化消息可用 Protocol Buffers 或 MessagePack 替代 JSON。应用层关键是消息批处理,把短时间内的多条小消息合并成一条发送,减少网络往返次数。架构层用连接池复用连接、用 uWS 替代 ws 库(性能高 10-20 倍)、通过 Redis Pub/Sub 分发消息实现多进程横向扩展。追问permessage-deflate 压缩在什么场景下反而降低性能?消息小于 1KB 时压缩开销大于收益,因为压缩本身消耗 CPU,且 deflate 帧头和上下文开销可能让数据反而变大。高并发场景下 CPU 是瓶颈,压缩会加剧竞争。实践中建议设 threshold 只压缩大消息,或者在 CPU 占用超过 70% 时关闭压缩。另外 clientNoContextTakeover: true 可以限制服务端内存占用,但会降低压缩率。消息批处理的延迟和吞吐怎么取舍?批处理本质是用延迟换吞吐——攒一批再发,单次传输数据量更大,网络效率更高,但接收端看到消息的时间晚了。实时聊天场景 batchTimeout 不宜超过 50ms,行情推送可设 100-200ms。可以动态调整:消息积压多时加大 batch 减少发送次数,消息稀疏时减小 timeout 保证低延迟。为什么 uWS 比 ws 库快那么多?有什么代价?uWS 用 C++ 编写,直接操作 epoll/kqueue,避免了 Node.js 事件循环的开销;ws 库是纯 JS 实现,每条消息都经过 V8 的 JS/C++ 边界。代价是 uWS 的 API 不兼容 ws,调试困难,社区生态小,遇到问题难以排查;而且它不支持 permessage-deflate 的所有配置项。对性能要求不极致的项目,ws 的开发效率更高。如何监控 WebSocket 连接的性能瓶颈?核心指标:单连接消息吞吐量(条/秒)、端到端延迟(ping-pong RTT)、服务端内存和 fd 占用、消息队列积压深度。用 PerformanceObserver 或自定义中间件采集每个连接的发送/接收字节数和延迟分布,P99 延迟超过 200ms 或内存持续增长说明有瓶颈。线上可用 Prometheus + Grafana 做大盘,重点关注连接数突增和消息积压告警。// permessage-deflate 配置示例(Node.js ws 库)const wss = new WebSocket.Server({ perMessageDeflate: { threshold: 1024, // 超过 1KB 才压缩 zlibDeflateOptions: { level: 3, // 压缩级别,3 是速度与压缩率平衡点 concurrency: 10 }, clientNoContextTakeover: false, // 允许上下文复用,压缩率更高 serverNoContextTakeover: false }});// 消息批处理class MessageBatcher { constructor(ws, batchSize = 10, batchTimeout = 100) { this.ws = ws; this.queue = []; this.timer = null; this.batchSize = batchSize; this.batchTimeout = batchTimeout; } add(msg) { this.queue.push(msg); if (this.queue.length >= this.batchSize) this.flush(); else if (!this.timer) { this.timer = setTimeout(() => this.flush(), this.batchTimeout); } } flush() { if (!this.queue.length) return; this.ws.send(JSON.stringify({ type: 'batch', messages: this.queue })); this.queue = []; clearTimeout(this.timer); this.timer = null; }}
服务端阅读 06月1日 09:19

WebSocket 和 HTTP 有什么区别?为什么需要 WebSocket?

WebSocket 是基于 TCP 的全双工通信协议,属于 HTML5 规范的一部分。与 HTTP 最大的区别在于通信模式:HTTP 是请求-响应模型,客户端发起请求后服务端才能返回数据,每次请求都携带完整头部(数百字节),连接完即断;WebSocket 通过 HTTP Upgrade 机制握手一次(客户端发送 Upgrade: websocket 头,服务端返回 101 Switching Protocols),之后在同一条 TCP 连接上双方可随时互发数据,头部开销仅 2-10 字节。这意味着服务端可以主动推送,延迟从 HTTP 轮询的数百毫秒降到毫秒级,适合聊天、实时行情、协同编辑、在线游戏等场景。WebSocket 连接是持久的,除非任一方发送关闭帧或网络中断,否则连接一直保持。追问WebSocket 握手为什么基于 HTTP?能不能跳过 HTTP 直接建立连接?不能跳过。WebSocket 复用 HTTP 握手是为了兼容现有基础设施——浏览器先发一个标准 HTTP GET 请求(携带 Upgrade 头),中间的代理、负载均衡、防火墙都能识别并放行;如果直接用新协议,这些中间件会拒绝或破坏连接。握手成功后协议才切换为 WebSocket,之后的数据帧完全脱离 HTTP 格式。HTTP/2 的 Server Push 能替代 WebSocket 吗?不能。HTTP/2 Server Push 是服务端主动推送资源到浏览器缓存,本质还是服务于请求-响应模型,客户端无法通过 JavaScript 读取推送的原始内容;而且 Push 的资源受浏览器缓存策略控制,不适合实时数据流。WebSocket 是真正的双向消息通道,客户端可以用 JS 直接处理每一条推送消息。WebSocket 连接断开时如何判断是网络问题还是服务端主动关闭?WebSocket 关闭帧(Close Frame)携带状态码:1000 表示正常关闭,1001 表示端点离开,1006 表示异常关闭(没有收到关闭帧,通常是网络中断)。浏览器 onclose 事件的 code 和 reason 字段可以区分:code=1006 是网络问题需要重连,code=1000/1001 是正常关闭不需要重连。大量 WebSocket 连接会压垮服务端吗?怎么控制?会。每个 WebSocket 连接占用一个文件描述符和内存(约 10-50KB),单机默认 fd 上限通常 65535。控制手段:用 ulimit -n 或 systemd 调高 fd 上限;设最大连接数阈值超限拒绝;用 Redis Pub/Sub 做跨进程消息分发,单机只维护本机连接;横向扩展用 sticky session 或网关层路由,把同一用户的连接固定到同一后端。// WebSocket 握手过程示例const ws = new WebSocket('wss://example.com/socket');ws.onopen = () => { console.log('连接已建立,协议已升级'); ws.send('hello'); // 直接发送,无需 HTTP 请求};ws.onmessage = (event) => { console.log('收到服务端推送:', event.data);};ws.onclose = (event) => { console.log('关闭码:', event.code, '原因:', event.reason); // code=1006 说明是异常断开,需要重连};
服务端阅读 06月1日 09:12

WebSocket 有哪些安全问题?CSWSH/注入/DoS 防御方案

WebSocket主要面临五大安全威胁:跨站WebSocket劫持(CSWSH)、数据注入、中间人攻击、认证缺失和拒绝服务。CSWSH是最典型的——浏览器WebSocket握手自带Cookie但不受同源策略约束,攻击者在恶意页面发起ws连接即可冒用用户身份。防御核心是服务端严格校验Origin头,拒绝非白名单来源。数据注入方面,WebSocket帧内容无格式约束,服务端若不做输入验证和转义就写数据库或渲染页面,等同于XSS/SQL注入入口。中间人攻击靠WSS(WebSocket over TLS)加密传输解决,和HTTPS同理。认证授权方面,WebSocket握手阶段无法自定义Header,常见做法是URL参数带token(wss://host/ws?token=xxx)或通过Sec-WebSocket-Protocol子协议传递,后者更安全因为不暴露在日志里。DoS方面,恶意客户端可狂开连接或发送巨量数据,需限制单IP连接数、单帧大小和消息频率。追问CSWSH和CSRF有什么区别?CSRF针对HTTP请求,浏览器自动携带Cookie;CSWSH针对WebSocket握手,同样自动带Cookie但连接建立后变为长连接,攻击者能持续收发消息,危害更大。防御上CSRF用CSRF Token,CSWSH靠Origin校验。URL传token有什么安全隐患?token出现在URL中会被浏览器历史记录、服务器访问日志、Referer头记录。改进方案:用一次性ticket——先通过HTTPS接口获取短时效ticket,再通过URL传ticket建立WebSocket,服务端验证后立即销毁,后续靠连接状态维持会话。WSS能防中间人,能防数据注入吗?不能。WSS只保证传输链路加密和完整性,不校验内容语义。恶意客户端照样通过WSS发送XSS payload或SQL注入语句,服务端必须独立做输入校验和输出转义。如何限制WebSocket的DoS攻击?服务端网关层做四件事:单IP最大连接数限制、单帧最大体积限制(如64KB)、消息频率限流(如每秒10条)、空闲超时断开。Nginx可通过limit_conn和proxy_read_timeout实现基础防护。Sec-WebSocket-Protocol传token具体怎么实现?客户端发起握手时在Header中设置Sec-WebSocket-Protocol: access_token, your_protocol,服务端从该头提取token验证,验证通过后在响应中只回传your_protocol(不回传token),这样连接正常建立。好处是token不出现在URL中,缺点是语义上属于对该字段的滥用。
服务端阅读 06月1日 09:12

WebSocket 性能怎么优化?连接/传输/服务端三层优化策略

WebSocket性能优化要从连接、传输、服务端三个层面入手。连接层:用单例模式复用连接,避免重复建连;连接池管理多服务端场景,按负载分配。传输层:消息压缩(pako或服务端gzip)减少带宽;批量发送合并高频小消息;用二进制格式替代JSON减少序列化开销;心跳间隔根据网络环境调优,移动端可适当缩短。服务端:ws库配置maxPayload防大包攻击,合理设置heartbeat超时;客户端设置binaryType为arraybuffer避免Blob转换损耗。负载均衡需用sticky session保证同一客户端请求落到同一服务节点。追问连接复用和连接池有什么区别?连接复用是单个客户端对同一服务端只维护一条连接,通过单例模式实现,避免重复握手开销。连接池则是预先创建多条连接供不同业务模块使用,适用于需要连接多个不同服务端的场景。实际开发中,连接复用更常见,连接池多用于微服务网关层。什么时候该用二进制而不是JSON?高频传输、数据量大、结构固定的场景优先二进制。比如实时坐标、传感器数据,用ArrayBuffer或protobuf比JSON体积小30%-60%,解析也更快。如果是低频控制指令,JSON可读性好,开发调试方便,没必要换。消息批量发送怎么实现?设一个缓冲区,短时间内的消息先攒起来,定时或达到阈值再一次性发送。比如50ms内的消息合并成一条发送。注意设置最大延迟,避免消息等太久。断线重连时缓冲区的消息要能补发。心跳间隔怎么定?局域网5-10秒足够,公网移动端建议15-30秒。间隔太短浪费流量和电量,太长又不能及时检测断连。关键原则:心跳超时时间至少是间隔的3倍,给网络抖动留余量。NAT超时一般2-5分钟,心跳间隔必须短于这个值。sticky session有什么问题?单点故障——某台服务端挂了,它上面的连接全部断开,不能自动漂移到其他节点。解决思路:服务端用Redis Pub/Sub同步状态,客户端断连后重试可落到新节点并恢复上下文。sticky session是妥协方案,真正想高可用得做有状态迁移。
服务端阅读 06月1日 09:12

Node.js 怎么搭建 WebSocket 服务器?ws 库实战指南

Node.js 搭建 WebSocket 服务器最主流的方式是用 ws 库,轻量且性能好。基础用法只需几行代码:创建 WebSocket.Server 实例,监听 connection 事件处理消息收发。实际项目中通常把它挂到已有的 HTTP 服务器上,这样 HTTP 和 WS 共用同一端口,部署和运维更方便。认证方面,ws 提供 verifyClient 回调,在握手阶段拦截非法连接,配合 JWT 做令牌校验即可。房间功能需要自己实现——用 Map 按房间 ID 存储客户端集合,提供 join/leave/broadcast 方法。广播就是遍历房间内所有连接逐个 send,注意排除发送者本身。获取客户端真实 IP 要从 req.headers['x-forwarded-for'] 或 req.socket.remoteAddress 取,反向代理场景下前者更可靠。消息格式验证建议在收到消息后先用 JSON.parse + schema 校验,避免畸形数据引发异常。const WebSocket = require('ws');const server = require('http').createServer();const wss = new WebSocket.Server({ server });const rooms = new Map();wss.on('connection', (ws, req) => { ws.on('message', (raw) => { const msg = JSON.parse(raw); if (msg.type === 'join') { if (!rooms.has(msg.room)) rooms.set(msg.room, new Set()); rooms.get(msg.room).add(ws); ws.room = msg.room; } if (msg.type === 'chat') { const room = rooms.get(msg.room); if (!room) return; room.forEach(c => { if (c !== ws && c.readyState === WebSocket.OPEN) c.send(JSON.stringify({ text: msg.text })); }); } }); ws.on('close', () => { if (ws.room && rooms.has(ws.room)) { rooms.get(ws.room).delete(ws); if (rooms.get(ws.room).size === 0) rooms.delete(ws.room); } });});server.listen(3000);追问ws 和 Socket.IO 怎么选?ws 是纯 WebSocket 实现,体积小、性能高、无额外协议。Socket.IO 在 WebSocket 之上加了自动降级(长轮询)、命名空间、房间等,但协议不兼容标准 WS 客户端。如果客户端不可控(比如浏览器),用 Socket.IO 更稳;如果前后端都是自己掌控,ws 更干净。高并发下房间广播性能怎么优化?遍历 Set 逐个 send 在万级连接时会有瓶颈。优化思路:用 ws 的批量发送;消息做二进制编码(MessagePack 替代 JSON);房间内连接超多时改用 pub/sub 模式(Redis),多进程各自只发自己管理的连接。verifyClient 里做异步校验怎么办?verifyClient 的回调 cb 可以稍后调用,所以你能在里面写数据库查询或远程校验逻辑,校验完再调 cb(true/false) 就行。别把请求阻塞太久,设个超时。如何防止消息洪水攻击?在 connection 事件中用令牌桶限流,比如每秒只允许每个连接发 N 条消息,超出直接断开。也可以在 nginx 层面对 WS 连接做 limit_conn 限制,双重保险。
服务端阅读 06月1日 09:12

浏览器中 WebSocket 怎么用?API 详解与封装实践

创建WebSocket实例即可建立长连接,通过四个事件回调处理通信生命周期:onopen连接就绪后用send()发消息,onmessage接收服务端数据,onclose和onerror处理断开与异常。协议用ws://或加密的wss://,消息支持文本、JSON、Blob和ArrayBuffer四种格式。连接状态通过readyState判断——0正在连接、1已连接、2正在关闭、3已关闭。收二进制数据前需设置binaryType为"arraybuffer"或"blob"。主动关闭调用close()并传入状态码(如1000正常关闭、1001离开页面)。生产环境建议封装客户端类,加入自动重连、心跳检测和消息队列,避免连接中断丢消息。追问readyState各状态值在什么场景下出现?0(CONNECTING)在new WebSocket()后瞬间出现,握手完成前;1(OPEN)是握手成功的正常工作态;2(CLOSING)是调用close()后服务端还未返回关闭帧的过渡态;3(CLOSED)是关闭帧确认或连接异常断开后的终态。注意:网络断开不会立刻变3,需心跳超时才能检测到。心跳检测怎么实现?连接建立后启动定时器,每隔N秒(通常30s)发送ping帧或自定义心跳消息,服务端回pong或应答。若超时未收到回复则判定断开,主动close()并触发重连。收到任何消息都重置超时计时器。关键点:心跳间隔要小于Nginx等中间代理的空闲超时(默认60s),否则代理会静默断开连接。断线重连策略如何设计?采用指数退避:首次1s后重连,失败则2s、4s、8s,上限30s左右。每次onclose触发时启动重连定时器,onopen时重置退避计数。注意区分正常关闭(状态码1000)和异常断开——前者不重连。重连前要清理旧实例的事件监听,避免内存泄漏。消息队列有什么用?连接未就绪时send()会报错,所以发消息前必须检查readyState。非OPEN状态下将消息推入队列,等onopen后再批量发出。队列需设上限(如100条),防止离线过久内存暴涨。对于实时性要求高的场景(如聊天),旧消息可丢弃只保留最新状态。
服务端阅读 06月1日 09:11

WebSocket 常见问题怎么排查?连接失败/消息丢失/频繁断开

WebSocket连接问题主要集中在四个环节:建立、握手、传输、稳定性。连接无法建立时,先确认服务器是否启动、端口是否正确、防火墙是否放行,再检查协议——本地开发用ws,生产环境必须wss,部分反向代理默认不转发WebSocket升级请求,需显式配置。握手失败通常是服务端校验不通过:Origin白名单未放行、子协议不匹配、认证信息缺失。消息丢失多因连接已断开但客户端未感知,或发送队列溢出被丢弃。连接频繁断开要排查心跳是否缺失、Nginx代理超时配置、网络中间件是否主动杀长连接。调试时重点监听onerror和onclose事件,通过close code定位原因(1006异常关闭、1008策略违规等),浏览器DevTools的Network面板可查看完整帧数据。追问ws和wss混用会导致什么问题?浏览器强制HTTPS页面只能发起wss连接,ws会被直接拦截。即使服务端支持ws,混用也会触发安全策略报错。开发环境可用ws,上线必须切wss并配置SSL证书。Nginx反向代理WebSocket需要什么额外配置?必须加Upgrade和Connection头:location /ws { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 3600s;}proxy_read_timeout默认60秒,超过不活动就断开,需按业务调大。如何检测连接是否还活着?实现心跳机制:客户端定时发ping帧,服务端回pong。若连续N次未收到响应则判定断连,主动重连。不要依赖TCP的keepalive,那个超时太长(默认2小时),不实用。close code各代表什么?1000正常关闭,1001端点离开,1006异常关闭(未收到关闭帧,通常是网络断开),1008策略违规,1011服务端异常。1006最常见,说明连接不是正常关闭的,要排查网络和中间件。消息发送队列溢出怎么处理?发送前判断readyState是否为OPEN,非OPEN状态入队缓存,连接恢复后补发。设队列上限,超出则丢弃旧消息或拒绝新消息,避免内存撑爆。关键业务消息要做服务端ACK确认,不能发完就当成功。
服务端阅读 06月1日 02:22

TypeScript 项目中 i18next 怎么做类型安全?

安装类型定义 @types/i18next 和 @types/react-i18next,然后定义翻译资源的类型结构。核心思路是把 JSON 翻译文件的结构映射为 TypeScript 接口,让 t() 函数的键名、插值参数、复数形式都获得类型检查。具体做法:定义 TranslationResources 接口描述所有命名空间的键值结构,通过泛型将资源类型注入 useTranslation 返回的 t 函数,使其只接受存在的键名;插值参数用 GreetingOptions 等接口约束,缺少必填变量时编译报错;复数和上下文分别用 PluralOptions、ContextOptions 约束枚举值;多命名空间场景定义 Namespaces 接口,通过 createTypedTranslation<NS> 工厂函数返回带命名空间类型的安全 Hook。实际项目中推荐用 i18next-resources-to-ts 从 JSON 自动生成类型定义,配合 CI 检查翻译键完整性,避免手动维护类型与实际翻译文件脱节。interface TranslationResources { common: { greeting: string; farewell: string }; errors: { notFound: string; network: string };}type TypedT = (key: FlattenKeys<TranslationResources>) => string;const { t } = useTranslation('common');// t('greeting') ✅ t('nonexist') ❌ 编译报错追问怎么让插值参数也有类型安全?定义插值参数接口,作为 t() 的第二个泛型:interface GreetingOptions { name: string; count?: number }t<GreetingOptions>('greet', { name: 'Tom' }) // ✅t<GreetingOptions>('greet', {}) // ❌ name 缺失复数和上下文的类型怎么约束?复数用 _one/_other 后缀的键名,类型上用枚举限制 count 值范围;上下文用 _male/_female 后缀,类型上约束 context 只能取合法值:interface PluralOptions { count: number }interface ContextOptions { context: 'male' | 'female' }多命名空间怎么处理类型?定义 Namespaces 接口映射命名空间名到资源类型,再用工厂函数生成带命名空间约束的 Hook:interface Namespaces { common: TranslationResources['common']; errors: TranslationResources['errors'];}function createTypedTranslation<NS extends keyof Namespaces>(ns: NS) { return useTranslation(ns as string);}Trans 组件怎么用类型?Trans 组件的 i18nKey 同样接受类型约束,配合 react-i18next 的泛型即可:<Trans i18nKey="common:greeting" components={{ bold: <strong /> }} />有哪些最佳实践?四点:用 i18next-resources-to-ts 自动生成类型,避免手写与 JSON 脱节;开启 i18next 的 returnNull: false 和严格模式;按功能拆分命名空间,按需加载减少包体积;CI 中用脚本校验翻译键完整性,确保多语言文件同步。
服务端阅读 06月1日 02:21

i18next 常见坑有哪些?翻译不显示/切换失效/插值失败排查指南

i18next 是前端最流行的国际化框架,使用中常遇到的问题按频率排列:翻译不显示多数是因为资源未加载完毕就渲染了组件、命名空间拼写错误、或语言代码格式不一致(zh vs zh-CN);语言切换不生效通常是组件未正确监听语言变化或 React 未触发重渲染;插值失败多半是 interpolation 配置缺失或变量名拼写不对;复数处理异常则是未配置对应语言的复数规则。性能方面,一次性加载所有语言资源会导致首屏变慢,应改为按需加载加缓存。缺失翻译可通过 fallbackLng 和 saveMissing 兜底。SSR 场景(如 Next.js)需注意服务端和客户端实例隔离。调试最直接的方式是开启 debug: true。追问翻译 key 存在但页面显示 key 原文?检查三点:资源文件是否在组件渲染前加载完成,可用 useTranslation 的 ready 状态判断;命名空间是否与 t() 调用一致;语言代码是否匹配,i18next 默认区分大小写,zh-CN 和 zh_cn 是不同 key。const { t, ready } = useTranslation('common');if (!ready) return <Loading />;语言切换后组件不更新?确保使用的是框架绑定库(如 react-i18next)提供的 useTranslation 或 withTranslation,而非直接调用 i18next.t()。后者是同步快照,不会响应语言变化。另外检查 initImmediate 是否设为 true,否则资源加载可能是异步的但切换时未等待完成。插值变量不替换怎么办?默认插值格式是 {{variable}},花括号前后不能有空格。确认变量名与模板一致,同时检查 interpolation.escapeValue 配置——在 React 中应设为 false,因为 React 自身会转义。// 正确t('hello', { name: '世界' }) // "你好 {{name}}" → "你好 世界"// 错误:花括号内有空格"你好 {{ name }}"复数形式不生效?i18next 的复数基于 Unicode CLDR 规则,需要对应语言的复数后缀(如英语 _plural,俄语有更多形式)。确认资源 key 拼写正确,且 interpolation 中 skipOnVariables 未被误设。对于中文这类无复数区分的语言,直接用普通 key 即可。{ "item": "{{count}} item", "item_plural": "{{count}} items"}生产环境如何处理缺失翻译?三步走:fallbackLng 指定回退语言;saveMissing: true 配合 missingKeyHandler 将缺失 key 上报至翻译管理平台;开发环境开启 debug: true 在控制台打印缺失警告。Next.js SSR 场景下,确保服务端和客户端各持独立的 i18next 实例,避免请求间状态污染。
服务端阅读 06月1日 02:21

i18next 翻译资源怎么管理?自动化工作流搭建指南

i18next 翻译资源管理的核心是「提取 → 翻译 → 校验 → 部署」的自动化流水线。提取阶段,用 i18next-scanner 扫描代码中的 t() 调用自动提取键名,或通过 Babel 插件 i18next-extract 在编译时完成,避免手动维护键的遗漏。翻译阶段对接管理平台:i18next-locize-backend 可直接从 locize 云端拉取翻译,实现运行时按需加载;Crowdin 则提供翻译记忆和术语库,适合大规模多语言团队。校验阶段写验证函数检查占位符一致性(如 {{name}} 在各语言是否齐全)、HTML 标签闭合、译文长度溢出,配合 i18next 的 missingKeyHandler 捕获缺失键。部署阶段将翻译文件纳入 Git 版本控制,CI 中执行键提取和覆盖率检查,增量更新只同步变更的键。整个流程串起来:开发者提交含新键的代码 → CI 提取键并推送至翻译平台 → 译者完成翻译 → CI 拉取翻译合并到仓库 → 自动发布。追问i18next-scanner 和 Babel 插件怎么选?i18next-scanner 基于 AST 解析,支持 JSX/TSX/Vue,配置灵活可过滤特定目录,适合已有构建流的项目。Babel 插件在编译时提取,零额外构建步骤,但只处理 Babel 管辖的文件。项目若已用 Babel,插件更省事;需要扫描非 JS 文件或精细控制时选 scanner。翻译缺失键如何在线上兜底?注册 missingKeyHandler,线上遇到缺失键时上报到日志或翻译平台,同时用 fallback 语言返回兜底译文:i18next.init({ saveMissing: true, missingKeyHandler: (lngs, ns, key) => { reportMissingKey({ lngs, ns, key }); }, fallbackLng: 'en',});如何保证占位符和 HTML 标签在各语言间一致?写校验函数在 CI 中运行,提取每种语言的占位符和标签集合做差集比对:function validatePlaceholders(base, translations) { const baseVars = (base.match(/\{\{.*?\}\}/g) || []).sort(); for (const [lang, text] of Object.entries(translations)) { const vars = (text.match(/\{\{.*?\}\}/g) || []).sort(); if (JSON.stringify(vars) !== JSON.stringify(baseVars)) { throw new Error(`${lang} 占位符不匹配: ${vars} vs ${baseVars}`); } }}增量更新和全量更新怎么取舍?翻译文件小时全量更新简单可靠。文件超过 50 个语言或单文件上千条时,增量更新只同步变更键,减少冲突和传输量。实现上用 Git diff 检出本次变更的键名,只推送对应条目到翻译平台,拉取时也只覆盖变更部分。locize 和 Crowdin 何时分别使用?locize 与 i18next 深度集成,支持运行时按需加载语言包,适合中小项目快速接入。Crowdin 提供翻译记忆、术语表、审核流程和供应商管理,适合专业翻译团队协作的大规模项目。团队无专职译者选 locize,有专业本地化流程选 Crowdin。
服务端阅读 06月1日 02:20

WebSocket 是什么?和 HTTP 有什么区别?

WebSocket 是 HTML5 引入的全双工通信协议,客户端和服务端可以同时收发数据,连接建立后持续保持,直到任一方主动关闭。它通过一次 HTTP 握手升级协议,之后不再走 HTTP 请求-响应模型,而是以帧为单位双向传输,帧头仅 2-14 字节,远小于 HTTP 每次请求携带的头部开销。适用于聊天、实时行情、多人游戏、协同编辑、监控推送等需要低延迟双向通信的场景。追问WebSocket 握手过程是怎样的?客户端发起一个普通 HTTP 请求,携带 Upgrade: websocket 和 Connection: Upgrade 头,并附上 Sec-WebSocket-Key。服务端同意后返回 101 状态码,带上 Sec-WebSocket-Accept(由客户端 Key 经 SHA-1 计算得出)。此后 TCP 连接升级为 WebSocket 连接,不再走 HTTP 协议。HTTP 长轮询能替代 WebSocket 吗?不能。长轮询是客户端发请求后服务端持有连接直到有数据才返回,本质仍是请求-响应模式。每次返回数据后客户端必须重新发起新请求,延迟高、开销大、无法服务端主动推送。WebSocket 连接建立后服务端随时可推数据,无需客户端轮询。WebSocket 数据帧结构是什么?帧由以下字段组成:FIN(1位,是否最后一帧)、RSV1-3(保留)、Opcode(4位,帧类型:0x1文本、0x2二进制、0x8关闭、0x9 ping、0xA pong)、MASK(1位,是否掩码)、Payload Length(7位或扩展)、Masking-key(0或4字节)、Payload Data。客户端发送帧MASK必须为1,服务器为0。什么场景下应该选 HTTP 而非 WebSocket?请求频率低、以客户端拉取为主的场景用 HTTP 更合适。WebSocket 需要维护长连接,占用服务端资源,且断线重连、心跳保活都需要额外处理。如果只是偶尔查个数据、提交个表单,HTTP 更简单可靠。
服务端阅读 06月1日 02:20

WebSocket 握手过程是怎样工作的?HTTP 升级机制与安全防御

WebSocket 握手是基于 HTTP 的协议升级过程。客户端发送一个特殊的 HTTP GET 请求,携带 Upgrade: websocket 和 Connection: Upgrade 头部,表明要切换协议。同时带上 Sec-WebSocket-Key(16字节随机值的 Base64 编码)和 Sec-WebSocket-Version(通常为13)。服务端验证请求合法后,返回 101 Switching Protocols 状态码,并附带 Sec-WebSocket-Accept 头部,其值由客户端 Key 拼接固定 GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11,经 SHA-1 哈希后再 Base64 编码得到。握手完成后,连接不再走 HTTP 协议,而是切换为 WebSocket 帧格式进行双向通信。还可通过 Sec-WebSocket-Protocol 协商子协议,Sec-WebSocket-Extensions 协商扩展(如压缩)。追问Sec-WebSocket-Accept 为什么要拼接固定 GUID?这是 RFC 6455 的设计:GUID 是公开的常量,不提供加密作用,而是确保握手方确实理解 WebSocket 协议——普通 HTTP 客户端不会拼接这个 GUID,从而防止误升级。服务端无需维护状态,只需按规则计算即可验证。const crypto = require('crypto');function acceptValue(key) { return crypto.createHash('sha1') .update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11') .digest('base64');}什么是 CSWSH 攻击?如何防御?CSWSH(Cross-Site WebSocket Hijacking)指恶意网页利用浏览器自动携带 Cookie 的特性,向目标站点发起 WebSocket 连接,冒充已登录用户。防御核心是服务端验证 Origin 头部,拒绝非白名单来源的升级请求。此外,避免仅依赖 Cookie 鉴权,应结合 Token 机制(如在 URL 参数或首条消息中携带)。wss 和 ws 的区别是什么?ws:// 是明文传输,wss:// 在 TCP 之上加了 TLS 加密层,等同于 HTTP 与 HTTPS 的关系。生产环境必须使用 wss,否则握手和后续帧数据均可被中间人窃听或篡改。TLS 还能防止缓存投毒攻击——代理服务器不会将加密流量误判为普通 HTTP 响应并缓存。握手失败会怎样?服务端如果不支持 WebSocket 或验证不通过,不会返回 101,而是返回普通 HTTP 响应(如 400/403)。客户端收到非 101 响应后,WebSocket 对象触发 error 事件并关闭连接,不会尝试回退。若需降级为轮询,需在应用层自行实现。
服务端阅读 06月1日 02:17

WebSocket 握手过程是怎样的?HTTP 升级机制详解

WebSocket 握手基于 HTTP 协议升级机制。客户端发送一个普通的 HTTP GET 请求,携带几个关键头部:Upgrade: websocket 表示要求协议升级,Connection: Upgrade 表示连接需切换协议,Sec-WebSocket-Key 是一个随机的 Base64 编码值(16 字节),Sec-WebSocket-Version: 13 指定协议版本,Origin 标识请求来源。服务器收到后,如果同意升级,返回 101 Switching Protocols 状态码,并携带 Sec-WebSocket-Accept 头部,其值由客户端的 Key 拼接固定 GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11,经 SHA-1 哈希后再 Base64 编码得到。这个计算过程既确认了服务器理解 WebSocket 协议,也防止了误响应。握手完成后,底层 TCP 连接从 HTTP 模式切换为 WebSocket 模式,双方可以全双工通信,不再遵循 HTTP 请求-响应模型。安全层面:服务端应验证 Origin 头部防止跨站劫持(CSRF),Sec-WebSocket-Key 的校验确保对方确实是 WebSocket 客户端而非普通浏览器,生产环境建议使用 wss:// 加密传输。追问Sec-WebSocket-Accept 的计算为什么不是直接返回 Key?直接回传 Key 无法证明服务器理解 WebSocket 协议——任何 HTTP 服务器都会原样返回请求头。拼接固定 GUID 再做 SHA-1 + Base64,是一种轻量级"握手证明":只有知道协议规范的 WebSocket 服务端才会执行这个计算,普通 HTTP 服务器或代理不会。同时客户端也会校验返回的 Accept 值,不匹配则拒绝连接。Origin 头在握手中的作用是什么?Origin 让服务端判断握手请求是否来自可信源。WebSocket 不受同源策略约束,任何网页都能发起连接,所以服务端必须在握手阶段校验 Origin,拒绝非授权来源,防止恶意页面利用已认证的用户会话建立 WebSocket 连接(即 CSRF 攻击)。为什么握手用 HTTP 而不是直接新建 TCP 连接?复用 HTTP 可以利用现有基础设施:经过 HTTP 代理、防火墙、负载均衡器时不会被视为未知协议而被拦截。同时 HTTP 80/443 端口通常是开放的,降低了部署门槛。本质上是用 HTTP 作为"引导协议",完成协商后再切换到 WebSocket 二进制帧协议。wss 和 ws 的区别是什么?ws:// 是明文传输,wss:// 在 TCP 之上加了 TLS 加密层,等价于 HTTP 与 HTTPS 的关系。握手阶段 wss 的 HTTP 升级请求走 HTTPS,后续 WebSocket 帧同样加密传输。生产环境必须用 wss,否则数据可被中间人窃听或篡改。
服务端阅读 06月1日 02:15

WebSocket 心跳机制怎么实现?ping/pong 超时重连方案

WebSocket心跳机制通过定时发送ping/pong帧来维持连接活跃。客户端每隔固定间隔向服务器发送ping,服务器收到后返回pong,若客户端在超时时间内未收到pong则判定连接断开并触发重连。心跳的核心作用有三个:一是检测连接是否存活,TCP连接半打开时应用层无法感知;二是防止NAT超时或防火墙因空闲连接而切断链路;三是实现快速故障恢复,避免等到下次数据发送时才发现连接已断。WebSocket协议本身定义了ping/pong控制帧,但实际开发中也常用自定义文本消息(如{"type":"ping"})实现,灵活性更高且便于扩展业务数据。追问心跳间隔怎么定?推荐30-60秒。间隔过短增加服务器负担和带宽消耗,间隔过长又无法及时检测断开。具体值需权衡:移动网络下NAT超时通常2-5分钟,心跳间隔应小于NAT超时的一半。对实时性要求高的场景可缩短到15-25秒。超时处理怎么做?发送ping后启动超时定时器,若在阈值内(通常为心跳间隔的2-3倍)未收到pong,则视为连接失效:let heartbeatTimer, timeoutTimer;function startHeartbeat() { heartbeatTimer = setInterval(() => { ws.send('ping'); timeoutTimer = setTimeout(() => { ws.close(); // 超时未收到pong,关闭连接 }, heartbeatInterval * 3); }, heartbeatInterval);}ws.onmessage = (e) => { if (e.data === 'pong') { clearTimeout(timeoutTimer); // 收到pong,清除超时 }};双向心跳是什么?只由客户端发起心跳时,如果客户端崩溃,服务器无从得知。双向心跳让服务器也定时发ping,双方互检连接状态,避免单侧故障盲区。实现时注意避免双方ping同时触发造成冲突,可错开间隔或由一方主导。重连策略怎么设计?断开后应采用指数退避重连,避免大量客户端同时重连导致服务器雪崩:let retryCount = 0;const maxRetry = 5;function reconnect() { const delay = Math.min(1000 * Math.pow(2, retryCount), 30000); setTimeout(() => { retryCount++; connect(); }, delay);}ws.onclose = () => { if (retryCount < maxRetry) reconnect();};还有哪些最佳实践?动态调整间隔:网络良好时拉长间隔节省资源,弱网下缩短间隔加快检测状态监控:记录心跳延迟和丢包率,用于运维告警和间隔调优重连成功后重置退避计数,避免下次断开仍从高延迟开始
服务端阅读 06月1日 02:15

WebSocket 消息帧格式是怎样的?各字段含义详解

WebSocket消息帧是通信的基本单位,由固定格式的二进制结构组成。一帧包含以下字段:FIN(1位)标识是否为最后一个分片;RSV1-3(各1位)保留给扩展使用;Opcode(4位)标识帧类型,0x0为继续帧、0x1文本帧、0x2二进制帧、0x8关闭帧、0x9 Ping、0xA Pong;MASK(1位)标识是否使用掩码;Payload Length(7位或扩展)表示载荷长度;Masking-key(0或4字节)为掩码密钥;Payload Data为实际数据。客户端发送的帧MASK必须为1,服务器发送的帧MASK必须为0。Payload Length采用变长编码:0-125直接用7位表示;126表示后续2字节为真实长度;127表示后续8字节为真实长度。掩码算法将Payload Data的每字节与Masking-key循环异或(XOR),服务器收到后用同样方式还原。大消息可通过分片传输,首帧FIN=0、Opcode为实际类型,中间帧FIN=0、Opcode=0x0,末帧FIN=1、Opcode=0x0。关闭帧(Opcode=0x8)可携带状态码和原因,用于正常或异常断开连接。追问掩码算法具体怎么计算?客户端发送时生成4字节随机Masking-key,对Payload Data逐字节异或:for (let i = 0; i < payload.length; i++) { decoded[i] = payload[i] ^ maskingKey[i % 4];}编码和解码是同一操作,异或两次还原原文。设计目的是防止缓存污染攻击,不让中间代理误判载荷内容。分片消息如何重组?接收方维护一个缓冲区。收到FIN=0的帧时,将Payload追加到缓冲区,Opcode只在首帧有效。后续帧Opcode为0x0,表示"接续上一帧"。直到收到FIN=1的帧,拼接完成,最终消息类型取首帧的Opcode。注意控制帧(Ping/Pong/Close)不允许分片,可在数据帧分片中间插入。为什么要区分三种Payload Length编码?用7位最多只能表示125,不够用。加2字节可到65535,覆盖绝大多数场景。超过64KB才动用8字节,最大支持2^63-1。这种变长设计兼顾了小帧的紧凑性和大帧的扩展性——小帧不浪费头部空间,大帧也不受限制。关闭帧里能带什么内容?Payload前2字节是状态码(如1000正常关闭、1001端点离开、1002协议错误),剩余部分是UTF-8编码的关闭原因字符串,最长123字节(受125字节载荷上限减去2字节状态码限制)。双方都可以发起关闭帧,收到关闭帧的一方应回复一个关闭帧,然后双方关闭TCP连接。
服务端阅读 06月1日 02:15

WebSocket 和 HTTP 轮询、长轮询有什么区别?各自适用什么场景?

HTTP轮询是客户端按固定间隔向服务器发请求,无论有无数据都返回响应。实现最简单,但大部分请求是无效的,浪费带宽和服务器资源,延迟取决于轮询间隔。HTTP长轮询是客户端发请求后服务器不立即返回,而是挂起连接直到有新数据或超时才响应,客户端收到后再发下一个请求。实时性明显好于普通轮询,但每次数据推送都需要重新建立连接,且服务器需要维护大量挂起请求。WebSocket通过一次HTTP握手升级为持久连接,建立后双方可以随时主动发数据,是真正的全双工通信。帧头仅2-10字节,开销远低于HTTP每次请求携带的完整头部。三种方式中,WebSocket实时性最好、开销最低,但需要服务器和客户端同时支持;轮询兼容性最强但效率最差;长轮询是两者之间的折中。追问性能对比| 维度 | 轮询 | 长轮询 | WebSocket ||------|------|--------|-----------|| 实时性 | 差(依赖间隔) | 较好 | 优 || 服务器负载 | 高(大量无效请求) | 中 | 低 || 带宽消耗 | 高 | 中 | 低 || 实现复杂度 | 低 | 中 | 中 || 浏览器兼容性 | 最好 | 好 | 好(IE10+) |各自适用什么场景?轮询适合对实时性要求不高的场景,比如配置中心定期拉取最新配置。长轮询适合无需双向通信但需要服务端及时推送的场景,比如消息通知、扫码登录状态检查。WebSocket适合高频双向通信场景,比如即时聊天、协同编辑、实时行情推送、在线游戏。WebSocket连接如何建立?客户端发一个特殊的HTTP请求,携带Upgrade: websocket头:GET /ws HTTP/1.1Upgrade: websocketConnection: UpgradeSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==Sec-WebSocket-Version: 13服务器返回101状态码表示协议切换成功:HTTP/1.1 101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=之后TCP连接保持,双方通过WebSocket帧格式通信,不再走HTTP协议。长轮询和WebSocket在断线处理上有什么区别?长轮询本身是"请求-响应"模式,断线后客户端只需重新发请求即可,天然具备重连能力,但重连期间会丢失消息,需要服务端做消息缓存。WebSocket断线后需要客户端主动重连并重新握手,通常配合心跳检测(ping/pong帧)判断连接存活,重连后还需处理会话恢复和消息补发逻辑。
服务端阅读 06月1日 02:13

WebSocket 断线重连怎么实现?指数退避+心跳完整方案

WebSocket 有四个连接状态:CONNECTING(0)、OPEN(1)、CLOSING(2)、CLOSED(3)。断线重连的核心是在 onclose 回调中判断关闭原因,对异常关闭自动重建连接。基础实现需控制重连次数上限,避免无限重试消耗资源。重连间隔采用指数退避策略,初始延迟如 1 秒,每次翻倍,上限 30 秒,避免短时间内大量重连请求冲击服务端。同时要区分正常关闭(code 1000)和异常关闭,正常关闭不应触发重连。心跳检测是重连机制的重要补充,客户端定时发送 ping,若 pong 超时未返回则主动关闭连接触发重连。还需监听浏览器的 online/offline 事件,离线时暂停重连,恢复在线后再尝试连接。完整方案应包含:指数退避间隔、最大重连次数、心跳检测、优雅关闭判断、离线状态感知、连接状态通知上层、断线期间消息队列缓存。class ReconnectWebSocket { constructor(url) { this.url = url; this.reconnectCount = 0; this.maxReconnect = 5; this.heartbeatInterval = null; this.heartbeatTimeout = null; this.messageQueue = []; this.connect(); } connect() { this.ws = new WebSocket(this.url); this.ws.onopen = () => { this.reconnectCount = 0; this.startHeartbeat(); this.flushQueue(); }; this.ws.onclose = (e) => { this.stopHeartbeat(); if (e.code !== 1000) this.reconnect(); }; this.ws.onmessage = (e) => { if (e.data === 'pong') { clearTimeout(this.heartbeatTimeout); return; } this.onMessage(e.data); }; } reconnect() { if (this.reconnectCount >= this.maxReconnect) return; const delay = Math.min(1000 * Math.pow(2, this.reconnectCount), 30000); setTimeout(() => { this.reconnectCount++; this.connect(); }, delay); } startHeartbeat() { this.heartbeatInterval = setInterval(() => { if (this.ws.readyState === WebSocket.OPEN) { this.ws.send('ping'); this.heartbeatTimeout = setTimeout(() => this.ws.close(), 5000); } }, 30000); } send(data) { if (this.ws.readyState === WebSocket.OPEN) { this.ws.send(data); } else { this.messageQueue.push(data); } }}追问为什么用指数退避而不是固定间隔重连?固定间隔在服务端故障时会产生大量同步重连请求(雷群效应)。指数退避让重连间隔逐步增大,分散请求压力。加上随机抖动(jitter)效果更好:delay = min(base * 2^n + Math.random() * 1000, 30000),避免多个客户端同一时刻同时重连。哪些关闭码不应该触发重连?关闭码 1000 表示正常关闭,1001 表示端点离开(如页面关闭),都不应重连。需要重连的是异常关闭码:1006(异常关闭,无关闭帧)、1002/1003(协议错误)、1011/1012(服务端异常/重启)、1013(稍后重试)。判断逻辑:code !== 1000 && code !== 1001 时触发重连。心跳检测和重连怎么配合?心跳负责主动发现"静默断连"——TCP 连接已断但 onclose 未触发。客户端每隔 30 秒发 ping,服务端回 pong,5 秒内未收到 pong 则判定连接已死,调用 ws.close() 主动关闭,触发 onclose 再进入重连流程。没有心跳,客户端可能长时间不知道连接已断。离线检测怎么实现?监听 window.addEventListener('online', reconnect) 和 window.addEventListener('offline', stopReconnect)。离线时暂停重连定时器,避免无效请求;恢复在线后立即尝试连接,并重置退避计数。比单纯靠 onclose 重连更及时。断线期间的消息怎么处理?维护一个消息队列,连接断开时将待发送消息入队而非丢弃。重连成功后(onopen 触发)按顺序发送队列中的消息。对于实时性要求高的场景,重连后可主动请求服务端补发缺失数据,队列只作为兜底。