index.js
const server = require(‘./server‘);
const router = require(‘./router‘);
server.start(router.route);
server.js
const http = require(‘http‘); // Node.js 内置的 http 模块
const url = require(‘url‘); // Node.js 内置的 url 模块
function start(route, port = 8080) {
/**
* 请求事件发生时调用的回调函数 (Node.js 是执行单线程异步非阻塞事件驱动的)
*/
function requestListener(req, resp) {
let pathname = url.parse(req.url).pathname;
console.log(`Request for ${pathname} received.`);
route(pathname, resp, req);
}
/**
* 创建一个基础的 HTTP 服务器
*
* 请求 http 模块提供的函数 createServer 创建一个服务器对象, 并调用该对象的 listen 方法监听 8080 端口
*
* See Also:
* createServer 函数的源码解析: https://blog.csdn.net/THEANARKH/article/details/88385964
* Node.js 是事件驱动: https://segmentfault.com/a/1190000014926921
*/
http.createServer(requestListener).listen(port);
console.log(`Server has started on ${port}`);
}
module.exports = {
start
}
router.js
const requestHandlers = require(‘./requestHandlers‘);
// 路由注册
const handler = {
‘/‘: requestHandlers.index,
‘/index‘: requestHandlers.index,
‘/start‘: requestHandlers.start,
‘/upload‘: requestHandlers.upload,
‘/show‘: requestHandlers.show,
};
// 路由处理
function route(pathname, resp, req) {
console.log(`About to route a request for ${pathname}`);
let handle = handler[pathname];
if (typeof handle === ‘function‘) {
handle(resp, req);
console.log(`${handle.name} was called`);
} else {
console.log(`No request handle found for ${pathname}`);
requestHandlers.NotFound(resp);
}
}
module.exports = {
route
}
requestHandlers.js
const fs = require(‘fs‘);
const querystring = require(‘querystring‘);
// third-party modules
const formidable = require(‘formidable‘);
function index(resp) {
fs.readFile(‘./index.html‘, function (err, data) {
if (err) {
ServerError(err, resp);
} else {
resp.writeHead(200, { ‘Content-Type‘: ‘text/html‘ });
resp.write(data);
resp.end();
}
});
}
function start(resp) {
const exec = require(‘child_process‘).exec;
let result = ‘empty‘;
setTimeout(function () {
exec(`echo ‘hello‘`, function (err, stdout, stderr) {
result = stdout;
console.log(result);
resp.writeHead(200,);
resp.write(`You‘ve sent: ${result}`);
resp.end(); // 等待事件循环调用回调函数拿到结果后才完成响应
});
}, 10000);
}
function upload(resp, req) {
let form = new formidable.IncomingForm();
form.parse(req, function (err, fields, files) {
if (err) {
ServerError(err, resp);
} else {
let imgName = `${fields.imgName || new Date().getTime().toString()}.jpg`;
try {
fs.renameSync(files.upload.path, imgName);
} catch {
imgName = `${new Date().getTime().toString()}.jpg`;
fs.renameSync(files.upload.path, imgName);
}
resp.writeHead(200, { ‘Content-Type‘: ‘text/html‘ });
resp.write(`<img src="/show?imgName=${imgName}">`);
resp.end();
}
})
}
function show(resp, req) {
let imgName = querystring.parse(req.url.split(‘?‘)[1]).imgName;
fs.readFile(imgName, ‘binary‘, function (err, file) {
if (err) {
ServerError(err, resp);
} else {
resp.writeHead(200, { ‘Content-Type‘: ‘image/jpg‘ });
resp.write(file, ‘binary‘);
resp.end();
}
})
}
function NotFound(resp, req) {
resp.writeHead(200, { ‘Content-Type‘: ‘text/html‘ });
resp.write(`<h1>NOT FOUND</h1>`);
resp.end();
}
function ServerError(err, resp, req) {
resp.writeHead(500, { ‘Content-Type‘: ‘text/plain‘ });
resp.write(err + ‘\n‘);
resp.end();
}
module.exports = {
index,
start,
upload,
show,
NotFound
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Demo</title>
</head>
<body>
<form action="/upload" enctype="multipart/form-data" method="post">
<input type="text" name="imgName">
<input type="file" name="upload" multiple>
<input type="submit" value="Upload file">
</form>
</body>
</html>
特点
- 请求处理程序进行阻塞操作时, 会阻塞其他请求的处理
(原因: 主(执行)线程被阻塞代码阻塞后, 其余所有请求必须等待该阻塞代码处理完毕之后才能执行) - 请求处理程序进行非阻塞操作时, 可以正确返回响应内容
(原因: 请求处理程序是以阻塞方式运行的, 非阻塞代码的回调函数执行时通过 resp 对象返回正确的响应内容)
总结
该服务器分为三层:
- server - 用于启动服务器, 接收请求与返回响应
- router - 用于路由注册和路由处理
- requestHandlers - 用于编写业务逻辑, 实现各个请求处理函数
各层之间的通信顺序为: req -> server -> router -> requestHandlers -> router -> server -> resp, 采用将服务器响应对象传递给请求处理程序, 在请求处理程序中返回响应的方式, 使用了一个三方模块 formidable
更方便的处理表单, 同时新增了一个上传 jpg 图片的功能