跳至主要内容

HTTP 服务器 API

Deno 目前有两个 HTTP 服务器 API

一个 "Hello World" 服务器

要启动一个指定端口的 HTTP 服务器,可以使用 Deno.serve 函数。该函数接受一个处理函数,该函数将针对每个传入请求被调用,并预期返回一个响应(或一个解析为响应的 Promise)。

以下是一个服务器的示例,它针对每个请求返回一个 "Hello, World!" 响应

Deno.serve((_req) => {
return new Response("Hello, World!");
});

ℹ️ 处理程序也可以返回一个 Promise<Response>,这意味着它可以是一个 async 函数。

默认情况下,Deno.serve 将监听端口 8000,但可以通过在选项包中传递端口号作为第一个或第二个参数来更改。

// To listen on port 4242.
Deno.serve({ port: 4242 }, handler);

// To listen on port 4242 and bind to 0.0.0.0.
Deno.serve({ port: 4242, hostname: "0.0.0.0", handler });

检查传入请求

大多数服务器不会对每个请求都以相同的响应进行回答。相反,它们会根据请求的各个方面改变它们的答案:HTTP 方法、头信息、路径或正文内容。

请求作为处理函数的第一个参数传入。以下是一个示例,展示了如何提取请求的各个部分

Deno.serve(async (req) => {
console.log("Method:", req.method);

const url = new URL(req.url);
console.log("Path:", url.pathname);
console.log("Query parameters:", url.searchParams);

console.log("Headers:", req.headers);

if (req.body) {
const body = await req.text();
console.log("Body:", body);
}

return new Response("Hello, World!");
});

⚠️ 请注意,如果用户在完全接收正文之前挂断了连接,则 req.text() 调用可能会失败。确保处理这种情况。请注意,这可能会发生在所有从请求正文读取的方法中,例如 req.json()req.formData()req.arrayBuffer()req.body.getReader().read()req.body.pipeTo() 等。

响应一个响应

大多数服务器也不会对每个请求都以 "Hello, World!" 进行响应。相反,它们可能会以不同的头信息、状态码和正文内容(甚至正文流)进行响应。

以下是一个返回带有 404 状态码、JSON 正文和自定义头信息的响应的示例

Deno.serve((req) => {
const body = JSON.stringify({ message: "NOT FOUND" });
return new Response(body, {
status: 404,
headers: {
"content-type": "application/json; charset=utf-8",
},
});
});

响应正文也可以是流。以下是一个返回每秒重复一次 "Hello, World!" 的流的响应的示例

Deno.serve((req) => {
let timer: number;
const body = new ReadableStream({
async start(controller) {
timer = setInterval(() => {
controller.enqueue("Hello, World!\n");
}, 1000);
},
cancel() {
clearInterval(timer);
},
});
return new Response(body.pipeThrough(new TextEncoderStream()), {
headers: {
"content-type": "text/plain; charset=utf-8",
},
});
});

ℹ️ 注意这里的 cancel 函数。当客户端挂断连接时,会调用此函数。这很重要,因为要确保你处理这种情况,否则服务器将永远继续排队消息,最终耗尽内存。

⚠️ 注意,当客户端挂断连接时,响应体流会被“取消”。确保处理这种情况。这可能会在附加到响应体 ReadableStream 对象(例如通过 TransformStream)的 WritableStream 对象上的 write() 调用中表现为错误。

HTTPS 支持

ℹ️ 要使用 HTTPS,你需要为你的服务器提供有效的 TLS 证书和私钥。

要使用 HTTPS,在选项包中传递两个额外的参数:certkey。它们分别是证书和密钥文件的内容。

Deno.serve({
port: 443,
cert: Deno.readTextFileSync("./cert.pem"),
key: Deno.readTextFileSync("./key.pem"),
}, handler);

HTTP/2 支持

使用 Deno 的 HTTP 服务器 API 时,HTTP/2 支持是“自动”的。你只需要创建你的服务器,服务器将无缝处理 HTTP/1 或 HTTP/2 请求。

HTTP/2 也支持在事先知道的情况下通过明文进行。

自动主体压缩

HTTP 服务器内置了对响应体的自动压缩。当响应发送到客户端时,Deno 会确定响应体是否可以安全地压缩。这种压缩发生在 Deno 的内部,因此它快速高效。

目前 Deno 支持 gzip 和 brotli 压缩。如果满足以下条件,则会自动压缩主体

  • 请求具有一个 Accept-Encoding 头部,它指示请求者支持 br(用于 Brotli)或 gzip。Deno 将尊重头部中 质量值 的偏好。
  • 响应包含一个 Content-Type,它被认为是可压缩的。(该列表来自 jshttp/mime-db,实际列表 在代码中。)
  • 响应体大于 64 字节。

当响应主体被压缩时,Deno 会设置 Content-Encoding 头部以反映编码,并确保调整或添加 Vary 头部以指示哪些请求头部影响了响应。

什么时候会跳过压缩?除了上面的逻辑之外,还有其他一些原因会导致响应不会自动压缩。

  • 响应包含 Content-Encoding 头部。这表明您的服务器已经执行了某种形式的编码。
  • 响应包含 Content-Range 头部。这表明您的服务器正在响应范围请求,其中字节和范围是在 Deno 内部控制之外协商的。
  • 响应具有 Cache-Control 头部,其中包含 no-transform 值。这表明您的服务器不希望 Deno 或任何下游代理修改响应。

Deno.serveHttp

我们通常建议您使用上面描述的 Deno.serve API,因为它处理了单个连接上并行请求的所有复杂性,包括错误处理等等。但是,如果您有兴趣在 Deno 中创建自己的健壮且高效的 Web 服务器,那么从 Deno 1.9 及更高版本开始,可以使用更低级的原生 HTTP 服务器 API。

⚠️ 您可能不应该使用此 API,因为它不容易正确使用。请改用 Deno.serve API。

监听连接

为了接受请求,您首先需要监听网络端口上的连接。在 Deno 中,您可以使用 Deno.listen() 来实现。

const server = Deno.listen({ port: 8080 });

ℹ️ 当提供端口时,Deno 假设您将监听 TCP 套接字以及绑定到本地主机。您也可以指定 transport: "tcp" 以更明确地指定,以及在 hostname 属性中提供 IP 地址或主机名。

如果打开网络端口时出现问题,Deno.listen() 将抛出异常,因此在服务器环境中,您通常需要将其包装在 try ... catch 块中以处理异常,例如端口已经被使用。

您还可以使用 Deno.listenTls() 监听 TLS 连接(例如 HTTPS)。

const server = Deno.listenTls({
port: 8443,
certFile: "localhost.crt",
keyFile: "localhost.key",
alpnProtocols: ["h2", "http/1.1"],
});

certFilekeyFile 选项是必需的,它们指向服务器的相应证书和密钥文件。它们相对于 Deno 的 CWD。alpnProtocols 属性是可选的,但如果您希望能够在服务器上支持 HTTP/2,则需要在此处添加协议,因为协议协商是在 TLS 协商期间与客户端和服务器之间进行的。

ℹ️ 生成 SSL 证书不在本文档的范围之内。网络上有很多资源可以解决这个问题。

处理连接

一旦我们开始监听连接,我们就需要处理连接。Deno.listen()Deno.listenTls() 的返回值是一个 Deno.Listener,它是一个异步可迭代对象,它会生成 Deno.Conn 连接,并提供一些用于处理连接的方法。

要将其用作异步可迭代对象,我们可以执行以下操作

const server = Deno.listen({ port: 8080 });

for await (const conn of server) {
// ...handle the connection...
}

每次建立连接都会生成一个分配给 connDeno.Conn。然后可以对连接进行进一步处理。

监听器上还有一个 .accept() 方法可以使用

const server = Deno.listen({ port: 8080 });

while (true) {
try {
const conn = await server.accept();
// ... handle the connection ...
} catch (err) {
// The listener has closed
break;
}
}

无论使用异步迭代器还是 .accept() 方法,都可能抛出异常,健壮的生产代码应该使用 try ... catch 块来处理这些异常。特别是在接受 TLS 连接时,可能存在许多条件,例如无效或未知证书,这些证书可能会在监听器上出现,可能需要在用户代码中进行处理。

监听器还有一个 .close() 方法,可以用来关闭监听器。

提供 HTTP 服务

一旦连接被接受,就可以使用 Deno.serveHttp() 来处理连接上的 HTTP 请求和响应。Deno.serveHttp() 返回一个 Deno.HttpConnDeno.HttpConn 类似于 Deno.Listener,它会异步地将连接从客户端接收到的请求生成 Deno.RequestEvent

要将 HTTP 请求作为异步可迭代对象处理,它看起来像这样

const server = Deno.listen({ port: 8080 });

for await (const conn of server) {
(async () => {
const httpConn = Deno.serveHttp(conn);
for await (const requestEvent of httpConn) {
// ... handle requestEvent ...
}
})();
}

Deno.HttpConn 还有一个 .nextRequest() 方法,可以用来等待下一个请求。它看起来像这样

const server = Deno.listen({ port: 8080 });

while (true) {
try {
const conn = await server.accept();
(async () => {
const httpConn = Deno.serveHttp(conn);
while (true) {
try {
const requestEvent = await httpConn.nextRequest();
// ... handle requestEvent ...
} catch (err) {
// the connection has finished
break;
}
}
})();
} catch (err) {
// The listener has closed
break;
}
}

请注意,在这两种情况下,我们都使用 IIFE 来创建一个内部函数来处理每个连接。如果我们在接收连接的相同函数作用域中等待 HTTP 请求,我们将阻塞接受其他连接,这将使我们的服务器看起来“冻结”。在实践中,可能更有意义的是完全使用一个单独的函数

async function handle(conn: Deno.Conn) {
const httpConn = Deno.serveHttp(conn);
for await (const requestEvent of httpConn) {
// ... handle requestEvent
}
}

const server = Deno.listen({ port: 8080 });

for await (const conn of server) {
handle(conn);
}

从现在开始的示例中,我们将重点关注示例 handle() 函数中会发生什么,并删除监听和连接的“样板代码”。

HTTP 请求和响应

Deno 中的 HTTP 请求和响应本质上是 Web 标准 Fetch API 的反面。Deno HTTP 服务器 API 和 Fetch API 利用 RequestResponse 对象类。因此,如果您熟悉 Fetch API,您只需要在脑海中将它们翻转过来,现在它就是一个服务器 API。

如上所述,Deno.HttpConn 会异步地生成 Deno.RequestEvent。这些请求事件包含一个 .request 属性和一个 .respondWith() 方法。

.request 属性是一个 Request 类的实例,包含有关请求的信息。例如,如果我们想知道请求的是哪个 URL 路径,我们可以这样做

async function handle(conn: Deno.Conn) {
const httpConn = Deno.serveHttp(conn);
for await (const requestEvent of httpConn) {
const url = new URL(requestEvent.request.url);
console.log(`path: ${url.pathname}`);
}
}

.respondWith() 方法是完成请求的方式。该方法接受一个 Response 对象或一个解析为 Response 对象的 Promise。使用基本的“hello world”进行响应如下所示

async function handle(conn: Deno.Conn) {
const httpConn = Deno.serveHttp(conn);
for await (const requestEvent of httpConn) {
await requestEvent.respondWith(
new Response("hello world", {
status: 200,
}),
);
}
}

请注意,我们等待了 .respondWith() 方法。这不是必需的,但在实践中,处理响应中的任何错误都会导致该方法返回的 promise 被拒绝,例如,如果客户端在所有响应发送之前断开连接。虽然您的应用程序可能不需要执行任何操作,但未处理拒绝会导致出现“未处理的拒绝”,这将终止 Deno 进程,这对服务器来说不是很好。此外,您可能希望等待返回的 promise 以确定何时对请求/响应周期进行任何清理。

Web 标准 Response 对象非常强大,允许轻松创建对客户端的复杂和丰富的响应,Deno 努力提供尽可能接近 Web 标准的 Response 对象,因此如果您想知道如何发送特定响应,请查看 Web 标准的文档 Response

HTTP/2 支持

HTTP/2 支持在 Deno 运行时中是透明的。通常,HTTP/2 在客户端和服务器之间通过 ALPN 在 TLS 连接设置期间协商。要启用此功能,您需要在通过 alpnProtocols 属性开始侦听时提供要支持的协议。这将使协商在建立连接时发生。例如

const server = Deno.listenTls({
port: 8443,
certFile: "localhost.crt",
keyFile: "localhost.key",
alpnProtocols: ["h2", "http/1.1"],
});

协议按优先级顺序提供。在实践中,目前支持的两种协议是 HTTP/2 和 HTTP/1.1,分别表示为 h2http/1.1

目前 Deno 不支持通过 `Upgrade` 头部将纯文本 HTTP/1.1 连接升级到 HTTP/2 明文连接(参见:#10275),因此 HTTP/2 支持仅通过 TLS/HTTPS 连接提供。

服务 WebSockets

Deno 可以将传入的 HTTP 请求升级到 WebSocket。这允许您在 HTTP 服务器上处理 WebSocket 端点。

要将传入的 `Request` 升级到 WebSocket,请使用 `Deno.upgradeWebSocket` 函数。这将返回一个包含 `Response` 和 Web 标准 `WebSocket` 对象的对象。返回的响应应使用 `respondWith` 方法用于响应传入的请求。只有在使用返回的响应调用 `respondWith` 后,WebSocket 才会被激活并可以使用。

由于 WebSocket 协议是对称的,因此 `WebSocket` 对象与可用于客户端通信的对象相同。可以在 MDN 上找到它的文档。

注意:我们知道此 API 可能难以使用,并且计划在 WebSocketStream 稳定并可以使用后切换到它。

async function handle(conn: Deno.Conn) {
const httpConn = Deno.serveHttp(conn);
for await (const requestEvent of httpConn) {
await requestEvent.respondWith(handleReq(requestEvent.request));
}
}

function handleReq(req: Request): Response {
const upgrade = req.headers.get("upgrade") || "";
if (upgrade.toLowerCase() != "websocket") {
return new Response("request isn't trying to upgrade to websocket.");
}
const { socket, response } = Deno.upgradeWebSocket(req);
socket.onopen = () => console.log("socket opened");
socket.onmessage = (e) => {
console.log("socket message:", e.data);
socket.send(new Date().toString());
};
socket.onerror = (e) => console.log("socket errored:", e);
socket.onclose = () => console.log("socket closed");
return response;
}

WebSocket 目前仅在 HTTP/1.1 上受支持。在执行 WebSocket 升级后,无法将创建 WebSocket 的连接用于 HTTP 流量。