zl程序教程

您现在的位置是:首页 >  其他

当前栏目

前端如何开始写第一个 Node Server: 从请求到响应深入浅出

2023-03-09 22:05:12 时间

 

本文涉及到以下库及模块:

  • mime: 解析 MIME TYPE
  • mime-types
  • parseurl: 用以解析 URL。同时也可使用原生模块 url。
  • qs: 用以接续 querystring。同时也可使用原生模块 querystring,但已被废弃,推荐使用 URLSearchParams。
  • raw-body: 用以解析 body。

一个服务端框架是对 HTTP 协议的高级封装,是对 HTTP 报文的解析与处理。因此,学习 Node Server 的第一步,从一个 HTTP 报文开始。

HTTP 报文

以下是对百度发起请求的简单 HTTP 报文格式,使用 nc 命令行工具可以从 HTTP 报文发起连接请求,接收响应。

  1. $ nc www.baidu.com 80 
  2. GET / HTTP/1.1 
  3. Hostname: www.baidu.com 
  4.  
  5. HTTP/1.1 200 OK 
  6. Accept-Ranges: bytes 
  7. Cache-Control: no-cache 
  8. Connection: keep-alive 
  9. Content-Length: 14615 
  10. Content-Type: text/html 
  11. Date: Sun, 04 Jul 2021 05:26:21 GMT 
  12. P3p: CP=" OTI DSP COR IVA OUR IND COM " 
  13. P3p: CP=" OTI DSP COR IVA OUR IND COM " 
  14. Pragma: no-cache 
  15. Server: BWS/1.1 
  16. Set-Cookie: BAIDUID=062BC32733955287CAD249DFFA961C83:FG=1; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com 
  17. Set-Cookie: BIDUPSID=062BC32733955287CAD249DFFA961C83; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com 
  18. Set-Cookie: PSTM=1625376381; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com 
  19. Set-Cookie: BAIDUID=062BC3273395528798622D3D30510E2B:FG=1; max-age=31536000; expires=Mon, 04-Jul-22 05:26:21 GMT; domain=.baidu.com; path=/; version=1; comment=bd 
  20. Traceid: 1625376381035471002611003607825099517305 
  21. Vary: Accept-Encoding 
  22. X-Ua-Compatible: IE=Edge,chrome=1 
  23.  
  24. <!DOCTYPE html><!--STATUS OK--> 
  25. <html> 
  26. <head> 
  27.         <meta http-equiv="content-type" content="text/html;charset=utf-8"
  28.         <meta http-equiv="X-UA-Compatible" content="IE=Edge"
  29.         <link rel="dns-prefetch" href="//s1.bdstatic.com"/> 
  30.         <link rel="dns-prefetch" href="//t1.baidu.com"/> 
  31.         <link rel="dns-prefetch" href="//t2.baidu.com"/> 
  32.         <link rel="dns-prefetch" href="//t3.baidu.com"/> 
  33.         <link rel="dns-prefetch" href="//t10.baidu.com"/> 
  34.         <link rel="dns-prefetch" href="//t11.baidu.com"/> 
  35.         <link rel="dns-prefetch" href="//t12.baidu.com"/> 
  36.         <link rel="dns-prefetch" href="//b1.bdstatic.com"/> 
  37.         <title>百度一下,你就知道</title> 
  38. ...... 

我们把报文划分为以下几部分,稍后在 Node Server 中解析

  • Request
    • Method
    • Path
    • HTTP Version
    • Request Header
  • Response
    • Response Status Code
    • Response Header
    • Response Body

Simple Server: hello, world

在 Node 中编写一个简单的服务端代码非常简单,仅需以下三行即可做到。其中 http 是完成此项事情的核心模块。

  1. const http = require('http'
  2.  
  3. const server = http.createServer((req, res) => res.end('hello, world')) 
  4.  
  5. server.listen(3000) 

此时,在浏览器中访问 http://localhost:3000,即可看到一个 hello, world 的页面。最简单的服务端应用完成了。

  1. $ nc localhost 3000 
  2. GET / HTTP/1.1 
  3.  
  4. HTTP/1.1 200 OK 
  5. Date: Sun, 04 Jul 2021 08:54:45 GMT 
  6. Connection: keep-alive 
  7. Keep-Alive: timeout=5 
  8. Content-Length: 12 
  9.  
  10. hello, world 

以下是关于 createServer 的核心 API,其中 req 对应刚开始解析后的请求报文,res 对应响应报文的处理与设计,如以上的状态码及几个响应头的处理。

  1. function createServer(requestListener?: RequestListener): Server; 
  2.  
  3. type RequestListener = (req: IncomingMessage, res: ServerResponse) => void; 
  4.  
  5. class IncomingMessage extends stream.Readable { 
  6.   constructor(socket: Socket); 
  7.  
  8.   aborted: boolean; 
  9.   httpVersion: string; 
  10.   httpVersionMajor: number; 
  11.   httpVersionMinor: number; 
  12.   complete: boolean; 
  13.   connection: Socket; 
  14.   socket: Socket; 
  15.   headers: IncomingHttpHeaders; 
  16.   rawHeaders: string[]; 
  17.   trailers: NodeJS.Dict<string>; 
  18.   rawTrailers: string[]; 
  19.   setTimeout(msecs: number, callback?: () => void): this; 
  20.   method?: string; 
  21.   url?: string; 
  22.   statusCode?: number; 
  23.   statusMessage?: string; 
  24.   destroy(error?: Error): void; 

JSON API 与 Content-Type

以上仅仅是一个 hello, world 的示例,而在实际项目中大部分为 JSON API 的设计。

我们如何响应一个 JSON 数据呢?

为了使地能够正确的发送一个携带中文信息的响应报文,我们进行如下设置:

  1. const server = http.createServer((req, res) => { 
  2.   res.setHeader('Content-Type''application/json; charset=utf-8'); 
  3.   const data = JSON.stringify({ username: '山月' }) 
  4.   res.end(data) 
  5. }) 
  1. $ nc localhost 3000 
  2. GET /json HTTP/1.1 
  3.  
  4. HTTP/1.1 200 OK 
  5. Content-Type: application/json; charset=utf-8 
  6. Date: Sun, 04 Jul 2021 11:03:05 GMT 
  7. Connection: keep-alive 
  8. Keep-Alive: timeout=5 
  9. Content-Length: 21 
  10.  
  11. {"username":"山月"

响应 JSON 数据需要设置特殊的 Content-Type,被称为 MIME。除 JSON 外,图片、视频都有不同的 MIME Type,可通过以下量给进行获取。

  • mime
  • mime-types
  1. > mime.getType('json'
  2. "application/json" 
  3.  
  4. > mime.getType('html'
  5. "text/html" 
  6.  
  7. > mime.getType('svg'
  8. "image/svg+xml" 
  9.  
  10. > mime.getType('jpg'
  11. "image/jpeg" 

如何处理用户输入: URL 与 QueryString 解析

在一个服务端应用中,需要解析请求地址为各种参数,方便调用,如常用的 hostname、path、querystring。

而实际上,当一次请求来临时,我们只可以拿到一个 req.url,而其它的参数需要我们自己解析。毕竟自己动手,丰衣足食。URL 可以使用以下库来解析。

  • parseurl: 用以解析 URL。同时也可使用原生模块 url。
  • qs: 用以接续 querystring。同时也可使用原生模块 querystring,但已被废弃,推荐使用 URLSearchParams。

这两类是服务端常用的库,在一般的面试手写代码中也会要求你来手写。

「为了提高性能,各大服务端框架一般使用 get/set 进行延时计算」。以下为 koa 源码

  1. const request = { 
  2.   get path() { 
  3.     return parse(this.req).pathname; 
  4.   }, 
  5.   set path(path) { 
  6.     const url = parse(this.req); 
  7.     if (url.pathname === path) return
  8.  
  9.     url.pathname = path; 
  10.     url.path = null
  11.  
  12.     this.url = stringify(url); 
  13.   }, 
  14.   get query() { 
  15.     const str = this.querystring; 
  16.     const c = this._querycache = this._querycache || {}; 
  17.     return c[str] || (c[str] = qs.parse(str)); 
  18.   }, 
  19.   set query(obj) { 
  20.     this.querystring = qs.stringify(obj); 
  21.   }, 

如何处理用户输入: Request Body 解析

在 JSON API 中作为输入传参,除了通过在 URL 上进行传参外,最流行最通用最安全的

在 Node Server 中,我们的 HTTP Request 基于 Readable Stream,因此对请求体(Request Body)的解析有可能并不是一件很简单的事情,需要涉及到读取流、编码、解析格式、、gzip 解压缩、异常处理、体积过大413处理等。

即使最简单的处理,也需要涉及到以下代码。

  1. const server = http.createServer((req, res) => { 
  2.   let body = '' 
  3.   req.on('data', chunk => body += chunk) 
  4.   req.on('end', () => { 
  5.     data = body 
  6.     res.end(data) 
  7.   }) 
  8. }) 

「好在有一个 Body 解析神器: raw-body。最受欢迎的 Node 服务端框架 Express 与 Koa 都是基于 raw-body 对请求体进行解析。」

  1. const server = http.createServer((req, res) => { 
  2.   getRawBody(req) 
  3.     .then((body) => { 
  4.       res.statusCode = 200 
  5.       res.end(body) 
  6.     }) 
  7.     .catch((err) => { 
  8.       res.statusCode = 500 
  9.       res.end(err.message) 
  10.     }) 
  11. }) 

在 JSON API 中,即使响应体是基于 JSON 的,但是对于请求体的格式还是有多种多样的,比如最常见的以下几种:

  • application/www-form-url-encoded
  • application/json
  • multipart/form-data
  • text/plain

对不同 MIME 的请求体应如何处理,我们在下一篇文章中进行分析!

此时,服务端即可以通过请求 URL 中所携带的 QueryString 及 Request Body 进行输入,并返回相应的响应体。

细数 Node 服务端框架

  • Express
  • Koa
  • Hapi
  • Nest
  • Fastify
  • Micro

总结

本篇文章从一段 HTTP 报文信息开始理解服务端,根据以下步骤最终理清从请求到响应的整个处理流程:

  1. 开始写第一个 Node 的 hello, world 版本服务端代码。无论任何请求,响应数据总是 hello, world。
  2. 开始写第一个 Node 的 JSON API 式服务端代码,加强版的 hello, world。无论任何请求,响应数据总是一个 JSON,并通过响应头 Content-Type 设置响应体类型。
  3. 通过解析请求报文的 URL 及 QueryString,解析输入,并响应相应的输出。
  4. 通过解析请求报文的 Body,解析输入,并响应响应的输出。

那下一步呢?

  1. 如何解析路由,作为请求的输入
  2. 解析 Body 时有哪些细节问题
  3. JSON API 最后一步的必要步骤序列化如何更加高效

本文转载自微信公众号「全栈成长之路」,作者全栈成长之路 。转载本文请联系全栈成长之路公众号。