网络分层模型
web协议栈
Internet Protocol (IP)
IP 地址、Mac 地址
URI 和 URL
URI:统一资源标识符
URL:统一资源定位符
简单的HTTP 请求和相应
const net = require('net');
let responseDataTpl = `HTTP/1.1 200 OK
Connection:keep-alive
Date: ${new Date()}
Content-Length: 12
Content-Type: text/plain
Hello world!
`;
let server = net.createServer((socket) => {
socket.setKeepAlive(true, 60000);
socket.write(responseDataTpl);
socket.pipe(socket);
socket.on('data', function(data){
console.log('DATA ' + socket.remoteAddress + ': ' + data);
//socket.end('goodbye\n');
});
socket.on('close', function(){
console.log('connection closed, goodbye!\n\n\n');
});
}).on('error', (err) => {
// handle errors here
throw err;
});
server.listen({
host: 'localhost',
port: 9999,
exclusive: true
}, ()=>{
console.log('opened server on', server.address());
HTML 资源和gzip
const net = require('net');
const zlib = require('zlib');
let responseData = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
`;
let server = net.createServer((socket) => {
socket.setKeepAlive(true, 60000);
zlib.gzip(new Buffer(responseData), function(err, content){
let responseDataTpl = `HTTP/1.1 200 OK
Connection: keep-alive
Date: ${new Date()}
Content-Length: ${content.length}
Content-Type: text/html
Content-encoding: gzip
`;
socket.write(responseDataTpl);
socket.write(content, 'binary');
socket.pipe(socket);
});
socket.on('data', function(data){
console.log('DATA ' + socket.remoteAddress + ': ' + data);
});
socket.on('close', function(){
console.log('connection closed, goodbye!\n\n\n');
});
}).on('error', (err) => {
// handle errors here
throw err;
});
server.listen({
host: '0.0.0.0',
port: 10080,
exclusive: true
}, ()=>{
console.log('opened server on', server.address());
});
使用http 模块
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('hello world');
})
server.on('clientError', (err, socket) => {
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n')
})
server.listen(10080)
Stream
http 与资源流
const http = require('http');
const url = require('url');
const fs = require("fs");
const server = http.createServer((req, res) => {
let srvUrl = url.parse(`http://${req.url}`);
let path = srvUrl.path;
if(path === '/') path = '/index.html';
let resPath = '.' + path;
if(!fs.existsSync(resPath)){
res.writeHead(404, {'Content-Type': 'text/html'});
return res.end('<h1>404 Not Found</h1>');
}
let resStream = fs.createReadStream(resPath);
res.writeHead(200, {'Content-Type': 'text/html'});
resStream.pipe(res);
});
server.on('clientError', (err, socket) => {
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});
server.listen(10080);
HTTP状态吗
常用状态吗
- 200
- 301
- 302
- 304
- 403
- 500
内容的格式
MIME(Multipurpose Internet Mail Extensions)
const http = require('http');
const url = require('url');
const fs = require("fs");
function getMimeType(res){
const EXT_MIME_TYPES = {
'default': 'text/html',
'.js': 'text/javascript',
'.css': 'text/css',
'.json': 'text/json',
'.jpeg': 'image/jpeg',
'.jpg': 'image/jpg',
'.png': 'image/png',
//...
}
let path = require('path');
let mime_type = EXT_MIME_TYPES[path.extname(res)] || EXT_MIME_TYPES['default'];
return mime_type;
}
const server = http.createServer((req, res) => {
let srvUrl = url.parse(`http://${req.url}`);
let path = srvUrl.path;
if(path === '/') path = '/index.html';
let resPath = '.' + path;
if(!fs.existsSync(resPath)){
res.writeHead(404, {'Content-Type': 'text/html'});
return res.end('<h1>404 Not Found</h1>');
}
let resStream = fs.createReadStream(resPath);
res.writeHead(200, {'Content-Type': getMimeType(resPath)});
resStream.pipe(res);
});
server.on('clientError', (err, socket) => {
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});
server.listen(10080);
响应内容协商
const http = require('http');
const json2html = require('node-json2html');
const server = http.createServer((req, res) => {
let responseData = {
name: 'akria',
birthday: '1981-12-29'
};
let accept = req.headers['accept'];
if(accept.indexOf('text/json') >= 0){ //不严格的判断
res.writeHead(200, {'Content-Type': 'text/json'});
res.end(JSON.stringify(responseData));
}else{
res.writeHead(200, {'Content-Type': 'text/html'});
let transform = {'tag': 'div', 'html': '${name} : ${birthday}'};
res.end(json2html.transform(responseData, transform));
}
});
server.on('clientError', (err, socket) => {
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});
server.listen(10080);
HTTP Verbs
表单数据POST
const http = require('http');
const server = http.createServer((req, res) => {
if (req.method == 'POST') {
let body = '';
req.on('data', function (data) {
body += data;
// Too much POST data, kill the connection!
// 1e6 === 1 * Math.pow(10, 6) === 1 * 1000000 ~~~ 1MB
if (body.length > 1e6)
req.connection.destroy();
});
req.on('end', function () {
console.log(body);
});
res.writeHead(200, {'Content-Type': 'text/json'});
res.end('{"err":"","state":"success"}');
}else{
res.writeHead(405, {'Content-Type': 'text/html'});
res.end('<h1>Method not allowed</h1>');
}
});
server.on('clientError', (err, socket) => {
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});
server.listen(10080);
表单数据POST + 使用流
const http = require('http');
const through = require('through2');
const server = http.createServer((req, res) => {
if (req.method == 'POST') {
let body = '';
res.writeHead(200, {'Content-Type': 'text/json'});
req.pipe(through.obj((content, encode, done) => {
if(encode === 'buffer'){
content = {
srcData: content.toString('utf-8'),
err: '',
state: 'success'
};
encode = 'utf-8';
}
done(null, JSON.stringify(content), encode)
})).pipe(res);
}else{
res.writeHead(405, {'Content-Type': 'text/html'});
res.end('<h1>Method not allowed</h1>');
}
});
server.on('clientError', (err, socket) => {
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});
server.listen(10080);
Cache Control
- 又称强缓存,普通刷新会忽略它,但并不会清除它,需要强制刷新
- 浏览器强制刷新,请求会带上 Cache-Control: no-cache 和 Pragma:no-cache 头
const http = require('http');
const url = require('url');
const fs = require("fs");
function getMimeType(res){
const EXT_MIME_TYPES = {
'default': 'text/html',
'.js': 'text/javascript',
'.css': 'text/css',
'.json': 'text/json',
'.jpeg': 'image/jpeg',
'.jpg': 'image/jpg',
'.png': 'image/png',
//...
}
let path = require('path');
let mime_type = EXT_MIME_TYPES[path.extname(res)] || EXT_MIME_TYPES['default'];
return mime_type;
}
const server = http.createServer((req, res) => {
let srvUrl = url.parse(`http://${req.url}`);
let path = srvUrl.path;
if(path === '/') path = '/index.html';
let resPath = '.' + path;
if(!fs.existsSync(resPath)){
res.writeHead(404, {'Content-Type': 'text/html'});
return res.end('<h1>404 Not Found</h1>');
}
let resStream = fs.createReadStream(resPath);
res.writeHead(200, {
'Content-Type': getMimeType(resPath),
'Cache-Control': 'max-age=86400'
});
resStream.pipe(res);
});
server.on('clientError', (err, socket) => {
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});
server.listen(10080);
协商缓存
- 又称若缓存,普通刷新会启用弱缓存,忽略强缓存
- 只有从地址栏或收藏夹输入地址,通过链接引用资源的情况下,浏览器才会启用强缓存
- 这也是为什么有时候我们更新一张图片,一个js文件,页面内容依旧是旧的, 但是直接浏览器访问那个图片或者文件,看到的内容确实新的。
const http = require('http');
const url = require('url');
const fs = require("fs");
function getMimeType(res){
const EXT_MIME_TYPES = {
'default': 'text/html',
'.js': 'text/javascript',
'.css': 'text/css',
'.json': 'text/json',
'.jpeg': 'image/jpeg',
'.jpg': 'image/jpg',
'.png': 'image/png',
//...
}
let path = require('path');
let mime_type = EXT_MIME_TYPES[path.extname(res)] || EXT_MIME_TYPES['default'];
return mime_type;
}
const server = http.createServer((req, res) => {
let srvUrl = url.parse(`http://${req.url}`);
let path = srvUrl.path;
if(path === '/') path = '/index.html';
let resPath = '.' + path;
let lastModified = req.headers['if-modified-since'];
if(lastModified && Date.now() - new Date(lastModified) < 86400000){
res.writeHead(304, {
'Content-Type': getMimeType(resPath),
'Last-Modified': new Date(lastModified),
'Expires': new Date(new Date(lastModified) + 86400000)
});
res.end();
}else{
if(!fs.existsSync(resPath)){
res.writeHead(404, {'Content-Type': 'text/html'});
return res.end('<h1>404 Not Found</h1>');
}
let resStream = fs.createReadStream(resPath);
res.writeHead(200, {
'Content-Type': getMimeType(resPath),
'Last-Modified': new Date(),
'Expires': new Date(Date.now() + 86400000)
});
resStream.pipe(res);
}
});
server.on('clientError', (err, socket) => {
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});
server.listen(10080);
协商缓存: etag
const http = require('http');
const url = require('url');
const fs = require("fs");
const checksum = require('checksum');
function getMimeType(res){
const EXT_MIME_TYPES = {
'default': 'text/html',
'.js': 'text/javascript',
'.css': 'text/css',
'.json': 'text/json',
'.jpeg': 'image/jpeg',
'.jpg': 'image/jpg',
'.png': 'image/png',
//...
}
let path = require('path');
let mime_type = EXT_MIME_TYPES[path.extname(res)] || EXT_MIME_TYPES['default'];
return mime_type;
}
const server = http.createServer((req, res) => {
let srvUrl = url.parse(`http://${req.url}`);
let path = srvUrl.path;
if(path === '/') path = '/index.html';
let resPath = '.' + path;
if(!fs.existsSync(resPath)){
res.writeHead(404, {'Content-Type': 'text/html'});
return res.end('<h1>404 Not Found</h1>');
}
checksum.file(resPath, (err, sum) => {
let resStream = fs.createReadStream(resPath);
sum = '"' + sum + '"'; //etag 要加双引号
if(req.headers['if-none-match'] === sum){
res.writeHead(304, {
'Content-Type': getMimeType(resPath),
'etag': sum
});
res.end();
}else{
res.writeHead(200, {
'Content-Type': getMimeType(resPath),
'etag': sum
});
resStream.pipe(res);
}
});
});
server.on('clientError', (err, socket) => {
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});
server.listen(10080);
HTTP Upgrade(WebSocket)
const net = require('net');
const http = require('http');
const crypto = require('crypto');
const content = `
<h1>
<a href="https://developer.mozilla.org/zh-CN/docs/WebSockets/Writing_WebSocket_servers">
WebSocket is HTTP Upgrade
</a>
</h1>
<script>
var ws = new WebSocket('ws://127.0.0.1:10080');
ws.onmessage = function(evt){
console.log('received: ' + evt.data);
}
ws.onopen = function(){
console.log('opened');
ws.send('hello~');
}
</script>
`;
class WSServer{
constructor(){
const httpServer = new http.Server();
httpServer.addListener("connection", function(){
// requests_recv++;
});
httpServer.addListener("request", function(req, res){
res.writeHead(200, {"Content-Type": "text/html"});
res.write(content);
res.end();
});
httpServer.addListener("upgrade", function(req, socket, upgradeHead){
let secWebSocketAccept = req.headers['sec-websocket-key'] + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
let sha1 = crypto.createHash('sha1');
sha1.update(secWebSocketAccept);
secWebSocketAccept = sha1.digest('base64');
let serverHandshake = `HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: ${secWebSocketAccept}
`;
socket.write(serverHandshake);
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-------+-+-------------+-------------------------------+
// |F|R|R|R| opcode|M| Payload len | Extended payload length |
// |I|S|S|S| (4) |A| (7) | (16/64) |
// |N|V|V|V| |S| | (if payload len==126/127) |
// | |1|2|3| |K| | |
// +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
// | Extended payload length continued, if payload len == 127 |
// + - - - - - - - - - - - - - - - +-------------------------------+
// | |Masking-key, if MASK set to 1 |
// +-------------------------------+-------------------------------+
// | Masking-key (continued) | Payload Data |
// +-------------------------------- - - - - - - - - - - - - - - - +
// : Payload Data continued ... :
// + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
// | Payload Data continued ... |
// +---------------------------------------------------------------+
socket.on('data', function(buffer){
let dataArr = new Uint8Array(buffer);
let payload_len = (dataArr[1] & 0x7f);
let MASK = dataArr.slice(2, 6);
let ENCODED = dataArr.slice(6, 6 + payload_len);
let DECODED = [];
for (let i = 0; i < ENCODED.length; i++) {
DECODED[i] = ENCODED[i] ^ MASK[i % 4];
}
console.log('server received: ' + String.fromCharCode(...DECODED));
let sendDataArr = [];
sendDataArr[0] = 0x81;
sendDataArr[1] = 0x2;
sendDataArr[2] = 'o'.charCodeAt(0);
sendDataArr[3] = 'k'.charCodeAt(0);
socket.write(new Buffer(sendDataArr));
});
});
this.httpServer = httpServer;
}
listen(port){
this.httpServer.listen(port);
}
}
var server = new WSServer();
server.listen(10080);
Node.js 常用Web服务器
总结
本课学习了什么?
- TCP和HTTP协议
- 服务端开发基础
课后练习
完善匿名聊天室 , 增加你认为有用/有趣的功能
应用上之前所学的各节课程中所介绍的内容
尝试融会贯通
代码提交到github