【物联网服务NodeJs-5天学习】第二天篇② —— 网络编程(TCP、HTTP、Web应用服务)

面向读者群体

  • ❤️ 电子物联网专业同学,想针对硬件功能构造简单的服务器,不需要学习专业的服务器开发知识 ❤️
  • ❤️ 业余爱好物联网开发者,有简单技术基础,想针对硬件功能构造简单的服务器❤️
  • ❤️ 本篇创建记录 2023-03-12 ❤️
  • ❤️ 本篇更新记录 2023-03-12 ❤️

技术要求

  • HTMLCSSJavaScript基础更好,当然也没事,就直接运行实例代码学习

专栏介绍

  • 通过简短5天时间的渐进式学习NodeJs,可以了解到基本的服务开发概念,同时可以学习到npm、内置核心API(FS文件系统操作、HTTP服务器、Express框架等等),最终能够完成基本的物联网web开发,而且能够部署到公网访问。

🙏 此博客均由博主单独编写,不存在任何商业团队运营,如发现错误,请留言轰炸哦!及时修正!感谢支持!🎉 欢迎关注 🔎点赞 👍收藏 ⭐️留言📝

NodeJs是一个面向网络而生的平台,具有事件驱动无阻塞单线程等特性,十分轻量,适合在分布式网络中扮演各种各样的角色。同时Node提供的API可以构建灵活的网络服务。

利用Node可以十分方便地搭建网络服务器。在web领域,大多数的编程语言需要专门的web服务器来做容器,如ASP、ASP.NET需要IIS作为服务器,PHP需要搭载Apache或者Nginx环境等,JSP需要Tomcat服务器等。但Node就相对简单,只需要几行代码即可构建服务器,无需额外容器。

Node提供了netdgramhttphttps这4个模块,分别用于处理TCPUDPHTTPHTTPS,适用于服务器和客户端。

1、client/server结构

说到服务器以及客户端,我们先来了解一下c/s结构。
在这里插入图片描述
C/S是Client/Server的缩写。

  • 服务器通常采用高性能的PC工作站小型机,并采用大型数据库系统,如Oracle、Sybase、Informix或 SQL Server。
  • 客户端需要安装专用的客户端软件,比如浏览器App小程序等等。当然从广义上来说,也有可能是一台服务器,比如一台服务器向另外一台服务器请求数据。

客户端发起一个请求内容给到服务器,服务器收到请求之后再解析请求并对请求内容作出响应,最终把响应内容返回给到客户端。

比如:

  • 我们在浏览器上访问百度,本质上就是浏览器client百度服务器server发起请求百度网页内容,百度服务器收到请求后解析成功并把百度首页页面内容响应给回到浏览器。
  • 我们在b站上搜索某一个学习视频,本质上就是 appb站服务器发起请求搜索请求,请求数据会带上要搜索的关键字,b站服务器收到请求后会把关键字解析出来并且发起搜索服务(搜索数据库等等),拿到搜索结果后再响应给回到app,这样app就可以展示我们要搜索的内容。

2. 服务器相关概念

在讲解 网络模块使用之前,我们需要先了解几个服务器概念。

  • IP地址 和 端口号,表示一个web服务的唯一响应位置。
  • DNS域名服务

2.1 IP地址 和 端口号

  • IP地址
    IP 地址就是互联网上每台计算机的唯一地址,因此 IP 地址具有唯一性(这个是相对,比如我设置一个静态IP地址)。如果把“个人电脑”比作“一台电话”,那么“IP地址”就相当于“电话号码”,只有在知道对方 IP 地址的前提下,才能与对应的电脑之间进行数据通信。
    IP 地址的格式:通常用“点分十进制”表示成(a.b.c.d)的形式,其中,a,b,c,d 都是 0~255 之间的十进制整数。例如:用点分十进表示的 IP地址(192.168.1.1)

注意:

  • 互联网中每台 Web 服务器,都有自己的 IP 地址,例如:大家可以在 Windows 的终端中运行 ping www.baidu.com 命令,即可查看到百度服务器的 IP 地址。
    在这里插入图片描述
  • 自己的电脑作为一个本地服务器时,可以在自己的浏览器中输入 127.0.0.1(或者输入localhost)这个IP 地址,就能把自己的电脑当做一台服务器进行访问了。

思考:为啥不用Mac地址?

  • 端口号
    计算机中的端口号,就好像是现实生活中的门牌号一样。通过门牌号,外卖小哥可以在整栋大楼众多的房间中,准确把外卖送到你的手中。
    同样的道理,在一台电脑中,可以运行成百上千个 web 服务。每个 web 服务都对应一个唯一的端口号。客户端发送过来的网络请求,通过端口号,可以被准确地交给对应的 web 服务进行处理。
    在这里插入图片描述

注意:

  • 唯一性决定了一个端口号不能同时被多个服务占用
  • 默认情况下,当不填端口是默认是80。比如 http://www.baidu.com:80/index.html 等同于 http://www.baidu.com/index.html

2.2 DNS域名服务

尽管 IP 地址能够唯一地标记网络上的计算机,但IP地址是一长串数字,不直观,而且不便于记忆,于是人们又发明了另一套字符型的地址方案,即所谓的域名(Domain Name)地址。

IP地址和域名是一一对应的关系,这份对应关系存放在一种叫做域名服务器(DNS,Domain name server)的电脑中。使用者只需通过好记的域名访问对应的服务器即可,对应的转换工作由域名服务器实现。因此,域名服务器就是提供 IP 地址和域名之间的转换服务的服务器。

当我们用域名访问服务器时,比如访问

http://www.baidu.com/index.html
它的步骤是:

  • 客户端向DNS服务器发起获取IP地址请求
  • DNS服务器向客户端响应具体对应的IP地址
  • 客户端向IP地址和端口号的服务器发起请求

在这里插入图片描述
当我们直接用IP地址和端口号访问服务器时,比如访问

http://xxxxxx:80/index.html
它的步骤是

  • 客户端向IP地址和端口号的服务器发起请求
    在这里插入图片描述
  • 单纯使用 IP 地址,互联网中的电脑也能够正常工作。但是有了域名的加持,能让互联网的世界变得更加方便。
  • 在开发测试期间, 127.0.0.1 对应的域名是 localhost,它们都代表我们自己的这台电脑,在使用效果上没有任何区别。
    如何设置本地DNS:
    windows”→“System32”→“drivers”→“etc" 目录下有一个hosts文件 用记事本打开。hosts文件相当于一个本地的DNS,当你访问某个域名时,是先通过hosts进行解析的,没有找到才进一步通过外网DNS服务器进行解析,所以我们只需要在该文件中设置好,就不怕外网DNS解析不了了。
    在这里插入图片描述
    在这里插入图片描述
    我们可以在这个文件上加上一些域名映射

思考:

  • 是不是直接IP地址和端口号速度更快?是否是一个优化速度方向?

3. 构建TCP

可以说,互联网上绝大部分的应用服务都是基于TCP的服务,TCP服务在网络应用中十分常见。

3.1 TCP

TCP又叫做传输控制协议,在网络模型中属于传输层协议。很多应用层协议都是基于TCP构建,比较典型就是我们HTTP协议。

TCP是面向连接的协议,在传输之前需要3次握手形成会话。
在这里插入图片描述
只有会话形成之后,服务器端和客户端之间才能互相发送数据。在创建会话的过程中,服务器端和客户端分别提供一个套接字socket,这两个套接字共同形成一个连接。服务器端与客户端则通过套接字实现两者之间连接的操作。

所以我们接下来需要分别学习 服务器端客户端内容。

3.2 创建TCP服务器端

  • ① 导入 net 模块

  • ② 创建TCP服务器实例,监听socket事件

  • ③ 配置服务器事件,启动服务器,监听客户端的请求

3.2.1 导入 net 模块

创建一个 TCP 服务器,对外提供 tcp 服务,需要导入 net 模块:

// 1.导入 net 模块
const net = require ('net')

推荐API学习:

在net模块中,需要了解到三个概念:

  • net 模块
    net模块下包含了server、client、socket,它们三者的关系。
    在这里插入图片描述
    在这里插入图片描述

  • net.Server
    用于创建一个 TCP 服务器。内部通过socket来实现与客户端的通信。
    在这里插入图片描述

  • net.Socket
    TCP Socket 的抽象。net.Socket 实例实现了一个双工流接口。 他们可以在用户创建客户端(使用 connect())时使用, 或者由 Node 创建它们,并通过 connection 服务器事件传递给用户。

也就是说我们需要区分server和socket,从而区分服务器事件和socket事件。一个server上根据客户端连接的多少决定了socket数量的多少。

3.2.2创建TCP服务器实例,监听socket事件

调用 http.createServer() 方法,即可快速创建一个 TCP 服务器实例:

// 2. 创建 TCP 服务器实例,回调方法表示有新的socket连接进来。
const server = net.createServer(function(socket){

  // 5. 表示一个新的socket连接进来
  var helloContent = `你好,我是服务端 ${socket.remoteAddress}:${socket.remotePort}`
  socket.write(helloContent); 
  
  // 以下是配置
  // 闲置超时时间 2s
  socket.setTimeout(2000)
  // 禁用Nagle算法, socket.write立即发送数据
  socket.setNoDelay(true)
  // 启用长连接
  socket.setKeepAlive(true)

  console.log("服务端:收到来自客户端的请求 %j:%j",socket.remoteAddress,socket.remotePort);

  // 以下是事件监听
  // 5.1 data事件:当收到另一侧传来的数据时触发
  //              事件传递的数据就是发送的数据
  socket.on('data', function(data) {
    console.log("服务端:收到客户端数据,内容为",data.toString());
  })

  // 5.2 end事件:当连接中的任意一端发送了FIN数据时,将会触发此事件(了解TCP四次挥手)
  socket.on('end', function(data) {
    console.log('服务端:客户端连接断开,FIN数据')
  })

  // 5.3 error事件:当有错误发生时,就会触发,参数为error
  socket.on("error",function(err){
    console.log("服务端:客户端异常关闭",err)
  })

  // 5.4 timeout事件:当 socket 空闲超时时触发,仅是表明 socket 已经空闲。用户必须手动关闭连接。
  socket.on("timeout",function(){
    console.log("服务端:socket已经超时,手动关闭连接")
    socket.end()
  })

  // 5.4 close事件:连接断开时触发。如果是因为传输错误导致的连接断开,则参数为error。
  socket.on('close', function(error) {
    if(!error){
        console.log("服务端:客户端连接断开 success! %j:%j",socket.remoteAddress,socket.remotePort)
    } else{
        console.log("服务端:客户端连接断开 error! %j:%j",socket.remoteAddress,socket.remotePort)
    }
  })
})

服务器可以同时与多个客户端保持连接(连接状态本质上来说就是socket的状态),
在这里插入图片描述

对于每个连接来说就是典型的可写可读Stream流对象。Stream流对象可以用于服务器端和客户端之间的通信,也就是说通过data事件可以从一端读取另外一端发过来的数据,也可以通过write() 方法从一端向另外一端发送数据。同时我们可以通过事件来监听socket的状态:

  • data事件
  • end事件
  • connect事件
  • error事件
  • close事件
  • timeout事件

在这里插入图片描述

关于Socket的api可以参考:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
我们下面重点讲解socket事件,从事件来理解socket状态。

3.2.2.1 connect 事件

成功建立 socket 连接时触发。

一般是在 net.createServer(callback) 中,回调函数被执行了表示触发了connect事件。

// 2. 创建 TCP 服务器实例,回调方法表示有新的socket连接进来。
const server = net.createServer(function(socket){
  // 5. 表示一个新的socket连接进来
  socket.write("你好,我是服务端");  
  console.log("服务端:收到来自客户端的请求 %j:%j",socket.remoteAddress,socket.remotePort);
})
3.2.2.2 data 事件

当收到另一侧传来的数据时触发,事件传递的数据就是发送的数据。这个就是最重要事件,处理数据。主要关联 socket.write 方法

// 2. 创建 TCP 服务器实例,回调方法表示有新的socket连接进来。
const server = net.createServer(function(socket){
  // 5. 表示一个新的socket连接进来
  socket.write("你好,我是服务端");  
  console.log("服务端:收到来自客户端的请求 %j:%j",socket.remoteAddress,socket.remotePort);
  // 5.1 data事件:当收到另一侧传来的数据时触发
  //              事件传递的数据就是发送的数据
  socket.on('data', function(data) {
    console.log("服务端:收到客户端数据,内容为",data.toString());
  })
})
3.2.2.3 error 事件

当有错误发生时,就会触发,参数为error。比如传输连接突然断开,连接异常等等,一般这种情况下我们需要做一些纠错处理,比如重连、关闭连接等等

// 2. 创建 TCP 服务器实例,回调方法表示有新的socket连接进来。
const server = net.createServer(function(socket){

  // 5. 表示一个新的socket连接进来
  socket.write("你好,我是服务端");  
  console.log("服务端:收到来自客户端的请求 %j:%j",socket.remoteAddress,socket.remotePort);
  // 5.1 data事件:当收到另一侧传来的数据时触发
  //              事件传递的数据就是发送的数据
  socket.on('data', function(data) {
    console.log("服务端:收到客户端数据,内容为",data.toString());
  })
  // 5.3 error事件:当有错误发生时,就会触发,参数为error
  socket.on("error",function(err){
    console.log("服务端:客户端异常关闭",err)
  })
})
3.2.2.4 end 事件

当数据传输准备结束时,连接中的任意一端发送了FIN数据,将会触发此事件(了解TCP四次挥手)。此事件会比close事件更早。

半关闭 socket。它发送一个 FIN 包。可能服务器仍在发送数据。可以调用socket.end方法。

// 2. 创建 TCP 服务器实例,回调方法表示有新的socket连接进来。
const server = net.createServer(function(socket){
  // 5. 表示一个新的socket连接进来
  socket.write("你好,我是服务端");  
  console.log("服务端:收到来自客户端的请求 %j:%j",socket.remoteAddress,socket.remotePort);
  // 5.1 data事件:当收到另一侧传来的数据时触发
  //              事件传递的数据就是发送的数据
  socket.on('data', function(data) {
    console.log("服务端:收到客户端数据,内容为",data.toString());
  })
  // 5.2 end事件:当连接中的任意一端发送了FIN数据时,将会触发此事件(了解TCP四次挥手)
  socket.on('end', function(data) {
    console.log('服务端:客户端连接断开,FIN数据')
  })
})
3.2.2.5 close 事件

当socket连接真正断开时触发。如果是因为传输错误导致的连接断开,则参数为error。

// 2. 创建 TCP 服务器实例,回调方法表示有新的socket连接进来。
const server = net.createServer(function(socket){

  // 5. 表示一个新的socket连接进来
  socket.write("你好,我是服务端");  
  console.log("服务端:收到来自客户端的请求 %j:%j",socket.remoteAddress,socket.remotePort);
  // 5.1 data事件:当收到另一侧传来的数据时触发
  //              事件传递的数据就是发送的数据
  socket.on('data', function(data) {
    console.log("服务端:收到客户端数据,内容为",data.toString());
  })
  // 5.2 end事件:当连接中的任意一端发送了FIN数据时,将会触发此事件(了解TCP四次挥手)
  socket.on('end', function(data) {
    console.log('服务端:客户端连接断开,FIN数据')
  })
  // 5.4 close事件:连接断开时触发。如果是因为传输错误导致的连接断开,则参数为error。
  socket.on('close', function(error) {
    if(!error){
        console.log("服务端:客户端连接断开 success! %j:%j",socket.remoteAddress,socket.remotePort)
    } else{
        console.log("服务端:客户端连接断开 error! %j:%j",socket.remoteAddress,socket.remotePort)
    }
  })
})
3.2.2.6 timeout 事件

当 socket 空闲超时时触发,仅是表明 socket 已经空闲。用户必须手动关闭连接。这里会受到 socket.setTimeout 方法影响.

  // 以下是配置
  // 闲置超时时间 2s
  socket.setTimeout(2000)
  // 5.4 timeout事件:当 socket 空闲超时时触发,仅是表明 socket 已经空闲。用户必须手动关闭连接。
  socket.on("timeout",function(){
    console.log("服务端:socket已经超时,需要手动关闭连接")
    socket.end()
  })
3.2.3 配置服务器事件,启动服务器,监听客户端的请求

服务器实例启动监听客户端请求,同时监控一些服务器事件:

// 3. 捕获server对象发生错误
server.on("error",()=>{
	console.log("server对象发生error事件")
})

// 4. 启动服务器,开始监听
server.listen(8266, function(){
    var address=server.address()
    console.log("服务端:开始监听来自客户端的请求 %j ",address)
})

在这里插入图片描述

3.2.4 完整步骤代码

创建一个test_tcp_server.js文件
在这里插入图片描述

写入以下代码:

// 1.导入 net 模块
const net = require ('net')

// 2. 创建 TCP 服务器实例,回调方法表示有新的socket连接进来。
const server = net.createServer(function(socket){

  // 5. 表示一个新的socket连接进来
  var helloContent = `你好,我是服务端 ${socket.remoteAddress}:${socket.remotePort}`
  socket.write(helloContent); 
  
  // 以下是配置
  // 闲置超时时间 2s
  socket.setTimeout(2000)
  // 禁用Nagle算法, socket.write立即发送数据
  socket.setNoDelay(true)
  // 启用长连接
  socket.setKeepAlive(true)

  console.log("服务端:收到来自客户端的请求 %j:%j",socket.remoteAddress,socket.remotePort);

  // 以下是事件监听
  // 5.1 data事件:当收到另一侧传来的数据时触发
  //              事件传递的数据就是发送的数据
  socket.on('data', function(data) {
    console.log("服务端:收到客户端数据,内容为",data.toString());
  })

  // 5.2 end事件:当连接中的任意一端发送了FIN数据时,将会触发此事件(了解TCP四次挥手)
  socket.on('end', function(data) {
    console.log('服务端:客户端连接断开,FIN数据')
  })

  // 5.3 error事件:当有错误发生时,就会触发,参数为error
  socket.on("error",function(err){
    console.log("服务端:客户端异常关闭",err)
  })

  // 5.4 timeout事件:当 socket 空闲超时时触发,仅是表明 socket 已经空闲。用户必须手动关闭连接。
  socket.on("timeout",function(){
    console.log("服务端:socket已经超时,手动关闭连接")
    socket.end()
  })

  // 5.4 close事件:连接断开时触发。如果是因为传输错误导致的连接断开,则参数为error。
  socket.on('close', function(error) {
    if(!error){
        console.log("服务端:客户端连接断开 success! %j:%j",socket.remoteAddress,socket.remotePort)
    } else{
        console.log("服务端:客户端连接断开 error! %j:%j",socket.remoteAddress,socket.remotePort)
    }
  })
})

// 3. 捕获server对象发生错误
server.on("error",()=>{
	console.log("server对象发生error事件")
})

// 4. 启动服务器,开始监听
server.listen(8266, function(){
    var address=server.address()
    console.log("服务端:开始监听来自客户端的请求 %j ",address)
})

执行一下test_tcp_server.js 看看。
在这里插入图片描述
这里表示已经开始监听TCP客户端请求,我们使用Tcpudp一些工具来测试一下。这里我使用 NetAssist 网络调试助手。
在这里插入图片描述
然后,调试助手作为TCP Client访问一下Node TCP Server,观察一下打印信息:
在这里插入图片描述
在这里插入图片描述
因为我们配置了setTimeout,所以这里会发现如果不发送数据,server会主动断开连接。剩下就是一个socket的生命周期事件(理解socket生命周期就意味着我们需要了解TCP状态机)。

上面有一个方法需要注意:

socket.setNoDelay(true)

TCP针对网络中的小数据包有一定的优化策略:Nagle算法。如果每次只发送一个字节的内容而不优化,网络中将充满只有极少数有效数据的数据包,将十分浪费网络资源。Nagle算法针对这种情况,要求缓冲区的数据达到一定数量或者一定时间后才将其发出,所以小数据包将会被Nagle算法合并,以此来优化网络。这种优化虽然使网络宽带被有效地使用,但是数据有可能被延迟发送。

在Node中,TCP默认启用了Nagle算法,可以通过调用 socket.setNoDelay(true)去掉Nagle算法,使得write可以立即发送数据到网络中。但是,尽管在网络的一端调用write会触发另外一端的data事件,但是并不意味着每次write都会触发一次data事件,在关掉Nagle算法后,另一端可能会将接收到的多个小数据包合并,然后只触发一次data事件。

3.3 创建TCP客户端

① 导入 net 模块

② 创建TCP客户端实例,监听socket事件

③发起客户端的请求

3.3.1 导入 net 模块

创建一个 TCP 客户端,需要导入 net 模块:

// 1.导入 net 模块
const net = require ('net')
3.3.2 创建TCP客户端实例,监听socket事件
var PORT = 8266;
var HOST = '127.0.0.1';

// 2. 创建 TCP 客户端实例
const client = net.createConnection(PORT, HOST);

client.on('connect', function(socket){
  console.log(`客户端:已经与服务端建立连接`);
})

client.on('data', function(data){
  console.log('客户端:收到服务端数据,内容为{'+ data +'}');
})

client.on('close', function(data){
  console.log('客户端:连接断开');
})
3.3.3 发起客户端的请求
// 3. 发起客户端的请求
client.write('你好,我是客户端');
client.end('客户端发送完毕');

通过write方法发送数据过去,通过end方法告诉服务器我已经发送完毕。

3.3.4 完整步骤代码

创建一个test_tcp_client.js文件:
在这里插入图片描述

写入以下代码:

// 1.导入 net 模块
const net = require ('net')

var PORT = 8266;
var HOST = '127.0.0.1';

// 2. 创建 TCP 客户端实例
const client = net.createConnection(PORT, HOST);

client.on('connect', function(socket){
  console.log(`客户端:已经与服务端建立连接`);
})

client.on('data', function(data){
  console.log('客户端:收到服务端数据,内容为{'+ data +'}');
})

client.on('close', function(data){
  console.log('客户端:连接断开');
})

// 3. 发起客户端的请求
client.write('你好,我是客户端');
client.end('客户端发送完毕');

这个客户端会访问我们上面做好的服务器端,所以这里需要新开两个终端去测试效果。

先执行一下test_tcp_server.js ,后执行一下test_tcp_client.js 看看。
在这里插入图片描述
从打印信息来看是可以完整看到整个socket连接以及接收数据的过程。

3.4 额外知识(选读)

要想更好理解socket事件,可以了解一些TCP相关知识。

  • TCP连接的建立(三次握手):

在这里插入图片描述
最开始的时候客户端和服务器都是处于CLOSED状态。主动打开连接的为客户端,被动打开连接的是服务器。

  • TCP服务器进程先创建传输控制块TCB,时刻准备接受客户进程的连接请求,此时服务器就进入了LISTEN(监听)状态;
  • TCP客户进程也是先创建传输控制块TCB,然后向服务器发出连接请求报文,这是报文首部中的同部位SYN=1,同时选择一个初始序列号 seq=x ,此时,TCP客户端进程进入了 SYN-SENT(同步已发送状态)状态。TCP规定,SYN报文段(SYN=1的报文段)不能携带数据,但需要消耗掉一个序号
  • TCP服务器收到请求报文后,如果同意连接,则发出确认报文。确认报文中应该 ACK=1,SYN=1,确认号是ack=x+1,同时也要为自己初始化一个序列号 seq=y,此时,TCP服务器进程进入了SYN-RCVD(同步收到)状态。这个报文也不能携带数据,但是同样要消耗一个序号。
  • TCP客户进程收到确认后,还要向服务器给出确认。确认报文的ACK=1,ack=y+1,自己的序列号seq=x+1,此时,TCP连接建立,客户端进入ESTABLISHED(已建立连接)状态。TCP规定,ACK报文段可以携带数据,但是如果不携带数据则不消耗序号。
  • 当服务器收到客户端的确认后也进入ESTABLISHED状态,此后双方就可以开始通信了。
    在这里插入图片描述
  • TCP连接的释放(四次挥手)

在这里插入图片描述
数据传输完毕后,双方都可释放连接。最开始的时候,客户端和服务器都是处于ESTABLISHED状态,然后客户端主动关闭,服务器被动关闭。

  • 客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号
  • 服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
  • 客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
  • 服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
  • 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
  • 服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些
    在这里插入图片描述

额外知识:

TCP中的四个计时器 - 重传计时器、坚持计时器、保活计时器、时间等待计时器

4、构建HTTP

上面介绍完TCP模块,但是实际应用中我们不会直接操作TCP,而更多的是使用基于TCP之上的应用层协议 —— HTTP。

http 模块是 Node.js 官方提供的、用来创建 web 服务器的模块。通过 http 模块提供的 http.createServer() 方法,就能方便的把一台普通的电脑,变成一台 Web 服务器(NodeJs可以很轻易把我们的一台电脑变成一个局域网服务器,当然可以借助内网穿透工具来实现外网访问),从而对外提供 Web 资源服务。

服务器和普通电脑的区别在于,服务器上安装了 web 服务器管理软件,例如:IISApache 等。通过安装这些服务器管理软件,就能把一台普通的电脑变成一台 web 服务器。
在这里插入图片描述
在Node.js 中,我们不需要使用 IIS、Apache、Tomcat 等这些第三方 web 服务器软件。因为我们可以基于 Node.js 提供的http 模块,通过几行简单的代码,就能轻松的手写一个服务器软件,从而对外提供 web 服务。

4.1 http web服务器

一般构建最基本的web服务器包括几个步骤:

  • ① 导入 http 模块

  • ② 创建web 服务器实例

  • ③ 为服务器实例绑定事件,监听客户端的请求

  • ④ 启动服务器

在这里插入图片描述
在这里插入图片描述

4.1.1 导入 http 模块

创建一个 web 服务器,对外提供 web 服务,需要导入 http 模块:

// 1. 导入 http 模块
const http = require('http')

Node的http模块包含了对HTTP处理的封装。在Node中,HTTP服务器继承自TCP服务器(net模块),它能够与多个客户端保持连接,由于其采用事件驱动,不需要为每一个连接创建额外的进程或者线程,能够保持较低的内存占用,所以能够实现高并发。HTTP服务与TCP服务模型有区别的地方在于,在开启keepalive后,一个TCP会话可以用于多次请求和响应。TCP服务以connection为单位进行服务,HTTP服务以request为单位进行服务。http模块将connection到request的过程进行了封装。

此外,http模块将连接套接字抽象为ServerRequestServerResponse对象,分别对应请求和响应。在请求产生的过程中,http模块拿到连接中传来的数据,调用二进制模块http_parser进行解析,在解析完请求报文的报头后,触发request事件,调用用户的业务逻辑

经过http_parser解析请求报文的报头后,request对象就存在诸多有用信息。注意,这里并没有解析 请求体的content内容,需要另外处理

4.1.2 创建web 服务器实例

调用 http.createServer() 方法,即可快速创建一个 web 服务器实例:

// 2.快速创建一个 web 服务器实例
const server = http.createServer()
4.1.3 为服务器实例绑定事件,监听客户端的请求

为服务器实例绑定事件,即可监听客户端发送过来的网络请求:

// 2. 创建 web 服务器实例
const sever = http.createServer()

// 3.为服务器实例绑定request 事件, 监听客户端的请求
// request req
// response res
sever.on('request' , function(req , res){
  console.log('服务端:我收到了客户端请求');
  // 3.1 打印请求状态行
  console.log(`请求方式: ${req.method}`);
  console.log(`HTTP协议版本: ${req.httpVersion}`)
  console.log(`请求地址: ${req.url}`)
  console.log(`请求头: `)
  console.log(req.headers)
  res.end('你好,我是HTTP服务器')
})

sever.on('connection', function(){
  console.log('服务端:我和客户端建立了底层TCP连接')
})

sever.on('close', function(){
  console.log("服务端:我已经关闭服务")
})

sever.on('checkContinue', function(){
  console.log("服务端:客户端数据较大")
})

sever.on('connect', function(){
  console.log("服务端:客户端发起了连接请求")
})

sever.on('upgrade', function(){
  console.log("服务端:客户端协议升级")
})

这里有两个对象 req(request 请求相关) 和 res(response,响应相关).,下面再重点讲解。

在这里插入图片描述
在这里插入图片描述

4.1.4 启动服务器

调用服务器实例的 listen() 方法,即可启动当前的 web 服务器实例:

// 4. 启动服务器
server.listen(80, function () {  
  console.log('server running at http://127.0.0.1:80')
})
4.1.5 完整步骤代码

创建一个test_http_server.js文件
在这里插入图片描述

写入以下代码:

// 1.导入 http 模块
const http = require ('http')

// 2. 创建 web 服务器实例
const sever = http.createServer()

// 3.为服务器实例绑定request 事件, 监听客户端的请求
// request req
// response res
sever.on('request' , function(req , res){
  console.log('服务端:我收到了客户端请求');
  // 3.1 打印请求状态行
  console.log(`请求方式: ${req.method}`);
  console.log(`HTTP协议版本: ${req.httpVersion}`)
  console.log(`请求地址: ${req.url}`)
  console.log(`请求头: `)
  console.log(req.headers)
  res.end('你好,我是HTTP服务器')
})

sever.on('connection', function(){
  console.log('服务端:我和客户端建立了底层TCP连接')
})

sever.on('close', function(){
  console.log("服务端:我已经关闭服务")
})

sever.on('checkContinue', function(){
  console.log("服务端:客户端数据较大")
})

sever.on('connect', function(){
  console.log("服务端:客户端发起了连接请求")
})

sever.on('upgrade', function(){
  console.log("服务端:客户端协议升级")
})

// 4. 启动服务器
sever.listen(80 , function(){
  console.log('server running at http://127.0.0.1:80');
})

执行一下test_http_server.js 看看。
在这里插入图片描述

然后,在浏览器上访问 http://127.0.0.1:80

注意:

  • 如果访问不了上面的IP地址,说明hosts文件配置有问题,可以参考上面关于本地DNS配置。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

那这里提出几个疑问点

  • res 和 req 具体包括了什么属性和方法?
  • 我能否根据不同的请求来响应不同的内容?比如微信里面可能有用户登录功能、朋友圈功能、用户列表等等

先留着这些疑问,来进行下一步的学习。

4.2 http 客户端(选读)

http模块提供了http.request()http.get()两个方法,功能是作为客户端向http服务器发起请求。

发起请求后,我们需要关注客户端事件。一般关注data和end事件即可。

4.2.1 完整步骤代码

创建一个test_tcp_client.js文件
在这里插入图片描述

写入以下代码:

/**
 *HTTP客户端,发送HTTP请求
 *控制台输出返回的响应内容
 */
 var http = require("http");
 var options = {
     host: "localhost",
     port: 80
 }
 
 var req = http.request(options,function(res) {
     res.on("data",function(chunk) {
         console.log(chunk.toString("utf-8"));
     });
     res.on("end",function() {
         console.log("----请求结束!----");
     });
 });
 
 req.on("error",function(err) {
     console.log(err.message);
 });
 
 req.end();

这个客户端会访问我们上面做好的服务器端,所以这里需要新开两个终端去测试效果。

先执行一下test_http_server.js ,后执行一下test_http_client.js 看看。
在这里插入图片描述

从打印信息来看是可以完整看到整个发起以及接收数据的过程。

4.3 http Header(选读)

4.3.1 请求头(了解即可,需要再查资料)

请求头由一系列的键值对(key-value)组成,允许客户端向服务器端发送一些附加信息或者客户端自身的信息,主要包括:

  • Accept

Accept: application/json 浏览器可以接受服务器回发的类型为 application/json。
Accept: / 代表浏览器可以处理所有类型,(一般浏览器发给服务器都是发这个)。

  • Accept-Encoding

Accept-Encoding: gzip, deflate 浏览器申明自己接收的编码方法,通常指定压缩方法,是否支持压缩,支持什么压缩方法(gzip,deflate),(注意:这不是只字符编码)。

  • Accept-Language

Accept-Language:zh-CN,zh;q=0.9 浏览器申明自己接收的语言。

  • Authorization

授权信息,通常出现在对服务器发送的WWW-Authenticate头的应答中。

  • Connection

Connection: keep-alive 当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的连接。
Connection: close 代表一个Request完成后,客户端和服务器之间用于传输HTTP数据的TCP连接会关闭, 当客户端再次发送Request,需要重新建立TCP连接。

  • Host(发送请求时,该报头域是必需的)

Host:www.baidu.com 请求报头域主要用于指定被请求资源的Internet主机和端口号,它通常从HTTP URL中提取出来的。

  • Referer

Referer:https://www.baidu.com/?start=1当浏览器向web服务器发送请求的时候,一般会带上Referer,告诉服务器我是从哪个页面链接过来的,服务器籍此可以获得一些信息用于处理。

  • User-Agent

User-Agent:Mozilla/...,告诉HTTP服务器, 客户端使用的操作系统和浏览器的名称和版本。

  • Cache-Control

Cache-Control:private 默认为private 响应只能够作为私有的缓存,不能再用户间共享
Cache-Control:public 响应会被缓存,并且在多用户间共享。正常情况, 如果要求HTTP认证,响应会自动设置为 private.
Cache-Control:must-revalidate 响应在特定条件下会被重用,以满足接下来的请求,但是它必须到服务器端去验证它是不是仍然是最新的。
Cache-Control:no-cache 响应不会被缓存,而是实时向服务器端请求资源.
Cache-Control:max-age=10 设置缓存最大的有效时间,但是这个参数定义的是时间大小(比如:60)而不是确定的时间点。单位是[秒 seconds]。
Cache-Control:no-store 在任何条件下,响应都不会被缓存,并且不会被写入到客户端的磁盘里,这也是基于安全考虑的某些敏感的响应才会使用这个。

  • Cookie 标识用户信息

Cookie是用来存储一些用户信息以便让服务器辨别用户身份的(大多数需要登录的网站上面会比较常见),比如cookie会存储一些用户的用户名和密码,当用户登录后就会在客户端产生一个cookie来存储相关信息,这样浏览器通过读取cookie的信息去服务器上验证并通过后会判定你是合法用户,从而允许查看相应网页。当然cookie里面的数据不仅仅是上述范围,还有很多信息可以存储是cookie里面,比如sessionid等。

  • Range(用于断点续传)

Range:bytes=0-5 指定第一个字节的位置和最后一个字节的位置。用于告诉服务器自己想取对象的哪部分。

  • Content-Length

表示请求消息正文的长度

// 3.为服务器实例绑定request 事件, 监听客户端的请求
// request req
// response res
sever.on('request' , function(req , res){
  console.log('服务端:我收到了客户端请求');
  console.log(`请求头: `)
  console.log(req.headers)
  res.end('你好,我是HTTP服务器')
})

在这里插入图片描述

4.3.2 响应头(了解即可,需要再查资料)
  • Set-Cookie

设置和页面关联的Cookie。

  • Cache-Control(对应请求中的Cache-Control)

Cache-Control:private 默认为private 响应只能够作为私有的缓存,不能再用户间共享
Cache-Control:public 浏览器和缓存服务器都可以缓存页面信息。
Cache-Control:must-revalidate 对于客户机的每次请求,代理服务器必须想服务器验证缓存是否过时。
Cache-Control:no-cache 浏览器和缓存服务器都不应该缓存页面信息。
Cache-Control:max-age=10 是通知浏览器10秒之内不要烦我,自己从缓冲区中刷新。
Cache-Control:no-store 请求和响应的信息都不应该被存储在对方的磁盘系统中。

  • Content-Type(中文编译错乱,考虑这个属性)

Content-Type:text/html;charset=UTF-8 告诉客户端,资源文件的类型,还有字符编码,客户端通过utf-8对资源进行解码,然后对资源进行html解析。通常我们会看到有些网站是乱码的,往往就是服务器端没有返回正确的编码。

  • Content-Encoding

Content-Encoding:gzip 告诉客户端,服务端发送的资源是采用gzip编码的,客户端看到这个信息后,应该采用gzip对资源进行解码。

  • Date

Date: Tue, 03 Apr 2020 03:52:28 GMT 这个是服务端发送资源时的服务器时间,GMT是格林尼治所在地的标准时间。http协议中发送的时间都是GMT的,这主要是解决在互联网上,不同时区在相互请求资源的时候,时间混乱问题。

  • Server

Server:Tengine/1.4.6 这个是服务器和相对应的版本,只是告诉客户端服务器信息。

  • Transfer-Encoding

Transfer-Encoding:chunked 这个响应头告诉客户端,服务器发送的资源的方式是分块发送的。一般分块发送的资源都是服务器动态生成的,在发送时还不知道发送资源的大小,所以采用分块发送,每一块都是独立的,独立的块都能标示自己的长度,最后一块是0长度的,当客户端读到这个0长度的块时,就可以确定资源已经传输完了。

  • Expires

Expires:Sun, 1 Jan 1994 01:00:00 GMT 这个响应头也是跟缓存有关的,告诉客户端在这个时间前,可以直接访问缓存副本,很显然这个值会存在问题,因为客户端和服务器的时间不一定会都是相同的,如果时间不同就会导致问题。所以这个响应头是没有Cache-Control:max-age=*这个响应头准确的,因为max-age=date中的date是个相对时间,不仅更好理解,也更准确。

  • Last-Modified

Last-Modified: Dec, 26 Dec 2019 17:30:00 GMT 所请求的对象的最后修改日期(按照 RFC 7231 中定义的“超文本传输协议日期”格式来表示)

  • Connection

Connection:keep-alive 这个字段作为回应客户端的Connection:keep-alive,告诉客户端服务器的tcp连接也是一个长连接,客户端可以继续使用这个tcp连接发送http请求。

  • Etag

ETag: "637060cd8c284d8af7ad3082f209582d" 就是一个对象(比如URL)的标志值,就一个对象而言,比如一个html文件,如果被修改了,其Etag也会别修改,所以,ETag的作用跟Last-Modified的作用差不多,主要供WEB服务器判断一个对象是否改变了。比如前一次请求某个html文件时,获得了其 ETag,当这次又请求这个文件时,浏览器就会把先前获得ETag值发送给WEB服务器,然后WEB服务器会把这个ETag跟该文件的当前ETag进行对比,然后就知道这个文件有没有改变了。

  • Refresh

Refresh: 5; url=http://baidu.com 用于重定向,或者当一个新的资源被创建时。默认会在5秒后刷新重定向。

  • Location

这个头配合302状态码使用,用于重定向接收者到一个新URI地址。表示客户应当到哪里去提取文档。Location通常不是直接设置的,而是通过HttpServletResponse的sendRedirect方法,该方法同时设置状态代码为302。

  • Access-Control-Allow-Origin

Access-Control-Allow-Origin: * 号代表所有网站可以跨域资源共享,如果当前字段为那么Access-Control-Allow-Credentials就不能为true
Access-Control-Allow-Origin: www.baidu.com 指定哪些网站可以跨域资源共享

  • Access-Control-Allow-Methods

Access-Control-Allow-Methods:GET,POST,PUT,DELETE 允许哪些方法来访问

  • Access-Control-Allow-Credentials

Access-Control-Allow-Credentials: true 是否允许发送cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。如果access-control-allow-origin为*,当前字段就不能为true

  • Content-Range

Content-Range: bytes 0-5/7877 指定整个实体中的一部分的插入位置,它也指示了整个实体的长度。在服务器向客户返回一个部分响应,它必须描述响应覆盖的范围和整个实体长度。

5、构建Web应用

对于一个Web应用而言,仅仅是上面的响应处理是远远达不到业务的需求。在具体业务中,我们可能有如下需求:

  • 请求方法的判断
  • URL的路径解析
  • URL中查询字符串解析
  • Cookie解析
  • Basic认证
  • 表单数据的解析
  • 任意格式文件的上传处理
  • Session会话

这些所有需求的开始都是以这里展开:

// 2. 创建 web 服务器实例
const sever = http.createServer()

// 3.为服务器实例绑定request 事件, 监听客户端的请求
// request req
// response res
sever.on('request' , function(req , res){
  console.log('服务端:我收到了客户端请求');
  // 下面需要进行具体业务处理
})
  • 请求方法的判断
  • URL的路径解析
  • URL中查询字符串解析
  • Cookie解析
  • Basic认证
  • 表单数据的解析
  • 任意格式文件的上传处理
  • Session会话
  • … etc

除开请求体的内容,HTTP_Parser会把请求报文头抽取出来,然后解析出 req对象的一系列内容。

Node的http模块只对HTTP报文的头部进行解析,然后触发request事件。如果请求中还带有内容部分(如POST请求,它有报文和内容,内容会包括表单提交、文件提交、JSON上传、XML上传等等),内容部分需要用户自行接收和解析
在这里插入图片描述

5.1 请求方法

在web应用中,最常见的请求方法就是GETPOST。此外还有HEAD、DELETE、PUT等方法。

请求方法存在于报文的第一行第一个单词,通常都是大写。比如:

GET /example.html HTTP/1.1

这里的请求方法通过 req.method获取。再通过请求方法来决定响应行为。

sever.on('request' , function(req , res){
  console.log('服务端:我收到了客户端请求');
  // 3.1 打印请求状态行
  console.log(`请求方式: ${req.method}`);
  console.log(`请求地址: ${req.url}`)
  console.log(`请求头: `)
  console.log(req.headers)

  switch(req.method) {
    case 'POST':
      handlePost(req,res)
      break
    case 'GET':
      handleGet(req,res)
      break
    case 'PUT':
      break
    case 'HEAD':
      break
    case 'DELETE':
      break
    default:
      break            
  }
})

上述代码根据请求方法将业务逻辑进行分发,后面我们一般思路也需要这样考虑。我们执行以下代码看看效果:

5.2 URL的路径解析

除了根据请求方法来进行分发外,我们还需要根据路径来进行处理。

通过请求路径,我们可以区分场景:

/login 表示登录请求
/user 表示用户信息
/send 表示发送信息

而HTTP报文上对应格式:

GET /login?uid=123 HTTP/1.1

HTTP_Parser将红色部分其解析为req.url。一般完整的URL地址如下:

http://user:pass@host.com:8080/p/a/t/h?query=string

  • query叫做查询字符串参数

我们分别测试以下几条链接:

  • http://127.0.0.1/login?user=dpjcn&from=csdn
  • http://127.0.0.1/luser?user=dpjcn&from=csdn
  • http://127.0.0.1/lsend?user=dpjcn&from=csdn

在这里插入图片描述
可以看到请求路径包含了pathquery两个部分。那么,我们要怎么继续拆成两个部分。需要用到url模块。

// 3.为服务器实例绑定request 事件, 监听客户端的请求
// request req
// response res
sever.on('request' , function(req , res){
  console.log('服务端:我收到了客户端请求');
  // 3.1 打印请求状态行
  console.log(`请求方式: ${req.method}`);
  console.log(`请求地址: ${req.url}`)

  var pathname = url.parse(req.url).pathname
  var query = url.parse(req.url).query
  console.log(`请求Path: ${pathname}`)
  console.log(`请求Query: ${query}`)
})

在这里插入图片描述
拿到Path之后,我们就可以参考请求方法的方式去进行业务分发。比如我们处理GET请求的路径分发:

// 3.为服务器实例绑定request 事件, 监听客户端的请求
// request req
// response res
sever.on('request' , function(req , res){
  console.log('服务端:我收到了客户端请求');
  // 3.1 打印请求状态行
  console.log(`请求方式: ${req.method}`);
  console.log(`请求地址: ${req.url}`)
  // console.log(`请求头: `)
  // console.log(req.headers)

  switch(req.method) {
    case 'POST':
      handlePost(req,res)
      break
    case 'GET':
      handleGet(req,res)
      break
    case 'PUT':
      break
    case 'HEAD':
      break
    case 'DELETE':
      break
    default:
      break            
  }
})

function handleGet(req, res){
  console.log('handleGet')

  var pathname = url.parse(req.url).pathname
  var query = url.parse(req.url).query
  console.log(`请求Path: ${pathname}`)
  console.log(`请求Query: ${query}`)

  switch(pathname) {
    case '/login':
      console.log('触发 /login 业务')
      break
    case '/user':
      console.log('触发 /user 业务')
      break
    case '/send':
      console.log('触发 /send 业务')
      break
    default:
      break            
  }

  res.end('你好,我是HTTP服务器')
}

在这里插入图片描述

5.3 URL中查询字符串解析

查询字符串位于路径Path之后,在地址栏路径后 ?xxx=xxx&xxx=xxx字符串就是查询字符串。一般用于判断具体参数。比如同样是 /login 路径,我们需要区分到底是哪个用户进行了登录。形式如下:

  • /login?user=A&age=11 用户A登录
  • /login?user=B&age=12 用户B登录

要解析这部分的内容,Node提供了querystring模块用于处理这部分数据 ,解析成一个JSON对象

代码:

function handleGet(req, res){
  console.log('handleGet')

  var pathname = url.parse(req.url).pathname
  var query = querystring.parse(url.parse(req.url).query)
  console.log(`请求Path: ${pathname}`)
  console.log(`请求query:`)
  console.log(query)
  
  switch(pathname) {
    case '/login':
      console.log(`触发 /login 业务 ${query.user} ${query.age}`)
      break
    case '/user':
      console.log(`触发 /user 业务`)
      break
    case '/send':
      console.log(`触发 /send 业务`)
      break
    default:
      break            
  }

  res.end('你好,我是HTTP服务器')
}

浏览器上输入:

http://127.0.0.1/login?user=dpjcn&age=18

在这里插入图片描述

5.4 Basic认证

Basic认证是当客户端与服务器端进行请求时,允许通过用户名和密码实现的一种身份认证方式。

如果一个页面需要Basic认证,它会检测请求报文头中的Authorization字段的内容。该字段的值由认证方式加密值构成。
在这里插入图片描述
在Basic认证中,它会将用户和密码部分组合:

username + ":" + password

然后进行base64编码。

var encode = function(username, password){
  return Buffer(username+":"+password).toString('base64');
}

如果用户首次访问该网页,URL地址中也没有携带认证内容,那么浏览器会响应一个 401未授权状态码。

// 1.导入 http 模块
const http = require ('http')
const url = require('url')
const querystring = require('querystring')

var encode = function(username, password){
  return Buffer(username+":"+password).toString('base64');
}

// 2. 创建 web 服务器实例
const sever = http.createServer()

// 3.为服务器实例绑定request 事件, 监听客户端的请求
// request req
// response res
sever.on('request' , function(req , res){
  console.log('服务端:我收到了客户端请求');
  // 3.1 打印请求状态行
  console.log(`请求方式: ${req.method}`);
  console.log(`请求地址: ${req.url}`)
  // console.log(`请求头: `)
  // console.log(req.headers)

  // 校验Basic
  var auth = req.headers['authorization'] || ''
  var parts = auth.split(" ")
  var method = parts[0] || '' // basic
  var encoded = parts[1] || '' // 加密值
  var decoded = Buffer(encoded, 'base64').toString('utf-8').split(":")
  var user = decoded[0]
  var pass = decoded[1]

  if (user != 'dpjcn') {
    console.log(`Basic 认证`)
    res.setHeader('WWW-Authenticate', 'Basic realm="Secure Area"')
    res.writeHead(401)
    res.end()
    return
  }

  switch(req.method) {
    case 'POST':
      handlePost(req,res)
      break
    case 'GET':
      handleGet(req,res)
      break
    case 'PUT':
      break
    case 'HEAD':
      break
    case 'DELETE':
      break
    default:
      break            
  }
})

function handleGet(req, res){
  console.log('handleGet')

  var pathname = url.parse(req.url).pathname
  var query = querystring.parse(url.parse(req.url).query)
  console.log(`请求Path: ${pathname}`)
  console.log(`请求query:`)
  console.log(query)
  
  switch(pathname) {
    case '/login':
      console.log(`触发 /login 业务 ${query.user} ${query.age}`)
      break
    case '/user':
      console.log(`触发 /user 业务`)
      break
    case '/send':
      console.log(`触发 /send 业务`)
      break
    default:
      break            
  }

  res.end('你好,我是HTTP服务器')
}

function handlePost(req, res){
  console.log('handlePost')
}

sever.on('connection', function(){
  console.log('服务端:我和客户端建立了底层TCP连接')
})

sever.on('close', function(){
  console.log("服务端:我已经关闭服务")
})

sever.on('checkContinue', function(){
  console.log("服务端:客户端数据较大")
})

sever.on('connect', function(){
  console.log("服务端:客户端发起了连接请求")
})

sever.on('upgrade', function(){
  console.log("服务端:客户端协议升级")
})

// 4. 启动服务器
sever.listen(80 , function(){
  console.log('server running at http://127.0.0.1:80');
})

在这里插入图片描述
这里我们定义了必须用户名是 dpjcn

注意:

  • 这种认证方式一般不太安全,用用就好。

5.6 数据上传

上述内容基本上集中在HTTP请求头,适用于GET请求和大多数其他请求。但是单纯的头部报文无法携带大量数据,在业务中,我们需要额外接收一些数据,比如表单提交文件提交JSON上传XML上传等。

Node的http模块只对HTTP报文头进行了解析(HTTP _Parser),然后触发request事件。如果请求中带有内容部分(比如POST请求,具有报头和内容,我们待会会用工具去模拟POST请求),内容部分需要用户自行接收和解析。

通过报头的Tranfer-Encoding或者Content-Length可以判断请求中是否带有内容。一般判断步骤:

  • 是否是POST请求
  • 是否有Tranfer-Encoding 或者 Content-Length

所以代码可以这样写:

// 判断是否存在消息体
var hasBody = function(req) {
  if(req.method == 'POST') {
    return 'transfer-encoding' in req.headers || 'content-length' in req.headers
  } else {
    return false
  }
}

在HTTP_Parser解析报头结束后,报文内容会通过data事件触发,我们只需要以流的方式处理即可。

// 处理Post请求
function handlePost(req, res){
  console.log('handlePost')
  if (hasBody(req)){
    var buffers = []
    req.on('data', function(chunk){
      buffers.push(chunk)
    })

    req.on('end', function(){
      req.rawBody = Buffer.concat(buffers).toString()
      handle(req, res)
    })
  } else {
    handle(req, res)
  }
}

function handle(req, res) {
  var pathname = url.parse(req.url).pathname
  console.log(`请求Path: ${pathname}`)
  
  switch(pathname) {
    case '/login':
      console.log(`触发 /login 业务 ${query.user} ${query.age}`)
      break
    case '/user':
      console.log(`触发 /user 业务`)
      break
    case '/send':
      console.log(`触发 /send 业务`)
      break
    default:
      break            
  }

  res.end('你好,我是HTTP服务器')
}

将接收到的Buffer列表转化为一个Buffer对象,再转换为没有乱码的字符串,挂载在req.rawBody. 下面我们就会根据具体场景来解析 rawBody。

在这里我们需要使用到一个API请求模拟工具。
这里使用一个国产工具 apifox

https://www.apifox.cn/

下载一个桌面版安装即可,非常简单。

5.6.1 表单数据

最为常见的数据提交就是通过网页表单提交数据到服务器端。

<script type="text/javascript">
    function check(){
        var name = document.getElementById("name").value;
        if(name ==  null || name == ''){
        alert("用户名不能为空");
        return false;
        }
    return true;
    }
</script>

<form action="http://baidu.com" method="post" onsubmit="return check()">
    <input type="text" id="name" name="name">用户名
    <input type="text" id="password" name="password">密码
    <input type="submit" value="提交">
</form>

默认的表单提交,请求头中的content-type字段值为 application/x-www-form-urlencoded

content-type: application/x-www-form-urlencoded

它的报文体内容跟查询字符串queryString相同。

xxxx=xxxx&xxxx=xxxx

所以解析代码可以这样写:

function handle(req, res) {
  var pathname = url.parse(req.url).pathname
  console.log(`请求Path: ${pathname}`)
  
  /// 解析表单类型
  if (req.headers['content-type'] === 'application/x-www-form-urlencoded') {
    console.log('服务端:这个请求是表单请求')
    req.body = querystring.parse(req.rawBody)
    console.log(req.body)
    // 下面业务就可以直接访问req.body
    switch(pathname) {
      case '/login':
        console.log(`触发 /login 业务`)
        break
      case '/user':
        console.log(`触发 /user 业务`)
        break
      case '/send':
        console.log(`触发 /send 业务`)
        break
      default:
        break      
    }
  }

  res.end('你好,我是HTTP服务器')
}

完整测试代码:

// 1.导入 http 模块
const http = require ('http')
const url = require('url')
const querystring = require('querystring')

// basic 编码
var encode = function(username, password){
  return Buffer(username+":"+password).toString('base64');
}

// 判断是否存在消息体
var hasBody = function(req) {
  if(req.method == 'POST') {
    return 'transfer-encoding' in req.headers || 'content-length' in req.headers
  } else {
    return false
  }
}

// 2. 创建 web 服务器实例
const sever = http.createServer()

// 3.为服务器实例绑定request 事件, 监听客户端的请求
// request req
// response res
sever.on('request' , function(req , res){
  console.log('服务端:我收到了客户端请求');
  // 3.1 打印请求状态行
  console.log(`请求方式: ${req.method}`);
  console.log(`请求地址: ${req.url}`)
  // console.log(`请求头: `)
  // console.log(req.headers)

  // 校验Basic
  // var auth = req.headers['authorization'] || ''
  // var parts = auth.split(" ")
  // var method = parts[0] || '' // basic
  // var encoded = parts[1] || '' // 加密值
  // var decoded = Buffer(encoded, 'base64').toString('utf-8').split(":")
  // var user = decoded[0]
  // var pass = decoded[1]

  // if (user != 'dpjcn') {
  //   console.log(`Basic 认证`)
  //   res.setHeader('WWW-Authenticate', 'Basic realm="Secure Area"')
  //   res.writeHead(401)
  //   res.end()
  //   return
  // }

  switch(req.method) {
    case 'POST':
      handlePost(req,res)
      break
    case 'GET':
      handleGet(req,res)
      break
    case 'PUT':
      break
    case 'HEAD':
      break
    case 'DELETE':
      break
    default:
      break            
  }
})

function handleGet(req, res){
  console.log('handleGet')

  var pathname = url.parse(req.url).pathname
  var query = querystring.parse(url.parse(req.url).query)
  console.log(`请求Path: ${pathname}`)
  console.log(`请求query:`)
  console.log(query)
  
  switch(pathname) {
    case '/login':
      console.log(`触发 /login 业务 ${query.user} ${query.age}`)
      break
    case '/user':
      console.log(`触发 /user 业务`)
      break
    case '/send':
      console.log(`触发 /send 业务`)
      break
    default:
      break            
  }

  res.end('你好,我是HTTP服务器')
}

// 处理Post请求
function handlePost(req, res){
  console.log('handlePost')
  if (hasBody(req)){
    var buffers = []
    req.on('data', function(chunk){
      buffers.push(chunk)
    })

    req.on('end', function(){
      req.rawBody = Buffer.concat(buffers).toString()
      handle(req, res)
    })
  } else {
    handle(req, res)
  }
}

function handle(req, res) {
  var pathname = url.parse(req.url).pathname
  console.log(`请求Path: ${pathname}`)
  
  /// 解析表单类型
  if (req.headers['content-type'] === 'application/x-www-form-urlencoded') {
    console.log('服务端:这个请求是表单请求')
    req.body = querystring.parse(req.rawBody)
    console.log(req.body)
    // 下面业务就可以直接访问req.body
    switch(pathname) {
      case '/login':
        console.log(`触发 /login 业务`)
        break
      case '/user':
        console.log(`触发 /user 业务`)
        break
      case '/send':
        console.log(`触发 /send 业务`)
        break
      default:
        break      
    }
  }

  res.end('你好,我是HTTP服务器')
}

sever.on('connection', function(){
  console.log('服务端:我和客户端建立了底层TCP连接')
})

sever.on('close', function(){
  console.log("服务端:我已经关闭服务")
})

sever.on('checkContinue', function(){
  console.log("服务端:客户端数据较大")
})

sever.on('connect', function(){
  console.log("服务端:客户端发起了连接请求")
})

sever.on('upgrade', function(){
  console.log("服务端:客户端协议升级")
})

// 4. 启动服务器
sever.listen(80 , function(){
  console.log('server running at http://127.0.0.1:80');
})

在apifox上我们创建一个POST请求用来提交表单数据。
在这里插入图片描述
在这里插入图片描述
发送的时候记得选择测试环境(也就是本地localhost)。

可以看到VSCode会打印如下信息:
在这里插入图片描述
这里表示收到了我们的表单请求。

5.6.2 JSON数据

常见的提交有JSON数据,判断和解析它们的原理都非常相似、也是依据content-type的值来决定,JSON类型的值为 application/json

需要注意的是,Content-type中可能还附加了编码信息:

content-type: application/json;charset=utf-8

所以解析代码可以这样写:

var mime = function(req) {
  var str = req.headers['content-type'] || ''
  return str.split(':')[0]
}

/// 解析JSON类型
function handleJSON(req, res) {
  var pathname = url.parse(req.url).pathname
  console.log(`请求Path: ${pathname}`)
  try {
    req.body = JSON.parse(req.rawBody)
    console.log(req.body)
  } catch (e) {
    // 异常内容
    res.writeHead(400)
    res.end('Invalid JSON')
    return
  }

    // 下面业务就可以直接访问req.body
  switch(pathname) {
      case '/login':
        console.log(`触发 /login 业务`)
        break
      case '/user':
        console.log(`触发 /user 业务`)
        break
      case '/send':
        console.log(`触发 /send 业务`)
        break
      default:
        break      
  }

  res.end('你好,我是HTTP服务器')
}

接下来我们测试一下效果,写入完整测试代码:

// 1.导入 http 模块
const http = require ('http')
const url = require('url')
const querystring = require('querystring')

// basic 编码
var encode = function(username, password){
  return Buffer(username+":"+password).toString('base64');
}

// 判断是否存在消息体
var hasBody = function(req) {
  if(req.method == 'POST') {
    return 'transfer-encoding' in req.headers || 'content-length' in req.headers
  } else {
    return false
  }
}

var mime = function(req) {
  var str = req.headers['content-type'] || ''
  return str.split(':')[0]
}

// 2. 创建 web 服务器实例
const sever = http.createServer()

// 3.为服务器实例绑定request 事件, 监听客户端的请求
// request req
// response res
sever.on('request' , function(req , res){
  console.log('服务端:我收到了客户端请求');
  // 3.1 打印请求状态行
  console.log(`请求方式: ${req.method}`);
  console.log(`请求地址: ${req.url}`)
  // console.log(`请求头: `)
  // console.log(req.headers)

  // 校验Basic
  // var auth = req.headers['authorization'] || ''
  // var parts = auth.split(" ")
  // var method = parts[0] || '' // basic
  // var encoded = parts[1] || '' // 加密值
  // var decoded = Buffer(encoded, 'base64').toString('utf-8').split(":")
  // var user = decoded[0]
  // var pass = decoded[1]

  // if (user != 'dpjcn') {
  //   console.log(`Basic 认证`)
  //   res.setHeader('WWW-Authenticate', 'Basic realm="Secure Area"')
  //   res.writeHead(401)
  //   res.end()
  //   return
  // }

  switch(req.method) {
    case 'POST':
      handlePost(req,res)
      break
    case 'GET':
      handleGet(req,res)
      break
    case 'PUT':
      break
    case 'HEAD':
      break
    case 'DELETE':
      break
    default:
      break            
  }
})

function handleGet(req, res){
  console.log('handleGet')

  var pathname = url.parse(req.url).pathname
  var query = querystring.parse(url.parse(req.url).query)
  console.log(`请求Path: ${pathname}`)
  console.log(`请求query:`)
  console.log(query)
  
  switch(pathname) {
    case '/login':
      console.log(`触发 /login 业务 ${query.user} ${query.age}`)
      break
    case '/user':
      console.log(`触发 /user 业务`)
      break
    case '/send':
      console.log(`触发 /send 业务`)
      break
    default:
      break            
  }

  res.end('你好,我是HTTP服务器')
}

// 处理Post请求
function handlePost(req, res){
  console.log('handlePost')
  if (hasBody(req)){
    var buffers = []
    req.on('data', function(chunk){
      buffers.push(chunk)
    })

    req.on('end', function(){
      req.rawBody = Buffer.concat(buffers).toString()
      handle(req, res)
    })
  } else {
    handle(req, res)
  }
}

function handle(req, res){
  if (req.headers['content-type'] === 'application/x-www-form-urlencoded') {
    console.log('服务端:这个请求是表单请求')
    handleForm(req,res)
  } else if (mime(req) === 'application/json') {
    console.log('服务端:这个请求体是JSON数据')
    handleJSON(req,res)
  }
}

/// 解析JSON类型
function handleJSON(req, res) {
  var pathname = url.parse(req.url).pathname
  console.log(`请求Path: ${pathname}`)
  try {
    req.body = JSON.parse(req.rawBody)
    console.log(req.body)
  } catch (e) {
    // 异常内容
    res.writeHead(400)
    res.end('Invalid JSON')
    return
  }

    // 下面业务就可以直接访问req.body
  switch(pathname) {
      case '/login':
        console.log(`触发 /login 业务`)
        break
      case '/user':
        console.log(`触发 /user 业务`)
        break
      case '/send':
        console.log(`触发 /send 业务`)
        break
      default:
        break      
  }

  res.end('你好,我是HTTP服务器')
}

/// 解析表单类型
function handleForm(req, res) {
  var pathname = url.parse(req.url).pathname
  console.log(`请求Path: ${pathname}`)
  req.body = querystring.parse(req.rawBody)
  console.log(req.body)
  // 下面业务就可以直接访问req.body
  switch(pathname) {
      case '/login':
        console.log(`触发 /login 业务`)
        break
      case '/user':
        console.log(`触发 /user 业务`)
        break
      case '/send':
        console.log(`触发 /send 业务`)
        break
      default:
        break      
  }

  res.end('你好,我是HTTP服务器')
}

sever.on('connection', function(){
  console.log('服务端:我和客户端建立了底层TCP连接')
})

sever.on('close', function(){
  console.log("服务端:我已经关闭服务")
})

sever.on('checkContinue', function(){
  console.log("服务端:客户端数据较大")
})

sever.on('connect', function(){
  console.log("服务端:客户端发起了连接请求")
})

sever.on('upgrade', function(){
  console.log("服务端:客户端协议升级")
})

// 4. 启动服务器
sever.listen(80 , function(){
  console.log('server running at http://127.0.0.1:80');
})

在apifox上我们创建一个POST请求用来提交JSON数据。
在这里插入图片描述
在这里插入图片描述

发送的时候记得选择测试环境(也就是本地localhost)。

可以看到VSCode会打印如下信息:
在这里插入图片描述

这里表示收到了我们JSON请求。

5.6.3 XML数据

常见数据格式还有XM格式,需要另外一个模块支持。这里我们使用xml2js模块。需要通过npm工具下载:

npm install xml2js --save

在这里插入图片描述
所以解析代码可以这样写:

const xml2js = require('xml2js')

/// 解析XML类型
function handleXML(req, res) {
  var pathname = url.parse(req.url).pathname
  console.log(`请求Path: ${pathname}`)

  xml2js.parseString(req.rawBody, function(err, xml) {
      if (err) {
         // 异常内容
         res.writeHead(400)
         res.end('Invalid XML')
         return
      }
      req.body = xml
      console.log(req.body)
        // 下面业务就可以直接访问req.body
      switch(pathname) {
         case '/login':
           console.log(`触发 /login 业务`)
           break
         case '/user':
           console.log(`触发 /user 业务`)
           break
         case '/send':
           console.log(`触发 /send 业务`)
           break
         default:
          break      
      }
      res.end('你好,我是HTTP服务器')
  })
}

接下来我们测试一下效果,写入完整测试代码:

// 1.导入 http 模块
const http = require ('http')
const url = require('url')
const querystring = require('querystring')
const xml2js = require('xml2js')

// basic 编码
var encode = function(username, password){
  return Buffer(username+":"+password).toString('base64');
}

// 判断是否存在消息体
var hasBody = function(req) {
  if(req.method == 'POST') {
    return 'transfer-encoding' in req.headers || 'content-length' in req.headers
  } else {
    return false
  }
}

var mime = function(req) {
  var str = req.headers['content-type'] || ''
  return str.split(':')[0]
}

// 2. 创建 web 服务器实例
const sever = http.createServer()

// 3.为服务器实例绑定request 事件, 监听客户端的请求
// request req
// response res
sever.on('request' , function(req , res){
  console.log('服务端:我收到了客户端请求');
  // 3.1 打印请求状态行
  console.log(`请求方式: ${req.method}`);
  console.log(`请求地址: ${req.url}`)
  // console.log(`请求头: `)
  // console.log(req.headers)

  // 校验Basic
  // var auth = req.headers['authorization'] || ''
  // var parts = auth.split(" ")
  // var method = parts[0] || '' // basic
  // var encoded = parts[1] || '' // 加密值
  // var decoded = Buffer(encoded, 'base64').toString('utf-8').split(":")
  // var user = decoded[0]
  // var pass = decoded[1]

  // if (user != 'dpjcn') {
  //   console.log(`Basic 认证`)
  //   res.setHeader('WWW-Authenticate', 'Basic realm="Secure Area"')
  //   res.writeHead(401)
  //   res.end()
  //   return
  // }

  switch(req.method) {
    case 'POST':
      handlePost(req,res)
      break
    case 'GET':
      handleGet(req,res)
      break
    case 'PUT':
      break
    case 'HEAD':
      break
    case 'DELETE':
      break
    default:
      break            
  }
})

function handleGet(req, res){
  console.log('handleGet')

  var pathname = url.parse(req.url).pathname
  var query = querystring.parse(url.parse(req.url).query)
  console.log(`请求Path: ${pathname}`)
  console.log(`请求query:`)
  console.log(query)
  
  switch(pathname) {
    case '/login':
      console.log(`触发 /login 业务 ${query.user} ${query.age}`)
      break
    case '/user':
      console.log(`触发 /user 业务`)
      break
    case '/send':
      console.log(`触发 /send 业务`)
      break
    default:
      break            
  }

  res.end('你好,我是HTTP服务器')
}

// 处理Post请求
function handlePost(req, res){
  console.log('handlePost')
  if (hasBody(req)){
    var buffers = []
    req.on('data', function(chunk){
      buffers.push(chunk)
    })

    req.on('end', function(){
      req.rawBody = Buffer.concat(buffers).toString()
      handle(req, res)
    })
  } else {
    handle(req, res)
  }
}

function handle(req, res){
  if (req.headers['content-type'] === 'application/x-www-form-urlencoded') {
    console.log('服务端:这个请求是表单请求')
    handleForm(req,res)
  } else if (mime(req) === 'application/json') {
    console.log('服务端:这个请求体是JSON数据')
    handleJSON(req,res)
  } else if (mime(req)=== 'application/xml') {
    console.log('服务端:这个请求体是XML数据')
    handleXML(req,res)
  }
}

/// 解析XML类型
function handleXML(req, res) {
  var pathname = url.parse(req.url).pathname
  console.log(`请求Path: ${pathname}`)

  xml2js.parseString(req.rawBody, function(err, xml) {
      if (err) {
         // 异常内容
         res.writeHead(400)
         res.end('Invalid XML')
         return
      }
      req.body = xml
      console.log(req.body)
        // 下面业务就可以直接访问req.body
      switch(pathname) {
         case '/login':
           console.log(`触发 /login 业务`)
           break
         case '/user':
           console.log(`触发 /user 业务`)
           break
         case '/send':
           console.log(`触发 /send 业务`)
           break
         default:
          break      
      }
      res.end('你好,我是HTTP服务器')
  })
}

/// 解析JSON类型
function handleJSON(req, res) {
  var pathname = url.parse(req.url).pathname
  console.log(`请求Path: ${pathname}`)
  try {
    req.body = JSON.parse(req.rawBody)
    console.log(req.body)
  } catch (e) {
    // 异常内容
    res.writeHead(400)
    res.end('Invalid JSON')
    return
  }

    // 下面业务就可以直接访问req.body
  switch(pathname) {
      case '/login':
        console.log(`触发 /login 业务`)
        break
      case '/user':
        console.log(`触发 /user 业务`)
        break
      case '/send':
        console.log(`触发 /send 业务`)
        break
      default:
        break      
  }

  res.end('你好,我是HTTP服务器')
}

/// 解析表单类型
function handleForm(req, res) {
  var pathname = url.parse(req.url).pathname
  console.log(`请求Path: ${pathname}`)
  req.body = querystring.parse(req.rawBody)
  console.log(req.body)
  // 下面业务就可以直接访问req.body
  switch(pathname) {
      case '/login':
        console.log(`触发 /login 业务`)
        break
      case '/user':
        console.log(`触发 /user 业务`)
        break
      case '/send':
        console.log(`触发 /send 业务`)
        break
      default:
        break      
  }

  res.end('你好,我是HTTP服务器')
}

sever.on('connection', function(){
  console.log('服务端:我和客户端建立了底层TCP连接')
})

sever.on('close', function(){
  console.log("服务端:我已经关闭服务")
})

sever.on('checkContinue', function(){
  console.log("服务端:客户端数据较大")
})

sever.on('connect', function(){
  console.log("服务端:客户端发起了连接请求")
})

sever.on('upgrade', function(){
  console.log("服务端:客户端协议升级")
})

// 4. 启动服务器
sever.listen(80 , function(){
  console.log('server running at http://127.0.0.1:80');
})

在apifox上我们创建一个POST请求用来提交XML数据。
在这里插入图片描述
在这里插入图片描述

发送的时候记得选择测试环境(也就是本地localhost)。

可以看到VSCode会打印如下信息:
在这里插入图片描述

这里表示收到了我们的XML请求.剩下就是我们一些具体业务逻辑处理即可。

6、总结

篇②主要是通过简单学习网络编程TCP、HTTP、Web应用部分,这是属于Node内置提供的模块,一般用于学习加深理解即可。

一般情况下我们会基于Node提供的基础API进一步封装出框架来方便使用,比如下一篇讲解的Express框架。但是博主还是希望大家可以多点了解底层原理而不是简单调用API方法。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

单片机菜鸟哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值