前言
前几天新来了一个前端小姐姐,而且就坐在我旁边,把母胎 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
- 第二行开始就是报文首部字段,例如
Host
,User-Agent
,Connection
等 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 服务,代码如下:
1 2 3 4 5 6 7 8 9 10 11
| 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 响应报文的格式编写返回值,根据报文去构造返回值的格式
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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
模块是这样创建服务的
1 2 3 4 5 6
| const http = require("http"); const server = http.createServer((req, res) => { res.end("hello world"); });
server.listen("9999");
|
依照这种创建服务的格式,来将其封装一下(代码可能有点长,源码地址会贴在文章最后小结里)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| import net from "net"; import { formatRequestMessage, IRequestData } from "./req"; import { Res } from "./res";
type handle = (req: IRequestData, res: Res) => void;
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"); }); };
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| export type IRequestData = { method: string; url: string; version: string; reqData: string; [key: string]: any; };
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); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| 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 服务搭建完成,来测试一下
1 2 3 4
| createServer((req, res) => { console.log(req); res.end(200, "hello world123"); });
|
请求报文能够成功截取到,浏览器中输出hello world123
!
小姐姐听了后,激动的说:太好了,我回去可以教我男朋友了 😄。
我: ???心想:RNM 退钱!!!
小结
本文简单地介绍了 HTTP 协议的内容,并且使用 node 利用 TCP 服务去编写一个 HTTP 服务,让我们对 HTTP 服务有一个更为深刻的理解。
源码地址
若文章中有不严谨或出错的地方请在评论区域指出。
参考文章
- 图解 HTTP
- MDN HTTP