服务器
使用 Bun.serve 在 Bun 中启动高性能 HTTP 服务器
基本设置
const server = Bun.serve({
// `routes` 需要 Bun v1.2.3+
routes: {
// 静态路由
"/api/status": new Response("OK"),
// 动态路由
"/users/:id": req => {
return new Response(`你好,用户 ${req.params.id}!`);
},
// 按 HTTP 方法处理
"/api/posts": {
GET: () => new Response("帖子列表"),
POST: async req => {
const body = await req.json();
return Response.json({ created: true, ...body });
},
},
// 通配符路由,匹配所有以 "/api/" 开头且未被其他路由匹配的请求
"/api/*": Response.json({ message: "未找到" }, { status: 404 }),
// 从 /blog/hello 重定向到 /blog/hello/world
"/blog/hello": Response.redirect("/blog/hello/world"),
// 懒加载文件并提供服务
"/favicon.ico": Bun.file("./favicon.ico"),
},
// (可选)未匹配路由的回退处理:
// 如果 Bun 版本低于 1.2.3,则需要
fetch(req) {
return new Response("未找到", { status: 404 });
},
});
console.log(`服务器正在运行于 ${server.url}`);
HTML 导入
直接将 HTML 文件导入服务器代码,以构建同时包含服务器端和客户端代码的全栈应用。HTML 导入支持两种模式:
开发环境(bun --hot): Bun 会在运行时按需打包资源,并启用热模块替换(HMR):当你修改前端代码时,浏览器会更新,而无需完整重新加载页面。
生产环境(bun build): 使用 bun build --target=bun 构建时,import index from "./index.html" 语句会解析为一个预构建的清单对象,其中包含所有已打包的客户端资源。Bun.serve 会从此清单提供资源,无需在运行时进行打包。
import myReactSinglePageApp from "./index.html";
Bun.serve({
routes: {
"/": myReactSinglePageApp,
},
});
HTML 导入的功能不止是提供 HTML:它们还会运行 Bun 的 打包器、JavaScript 转译器和 CSS 解析器,因此你可以使用 React、TypeScript 和 Tailwind CSS 构建前端。
如需了解使用 HTML 导入构建全栈应用的完整指南,请参阅全栈开发服务器。
配置
更改 port 和 hostname
要配置服务器监听的端口和主机名,请在选项对象中设置 port 和 hostname。
Bun.serve({
port: 8080, // 默认使用 $BUN_PORT、$PORT、$NODE_PORT,否则为 3000 // [!code ++]
hostname: "mydomain.com", // 默认使用 "0.0.0.0" // [!code ++]
fetch(req) {
return new Response("404!");
},
});
设置 port 为 0 来随机选择一个可用端口。
const server = Bun.serve({
port: 0, // 随机端口 // [!code ++]
fetch(req) {
return new Response("404!");
},
});
// server.port 是随机选择的端口
console.log(server.port);
从服务器的 port 或 url 属性中读取所选端口。
console.log(server.port); // 3000
console.log(server.url); // http://localhost:3000
配置默认端口
有多个标志和环境变量可以设置默认端口,当未设置 port 选项时,Bun 会使用该端口。
--port命令行参数
bun --port=4002 server.ts
BUN_PORT环境变量
BUN_PORT=4002 bun server.ts
PORT环境变量
PORT=4002 bun server.ts
NODE_PORT环境变量
NODE_PORT=4002 bun server.ts
Unix 域套接字
要监听 Unix 域套接字,请传入 unix 选项并指定套接字路径。
Bun.serve({
unix: "/tmp/my-socket.sock", // 套接字路径
fetch(req) {
return new Response(`404!`);
},
});
抽象命名空间套接字
在 Linux 上,Bun 还支持抽象命名空间套接字:在 unix 路径前加上一个空字节。
Bun.serve({
unix: "\0my-abstract-socket", // 抽象命名空间套接字
fetch(req) {
return new Response(`404!`);
},
});
与 Unix 域套接字不同,抽象命名空间套接字不绑定到文件系统,并且在最后一个引用关闭时会自动删除。
HTTP/3 (QUIC)
Bun.serve 也可以通过 QUIC 监听 HTTP/3。将 http3: true 与 tls 一起设置;HTTP/3 需要 TLS。
Bun.serve({
tls: {
key: Bun.file("./key.pem"),
cert: Bun.file("./cert.pem"),
},
http3: true, // [!code ++]
fetch(req) {
return new Response("通过 HTTP/3 你好!");
},
});
启用 http3 后,服务器会在同一端口上同时通过 TCP(HTTP/1.1)和 UDP(HTTP/3)监听。HTTP/1.1 响应会包含一个 Alt-Svc 标头,声明 HTTP/3 端点,以便支持的客户端可以自动升级。
若要仅提供 HTTP/3 服务而完全不启用 TCP 监听,请设置 http1: false:
Bun.serve({
tls: {
key: Bun.file("./key.pem"),
cert: Bun.file("./cert.pem"),
},
http3: true,
http1: false, // [!code ++]
fetch(req) {
return new Response("仅 HTTP/3");
},
});
HTTP/2
设置 http2: true,即可在同一端口上使用与 HTTP/1.1 相同的路由和 fetch 处理程序提供 HTTP/2 服务。
Bun.serve({
tls: {
key: Bun.file("./key.pem"),
cert: Bun.file("./cert.pem"),
},
http2: true, // [!code ++]
fetch(req) {
return new Response("通过 HTTP/2 你好!");
},
});
使用 tls 时,Bun 会通过 ALPN 为每个连接选择协议。提供 h2 的客户端(浏览器、curl、node:http2)会使用 HTTP/2。其他客户端则使用 HTTP/1.1。
不使用 tls 时,Bun 会为使用 HTTP/2 前言(“先验知识”)建立连接的客户端提供 HTTP/2 服务,例如 curl --http2-prior-knowledge 或来自 node:http2 的 http2.connect("http://...")。其他连接则使用 HTTP/1.1。
设置 http1: false 可拒绝 HTTP/1.x 客户端。使用 TLS 时,提供 ALPN 但不包含 h2 的客户端会在握手时失败;完全不发送 ALPN 的客户端,以及未以该前言开头的明文连接,会收到 505 HTTP Version Not Supported。设置 http1: false 后,服务器无法接受 WebSocket 连接,因为 server.upgrade() 仅适用于 HTTP/1.1。
server.upgrade() 仅适用于 HTTP/1.1 请求。不支持 HTTP/2 上的 WebSocket、服务器推送和响应尾部标头(gRPC 所需)。
idleTimeout
默认情况下,Bun.serve 会在连接空闲 10 秒后将其关闭。没有数据正在发送或接收时,连接即为空闲。这也包括正在处理中的请求:处理程序仍在运行,但尚未向响应写入任何字节。浏览器和 fetch() 客户端会将其视为连接重置。
要进行配置,请设置 idleTimeout 字段(单位为秒)。最大值为 255,设置为 0 则完全禁用超时。
Bun.serve({
// 30 秒(默认值为 10)
idleTimeout: 30,
fetch(req) {
return new Response("Bun!");
},
});
export default 语法
无需将服务器选项传入 Bun.serve,你可以直接 export default 这些选项。
import type { Serve } from "bun";
export default {
fetch(req) {
return new Response("Bun!");
},
} satisfies Serve.Options<undefined>;
类型参数 <undefined> 是 WebSocket 数据类型。如果你添加了一个 WebSocket 处理程序,并通过 server.upgrade(req, { data: ... }) 附加自定义数据,请将 undefined 替换为你的数据类型。
可以直接运行此文件:当 Bun 检测到某个文件的 default 导出中包含 fetch 处理程序时,就会将该导出传入 Bun.serve。
热路由重载
使用 server.reload() 无需重启服务器即可更新路由:
const server = Bun.serve({
routes: {
"/api/version": () => Response.json({ version: "1.0.0" }),
},
});
// 部署新路由无停机
server.reload({
routes: {
"/api/version": () => Response.json({ version: "2.0.0" }),
},
});
服务器生命周期方法
server.stop()
停止服务器接受新连接:
const server = Bun.serve({
fetch(req) {
return new Response("Hello!");
},
});
// 优雅停止服务器(等待正在进行的请求完成)
await server.stop();
// 强制停止并关闭所有活跃连接
await server.stop(true);
默认情况下,stop() 会允许正在处理的请求和 WebSocket 连接完成。服务器会立即关闭空闲的 keep-alive 连接。正在处理请求的连接会在服务器发送响应后关闭。传入 true 则会立即终止所有连接。每当所有连接都关闭后,返回的 Promise 就会解析。
server.closeIdleConnections()
在不停止服务器的情况下,关闭当前未处理请求的 keep-alive 连接:
const closed = server.closeIdleConnections();
console.log(`closed ${closed} idle connections`);
它会返回已关闭的连接数量。正在处理请求的连接和已打开的 WebSocket 不受影响,服务器仍会接受新连接。此方法与 node:http 的 server.closeIdleConnections() 行为一致,后者不返回任何内容。
server.ref() 和 server.unref()
控制服务器是否保持 Bun 进程存活:
// 如果服务器是唯一运行的东西,不阻止进程退出
server.unref();
// 恢复默认行为 - 保持进程存活
server.ref();
server.reload()
无需重启即可更新服务器处理器:
const server = Bun.serve({
routes: {
"/api/version": Response.json({ version: "v1" }),
},
fetch(req) {
return new Response("v1");
},
});
// 更新为新处理器
server.reload({
routes: {
"/api/version": Response.json({ version: "v2" }),
},
fetch(req) {
return new Response("v2");
},
});
可将此方法用于开发和热重载。你只能更新 fetch、error、routes 和 websocket。
每请求控制
server.timeout(Request, seconds)
覆盖单个请求的空闲超时时间。传递 0 可以完全禁用该请求的超时时间。
const server = Bun.serve({
async fetch(req, server) {
// 给这个请求最多 60 秒的无活动时间,而不是默认的 10 秒
server.timeout(req, 60);
// 如果请求体发送超过 60 秒,则请求将被中止
await req.text();
return new Response("Done!");
},
});
使用 server.timeout(req, 0) 可让长时间运行的流式响应(例如服务器发送事件)保持连接,而无需为每个请求调高全局 idleTimeout。在 HTTP/2 上,超时作用于连接,因此其打开的请求中最宽松的超时值会生效:
Bun.serve({
routes: {
"/events": (req, server) => {
// 为此流式响应禁用空闲超时。
// 否则如果 10 秒内(默认的 idleTimeout)
// 没有发送字节,连接将被关闭。
server.timeout(req, 0);
return new Response(
async function* () {
yield "data: hello\n\n";
// 事件可以零星到达,而不会导致连接被终止
},
{ headers: { "Content-Type": "text/event-stream" } },
);
},
},
});
server.requestIP(Request)
获取客户端 IP 和端口信息:
const server = Bun.serve({
fetch(req, server) {
const address = server.requestIP(req);
if (address) {
return new Response(`Client IP: ${address.address}, Port: ${address.port}`);
}
return new Response("Unknown client");
},
});
对于已关闭的请求或 Unix 域套接字,返回 null。
服务器指标
server.pendingRequests 和 server.pendingWebSockets
使用内置计数器监控服务器活动:
const server = Bun.serve({
fetch(req, server) {
return new Response(
`活动请求数:${server.pendingRequests}\n` + `活动 WebSocket 数:${server.pendingWebSockets}`,
);
},
});
server.subscriberCount(topic)
获取某个 WebSocket 主题的订阅者数量:
const server = Bun.serve({
fetch(req, server) {
const chatUsers = server.subscriberCount("chat");
return new Response(`${chatUsers} 位用户正在聊天`);
},
websocket: {
message(ws) {
ws.subscribe("chat");
},
},
});
性能基准
以下 Bun 和 Node.js 服务器会对每个传入的 Request 响应 Bun!。
Bun.serve({
fetch(req: Request) {
return new Response("Bun!");
},
port: 3000,
});
require("http")
.createServer((req, res) => res.end("Bun!"))
.listen(8080);
在 Linux 上,Bun.serve 服务器每秒可处理请求数大约是 Node.js 的 2.5 倍。
| 运行环境 | 每秒请求数 |
|---|---|
| Node 16 | ~64,000 |
| Bun | ~160,000 |

实例示范:REST API
这是使用 Bun 路由的基本数据库驱动 REST API,无任何依赖:
import type { Post } from "./types.ts";
import { Database } from "bun:sqlite";
const db = new Database("posts.db");
db.exec(`
CREATE TABLE IF NOT EXISTS posts (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
created_at TEXT NOT NULL
)
`);
Bun.serve({
routes: {
// 列出帖子
"/api/posts": {
GET: () => {
const posts = db.query("SELECT * FROM posts").all();
return Response.json(posts);
},
// 创建帖子
POST: async req => {
const post: Omit<Post, "id" | "created_at"> = await req.json();
const id = crypto.randomUUID();
db.query(
`INSERT INTO posts (id, title, content, created_at)
VALUES (?, ?, ?, ?)`,
).run(id, post.title, post.content, new Date().toISOString());
return Response.json({ id, ...post }, { status: 201 });
},
},
// 根据 ID 获取帖子
"/api/posts/:id": req => {
const post = db.query("SELECT * FROM posts WHERE id = ?").get(req.params.id);
if (!post) {
return new Response("未找到", { status: 404 });
}
return Response.json(post);
},
},
error(error) {
console.error(error);
return new Response("内部服务器错误", { status: 500 });
},
});
export interface Post {
id: string;
title: string;
content: string;
created_at: string;
}
参考
interface Server extends Disposable {
/**
* 停止服务器接受新连接。
* @param closeActiveConnections 如果为 true,则立即终止所有连接
* @returns 服务器停止时解析的 Promise
*/
stop(closeActiveConnections?: boolean): Promise<void>;
/**
* Close idle keep-alive connections without stopping the server.
* @returns The number of connections closed
*/
closeIdleConnections(): number;
/**
* Update handlers without restarting the server.
* Only fetch, error, routes, and websocket can be updated.
*/
reload(options: Serve.Options<undefined>): Server;
/**
* 向运行中的服务器发出请求。
* 用于测试或内部路由。
*/
fetch(request: Request | string): Response | Promise<Response>;
/**
* 将 HTTP 请求升级为 WebSocket 连接。
* @returns 升级成功时为 true,否则为 false
*/
upgrade<T = undefined>(
request: Request,
options?: {
headers?: Bun.HeadersInit;
data?: T;
},
): boolean;
/**
* 向所有订阅了某个主题的 WebSocket 客户端发布消息。
* @returns 发送的字节数;如果消息被丢弃则为 0;如果应用了背压则为 -1
*/
publish(
topic: string,
data: string | ArrayBufferView | ArrayBuffer | SharedArrayBuffer,
compress?: boolean,
): ServerWebSocketSendStatus;
/**
* 获取订阅了某个主题的 WebSocket 客户端数量。
*/
subscriberCount(topic: string): number;
/**
* 获取客户端 IP 地址和端口。
* @returns 对于已关闭的请求或 Unix 套接字,返回 null
*/
requestIP(request: Request): SocketAddress | null;
/**
* 为请求设置自定义空闲超时时间。
* @param seconds 超时时间(以秒为单位),设为 0 可禁用
*/
timeout(request: Request, seconds: number): void;
/**
* 服务器运行时保持进程处于活动状态。
*/
ref(): void;
/**
* 允许服务器作为唯一运行中的运行时进程退出。
*/
unref(): void;
/** 当前正在处理的 HTTP 请求数量 */
readonly pendingRequests: number;
/** 当前活动的 WebSocket 连接数量 */
readonly pendingWebSockets: number;
/** 完整的服务器 URL,包括协议、主机名和端口 */
readonly url: URL;
/** 服务器正在监听的端口 */
readonly port: number;
/** 服务器绑定到的主机名 */
readonly hostname: string;
/** 服务器是否处于开发模式 */
readonly development: boolean;
/** 服务器实例 ID */
readonly id: string;
}
interface WebSocketHandler<T = undefined> {
/** WebSocket 消息的最大大小(以字节为单位) */
maxPayloadLength?: number;
/** 应用背压前排队消息的字节数 */
backpressureLimit?: number;
/** 达到背压限制时是否关闭连接 */
closeOnBackpressureLimit?: boolean;
/** 背压解除时调用 */
drain?(ws: ServerWebSocket<T>): void | Promise<void>;
/** 空闲超时时间(以秒为单位) */
idleTimeout?: number;
/** 启用按消息进行的 deflate 压缩 */
perMessageDeflate?:
| boolean
| {
compress?: WebSocketCompressor | boolean;
decompress?: WebSocketCompressor | boolean;
};
/** 发送 Ping 帧以保持连接处于活动状态 */
sendPings?: boolean;
/** Whether ws.publish() also sends the message to the socket that published it, if subscribed */
publishToSelf?: boolean;
/** 连接打开时调用 */
open?(ws: ServerWebSocket<T>): void | Promise<void>;
/** 收到消息时调用 */
message(ws: ServerWebSocket<T>, message: string | Buffer): void | Promise<void>;
/** 连接关闭时调用 */
close?(ws: ServerWebSocket<T>, code: number, reason: string): void | Promise<void>;
/** 收到 Ping 帧时调用 */
ping?(ws: ServerWebSocket<T>, data: Buffer): void | Promise<void>;
/** 收到 Pong 帧时调用 */
pong?(ws: ServerWebSocket<T>, data: Buffer): void | Promise<void>;
}
interface TLSOptions {
/** 证书颁发机构链 */
ca?: string | Buffer | BunFile | Array<string | Buffer | BunFile>;
/** 服务器证书 */
cert?: string | Buffer | BunFile | Array<string | Buffer | BunFile>;
/** DH 参数文件的路径 */
dhParamsFile?: string;
/** 私钥 */
key?: string | Buffer | BunFile | Array<string | Buffer | BunFile>;
/** 降低 TLS 内存使用量 */
lowMemoryMode?: boolean;
/** 私钥密码短语 */
passphrase?: string;
/** OpenSSL 选项标志 */
secureOptions?: number;
/** SNI 服务器名称 */
serverName?: string;
}