原生Deno处理HTTP响应

May 4, 2020 · View on GitHub

前言

前面的文章讲述了原生Deno处理HTTP请求,有HTTP请求,必须有HTTP响应。对比于HTTP请求操作,HTTP的响应操作会比较简单,因为只要对HTTP响应报文有了解的,按照报文的响应行,响应头和响应体按照顺序依次写入TCP对话连接,最后关闭TCP对话连接,就完成了次HTTP的响应过程。

实现原理

  • TCP对话写入状态行
    • 协议
    • 状态码
    • 状态结果
  • TCP对话写入响应头
    • 响应体长度
    • 其他响应信息
  • TCP对话写入空行作为响应头和响应体的间隔
  • TCP对话写入响应体
  • TCP对话关闭

最简单例子

最简源码

具体代码地址

https://github.com/chenshenhai/deno_note/blob/master/demo/response/example_sample.ts

具体代码讲解

./demo/response/example_sample.ts

const listen = Deno.listen;

/**
 * HTTP响应操作
 * @param conn {Conn}
 */
async function response(conn: Deno.Conn) {
  const encoder = new TextEncoder();
  // 准备响应体数据流
  const bodyStr = "hello world";
  const body = encoder.encode(bodyStr);

  // TCP对话连接 写入响应行
  //    协议: HTTP/1.1
  //    状态码: 200
  //    状态结果: OK
  await conn.write(encoder.encode('HTTP/1.1 200 OK\r\n'));
  // TCP对话连接 写入响应头
  //   响应内容长度: content-length (如果是非按数据块返回的,就必须声明内容长度)
  //   其他响应头信息
  await conn.write(encoder.encode(`content-length: ${body.byteLength}\r\n`));
  await conn.write(encoder.encode(`content-xxxxxxx: abcdefg12345\r\n`));
  // TCP对话连接 写入空行,区分响应头和响应体
  await conn.write(encoder.encode('\r\n'));
  // TCP对话连接 写入响应体
  await conn.write(body);
  conn.close();
}

/**
 * HTTP服务
 * @param addr {string}
 */
async function server(opts: Deno.ListenOptions) {
  // 创建TCP服务
  const listener: Deno.Listener = Deno.listen(opts) as Deno.Listener;
  console.log(`listening on ${opts.hostname}:${opts.port}`);
  // 死循环监听TCP请求
  while (true) {
    // 等待TCP连接
    const connection = await listener.accept();
    // 执行响应
    response(connection);
  }
}

const opts: Deno.ListenOptions = {
  hostname: "127.0.0.1",
  port: 3001
}
server(opts);

浏览器验证

  • 启动代码
deno run example_sample.ts

优化实现

从上述的实现,可以看到Deno实现响应的操作就只有按部就班的短短几步,但是在现实项目中,响应的操作更加复杂,我们可以把对应的操作进行封装优化。

实现代码

具体代码地址

https://github.com/chenshenhai/deno_note/blob/master/demo/response/mod.ts

具体代码讲解

./demo/response/mod.ts

const encoder = new TextEncoder();
const CRLF = "\r\n";

// 响应码对应信息
const statusMap: {[key: string]: string} = {
  "200": "OK",
  "404": "Not Found",
  "500": "Server Error",
  "unknown": "Unknown Error"
  // TODO ....
  // 其他状态码信息
}

export interface Response {
  setHeader(key: string, val: string): boolean;
  getHeaders(): Headers;
  setStatus(code: number): boolean;
  getStatus(): number;
  setBody(body: string): boolean;
  getBody(): string;
  flush(): Promise<number>;
}

export class ResponseWriter implements Response {
  private _conn: Deno.Conn;
  private _status: number = 404;
  private _body: string = "";
  private _headers: Headers = new Headers();

  constructor(conn: Deno.Conn) {
    this._conn = conn;
  }

  /**
   * 设置响应头信息
   * @param {string} key 响应头信息 key
   * @param {string} val 响应头信息 值
   * @return {boolean} 是否设置成功
   * */
  setHeader(key: string, val: string): boolean {
    try {
      if (this._headers.has(key)) {
        this._headers.set(key, val);
      } else {
        this._headers.append(key, val);
      }
      return true;
    } catch (err) {
      console.log(err);
      return false;
    }
  }

  /**
   * 获取所有响应头信息
   * @return {Headers}
   * */
  getHeaders() {
    return this._headers;
  }

  /**
   * 设置响应体信息
   * @param {string} body 响应体信息
   * @return {boolean} 是否设置成功
   * */
  setBody(body: string) {
    try {
      this._body = body;
      return true;
    } catch (err) {
      console.log(err);
      return false;
    }
  }

  /**
   * 获取所有响应状态码
   * @return {number}
   * */
  getStatus() {
    return this._status;
  } 

  /**
   * 获取请求体
   * @return {string}
   * */
  getBody(): string {
    return this._body;
  }

  /**
   * 设置状态码
   * @param {number} status
   * @return {boolean} 是否设置成功
   * */
  setStatus(status: number) {
    try {
      this._status = status;
      return true;
    } catch (err) {
      console.log(err);
      return false;
    }
  }

  /**
   * 响应信息写入对话
   * @return {number} 对话写入的长度
   * */
  async flush() {
    const resStream = this.createReqStream();
    const conn = this._conn;
    // TODO: 需要优化循环判断返回长度是否等于写入的数据
    const n = await conn.write(resStream);
    return n;
  }

  private createReqStream() {
    const headers = this._headers;
    const body = this._body;
    const bodyStream = encoder.encode(body);
    headers.set("content-length", `${bodyStream.byteLength}`);
    const resLines = [];
    const status = this._status;
    let statusKey: string = `${status}`;
    if (!(statusKey in statusMap)) {
      statusKey = 'unknown';
    }
    const statusVal = statusMap[statusKey];

    // TODO: HTTP目前写死 1.1版本
    resLines.push(`HTTP/1.1 ${status} ${statusVal}`);
    for ( const key of headers.keys() ) {
      const val = headers.get(key) || "";
      resLines.push(`${key}:${val}`);
    }
    resLines.push(CRLF);
    const headerStr = resLines.join(CRLF);
    const headerChunk = encoder.encode(headerStr);
    const bodyChunk = encoder.encode(body);
    const res = new Uint8Array(headerChunk.byteLength + bodyChunk.byteLength);
    res.set(headerChunk, 0);
    res.set(bodyChunk, headerChunk.byteLength);
    return res;
  }

}

具体使用例子

例子代码地址

https://github.com/chenshenhai/deno_note/blob/master/demo/response/example.ts

例子代码讲解

./demo/response/example.ts

import { Response, ResponseWriter } from "./mod.ts";

async function server(opts: Deno.ListenOptions) {
  const listener: Deno.Listener = Deno.listen(opts) as Deno.Listener;
  console.log(`listening on ${opts.hostname}:${opts.port}`);
  while (true) {
    const conn = await listener.accept();
    const response: Response = new ResponseWriter(conn);
    response.setBody("hello world");
    response.setStatus(200);
    const num = await response.flush();
    conn.close();
  }
}

const opts: Deno.ListenOptions = {
  hostname: "127.0.0.1",
  port: 3001
}
server(opts);

实现代码的单元测试

本书的学习方式,是依据严谨的开发方式学习,每个实现的功能都必备着单元测试。对于HTTP响应单元测试,主要是测试是否符合输入输出的预期,所以没必要展开讲解,可以查看一下的单元测试的代码。

单元测试地址