(掌握)Http和Https的区别?
http协议和https协议的区别:传输信息安全性不同、连接方式不同、端口不同、证书申请方式不同
一、传输信息安全性不同
1、http协议:是超文本传输协议,信息是明文传输。如果攻击者截取了Web浏览器和网站服务器之间的传输报文,就可以直接读懂其中的信息。
2、https协议:是具有安全性的ssl加密传输协议,为浏览器和服务器之间的通信加密,确保数据传输的安全。
二、连接方式不同
1、http协议:http的连接很简单,是无状态的。
2、https协议:是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议。 三、端口不同
1、http协议:使用的端口是80。
2、https协议:使用的端口是443.
四、证书申请方式不同
1、http协议:免费申请。
2、https协议:需要到ca申请证书,一般免费证书很少,需要交费。
(掌握)HTTP与TCP的关系,介绍一下TCP的三次握手,能否为两次握手,为什么?四次挥手
HTTP是基于TCP协议的,所以每次都是客户端发送请求,服务器应答,但是TCP还可以给其他应用层提供服务,即可能A、B在建立链接之后,谁都可能先开始通信。
要知道TCP是全双工的,即客户端在给服务器端发送信息的同时,服务器端也可以给客户端发送信息。而半双工的意思是A可以给B发,B也可以给A发,但是A在给B发的时候,B不能给A发,即不同时,为半双工。 单工为只能A给B发,B不能给A发; 或者是只能B给A发,不能A给B发。
TCP三次握手:
第一次握手:客户端发送一个TCP的SYN(同步,代表开始会话请求)标志位置1的包指明客户打算连接的服务器的端口,以及初始序号X,保存在包头的序列号(Sequence Number)字段里
第二次握手 :服务器发回确认包(ACK)应答。即SYN标志位和ACK标志位均为1同时,将确认序号(Acknowledgement Number)设置为客户的I S N加1以.即X+1。
第三次握手:客户端再次发送确认包(ACK) SYN标志位为0,ACK标志位为1.并且把服务器发来ACK的序号字段+1,放在确定字段中发送给对方.并且在数据段放写ISN的+1
如果两次,那么服务器无法确定服务器的信息客户端是否能收到,所以如果服务器先发送数据,可能后面的客户端都收不到,会出现问题 。
TCP四次挥手:
第一次挥手:首先客户端想要释放连接,向服务器发送一段报文(FIN:结束会话);
第二次挥手:服务器接收到客户端发送过来的报文,确定了客户端要释放连接;随后服务器进入CLOSE-WAIT阶段(半关闭状态)并返回一段报文给客户端(ASK:应答);
第三次挥手:服务器自从发送了ASK(应答)后,经过CLOSE-WAIT阶段(在这个阶段把没有发送完的数据先完成发送),做好了释放服务器端到客户端方向上的连接,再次向客户端发送报文(FIN:结束会话)
第四次挥手:客户端接收到服务器端发送的释放连接的报文,确定服务器做好释放连接的准备,最后客户端再次向服务器发送一段报文(ASK:应答)
(掌握)HTTP method
一台服务器要与
HTTP1.1
兼容,只要为资源实现GET
和HEAD
方法即可。GET
是最常用的方法,通常用于请求服务器
发送某个资源。HEAD
与GET
类似,但服务器在响应中值返回首部,不返回实体的主体部分。PUT
让服务器用请求的主体部分来创建一个由所请求的URL
命名的新文档,或者,如果那个URL
已经存在的话,就用干这个主体替代它。POST
起初是用来向服务器输入数据的。实际上,通常会用它来支持HTML的表单。表单中填好的数据通常会被送给服务器,然后由服务器将其发送到要去的地方。TRACE
会在目的服务器端发起一个环回诊断
,最后一站的服务器会弹回一个TRACE
响应并在响应主体中携带它收到的原始请求报文
。TRACE
方法主要用于诊断,用于验证请求是否如愿穿过了请求/响应链
。OPTIONS
方法请求web
服务器告知其支持的各种功能。可以查询服务器支持哪些方法或者对某些特殊资源支持哪些方法。DELETE
请求服务器删除请求URL
指定的资源。
HTTP状态码及其含义
举例状态码类型:
状态码 | 类别 | 原因短语 |
---|---|---|
1XX | Information(信息性状态码) | 接收的请求正在处理 |
2XX | Success(成功状态码) | 请求正常处理完毕 |
3XX | Redirection(重定向状态码) | 需要进行附加的操作以完成请求 |
4XX | Client Error(客户端错误状态码) | 服务器无法处理请求 |
5XX | Server Error(服务端错误状态码) | 服务器处理请求出错 |
204 | 服务器成功处理,但未返回内容。 | |
304 | Not Modified 未修改。 | 所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源 |
400 | Bad Request | 客户端请求的语法错误,服务器无法理解 |
403 | Forbidden | 服务器理解请求客户端的请求,但是拒绝执行此请求 |
404 | Not Found | 服务器无法根据客户端的请求找到资源(网页)。通过此代码,网站设计人员可设置"您所请求的资源无法找到"的个性页面 |
(掌握)为什么JavaScript是单线程?
- 防止DOM渲染冲突的问题;
- Html5中的Web Worker可以实现多线程
同步和异步任务
- 同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
- 异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
(掌握)什么是事件循环(EventLoop)?
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。
(掌握)什么是内存泄漏,以及常见内存泄漏的原因,和排查的方法
1、概念
内存泄漏(Memory Leak)指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。如果内存泄漏的位置比较关键,那么随着处理的进行可能持有越来越多的无用内存,这些无用的内存变多会引起服务器响应速度变慢。
2、原因
严重的情况下导致内存达到某个极限(可能是进程的上限,如 v8 的上限;也可能是系统可提供的内存上限)会使得应用程序崩溃。常见内存泄漏的原因内存泄漏的几种情况:
-
全局变量
全局变量直接挂在 root 对象上,不会被清除掉。
-
闭包
闭包会引用到父级函数中的变量,如果闭包未释放,就会导致内存泄漏。
-
事件监听
Node.js 的事件监听也可能出现的内存泄漏。例如对同一个事件重复监听,忘记移除(removeListener),将造成内存泄漏。这种情况很容易在复用对象上添加事件时出现。
3、排查方法
- 想要定位内存泄漏,通常会有两种情况:
- 对于只要正常使用就可以重现的内存泄漏,这是很简单的情况只要在测试环境模拟就可以排查了。
- 对于偶然的内存泄漏,一般会与特殊的输入有关系。想稳定重现这种输入是很耗时的过程。如果不能通过代码的日志定位到这个特殊的输入,那么推荐去生产环境打印内存快照了。
- 需要注意的是,打印内存快照是很耗 CPU 的操作,可能会对线上业务造成影响。快照工具推荐使用 heapdump 用来保存内存快照,使用 devtool 来查看内存快照。
- 使用 heapdump 保存内存快照时,只会有 Node.js 环境中的对象,不会受到干扰(如果使用 node-inspector 的话,快照中会有前端的变量干扰)。
(掌握)简述ajax原理和XmlHttpRequest对象
Ajax的原理简单来说通过XmlHttpRequest对象来向服务器发送异步请求,从服务器获得数据,然后用javascript来操作DOM而更新页面。这其中最关键的一步就是从服务器获得请求数据。要清楚这个过程和原理,我们必须对 XMLHttpRequest有所了解。
XMLHttpRequest是ajax的核心机制,它是在IE5中首先引入的,是一种支持异步请求的技术。简单的说,也就是javascript可以及时向服务器提出请求和处理响应,而不阻塞用户。达到无刷新的效果。
#(掌握)简述JWT(JSON Web Token)鉴权的原理
(了解)一个tcp接连能发几个http请求?
- http 1.0 默认一次tcp连接进行一次http请求之后就会断开(由于资源浪费,有些服务器支持通过配置支持多次)
- http 1.1 请求头配置:Connection:keep-alive = true,只要tcp连接不断开(默认2小时),一直可以进行http请求,但是一个tcp连接同一时间只支持一个http请求 Connection:keep-alive = false,只能发一次http请求
- http 2.0:多路复用技术Multiplexing,一个tcp可以并发多个http请求(理论无上限,但是一般浏览器会有tcp并发数的限制)‘
(了解)NodeJs中间件原理
中间件其实就是一个函数
function(req,res,next){ req:请求对象 res:响应对象 next:下一个中间件 }
简单描述
- 洋葱圈模型,就是说中间件执行就像洋葱一样,最早use的中间件,就放在最外层。处理顺序从左到右,左边接收一个request,右边输出返回response
- 一般的中间件都会执行两次,调用next之前为第一次,调用next时把控制传递给下游的下一个中间件。当下游不再有中间件或者没有执行next函数时,就将依次恢复上游中间件的行为,让上游中间件执行next之后的代码
(洋葱圈模型,了解一下)
(了解)Express如何使用中间件?
三种方式:
1、app.use('pathname',中间件) :
pathname不写: 任何请求路径都会执行这个中间件
pathname写了:任何以pathname开头的请求路径都会执行这个中间件
2、app.get('pathname',中间件) :
请求路径为pathname的get请求会执行这个中间件
3、app.post('pathname',中间件) :
请求路径为pathname的post请求会执行这个中间件
(了解)cookies机制和session机制的区别
1、cookies数据保存在客户端。session数据保存在服务端
2、cookies可以减轻服务器压力,但是不安全,容易进行cookies欺骗
3、session安全一点,但是占用服务器资源。
(了解)你知道哪些http头部?
HTTP Request Header 请求头
Accept:指定客户端能够接收的内容类型。
Accept-Charset:浏览器可以接受的字符编码集。
Accept-Encoding:指定浏览器可以支持的web服务器返回内容压缩编码类型。
Accept-Language:浏览器可接受的语言。
Cache-Control:指定请求和响应遵循的缓存机制。
Connection:表示是否需要持久连接。(HTTP 1.1默认进行持久连接)
CookieHTTP:请求发送时,会把保存在该请求域名下的所有cookie值一起发送给web服务器。
Content-Length:请求的内容长度。
Content-Type:请求的与实体对应的MIME信息。
Date:请求发送的日期和时间。
From:发出请求的用户的Email。
Host:指定请求的服务器的域名和端口号。
Referer:先前网页的地址,当前请求网页紧随其后,即来路。
User-Agent:的内容包含发出请求的用户信息。
1、Node 模块机制
1.1 请介绍一下 node 里的模块是什么
Node 中,每个文件模块都是一个对象,它的定义如下:
function Module(id, parent) { this.id = id; this.exports = {}; this.parent = parent; this.filename = null; this.loaded = false; this.children = [];}module.exports = Module;var module = new Module(filename, parent);
所有的模块都是 Module 的实例。可以看到,当前模块(module.js)也是 Module 的一个实例。
1.2 请介绍一下 require 的模块加载机制
这道题基本上就可以了解到面试者对 Node 模块机制的了解程度 基本上面试提到
- 1、先计算模块路径
- 2、如果模块在缓存里面,取出缓存
- 3、加载模块
- 4、的输出模块的 exports 属性即可
// require 其实内部调用 Module._load 方法Module._load = function(request, parent, isMain) { // 计算绝对路径 var filename = Module._resolveFilename(request, parent); // 第一步:如果有缓存,取出缓存 var cachedModule = Module._cache[filename]; if (cachedModule) { return cachedModule.exports; // 第二步:是否为内置模块 if (NativeModule.exists(filename)) { return NativeModule.require(filename); } /********************************这里注意了**************************/ // 第三步:生成模块实例,存入缓存 // 这里的Module就是我们上面的1.1定义的Module var module = new Module(filename, parent); Module._cache[filename] = module; /********************************这里注意了**************************/ // 第四步:加载模块 // 下面的module.load实际上是Module原型上有一个方法叫Module.prototype.load try { module.load(filename); hadException = false; } finally { if (hadException) { delete Module._cache[filename]; } } // 第五步:输出模块的exports属性 return module.exports;};
接着上一题继续发问
1.3 加载模块时,为什么每个模块都有__dirname,__filename 属性呢,new Module 的时候我们看到 1.1 部分没有这两个属性的,那么这两个属性是从哪里来的
// 上面(1.2部分)的第四步module.load(filename)// 这一步,module模块相当于被包装了,包装形式如下// 加载js模块,相当于下面的代码(加载node模块和json模块逻辑不一样)(function (exports, require, module, __filename, __dirname) { // 模块源码 // 假如模块代码如下 var math = require('math'); exports.area = function(radius){ return Math.PI * radius * radius }});
也就是说,每个 module 里面都会传入__filename, __dirname 参数,这两个参数并不是 module 本身就有的,是外界传入的
1.4 我们知道 node 导出模块有两种方式,一种是 exports.xxx=xxx 和 Module.exports={}有什么区别吗
- exports 其实就是 module.exports
- 其实 1.3 问题的代码已经说明问题了,接着我引用廖雪峰大神的讲解,希望能讲的更清楚
module.exports vs exports很多时候,你会看到,在Node环境中,有两种方法可以在一个模块中输出变量:方法一:对module.exports赋值:// hello.jsfunction hello() { console.log('Hello, world!');}function greet(name) { console.log('Hello, ' + name + '!');}module.exports = { hello: hello, greet: greet};方法二:直接使用exports:// hello.jsfunction hello() { console.log('Hello, world!');}function greet(name) { console.log('Hello, ' + name + '!');}function hello() { console.log('Hello, world!');}exports.hello = hello;exports.greet = greet;但是你不可以直接对exports赋值:// 代码可以执行,但是模块并没有输出任何变量:exports = { hello: hello, greet: greet};如果你对上面的写法感到十分困惑,不要着急,我们来分析Node的加载机制:首先,Node会把整个待加载的hello.js文件放入一个包装函数load中执行。在执行这个load()函数前,Node准备好了module变量:var module = { id: 'hello', exports: {}};load()函数最终返回module.exports:var load = function (exports, module) { // hello.js的文件内容 ... // load函数返回: return module.exports;};var exportes = load(module.exports, module);也就是说,默认情况下,Node准备的exports变量和module.exports变量实际上是同一个变量,并且初始化为空对象{},于是,我们可以写:exports.foo = function () { return 'foo'; };exports.bar = function () { return 'bar'; };也可以写:module.exports.foo = function () { return 'foo'; };module.exports.bar = function () { return 'bar'; };换句话说,Node默认给你准备了一个空对象{},这样你可以直接往里面加东西。但是,如果我们要输出的是一个函数或数组,那么,只能给module.exports赋值:module.exports = function () { return 'foo'; };给exports赋值是无效的,因为赋值后,module.exports仍然是空对象{}。结论如果要输出一个键值对象{},可以利用exports这个已存在的空对象{},并继续在上面添加新的键值;如果要输出一个函数或数组,必须直接对module.exports对象赋值。所以我们可以得出结论:直接对module.exports赋值,可以应对任何情况:module.exports = { foo: function () { return 'foo'; }};或者:module.exports = function () { return 'foo'; };最终,我们强烈建议使用module.exports = xxx的方式来输出模块变量,这样,你只需要记忆一种方法。
2、Node 的异步 I/O
本章的答题思路大多借鉴于朴灵大神的《深入浅出的 NodeJS》
2.1 请介绍一下 Node 事件循环的流程
在进程启动时,Node 便会创建一个类似于 while(true)的循环,每执行一次循环体的过程我们成为 Tick。
每个 Tick 的过程就是查看是否有事件待处理。如果有就取出事件及其相关的回调函数。然后进入下一个循环,如果不再有事件处理,就退出进程。
2.2 在每个 tick 的过程中,如何判断是否有事件需要处理呢?
每个事件循环中有一个或者多个观察者,而判断是否有事件需要处理的过程就是向这些观察者询问是否有要处理的事件。
在 Node 中,事件主要来源于网络请求、文件的 I/O 等,这些事件对应的观察者有文件 I/O 观察者,网络 I/O 的观察者。
事件循环是一个典型的生产者/消费者模型。异步 I/O,网络请求等则是事件的生产者,源源不断为 Node 提供不同类型的事件,这些事件被传递到对应的观察者那里,事件循环则从观察者那里取出事件并处理。
在 windows 下,这个循环基于 IOCP 创建,在*nix 下则基于多线程创建
2.3 请描述一下整个异步 I/O 的流程
3、V8 的垃圾回收机制
3.1 如何查看 V8 的内存使用情况
使用 process.memoryUsage(),返回如下
{ rss: 4935680, heapTotal: 1826816, heapUsed: 650472, external: 49879}
heapTotal 和 heapUsed 代表 V8 的内存使用情况。external 代表 V8 管理的,绑定到 Javascript 的 C++对象的内存使用情况。rss, 驻留集大小, 是给这个进程分配了多少物理内存(占总分配内存的一部分) 这些物理内存中包含堆,栈,和代码段。
3.2 V8 的内存限制是多少,为什么 V8 这样设计
64 位系统下是 1.4GB, 32 位系统下是 0.7GB。因为 1.5GB 的垃圾回收堆内存,V8 需要花费 50 毫秒以上,做一次非增量式的垃圾回收甚至要 1 秒以上。这是垃圾回收中引起 Javascript 线程暂停执行的事件,在这样的花销下,应用的性能和影响力都会直线下降。
3.3 V8 的内存分代和回收算法请简单讲一讲
在 V8 中,主要将内存分为新生代和老生代两代。新生代中的对象存活时间较短的对象,老生代中的对象存活时间较长,或常驻内存的对象。
3.3.1 新生代
新生代中的对象主要通过 Scavenge 算法进行垃圾回收。这是一种采用复制的方式实现的垃圾回收算法。它将堆内存一份为二,每一部分空间成为 semispace。在这两个 semispace 空间中,只有一个处于使用中,另一个处于闲置状态。处于使用状态的 semispace 空间称为 From 空间,处于闲置状态的空间称为 To 空间。
当开始垃圾回收的时候,会检查 From 空间中的存活对象,这些存活对象将被复制到 To 空间中,而非存活对象占用的空间将会被释放。完成复制后,From 空间和 To 空间发生角色对换。
应为新生代中对象的生命周期比较短,就比较适合这个算法。
当一个对象经过多次复制依然存活,它将会被认为是生命周期较长的对象。这种新生代中生命周期较长的对象随后会被移到老生代中。
3.3.2 老生代
老生代主要采取的是标记清除的垃圾回收算法。与 Scavenge 复制活着的对象不同,标记清除算法在标记阶段遍历堆中的所有对象,并标记活着的对象,只清理死亡对象。活对象在新生代中只占叫小部分,死对象在老生代中只占较小部分,这是为什么采用标记清除算法的原因。
3.3.3 标记清楚算法的问题
主要问题是每一次进行标记清除回收后,内存空间会出现不连续的状态
- 这种内存碎片会对后续内存分配造成问题,很可能出现需要分配一个大对象的情况,这时所有的碎片空间都无法完成此次分配,就会提前触发垃圾回收,而这次回收是不必要的。
- 为了解决碎片问题,标记整理被提出来。就是在对象被标记死亡后,在整理的过程中,将活着的对象往一端移动,移动完成后,直接清理掉边界外的内存。
3.3.4 哪些情况会造成 V8 无法立即回收内存
闭包和全局变量
3.3.5 请谈一下内存泄漏是什么,以及常见内存泄漏的原因,和排查的方法
什么是内存泄漏
- 内存泄漏(Memory Leak)指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。
- 如果内存泄漏的位置比较关键,那么随着处理的进行可能持有越来越多的无用内存,这些无用的内存变多会引起服务器响应速度变慢。
- 严重的情况下导致内存达到某个极限(可能是进程的上限,如 v8 的上限;也可能是系统可提供的内存上限)会使得应用程序崩溃。常见内存泄漏的原因 内存泄漏的几种情况:
一、全局变量
a = 10;//未声明对象。global.b = 11;//全局变量引用这种比较简单的原因,全局变量直接挂在 root 对象上,不会被清除掉。
二、闭包
function out() { const bigData = new Buffer(100); inner = function () { }}
闭包会引用到父级函数中的变量,如果闭包未释放,就会导致内存泄漏。上面例子是 inner 直接挂在了 root 上,那么每次执行 out 函数所产生的 bigData 都不会释放,从而导致内存泄漏。需要注意的是,这里举得例子只是简单的将引用挂在全局对象上,实际的业务情况可能是挂在某个可以从 root 追溯到的对象上导致的。三、事件监听Node.js 的事件监听也可能出现的内存泄漏。例如对同一个事件重复监听,忘记移除(removeListener),将造成内存泄漏。这种情况很容易在复用对象上添加事件时出现,所以事件重复监听可能收到如下警告:
emitter.setMaxListeners() to increase limit
例如,Node.js 中 Agent 的 keepAlive 为 true 时,可能造成的内存泄漏。当 Agent keepAlive 为 true 的时候,将会复用之前使用过的 socket,如果在 socket 上添加事件监听,忘记清除的话,因为 socket 的复用,将导致事件重复监听从而产生内存泄漏。原理上与前一个添加事件监听的时候忘了清除是一样的。在使用 Node.js 的 http 模块时,不通过 keepAlive 复用是没有问题的,复用了以后就会可能产生内存泄漏。所以,你需要了解添加事件监听的对象的生命周期,并注意自行移除。排查方法想要定位内存泄漏,通常会有两种情况:
对于只要正常使用就可以重现的内存泄漏,这是很简单的情况只要在测试环境模拟就可以排查了。
对于偶然的内存泄漏,一般会与特殊的输入有关系。想稳定重现这种输入是很耗时的过程。如果不能通过代码的日志定位到这个特殊的输入,那么推荐去生产环境打印内存快照了。
需要注意的是,打印内存快照是很耗 CPU 的操作,可能会对线上业务造成影响。快照工具推荐使用 heapdump 用来保存内存快照,使用 devtool 来查看内存快照。
使用 heapdump 保存内存快照时,只会有 Node.js 环境中的对象,不会受到干扰(如果使用 node-inspector 的话,快照中会有前端的变量干扰)。
PS:安装 heapdump 在某些 Node.js 版本上可能出错,建议使用 npm install heapdump -target=Node.js 版本来安装。
4、Buffer 模块
4.1 新建 Buffer 会占用 V8 分配的内存吗
不会,Buffer 属于堆外内存,不是 V8 分配的。
4.2 Buffer.alloc 和 Buffer.allocUnsafe 的区别
Buffer.allocUnsafe 创建的 Buffer 实例的底层内存是未初始化的。新创建的 Buffer 的内容是未知的,可能包含敏感数据。使用 Buffer.alloc() 可以创建以零初始化的 Buffer 实例。
4.3 Buffer 的内存分配机制
为了高效的使用申请来的内存,Node 采用了 slab 分配机制。slab 是一种动态的内存管理机制。Node 以 8kb 为界限来来区分 Buffer 为大对象还是小对象,如果是小于 8kb 就是小 Buffer,大于 8kb 就是大 Buffer。例如第一次分配一个 1024 字节的 Buffer,Buffer.alloc(1024),那么这次分配就会用到一个 slab,接着如果继续 Buffer.alloc(1024),那么上一次用的 slab 的空间还没有用完,因为总共是 8kb,1024+1024 = 2048 个字节,没有 8kb,所以就继续用这个 slab 给 Buffer 分配空间。如果超过 8kb,那么直接用 C++底层地宫的 SlowBuffer 来给 Buffer 对象提供空间。
4.4 Buffer 乱码问题
例如一个份文件 test.md 里的内容如下:
床前明月光,疑是地上霜,举头望明月,低头思故乡
我们这样读取就会出现乱码:
var rs = require('fs').createReadStream('test.md', {highWaterMark: 11});// 床前明???光,疑???地上霜,举头???明月,???头思故乡
一般情况下,只需要设置 rs.setEncoding('utf8')即可解决乱码问题
5、webSocket
5.1 webSocket 与传统的 http 有什么优势
- 客户端与服务器只需要一个 TCP 连接,比 http 长轮询使用更少的连接
- webSocket 服务端可以推送数据到客户端
- 更轻量的协议头,减少数据传输量
5.2 webSocket 协议升级时什么,能简述一下吗?
首先,WebSocket 连接必须由浏览器发起,因为请求协议是一个标准的 HTTP 请求,格式如下:
GET ws://localhost:3000/ws/chat HTTP/1.1Host: localhostUpgrade: websocketConnection: UpgradeOrigin: http://localhost:3000Sec-WebSocket-Key: client-random-stringSec-WebSocket-Version: 13
该请求和普通的 HTTP 请求有几点不同:
- GET 请求的地址不是类似/path/,而是以 ws://开头的地址;
- 请求头 Upgrade: websocket 和 Connection: Upgrade 表示这个连接将要被转换为 WebSocket 连接;
- Sec-WebSocket-Key 是用于标识这个连接,并非用于加密数据;
- Sec-WebSocket-Version 指定了 WebSocket 的协议版本。
随后,服务器如果接受该请求,就会返回如下响应:
HTTP/1.1 101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: server-random-string
该响应代码 101 表示本次连接的 HTTP 协议即将被更改,更改后的协议就是 Upgrade: websocket 指定的 WebSocket 协议。
6、https
6.1 https 用哪些端口进行通信,这些端口分别有什么用
- 443 端口用来验证服务器端和客户端的身份,比如验证证书的合法性
- 80 端口用来传输数据(在验证身份合法的情况下,用来数据传输)
6.2 身份验证过程中会涉及到密钥, 对称加密,非对称加密,摘要的概念,请解释一下
密钥:密钥是一种参数,它是在明文转换为密文或将密文转换为明文的算法中输入的参数。密钥分为对称密钥与非对称密钥,分别应用在对称加密和非对称加密上。
对称加密:对称加密又叫做私钥加密,即信息的发送方和接收方使用同一个密钥去加密和解密数据。对称加密的特点是算法公开、加密和解密速度快,适合于对大数据量进行加密,常见的对称加密算法有 DES、3DES、TDEA、Blowfish、RC5 和 IDEA。
非对称加密:非对称加密也叫做公钥加密。非对称加密与对称加密相比,其安全性更好。对称加密的通信双方使用相同的密钥,如果一方的密钥遭泄露,那么整个通信就会被破解。而非对称加密使用一对密钥,即公钥和私钥,且二者成对出现。私钥被自己保存,不能对外泄露。公钥指的是公共的密钥,任何人都可以获得该密钥。用公钥或私钥中的任何一个进行加密,用另一个进行解密。
摘要:摘要算法又称哈希/散列算法。它通过一个函数,把任意长度的数据转换为一个长度固定的数据串(通常用 16 进制的字符串表示)。算法不可逆。
6.3 为什么需要 CA 机构对证书签名
如果不签名会存在中间人攻击的风险,签名之后保证了证书里的信息,比如公钥、服务器信息、企业信息等不被篡改,能够验证客户端和服务器端的“合法性”。
6.4 https 验证身份也就是 TSL/SSL 身份验证的过程
简要图解如下
7、进程通信
7.1 请简述一下 node 的多进程架构
面对 node 单线程对多核 CPU 使用不足的情况,Node 提供了 child_process 模块,来实现进程的复制,node 的多进程架构是主从模式,如下所示:
[图片上传中...(image-18d2fa-1621776695856-3)]
var fork = require('child_process').fork;var cpus = require('os').cpus();for(var i = 0; i < cpus.length; i++){ fork('./worker.js');}
在 linux 中,我们通过 ps aux | grep worker.js 查看进程
这就是著名的主从模式,Master-Worker
7.2 请问创建子进程的方法有哪些,简单说一下它们的区别
创建子进程的方法大致有:
- spawn():启动一个子进程来执行命令
- exec(): 启动一个子进程来执行命令,与 spawn()不同的是其接口不同,它有一个回调函数获知子进程的状况
- execFlie(): 启动一个子进程来执行可执行文件
- fork(): 与 spawn()类似,不同电在于它创建 Node 子进程需要执行 js 文件
- spawn()与 exec()、execFile()不同的是,后两者创建时可以指定 timeout 属性设置超时时间,一旦创建的进程超过设定的时间就会被杀死
- exec()与 execFile()不同的是,exec()适合执行已有命令,execFile()适合执行文件。
7.3 请问你知道 spawn 在创建子进程的时候,第三个参数有一个 stdio 选项吗,这个选项的作用是什么,默认的值是什么。
- 选项用于配置在父进程和子进程之间建立的管道。
- 默认情况下,子进程的 stdin、 stdout 和 stderr 会被重定向到 ChildProcess 对象上相应的 subprocess.stdin、subprocess.stdout 和 subprocess.stderr 流。
- 这相当于将 options.stdio 设置为 ['pipe', 'pipe', 'pipe']。
7.4 请问实现一个 node 子进程被杀死,然后自动重启代码的思路
- 在创建子进程的时候就让子进程监听 exit 事件,如果被杀死就重新 fork 一下
var createWorker = function(){ var worker = fork(__dirname + 'worker.js') worker.on('exit', function(){ console.log('Worker' + worker.pid + 'exited'); // 如果退出就创建新的worker createWorker() })}
7.5 在 7.4 的基础上,实现限量重启,比如我最多让其在 1 分钟内重启 5 次,超过了就报警给运维
- 思路大概是在创建 worker 的时候,就判断创建的这个 worker 是否在 1 分钟内重启次数超过 5 次
- 所以每一次创建 worker 的时候都要记录这个 worker 创建时间,放入一个数组队列里面,每次创建 worker 都去取队列里前 5 条记录
- 如果这 5 条记录的时间间隔小于 1 分钟,就说明到了报警的时候了
7.6 如何实现进程间的状态共享,或者数据共享
我自己没用过 Kafka 这类消息队列工具,问了 java,可以用类似工具来实现进程间通信,更好的方法欢迎留言
8、中间件
8.1 如果使用过 koa、egg 这两个 Node 框架,请简述其中的中间件原理,最好用代码表示一下
- 上面是在网上找的一个示意图,就是说中间件执行就像洋葱一样,最早 use 的中间件,就放在最外层。处理顺序从左到右,左边接收一个 request,右边输出返回 response
- 一般的中间件都会执行两次,调用 next 之前为第一次,调用 next 时把控制传递给下游的下一个中间件。当下游不再有中间件或者没有执行 next 函数时,就将依次恢复上游中间件的行为,让上游中间件执行 next 之后的代码
- 例如下面这段代码
const Koa = require('koa')const app = new Koa()app.use((ctx, next) => { console.log(1) next() console.log(3)})app.use((ctx) => { console.log(2)})app.listen(3001)执行结果是1=>2=>3
koa 中间件实现源码大致思路如下:
// 注意其中的compose函数,这个函数是实现中间件洋葱模型的关键// 场景模拟// 异步 promise 模拟const delay = async () => { return new Promise((resolve, reject) => { setTimeout(() => { resolve(); }, 2000); });}// 中间间模拟const fn1 = async (ctx, next) => { console.log(1); await next(); console.log(2);}const fn2 = async (ctx, next) => { console.log(3); await delay(); await next(); console.log(4);}const fn3 = async (ctx, next) => { console.log(5);}const middlewares = [fn1, fn2, fn3];// compose 实现洋葱模型const compose = (middlewares, ctx) => { const dispatch = (i) => { let fn = middlewares[i]; if(!fn){ return Promise.resolve() } return Promise.resolve(fn(ctx, () => { return dispatch(i+1); })); } return dispatch(0);}compose(middlewares, 1);
其它
现在在重新过一遍 node 12 版本的主要 API,有很多新发现,比如说
- fs.watch 这个模块,事件的回调函数有一个参数是触发的事件名称,但是呢,无论我增删改,都是触发 rename 事件(如果更改是 update 事件,删除 delete 事件,重命名是 rename 事件,这样语义明晰该多好)。后来网上找到一个 node-watch 模块,此模块增删改都有对应的事件, 并且还高效的支持递归 watch 文件。
- util 模块有个 promisify 方法,可以让一个遵循异常优先的回调风格的函数,即 (err, value) => ... 回调函数是最后一个参数,返回一个返回值是一个 promise 版本的函数。
const util = require('util');const fs = require('fs');const stat = util.promisify(fs.stat);stat('.').then((stats) => { // 处理 `stats`。}).catch((error) => { // 处理错误。});