Node.js 网络编程与 Web 服务 Node.js 作为一款服务端 JavaScript 运行时,网络编程是其最核心的应用领域之一。本章将从 HTTP 协议基础讲起,逐步介绍 Node.js 中用于构建 Web 服务的 http 模块、静态文件服务实现、底层 net 模块的 TCP 编程,以及基于 WebSocket 的实时通信,最后通过实战练习巩固所学知识。
一、HTTP 协议基础 HTTP(HyperText Transfer Protocol,超文本传输协议)是 Web 应用中最基础的协议,它定义了客户端与服务器之间通信的格式和规则。在学习 Node.js 的 http 模块之前,我们需要先了解 HTTP 协议的核心概念。
1.1 请求与响应 HTTP 通信采用经典的 请求-响应(Request-Response) 模式:
客户端(Client) :发起请求的一方,通常是浏览器或移动端应用
服务器(Server) :接收请求并返回响应的一方
一次完整的 HTTP 交互包含两个核心部分:
请求(Request) 由三部分组成:
请求行 :包含请求方法、请求 URL 和 HTTP 协议版本
请求头(Request Header) :附加信息,如 Content-Type、User-Agent、Cookie 等
请求体(Request Body) :可选,通常用于 POST/PUT 请求传递数据
1 2 3 4 5 6 POST /api/login HTTP/1.1 Host : example.comContent-Type : application/jsonUser-Agent : Mozilla/5.0{ "username" : "admin" , "password" : "123456" }
响应(Response) 同样由三部分组成:
状态行 :包含 HTTP 协议版本、状态码和状态描述
响应头(Response Header) :附加信息,如 Content-Type、Set-Cookie、Cache-Control 等
响应体(Response Body) :实际返回的数据内容
1 2 3 4 5 HTTP/1.1 200 OKContent-Type : application/jsonSet-Cookie : session=abc123; HttpOnly{"code ":0 ,"message" :"登录成功" }
1.2 HTTP 状态码 状态码用于描述服务器对请求的处理结果,共分为五类:
状态码范围
类别
说明
常见状态码
100-199
信息响应
表示请求已收到,继续处理
100 Continue
200-299
成功响应
表示请求已成功处理
200 OK、201 Created、204 No Content
300-399
重定向
表示需要客户端进一步操作
301 Moved Permanently、302 Found、304 Not Modified
400-499
客户端错误
表示客户端请求有误
400 Bad Request、401 Unauthorized、403 Forbidden、404 Not Found、405 Method Not Allowed
500-599
服务端错误
表示服务器处理请求时出错
500 Internal Server Error、502 Bad Gateway、503 Service Unavailable、504 Gateway Timeout
常见状态码说明:
200 OK :请求成功处理
301/302 :永久/临时重定向,浏览器会自动跳转到新地址
304 Not Modified :协商缓存命中,直接使用本地缓存
400 Bad Request :请求参数错误
401 Unauthorized :未认证或认证失败(未登录)
403 Forbidden :服务器拒绝访问(权限不足)
404 Not Found :请求的资源不存在
500 Internal Server Error :服务器内部错误
502 Bad Gateway :网关从上游收到无效响应
503 Service Unavailable :服务暂时不可用
1.3 HTTP 请求方法 HTTP/1.1 定义了多种请求方法,每种方法具有不同的语义:
方法
描述
幂等性
是否有请求体
GET
获取资源
幂等
否
POST
创建资源
非幂等
是
PUT
全量更新资源
幂等
是
PATCH
部分更新资源
非幂等
是
DELETE
删除资源
幂等
可选
HEAD
获取响应头(无响应体)
幂等
否
OPTIONS
查询支持的 HTTP 方法
幂等
否
RESTful API 设计建议:
使用名词而非动词命名 URL,如 /users 而非 /getUsers
使用复数形式,如 /users 而非 /user
通过不同的 HTTP 方法区分操作:GET 查询、POST 新增、PUT 修改、DELETE 删除
通过 URL 路径表达层级关系,如 /users/:id/orders
Header 是 HTTP 请求和响应中的元数据部分,用于传递附加信息。
常用请求头:
Header
作用
示例
Host
指定请求的主机名
Host: example.com
User-Agent
客户端信息
User-Agent: Mozilla/5.0
Accept
客户端能接收的内容类型
Accept: application/json
Content-Type
请求体的数据格式
Content-Type: application/json
Cookie
携带 Cookie 信息
Cookie: session=abc123
Authorization
认证凭证
Authorization: Bearer token123
Referer
来源页面
Referer: https://example.com/home
Origin
请求来源(CORS 用)
Origin: https://example.com
常用响应头:
Header
作用
示例
Content-Type
响应体的数据格式
Content-Type: text/html; charset=utf-8
Set-Cookie
设置 Cookie
Set-Cookie: session=abc123; HttpOnly
Cache-Control
缓存策略
Cache-Control: max-age=3600
Location
重定向目标
Location: /new-url
Access-Control-Allow-Origin
CORS 允许的源
Access-Control-Allow-Origin: *
Content-Disposition
下载文件信息
Content-Disposition: attachment; filename="file.zip"
Etag
资源标识(缓存用)
ETag: "abc123"
Last-Modified
资源最后修改时间
Last-Modified: Mon, 25 Sep 2024 10:00:00 GMT
1.5 Cookie 与 Session Cookie 和 Session 是两种常见的会话跟踪机制,用于在无状态的 HTTP 协议上维护客户端与服务器之间的状态。
Cookie:
Cookie 是由服务器通过 Set-Cookie 响应头设置,存储在客户端浏览器中的一小段数据。浏览器在后续请求中会自动通过 Cookie 请求头将其发送回服务器。
1 2 3 4 5 6 7 // 服务器设置 Cookie HTTP/1.1 200 OKSet-Cookie : user_id=123; Path=/; Domain=example.com; Max-Age=3600; HttpOnly; Secure// 后续请求自动携带 CookieGET /profile HTTP/1.1 Cookie: user_id= 123
Cookie 的常用属性:
Expires / Max-Age:过期时间
Path:Cookie 生效的路径
Domain:Cookie 生效的域名
HttpOnly:禁止 JavaScript 访问(防范 XSS)
Secure:只在 HTTPS 连接中发送
SameSite:跨站请求限制(Strict、Lax、None)
Session:
Session 是另一种会话跟踪机制,由服务器端保存会话状态。通常的做法是:
客户端首次请求时,服务器创建 Session 对象并生成唯一的 Session ID
服务器通过 Set-Cookie 将 Session ID 返回给客户端
客户端后续请求携带 Cookie(包含 Session ID)
服务器根据 Session ID 查找对应的 Session 数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ┌──────────┐ 第一次请求(无 Cookie) ┌──────────┐ │ 客户端 │ ──────────────────────────→ │ 服务器 │ │ │ │ │ │ │ ←────────────────────────── │ │ │ │ Set-Cookie: JSESSIONID=abc │ 创建 Session 并存储数据 └──────────┘ └──────────┘ ┌──────────┐ 后续请求(携带 Cookie) ┌──────────┐ │ 客户端 │ ──────────────────────────→ │ 服务器 │ │ │ Cookie: JSESSIONID=abc │ 查找 Session 并获取数据 │ │ │ │ │ │ ←────────────────────────── │ │ │ │ 返回带用户信息的响应 │ │ └──────────┘ └──────────┘
Cookie vs Session 对比:
对比项
Cookie
Session
存储位置
客户端浏览器
服务器端
安全性
相对不安全,可被篡改
相对安全,数据在服务端
存储容量
单个 Cookie 最大 4KB
服务器内存/磁盘,容量大
跨域支持
可通过 Domain 设置
需额外处理(Redis 共享)
生命周期
可设置过期时间
通常由服务器控制
安全建议: 敏感信息(如用户身份认证状态)应使用 Session 存储,不要直接存储在 Cookie 中。如果必须使用 Cookie 存储 Token,应结合 HttpOnly 和 Secure 属性,并使用 HTTPS 传输。
二、http 模块 Node.js 的 http 模块是构建 Web 服务的核心,它封装了 HTTP 协议的解析和处理逻辑。
2.1 创建 HTTP 服务器 使用 http.createServer() 方法可以快速创建一个 HTTP 服务器。
1 2 3 4 5 6 7 8 9 10 11 const http = require ('http' );const server = http.createServer ((req, res ) => { res.writeHead (200 , { 'Content-Type' : 'text/plain; charset=utf-8' }); res.end ('Hello, Node.js!' ); }); const PORT = 3000 ;server.listen (PORT , () => { console .log (`服务器已启动: http://localhost:${PORT} ` ); });
运行上述代码,然后在浏览器中访问 http://localhost:3000,将看到 “Hello, Node.js!” 的输出。
注意: createServer 的回调函数有两个参数:
req(IncomingMessage):客户端请求对象,包含请求的所有信息
res(ServerResponse):服务器响应对象,用于构建和发送响应
2.2 处理请求与响应 获取请求信息 req 对象包含了客户端请求的所有信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const http = require ('http' );const server = http.createServer ((req, res ) => { console .log ('请求方法:' , req.method ); console .log ('请求 URL:' , req.url ); console .log ('HTTP 版本:' , req.httpVersion ); console .log ('请求头:' , req.headers ); console .log ('客户端 IP:' , req.socket .remoteAddress ); res.writeHead (200 , { 'Content-Type' : 'application/json; charset=utf-8' }); res.end (JSON .stringify ({ method : req.method , url : req.url })); }); server.listen (3000 );
获取请求体 对于 POST/PUT 请求,请求体数据需要通过事件监听来获取:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const http = require ('http' );const server = http.createServer ((req, res ) => { let body = '' ; req.on ('data' , (chunk ) => { body += chunk.toString (); }); req.on ('end' , () => { console .log ('请求体:' , body); res.writeHead (200 , { 'Content-Type' : 'application/json; charset=utf-8' }); res.end (JSON .stringify ({ received : body })); }); }); server.listen (3000 );
提示: 在实际项目中,通常会使用 body-parser、multer 等中间件来处理更复杂的请求体解析(如 application/json、multipart/form-data 等)。
发送响应 res 对象提供了多种方法来构建响应:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const http = require ('http' );const server = http.createServer ((req, res ) => { res.statusCode = 200 ; res.setHeader ('Content-Type' , 'text/plain; charset=utf-8' ); res.setHeader ('X-Custom-Header' , 'custom-value' ); res.write ('第一行内容\n' ); res.write ('第二行内容\n' ); res.end ('响应结束' ); }); server.listen (3000 );
2.3 路由分发 通过判断 req.url 和 req.method,可以实现简单的路由分发:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 const http = require ('http' );const server = http.createServer ((req, res ) => { res.setHeader ('Content-Type' , 'text/html; charset=utf-8' ); const url = new URL (req.url , `http://${req.headers.host} ` ); const pathname = url.pathname ; if (req.method === 'GET' && pathname === '/' ) { res.writeHead (200 ); res.end ('<h1>首页</h1>' ); } else if (req.method === 'GET' && pathname === '/about' ) { res.writeHead (200 ); res.end ('<h1>关于我们</h1>' ); } else if (req.method === 'GET' && pathname === '/api/user' ) { res.setHeader ('Content-Type' , 'application/json; charset=utf-8' ); res.writeHead (200 ); res.end (JSON .stringify ({ id : 1 , name : '张三' })); } else if (req.method === 'POST' && pathname === '/api/login' ) { let body = '' ; req.on ('data' , (chunk ) => { body += chunk; }); req.on ('end' , () => { const { username, password } = JSON .parse (body); if (username === 'admin' && password === '123456' ) { res.writeHead (200 ); res.end (JSON .stringify ({ code : 0 , message : '登录成功' })); } else { res.writeHead (401 ); res.end (JSON .stringify ({ code : 1 , message : '用户名或密码错误' })); } }); } else { res.writeHead (404 ); res.end ('<h1>404 - 页面未找到</h1>' ); } }); server.listen (3000 , () => { console .log ('服务器已启动: http://localhost:3000' ); });
2.4 获取请求参数 URL 查询参数(GET) 使用 URL 类或 url 模块来解析查询参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const http = require ('http' );const server = http.createServer ((req, res ) => { const url = new URL (req.url , `http://${req.headers.host} ` ); const name = url.searchParams .get ('name' ); const age = url.searchParams .get ('age' ); console .log ('name:' , name); console .log ('age:' , age); res.writeHead (200 , { 'Content-Type' : 'application/json; charset=utf-8' }); res.end (JSON .stringify ({ name, age })); }); server.listen (3000 );
访问 http://localhost:3000/?name=张三&age=25 即可获取参数。
路由参数(动态路由) 通过手动解析路径来实现动态路由:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const http = require ('http' );const server = http.createServer ((req, res ) => { const url = new URL (req.url , `http://${req.headers.host} ` ); const pathname = url.pathname ; const match = pathname.match (/^\/api\/users\/(\d+)$/ ); if (match) { const userId = match[1 ]; res.writeHead (200 , { 'Content-Type' : 'application/json; charset=utf-8' }); res.end (JSON .stringify ({ id : userId, name : `用户${userId} ` })); } else { res.writeHead (404 , { 'Content-Type' : 'application/json; charset=utf-8' }); res.end (JSON .stringify ({ error : 'Not Found' })); } }); server.listen (3000 );
2.5 返回 HTML 与 JSON 返回 HTML 页面 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 const http = require ('http' );const fs = require ('fs' );const path = require ('path' );const server = http.createServer ((req, res ) => { if (req.url === '/' ) { const html = ` <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Node.js 服务器</title> </head> <body> <h1>欢迎使用 Node.js</h1> <p>这是一个动态生成的页面</p> <p>当前时间:${new Date ().toLocaleString()} </p> </body> </html> ` ; res.writeHead (200 , { 'Content-Type' : 'text/html; charset=utf-8' }); res.end (html); } }); server.listen (3000 );
返回 JSON 数据 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 const http = require ('http' );const server = http.createServer ((req, res ) => { res.writeHead (200 , { 'Content-Type' : 'application/json; charset=utf-8' , 'Access-Control-Allow-Origin' : '*' , 'Access-Control-Allow-Methods' : 'GET, POST, PUT, DELETE' , 'Access-Control-Allow-Headers' : 'Content-Type, Authorization' }); const data = { code : 0 , message : 'success' , data : { users : [ { id : 1 , name : '张三' , email : 'zhangsan@example.com' }, { id : 2 , name : '李四' , email : 'lisi@example.com' }, { id : 3 , name : '王五' , email : 'wangwu@example.com' } ], total : 3 } }; res.end (JSON .stringify (data)); }); server.listen (3000 );
CORS 跨域处理 当浏览器从不同源(协议+域名+端口)请求 API 时会触发跨域限制。以下是一个完整的 CORS 处理方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const http = require ('http' );const server = http.createServer ((req, res ) => { res.setHeader ('Access-Control-Allow-Origin' , 'http://localhost:8080' ); res.setHeader ('Access-Control-Allow-Methods' , 'GET, POST, PUT, DELETE, OPTIONS' ); res.setHeader ('Access-Control-Allow-Headers' , 'Content-Type, Authorization' ); res.setHeader ('Access-Control-Allow-Credentials' , 'true' ); res.setHeader ('Access-Control-Max-Age' , '86400' ); if (req.method === 'OPTIONS' ) { res.writeHead (204 ); res.end (); return ; } res.writeHead (200 , { 'Content-Type' : 'application/json; charset=utf-8' }); res.end (JSON .stringify ({ message : '跨域请求成功' })); }); server.listen (3000 );
三、静态文件服务 静态文件服务是 Web 服务器的基本功能,用于向客户端提供 HTML、CSS、JavaScript、图片等静态资源。
3.1 读取静态文件 使用 fs 模块读取文件内容并返回给客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 const http = require ('http' );const fs = require ('fs' );const path = require ('path' );const PUBLIC_DIR = path.join (__dirname, 'public' );const server = http.createServer ((req, res ) => { let filePath = path.join (PUBLIC_DIR , req.url === '/' ? 'index.html' : req.url ); if (!filePath.startsWith (PUBLIC_DIR )) { res.writeHead (403 ); res.end ('Forbidden' ); return ; } fs.readFile (filePath, (err, data ) => { if (err) { if (err.code === 'ENOENT' ) { res.writeHead (404 ); res.end ('File Not Found' ); } else { res.writeHead (500 ); res.end ('Server Error' ); } return ; } res.writeHead (200 , { 'Content-Type' : 'text/html; charset=utf-8' }); res.end (data); }); }); server.listen (3000 );
3.2 MIME 类型 MIME(Multipurpose Internet Mail Extensions)类型告诉浏览器响应数据的类型。正确设置 MIME 类型对于文件正常显示至关重要。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const mimeTypes = { '.html' : 'text/html; charset=utf-8' , '.css' : 'text/css; charset=utf-8' , '.js' : 'application/javascript; charset=utf-8' , '.json' : 'application/json; charset=utf-8' , '.png' : 'image/png' , '.jpg' : 'image/jpeg' , '.jpeg' : 'image/jpeg' , '.gif' : 'image/gif' , '.svg' : 'image/svg+xml' , '.ico' : 'image/x-icon' , '.txt' : 'text/plain; charset=utf-8' , '.xml' : 'application/xml; charset=utf-8' , '.pdf' : 'application/pdf' , '.zip' : 'application/zip' , '.woff' : 'font/woff' , '.woff2' : 'font/woff2' , '.ttf' : 'font/ttf' , '.mp3' : 'audio/mpeg' , '.mp4' : 'video/mp4' };
改进版静态文件服务(支持 MIME 类型):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 const http = require ('http' );const fs = require ('fs' );const path = require ('path' );const PUBLIC_DIR = path.join (__dirname, 'public' );const mimeTypes = { '.html' : 'text/html; charset=utf-8' , '.css' : 'text/css; charset=utf-8' , '.js' : 'application/javascript; charset=utf-8' , '.json' : 'application/json; charset=utf-8' , '.png' : 'image/png' , '.jpg' : 'image/jpeg' , '.jpeg' : 'image/jpeg' , '.gif' : 'image/gif' , '.svg' : 'image/svg+xml' , '.ico' : 'image/x-icon' , '.txt' : 'text/plain; charset=utf-8' , '.pdf' : 'application/pdf' , '.zip' : 'application/zip' , '.woff' : 'font/woff' , '.woff2' : 'font/woff2' }; const server = http.createServer ((req, res ) => { let filePath = path.join (PUBLIC_DIR , req.url === '/' ? 'index.html' : req.url ); if (!filePath.startsWith (PUBLIC_DIR )) { res.writeHead (403 ); res.end ('Forbidden' ); return ; } const ext = path.extname (filePath).toLowerCase (); const contentType = mimeTypes[ext] || 'application/octet-stream' ; fs.readFile (filePath, (err, data ) => { if (err) { res.writeHead (404 ); res.end ('Not Found' ); return ; } res.writeHead (200 , { 'Content-Type' : contentType }); res.end (data); }); }); server.listen (3000 );
3.3 缓存控制 合理使用缓存可以显著提升性能,减少网络传输。HTTP 缓存主要分为两类:协商缓存 和 强缓存 。
强缓存(由服务端控制):
1 2 3 4 5 res.writeHead (200 , { 'Content-Type' : contentType, 'Cache-Control' : 'public, max-age=3600' });
Cache-Control 常用指令:
指令
说明
max-age=3600
缓存有效期为 3600 秒
no-cache
需要协商缓存,每次都要验证
no-store
完全不缓存
public
允许所有缓存(包括 CDN)存储
private
仅浏览器缓存,CDN 不可缓存
协商缓存(需要浏览器和服务器配合):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 const http = require ('http' );const fs = require ('fs' );const path = require ('path' );const PUBLIC_DIR = path.join (__dirname, 'public' );const server = http.createServer ((req, res ) => { let filePath = path.join (PUBLIC_DIR , req.url === '/' ? 'index.html' : req.url ); fs.stat (filePath, (err, stats ) => { if (err) { res.writeHead (404 ); res.end ('Not Found' ); return ; } const etag = `"${stats.size} -${stats.mtime.getTime()} "` ; const lastModified = stats.mtime .toUTCString (); const ifNoneMatch = req.headers ['if-none-match' ]; const ifModifiedSince = req.headers ['if-modified-since' ]; if (ifNoneMatch === etag) { res.writeHead (304 ); res.end (); return ; } if (ifModifiedSince && new Date (ifModifiedSince) >= stats.mtime ) { res.writeHead (304 ); res.end (); return ; } fs.readFile (filePath, (err, data ) => { if (err) { res.writeHead (500 ); res.end ('Server Error' ); return ; } res.writeHead (200 , { 'Content-Type' : 'application/octet-stream' , 'Cache-Control' : 'public, max-age=3600' , 'ETag' : etag, 'Last-Modified' : lastModified }); res.end (data); }); }); }); server.listen (3000 );
3.4 Range 请求 Range 请求支持客户端请求文件的部分内容,常用于视频播放、断点续传等场景。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 const http = require ('http' );const fs = require ('fs' );const path = require ('path' );const PUBLIC_DIR = path.join (__dirname, 'public' );const server = http.createServer ((req, res ) => { let filePath = path.join (PUBLIC_DIR , req.url === '/' ? 'index.html' : req.url ); fs.stat (filePath, (err, stats ) => { if (err) { res.writeHead (404 ); res.end ('Not Found' ); return ; } const fileSize = stats.size ; const range = req.headers ['range' ]; if (range) { const matches = /bytes=(\d*)-(\d*)/ .exec (range); let start = 0 ; let end = fileSize - 1 ; if (matches[1 ]) start = parseInt (matches[1 ]); if (matches[2 ]) end = parseInt (matches[2 ]); if (matches[1 ] === '' && matches[2 ]) { start = fileSize - end; end = fileSize - 1 ; } if (start >= fileSize || end >= fileSize) { res.writeHead (416 ); res.setHeader ('Content-Range' , `bytes */${fileSize} ` ); res.end (); return ; } const contentLength = end - start + 1 ; res.writeHead (206 , { 'Content-Type' : 'application/octet-stream' , 'Content-Range' : `bytes ${start} -${end} /${fileSize} ` , 'Content-Length' : contentLength, 'Accept-Ranges' : 'bytes' }); const stream = fs.createReadStream (filePath, { start, end }); stream.pipe (res); } else { res.writeHead (200 , { 'Content-Type' : 'application/octet-stream' , 'Content-Length' : fileSize, 'Accept-Ranges' : 'bytes' }); const stream = fs.createReadStream (filePath); stream.pipe (res); } }); }); server.listen (3000 );
3.5 实现简易静态服务器 综合以上功能,我们来实现一个功能完整的静态文件服务器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 const http = require ('http' );const fs = require ('fs' );const path = require ('path' );const url = require ('url' );const PORT = process.env .PORT || 3000 ;const ROOT_DIR = path.resolve (process.argv [2 ] || __dirname);const mimeTypes = { '.html' : 'text/html; charset=utf-8' , '.css' : 'text/css; charset=utf-8' , '.js' : 'application/javascript; charset=utf-8' , '.json' : 'application/json; charset=utf-8' , '.png' : 'image/png' , '.jpg' : 'image/jpeg' , '.jpeg' : 'image/jpeg' , '.gif' : 'image/gif' , '.svg' : 'image/svg+xml' , '.ico' : 'image/x-icon' , '.txt' : 'text/plain; charset=utf-8' , '.pdf' : 'application/pdf' , '.zip' : 'application/zip' , '.woff' : 'font/woff' , '.woff2' : 'font/woff2' , '.ttf' : 'font/ttf' , '.mp3' : 'audio/mpeg' , '.mp4' : 'video/mp4' }; function getFilePath (urlPath ) { const decodedPath = decodeURIComponent (urlPath); let filePath = path.join (ROOT_DIR , decodedPath); if (!filePath.startsWith (ROOT_DIR )) { return null ; } if (fs.existsSync (filePath) && fs.statSync (filePath).isDirectory ()) { filePath = path.join (filePath, 'index.html' ); } return filePath; } const server = http.createServer ((req, res ) => { const parsedUrl = url.parse (req.url ); const filePath = getFilePath (parsedUrl.pathname ); if (!filePath) { res.writeHead (403 ); res.end ('Forbidden' ); return ; } fs.stat (filePath, (err, stats ) => { if (err || !stats.isFile ()) { res.writeHead (404 , { 'Content-Type' : 'text/plain; charset=utf-8' }); res.end ('404 Not Found' ); return ; } const ext = path.extname (filePath).toLowerCase (); const contentType = mimeTypes[ext] || 'application/octet-stream' ; const fileSize = stats.size ; const etag = `"${stats.size} -${stats.mtime.getTime()} "` ; const lastModified = stats.mtime .toUTCString (); if (req.headers ['if-none-match' ] === etag) { res.writeHead (304 ); res.end (); return ; } if (req.headers ['if-modified-since' ] && new Date (req.headers ['if-modified-since' ]) >= stats.mtime ) { res.writeHead (304 ); res.end (); return ; } const range = req.headers ['range' ]; if (range) { const matches = /bytes=(\d*)-(\d*)/ .exec (range); let start = 0 ; let end = fileSize - 1 ; if (matches[1 ]) { start = parseInt (matches[1 ]); } if (matches[2 ]) { end = parseInt (matches[2 ]); } if (!matches[1 ] && matches[2 ]) { start = fileSize - parseInt (matches[2 ]); } if (start >= fileSize) { res.writeHead (416 ); res.setHeader ('Content-Range' , `bytes */${fileSize} ` ); res.end (); return ; } end = Math .min (end, fileSize - 1 ); const contentLength = end - start + 1 ; res.writeHead (206 , { 'Content-Type' : contentType, 'Content-Range' : `bytes ${start} -${end} /${fileSize} ` , 'Content-Length' : contentLength, 'Accept-Ranges' : 'bytes' , 'Cache-Control' : 'public, max-age=3600' , 'ETag' : etag, 'Last-Modified' : lastModified }); fs.createReadStream (filePath, { start, end }).pipe (res); } else { res.writeHead (200 , { 'Content-Type' : contentType, 'Content-Length' : fileSize, 'Accept-Ranges' : 'bytes' , 'Cache-Control' : 'public, max-age=3600' , 'ETag' : etag, 'Last-Modified' : lastModified }); fs.createReadStream (filePath).pipe (res); } }); }); server.listen (PORT , () => { console .log (`静态服务器已启动: http://localhost:${PORT} ` ); console .log (`根目录: ${ROOT_DIR} ` ); });
运行方式:node static-server.js [目录路径]
四、net 模块与 TCP 编程 net 模块是 Node.js 中底层的网络编程模块,提供了 TCP 服务器和客户端的实现。它是 http、https 等模块的基础。
4.1 TCP 协议简介 TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层协议。
TCP 协议特点:
面向连接 :通信双方需要先建立连接,再进行数据传输
可靠性 :通过确认、重传、超时等机制保证数据可靠传输
有序性 :保证数据按发送顺序到达
流量控制 :防止发送方过快导致接收方溢出
拥塞控制 :防止网络过载
4.2 创建 TCP 服务器 使用 net.createServer() 创建 TCP 服务器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 const net = require ('net' );const server = net.createServer ((socket ) => { console .log ('客户端已连接:' , socket.remoteAddress , socket.remotePort ); socket.on ('data' , (data ) => { console .log ('收到数据:' , data.toString ()); socket.write (`服务器回复: ${data} ` ); }); socket.on ('close' , () => { console .log ('客户端已断开连接' ); }); socket.on ('error' , (err ) => { console .error ('Socket 错误:' , err.message ); }); }); const PORT = 8080 ;server.listen (PORT , () => { console .log (`TCP 服务器已启动: ${PORT} ` ); });
4.3 创建 TCP 客户端 使用 net.createConnection() 创建 TCP 客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const net = require ('net' );const client = net.createConnection ({ port : 8080 , host : 'localhost' }, () => { console .log ('已连接到服务器' ); client.write ('Hello, TCP Server!' ); }); client.on ('data' , (data ) => { console .log ('收到服务器回复:' , data.toString ()); client.end (); }); client.on ('close' , () => { console .log ('连接已关闭' ); }); client.on ('error' , (err ) => { console .error ('客户端错误:' , err.message ); });
4.4 TCP 三次握手 TCP 建立连接需要经过三次握手,这是一个重要的网络概念:
1 2 3 4 5 6 7 8 9 10 11 12 客户端 服务器 │ │ │──── SYN ─────────────────────────→│ │ (seq=100) │ 接收 SYN │ │ │←──── SYN + ACK ──────────────────│ │ (seq=200, ack=101) │ │ │ │──── ACK ─────────────────────────→│ │ (ack=201) │ 连接建立 │ │ │════════ 数据传输阶段 ══════════════│
在 Node.js 中,当调用 server.listen() 时,服务器开始监听并准备接受客户端的 SYN 请求;当客户端调用 net.createConnection() 时,会自动完成三次握手过程。
4.5 粘包问题 TCP 是面向字节流的协议,没有消息边界的概念。发送方发送的多次数据可能被接收方一次接收,这就是所谓的”粘包”问题。
粘包产生的原因:
发送方在短时间内连续发送小数据,TCP 会将它们合并发送
接收方在一次 data 事件中可能收到多个数据包的内容
粘包解决方案:
方案一:固定长度协议
每个消息固定长度,不足部分填充:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 const net = require ('net' );const MESSAGE_LENGTH = 10 ;function encode (msg ) { const buf = Buffer .alloc (MESSAGE_LENGTH ); const msgBuf = Buffer .from (msg, 'utf8' ); msgBuf.copy (buf); return buf; } function decode (buf ) { return buf.toString ('utf8' ).replace (/\0+$/ , '' ); } const server = net.createServer ((socket ) => { let buffer = Buffer .alloc (0 ); socket.on ('data' , (data ) => { buffer = Buffer .concat ([buffer, data]); while (buffer.length >= MESSAGE_LENGTH ) { const msg = decode (buffer.slice (0 , MESSAGE_LENGTH )); console .log ('收到消息:' , msg); buffer = buffer.slice (MESSAGE_LENGTH ); } }); }); server.listen (8080 ); const client = net.createConnection ({ port : 8080 }, () => { client.write (encode ('Hello' )); client.write (encode ('World' )); });
方案二:分隔符协议
使用特殊字符(如 \n)作为消息分隔符:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const net = require ('net' );const server = net.createServer ((socket ) => { let buffer = '' ; socket.on ('data' , (data ) => { buffer += data.toString (); let index; while ((index = buffer.indexOf ('\n' )) !== -1 ) { const msg = buffer.slice (0 , index); console .log ('收到消息:' , msg); buffer = buffer.slice (index + 1 ); } }); }); server.listen (8080 ); const client = net.createConnection ({ port : 8080 }, () => { client.write ('第一条消息\n' ); client.write ('第二条消息\n' ); });
方案三:长度前缀协议(推荐)
在消息前添加固定长度的字节表示消息体长度:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 const net = require ('net' );function encode (msg ) { const msgBuf = Buffer .from (msg, 'utf8' ); const header = Buffer .alloc (4 ); header.writeUInt32BE (msgBuf.length , 0 ); return Buffer .concat ([header, msgBuf]); } const server = net.createServer ((socket ) => { let buffer = Buffer .alloc (0 ); socket.on ('data' , (data ) => { buffer = Buffer .concat ([buffer, data]); while (buffer.length >= 4 ) { const msgLength = buffer.readUInt32BE (0 ); if (buffer.length >= 4 + msgLength) { const msg = buffer.slice (4 , 4 + msgLength).toString ('utf8' ); console .log ('收到消息:' , msg); socket.write (encode (`回复: ${msg} ` )); buffer = buffer.slice (4 + msgLength); } else { break ; } } }); }); server.listen (8080 , () => { console .log ('TCP 服务器已启动: 8080' ); }); const client = net.createConnection ({ port : 8080 }, () => { client.write (encode ('Hello, TCP!' )); client.write (encode ('This is a test message' )); }); let clientBuffer = Buffer .alloc (0 );client.on ('data' , (data ) => { clientBuffer = Buffer .concat ([clientBuffer, data]); while (clientBuffer.length >= 4 ) { const msgLength = clientBuffer.readUInt32BE (0 ); if (clientBuffer.length >= 4 + msgLength) { const msg = clientBuffer.slice (4 , 4 + msgLength).toString ('utf8' ); console .log ('收到回复:' , msg); clientBuffer = clientBuffer.slice (4 + msgLength); } else { break ; } } });
方案对比:
固定长度 :实现简单,但浪费带宽,不适合变长消息
分隔符 :实现简单,适合文本协议,但分隔符可能出现在消息内容中
长度前缀 :最常用和可靠的方案,广泛应用于各种协议中
五、WebSocket 实时通信 HTTP 协议是无状态的请求-响应模式,不支持服务器主动向客户端推送数据。WebSocket 的出现弥补了这一不足,实现了真正的全双工实时通信。
5.1 WebSocket 协议基础 WebSocket 是一种在单个 TCP 连接上进行全双工通信的网络协议。
WebSocket 连接建立过程:
1 2 3 4 5 6 7 8 9 10 11 12 客户端 服务器 │ │ │──── HTTP Upgrade 请求 ───────────────→│ │ Connection: Upgrade │ │ Upgrade: websocket │ │ Sec-WebSocket-Key: dGhlIHNhbXBsZ... │ │ │ │←──── HTTP 101 Switching Protocols ─────│ │ Upgrade: websocket │ │ Sec-WebSocket-Accept: s3pPLMBiTxaQ... │ │ │ │════════ 全双工通信通道建立 ════════════│
WebSocket vs HTTP 对比:
对比项
HTTP
WebSocket
通信模式
请求-响应
全双工
连接方式
短连接/长连接
持久连接
服务器推送
不支持(需轮询/SSE)
原生支持
协议开销
每次请求都有 Header
连接建立后开销极小
适用场景
普通 Web 请求
实时聊天、在线游戏、数据推送
5.2 使用 ws 库 ws 是 Node.js 中最流行的 WebSocket 实现库。
安装:
5.3 实现 WebSocket 服务器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 const WebSocket = require ('ws' );const wss = new WebSocket .Server ({ port : 8080 });console .log ('WebSocket 服务器已启动: ws://localhost:8080' );wss.on ('connection' , (ws, req ) => { const clientIp = req.socket .remoteAddress ; console .log (`新客户端连接: ${clientIp} ` ); ws.send (JSON .stringify ({ type : 'system' , message : '欢迎连接到 WebSocket 服务器' , timestamp : Date .now () })); ws.on ('message' , (message ) => { console .log (`收到消息: ${message} ` ); ws.send (JSON .stringify ({ type : 'echo' , message : `服务器收到: ${message} ` , timestamp : Date .now () })); }); ws.on ('close' , () => { console .log (`客户端断开: ${clientIp} ` ); }); ws.on ('error' , (err ) => { console .error ('WebSocket 错误:' , err.message ); }); }); setInterval (() => { const time = new Date ().toLocaleTimeString (); wss.clients .forEach ((client ) => { if (client.readyState === WebSocket .OPEN ) { client.send (JSON .stringify ({ type : 'broadcast' , message : `当前时间: ${time} ` , timestamp : Date .now () })); } }); }, 5000 );
使用浏览器原生 WebSocket API 连接:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const ws = new WebSocket ('ws://localhost:8080' );ws.onopen = () => { console .log ('连接已建立' ); ws.send ('Hello, WebSocket!' ); }; ws.onmessage = (event ) => { const data = JSON .parse (event.data ); console .log ('收到消息:' , data); }; ws.onclose = () => { console .log ('连接已关闭' ); }; ws.onerror = (err ) => { console .error ('连接错误:' , err); };
5.4 实现聊天服务器 下面实现一个功能完善的聊天室服务器,支持广播、私信、用户管理等功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 const WebSocket = require ('ws' );const wss = new WebSocket .Server ({ port : 8080 });const clients = new Map ();let userIdCounter = 0 ;function broadcast (message, excludeWs = null ) { const data = JSON .stringify (message); wss.clients .forEach ((client ) => { if (client.readyState === WebSocket .OPEN && client !== excludeWs) { client.send (data); } }); } function getOnlineUsers ( ) { const users = []; clients.forEach ((client, id ) => { users.push ({ id, nickname : client.nickname }); }); return users; } wss.on ('connection' , (ws ) => { const userId = ++userIdCounter; clients.set (userId, { ws, nickname : `用户${userId} ` }); ws.send (JSON .stringify ({ type : 'system' , code : 0 , data : { userId, nickname : `用户${userId} ` , message : '连接成功,请设置昵称' , onlineUsers : getOnlineUsers () } })); broadcast ({ type : 'system' , data : { message : `${userId} 号用户上线了` , onlineUsers : getOnlineUsers () } }, ws); ws.on ('message' , (rawMessage ) => { let message; try { message = JSON .parse (rawMessage); } catch { ws.send (JSON .stringify ({ type : 'error' , data : { message : '消息格式错误,请使用 JSON 格式' } })); return ; } const client = clients.get (userId); if (!client) return ; switch (message.type ) { case 'setNickname' : client.nickname = message.nickname || `用户${userId} ` ; broadcast ({ type : 'system' , data : { message : `用户${userId} 设置昵称为: ${client.nickname} ` , onlineUsers : getOnlineUsers () } }); break ; case 'chat' : broadcast ({ type : 'chat' , data : { from : client.nickname , content : message.content , timestamp : Date .now () } }); break ; case 'private' : { const targetClient = clients.get (message.targetId ); if (targetClient && targetClient.ws .readyState === WebSocket .OPEN ) { targetClient.ws .send (JSON .stringify ({ type : 'private' , data : { from : client.nickname , content : message.content , timestamp : Date .now () } })); ws.send (JSON .stringify ({ type : 'private' , data : { from : client.nickname , content : message.content , to : targetClient.nickname , timestamp : Date .now () } })); } else { ws.send (JSON .stringify ({ type : 'error' , data : { message : '目标用户不在线' } })); } break ; } case 'online' : ws.send (JSON .stringify ({ type : 'online' , data : { users : getOnlineUsers () } })); break ; default : ws.send (JSON .stringify ({ type : 'error' , data : { message : `未知消息类型: ${message.type} ` } })); } }); ws.on ('close' , () => { clients.delete (userId); broadcast ({ type : 'system' , data : { message : `${userId} 号用户(${getNickname(userId)} )下线了` , onlineUsers : getOnlineUsers () } }); }); ws.on ('error' , () => { clients.delete (userId); }); }); function getNickname (userId ) { const client = clients.get (userId); return client ? client.nickname : '未知' ; } console .log ('聊天服务器已启动: ws://localhost:8080' );
对应的客户端 HTML 页面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 <!DOCTYPE html > <html lang ="zh-CN" > <head > <meta charset ="UTF-8" > <title > Node.js 聊天室</title > <style > * { box-sizing : border-box; margin : 0 ; padding : 0 ; } body { font-family : 'Microsoft YaHei' , sans-serif; background : #f5f5f5 ; } .container { max-width : 800px ; margin : 20px auto; background : #fff ; border-radius : 8px ; box-shadow : 0 2px 10px rgba (0 ,0 ,0 ,0.1 ); } .header { background : #4CAF50 ; color : #fff ; padding : 15px ; border-radius : 8px 8px 0 0 ; } .header h1 { font-size : 20px ; } .chat-area { height : 400px ; overflow-y : auto; padding : 15px ; } .message { margin-bottom : 10px ; padding : 8px 12px ; border-radius : 15px ; max-width : 70% ; } .message .system { background : #e0e0e0 ; text-align : center; margin : 10px auto; } .message .chat { background : #d4edda ; } .message .private { background : #fff3cd ; } .message .meta { font-size : 12px ; color : #888 ; } .message .content { margin-top : 4px ; } .input-area { display : flex; padding : 15px ; border-top : 1px solid #eee ; } .input-area input { flex : 1 ; padding : 10px ; border : 1px solid #ddd ; border-radius : 4px ; } .input-area button { margin-left : 10px ; padding : 10px 20px ; background : #4CAF50 ; color : #fff ; border : none; border-radius : 4px ; cursor : pointer; } .input-area button :hover { background : #45a049 ; } .user-list { width : 200px ; padding : 10px ; border-left : 1px solid #eee ; } </style > </head > <body > <div class ="container" > <div class ="header" > <h1 > Node.js 聊天室</h1 > <input type ="text" id ="nickname" placeholder ="输入昵称" style ="margin-top:10px;padding:5px;border-radius:4px;border:none;" > <button onclick ="setNickname()" style ="margin-top:10px;padding:5px 10px;background:#fff;color:#4CAF50;border:none;border-radius:4px;cursor:pointer;" > 设置</button > </div > <div class ="chat-area" id ="chatArea" > </div > <div class ="input-area" > <input type ="text" id ="messageInput" placeholder ="输入消息..." onkeypress ="if(event.key==='Enter')sendMessage()" > <button onclick ="sendMessage()" > 发送</button > </div > </div > <script > const ws = new WebSocket ('ws://localhost:8080' ); let myUserId = null ; let myNickname = '' ; ws.onmessage = (event ) => { const msg = JSON .parse (event.data ); const chatArea = document .getElementById ('chatArea' ); switch (msg.type ) { case 'system' : if (msg.data .userId ) { myUserId = msg.data .userId ; } chatArea.innerHTML += `<div class="message system">${msg.data.message} </div>` ; break ; case 'chat' : chatArea.innerHTML += `<div class="message chat"> <div class="meta">${msg.data.from } - ${new Date (msg.data.timestamp).toLocaleTimeString()} </div> <div class="content">${msg.data.content} </div> </div>` ; break ; case 'private' : chatArea.innerHTML += `<div class="message private"> <div class="meta">[私信] ${msg.data.from } - ${new Date (msg.data.timestamp).toLocaleTimeString()} </div> <div class="content">${msg.data.content} </div> </div>` ; break ; case 'error' : alert (msg.data .message ); break ; } chatArea.scrollTop = chatArea.scrollHeight ; }; function setNickname ( ) { const nickname = document .getElementById ('nickname' ).value ; if (!nickname) return ; myNickname = nickname; ws.send (JSON .stringify ({ type : 'setNickname' , nickname })); } function sendMessage ( ) { const input = document .getElementById ('messageInput' ); const content = input.value .trim (); if (!content) return ; ws.send (JSON .stringify ({ type : 'chat' , content })); input.value = '' ; } </script > </body > </html >
5.5 广播与私信 上面的聊天室已经实现了广播与私信功能,这里做一个总结:
广播(Broadcast): 向所有连接的客户端发送消息。
1 2 3 4 5 6 7 8 9 function broadcast (message ) { const data = JSON .stringify (message); wss.clients .forEach ((client ) => { if (client.readyState === WebSocket .OPEN ) { client.send (data); } }); }
私信(Private Message): 只向指定的客户端发送消息。
1 2 3 4 5 6 7 function sendPrivateMessage (targetUserId, message ) { const targetClient = clients.get (targetUserId); if (targetClient && targetClient.ws .readyState === WebSocket .OPEN ) { targetClient.ws .send (JSON .stringify (message)); } }
心跳检测: 为了及时发现断开的客户端,需要实现心跳机制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 const HEARTBEAT_INTERVAL = 30000 ; wss.on ('connection' , (ws ) => { ws.isAlive = true ; ws.on ('pong' , () => { ws.isAlive = true ; }); }); const interval = setInterval (() => { wss.clients .forEach ((ws ) => { if (ws.isAlive === false ) { console .log ('心跳检测失败,关闭连接' ); return ws.terminate (); } ws.isAlive = false ; ws.ping (); }); }, HEARTBEAT_INTERVAL ); wss.on ('close' , () => { clearInterval (interval); });
六、实战练习 实战一:实现简易静态文件服务器 需求: 实现一个支持 MIME 类型、缓存控制、目录列表的静态文件服务器。
实现代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 const http = require ('http' );const fs = require ('fs' );const path = require ('path' );const PORT = process.env .PORT || 3000 ;const ROOT_DIR = path.resolve (process.argv [2 ] || __dirname);const mimeTypes = { '.html' : 'text/html; charset=utf-8' , '.css' : 'text/css; charset=utf-8' , '.js' : 'application/javascript; charset=utf-8' , '.json' : 'application/json; charset=utf-8' , '.png' : 'image/png' , '.jpg' : 'image/jpeg' , '.gif' : 'image/gif' , '.svg' : 'image/svg+xml' , '.ico' : 'image/x-icon' , '.txt' : 'text/plain; charset=utf-8' }; function listDirectory (dirPath ) { return new Promise ((resolve, reject ) => { fs.readdir (dirPath, (err, files ) => { if (err) return reject (err); const fileList = []; files.forEach ((file ) => { const filePath = path.join (dirPath, file); try { const stats = fs.statSync (filePath); fileList.push ({ name : file, isDirectory : stats.isDirectory (), size : stats.isDirectory () ? '' : stats.size , url : '/' + path.relative (ROOT_DIR , filePath).replace (/\\/g , '/' ) }); } catch {} }); resolve (fileList); }); }); } function generateDirectoryPage (dirPath, fileList ) { const relativePath = '/' + path.relative (ROOT_DIR , dirPath).replace (/\\/g , '/' ); let html = `<!DOCTYPE html><html><head><meta charset="UTF-8">` ; html += `<title>目录列表 - ${relativePath} </title>` ; html += `<style>` ; html += `body{font-family:sans-serif;margin:40px auto;max-width:800px;}` ; html += `h1{color:#333;}ul{list-style:none;padding:0;}` ; html += `li{padding:10px;border-bottom:1px solid #eee;display:flex;justify-content:space-between;}` ; html += `a{color:#4CAF50;text-decoration:none;}` ; html += `a:hover{text-decoration:underline;}` ; html += `.size{color:#999;font-size:0.9em;}` ; html += `</style></head><body>` ; html += `<h1>📁 ${relativePath || '/' } </h1>` ; html += `<ul>` ; html += `<li><a href="${path.dirname(relativePath) || '/' } ">⬅ 返回上级目录</a><span class="size"></span></li>` ; fileList.forEach ((file ) => { const icon = file.isDirectory ? '📁' : '📄' ; const sizeStr = file.size ? `${(file.size / 1024 ).toFixed(2 )} KB` : '' ; html += `<li><a href="${file.url} ">${icon} ${file.name} </a><span class="size">${sizeStr} </span></li>` ; }); html += `</ul></body></html>` ; return html; } const server = http.createServer (async (req, res) => { const urlPath = decodeURIComponent (req.url .split ('?' )[0 ]); let filePath = path.join (ROOT_DIR , urlPath); if (!filePath.startsWith (ROOT_DIR )) { res.writeHead (403 , { 'Content-Type' : 'text/plain; charset=utf-8' }); res.end ('403 Forbidden' ); return ; } fs.stat (filePath, async (err, stats) => { if (err) { res.writeHead (404 , { 'Content-Type' : 'text/plain; charset=utf-8' }); res.end ('404 Not Found' ); return ; } if (stats.isDirectory ()) { const indexPath = path.join (filePath, 'index.html' ); try { fs.accessSync (indexPath); const html = fs.readFileSync (indexPath); res.writeHead (200 , { 'Content-Type' : 'text/html; charset=utf-8' }); res.end (html); } catch { try { const fileList = await listDirectory (filePath); const html = generateDirectoryPage (filePath, fileList); res.writeHead (200 , { 'Content-Type' : 'text/html; charset=utf-8' }); res.end (html); } catch { res.writeHead (500 ); res.end ('500 Internal Server Error' ); } } } else { const ext = path.extname (filePath).toLowerCase (); const contentType = mimeTypes[ext] || 'application/octet-stream' ; const etag = `"${stats.size} -${stats.mtime.getTime()} "` ; if (req.headers ['if-none-match' ] === etag) { res.writeHead (304 ); res.end (); return ; } res.writeHead (200 , { 'Content-Type' : contentType, 'Content-Length' : stats.size , 'Cache-Control' : 'public, max-age=3600' , 'ETag' : etag }); fs.createReadStream (filePath).pipe (res); } }); }); server.listen (PORT , () => { console .log (`静态文件服务器已启动: http://localhost:${PORT} ` ); console .log (`根目录: ${ROOT_DIR} ` ); });
运行方式:
1 2 3 4 5 6 7 8 node static-server.js node static-server.js /path/to/your/directory PORT=8080 node static-server.js
功能特性:
✅ 支持 MIME 类型识别
✅ 支持协商缓存(ETag)
✅ 支持强缓存(Cache-Control)
✅ 支持目录列表
✅ 支持默认首页(index.html)
✅ 防止路径穿越攻击
✅ 使用流处理大文件
实战二:实现一个聊天室 需求: 实现一个支持多房间、昵称、在线人数统计的 WebSocket 聊天室。
服务端实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 const WebSocket = require ('ws' );const wss = new WebSocket .Server ({ port : 8080 });const rooms = new Map ();const clients = new Map ();let clientIdCounter = 0 ;function getOrCreateRoom (roomId ) { if (!rooms.has (roomId)) { rooms.set (roomId, new Set ()); } return rooms.get (roomId); } function broadcastToRoom (roomId, message, excludeWs = null ) { const room = rooms.get (roomId); if (!room) return ; const data = JSON .stringify (message); room.forEach ((client ) => { if (client !== excludeWs && client.readyState === WebSocket .OPEN ) { client.send (data); } }); } function getRoomUsers (roomId ) { const room = rooms.get (roomId); if (!room) return []; const users = []; room.forEach ((ws ) => { const client = clients.get (ws.clientId ); if (client) { users.push ({ id : client.id , nickname : client.nickname }); } }); return users; } wss.on ('connection' , (ws ) => { const clientId = ++clientIdCounter; ws.clientId = clientId; clients.set (clientId, { ws, nickname : `用户${clientId} ` , roomId : null }); ws.send (JSON .stringify ({ type : 'system' , data : { message : '连接成功' , userId : clientId } })); ws.on ('message' , (rawMessage ) => { let message; try { message = JSON .parse (rawMessage); } catch { ws.send (JSON .stringify ({ type : 'error' , data : { message : '消息格式错误' } })); return ; } const client = clients.get (clientId); if (!client) return ; switch (message.type ) { case 'setNickname' : client.nickname = message.nickname || client.nickname ; ws.send (JSON .stringify ({ type : 'system' , data : { message : `昵称已更新为: ${client.nickname} ` } })); break ; case 'joinRoom' : { const roomId = message.roomId ; if (client.roomId ) { const oldRoom = rooms.get (client.roomId ); if (oldRoom) oldRoom.delete (ws); } client.roomId = roomId; getOrCreateRoom (roomId).add (ws); broadcastToRoom (roomId, { type : 'system' , data : { message : `${client.nickname} 加入了房间` , users : getRoomUsers (roomId) } }); break ; } case 'leaveRoom' : { if (client.roomId ) { const room = rooms.get (client.roomId ); if (room) room.delete (ws); broadcastToRoom (client.roomId , { type : 'system' , data : { message : `${client.nickname} 离开了房间` , users : getRoomUsers (client.roomId ) } }); client.roomId = null ; } break ; } case 'chat' : if (client.roomId ) { broadcastToRoom (client.roomId , { type : 'chat' , data : { from : client.nickname , content : message.content , timestamp : Date .now () } }); } else { ws.send (JSON .stringify ({ type : 'error' , data : { message : '请先加入一个房间' } })); } break ; case 'online' : ws.send (JSON .stringify ({ type : 'online' , data : { users : client.roomId ? getRoomUsers (client.roomId ) : [] } })); break ; default : ws.send (JSON .stringify ({ type : 'error' , data : { message : `未知消息类型: ${message.type} ` } })); } }); ws.on ('close' , () => { const client = clients.get (clientId); if (client && client.roomId ) { const room = rooms.get (client.roomId ); if (room) room.delete (ws); broadcastToRoom (client.roomId , { type : 'system' , data : { message : `${client.nickname} 离开了房间` , users : getRoomUsers (client.roomId ) } }); } clients.delete (clientId); }); }); console .log ('多房间聊天室已启动: ws://localhost:8080' );
客户端 HTML 页面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 <!DOCTYPE html > <html lang ="zh-CN" > <head > <meta charset ="UTF-8" > <title > Node.js 多房间聊天室</title > <style > * { box-sizing : border-box; margin : 0 ; padding : 0 ; } body { font-family : 'Microsoft YaHei' , sans-serif; background : linear-gradient (135deg , #667eea 0% , #764ba2 100% ); min-height : 100vh ; padding : 20px ; } .container { max-width : 900px ; margin : 0 auto; background : #fff ; border-radius : 12px ; box-shadow : 0 10px 40px rgba (0 ,0 ,0 ,0.2 ); overflow : hidden; } .header { background : linear-gradient (135deg , #667eea 0% , #764ba2 100% ); color : #fff ; padding : 20px 30px ; } .header h1 { font-size : 22px ; margin-bottom : 15px ; } .header .controls { display : flex; gap : 10px ; flex-wrap : wrap; } .header input , .header select { padding : 8px 12px ; border : none; border-radius : 6px ; font-size : 14px ; } .header button { padding : 8px 16px ; background : rgba (255 ,255 ,255 ,0.2 ); color : #fff ; border : 1px solid rgba (255 ,255 ,255 ,0.4 ); border-radius : 6px ; cursor : pointer; font-size : 14px ; transition : all 0.3s ; } .header button :hover { background : rgba (255 ,255 ,255 ,0.3 ); } .main { display : flex; height : 500px ; } .chat-area { flex : 1 ; padding : 20px ; overflow-y : auto; background : #f9f9fb ; } .sidebar { width : 200px ; background : #f0f0f5 ; padding : 15px ; border-left : 1px solid #e0e0e0 ; } .sidebar h3 { font-size : 14px ; color : #666 ; margin-bottom : 10px ; padding-bottom : 8px ; border-bottom : 2px solid #667eea ; } .sidebar ul { list-style : none; } .sidebar li { padding : 6px 10px ; margin : 4px 0 ; background : #fff ; border-radius : 4px ; font-size : 13px ; } .message { margin-bottom : 12px ; padding : 10px 14px ; border-radius : 12px ; max-width : 80% ; word-wrap : break-word; } .message .system { background : #e8e8ef ; text-align : center; margin : 12px auto; max-width : 90% ; font-size : 13px ; color : #666 ; } .message .chat { background : linear-gradient (135deg , #667eea 0% , #764ba2 100% ); color : #fff ; margin-left : auto; } .message .chat .other { background : #fff ; color : #333 ; border : 1px solid #e0e0e0 ; margin-right : auto; } .message .meta { font-size : 11px ; opacity : 0.8 ; margin-bottom : 4px ; } .input-area { display : flex; padding : 15px 20px ; background : #fff ; border-top : 1px solid #eee ; } .input-area input { flex : 1 ; padding : 12px ; border : 2px solid #e0e0e0 ; border-radius : 8px ; font-size : 14px ; outline : none; transition : border-color 0.3s ; } .input-area input :focus { border-color : #667eea ; } .input-area button { margin-left : 10px ; padding : 12px 24px ; background : linear-gradient (135deg , #667eea 0% , #764ba2 100% ); color : #fff ; border : none; border-radius : 8px ; cursor : pointer; font-size : 14px ; font-weight : bold; } .input-area button :hover { transform : translateY (-1px ); box-shadow : 0 4px 12px rgba (102 ,126 ,234 ,0.4 ); } </style > </head > <body > <div class ="container" > <div class ="header" > <h1 > 💬 Node.js 多房间聊天室</h1 > <div class ="controls" > <input type ="text" id ="nickname" placeholder ="输入昵称" style ="width:120px;" > <select id ="roomSelect" > <option value ="room1" > 房间 1</option > <option value ="room2" > 房间 2</option > <option value ="room3" > 房间 3</option > </select > <button onclick ="joinRoom()" > 加入房间</button > <button onclick ="leaveRoom()" > 离开房间</button > </div > </div > <div class ="main" > <div class ="chat-area" id ="chatArea" > </div > <div class ="sidebar" > <h3 > 在线用户</h3 > <ul id ="userList" > <li style ="color:#999" > 暂无数据</li > </ul > </div > </div > <div class ="input-area" > <input type ="text" id ="messageInput" placeholder ="输入消息,按 Enter 发送..." onkeypress ="if(event.key==='Enter')sendMessage()" > <button onclick ="sendMessage()" > 发送</button > </div > </div > <script > const ws = new WebSocket ('ws://localhost:8080' ); let myUserId = null ; let myNickname = '' ; ws.onmessage = (event ) => { const msg = JSON .parse (event.data ); const chatArea = document .getElementById ('chatArea' ); switch (msg.type ) { case 'system' : if (msg.data .userId ) myUserId = msg.data .userId ; chatArea.innerHTML += `<div class="message system">${msg.data.message} </div>` ; break ; case 'chat' : const isMine = msg.data .from === myNickname; chatArea.innerHTML += `<div class="message chat ${isMine ? '' : 'other' } "> <div class="meta">${msg.data.from } · ${new Date (msg.data.timestamp).toLocaleTimeString()} </div> <div>${msg.data.content} </div> </div>` ; break ; case 'online' : const userList = document .getElementById ('userList' ); if (msg.data .users && msg.data .users .length > 0 ) { userList.innerHTML = msg.data .users .map (u => `<li>👤 ${u.nickname} </li>` ).join ('' ); } else { userList.innerHTML = '<li style="color:#999">暂无用户</li>' ; } break ; case 'error' : alert (msg.data .message ); break ; } chatArea.scrollTop = chatArea.scrollHeight ; }; function joinRoom ( ) { const nickname = document .getElementById ('nickname' ).value .trim (); if (nickname) { myNickname = nickname; ws.send (JSON .stringify ({ type : 'setNickname' , nickname })); } const roomId = document .getElementById ('roomSelect' ).value ; ws.send (JSON .stringify ({ type : 'joinRoom' , roomId })); } function leaveRoom ( ) { ws.send (JSON .stringify ({ type : 'leaveRoom' })); } function sendMessage ( ) { const input = document .getElementById ('messageInput' ); const content = input.value .trim (); if (!content) return ; ws.send (JSON .stringify ({ type : 'chat' , content })); input.value = '' ; } </script > </body > </html >
运行方式:
1 2 3 4 5 6 7 8 9 npm install ws node chat-room-server.js
功能特性:
✅ 多房间支持,用户可加入/离开任意房间
✅ 昵称设置与在线用户列表
✅ 实时消息广播
✅ 房间隔离(不同房间消息互不影响)
✅ 用户加入/离开房间提示
本章小结 本章详细介绍了 Node.js 网络编程与 Web 服务的核心知识:
HTTP 协议基础 :理解了请求/响应模型、状态码、请求方法、Header 和 Cookie/Session 的概念
http 模块 :学习了如何创建 HTTP 服务器、处理请求响应、实现路由分发、获取请求参数、返回 HTML/JSON、处理 CORS 跨域
静态文件服务 :掌握了读取静态文件、MIME 类型映射、缓存控制(强缓存与协商缓存)、Range 请求的实现
net 模块与 TCP 编程 :了解了 TCP 协议、创建 TCP 服务器/客户端、三次握手流程、粘包问题及解决方案
WebSocket 实时通信 :学习了 WebSocket 协议、使用 ws 库、实现广播与私信、心跳检测机制
实战练习 :通过实现简易静态文件服务器和多房间聊天室,综合运用了本章所学知识
通过本章的学习,你已经具备了使用 Node.js 构建 Web 服务和实时通信应用的能力。在实际项目中,可以进一步学习 Express、Koa 等框架来简化开发流程。