← StrongNode与Node.js区别 Node.js 安全实践 →

Node.js服务器类型:HTTP、HTTPS、WebSocket、REST与UDP的职责边界与选型依据

著
原创 2026-10-20 Node.js 已有人查阅

一、选型的起点不是“用哪个模块”,而是通信模式

在Node.js中创建服务器,语法层面并不复杂:http.createServer()、https.createServer()、dgram.createSocket()都是一行调用。真正需要判断的是通信模式本身:客户端与服务端之间是请求-响应还是双向持续通信,数据是否需要可靠到达,消息边界是否必须保留。

把这几个问题回答清楚,服务器类型的选择基本就确定了。反过来,如果从“听说WebSocket性能好”或“UDP比较快”出发做决定,很容易在后续遇到与业务需求不匹配的约束。

二、HTTP与HTTPS:同一套模型,加密层差异

http.createServer()返回的http.Server实例,核心行为由请求-响应模型决定:客户端发起请求,服务端返回响应,连接在响应结束后可以关闭或复用。HTTPS在此基础上增加了TLS握手和加密传输,https.createServer(options,listener)的options需要提供key和cert,其余请求处理逻辑与HTTP一致。https.Server继承自tls.Server,事件模型与http.Server相同。

一个在版本迁移中值得留意的细节是keepAliveTimeout的默认值。Node.js当前文档中http.createServer的keepAliveTimeout默认值为65000毫秒(65秒),headersTimeout默认值为60000毫秒(60秒)。这与许多旧资料中记录的5000毫秒不同。如果服务端部署在反向代理或负载均衡器之后,代理侧的空闲超时可能短于65秒,导致客户端复用的连接在代理处已被关闭,而Node.js服务端尚未感知。这类问题表现为偶发的ECONNRESET,排查时需要同时核对代理配置和服务端的keepAliveTimeout,而非只调整其中一端。

HTTPS的额外成本不只在加解密计算。证书管理、TLS版本协商、密钥长度限制都会影响实际可用性。Node.js24搭载的OpenSSL3.5将默认安全等级提升到2,低于2048位的RSA密钥和低于224位的ECC密钥会被拒绝。在使用自签名证书或遗留系统的证书升级场景中,握手失败的原因可能不在Node.js代码,而在证书本身的密钥强度。

三、WebSocket:Node.js没有内置,Express也没有

WebSocket的全双工通信能力不来自Node.js核心模块。http模块只提供了'upgrade'事件,允许开发者接管协议升级请求。帧的解析、掩码处理、分片重组、ping/pong控制帧,由ws或类似库实现。

ws的典型用法是挂载到已有的http.Server上,或使用noServer:true配合手动处理upgrade事件,以便在同一端口上共存多个WebSocket服务或与普通HTTP路由共享端口。这个模式在需要鉴权的场景中尤其重要:Express的中间件链只作用于HTTP请求,WebSocket握手完成后的消息帧不经过app.use()。鉴权必须在upgrade事件或connection回调中完成。

心跳机制不是可选项。TCP连接在客户端断电、切换网络或中间设备静默断开时,不会立即触发close事件。连接对象会留在wss.clients中,继续接收发送尝试但实际无法到达对端。定期的ping/pong检测配合超时terminate()是长连接服务的必要逻辑。ws的perMessageDeflate默认在服务端关闭、客户端开启,官方文档提示该扩展在高并发下可能带来内存碎片和性能下降,建议仅在确有压缩需求时启用,并针对实际负载做基准测试。

四、RESTAPI服务器:Express是框架,不是协议

REST不是一种服务器类型,而是一种基于HTTP方法语义的接口设计风格。express提供的是路由和中间件机制,底层仍然是http.Server。app.listen()内部调用的是http.Server.listen()。

Express5要求Node.js18或更高版本。在路由定义中,app.get()、app.post()、app.put()、app.delete()的方法名直接对应HTTP方法,但语义一致性需要开发者自己保证。用app.put()处理“只更新传入字段”的请求,在语义上接近PATCH而非PUT。PUT的规范语义是整体替换:客户端发送完整的资源表示,未发送的字段应被视为清除。如果服务端实现的是部分更新,却挂在PUT路由下,对接方按标准理解时会出错。

REST的另一个实际问题是响应体积的控制。GET/api/users返回完整的用户对象列表,而调用方可能只需要id和name。这是over-fetching的典型场景。GraphQL或字段筛选参数可以缓解,但引入了新的复杂度。在内部服务之间,约定固定的精简响应结构往往比引入查询语言更直接。

五、UDP服务器:轻量但不可靠

dgram.createSocket('udp4')创建的UDP套接字是无连接的。服务端不需要listen和accept,直接bind到端口后即可通过'message'事件接收数据报,通过send()回复。rinfo参数携带发送方的地址和端口,服务端据此回送响应。

UDP的核心约束是不保证送达。官方文档明确指出,发送超过接收方MTU的数据报会被静默丢弃,发送方不会收到通知。IPv4的理论上限是65507字节(扣除IP头和UDP头),但实际可用大小取决于路径上每一跳的MTU。在跨公网或经过隧道封装的场景中,超过1500字节的数据报被分片或丢弃的概率显著增加。

这个特性决定了UDP的适用场景:DNS查询(单次请求-响应,数据量小,丢包可重试)、实时音视频(容忍少量丢包,不容忍延迟)、游戏状态同步(频繁发送小数据报,旧数据被新数据覆盖)。把需要可靠传输的文件或JSON文档放在UDP上发送,是在用应用层重新实现TCP的确认与重传机制,复杂度高于直接使用TCP或HTTP。

六、示例:同一端口上的HTTP与WebSocket共存

以下示例演示一个http.Server同时处理普通HTTP请求和WebSocket升级,并在upgrade阶段完成简单鉴权。示例中的code_number为代码号学习编程的标识。

const http = require('node:http');
const { WebSocketServer } = require('ws');

const server = http.createServer((req, res) => {
  if (req.url === '/code_number/health') {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('ok');
    return;
  }
  res.writeHead(404);
  res.end();
});

const wss = new WebSocketServer({ noServer: true });

server.on('upgrade', (req, socket, head) => {
  const token = new URL(req.url, 'http://localhost').searchParams.get('token');
  if (token !== 'code_number_secret') {
    socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
    socket.destroy();
    return;
  }
  wss.handleUpgrade(req, socket, head, (ws) => {
    wss.emit('connection', ws, req);
  });
});

wss.on('connection', (ws) => {
  ws.on('message', (data) => {
    ws.send(`收到: ${data}`);
  });
});

server.listen(3000, () => {
  console.log('HTTP + WebSocket 服务运行在 3000 端口');
});

noServer:true的作用是让ws不自行创建HTTP服务器,而是由开发者手动调用handleUpgrade。这样可以在升级之前插入鉴权、路由或其他检查。如果鉴权失败,直接写入401响应并销毁socket,不进入WebSocket协议层。这个结构比在connection回调中再检查token更合理:后者意味着协议升级已经完成,消耗了握手资源。

七、常见错误与边界情况

HTTPEADDRINUSE:端口已被占用。server.on('error')可以捕获,但如果没有注册监听器,进程会因未处理的'error'事件崩溃。在容器化部署中,端口冲突可能来自残留进程或编排配置重叠。

HTTPS证书加载失败:fs.readFileSync('key.pem')在文件不存在时抛出ENOENT,证书格式错误在https.createServer()时抛出ERR_OSSL_*系列错误。自签名证书在客户端rejectUnauthorized:true(默认值)下会被拒绝,测试环境中需要显式关闭或提供CA。

WebSocket连接不触发close:TCP半开连接不会立即产生close事件。没有心跳机制时,wss.clients中的连接计数会持续增长,内存和文件描述符被占用。ws提供了isAlive模式的ping/pong心跳示例,需要定期执行并清理未响应的连接。

UDPEMSGSIZE:发送超过系统限制的数据报时抛出。即使未超过理论上的65507字节,经过MTU较小的链路时仍可能被静默丢弃,发送方无感知。

八、测试中的验证策略

HTTP服务器的测试可以使用node:test配合fetch或http.request向临时端口发起请求。server.listen(0)让操作系统分配随机端口,避免测试间的端口冲突,通过server.address().port获取实际端口。

WebSocket的测试需要同时启动服务端和客户端,或在同一个测试文件中用ws创建客户端连接。鉴权逻辑的测试应当覆盖无token、错误token、正确token三种情况,确认upgrade事件中的分支行为。

UDP的测试不需要建立连接,但需要处理异步时序:服务端bind完成后再发送,客户端收到响应后关闭。dgram的send回调只表示数据报已交给操作系统,不表示对端已收到。测试中不应依赖回调的时机判断送达。

九、练习与思考

  1. 一个服务部署在Nginx反向代理之后,Nginx的proxy_read_timeout为60秒,Node.js的keepAliveTimeout使用默认值。客户端每隔55秒发起一次请求,可能遇到什么问题?调整哪一端的配置更合适?

  2. WebSocket服务使用wss.clients.size监控在线连接数。在没有心跳机制的情况下,这个数值在客户端异常断开后会如何变化?对容量规划和计费逻辑有什么影响?

  3. 一个内部服务需要传输每秒100条的小型状态更新,每条约200字节,允许偶尔丢失但要求低延迟。HTTP轮询、WebSocket和UDP分别适合吗?给出判断依据而非直接结论。

十、延伸阅读与参考文献

Node.js官方文档http.createServer(options),nodejs.org,列出keepAliveTimeout默认65000毫秒、headersTimeout默认60000毫秒以及requestTimeout等选项的新默认值。https://beta.docs.nodejs.org/http/createServer

Node.js官方文档https.createServer(options[,requestListener]),nodejs.org历史版本镜像,说明https.Server继承自tls.Server,事件模型与http.Server一致,options与tls.createServer()相同。https://unencrypted.nodejs.org/download/nightly/v6.7.1-nightly20161001d7454e7547/docs/api/https.html

Node.js官方文档dgram模块,nodejs.org历史版本镜像,说明UDP数据报超过MTU会被静默丢弃、发送方不会收到通知,以及IPv4数据报的理论上限计算方式。https://nodejs.org/download/rc/v12.16.1-rc.0/docs/api/dgram.json

ws库npm文档,包含noServer模式、perMessageDeflate默认状态与性能警告、外部HTTP/S服务器挂载示例。https://www.npmjs.com/package/@kevisual/ws/v/8.0.0

Express5.xAPI文档,expressjs.com,说明app.listen()与http.Server.listen()的等价关系,以及Express5要求Node.js18或更高版本。https://expressjs.com/llms/api-5x.txt

Express4.xAPI文档,expressjs.com,提供app.all('*')等路由定义方式的历史参考,以及app.listen返回http.Server对象的说明。https://expressjs.com/llms/api-4x.txt

Node.jsv22到v24迁移指南,nodejs.org,记录OpenSSL3.5默认安全等级2对密钥长度的限制,以及Node.js24中若干API的移除与行为变更。https://nodejs.org/ta/blog/migrations/v22-to-v24

← StrongNode与Node.js区别 Node.js 安全实践 →
分享笔记 (共有 篇笔记)
验证码:
微信公众号