HTTP
http 是 Node 内置的TCP + HTTP 协议封装,基于事件驱动。
const http = require('http')
const server = http.createServer((req, res) => {
// 每次请求都会进来
})
server.listen(3000)
这行回调本质就是:
server.on('request', (req, res) => { ... })
从客户端连进来 → 发请求 → 响应 → 断开,完整事件顺序如下:
-
- TCP 连接建立
server.on('connection', (socket) => {
// 客户端刚连上 TCP
// 参数只有:socket(net.Socket)
})
-
- 收到 HTTP 请求
server.on('request', (req, res) => {
// 每次浏览器/接口访问都会触发
// 参数:req(请求), res(响应)
})
-
- req 身上的事件(接收数据)
let body = ''
req.on('data', (chunk) => {
// 收到一段 body 数据
body += chunk
})
req.on('end', () => {
// 数据接收完毕
})
req.on('error', (err) => {})
req.on('aborted', () => {
// 客户端中途断开
})
aborted:请求被中途取消 / 断开(客户端主动断连、网络断了、浏览器关掉);
error:请求底层出错(网络错误、解析错误、连接异常崩溃)
req:请求对象(IncomingMessage),可读流。
最常用属性有:
req.method // 请求方法:GET / POST / PUT...
req.url // 请求地址:/api/user / /xxx
req.headers // 所有请求头(对象)
req.httpVersion // HTTP版本:1.1
-
- res 身上的事件(发送响应)
// 响应头 + 响应体 已经全部发送给客户端,Node 已经把数据交给操作系统内核,马上就到客户端
res.on('finish', () => {
// 响应已经发给客户端
})
// 底层 TCP 连接关闭了(socket 关闭, keep-alive),可以是客户端中途断开,也可以是服务器主动关闭
res.on('close', () => {
// 连接被关闭
})
res.on('error', (err) => {})
res:响应对象(ServerResponse)
最核心方法有:
res.writeHead(状态码,响应头)
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' })
res.end(数据) 结束响应,把数据返回给客户端(必须调用)
res.end('hello')
res.end('<h1>Hi</h1>')
res.end(JSON.stringify({ name: 'zs' }))
res.write(数据) 向客户端写数据(可多次调用,最后必须 end)
res.write('hello')
res.write(' world')
res.end()
res.setHeader(name, value)单独设置响应头
res.setHeader('Content-Type', 'text/plain')
res.setHeader('X-Custom', 'test')
res.statusCode直接设置状态码(不用 writeHead)
res.statusCode = 404
-
- 协议升级(WebSocket 专用)
server.on('upgrade', (req, socket, head) => {
// 客户端要从 HTTP 升级成 WebSocket
// 参数:req, socket, head(已读缓冲)
})
-
- 错误事件(必写)
server.on('error', (err) => { // 端口被占用、服务异常 })
-
- 服务关闭
server.on('close', () => { // server.close() 后触发 })
HTTP 服务完整生命周期流程图:
const http = require('http')
const server = http.createServer()
// 1. TCP 连接
server.on('connection', (socket) => {
console.log('客户端连接:', socket.remoteAddress)
})
// 2. HTTP 请求
server.on('request', (req, res) => {
console.log('请求:', req.method, req.url)
let body = ''
req.on('data', (chunk) => { body += chunk })
req.on('end', () => {
res.end('hello')
})
// 执行res.end('hello')触发
res.on('finish', () => {
console.log('响应发送完成')
})
})
// 3. WebSocket 升级
server.on('upgrade', (req, socket, head) => {
console.log('升级协议')
})
// 4. 错误
server.on('error', (err) => {
console.log('服务器错误', err)
})
server.listen(3000, () => {
console.log('run on 3000')
})
TCP
Node.js 原生 TCP 模块就是net 模块。它的作用是:
- 创建 TCP 服务器(监听连接)
- 创建 TCP 客户端(连接服务器)
- 实现长连接、双向通信(聊天室、游戏、WebSocket 底层都靠它)
HTTP 是基于 TCP 封装的协议,所以 http 模块底层 = net 模块。
TCP 通信里只有两个东西:
- 服务器:
net.createServer() - 连接(Socket):客户端和服务器之间的双向通道,Socket 既是可读流,又是可写流,能发数据、能收数据、能监听关闭、错误。
创建 TCP 服务器
const net = require('net')
// 1. 创建 TCP 服务器
const server = net.createServer((socket) => {
console.log('客户端已连接')
// 获取客户端信息
console.log('客户端IP:', socket.remoteAddress)
console.log('客户端端口:', socket.remotePort)
// 2. 收到客户端发来的数据
socket.on('data', (chunk) => {
console.log('收到:', chunk.toString())
// 3. 给客户端回消息
socket.write('服务器已收到:' + chunk.toString())
})
// 4. 客户端断开连接
socket.on('end', () => {
console.log('客户端断开连接')
})
// 5. 连接错误
socket.on('error', (err) => {
console.log('客户端异常:', err.message)
})
})
// 监听端口
server.listen(3000, () => {
console.log('TCP 服务器已启动 :3000')
})
// 服务器自身错误
server.on('error', (err) => {
console.log('服务器异常:', err.message)
})
TCP 客户端(连接 TCP 服务器)
const net = require('net')
// 连接服务器
const client = net.createConnection({ port: 3000 }, () => {
console.log('已连接到服务器')
// 发送数据
client.write('Hello TCP Server!')
})
// 收到服务器返回的数据
client.on('data', (chunk) => {
console.log('服务器说:', chunk.toString())
})
// 服务器断开
client.on('end', () => {
console.log('服务器断开连接')
})
Socket(连接对象)
Socket = 客户端与服务端之间的通信通道。
Socket 既是可读流,也是可写流,所以它是双工流(Duplex)。
从 API 上一眼就能看出来,可读流的方法(读数据):
socket.on('data', (chunk) => {})
socket.on('end', () => {})
socket.pause()
socket.resume()
可写流的方法(发数据):
socket.write('hello')
socket.end()
net 模块 4 个核心点
- net.createServer → 创建 TCP 服务
- connection → 客户端连上来,给你一个 socket
- socket.on('data') → 接收消息
- socket.write() → 发送消息
UDP
UDP = User Datagram Protocol,和 TCP 一样,都在传输层,基于IP 协议,发的是数据报(Datagram),无连接、不可靠、速度极快。
TCP vs UDP 对比:
UDP 适合什么场景?
允许丢一点包,但必须快、实时性强的场景
- 游戏实时同步
- 语音通话 / 视频会议
- 直播
- DNS 查询
- 心跳包
- 实时监控数据
- 局域网高速通信
不适合:
- 接口请求
- 文件传输
- 支付
- 登录
- 微服务 RPC(除非自己做可靠性)
对于 DNS 查询的场景,DNS 是查询,不是传文件,不是支付,丢了就丢了,客户端等 200ms 没收到,再发一次就行,成本极低、体验几乎不受影响。
这种场景,UDP 的 “不可靠” 完全不是问题。
Node.js 中的 UDP:dgram 模块
UDP 不是 Stream,不是双工流,它是数据报。
UDP 服务器:
const dgram = require('dgram')
const server = dgram.createSocket('udp4') // udp4 = IPv4
// 收到消息
server.on('message', (msg, remoteInfo) => {
console.log('收到:', msg.toString())
console.log('来自:', remoteInfo.address, remoteInfo.port)
// 给客户端回消息
server.send('已收到: ' + msg, remoteInfo.port, remoteInfo.address)
})
// 启动监听
server.bind(3000, () => {
console.log('UDP 服务器启动 :3000')
})
客户端:
const dgram = require('dgram')
const client = dgram.createSocket('udp4')
const msg = Buffer.from('Hello UDP')
// 发消息
client.send(msg, 3000, 'localhost', (err) => {
console.log('消息已发送')
})
// 接收服务器回复
client.on('message', (msg) => {
console.log('服务器回:', msg.toString())
})
- TCP Socket = Duplex 双工流(字节流)
- UDP = 数据报(一包一包发,一包一包收)
UDP 没有:
- data 事件
- end 事件
- finish 事件
- pipe
- write 持续写入
它只有:
message:收到一个包send:发一个包errorclose
RPC
RPC = Remote Procedure Call(远程过程调用)
像调用本地函数一样,调用另一台机器上的函数。
举例:你在 A 机器:
js
const user = getUserById(100)
实际这个函数 在 B 机器上执行,但你写起来跟本地一样。
这就是 RPC 的核心思想:隐藏网络通信细节,让分布式调用像本地调用一样简单。
要实现 “像本地函数一样调用”,RPC 框架必须做 4 件事:
-
序列化 / 反序列化:把参数、返回值转成二进制(Protobuf)
-
传输协议:TCP、HTTP、HTTP/2
-
协议格式(数据分包):解决 TCP 粘包,比如:
长度 + 消息ID + 数据 -
服务寻址与路由:知道调用哪台机器、哪个服务、哪个方法
这 4 件事组合起来,才叫一个 RPC 框架。
RPC 执行流程:
全程你无感,只觉得调用了一个本地方法。
RPC 和 HTTP 的区别
HTTP:
- 基于应用层协议
- 文本协议,头信息大
- 接口风格 RESTful
- 通用、浏览器能直接调
- 适合:对外接口、前端调用、跨公司对接
RPC:
- 自定义应用协议
- 一般是二进制协议,更小更快
- 方法调用风格:服务名.方法名 (参数)
- 长连接、高并发、低延迟
- 适合:微服务内部调用、后端集群互相调用
一句话总结:
- 对外 → HTTP
- 对内微服务 → RPC
RPC 底层大部分都采用的是 TCP,也可用 HTTP。
都说 RPC 用纯 TCP 性能最好,那为什么王者 gRPC 非要跑在 HTTP/2 上?
这是因为 gRPC 选择 HTTP/2,是为了在 “TCP 的高性能” 和 “互联网通用标准” 之间做平衡。
它既拿到了 TCP 的速度,又拿到了 HTTP/2 的成熟特性,不用自己造轮子。
HTTP/2 并不是抛弃 TCP,而是在 TCP 之上做了极度优化的 HTTP 协议。
所以:
- 纯 TCP RPC = 自己实现所有高级特性
- gRPC = 站在 HTTP/2 肩膀上,直接复用成熟能力
gRPC 用 HTTP/2 的真正原因:
-
多路复用:HTTP/2 支持 一个 TCP 连接,同时并发多个请求,互不阻塞。纯 TCP RPC 要实现多路复用,必须自己造。
-
成熟、标准化、全平台支持
-
自带头部压缩、二进制分帧:HTTP/1.1 头大、慢。HTTP/2 是二进制协议,头压缩(HPACK),极快。gRPC 直接复用,不用自己实现压缩、帧格式。
-
TLS 天然支持: 互联网环境必须加密。HTTP/2 几乎总是带 TLS,gRPC 直接安全可用,不用自己处理加密。
等等优点...
手写一个极简 RPC
- 服务端(提供函数)
const net = require('net')
const server = net.createServer(socket => {
socket.on('data', data => {
const req = JSON.parse(data)
// 模拟方法
if (req.method === 'add') {
const result = req.params.a + req.params.b
socket.write(JSON.stringify({ result }))
}
})
})
server.listen(4000)
- 客户端(像调用本地函数)
const net = require('net')
// RPC 代理:像本地函数一样调用
function add(a, b) {
return new Promise(resolve => {
const socket = net.createConnection({ port: 4000 })
socket.write(JSON.stringify({
method: 'add',
params: { a, b }
}))
socket.on('data', data => {
const res = JSON.parse(data)
resolve(res.result)
})
})
}
// 使用!像本地函数一样
async function test() {
const sum = await add(3, 5)
console.log(sum) // 8
}
test()
webSocket
HTTP 的协议格式我们很清楚,就是 header、body 这些。
那 WebSocket 的协议格式是什么样的呢?
WebSocket 是一种基于 TCP 的、浏览器支持的、全双工长连接协议。目的是:让客户端和服务器能随时互相发消息,不用再反复 HTTP 请求。
实时的双向数据通信,我们一般会用 WebSocket 来做。
HTTP 有个致命缺点:只能客户端主动问,服务器不能主动推。
以前要做实时消息 / 聊天 / 通知,只能用轮询:每秒发一次请求(浪费、延迟高)。
WebSocket 出来后:一次连接,永久双向通信,服务器想发就发。
WebSocket 核心特点:
- 基于 TCP(可靠、不丢包)
- 全双工:客户端 ↔ 服务器 同时收发
- 长连接:连接一直保持,不用重复握手
- 轻量级协议头:只有 2~10 字节,比 HTTP 轻太多
- 浏览器原生支持:ws://、wss://
- 能跨域(靠服务端配置)
下面我们手写一个一个websocket协议。
import EventEmitter from 'node:events'
import http from 'node:http'
import crypto from 'node:crypto'
const OPCODES = {
CONTINUE: 0,
TEXT: 1, // 文本
BINARY: 2, // 二进制
CLOSE: 8,
PING: 9,
PONG: 10,
}
function hashKey(key) {
const sha1 = crypto.createHash('sha1')
sha1.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
return sha1.digest('base64')
}
function handleMask(maskBytes, data) {
const payload = Buffer.alloc(data.length)
for (let i = 0; i < data.length; i++) {
payload[i] = maskBytes[i % 4] ^ data[i]
}
return payload
}
// 用于将数据打包成符合 WebSocket 协议格式的二进制帧
function encodeMessage(opcode, payload) {
// payload.length < 126
// WebSocket 帧头部固定 2 字节(对于小于 126 字节的负载)
let bufferData = Buffer.alloc(payload.length + 2 + 0)
// 设置 FIN 为 1
let byte1 = parseInt('10000000', 2) | opcode
let byte2 = payload.length
bufferData.writeUInt8(byte1, 0)
bufferData.writeUInt8(byte2, 1)
// 将负载数据复制到 Buffer 的第 3 字节开始的位置(跳过 2 字节头部)
payload.copy(bufferData, 2)
return bufferData
}
export class MyWebsocket extends EventEmitter {
constructor(options) {
super(options)
const server = http.createServer()
server.listen(options.port || 8080)
// 当客户端请求升级 HTTP 连接到 WebSocket 时触发此事件。
server.on('upgrade', (req, socket) => {
// 保存 socket 引用并启用 TCP keep-alive 保持连接活跃。
this.socket = socket
socket.setKeepAlive(true)
const resHeaders = [
'HTTP/1.1 101 Switching Protocols',
'Upgrade: websocket',
'Connection: Upgrade',
'Sec-WebSocket-Accept: ' + hashKey(req.headers['sec-websocket-key']),
'',
'',
].join('\r\n')
// 使用 101 Switching Protocols 状态码完成 HTTP → WebSocket 的协议切换。
socket.write(resHeaders)
// 处理客户端发来的数据,都是二进制数据
socket.on('data', (data) => {
console.log(data)
// 解析二进制数据为人能看懂的字符
this.processData(data)
})
socket.on('close', (error) => {
this.emit('close')
})
})
}
processData(bufferData) {
// 读取第一个字节
const byte1 = bufferData.readUInt8(0)
// byte1 & 0x0f 保留低 4 位,即提取 opcode(操作码)0x0f = 00001111
let opcode = byte1 & 0x0f
const byte2 = bufferData.readUInt8(1)
// 转为二进制字符串'10001100'
const str2 = byte2.toString(2)
const MASK = str2[0]
let curByteIndex = 2
let payloadLength = parseInt(str2.substring(1), 2)
console.log('====payloadLength', payloadLength)
if (payloadLength === 126) {
// 126:表示长度用接下来的 2 字节(16位)表示,最大 65535
payloadLength = bufferData.readUInt16BE(2)
curByteIndex += 2
} else if (payloadLength === 127) {
// 127:表示长度用接下来的 8 字节(64位)表示
payloadLength = bufferData.readBigUInt64BE(2)
curByteIndex += 8
}
let realData = null
if (MASK) {
const maskKey = bufferData.slice(curByteIndex, curByteIndex + 4)
curByteIndex += 4
const payloadData = bufferData.slice(
curByteIndex,
curByteIndex + payloadLength,
)
realData = handleMask(maskKey, payloadData)
}
this.handleRealData(opcode, realData)
}
handleRealData(opcode, realDataBuffer) {
switch (opcode) {
case OPCODES.TEXT:
// 触发事件
this.emit('data', realDataBuffer.toString('utf-8'))
break
case OPCODES.BINARY:
this.emit('data', realDataBuffer)
break
default:
this.emit('close')
break
}
}
// 把服务端的数据转化为符合websocket协议的二进制数据,并发送
send(data) {
let opcode
let buffer
if (Buffer.isBuffer(data)) {
opcode = OPCODES.BINARY
buffer = data
} else if (typeof data === 'string') {
opcode = OPCODES.TEXT
buffer = Buffer.from(data, 'utf8')
} else {
console.error('暂不支持发送的数据类型')
}
this.doSend(opcode, buffer)
}
doSend(opcode, bufferDatafer) {
this.socket.write(encodeMessage(opcode, bufferDatafer))
}
}
上面创建了一个websocket的类,根据这个类就可以创建一个websocket服务:
import { MyWebsocket } from './ws.mjs'
const ws = new MyWebsocket({ port: 8080 })
// 因为继承了EventEmitter,所以可以监听事件
ws.on('data', (data) => {
console.log('receive data:' + data)
// 往客户端发送数据
setInterval(() => {
ws.send(data + ' ' + Date.now())
}, 1000)
})
ws.on('close', (code, reason) => {
console.log('close:', code, reason)
})
客户端代码:
<!doctype html>
<html>
<body>
<script>
const ws = new WebSocket('ws://localhost:8080')
ws.onopen = function () {
ws.send('发送数据')
// setTimeout(() => {
// ws.send('发送数据2')
// }, 3000)
}
ws.onmessage = function (evt) {
console.log(evt)
}
ws.onclose = function () {}
</script>
</body>
</html>