title: 新入职的女程序员问我怎么用TCP编写一个HTTP服务 date: 2021-06-02 23:30:00 updated: 2021-06-02 23:30:00 photos:

  • https://img.yzmblog.top/blog/article_http.jpeg tags:
  • 前端
  • node categories:
  • Javascript excerpt: 前几天新来了一个前端小姐姐,而且就坐在我旁边,把母胎solo的我激动得说不出话来!!!今天她突然问我:怎么用TCP编写一个HTTP服务,我懵了一下,心想:这次是我表现的机会了😜,然后我就娓娓道来。

前言

前几天新来了一个前端小姐姐,而且就坐在我旁边,把母胎 solo 的我激动得说不出话来!!!
今天她突然问我:怎么用 TCP 编写一个 HTTP 服务,我懵了一下,心想:这次是我表现的机会了 😜,然后我就娓娓道来。

什么是 HTTP

言归正传,要写一个 http 服务首先要了解一下 HTTP 是一个什么样的东西。HTTP(HyperText Transfer Protocol)译为超文本传输协议,它是一种协议规范,也就是双方都要遵循的约定
HTTP 协议属于应用层协议,如图 1 所示,它在传输层之上,且基于传输层 TCP 和网络层 IP 协议进行数据传输。

图 1 TCP/IP 传输通信协议层级

HTTP 请求报文和响应报文

了解完 HTTP 的定义,既然 HTTP 是一种协议规范,那它肯定会遵循一些发送和响应的规范,它们称之为 HTTP 的请求(响应)报文。那么下面就来了解一下报文里面会有什么。

图 2 http 请求报文-1

  • 如图二所示,HTTP 请求报文里面第一行会定义方法URL版本号,以空格隔开,最后面是一个换行符\r\n
  • 第二行开始就是报文首部字段,例如HostUser-AgentConnection等 header 信息,每行以换行符\r\n隔开,最后用一个空行表示首部字段结束。
  • 首部字段结束后就是报文主体,一般我们 POST 请求的数据会放在这里。
    来看一个真实的 HTTP 请求报文头(图 3)

图 3 http 请求报文-2


图 4 http 响应报文

同理,HTTP 响应报文与请求报文大致相同,只是第一行有一些不一样,第一行按顺序填入的是版本状态码短语,最后是换行符\r\n
首部字段和报文主体与请求报文大致相同 文章主要讲解的是 HTTP 协议,如果想了解更多 TCP 的知识(TCP 连接三次握手,断开四次挥手),请戳这里

大概的概念了解完了,接下来就是实践的一下了,怎么样用 TCP 手写一个 HTTP 服务(node)。

用 TCP 编写一个 HTTP 服务

1. 建立一个 TCP 连接

首先我们需要创建一个 TCP 服务,代码如下:

import net from "net";

const server = net.createServer((socket) => {
  socket.write("hello world");
  socket.pipe(socket);
  socket.end(); // 关闭连接
});

server.listen(9999, () => {
  console.log("tcp server running at 9999");
});

到这里,一个简易的 tcp 服务就搭起来,测试的时候使用的是telnet命令

可以看到返回的是hello world,到这里其实已经成功了一半,现在在浏览器访问的时候,他会报错,意思是:这是一个错误的响应,因为我们没有遵循 HTTP 响应报文去返回值。

2.按响应报文格式返回 data

知道了问题所在,那我们再看回 HTTP 响应报文的格式编写返回值,根据报文去构造返回值的格式

import net from "net";

const server = net.createServer((socket) => {
  socket.write(
    `HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nhello world`
  );
  socket.pipe(socket);
  socket.end(); // 关闭连接
});

server.listen(9999, () => {
  console.log("tcp server running at 9999");
});

我们再使用浏览器打开 http://localhost:9999,成功了!这时他不会报错了,并且返回的也是我们在内容实体输入的hello world

3.最后封装

上面代码其实已经基本上实现了 HTTP 服务了,但是 node 中的http模块是这样创建服务的

const http = require("http");
const server = http.createServer((req, res) => {
  res.end("hello world");
});

server.listen("9999");

依照这种创建服务的格式,来将其封装一下(代码可能有点长,源码地址会贴在文章最后小结里)

// index.ts 入口文件,对外暴露方法
import net from "net";
import { formatRequestMessage, IRequestData } from "./req";
import { Res } from "./res";

type handle = (req: IRequestData, res: Res) => void;

/**
 * 创建函数
 * @param handler function
 */
export const createServer = function (handler: handle) {
  const server = net.createServer((socket) => {
    closeConnection(socket);
    handleError(socket);
    console.log("user connect");
    socket.on("data", (data) => {
      console.log(data.toString());

      const req: IRequestData = formatRequestMessage(data.toString());
      const res = new Res({ socket });
      handler(req, res);
    });
  });

  function closeConnection(socket: net.Socket) {
    socket.on("end", () => {
      console.log("close connection");
    });
  }

  function handleError(socket: net.Socket) {
    socket.on("error", (err) => {
      console.log(err);
    });
  }

  server.listen("9999", () => {
    console.log("tcp server running at 9999");
  });
};
// req.ts 截取请求报文
export type IRequestData = {
  method: string;
  url: string;
  version: string;
  reqData: string;
  [key: string]: any;
};

/**
 * format request data
 * @param requestMsg user request http header
 */
export function formatRequestMessage(requestMsg: string): IRequestData {
  const requestArr = requestMsg.split("\r\n");

  const [method, url, version] = requestArr.splice(0, 1)[0].split(" ");
  let header: Record<string, any> = {};
  let reqData: string = "";
  let isHeader = true;
  for (let x in requestArr) {
    if (requestArr[x] !== "" && isHeader) {
      const [key, value] = requestArr[x].split(": ");
      header[key] = value;
    } else if (isHeader) {
      isHeader = false;
    } else {
      reqData += requestArr[x];
    }
  }

  return Object.assign({ method, url, version, reqData }, header);
}
// res.ts 响应报文处理方法
import net from "net";

type resData = {
  version: string;
  socket: net.Socket;
};

interface IConstructorData {
  version?: string;
  socket: net.Socket;
}

export class Res implements resData {
  public version: string;
  public socket: net.Socket;
  constructor({ version, socket }: IConstructorData) {
    this.version = version || "HTTP/1.1";
    this.socket = socket;
  }

  private formatSendData(
    status: number,
    message: string | number,
    header: Record<string, any> = {}
  ): string {
    const statusStr = this.getStatusStr(status);
    const resHead = `${this.version} ${status} ${statusStr}`;
    let headerStr = ``;
    for (let x in header) {
      headerStr += `${x}: ${header[x]}\r\n`;
    }
    return [resHead, headerStr, message].join("\r\n");
  }

  private getStatusStr(status: number): string {
    switch (status) {
      case 200:
        return "OK";
      case 400:
        return "Bad Request";
      case 401:
        return "Unauthorized";
      case 403:
        return "Forbidden";
      case 404:
        return "Not Found";
      case 500:
        return "Internal Server Error";
      case 503:
        return "Server Unavailable";
      default:
        return "Bad Request";
    }
  }

  // 暴露输出方法
  public end(
    status: number,
    message: any,
    options: { header?: {} } = { header: {} }
  ): void {
    const resFormatMsg = this.formatSendData(status, message, options.header);

    this.socket.write(resFormatMsg);
    this.socket.pipe(this.socket);
    this.socket.end();
  }
}

至此,一个简易的 HTTP 服务搭建完成,来测试一下

createServer((req, res) => {
  console.log(req);
  res.end(200, "hello world123");
});

请求报文能够成功截取到,浏览器中输出hello world123!

小姐姐听了后,激动的说:太好了,我回去可以教我男朋友了 😄。
我: ???心想:RNM 退钱!!!

小结

本文简单地介绍了 HTTP 协议的内容,并且使用 node 利用 TCP 服务去编写一个 HTTP 服务,让我们对 HTTP 服务有一个更为深刻的理解。

源码地址

若文章中有不严谨或出错的地方请在评论区域指出。

参考文章

  1. 图解 HTTP
  2. MDN HTTP