Web服务器到底在做什么
浏览器地址栏敲下回车,到页面呈现在屏幕上,中间有一个角色始终在默默工作——Web服务器。它本质上是一段软件程序,监听某个端口,接收HTTP客户端(浏览器、移动端App、爬虫脚本等)发来的HTTP请求,解析请求行、请求头、请求体,然后决定返回什么内容。
返回的内容通常不只是一份HTML文档。现在页面往往同时拉取样式表、脚本文件、图片、字体、接口数据,这些请求都会落到Web服务器上。服务器需要根据请求的URL路径,找到对应的资源,附带正确的Content-Type响应头,再写回响应体。
有一类请求服务器自己处理不了:需要查数据库、做复杂计算、调用第三方接口。这时候Web服务器会把请求转交给应用服务器(ApplicationServer),由后者完成业务逻辑,再把结果回传给Web服务器,最终由Web服务器封装成HTTP响应发给客户端。Apache是这类传统Web服务器的代表,开源、稳定、模块丰富,至今仍大量部署在production环境中。
Web应用的四层结构
把一套Web应用拆开看,可以分成四个层次,每层职责边界清晰:
客户端层:浏览器、移动端WebView、原生App、Postman这类HTTP客户端。它们发起请求,等待响应,负责渲染或消费数据。
服务器层:Web服务器所在的位置。它拦截客户端请求,做反向代理、负载均衡、静态资源缓存、TLS终止,然后把动态请求转发给下一层。
业务层:应用服务器。真正的业务逻辑在这里执行——订单校验、权限判断、数据聚合。它通过数据库驱动或HTTP客户端与数据层通信。
数据层:数据库、缓存、对象存储、消息队列,以及任何提供数据的外部系统。
这个分层不是理论摆设。实际排障时,先定位问题出在哪一层,能省掉大量无效排查。比如页面404,可能是服务器层路径映射错了;接口500,多半在业务层;数据不一致,往往要往数据层查。
Node.js的http模块能做什么
Node.js内置的http模块同时提供HTTP客户端和服务器能力。创建服务器用http.createServer(),传入一个回调函数,回调接收request和response两个对象。
request是http.IncomingMessage的实例,包含请求方法、URL、请求头、客户端IP等信息。response是http.ServerResponse的实例,用来设置状态码、响应头,并写入响应体。
配合fs模块读取文件、url模块解析请求路径,就能实现一个最基础的文件服务器。下面这份代码是很多Node.js教程的起点,但其中藏着不少值得推敲的细节。
一个可运行的静态文件服务器
新建server.js:
var http = require('http');
var fs = require('fs');
var url = require('url');
http.createServer(function (request, response) {
var pathname = url.parse(request.url).pathname;
console.log("Request for " + pathname + " received.");
fs.readFile(pathname.substr(1), function (err, data) {
if (err) {
console.log(err);
response.writeHead(404, {'Content-Type': 'text/html'});
} else {
response.writeHead(200, {'Content-Type': 'text/html'});
response.write(data.toString());
}
response.end();
});
}).listen(8081);
console.log('Server running at http://127.0.0.1:8081/');
同目录下新建index.html:
<html>
<head>
<title>Sample Page</title>
</head>
<body>
Hello World!
</body>
</html>
命令行执行nodeserver.js,浏览器打开http://127.0.0.1:8081/index.html,就能看到页面内容。
这段代码能跑通,但距离一个能用的静态服务器还有距离。下面逐条说问题。
几个容易被忽略的坑
路径解析的隐患。url.parse()在新版Node.js中已被标记为遗留API,推荐用newURL(request.url,'http://'+request.headers.host)替代。更关键的是,pathname.substr(1)直接拼接文件路径,存在目录穿越风险。请求/../../etc/passwd这类路径时,服务器可能读到项目目录之外的文件。正确做法是用path.join()拼接根目录,再用path.resolve()校验最终路径是否仍在允许范围内。
Content-Type写死了。所有文件都返回text/html,浏览器拿到CSS或JS文件时无确解析。实际项目中需要根据文件扩展名映射MIME类型,至少覆盖.html、.css、.js、.json、.png、.jpg、.svg、.woff2这些常见格式。
同步阻塞与异步错误。fs.readFile是异步的,这点没问题。但data.toString()对二进制文件(如图片)会破坏原始字节。应该直接response.write(data),或者用流式写法fs.createReadStream(filePath).pipe(response),后者内存占用更低,大文件场景优势明显。
没有处理请求方法。上面的代码对POST、PUT、DELETE一视同仁,都去读文件。生产环境需要判断request.method,非GET/HEAD请求应当返回405。
缺少缓存控制。静态资源没有Cache-Control或ETag,浏览器每次都要重新下载。加上Cache-Control:public,max-age=3600能显著减少重复请求。
现在工程中的惯用写法
直接用http模块裸写服务器,在教学场景下有助于理解底层机制。但真实项目里,通常会选择Express、Koa、Fastify这类框架,或者用Nginx托管静态资源,Node.js只负责API。
以Express为例,静态文件服务一行搞定:
app.use(express.static('public'));
框架内部做了MIME映射、路径安全校验、缓存头设置、Range请求支持。自己实现这些不是不可以,但要考虑投入产出比。
如果确实需要用原生模块,建议用fs.promises配合async/await,代码可读性更好,也便于统一错误处理:
const fs = require('fs').promises;
const path = require('path');
async function serveFile(filePath, response) {
try {
const data = await fs.readFile(filePath);
response.writeHead(200, { 'Content-Type': 'text/html' });
response.end(data);
} catch (err) {
response.writeHead(404, { 'Content-Type': 'text/plain' });
response.end('Not Found');
}
}
错误处理与类型安全
Node.js的回调风格容易导致错误被吞掉。fs.readFile的err参数必须检查,不能想当然认为文件一定存在。未捕获的异常会触发uncaughtException,进程直接退出。合理的做法是在服务器层面加一层兜底:
process.on('uncaughtException', (err) => {
console.error('Uncaught Exception:', err);
});
但更推荐用try/catch或Promise的.catch()在具置处理,不要依赖全局兜底。全局兜底只适合记录日志后优雅退出,不适合让进程继续带病运行。
TypeScript项目中,http.IncomingMessage和http.ServerResponse的类型定义由@types/node提供。请求对象的url属性类型是string|undefined,直接使用前需要判空。这类类型约束能在编译期拦掉一批低级错误。
本节课程知识要点
-
Web服务器负责接收HTTP请求并返回响应,动态请求可转交应用服务器处理。
-
Web应用可分为客户端层、服务器层、业务层、数据层,排障时先定位层次。
-
Node.js的
http模块可创建HTTP服务器,配合fs、url模块实现文件服务。 -
路径拼接必须做安全校验,防止目录穿越。
-
Content-Type应根据文件类型动态设置,不能写死。
-
二进制文件不要用
toString()转换,优先使用流式传输。 -
生产环境建议用框架或Nginx处理静态资源,原生模块适合学习与轻量场景。
练习与思考
-
把示例中的
url.parse()替换为newURL(),并处理解析失败的情况。 -
为服务器添加MIME类型映射表,至少支持HTML、CSS、JS、JSON、PNG五种格式。
-
用
fs.createReadStream()改写文件读取逻辑,观察大文件传输时的内存占用变化。 -
思考:如果请求路径是
/,应该返回什么?如何优雅地处理默认首页? -
在Express中实现同样的静态文件服务,对比两种写法的代码量与可维护性。