Web服务器开发基础

写在前面


大家好,我是一溪风月,一名前端程序员,这篇文章我们将讲解Web服务器端具体的内容,这些内容我们将对Node中原生api对服务器开发的方式,虽然在实际的开发中我们可能会使用框架来开发,但是实际上在框架的底层也是这样进行操作的,所以学习这些内容有利于我们理解框架的运行方式,这篇文章的内容会按照如下的思维导图来划分,好了废话不多说了,让我们开始吧!

一.认识Stream


事实上Node中的很多对象都是基于流实现的,比如http模块的requestresponse 那么什么是流哪? 我们对流的第一反应就是流水,源源不断的流动,事实上程序中的流也是类似的含义,我们可以想象当从一个文件中读取数据的时候,二进制的数据被源源不断的读取到了我们的程序中,而这一连串的字节就是我们程序中的流,说到这里是不是大脑中会有如下的一个场景?

所以我们可以这样理解流,是连续字节的一种表现形式和抽象概念,流是可读的也是可写的

在我们之前文件读写的时候,我们可以直接通过readFilewriteFile 方式读取文件,为什还需要流哪?

因为直接读取文件的方式,虽然简单,但是无法对一些细节进行控制,比如从某个位置开始读,读到什么位置,一次性读取多少个字节,读到某个位置后,暂停读取,某个时刻恢复读取等等,或者这个文件比较大,比如一个视频文件,并不适合一次性进行读取。

二.文件读取的Stream


官方文档中指明,所有的流都是EventEmitter的实例。

那么在Node中都有哪些流哪? Node.js 中有四种基本流类型:

流类型 描述 示例
Writable 可写入数据的流 fs.createWriteStream()
Readable 可读取数据的流 fs.createReadStream()
Duplex 同时具备可读和可写功能 net.Socket
Transform 在读写数据时可修改或转换数据的Duplex流 zlib.createDeflate()

在日常中使用最频繁的是ReadableWriteable 其他两个有需要再自行学习。

三.Readable


在之前的文章中我们读取一个文件信息,我们会通过这个方式来读取。

js 复制代码
const fs = require("fs")
fs.readFile("./foo.txt", (err, data) => {
  if (err) {
    console.log(err)
  } else {
    console.log(data)
  }
})

这种方式是一次性将所有的内容都读取到程序内,但是这种读取方式存在很多问题,我们已经提到了,文件过大,读取的位置,结束的位置,一次读取的大小,这个问题我们需要使用createReadStream来进行处理,他们有如下几个参数:

  • start:文件读取的开始位置。
  • end:文件读取的结束位置。
  • highWaterMark:一次性读取字节的长度,默认是64Kb。

然后我们来进行Readable的使用和创建

js 复制代码
const fs = require("fs")
const read = fs.createReadStream("./foo.txt", {
  start: 3,
  end: 8,
  highWaterMark: 4
})
// 

获取到上述的内容可以使用以下的方式

js 复制代码
read.on("data", (data) => {
  console.log(data)
})

也可以做一些其他的操作:监听其他事件,暂停或者恢复

js 复制代码
// 监听到文件打开
read.on("open", (fd) => {
  console.log(fd)
})
​
// 文件读取结束
read.on("end", (fd) => {
  console.log("文件读取结束")
})
​
// 文件被关闭
read.on("close", (fd) => {
  console.log("文件被关闭")
})
​
// 读取暂停
read.pause()
​
setTimeout(() => {
  // 读取恢复
  read.resume()
}, 3000)

四.Writable


在之前的文件中我们编写的写入文件的方式是这样的

js 复制代码
const fs = require("fs")
const content = "这是一个内容!"
fs.writeFile("./foo.txt", content, ((err, data) => {
  if (err) {
    console.log("文件写入发生错误!")
  }
  console.log(data)
}))

这种方式其实跟读取是一样的,也是一次性将内容写入到了文件中,对文件写入如果使用流的话就可以使用createWriteStream它有以下几个常见的参数。

  • flags:默认是w,如果我们希望追加写入,可以使用a或者a+
  • start:写入的位置。

然后我们来使用Writeable来进行一次简单的写入

js 复制代码
const fs = require("fs")
const writStream = fs.createWriteStream("./foo.txt", {
  flags: 'a+'
})
​
writStream.write("Mongo")

五.close的监听


我们在读取和写入的时候是监听不到close事件的,这是因为写入流在打开后是不会自动关闭的,我们必须手动关闭,来告诉Node已经写入结束了,并且发出一个finish事件的。

除了close 方法还是end方法,end方法相当于做了两个操作:write传入的数据和调用close方法。

js 复制代码
// close
writer.on("close",()=>{
    console.log("文件关闭")
})
// end
writer.end("HelloWorld")

六.pipe方法


正常情况下,我们可以将读取到的输入流,手动的放到输出流进行写入。

js 复制代码
// 不用pipe的情况
const fs = require("fs")
const reader = fs.createReadStream("./foo.txt")
const writer = fs.createWriteStream("./bar.txt")
​
reader.on("data", (data) => {
  console.log(data)
  writer.write(data, (err) => {
    console.log(err)
  })
})
js 复制代码
// 使用pipe的情况
const fs = require("fs")
const reader = fs.createReadStream("./foo.txt")
const writer = fs.createWriteStream("./bar.txt")
​
reader.pipe(writer)

七.Web服务器


日常进行前端开发的你是否听说过Web服务器的概念,那么什么是Web服务器哪?当应用程序(客户端)需要某一个资源时,可以向一台服务器,通过http请求获取到这个资源,提供这个资源的服务器就是Web服务器

目前有很多开源的Web服务器:Nginx,Apache(静态),Apache Tomcat(静态,动态),Node.js等。

八.http模块


在Node中,提供web服务器的资源返回给浏览器,主要通过http模块,我们来进行简单的使用

js 复制代码
const http = require("http")
// 创建一个http服务器
const PORT = 3000
const server = http.createServer()
// 开启对应的服务器,并告知端口
// 监听端口监听1024以上~65535以下的端口,1024以下是特殊服务的端口
server.listen(PORT, () => {
  console.log(`服务器开启在${PORT}端口~`)
})

为什么需要指定不同的端口哪?因为可能不同的端口会指定不同的服务如下图。

我们在创建http服务的时候使用的是createServer来完成的http.createServer会返回服务器的对象,底层其实是直接进行了new Server对象。

js 复制代码
function createServer(opts,requestListener){
    return new Server(opts,requestListener)
}

当然,我们也可以自己来创建这个对象。

js 复制代码
const server2 = new http.Server((req,res)=>{
    res.end("Hello Server2");
});
​
server2.listen(9000,()=>{
    console.log("服务器启动成功~")
})

💡Tips:127.0.0.1:回环地址,表达的意思其实是我们主机自己发出去的包,直接被自己接收;

我们会发现每次我们修改代码都需要重新进行代码的执行来启动服务,所以我们可以使用社区的工具来做这个事情

js 复制代码
npm install nodemon

然后我们就可以直接使用nodemon 来进行启动,可以进行服务的自动热重启。

九.request对象


在向服务器发送请求的时候,我们会携带很多数据信息,比如:

  • 本次请求的URL,服务器需要根据不同的URL进行不同的处理。
  • 本次请求的请求方式:比如GET,POST,请求传入的参数和处理的方式是不同的
  • 本次请求的headers中也会携带一些信息,比如客户端信息,接受数据的格式,支持的编码格式等。

这些信息,Node会帮助我们封装📦到一个request 的对象中,我们可以直接处理这个request对象。

js 复制代码
const http = require("http")
// 创建一个http服务器
const PORT = 3000
const server = http.createServer((req,res)=>{
    console.log(req.url);
    console.log(req.method);
    console.log(req.headers);
})
// 开启对应的服务器,并告知端口
// 监听端口监听1024以上~65535以下的端口,1024以下是特殊服务的端口
server.listen(PORT, () => {
  console.log(`服务器开启在${PORT}端口~`)
})

十.URL的处理


客户端在发送请求的时候,会请求不同的数据,那么会传入不同的请求地址,服务器需要根据不同的请求地址,做出不同的响应。

js 复制代码
const http = require("http")
// 创建一个http服务器
const PORT = 8000
const server = http.createServer((req, res) => {
  let url = req.url;
  let method = req.method;
  if (url === '/home') {
    res.end("你好啊~")
  }
​
  if (url === "/login") {
    res.end("登录成功!")
  }
})
// 开启对应的服务器,并告知端口
// 监听端口监听1024以上~65535以下的端口,1024以下是特殊服务的端口
server.listen(PORT, () => {
  console.log(`服务器开启在${PORT}端口~`)
})

十一.URL的解析


如果客户的地址中还携带了一些额外的参数哪?

js 复制代码
http://localhost:8000/login?name=why&password=123

这个时候url 的值是/login?name=why&password=123那么我们该如何对它进行解析哪?使用内置的url

js 复制代码
let urlPath = req.url
let urlInfo = url.parse(urlPath)
console.log(urlInfo.query, urlInfo.pathname)

但是query如何可以获取哪进行解析哪?我们可以使用queryString模块来处理解析。

js 复制代码
const http = require("http")
const url = require("url")
const qs = require("querystring")
// 创建一个http服务器
const PORT = 8000
const server = http.createServer((req, res) => {
  let urlPath = req.url
  let urlInfo = url.parse(urlPath)
  let queryObj = qs.parse(urlInfo.query)
  console.log(queryObj)
})
// 开启对应的服务器,并告知端口
// 监听端口监听1024以上~65535以下的端口,1024以下是特殊服务的端口
server.listen(PORT, () => {
  console.log(`服务器开启在${PORT}端口~`)
})
​
// 发送请求就可以获取到对象 [Object: null prototype] { name: 'why', age: '12' }

十二.method的解析

在restful接口规范中我们对数据的增删改查可以通过不同的请求方式:

GET:查询数据

POST:新建数据

PATCH:更新数据

DELETE:删除数据

我们可以通过不同的方式来进行不同的增删改查的操作,在代码中我们可以通过这种方式来获取method

js 复制代码
 let method = req.method;

十三.body的解析


了解和使用了query参数之后我们还需要获取下body参数进行解析,在进行body解析的时候我们还需要了解一个知识,如果我们直接通过req来获取body其实是根本获取不到的,因为根本没有这个参数,想要获取body需要了解到我们前面讲解的可读流的知识,因为request本质上是一个readable可读流,我们可以通过如下的这种方式来解析和处理。

js 复制代码
const http = require("http")
const server = http.createServer((req, res) => {
  req.setEncoding("utf-8")
  let isLogin = false
  req.on("data", (res) => {
    console.log(res)
    const dataString = res
    const loginInfo = JSON.parse(dataString)
    if (loginInfo.name === "zzz" && loginInfo.age === 12) {
      isLogin = true
    } else {
      isLogin = false
    }
  })
    
  req.on("end", () => {
    if (isLogin) {
      res.end("登录成功,欢迎回来~")
    }
  })
})
​
server.listen(8000, () => {
  console.log("服务器启动在8000端口")
})

十四.header的解析


request对象的header中也包含很多有用的信息,客户端会默认传递过来一些信息。

  • content-type :表示这次请求携带的数据的类型,常见的值有application/x-www-form-urlencoded(数据被编码成以&分隔的键 - 值对,同时以=分隔键和值)、application/json(表示是一个json类型)、text/plain(表示是文本类型)、application/xml(表示是xml类型)、multipart/form-data(表示是上传文件)。
  • content-length:表示文件的大小长度。
  • keep-alive :HTTP是基于TCP协议的,通常一次请求和响应结束后会立刻中断连接。在HTTP1.0中,如果想要保持连接,浏览器需要在请求头中添加connection: keep-alive,服务器需要在响应头中添加connection: keep-alive,当客户端再次发送请求时,就会使用同一个连接,直到一方中断连接;在HTTP1.1中,所有连接默认是connection: keep-alive的,不同的Web服务器保持keep-alive的时间不同,Node中默认是5秒。
  • accept-encoding :告知服务器客户端支持的文件压缩格式,如js文件可以使用gzip编码,对应.gz文件。
  • accept:告知服务器客户端可接受文件的格式类型。
  • user-agent:包含客户端相关的信息。

十五.返回响应结果


向客户端返回响应结果有两种常用方式:

  • write方法 :直接写出数据,但不会关闭流,可以多次调用write方法连续写入数据。例如:
js 复制代码
res.write("Hello World");
res.write("Hello Response");
  • end方法 :写出最后的数据,并在写出后关闭流。通常在所有数据都写入完成后调用end方法,例如:
js 复制代码
res.end("message end");

如果没有调用endclose方法,客户端将会一直等待结果,所以客户端在发送网络请求时,都会设置超时时间。

返回状态码与响应头文件

HTTP状态码(HTTP Status Code)用于表示HTTP响应状态,常见的状态码如下:

状态码 状态描述 信息说明
200 OK 客户端请求成功 表示请求正常处理并成功返回数据
201 Created POST请求,创建新的资源 用于表示通过POST请求成功创建了新资源
301 Moved Permanently 请求资源的URL已经修改,响应中会给出新的URL 用于重定向,告知客户端资源的新地址
400 Bad Request 客户端的错误,服务器无法或者不进行处理 表示客户端请求存在语法错误或其他问题
401 Unauthorized 未授权的错误,必须携带请求的身份信息 表示客户端未提供有效的身份验证信息
403 Forbidden 客户端没有权限访问,被拒接 表示客户端有权限访问,但被服务器拒绝
404 Not Found 服务器找不到请求的资源 表示请求的资源在服务器上不存在
500 Internal Server Error 服务器遇到了不知道如何处理的情况 表示服务器内部发生错误
503 Service Unavailable 服务器不可用,可能处理维护或者重载状态,暂时无法访问 表示服务器当前无法处理请求

设置响应状态码可以这样操作:

js 复制代码
res.statusCode = 400;
res.writeHead(200);

返回头部信息主要有两种方式:

  • res.setHeader:一次写入一个头部信息,例如:
js 复制代码
res.setHeader("Content-Type", "application/json; charset=utf8");
  • res.writeHead:同时写入header和status,例如:
js 复制代码
res.writeHead(200, {
    "Content-Type": "application/json; charset=utf8"
});

设置Content-Type的作用是告知客户端接收到的数据类型,这样客户端可以按照正确的方式进行处理。如果不设置,默认客户端接收到的是字符串,客户端会按照自己默认的方式处理数据,可能导致数据解析错误。

十六.axios库在Node中的使用


axios库既可以在浏览器中使用,也可以在Node中使用。在Node中,axios使用的是http内置模块。使用axios发送HTTP请求示例如下:

js 复制代码
const http = require('http');
const axios = require('axios');
// GET请求
axios.get("http://localhost:8000").then(response => {
    console.log(response.data);
}).catch(error => {
    console.error(error);
});
// POST请求
axios.post("http://localhost:8000", {
    username: "test",
    password: "123"
}).then(response => {
    console.log(response.data);
}).catch(error => {
    console.error(error);
});

上述代码中,分别展示了使用axios发送GETPOST请求的方式,通过then方法处理成功响应,通过catch方法处理错误。

十七.文件上传的实现(了解即可)


文件上传的错误示范

在处理文件上传时,如果直接使用以下方式可能会出现问题:

js 复制代码
const fs = require('fs');
const http = require('http');
const server = http.createServer((req, res) => {
    const filewriter = fs.createWriteStream('./foo.png');
    req.pipe(filewriter);
    const fileSize = req.headers['content-Length'];
    let curSize = 0;
    console.log(fileSize);
    req.on("data", (data) => {
        curSize += data.length;
        console.log(curSize);
        res.write(`文件上传进度: ${curSize / fileSize * 100}%\n`);
    });
    req.on('end', () => {
        res.end("文件上传完成~");
    });
});
server.listen(8000, () => {
    console.log("服务器启动成功~");
});

这种方式的问题在于,没有正确处理文件上传的边界情况和数据格式。对于大文件上传,可能会导致内存占用过高,并且无法准确解析文件内容。

文件上传的正确做法

正确的文件上传实现需要考虑多个方面,以下是详细步骤:

  1. 设置编码与获取边界值 :首先,需要将请求设置为二进制编码,并从content-type中获取boundary的值。
js 复制代码
req.setEncoding('binary');
var boundary = req.headers['content-type'].split(";")[1].replace('boundary=', '');
  1. 记录数据与监听数据 :记录文件大小和当前已接收数据的大小,通过监听data事件获取上传的数据。
js 复制代码
const fileSize = req.headers['content-length'];
let curSize = 0;
let body = '';
req.on("data", (data) => {
    curSize += data.length;
    res.write(`文件上传进度: ${curSize / fileSize * 100}%\n`);
    body += data
})
js 复制代码
// 数据结构
req.on('end', () => {
    // 切割数据
    const payload = qs.parse(body, "\r\n", ":");
    // 获取最后的类型(image/png)
    const fileType = payload["Content-Type"].substring(1);
    // 获取要截取的长度
    const fileTypePosition = body.indexOf(fileType) + fileType.length;
    let binaryData = body.substring(fileTypePosition);
    binaryData = binaryData.replace(/^\s\s*/, '');
    // binaryData = binaryData.replaceAll('\r\n','');
    const finalData = binaryData.substring(0, binaryData.indexOf('--'+boundary+'--'));
​
    fs.writeFile('./boo.png', finalData, 'binary', (err) => {
        console.log(err);
        res.end("文件上传完成~");
    });
});

十八.总结


本文聚焦 Node.js 的 Web 服务器开发与文件上传技术。先介绍 Stream,它是连续字节的抽象,有可读、可写特性,弥补传统文件读写不足,包含 Writable、Readable 等四种类型 。接着阐述 Web 服务器相关知识,如 http 模块创建服务器、request 和 response 对象处理,以及 URL、method、请求头处理和状态码、响应头设置。还讲解了 axios 库在 Node 中的使用。最后探讨文件上传,指出错误示范问题,给出正确做法,包括设置编码、获取边界值、监听数据等步骤。

相关推荐
花楸树17 分钟前
前端搭建 MCP Client(Web版)+ Server + Agent 实践
前端·人工智能
wuaro17 分钟前
RBAC权限控制具体实现
前端·javascript·vue
专业抄代码选手22 分钟前
【JS】instanceof 和 typeof 的使用
前端·javascript·面试
用户00798136209722 分钟前
6000 字+6 个案例:写给普通人的 MCP 入门指南
前端
用户876128290737427 分钟前
前端ai对话框架semi-design-vue
前端·人工智能
干就完了130 分钟前
项目中遇到浏览器跨域前端和后端解决方案以及大概过程
前端
我是福福大王32 分钟前
前后端SM2加密交互问题解析与解决方案
前端·后端
实习生小黄35 分钟前
echarts 实现环形渐变
前端·echarts
_未知_开摆42 分钟前
uniapp APP端在线升级(简版)
开发语言·前端·javascript·vue.js·uni-app
sen_shan1 小时前
Vue3+Vite+TypeScript+Element Plus开发-02.Element Plus安装与配置
前端·javascript·typescript·vue3·element·element plus