1. 创建一个 TCP 服务器:net.createServer()
  2. 服务端监听 xxx 端口:server.listen(xxx, <接收到客户端请求时触发的回调>)
  3. 服务端监听来自客户端的连接:server.on('connection', <客户端发送连接时触发的回调>)
    1. 回调函数接收的第一个参数是请求连接的客户端与当前的服务端建立的 socket
    2. 服务端通过这个 socket 和对应的客户端进行通信
    3. 监听客户端发送的请求消息:socket.on('data', <客户端发送请求时触发的回调>)
    4. 服务端响应来自客户端的请求:socket.write('...')
    5. 服务端监听客户端关闭连接:socket.on('end', <连接断开时触发的回调>)
    6. 服务端手动关闭连接:socket.end()
  4. 创建一个 TCP 客户端:net.createConnection(<需连接的服务端参数>[, <连接成功后的回调>])
  5. 客户端向服务端发起请求:client.write('...')
  6. 客户端监听服务端的响应:client.on('data', <服务端响应消息时触发的回调>)
    1. 回调函数接收的第一个参数是服务端给客户端响应的内容
  7. 客户端监听服务端关闭连接:client.on('end', <连接断开时触发的回调>)
  8. 客户端手动关闭连接:client.end()

前言

实现几个小 demo,体验一下 Node.js 的 net 模块,👇 是 demo 的功能描述:

  1. demo1:使用 Node.js 的 net 模块,搭建一个简单的本地服务,分别定义 TCP 客户端、服务端,并实现简单的本地通信。
  2. demo2:写一个 TCP 客户端来模拟 http 请求,向 www.baidu.com 发起请求,并将接收到的响应体内容原样输出,接收完毕后,关闭连接。
  3. demo3:写一个 TCP 服务端来模拟 web 服务器,作用是返回一张图片。要求可以使用浏览器成功请求到该服务,并将请求到的 图片 给渲染出来。

参考资料:

demo1

  1. // client.js
  2. const net = require("net");
  3. // 创建客户端
  4. const client = net.createConnection(
  5. {
  6. port: 2155,
  7. host: "localhost",
  8. },
  9. () => {
  10. console.log("成功连接服务端");
  11. }
  12. );
  13. // 监听来自服务端的消息
  14. client.on("data", (chunk) => {
  15. console.log("来自服务端的消息:", chunk.toString());
  16. client.end(); // 客户端主动关闭连接
  17. });
  18. // 向服务端发送请求
  19. client.write('你好,我是客户端');
  20. // 注册监听请求断开的事件
  21. client.on("end", () => {
  22. console.log("连接断开了");
  23. });
  1. // server.js
  2. const net = require("net")
  3. const server = net.createServer()
  4. server.listen(2155, () => {
  5. console.log("开始监听 2155 端口")
  6. })
  7. server.on("connection", (socket) => {
  8. console.log("监听到有客户端连接该服务")
  9. socket.on("data", (chunk) => {
  10. console.log("接收到来自客户端的数据", '\n=> ', chunk.toString())
  11. socket.write(
  12. `你好,我是服务端,我已经收到了你发送来的数据 => ${chunk.toString()}`, 'utf-8'
  13. )
  14. })
  15. socket.on("end", () => {
  16. console.log("连接断开了")
  17. })
  18. })

执行以下命令,查看最终效果:

  1. 启动服务端:node ./server.js
  2. 启动客户端:node ./client.js

最终效果:
0.gif

demo2

模拟 http 请求,将接收到的响应体内容原样输出,接收完毕后,关闭连接。先来看看最终效果:

接收到的数据:
image.png

解析后的数据:
image.png

👇🏻 下面简单分析一下 demo2 的实现流程:

  1. 创建客户端 const client = net.createConnection(options, cb),其中回调 cb 会在连接成功时触发一次。
  2. 注册监听函数 client.on('data', (chunk) => { ... })
    1. data 事件在每次接收到来自服务端的消息时,都会触发。
    2. 第一次接收到响应消息时,解析出响应行、响应头信息。之后每次监听到响应消息,都是 剩下 的响应体的消息,只要不断拼接到响应体中即可。
    3. 每次接收完消息后,都要判断来自服务端的消息是否已经接收完毕了,如果接收完毕了,则需要断开链接。
  3. 发送请求 client.write()
  4. 断开连接 client.end()
    1. 判断是否需要断开连接,可以借助解析出的响应头的 Content-Length 字段来判断。
    2. ① 服务端返回的响应体消息的总字节数:Content-Length 的值
    3. ② 当前接收到的消息的总字节数:Buffer.from(目前接收到的服务端消息体字符串, 'utf-8').byteLength
    4. 一旦 ② >= ①,那么意味着服务端吐给我们的数据,我们都拿到了,此时需要断开可服务端的连接。

👇🏻 下面是 demo2 的客户端源码实现:

  1. // client.js
  2. const net = require("net");
  3. const responseData = {
  4. line: null, // 响应行
  5. header: null, // 响应头
  6. body: "", // 响应体
  7. };
  8. const separator = "\r\n"; // 分隔符
  9. // 创建客户端
  10. const client = net.createConnection(
  11. {
  12. port: 80, // HTTP 协议,默认端口 80
  13. host: "www.baidu.com", // default val => 'localhost'
  14. },
  15. () => {
  16. // 连接成功之后的回调
  17. console.log("连接成功~");
  18. }
  19. );
  20. // 发送请求
  21. client.write(`GET / HTTP/1.1
  22. Connection: keep-alive
  23. Host: www.baidu.com
  24. `);
  25. // 监听响应
  26. client.on("data", (chunk) => {
  27. console.log("chunk => ", chunk.toString("utf-8"));
  28. if (!responseData.line) { // 第一次收到的响应消息
  29. // 解析第一次接收到的 chunk 获取到响应行、响应头以及响应体的部分信息
  30. parseResponse(chunk.toString("utf-8"));
  31. } else { // 非第一次接收到的响应消息
  32. responseData.body += chunk.toString("utf-8");
  33. }
  34. isOver();
  35. });
  36. // 监听断开
  37. client.on("close", () => {
  38. console.log("连接断开~");
  39. });
  40. /**
  41. * 解析响应消息
  42. * @param {String} response 响应消息
  43. */
  44. function parseResponse(response) {
  45. const lineEndIndex = response.indexOf(separator); // => 响应行的结束位置
  46. const headerEndIndex = response.indexOf(separator + separator); // => 响应头的结束位置
  47. const lineStr = response.slice(0, lineEndIndex);
  48. const headerStr = response.slice(lineEndIndex + 2, headerEndIndex);
  49. const bodyStr = response.slice(headerEndIndex + 4);
  50. const lineArr = lineStr.split(" ");
  51. const headerArr = headerStr.split(separator);
  52. // 响应行
  53. responseData.line = {
  54. HTTPVersion: lineArr[0], // => 协议版本
  55. StatusCode: lineArr[1], // => 状态码
  56. ReasonPhrase: lineArr[2], // => 状态码描述
  57. };
  58. // 响应头
  59. responseData.header = headerArr
  60. .map((it) => {
  61. const keyEndIndex = it.indexOf(": "),
  62. key = it.slice(0, keyEndIndex),
  63. val = it.slice(keyEndIndex + 2);
  64. return [key, val];
  65. })
  66. .reduce((a, b) => {
  67. a[b[0]] = b[1];
  68. return a;
  69. }, {});
  70. // 响应体
  71. responseData.body = bodyStr;
  72. }
  73. /**
  74. * 判断来自服务器的消息是否已经接收完毕
  75. */
  76. function isOver() {
  77. const contentLength = +responseData.header["Content-Length"],
  78. curLen = Buffer.from(responseData.body).byteLength;
  79. // 消息接收完毕
  80. if (curLen >= contentLength) {
  81. client.end(); // 关闭连接
  82. }
  83. }

主要解决几个问题:

  1. 如何创建客户端,建立与服务端的链接
  2. 如何使用客户端发送 HTTP 请求
  3. 如何拿到服务端返回的 HTTP 响应数据
  4. 如何判断服务端响应的内容是否都接收完毕,并在接收完毕之后,关闭连接

补充:

响应消息中,有些字段是重复的,暂时还不理解这些重复的 key 是干啥的,使用上述逻辑处理的最终结果是,后者覆盖前者。

image.png

demo3

模拟 HTTP 服务器,使用浏览器访问该服务,得到一个静态资源,http://localhost:2155/ 使用浏览器访问本地搭建的一个服务,可以获取到我们返回的静态资源。

image.png

👇 下面来简单介绍一下流程:

初始化:

  1. 创建服务端:const localServer = net.createServer()
  2. 监听 2155 端口:localServer.listen(2155, () => {}) 注意:回调函数仅会在客户端连接 2155 端口成功时触发一次。
  3. 监听来自客户端的连接请求:localServer.on("connection", (socket) => {}) 注意:每次有客户端连接,都会触发 connection 事件。每个客户端都对应一个 socket,每个 socket 之间是相互独立的。

处理 socket:

  • 注册监听事件:socket.on("data", (chunk) => {}) 注册 data 事件,每次接收到来自客户端的数据时触发
  • 注册 end 事件,连接断开时触发:socket.on('end', () => {})

准备响应的数据:响应数据 = 响应头 + 响应体

  1. const headBuffer = Buffer.from(
  2. `HTTP/1.1 200 OK
  3. Content-Type: image/jpeg
  4. `, "utf-8");
  • 响应体:读取静态文件资源(buffer 格式)稍后作为 响应体 返回:const bodyBuffer = await fs.promises.readFile(path.resolve(__dirname, './xxx'))
  • 响应数据:拼接响应头和响应体,并返回给客户端:socket.write(Buffer.concat([headBuffer, bodyBuffer]))
  • 断开连接:socket.end()

以下是源码实现

  1. const net = require("net");
  2. const path = require("path");
  3. const fs = require("fs");
  4. const localServer = net.createServer();
  5. localServer.listen(2155, () => {
  6. console.log("开始监听 2155 端口");
  7. }); // => 监听 2155 端口
  8. localServer.on("connection", (socket) => {
  9. console.log("有客户端连接到该服务了");
  10. socket.on("data", async (chunk) => {
  11. console.log("接收到来自客户端的数据:", chunk.toString("utf-8"));
  12. const headBuffer = Buffer.from(
  13. `HTTP/1.1 200 OK
  14. Content-Type: image/jpeg
  15. `,
  16. "utf-8"
  17. );
  18. // 读取本地头像文件 avatar.jpeg
  19. const filename = path.resolve(__dirname, "./avatar.jpeg");
  20. // const filename = path.resolve(__dirname, "./index.html");
  21. const bodyBuffer = await fs.promises.readFile(filename);
  22. socket.write(Buffer.concat([headBuffer, bodyBuffer]));
  23. socket.end();
  24. });
  25. socket.on("end", () => {
  26. console.log("连接关闭了");
  27. });
  28. });