个人总结:读完这篇文章需要30分钟
http2部分很有学习价值,可以好好看。
- 用node搭建TCP服务器
- 用node搭建HTTP服务器
- 用node文件fs模块对文件读取,并用流的方式写入
- 用url路径模块,完成了node路由
- path模块判断文件类型
- 用gzip对文件进行压缩
- 浏览器缓存协议的实现
- node处理跨域
- https的node服务器的搭建
- http2的node服务器的搭建
*n*node的事件机制:
//events 模块只提供了一个对象: events.EventEmitter
//EventEmitter 的核心就是事件触发与事件监听器功能的封装。
var EventEmitter = require('events').EventEmitter;
//一个socket对象
var socket = new EventEmitter();
//我们在socket对象上绑定data事件,如果是多个函数会被先后调用
socket.on('data', function(res) {
console.log(res);
});
socket.on('data', function(res) {
console.log(res + '111');
});
//我们用emit的方法去触发事件,在1秒后我们出发,我们触发事件时,可以传递参数。
setTimeout(function() {
socket.emit('data' , "hello" );
}, 1000);
1 node创建TCP服务器
const net = require('net');
let server = net.createServer((socket)=>{
socket.on('data',function (res) {
console.log(res.toString())
});
});
server.listen({
host: 'localhost',
port: 8080
});
访问localhost:8080,
在终端看到下面信息:
将一段符合http格式的数据用socket.write(responseDataTpl)返回数据
let responseDataTpl = `HTTP/1.1 200 OK
Connection:keep-alive
Date: ${new Date()}
Content-Length: 12
Content-Type: text/plain
Hello world!
`;
,在浏览器中,看到返回的Hello world!
问题:写出固定格式的http响应报文比较麻烦的,给他封装一层
2 node创建HTTP服务器
2.1 创建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)
在浏览器里面访问localhost:10080,看到浏览器上显示'hello world'
问题:这个时候,如果我希望传进去是一个文件而不是字符串
2.2 node文件模块(fs)
node的文件模块是非常强大的,可以对文件进行读取,增删改查。这里我们先讲如何读取的。读取分两种一种同步,一种异步。
const fs = require('fs');
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html' });
// 同步
// let data = fs.readFileSync('index.html');
// res.write(data);
// res.end(); // 发送响应数据
// 异步
fs.readFile('index.html', function (err, data) {
res.write(data);
res.end(); // 发送响应数据
})
})
server.on('clientError', (err, socket) => {
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n')
})
server.listen(8088)
- 我们引入文件模块,const fs = require('fs');
- 同步的时候,我们先读取,执行后边的写入和发送函数
- 异步的时候,我们在异步读取的回调函数中执行写入和发送
问题:那么现在有一个问题,无论是同步还是异步,我们都需要先读文件,再写入,那么文件很大时,对内存的压力就会非常大。有没有什么办法,边读取边写入
2.3 node流(Stream)
Stream 是一个抽象接口,作用就是能把文件,读一点写一点。这样不就不用占很大内存了。我们来看看怎么实现的?
const fs = require('fs');
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html' });
// let resStream = fs.createReadStream('index.html');
// resStream.pipe(res);
//流是可以支持链式操作的
fs.createReadStream('index.html').pipe(res)
})
server.on('clientError', (err, socket) => {
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n')
})
server.listen(10080)
- 用fs.createReadStream('index.html')创建一个可读流。
- 用resStream.pipe(res);管道读写操作,写入响应报文
- 上面代码中我们并没有用res.end(); 发送数据 。因为默认情况下,当数据传送完毕,会自动触发'end'事件
- 流是支持链式操作的
问题:在我们解决了内存问题后,你会发现,我们index.html中是有一张图片没有加载出来的。原因很简单。因为无论发送什么请求,我们都只返回同样的操作。
2.4 node路由
我们知道在应用成协议中用URL来表示文件的位置。区分不同请求的一个重要任务就是区分路径。那么对路径的处理node中提供了一个url模块,让我们来看看吧。
const fs = require('fs');
const http = require('http');
const url = require("url");
const server = http.createServer((req, res) => {
//pathname是取到端口号后面的地址
let pathname = url.parse(req.url).pathname;
if(pathname === '/') pathname = '/index.html';
let resPath = '.' + pathname;
//判断路径是否存在
if(!fs.existsSync(resPath)){
res.writeHead(404, {'Content-Type': 'text/html'});
return res.end('<h1>404 Not Found</h1>');
}
//如果存在,将在路径下的文件返回给页面
res.writeHead(200, { 'Content-Type': 'text/html' });
fs.createReadStream(resPath).pipe(res)
})
server.on('clientError', (err, socket) => {
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n')
})
server.listen(10080)
- 我们引入了一个url模块,帮助我们去处理路径
- url.parse(req.url)是将一个路径,帮我们处理成对象,它包含我们常用的路径属性
- 其中有一个属性是pathname,就是URL端口号和参数之间的路径,也就是我们访问的路径
- 如果我们直接访问网站后面不加路径,我们给默认指向/index.html
- 相对路径访问我们给前面加一个'.'
- 然后我们用文件模块提供的existsSync方法去判断服务器上是否有这个文件
- 如果没有我们返回404,告诉没有找到文件。有就将文件返回。
问题:Content-Type是处理文件类型的,那么图片类型肯定不会是'text/html' ,虽然浏览器很智能帮我显示出来了,但是我们还是要把这样的错误改过来。
2.5 path模块判断文件类型
我们知道,只要改变 'Content-Type'的文件类型即可。
function getFileType(resPath){
const EXT_FILE_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_FILE_TYPES[path.extname(resPath)] || EXT_FILE_TYPES['default'];
return mime_type;
}
- 定义了一个getFileType函数,并给出常用的文件类型和它们Content-Type的值
- 应用了path模块,用path模块上的extname方法取出扩展名
- 然后跟定义的对象去匹配,如果没有找到,就给一个默认的值
2.5 用gzip对文件进行压缩
(1)我们先取出请求头中的accept-encoding参数,如果参数不存在,我们赋值成''
let acceptEncoding = req.headers['accept-encoding'];
if (!acceptEncoding) { acceptEncoding = '';};
(2)然后我们用正则去判断acceptEncoding是否用了gzip压缩,当然这里可以有多个判断压缩格式。这里我们只写一个。
if(/\bgzip\b/.test(acceptEncoding)){
//执行压缩,并在响应头中告诉浏览器压缩的格式
}else{
//不执行压缩
}
(3)我们需要引用zlib模块对文件进行压缩。这里我们用Gzip,就调用Gzip的方法。 然后我们对文件流先进行一步压缩,再写到响应体中。
const zlib = require('zlib')
let raw = fs.createReadStream(resPath);
raw.pipe(zlib.createGzip()).pipe(res);
(4)最后我们还需要在响应头中告诉浏览器我的文件已经给你压缩成什么格式
'Content-Encoding': gzip
然后我们开两个终端分别用启动有gzip和没有gzip压缩的
- http://localhost:8088/home.html
- http://localhost:10080/home.html
home文件中放了一张我在颐和园用相机拍的5M的图片
你可以打开多个浏览器窗口,分别先访问两个文件,可以多测几遍,你会发现有gzip压缩的明显要慢。
为什么会这样呢,道理很简单,因为我们的服务器和浏览器都在同一台电脑上,传输速度很快。所以压缩和解压的时间就被放大了。这也告诉我们并不是什么场景都适合对文件进行压缩的。
2.6 浏览器缓存协议的实现
对http浏览器缓存协议进行一个实现。
**(1)强缓存 ** 强缓存我们在响应头中给一个一周的过期时间 参考代码cache.js
Cache-Control : max-age = 604800'
- 我们可以看到在第二次刷新的时候,文件中的资源就会从浏览的缓存中取。
- 如果不想从缓存中取,可以强制刷新,或打开Disable Cache
- 强刷的时候,你再看localhost请求头中会带上 Cache-Control: no-cache
- 普通刷新资源文件会有Cache-Control: no-cache,这是因为资源文件是从缓存中取的,而Cache-Control: no-cache是你上次强刷的时候带上去的。
- 如果新打开一个窗口,再次访问同一个网页,不用从缓存中取
**(2)弱缓存 ** 参考代码cache2.js
etag需要一个双引号的字符串,然后我们把它写入响应头中
let etagStr = "dajuan"; //etag 要加双引号
res.writeHead(200, {
'Content-Type': getFileType(resPath),
'etag' : etagStr
});
当再次访问的时候我们需要判断一下,if-none-match带的值于现在etagStr值是否一致。如果一致直接返回304,不用在返回文件。浏览器看到304,就知道了要从缓存中拿。
let etagStr = "dajuan"; //etag 要加双引号
if(req.headers['if-none-match'] === etagStr){
res.writeHead(304, {
'Content-Type': getFileType(resPath),
'etag' : etagStr
});
res.end();
}
这里只是举了一个最简单的例子,真实项目中是不可能把所有的文件都返回同一个字符串的。
2.7 node处理post和get请求
(1)我们首先分别用get 和 post 写一个表单提交,让其点击都跳转到form_result.html,有一行你好,name
//form.html
<form action="form_result.html" method="get">
<p> get: <input type="text" name="name" /></p>
<input type="submit" value="Submit" />
</form>
<form action="form_result.html" method="post">
<p> post: <input type="text" name="name" /></p>
<input type="submit" value="Submit" />
</form>
//form_result.html
<div>你好,name</div>
(2)get方法去处理 参考代码method.js
let pathAll = url.parse(req.url);
let getArgument = pathAll.query; //取出参数 name=XXX
if(pathname === '/form_result.html' && getArgument != undefined){
let text = fs.readFileSync('form_result.html').toString().replace(/name/, getArgument)
fs.writeFileSync('form_result.html',text)
}
- 我们知道url.parsl()能读取url,query就是get方法带的的参数
- 当要跳转的路径是是'/form_result.html'并且getArgument有值时
- 我们用文件模块同步读取出'form_result.html'的内容
- 转换成字符串之后,在将表单中的name替换成name=XXX
这时候get提交的表单可以去处理啦,但是post的参数并没有在URL中,所以对post没有影响
(3)post方法去处理 参考代码method2.js
req.on('data',(data)=>{
let text = fs.readFileSync('form_result.html').toString().replace(/name/, 'post'+ data)
fs.writeFileSync('form_result.html',text)
})
- post方法是在请求头中监听data事件的,请求报文中,有请求体时,被触发
- 所以我们在监听到‘data’事件被触发时,我们也是执行上面操作
- 而这个时候如果发送get请求,就不会被响应
- 我们学事件知道,我们可以给‘data’绑定多个事件,而每次post请求必然会触发。这就是对服务器造成的副作用。
2.8 node处理跨域
参考代码:cors.js cors2.js
if(req.headers['origin'] ) {
res.writeHead(200, {
'Access-Control-Allow-Origin': 'http://localhost:5000',
'Content-Type': 'text/html'
});
return fs.createReadStream(resPath).pipe(res)
};
- 我们分别在本地启动了两个服务
- 让一个端口是5000,另一个端口是9088
- 我们在5000的端口访问,cors.html
- 在html中,我们ajax调用9088端口的data.json
- 这样就形成了跨域,我们允许5000端口访问,就会返回数据
- 如果我们把不填,或者不写5000端口,你会看到收不到数据
3 https与http2
3.1 https的node服务器的搭建
openssl win32(64)下载:http://slproweb.com/products/Win32OpenSSL.html
配置环境变量path
知道了原理后,我们在终端生成证书和私钥吧。
(1)openssl genrsa -out server.key 1024 //生成服务器私钥
(2)openssl rsa -in server.key -pubout -out server.pem // 生成公钥
//自己扮演CA机构,给自己服务器颁发证书,CA机构也需要自己私钥,CSR文件(证书签名请求文件),和证书
(3) openssl genrsa -out ca.key 1024 //生成CA 私钥
openssl req -new -key ca.key -out ca.csr //生成CA CSR文件
openssl x509 -req -in ca.csr -signkey ca.key -out ca.crt //生成CA 证书
//生成证书签名请求文件
(4) openssl req -new -key server.key -out server.csr //生成server CSR文件
//向自己的机构请求生成证书
(5) openssl x509 -req -CA ca.crt -CAkey ca.key -CAcreateserial -in server.csr -out server.crt //生成server 证书
注意:信息随便填,但提示里有格式要注意啊。。。
const https = require('https');
const fs = require('fs');
const options = {
key: fs.readFileSync('./key/server.key'),
cert: fs.readFileSync('./key/server.crt')
};
https.createServer(options, (req, res) => {
res.writeHead(200);
res.end('hello world\n');
}).listen(8000);
- 我们引入https模块,填好我们证书和私钥
- 剩下的代码现在看起来是不是很简单
服务器访问: https://localhost:8000/
- 这样我们访问https就能请求到网页了
- 当然会提示我们不安全,继续就好啦
- 为啥会提示我们不安全,刚才自己怎么填的证书,心里没数嘛
3.2 http2的node服务器的搭建
node的http2是试验的API。如果node版本比较低,请先升级。我的是v8.11.3
const http2 = require('http2');
const fs = require('fs');
const server = http2.createSecureServer({
key: fs.readFileSync('./key/server.key'),
cert: fs.readFileSync('./key/server.crt')
});
server.on('error', (err) => console.error(err));
server.on('stream', (stream, headers) => {
// stream is a Duplex
stream.respond({
'content-type': 'text/html',
':status': 200
});
stream.end('<h1>Hello World</h1>');
});
server.listen(8443);
- 我们还是引入https时创建的私钥和证书
- 我们创建http2的服务
- 在http2流的概念。所以我们写入请求头。并返回请求体
- 我们在浏览器*问:https://localhost:8443/
这样我们就完成了一个最简单的http2的访问。