物联网服务NodeJs-5天学习第二天篇② —— 网络编程(TCPHTTPWeb应用服务)

Posted 单片机菜鸟哥

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了物联网服务NodeJs-5天学习第二天篇② —— 网络编程(TCPHTTPWeb应用服务)相关的知识,希望对你有一定的参考价值。

【NodeJs-5天学习】第二天篇② ——网络编程

面向读者群体

  • ❤️ 电子物联网专业同学,想针对硬件功能构造简单的服务器,不需要学习专业的服务器开发知识 ❤️
  • ❤️ 业余爱好物联网开发者,有简单技术基础,想针对硬件功能构造简单的服务器❤️
  • ❤️ 本篇创建记录 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.物联网服务NodeJs-5天学习第二天篇① ——fs文件系统

物联网服务NodeJs-5天学习第二天篇④ ——项目模块化

物联网服务NodeJs-5天学习第三天实战篇② ——基于物联网的WiFi自动打卡考勤系统

物联网服务NodeJs-5天学习第四天存储篇② ——NodeJs连接操作mysql 8.0

物联网服务NodeJs-5天学习第一天篇② —— 安装NodeJs环境以及VsCode开发工具

基于物联网的NodeJs-5天学习入门指引