NodeJS创建HTTP、HTTPS服务器与客户端
bigegpt 2024-10-08 00:54 55 浏览
超文本传输协议(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=27 let 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既可以是相对路径也可以是绝对路径。
// http://ligangblog.com/javascript/a?a=1 url.resolve('http://ligangblog.com/javascript/', 'a?a=1'); // http://ligangblog.com/a?a=1 url.resolve('http://ligangblog.com/javascript/', '/a?a=1');
注意:具体合并规则,请查看《Node权威指南》— 8.1HTTP服务器。
var urlStr = 'http://ligangblog.com/javascript/?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: 'http://ligangblog.com/javascript/?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': 'http://localhost'}); // 或者 response.statusCode = 200; response.setHeader('Content-Type', 'text/plain'); response.setHeader('Access-Control-Allow-Origin', 'http://localhost');
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('http://localhost:10000' + req.url); resolve(options); }else if(req.method === 'POST') { req.on('data', (data) => { result += data; }); req.on('end', () => { options = url.parse('http://localhost: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 网站当然就没问题了,所以这里的私钥和公钥只是格式书写,没有太大意义,只要将请求回来的数据原原本本交给浏览器来解析就算完成任务。
相关推荐
- pyproject.toml到底是什么东西?(py trim)
-
最近,在Twitter上有一个Python项目的维护者,他的项目因为构建失败而出现了一些bug(这个特别的项目不提供wheel,只提供sdist)。最终,发现这个bug是由于这个项目使用了一个pypr...
- BDP服务平台SDK for Python3发布(bdp数据平台)
-
下载地址https://github.com/imysm/opends-sdk-python3.git说明最近在开发和bdp平台有关的项目,用到了bdp的python的sdk,但是官方是基于p...
- Python-for-Android (p4a):(python-for-android p4a windows)
-
一、Python-for-Android(p4a)简介Python-for-Android(p4a),一个强大的开发工具,能够将你的Python应用程序打包成可在Android设备上运行...
- Qt for Python—Qt Designer 概览
-
前言本系列第三篇文章(QtforPython学习笔记—应用程序初探)、第四篇文章(QtforPython学习笔记—应用程序再探)中均是使用纯代码方式来开发PySide6GUI应用程序...
- Python:判断质数(jmu-python-判断质数)
-
#Python:判断质数defisPrime(n):foriinrange(2,n):ifn%i==0:return0re...
- 为什么那么多人讨厌Python(为什么python这么难)
-
Python那么棒,为什么那么多人讨厌它呢?我整理了一下,主要有这些原因:用缩进替代大括号许多人抱怨Python完全依赖于缩进来创建代码块,代码多一点就很难看到函数在哪里结束,那么你就需要把一个函数拆...
- 一文了解 Python 中带有 else 的循环语句 for-else/while-else
-
在本文中,我们将向您介绍如何在python中使用带有else的for/while循环语句。可能许多人对循环和else一起使用感到困惑,因为在if-else选择结构中else正常...
- python的numpy向量化语句为什么会比for快?
-
我们先来看看,python之类语言的for循环,和其它语言相比,额外付出了什么。我们知道,python是解释执行的。举例来说,执行x=1234+5678,对编译型语言,是从内存读入两个shor...
- 开眼界!Python遍历文件可以这样做
-
来源:【公众号】Python技术Python对于文件夹或者文件的遍历一般有两种操作方法,一种是至二级利用其封装好的walk方法操作:import osfor root,d...
- 告别简单format()!Python Formatter类让你的代码更专业
-
Python中Formatter类是string模块中的一个重要类,它实现了Python字符串格式化的底层机制,允许开发者创建自定义的格式化行为。通过深入理解Formatter类的工作原理和使用方法,...
- python学习——038如何将for循环改写成列表推导式
-
在Python里,列表推导式是一种能够简洁生成列表的表达式,可用于替换普通的for循环。下面是列表推导式的基本语法和常见应用场景。基本语法result=[]foriteminite...
- 详谈for循环和while循环的区别(for循环语句与while循环语句有什么区别)
-
初九,潜龙勿用在刚开始使用python循环语句时,经常会遇到for循环和while循环的混用,不清楚该如何选择;今天就对这2个循环语句做深入的分析,让大家更好地了解这2个循环语句以方便后续学习的加深。...
- Python编程基础:循环结构for和while
-
Python中的循环结构包括两个,一是遍历循环(for循环),一是条件循环(while循环)。遍历循环遍历循环(for循环)会挨个访问序列或可迭代对象的元素,并执行里面的代码块。foriinra...
- 学习编程第154天 python编程 for循环输出菱形图
-
今天学习的是刘金玉老师零基础Python教程第38期,主要内容是python编程for循环输出菱形※。(一)利用for循环输出菱形形状的*号图形1.思路:将菱形分解为上下两个部分三角形图案,分别利用...
- python 10个堪称完美的for循环实践
-
在Python中,for循环的高效使用能显著提升代码性能和可读性。以下是10个堪称完美的for循环实践,涵盖数据处理、算法优化和Pythonic编程风格:1.遍历列表同时获取索引(enumerate...
- 一周热门
- 最近发表
-
- pyproject.toml到底是什么东西?(py trim)
- BDP服务平台SDK for Python3发布(bdp数据平台)
- Python-for-Android (p4a):(python-for-android p4a windows)
- Qt for Python—Qt Designer 概览
- Python:判断质数(jmu-python-判断质数)
- 为什么那么多人讨厌Python(为什么python这么难)
- 一文了解 Python 中带有 else 的循环语句 for-else/while-else
- python的numpy向量化语句为什么会比for快?
- 开眼界!Python遍历文件可以这样做
- 告别简单format()!Python Formatter类让你的代码更专业
- 标签列表
-
- mybatiscollection (79)
- mqtt服务器 (88)
- keyerror (78)
- c#map (65)
- xftp6 (83)
- bt搜索 (75)
- c#var (76)
- xcode-select (66)
- mysql授权 (74)
- 下载测试 (70)
- linuxlink (65)
- pythonwget (67)
- androidinclude (65)
- libcrypto.so (74)
- linux安装minio (74)
- ubuntuunzip (67)
- vscode使用技巧 (83)
- secure-file-priv (67)
- vue阻止冒泡 (67)
- jquery跨域 (68)
- php写入文件 (73)
- kafkatools (66)
- mysql导出数据库 (66)
- jquery鼠标移入移出 (71)
- 取小数点后两位的函数 (73)