持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天,点击查看活动详情
本文将介绍使用 Node.js 进行服务端编程。
计算机网络分层架构速览
TCP/IP 模型
ARPA 在研究 ARPAnet 时提出了 TCP/IP 模型。TCP/IP 由于得到广泛应用而成为事实上的国际标准。
网络 ISO/OSI 参考模型
国际标准化组织 (ISO) 提出的网络体系结构模型,称为开放系统互连参考模型 (OSI/ RM), 通常简称为 OSI(Open System Interconnection Model) 参考模型。
两个模型的对比
相同点:
- 都采取分层的体系结构,将庞大且复杂的问题划分为若干较容易处理的、范围较小的问题,而且分层的功能也大体相似。
- 都是基于独立的协议栈的概念。
- 都可以解决异构网络的互联,实现世界上不同厂家生产的计算机之间的通信。
不同点:
- OSI 参考模型的最大贡献就是精确地定义了三个主要概念:服务、协议和接口,这与现代的面向对象程序设计思想非常吻合。而 TCP/IP 模型在这三个概念上却没有明确区分,不符合软件工程的思想。
- OSI 参考模型产生在协议发明之前,没有偏向于任何特定的协议,通用性良好。但设计者在协议方面没有太多经验,不知道把哪些功能放到哪一层更好。 TCP/IP 模型正好相反,首先出现的是协议,模型实际上是对已有协议的描述,因此不会出现协议不能匹配模型的情况,但TCP/IP 模型不适合于任何其他非 TCP/IP 的协议栈。
- TCP/IP 模型在设计之初就考虑到了多种异构网的互联问题,并将
IP协议作为一个单独的重要层次(网络层)。 OSI 参考模型最初只考虑到用一种标准的公用数据网将各种不同的系统互联,在认识到 IP 的重要性后,只好在网络层中划分出一个子层来完成类似 TCP/IP 模型中的 IP 的功能。
- OSI 参考模型在网络层支持无连接和面向连接的通信,但在传输层仅有面向连接的通信。而 TCP/IP 模型认为可靠性是端到端的问题,因此它在网际层仅有一种无连接的通信模式(IP协议), 但传输层支持无连接(UDP)和面向连接(TCP)两种模式。
五层模型
无论是 OSI 参考模型还是 TCP/IP 模型,都不是完美的。 OSI 参考模型的设计者从工作的开始,就试图建立一个全世界的计算机网络都要遵循的统一标准。这也导致 OSI 参考模型效率低,结构复杂。在研究计算机网络时,我们通常采取折中的办法,即综合 OSI 参考模型和 TCP/IP 模型的优点,采用一种只有层协议的体系结构,即我们所熟知的物理层、数据链路层、网络层、传输层和应用层。
在我们编写 Web 应用时,通常很少和物理层和数据链路层打交道,基本上是和网络层、传输层和应用层打交道。网络层主要是 IP 协议;传输层主要是 TCP/ UDP 协议;应用层主要是 HTTP 协议。 HTTP 协议是基于传输层 TCP 协议建立在 TCP 协议之上的文本协议,因此 TCP 服务可以处理 HTTP 请求和响应。
用 TCP 服务处理 HTTP 请求
我们知道,应用层的 HTTP 报文需要借助下层(传输层)提供的服务,即使用 Socket 进行传输。Node.js 的内置模块net 模块,可以方便地创建 TCP 服务,监听端口,接受远程客户端的连接。
TCP 服务是基于 Client/Server 模式的,我们首先需要创建一个服务器:
// server.js
const net = require("net");
const HOST = "127.0.0.1";
const PORT = 6868;
const server = net.createServer();
server.listen({PORT, HOST}, () => {
console.log(`server listen on `, server.address());
});
// 双方建立链接时,会自动获得一个socket对象(std,socket描述符)
// 当 server 被启动(连接)时,执行回调函数
server.on("connection", (socket) => {
// 远程客户端地址
console.log(`connected:${socket.remoteAddress}:${socket.remotePort}`);
// 本地服务端地址
console.log(`local:${socket.localAddress}:${socket.localPort}`);
// 向客户端发送数据
// socket.write("服:你好客户端");
// 收到客户端数据时
socket.on("data", (data) => {
console.log(`${data}`);
});
// 客户端主动断连,触发end事件
socket.on("end", () => {
console.log(`客户端${socket.remoteAddress}:${socket.remotePort}已断连`);
});
// 如果链接断开,write方法就无效了
setInterval(() => {
socket.write(`服:定时消息${Date.now()}`);
}, 1000);
});
net.createServer表示创建并返回一个 server 对象。它的参数是一个回调函数,这个回调函数会在连接建立的时候被调用。
-
net.createServer创建的server对象需要调用 listen 方法才能够与客户端建立连接。listen 方法的第一个参数是一个配置项:host表示校验服务器名或 IP 地址。如果设置为0.0.0.0,则表示不校验名称及 IP 地址。也就是说只要能访问到运行server.js的这台服务器,不管是通过哪个 IP 地址或者服务器名访问的,都允许建立连接。port表示要连接的端口号。- listen 方法第二个参数是一个回调函数。
此时控制台打印如下信息:
server listen on { address: '127.0.0.1', family: 'IPv4', port: 6868 }
同时服务器将持续监听 6868端口 的TCP 请求。
💡 Node.js 中 socket 对象:
{
"connecting" :false,
"_hadError" :false,
"_parent" :null,
"_host" :null,
"_readableState" :{/* ... */},
"_events" :{},
"_eventsCount" :1,
"_writableState" :{/* ... */},
"allowHalfOpen" :false,
"_sockname" :null,
"_pendingData" :null,
"_pendingEncoding" : "" ,
"server" :{
"_events" :{
},
"_eventsCount" :1,
"_connections" :1,
"_handle" :{
"reading" :false
},
"_usingWorkers" :false,
"_workers" :[
],
"_unref" :false,
"allowHalfOpen" :false,
"pauseOnConnect" :false,
"noDelay" :false,
"keepAlive" :false,
"keepAliveInitialDelay" :0,
"_connectionKey" : "4:127.0.0.1:6868"
},
"_server" :{/* ... */}
}
用 TCP 服务发送 HTTP 请求
我们可以再创建一个客户端来发送请求:
// client.js
const net = require("net");
const HOST = "127.0.0.1";
const PORT = 6868;
const client = new net.Socket();
client.connect(PORT, HOST, () => {
console.log(`connected to:${HOST}:${PORT}`);
// 向服务端发送数据
client.write("客:你好服务端");
// 收到服务端数据时
client.on("data", (data) => {
console.log(data.toString());
client.write("客:已收到");
});
// 客户端主动断连,触发自己的end事件
client.on("end", () => {
console.log("链接已主动断开");
});
// 3s后主动关闭此次tcp长连接
setTimeout(() => {
client.end()
}, 3000)
});
用浏览器连接 TCP 服务
因为浏览器通信是通过建立 HTTP 连接,而 HTTP 协议是基于 TCP 的。因此我们可以使用浏览器与TCP 服务器建立连接。
打开浏览器,输入 localhost:6868 可以向服务端发送 HTTP 请求。此时控制台会打印输出:
connected:127.0.0.1:62493
local:127.0.0.1:6868
GET / HTTP/1.1
Host: localhost:6868
Connection: keep-alive
Cache-Control: max-age=0
sec-ch-ua: "Microsoft Edge";v="105", "Not)A;Brand";v="8", "Chromium";v="105"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36 Edg/105.0.1343.53
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
客户端127.0.0.1:62493已断连
HTTP 报文
通过 createServer 的回调函数返回的网络连接套接字 socket 对象的 data 事件socket.on("data", callback),我们可以获取到客户端向服务器发送的 HTTP 请求报文:
GET / HTTP/1.1
Host: localhost:6868
Connection: keep-alive
Cache-Control: max-age=0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36 Edg/105.0.1343.53
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
- 第一行是:
GET / HTTP/1.1,表示浏览器向服务器发起了 HTTP GET 请求,HTTP的版本是 1.1,请求的路径是/。
- 从第二行开始是
键-值的形式,表示 HTTP 的请求头。
- 最后是两个空行。第一个空行是 HTTP 请求头(HTTP Header)和 HTTP 内容(HTTP Body)的分隔
crlf;第二个空行是服务器响应GET请求的返回数据。此时返回为空,显示为一个空行。
此时,为了响应浏览器发送的 HTTP Request,我们可以修改一下服务端代码:
const net = require("net");
const HOST = "127.0.0.1";
const PORT = 6868;
const responseFunc = (str) => {
return `HTTP/1.1 200 OK
Connection: keep-alive
Date: ${new Date()}
Content-Length: ${str.length}
Content-Type: text/html
${str}`;
}
const server = net.createServer();
server.listen({
port: PORT,
host: HOST
}, () => {
console.log(`server listen on `, server.address());
});
// 重要:双方建立链接时,会自动获得一个socket对象(std,socket描述符)
server.on("connection", (socket) => {
// 远程客户端地址
console.log(`connected:${socket.remoteAddress}:${socket.remotePort}`);
// 本地服务端地址
console.log(`local:${socket.localAddress}:${socket.localPort}`);
// 收到客户端数据时
socket.on("data", (data) => {
console.log(`${data}`);
// 如果请求中有GET字段,则返回 HTTP Response
if (data.indexOf('GET') !== -1) {
socket.write(responseFunc('<h2>Hello world</h2>'));
}
});
// 客户端主动断连,触发end事件
socket.on("end", () => {
console.log(`客户端${socket.remoteAddress}:${socket.remotePort}已断连`);
});
// 如果链接断开,write方法就无效了
setInterval(() => {
socket.write(`服:定时消息${Date.now()}`);
}, 1000);
});
另外,HTTP Response 有许多不同的状态码。服务器可以给浏览器返回不同的状态码和响应内容,浏览器会根据响应内容、状态码执行不同的动作。
const net = require("net");
const HOST = "127.0.0.1";
const PORT = 6868;
const responseFunc = (str, status = 200, desc = 'OK') => {
return `HTTP/1.1 ${status} ${desc}
Connection: keep-alive
Date: ${new Date()}
Content-Length: ${str.length}
Content-Type: text/html
${str}`;
}
const server = net.createServer();
server.listen({
port: PORT,
host: HOST
}, () => {
console.log(`server listen on `, server.address());
});
// 重要:双方建立链接时,会自动获得一个socket对象(std,socket描述符)
server.on("connection", (socket) => {
// 远程客户端地址
console.log(`connected:${socket.remoteAddress}:${socket.remotePort}`);
// 本地服务端地址
console.log(`local:${socket.localAddress}:${socket.localPort}`);
// 收到客户端数据时
socket.on("data", (data) => {
console.log(`data: ${data.toString('utf-8')}`);
// 获得请求路径
const matched = data.toString('utf-8').match(/^GET ([/\s/\S]+) HTTP/);
if (matched) {
const path = matched[1];
console.log(`path: ${path}`);
if(path.indexOf('/404') !== -1) { // 如果路径包含404,则返回 404 Not Found
socket.write(responseFunc('<h1>Not Found</h1>', 404, 'NOT FOUND'));
} else { // 否则正常显示
socket.write(responseFunc('<h1>Hello world</h1>'));
}
}
});
// 客户端主动断连,触发end事件
socket.on("end", () => {
console.log(`客户端${socket.remoteAddress}:${socket.remotePort}已断连`);
});
});
💡 此时,当前端重定向页面至404时,服务器将发送
404 Not Found
HTTP 请求逻辑
浏览器发送 HTTP Request 向服务器请求内容,根据服务器发送的 HTTP Response 执行对应的动作,并将响应内容(HTTP Body)渲染出来。
下一章我们将介绍使用
Node.js内置的http模块简化上述操作。