查看原文
其他

前端进阶-让你升级的网络知识

2016-09-21 jimmy_thr 前端圈

本文是由田淮仁投稿


在正文之前,我想问大家一个问题: 问:亲,你有基础吗? 答: 有啊,你说前端吗? 不就是HTML,JS,CSS 吗? so easy~ 问: oh-my-zsh… 好吧,那问题来了,挖掘机技术哪家强… 开玩笑。 现在才是问题的正内容。

  • 你知道TCP的基本内容吗?(母鸡啊~)

  • 好吧,那你知道TCP的3次握手,4次挥手吗?(知道一点点)

  • 恩,好,那什么是进程呢?什么是线程呢?(母鸡啊。。)

  • 那并发和并行又是什么呢?(母鸡啊)

  • OMG, 那nodeJS多进程实现你会吗?(不会呀~~~ )

其实,说多了都是泪,这些都是程序员的基本素质呀。。。 面Tencent的时候,被一个总监,骂的阿弥陀佛么么哒. 今天在这里和大家分享一下,我的血泪史。

一、TCP内容


工欲善其事,必先利其器。


一个程序员境界的提升,并不在于你写的一首好代码,更在于你能说出代码背后的故事。ok~ 鸡汤灌完了。我们开始说方法了。


首先这幅图大家必须记得非常清楚才行。

对了还有,OSI七层模型大家应该烂熟于心的。


其中TCP处理transport层,主要是用来建立可靠的连接。 而建立连接的基础,是他丰富的报文内容(md~超级多).我们先来解释一下。 首先,我们TCP3次握手用的报文就是绿色的"TCP Flags"内容。 通过发送ACK,SYN包实现。具体涉及的Tag详见:

  • Source Port / Destination Port:这个就是客户端口(源端口)和服务器端口(目的端口). 端口就是用来区别主机中的不同进程,通过结合源IP和目的IP结合,得出唯一的TCP连接。

  • Sequence Number(seqNumber): 一般由 客户端发送,用来表示报文段中第一个数据字节在数据流中的序号,主要用来解决网络包乱序的问题。

  • Acknowledgment Number(ACK): 即就是用来存放客户端发来的seqNumber的下一个信号(seqNumber+1). 只有当 TCP flags中的ACK为1时才有效. 主要是用来解决不丢包的问题。

  • TCP flags: TCP中有6个首部,用来控制TCP连接的状态.取值为0,1.这6个有:URG,ACK,PSH,RST,SYN,FIN.

    • URG 当为1时,用来保证TCP连接不被中断, 并且将该次TCP内容数据的紧急程度提升(就是告诉电脑,你丫赶快把这个给resolve了)

    • ACK 通常是服务器端返回的。 用来表示应答是否有效。 1为有效,0为无效

    • PSH 表示,当数据包得到后,立马给应用程序使用(PUSH到最顶端)

    • RST 用来确保TCP连接的安全。 该flag用来表示 一个连接复位的请求。 如果发生错误连接,则reset一次,重新连。当然也可以用来拒绝非法数据包。

    • SYN 同步的意思,通常是由客户端发送,用来建立连接的。第一次握手时: SYN:1 , ACK:0. 第二次握手时: SYN:1 ACK:1

    • FIN 用来表示是否结束该次TCP连接。 通常当你的数据发送完后,会自动带上FIN 然后断开连接

恩,基本的TCP内容,大家应该掌握了吧。OK, go on.


二、What's TCP 3次握手


还是一样, 先上张图,让大家先看一下。 上面大家已经基本了解了TCP里面相应的字段,现在看看图里面的是不是觉得有些亲切嘞?


其实,大家看上面的图,差不多都已经能够摸清楚,每次发送请求的内容。其实,TCP3次握手是为了建立 稳定可靠的连接。所以也就不存在神马 2次连接等的怪癖。


(图中flag说明:SYN包表示标志位syn=1,ACK包表示标志位ack=1,SYN+ACK包表示标志位syn=1,ack=1)


现在,我们来正式进入3次握手环节。

  • 第一次握手. 客户端向服务器发送一个SYN包,并且添加上seqNumber(假设为x),然后进入SYN_SEND状态,并且等待服务器的确认。

  • 第二次握手: 服务器接受SYN包,并且进行确认,如果该请求有效,则将TCP flags中的ACK 标志位置1, 然后将AckNumber置为(seqNumber+1),并且再添加上自己的seqNumber(y), 完成后,返回给客户端.服务器进入SYN_RECV状态.(这里服务端是发送SYN+ACK包)

  • 第三次握手 客户端接受ACK+SYN报文后,获取到服务器发送seqNumber(y), 并且 将新头部的AckNumber变为(y+1).然后发送给服务器,完成TCP3次连接。此时服务器和客户端都进入ESTABLISHED状态.

回答一下这个比较尴尬的问题,为什么只有3次握手,而不是4次,或者2次?
很简单呀,因为3次就够了。举个例子吧,假如是2次的话, 可能会出现这样一个情况。

  • 当客户端发送一次请求A后,但是A在网络延迟了很久, 接着客户端又发送了一次B,但是此时A已经无效了。 接着服务器相应了B,并返回TCP连接头,建立连接(这里就2次哈)。 然后,A 历经千山万水终于到服务器了, 服务器一看有请求来了,则接受,由于一开始A带着的TCP格式都是正确的,那么服务器,理所应当的也返回成功连接的flag,但是,此时客户端已经判断该次请求无效,废弃了。 然后服务器,就这么一直挂着(浪费资源),造成的一个问题是,md, 这个锅是谁的? 所以,为了保险起见,再补充一次连接就可以了。所以3次是最合适的。在Chinese中,以3为起称为,如果你用4,5,6,7,8...次的话,这不更浪费吗?

三、TCP4次挥手


TCP4次挥手,是比较简单的。大家对照上面那个图,我们一步一步进行一下讲解。


  • 第一次挥手: A机感觉此时如果keep-alive比较浪费资源,则他提出了分手的请求。设置SeqNumberAckNumber之后,向B机发送FIN包, 表示我这已经没有数据给你了。然后A机进入FIN_WAIT_1状态

  • 第二次挥手:B机收到了A机的FIN包,已经知道了A机没有数据再发送了。此时B机会给A机发送一个ACK包,并且将AckNumber变为 A机传输来的SeqNumber+1. 当A机接受到之后,则变为FIN_WAIT_2状态。表示已经得到B机的许可,可以进行关闭操作。不过此时,B机还是可以向A机发送请求的。

  • 第三次挥手 B机向A机发送FIN包,请求关闭,相当于告诉A机,我这里也没有你要的数据了。然后B机进入CLOSE_WAIT状态.(这里还需要带上SeqNumber,大家看图说话就可以了)

  • 第四次挥手 A机接收到B机的FIN包之后,然后同样,发送一个ACK包给B机。 B机接受到之后,就断开了。 而A机 会等待2MSL之后,如果没有回复,确保服务器端确实是关闭了。然后A机也可以关闭连接。A,B都进入了CLOSE状态.

明白了吗?
大哥~ 等等,什么是2MSL呀~ 
哦,对哦。 这个还么说...
2MSL=2*MSL. 而MSL其实就是Maximum Segment Lifetime,中文意思就是报文最大生存时间。RFC 793中规定MSL为2分钟,实际应用中常用的是30秒,1分钟和2分钟等。 同样上面的TIME_WAT状态其实也就是2MSL状态。 如果超过改时间,则会将该报文废弃,然后直接进入CLOSED状态.


四、进程?线程?


亲,请问php是一门什么语言? (提示,关于进程)
官方回答: php是一门基于多线程的语言


亲,请问nodeJS是一门什么语言?(提示,关于线程)
官方回答: Node.js是单线程!异步!非阻塞!(不过早已可以实现多进程交互了)


那php和nodeJS区别在哪呢?具体可以见图:
PHP:


NodeJS:


亲,那进程和线程区别是什么嘞?

这算是计算机的基本知识吧。 首先我们需要记住的是,进程包括线程。这非常重要。

进程就是系统分配资源的基本单位(比如CPU,内存等)线程就是程序执行的最小单位

进程有自己的空间,如果一个进程崩溃不会引起其它进程的崩溃。


线程,没有自己独立的空间,多个线程共享的是进程的地址空间,当然处理一些基本的如程序计数器,一组寄存器和栈等。


如果一个线程崩溃,它所在的进程就崩溃了。 虽然说,多进程很稳定,但是进程切换时,耗费的资源也是很大的。 所以对于大并发的nodeJS来说,使用多线程的效果要远远比多进程快,稳定。

五、线程的优势

系统在启动一个进程的时候,会首先在资源中独立一块出来,在后台建立一些列表进行维护。 而,线程是比进程低一个level的,所以创建线程所耗费的资源要远远比,创建进程的资源少。

由于进程本身就比较复杂,所以如果进行进程切换的话,造成的性能损耗也是不言而喻的(因为多个进程独立,在切换的时候还需要保证各自的独立性)。 而线程切换就不同了,因为在处在同一进程下面,对于其他的进程都是透明化的(内存共享),所以在进行进程切换时,所耗费的资源远远比进程切换的小。


在Linux和window下,CPU的分配是根据线程数来的,如果

总线程数<=CPU数量:并行运行
总线程数> CPU数量:并发运行

并行指的是,当你的CPU核数比线程数多的话,则会将每个线程都分在一个CPU核里进行处理。


并发指的是,当你的CPU核数比线程数少的话,则会利用“时间片轮转进程调度算法”,对每个线程进行同等的运行。

细化进程的处理,通常一个进程可以拆分为多个线程进行处理,就和模块化处理是类似的,使用模块化书写的效果要远远比使用单main入口方式书写 清晰,稳定。

六、并发,并行原理

亲, 并发和并行有什么共同点吗?
恩~ 有的, 他们都有个‘并’子,字面上看起来都是同时执行的意思。
没错,当然只是字面上而已。
实际上,并发和并行是完全不同的概念。 这里主要和CPU核数有关。这里为了理解,拿线程来作为参考吧。
当你的

总线程数<=CPU数量:并行运行
总线程数> CPU数量:并发运行

很明显,并行其实是真正意义上的同时执行。 当线程数< CPU核数时,每个线程会独立分配到一个CPU里进行处理。


大家看过火影忍者吗?
没错,就是鸣人 出关 口遁九尾之后。 他使用影分身,跑去各地支援同伴,对抗斑。 这里类比来说,就可以理解为, 每个CPU 都是鸣人的一个影分身,他们执行这各自不同的工作,但是,在同一时间上,他们都在运行。 这就是并行


那并发嘞?
其实,并发有点难以理解,他做的工作其实,就是利用一系列算法实现,并行做的事。一个比较容易理解的就是“时间片轮转进程调度算法”。


即: 在系统控制下,每个线程轮流使用CPU,而且,每个线程使用时间必须很短(比如10ms), 所以这样切换下来。我们(愚蠢的人类,哈哈哈), 天真的以为任务,真的是在"并行"执行.


七、nodeJS的进程实现

一开始nodeJS最令人诟病的就是他的单线程特性。既是绝招也是死穴,不过nodeJS发展很快,在v0.8版本就已经添加了cluster作为内置模块,实现多核的利用。关于nodeJS的进程模块,最主要的当然还是cluster. 通过调用child_process.fork()函数来开启进程。 先看一个具体的demo(from 官网)

var cluster = require('cluster');
var http = require('http');
var numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
   console.log("master start...");
   // Fork workers.    for (var i = 0; i < numCPUs; i++) {        cluster.fork();    }
   //用来监听子worker创建监听服务    cluster.on('listening',function(worker,address){
     console.log('listening: worker ' + worker.process.pid +', Address: '+address.address+":"+address.port);    });    cluster.on('exit', function(worker, code, signal) {
     console.log('worker ' + worker.process.pid + ' died');    }); } else {    http.createServer(function(req, res) {        res.writeHead(200);        res.end("hello world\n");    }).listen(0); }

存放为app.js 然后运行node app.js就可以实现一个简单的多进程效果。
结果可能为下:

master start... listening: worker 1559, Address: null:57803
listening: worker 1556, Address: null:57803
listening: worker 1558, Address: null:57803
listening: worker 1557, Address: null:57803

可以从上面的demo中看出,通过cluster.isMaster来区分master和worker. 而master和worker之间使用listen(0)进行通信.

  • server.listen(0):在master和worker通信过程,集群中的worker会打开一个随机端口共用,通过socket通信像上例中的57803

当然你也可以手动打开一个端口共享监听。像这样.

http.createServer(function(req, res) {        res.writeHead(200);        res.end("hello world\n");    }).listen(3000);

cluster对应API

cluster对象的属性和函数

  • cluster.setttings:配置集群参数对象

  • cluster.isMaster:判断是不是master节点*

  • cluster.isWorker:判断是不是worker节点*

  • Event: 'fork': 监听创建worker进程事件

  • Event: 'online': 监听worker创建成功事件

  • Event: 'listening': 监听worker开启的http.listen

  • Event: 'disconnect': 监听worker断线事件

  • Event: 'exit': 监听worker退出事件

  • Event: 'setup': 监听setupMaster事件

  • cluster.setupMaster([settings]): 设置集群参数

  • cluster.fork([env]): 创建worker进程

  • cluster.disconnect([callback]): 关闭worket进程*

  • cluster.worker: 获得当前的worker对象*

  • cluster.workers: 获得集群中所有存活的worker对象*

通过cluster.worker获得的worker对象和相应的参数

  • worker.id: 进程ID号

  • worker.process: ChildProcess对象*

  • worker.suicide: 在disconnect()后,判断worker是否自杀*

  • worker.send(message, [sendHandle]):* master给worker发送消息。注:worker给发master发送消息要用process.send(message)

  • worker.kill([signal='SIGTERM']): 杀死指定的worker,别名destory()*

  • worker.disconnect(): 断开worker连接,让worker自杀

  • Event: 'message': 监听master和worker的message事件

  • Event: 'online': 监听指定的worker创建成功事件

  • Event: 'listening': 监听master向worker状态事件

  • Event: 'disconnect': 监听worker断线事件

  • Event: 'exit': 监听worker退出事件

这些就是cluster的全部内容。不过这仅仅只是内容而已,如果使用cluster,这便是我们程序员要做的事了。


进程通信

由于nodeJS 只能实现单进程的效果,所以他的进程数只能为一个,但是通过引用cluster模块,可以开启多个子进程实现CPU的利用。

var cluster = require('cluster');
var http = require('http');
var numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
   console.log('[master] ' + "start master...");
   
   for (var i = 0; i < numCPUs; i++) {
           //根据CPUS创建worker        var wk = cluster.fork();
           //master给新建的worker发送信息        wk.send('[master] ' + 'hi worker' + wk.id);    }
   //监听创建work的进程事件 即就是fork    cluster.on('fork', function (worker) {
     console.log('[master] ' + 'fork: worker' + worker.id);    });
   //成功创建work的进程事件    cluster.on('online', function (worker) {
     console.log('[master] ' + 'online: worker' + worker.id);    });
   //master监听worker创建服务    cluster.on('listening', function (worker, address) {
     console.log('[master] ' + 'listening: worker' + worker.id + ',pid:' + worker.process.pid + ', Address:' + address.address + ":" + address.port);    });
   //监听worder断线    cluster.on('disconnect', function (worker) {
     console.log('[master] ' + 'disconnect: worker' + worker.id);    });
   //监听work的退出    cluster.on('exit', function (worker, code, signal) {
     console.log('[master] ' + 'exit worker' + worker.id + ' died');    });
   function eachWorker(callback) {
     for (var id in cluster.workers) {        callback(cluster.workers[id]);      }    } } else if (cluster.isWorker) {
   console.log('[worker] ' + "start worker ..." + cluster.worker.id);    http.createServer(function (req, res) {            res.writeHead(200, {"content-type": "text/html"});            res.end('worker'+cluster.worker.id+',PID:'+process.pid);    }).listen(3000); }

运行后的结果为:

[master] start master... [master] fork: worker1 [master] fork: worker2 [master] fork: worker3 [master] fork: worker4 [master] online: worker1 [master] online: worker4 [master] online: worker2 [master] online: worker3 [worker] start worker ...1
[worker] start worker ...4
[worker] start worker ...2
[master] listening: worker4,pid:990, Address:null:3000
[master] listening: worker1,pid:987, Address:null:3000
[master] listening: worker2,pid:988, Address:null:3000
[worker] start worker ...3
[master] listening: worker3,pid:989, Address:null:3000

参照注释代码和上述的结果,我们可以很容易的得到一个触发逻辑。运行过程是:

  • 首先fork子进程

  • 触发fork事件

  • 创建成功,触发online事件

  • 然后重新执行一遍app.js,通过isWorker判断子进程

  • 创建子进程服务->触发master上的listening


上面只是创建满负载子进程的流程。 但怎样实现进程间的交互呢? 很简单,master和worker监听message事件,通过传递参数,进行交互。

  • cluster.worker.send(message[,handleFn]) master向worker发送信息

  • process.send(message[,handleFn]); worker向master发送信息

这个是多进程之间的通信:https://jsfiddle.net/jimmyTHR/jcc8ezo3/

我们来分解一下代码块:

//开启master监听worker的通信
cluster.workers[id].on('message', function(msg){
         //...        });
//开启worker监听master的通信
process.on('message', function(msg) {
      //...    });

运行上面的demo. 这里就不细说,整个流程,只看一下信息通信这一块了。

  • 创建子进程,触发listening事件

  • 使用process.on监听message

  • 接受master发送过来的消息

  • 再向master返回消息



nodeJS负载均衡


现在,nodeJS负载均衡应该是最容易实现的,其内部已经帮我们封装好了,我们直接调用就over了。其中,实现负载均衡的模块就是cluster。以前cluster确实很累赘。负载均衡的算法实现的不是很好,导致的下场就是npm2的兴起。不过现在已经实现了负载均衡,官方说法就是用round-robin,来进行请求分配。 round-robin其实就是一个队列的循环,灰常容易理解。先看一下,cluster封装好实现的负载均衡。

var cluster = require('cluster');
var http = require('http');
var numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
   console.log('[master] ' + "start master...");
   
   for (var i = 0; i < numCPUs; i++) {        cluster.fork();    }    cluster.on('listening', function (worker, address) {
       console.log('[master] ' + 'listening: worker' + worker.id + ',pid:' + worker.process.pid + ', Address:' + address.address + ":" + address.port);    }); } else if (cluster.isWorker) {
       console.log('[worker] ' + "start worker ..." + cluster.worker.id);
       var num = 0;        http.createServer(function (req, res) {            num++;
           console.log('worker'+cluster.worker.id+":"+num);            res.end('worker'+cluster.worker.id+',PID:'+process.pid);    }).listen(3000); }

(哥哥,你骗人,这哪里实现了负载均衡,这不就是上面的算法么?)
是呀,,, 我又没说负载均衡不是这个。


负载均衡就是帮你解决请求的分配问题。ok~ 为了证明,我没有骗你,我们来进行测试一下。


使用brew安装siege测试,当然你也可以使用其他测试工具,不过在MAC 上面最好使用siege和webbench或者ab,我这里使用siege:

brew install siege

使用的测试语法就是

siege -c 并发数 -t 运行测试时间 URL

测试的时间后面需要带上单位,比如s,m,h,d等。默认单位是m(分钟)。举个例子吧:

siege -c 100 -t 10s http://girls.hustonline.net

对女生节网页进行 100次并发测试,持续时间是10s.当然siege里还有其他的参数.

  • -c NUM 设置并发的数量.eg: -c 100; //设置100次并发

  • -r NUM 设置发送几轮的请求,即,总的请求数为: -cNum*-rNum但是, -r不能和-t一起使用(为什么呢?你猜).eg: -r 20

  • -t NUM 测试持续时间,指你运行一次测试需要的时间,在timeout后,结束测试.

  • -f file. 用来测试file里面的url路径.file的尾缀需要为.url. eg: -f girls.url.

  • -b . 就是询问开不开启基准测试(benchmark)。 这个参数不太重要,有兴趣的同学,可以下去学习一下。

siege常用的就是这几个. 通常我们是搭配 -c + -r 或者-c + -t。


OK,现在我们开始我们的测试 procedure。


首先开启多进程NodeJS. node app.js


使用siege -c 100 -t 10s 127.0.0.1:3000. (Ps: 当然也可以使用进行代替)


得到的结果为

Transactions:             600 hits Availability:          100.00 % Elapsed time:            6.08 secs Data transferred:        0.01 MB Response time:           0.01 secs Transaction rate:  98.68 trans/sec Throughput:            0.00 MB/sec Concurrency:                0.88
Successful transactions:     600
Failed transactions:           0
Longest transaction:        0.04
Shortest transaction:       0.00

在10s内,发起了600次请求,最大的峰值是98.68 trans/sec。 通过统计分析,得到每个worker的分发量。

worker1:162
worker2:161
worker3:167
worker4:170

可以看出,基本上每个负载上分配的请求的数目都差不多。这就已经达到了负载均衡的效果。


下一篇会对nodeJS已经相关的测试工具做一些介绍哦。
ending~



【React启蒙系列文章】

一、[React启蒙系列] 初探React

二、[React启蒙系列] 学习React前需要理解的名词

三、[React启蒙系列] React和Babel的基本使用

四、[React启蒙系列]React节点

五、[React启蒙系列]使用JSX

六、[React启蒙系列]React 组件属性

七、[React启蒙系列]React 组件状态


【您可能感兴趣的文章】

一、手把手教你用react

二、React入门及资源指引

三、利用ESLint检查代码质量

四、构建一个安全的 JavaScript 沙箱

五、入门Webpack,看这篇就够了

六、第三届CSS大会广州找场地啦~~求介绍~~

七、Web Components 是个什么样的东西

八、JavaScript 被忽视的细节

九、[心得] 如何提高 React App 的性能

十、[心得] 自己动手打造 ES7 开发环境

十一、[译文] 如何运用新技术提升网页速度和性能

十二、你应该知道的HTTP基础知识

十三、Webpack from First Principles

十四、没时间阅读?佐克伯、比尔盖兹、马斯克教你「5小时原则」

十五、没快速成长,别说你在创业

十六、TCP三次握手&Render Tree页面渲染=>从输入URL到页面显示的过程?

十七、[翻译]React最佳实践与实用函数

十八、前端知识普及之HTML



前端圈--打造专业的前端技术会议

为web前端开发者提供技术分享和交流的平台

打造一个良好的前端圈生态,推动web标准化的发展

官网:http://fequan.com

微博:fequancom | QQ群:41378087


长按二维码关注我们

投稿:content@fequan.com

赞助合作:apply@fequan.com

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存