HTTP 服务器 API
Deno 目前有两个 HTTP 服务器 API
Deno.serve
: 原生,更高级,支持 HTTP/1.1 和 HTTP2,这是在 Deno 中编写 HTTP 服务器的首选 API。Deno.serveHttp
: 原生,低级,支持 HTTP/1.1 和 HTTP2。- 一个“Hello World”服务器
- 检查传入请求
- 响应一个响应
- HTTPS 支持
- HTTP/2 支持
- 服务 WebSockets
一个 "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,在选项包中传递两个额外的参数:cert
和 key
。它们分别是证书和密钥文件的内容。
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"],
});
certFile
和 keyFile
选项是必需的,它们指向服务器的相应证书和密钥文件。它们相对于 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...
}
每次建立连接都会生成一个分配给 conn
的 Deno.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.HttpConn
。Deno.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 利用 Request
和 Response
对象类。因此,如果您熟悉 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,分别表示为 h2
和 http/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 流量。