面向读者群体
- ❤️ 电子物联网专业同学,想针对硬件功能构造简单的服务器,不需要学习专业的服务器开发知识 ❤️
- ❤️ 业余爱好物联网开发者,有简单技术基础,想针对硬件功能构造简单的服务器❤️
- ❤️ 本篇创建记录 2023-03-12 ❤️
- ❤️ 本篇更新记录 2023-03-12 ❤️
技术要求
- 有HTML、CSS、JavaScript基础更好,当然也没事,就直接运行实例代码学习
专栏介绍
- 通过简短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提供了net
、dgram
、http
、https
这4个模块,分别用于处理TCP
、UDP
、HTTP
、HTTPS
,适用于服务器和客户端。
说到服务器以及客户端,我们先来了解一下c/s结构。
C/S是Client/Server的缩写。
PC
、工作站
或小型机
,并采用大型数据库
系统,如Oracle、Sybase、Informix或 SQL Server。客户端软件
,比如浏览器
、App
、小程序
等等。当然从广义上来说,也有可能是一台服务器,比如一台服务器向另外一台服务器请求数据。客户端发起一个请求内容给到服务器,服务器收到请求之后再解析请求并对请求内容作出响应,最终把响应内容返回给到客户端。
比如:
- 我们在浏览器上访问百度,本质上就是
浏览器client
向百度服务器server
发起请求百度网页内容,百度服务器收到请求
后解析成功并把百度首页页面内容响应
给回到浏览器。- 我们在b站上搜索某一个学习视频,本质上就是
app
向b站服务器
发起请求搜索请求,请求数据会带上要搜索的关键字
,b站服务器收到请求后会把关键字解析出来并且发起搜索服务
(搜索数据库等等),拿到搜索结果后再响应给回到app,这样app就可以展示我们要搜索的内容。
在讲解 网络模块使用之前,我们需要先了解几个服务器概念。
唯一地址
,因此 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地址?
门牌号
一样。通过门牌号,外卖小哥可以在整栋大楼众多的房间中,准确把外卖送到你的手中。注意:
唯一性
决定了一个端口号
不能同时被多个服务占用- 默认情况下,当不填端口是默认是
80
。比如 http://www.baidu.com:80/index.html 等同于 http://www.baidu.com/index.html
尽管 IP 地址能够唯一地标记网络上的计算机,但IP地址是一长串数字,不直观,而且不便于记忆,于是人们又发明了另一套字符型的地址方案,即所谓的域名
(Domain Name)地址。
IP地址和域名是一一对应的关系,这份对应关系存放在一种叫做域名服务器
(DNS,Domain name server)的电脑中。使用者只需通过好记的域名访问对应的服务器即可,对应的转换工作由域名服务器实现。因此,域名服务器就是提供 IP 地址和域名之间的转换服务的服务器。
当我们用域名访问服务器时,比如访问
http://www.baidu.com/index.html
它的步骤是:
当我们直接用IP地址和端口号访问服务器时,比如访问
http://xxxxxx:80/index.html
它的步骤是
- 单纯使用 IP 地址,互联网中的电脑也能够正常工作。但是有了域名的加持,能让互联网的世界变得更加方便。
- 在开发测试期间, 127.0.0.1 对应的域名是 localhost,它们都代表我们自己的这台电脑,在使用效果上没有任何区别。
如何设置本地DNS:
在windows
”→“System32
”→“drivers
”→“etc
" 目录下有一个hosts
文件 用记事本打开。hosts文件相当于一个本地的DNS,当你访问某个域名时,是先通过hosts进行解析的,没有找到才进一步通过外网DNS服务器进行解析,所以我们只需要在该文件中设置好,就不怕外网DNS解析不了了。
我们可以在这个文件上加上一些域名映射思考:
- 是不是直接IP地址和端口号速度更快?是否是一个优化速度方向?
可以说,互联网上绝大部分的应用服务都是基于TCP的服务,TCP服务在网络应用中十分常见。
TCP又叫做传输控制协议
,在网络模型中属于传输层协议。很多应用层协议都是基于TCP构建,比较典型就是我们HTTP协议。
TCP是面向连接的协议,在传输之前需要3次握手形成会话。
只有会话形成之后,服务器端和客户端之间才能互相发送数据。在创建会话的过程中,服务器端和客户端分别提供一个套接字socket
,这两个套接字共同形成一个连接。服务器端与客户端则通过套接字实现两者之间连接的操作。
所以我们接下来需要分别学习 服务器端
和客户端
内容。
① 导入 net
模块
② 创建TCP服务器实例
,监听socket事件
③ 配置服务器事件,启动服务器,监听客户端的请求
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数量的多少。
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的状态:
关于Socket的api可以参考:
我们下面重点讲解socket事件,从事件来理解socket状态。
成功建立 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);
})
当收到另一侧传来的数据时触发,事件传递的数据就是发送的数据。这个就是最重要事件,处理数据。主要关联 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());
})
})
当有错误发生时,就会触发,参数为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)
})
})
当数据传输准备结束时,连接中的任意一端发送了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数据')
})
})
当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)
}
})
})
当 socket 空闲超时时触发,仅是表明 socket 已经空闲。用户必须手动关闭连接。这里会受到 socket.setTimeout
方法影响.
// 以下是配置
// 闲置超时时间 2s
socket.setTimeout(2000)
// 5.4 timeout事件:当 socket 空闲超时时触发,仅是表明 socket 已经空闲。用户必须手动关闭连接。
socket.on("timeout",function(){
console.log("服务端:socket已经超时,需要手动关闭连接")
socket.end()
})
服务器实例启动监听客户端请求,同时监控一些服务器事件:
// 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文件
写入以下代码:
// 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事件。
① 导入 net 模块
② 创建TCP客户端实例,监听socket事件
③发起客户端的请求
net
模块创建一个 TCP 客户端,需要导入 net 模块:
// 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('客户端发送完毕');
通过write方法发送数据过去,通过end方法告诉服务器我已经发送完毕。
创建一个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连接以及接收数据的过程。
要想更好理解socket事件,可以了解一些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
状态,此后双方就可以开始通信了。
数据传输完毕后,双方都可释放连接。最开始的时候,客户端和服务器都是处于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中的四个计时器 - 重传计时器、坚持计时器、保活计时器、时间等待计时器
上面介绍完TCP模块,但是实际应用中我们不会直接操作TCP,而更多的是使用基于TCP之上的应用层协议 —— HTTP。
http 模块是 Node.js 官方提供的、用来创建 web 服务器的模块。通过 http 模块提供的 http.createServer() 方法,就能方便的把一台普通的电脑,变成一台 Web 服务器
(NodeJs可以很轻易把我们的一台电脑变成一个局域网服务器,当然可以借助内网穿透工具来实现外网访问),从而对外提供 Web 资源服务。
服务器和普通电脑的区别在于,服务器上安装了
web 服务器管理软件
,例如:IIS
、Apache
等。通过安装这些服务器管理软件,就能把一台普通的电脑变成一台 web 服务器。
在Node.js 中,我们不需要使用 IIS、Apache、Tomcat 等这些第三方 web 服务器软件。因为我们可以基于Node.js 提供的http 模块
,通过几行简单的代码,就能轻松的手写一个服务器软件,从而对外提供 web 服务。
一般构建最基本的web服务器包括几个步骤:
① 导入 http
模块
② 创建web 服务器实例
③ 为服务器实例绑定事件
,监听客户端的请求
④ 启动服务器
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模块将连接套接字抽象为ServerRequest
和ServerResponse
对象,分别对应请求和响应。在请求产生的过程中,http模块拿到连接中传来的数据,调用二进制模块http_parser进行解析,在解析完请求报文的报头后,触发request事件,调用用户的业务逻辑
。
经过http_parser解析请求报文的报头后,request对象就存在诸多有用信息。
注意,这里并没有解析 请求体的content内容,需要另外处理
。
web 服务器实例
调用 http.createServer()
方法,即可快速创建一个 web 服务器实例:
// 2.快速创建一个 web 服务器实例
const server = http.createServer()
为服务器实例绑定事件,即可监听客户端发送过来的网络请求:
// 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,响应相关).,下面再重点讲解。
调用服务器实例的 listen()
方法,即可启动当前的 web 服务器实例:
// 4. 启动服务器
server.listen(80, function () {
console.log('server running at http://127.0.0.1:80')
})
创建一个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配置。
那这里提出几个疑问点
:
先留着这些疑问,来进行下一步的学习。
http模块提供了http.request()
和http.get()
两个方法,功能是作为客户端向http服务器发起请求。
发起请求后,我们需要关注客户端事件。一般关注data和end事件即可。
创建一个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 看看。
从打印信息来看是可以完整看到整个发起以及接收数据的过程。
请求头由一系列的键值对
(key-value)组成,允许客户端向服务器端发送一些附加信息或者客户端自身的信息,主要包括:
Accept: application/json
浏览器可以接受服务器回发的类型为 application/json。
Accept: / 代表浏览器可以处理所有类型,(一般浏览器发给服务器都是发这个)。
Accept-Encoding: gzip,
deflate 浏览器申明自己接收的编码方法,通常指定压缩方法,是否支持压缩,支持什么压缩方法(gzip,deflate),(注意:这不是只字符编码)。
Accept-Language:zh-CN,zh;q=0.9 浏览器申明自己接收的语言。
授权信息,通常出现在对服务器发送的WWW-Authenticate头的应答中。
Connection: keep-alive
当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的连接。
Connection: close
代表一个Request完成后,客户端和服务器之间用于传输HTTP数据的TCP连接会关闭, 当客户端再次发送Request,需要重新建立TCP连接。
Host:www.baidu.com
请求报头域主要用于指定被请求资源的Internet主机和端口号,它通常从HTTP URL中提取出来的。
Referer:https://www.baidu.com/?start=1
当浏览器向web服务器发送请求的时候,一般会带上Referer,告诉服务器我是从哪个页面链接过来的,服务器籍此可以获得一些信息用于处理。
User-Agent:Mozilla/...
,告诉HTTP服务器, 客户端使用的操作系统和浏览器的名称和版本。
Cache-Control:private
默认为private 响应只能够作为私有的缓存,不能再用户间共享
Cache-Control:public
响应会被缓存,并且在多用户间共享。正常情况, 如果要求HTTP认证,响应会自动设置为 private.
Cache-Control:must-revalidate
响应在特定条件下会被重用,以满足接下来的请求,但是它必须到服务器端去验证它是不是仍然是最新的。
Cache-Control:no-cach
e 响应不会被缓存,而是实时向服务器端请求资源.
Cache-Control:max-age=10
设置缓存最大的有效时间,但是这个参数定义的是时间大小(比如:60)而不是确定的时间点。单位是[秒 seconds]。
Cache-Control:no-store
在任何条件下,响应都不会被缓存,并且不会被写入到客户端的磁盘里,这也是基于安全考虑的某些敏感的响应才会使用这个。
Cookie是用来存储一些用户信息以便让服务器辨别用户身份的(大多数需要登录的网站上面会比较常见),比如cookie会存储一些用户的用户名和密码,当用户登录后就会在客户端产生一个cookie来存储相关信息,这样浏览器通过读取cookie的信息去服务器上验证并通过后会判定你是合法用户,从而允许查看相应网页。当然cookie里面的数据不仅仅是上述范围,还有很多信息可以存储是cookie里面,比如
sessionid
等。
Range:bytes=0-5 指定第一个字节的位置和最后一个字节的位置。用于告诉服务器自己想取对象的哪部分。
表示请求消息正文的长度
// 3.为服务器实例绑定request 事件, 监听客户端的请求
// request req
// response res
sever.on('request' , function(req , res){
console.log('服务端:我收到了客户端请求');
console.log(`请求头: `)
console.log(req.headers)
res.end('你好,我是HTTP服务器')
})
设置和页面关联的Cookie。
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:text/html;charset=UTF-8
告诉客户端,资源文件的类型,还有字符编码,客户端通过utf-8对资源进行解码,然后对资源进行html解析。通常我们会看到有些网站是乱码的,往往就是服务器端没有返回正确的编码。
Content-Encoding:gzip
告诉客户端,服务端发送的资源是采用gzip编码的,客户端看到这个信息后,应该采用gzip对资源进行解码。
Date: Tue, 03 Apr 2020 03:52:28 GMT 这个是服务端发送资源时的服务器时间,GMT是格林尼治所在地的标准时间。http协议中发送的时间都是GMT的,这主要是解决在互联网上,不同时区在相互请求资源的时候,时间混乱问题。
Server:Tengine/1.4.
6 这个是服务器和相对应的版本,只是告诉客户端服务器信息。
Transfer-Encoding
:chunked 这个响应头告诉客户端,服务器发送的资源的方式是分块发送的。一般分块发送的资源都是服务器动态生成的,在发送时还不知道发送资源的大小,所以采用分块发送,每一块都是独立的,独立的块都能标示自己的长度,最后一块是0长度的,当客户端读到这个0长度的块时,就可以确定资源已经传输完了。
Expires:Sun, 1 Jan 1994 01:00:00 GMT
这个响应头也是跟缓存有关的,告诉客户端在这个时间前,可以直接访问缓存副本,很显然这个值会存在问题,因为客户端和服务器的时间不一定会都是相同的,如果时间不同就会导致问题。所以这个响应头是没有Cache-Control:max-age=*这个响应头准确的,因为max-age=date中的date是个相对时间,不仅更好理解,也更准确。
Last-Modified: Dec, 26 Dec 2019 17:30:00 GMT
所请求的对象的最后修改日期(按照 RFC 7231 中定义的“超文本传输协议日期”格式来表示)
Connection:keep-alive
这个字段作为回应客户端的Connection:keep-alive,告诉客户端服务器的tcp连接也是一个长连接,客户端可以继续使用这个tcp连接发送http请求。
ETag: "637060cd8c284d8af7ad3082f209582d"
就是一个对象(比如URL)的标志值,就一个对象而言,比如一个html文件,如果被修改了,其Etag也会别修改,所以,ETag的作用跟Last-Modified的作用差不多,主要供WEB服务器判断一个对象是否改变了。比如前一次请求某个html文件时,获得了其 ETag,当这次又请求这个文件时,浏览器就会把先前获得ETag值发送给WEB服务器,然后WEB服务器会把这个ETag跟该文件的当前ETag进行对比,然后就知道这个文件有没有改变了。
Refresh: 5; url=http://baidu.com
用于重定向
,或者当一个新的资源被创建时。默认会在5秒后刷新重定向。
这个头配合302状态码使用,用于重定向接收者到一个新URI地址。表示客户应当到哪里去提取文档。Location通常不是直接设置的,而是通过HttpServletResponse的sendRedirect方法,该方法同时设置状态代码为302。
Access-Control-Allow-Origin: * 号代表所有网站可以
跨域资源共享
,如果当前字段为那么Access-Control-Allow-Credentials就不能为true
Access-Control-Allow-Origin: www.baidu.com 指定哪些网站可以跨域资源共享
Access-Control-Allow-Methods:GET,POST,PUT,DELETE
允许哪些方法来访问
Access-Control-Allow-Credentials: true 是否允许发送cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。如果access-control-allow-origin为*,当前字段就不能为true
Content-Range: bytes 0-5/7877 指定整个实体中的一部分的插入位置,它也指示了整个实体的长度。在服务器向客户返回一个部分响应,它必须描述响应覆盖的范围和整个实体长度。
对于一个Web应用而言,仅仅是上面的响应处理是远远达不到业务的需求。在具体业务中,我们可能有如下需求:
这些所有需求的开始都是以这里展开:
// 2. 创建 web 服务器实例
const sever = http.createServer()
// 3.为服务器实例绑定request 事件, 监听客户端的请求
// request req
// response res
sever.on('request' , function(req , res){
console.log('服务端:我收到了客户端请求');
// 下面需要进行具体业务处理
})
除开请求体的内容,HTTP_Parser会把请求报文头抽取出来,然后解析出 req对象的一系列内容。
Node的http模块只对HTTP报文的头部进行解析,然后触发request事件。如果请求中还带有内容部分(如POST请求,它有报文和内容,内容会包括表单提交、文件提交、JSON上传、XML上传等等),内容部分
需要用户自行接收和解析
。
在web应用中,最常见的请求方法就是GET
和POST
。此外还有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
}
})
上述代码根据请求方法将业务逻辑进行分发,后面我们一般思路也需要这样考虑。我们执行以下代码看看效果:
除了根据请求方法来进行分发外,我们还需要根据路径来进行处理。
通过请求路径,我们可以区分场景:
/login
表示登录请求
/user
表示用户信息
/send
表示发送信息
…
而HTTP报文上对应格式:
GET
/login?uid=123
HTTP/1.1
HTTP_Parser将红色部分其解析为req.url
。一般完整的URL地址如下:
http://user:[email protected]: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
可以看到请求路径包含了path
和query
两个部分。那么,我们要怎么继续拆成两个部分。需要用到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服务器')
}
查询字符串位于路径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
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
注意:
上述内容基本上集中在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/
下载一个桌面版安装即可,非常简单。
最为常见的数据提交就是通过网页表单提交数据到服务器端。
<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会打印如下信息:
这里表示收到了我们的表单请求。
常见的提交有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请求。
常见数据格式还有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请求.剩下就是我们一些具体业务逻辑处理即可。
篇②主要是通过简单学习网络编程TCP、HTTP、Web应用部分,这是属于Node内置提供的模块,一般用于学习加深理解即可。
一般情况下我们会基于Node提供的基础API进一步封装出框架来方便使用,比如下一篇讲解的Express框架。但是博主还是希望大家可以多点了解底层原理而不是简单调用API方法。