NodeJS创建HTTP、HTTPS服务器与客户端

点击上方“前端大学”,选择“星标”

前端IT干货第一时间送达!

作者: 奋飛

超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议。所有的WWW文件都必须遵守这个标准。设计HTTP最初的目的是为了提供一种发布和接收HTML页面的方法。其属于下图七层网路协议的“应用层”。HTTP服务器创建HTTP服务器创建服务方式一:回调方式var server = http.createServer((request, response) => {// 接受客户端请求时触发...});server.listen(10000, localhost, 511, () => {// 开始监听 ...});

方式二:事件监听方式var server = http.createServer();// 接受客户端请求时触发server.on(request, (request, rsponse) => {...});server.listen(10000, localhost, 511);// 开始监听server.on(listening, () => {...});

注意:server.listen(port, [host], [backlog], [callback])中的backlog参数为整数,指定位于等待队列中客户端连接的最大数量,一旦超过这个长度,HTTP服务器将开始拒绝来自新客户端的连接,默认值为511。在HTTP请求服务器时,会发送两次请求。一次是用户发出请求,另一次是浏览器为页面在收藏夹中的显示图标(默认为favicon.ico)而自动发出的请求。

关闭服务器server.close();// 服务器关闭时会触发close事件server.on(close, () => {...});

超时server.setTimeout(60 * 1000, () => {console.log(超时了);});// 或者通过事件形式server.setTimeout(60 * 1000);server.on(timeout, () => {...});

注意:默认超时时间为2分钟

错误server.on(error, (e) => {if(e.code === EADDRINUSE) {// 端口被占用}});获取客户端请求信息当从客户端请求流中读取到数据时会触发data事件,当读取完客户端请求流中的数据时触发end事件。

Get请求server.on(request, (request, response) => {if(request.url !== /favicon.ico) {/* Get请求 */var params = url.parse(req.url, true).query;// 或者// var params = querystring.parse(url.parse(request.url).query);// 根据参数做处理// ...// 结束请求response.end();}});

Post请求server.on(request, (request, response) => {request.setEncoding(utf-8);if(request.url !== /favicon.ico) {let result = ;request.on(data, (data) => {result += data;});request.on(end, () => {var params = JSON.parse(postData);console.log(`数据接收完毕:${result}`);});// 结束本次请求response.end();}// 结束本次请求response.end(JSON.stringify({status: success}));});

转换URL字符串与查询字符串querystring模块:转换URL中的查询字符串(URL中?之后,#之前)

querystring.stringify(obj, [sep], [eq])querystring.parse(str, [sep], [eq], [option])

sep:分割符,默认&eq:分配字符,默认=options:{maxKeys: number}指定转换后对象中的属性个数let str = querystring.stringify({name: ligang, age: 27});console.log(str); // name=ligang&age=27let obj = querystring.parse(str);console.log(obj); // { name: ligang, age: 27 }

url模块:转换完整URL字符串url.parse(urlStr, [parseQueryString])

parseQueryString:如果为true,将查询字符通过querystring转换为对象;默认false。url.resolve(from, to);将二者结合成一个路径,from、to既可以是相对路径也可以是绝对路径。// ?a=1url.resolve( a?a=1); // ?a=1url.resolve( /a?a=1);

注意:具体合并规则,请查看《Node权威指南》— 8.1HTTP服务器。

var urlStr = ?name=lg&uid=1#a/b/c;console.log(url.parse(urlStr, true));/*Url {protocol: http:,slashes: true,auth: null,host: ligangblog.com,port: null,hostname: ligangblog.com,hash: #a/b/c,search: ?name=lg&uid=1,query: { name: lg, uid: 1 },pathname: /javascript/,path: /javascript/?name=lg&uid=1,href: ?name=lg&uid=1#a/b/c}*/发送服务器端响应流response.writeHead(statusCode, [reasonPhrase], [headers]);// 或者response.setHeader(name, value);响应头中包含的一些常用字段:

示例:response.writeHead(200, {Content-Type: text/plain, Access-Control-Allow-Origin: });// 或者response.statusCode = 200;response.setHeader(Content-Type, text/plain);response.setHeader(Access-Control-Allow-Origin, );

writeHead和setHeader区别:writeHead:该方法被调用时发送响应头setHeader:write方法第一次被调用时发送响应头/* 获取响应头中的某个字段值 */response.getHeader(name);/* 删除一个响应字段值 */response.removeHeader(name);/* 该属性表示响应头是否已发送 */response.headersSent;/* 在响应数据的尾部增加一个头信息 */response.addTrailers(headers);

示例:// 必须再响应头中添加Trailer字段,并且其值设置为追加的响应头中所指定的字段名response.write(200, {Content-Type: text/plain, Trailer: Content-MD5});response.write(....);response.addTrailers({Content-MD5, ...});response.end();

特别说明:

当再快速网路且数据量很小的情况下,Node将数据直接发送到操作系统内核缓存区中,然后从该内核缓存区中取出数据发送给请求方;如果网速很慢或者数据量很大,Node通常会将数据缓存在内存中,在对方可以接受数据的情况下将内存中的数据通过操作系统内核缓存区发送给请求方。

response.write返回true,说明直接写到了操作系统内核缓存区中;返回false,说明暂时缓存的内存中。每次需要通过调用response.end()来结束响应。响应超时会触发timeout事件;response.end()方法调用之前,如果连接中断,会触发close事件。

/* 设置请求超时时间2分钟 */response.setTimeout(2 * 60 * 1000, () => {console.error(请求超时!); });// 或者response.setTimout(2 * 60 * 1000);response.on(timeout, () => {console.error(请求超时!);});/* 连接中断 */response.on(close, () => {console.error(连接中断!);});/** * HTTP服务端 * Created by ligang on 17/5/28. */import http from http;var server = http.createServer();// 接受客户端请求时触发server.on(request, (request, response) => {if(request.url !== /favicon.ico) {response.setTimeout(2 * 60 * 1000, () => {console.error(请求超时!);});response.on(close, () => {console.error(请求中断!);});let result = ;request.on(data, (data) => {result += data;});request.on(end, () => {console.log(`服务器数据接收完毕:${result}`);response.statusCode = 200;response.write(收到!);response.end(); // 结束本次请求});}});server.listen(10000, localhost, 511);// 开始监听server.on(listening, () => {console.log(开始监听);});server.on(error, (e) => {if(e.code === EADDRINUSE) {console.log(端口被占用);}else {console.log(`发生错误:${e.code}`);}});

HTTP客户端Node.js可以轻松向任何网站发送请求并读取网站的响应数据。var req = http.request(options, callback);// get请求var req = http.get(options, callback);// 向目标网站发送数据req.write(chunk, [encoding]);// 结束本次请求req.end([chucnk], [encoding]);// 中止本次请求req.abort();

其中,options用于指定目标URL地址,如果该参数是一个字符串,将自动使用url模块中的parse方法转换为一个对象。注意:http.get()方法只能使用Get方式请求数据,且无需调用req.end()方法,Node.js会自动调用。

/** * HTTP客户端 * Created by ligang on 17/5/30. */import http from http;const options = {hostname: localhost,port: 10000,path: /,method: post},    req = http.request(options);req.write(你好,服务器);req.end();req.on(response, (res) => {console.log(`状态码:${res.statusCode}`);let result = ;res.on(data, (data) => {result += data;});res.on(end, () => {console.log(`客户端接受到响应:${result}`);})});req.setTimeout(60* 1000, () => {console.log(超时了);req.abort();});req.on(error, (error) => {if(error.code === ECONNERSET) {console.log(socket端口超时);}else {console.log(`发送错误:${error.code}`);}});

代理服务器/** * HTTP代理 * Created by ligang on 17/5/30. */import http from http;import url from url;/** * 服务端 */const server = http.createServer(async (req, res) => {// req.setEncoding(utf-8);/* 超时 2分钟 */res.setTimeout(2 * 60 * 1000, () => {// ...});/* 连接中断 */res.on(close, () => {// ...    });let options = {},        result = "";options = await new Promise((resolve, reject) => {if(req.method === GET) {options = url.parse(:10000 + req.url);resolve(options);}else if(req.method === POST) {req.on(data, (data) => {result += data;            });req.on(end, () => {options = url.parse(:10000 + req.url);// post请求必须制定options.headers = {content-type: application/json,};resolve(options);});}});    options.method = req.method;let content = await clientHttp(options, result ? JSON.parse(result) : result);res.setHeader(Content-Type, text/html);res.write(<html><head><meta charset="UTF-8" /></head>)res.write(content);res.write(</html>);// 结束本次请求res.end();});server.listen(10010, localhost, 511);/* 开始监听 */server.on(listening, () => {// ...});/* 监听错误 */server.on(error, (e) => {console.log(e.code);// ...});/** * 客户端 * @param options 请求参数 * @param data 请求数据 */async function clientHttp(options, data) {let output = new Promise((resolve, reject) => {let req = http.request(options, (res) => {let result = ;res.setEncoding(utf8);res.on(data, function (chunk) {result += chunk;});res.on(end, function () {resolve(result);});});req.setTimeout(60000, () => {console.error(`连接后台超时 ${options.href}`);reject();req.abort();});req.on(error, err => {console.error(`连接后台报错 ${err}`);if (err.code === ECONNRESET) {console.error(`socket超时 ${options.href}`);} else {console.error(`连接后台报错 ${err}`);}reject();req.abort();});// 存在请求数据,发送请求数据if (data) {req.write(JSON.stringify(data));}req.end();});return await output;}

注意:

POST请求必须指定headers信息,否则会报错socket hang up获取到options后需要重新指定其methodoptions.method = req.method;

HTTPS服务器HTTPS使用https协议,默认端口号44;HTTPS需要向证书授证中心申请证书;HTTPS服务器与客户端之间传输是经过SSL安全加密后的密文数据;

创建公钥、私钥及证书(1)创建私钥openssl genrsa -out privatekey.pem 1024(2)创建证书签名请求openssl req -new -key privatekey.pem -out certrequest.csr(3)获取证书,线上证书需要经过证书授证中心签名的文件;下面只创建一个学习使用证书

openssl x509 -req -in certrequest.csr -signkey privatekey.pem -out certificate.pem(4)创建pfx文件openssl pkcs12 -export -in certificate.pem -inkey privatekey.pem -out certificate.pfx

HTTPS服务创建HTTPS服务器同HTTP服务器大致相同,需要增加证书,创建HTTPS服务器时通过options参数设置。import https from https;import fs from fs;var pk = fs.readFileSync(privatekey.pem),    pc = fs.readFileSync(certificate.pem);var opts = {key: pk,cert: pc};var server = https.createServer(opts);

opts参数为一个对象,用于指定创建HTTPS服务器时配置的各种选项,下面只描述几个必要选项:

HTTPS客户端const options = {hostname: localhost,port: 1443,path: /,method: post,key: fs.readFileSync(privatekey.pem),cert: fs.readFileSync(certificate.pem),rejectUnhauthorized: false,agent: false // 从连接池中指定挑选一个当前连接状态为关闭的https.Agent},    req = https.request(options);// 或者const options = {hostname: localhost,port: 1443,path: /,method: post,key: fs.readFileSync(privatekey.pem),cert: fs.readFileSync(certificate.pem),rejectUnhauthorized: false,};// 显示指定https.Agent对象options.agent = new https.Agent(options);var req = https.request(options);

说明: 普通的 HTTPS 服务中,服务端不验证客户端的证书(但是需要携带证书),中间人可以作为客户端与服务端成功完成 TLS 握手; 但是中间人没有证书私钥,无论如何也无法伪造成服务端跟客户端建立 TLS 连接。当然如果你拥有证书私钥,代理证书对应的 HTTPS 网站当然就没问题了,所以这里的私钥和公钥只是格式书写,没有太大意义,只要将请求回来的数据原原本本交给浏览器来解析就算完成任务。 

觉得本文对你有帮助?请分享给更多人!

关注「前端大学」,提升前端技能!

分享前端好文,点个 在看