本页内容

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,请在选项包中传递两个额外的参数:`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 标头,该标头指示请求者支持 Brotli 的 `br` 或 `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 在客户端和服务器之间通过 TLS 连接设置期间通过 ALPN 进行协商。要启用此功能,您需要在通过 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;
}

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