Skip to content

Latest commit

 

History

History
118 lines (69 loc) · 9.31 KB

webSocket.md

File metadata and controls

118 lines (69 loc) · 9.31 KB

WebSocket

WebSockets 是一个可以创建和服务器间进行双向会话的高级技术。通过这个API你可以向服务器发送消息并接受基于事件驱动的响应,这样就不用向服务器轮询获取数据了。

接口

WebSocket

用于连接WebSocket服务器的主要接口,之后可以在这个连接上发送 和接受数据。

CloseEvent

当连接关闭的时候WebSocket对象触发的事件。

MessageEvent

当从服务器获取到消息的时候WebSocket对象触发的事件。

WebSocket的诞生本质上就是为了解决HTTP协议本身的单向性问题:请求必须由客户端向服务端发起,然后服务端进行响应。这个Request-Response的关系是无法改变的。

WebSocket就是从技术根本上解决这个问题的:看名字就知道,它借用了Web的端口和消息头来创建连接,后续的数据传输又和基于TCP的Socket几乎完全一样,但封装了好多原本在Socket开发时需要我们手动去做的功能。

比如原生支持wss安全访问(跟https共用端口和证书)、创建连接时的校验、从数据帧中自动拆分消息包等等。

对于服务器与客户端的双向通信,WebSocket简直是不二之选。如果不是还有少数旧版浏览器尚在服役的话,所有的轮询、长连接等方式早就该废弃掉。那些整合多种双向推送消息方式的库(如http://Socket.IO、SignalR)当初最大的卖点就是兼容所有浏览器版本,自动识别旧版浏览器并采取不同的连接方式,现在也渐渐失去了优势——所有新版浏览器都兼容WebSocket,直接用原生的就行了。

WebSocket在用于双向传输、推送消息方面能够做到灵活、简便、高效,但在普通的Request-Response过程中并没有太大用武之地,比起普通的HTTP请求来反倒麻烦了许多,甚至更为低效。

优点是:

1.原先在Socket连接后还要进行一些复杂的身份验证,同时要阻止未验证的连接发送控制指令。现在不需要了,在建立WebSocket连接的url里就能携带身份验证参数,验证不通过可以直接拒绝,不用设置状态;

2.原先自己实现了一套类似SSL的非对称加密机制,现在完全不需要了,直接通过wss加密,还能顺便保证证书的可信性;

3.原先要自己定义Socket数据格式,设置长度与标志,处理粘包、分包等问题,现在WebSocket收到的直接就是完整的数据包,完全不用自己处理;

4.前端的nginx可以直接进行转发与负载均衡,部署简单多了。

首先我们来定义流的概念,一个流可以是文件,socket,pipe等等可以进行I/O操作的内核对象。

不管是文件,还是套接字,还是管道,我们都可以把他们看作流。

I/O的操作,通过read,我们可以从流中读入数据;通过write,我们可以往流写入数据。

我们需要从流中读数据,但是流中还没有数据,(典型的例子为,客户端要从socket读如数据,但是服务器还没有把数据传回来),这时候该怎么办?

阻塞

阻塞是个什么概念呢?比如某个时候你在等快递,但是你不知道快递什么时候过来,而且你没有别的事可以干(或者说接下来的事要等快递来了才能做);那么你可以去睡觉了,因为你知道快递把货送来时一定会给你打个电话(假定一定能叫醒你)。

**非阻塞忙轮询。**接着上面等快递的例子,如果用忙轮询的方法,那么你需要知道快递员的手机号,然后每分钟给他挂个电话:“你到了没?”

为了了解阻塞是如何进行的,我们来讨论缓冲区,以及内核缓冲区,最终把I/O事件解释清楚。

当你操作一个流时,更多的是以缓冲区为单位进行操作,这是相对于用户空间而言。对于内核来说,也需要缓冲区。

这四个情形涵盖了四个I/O事件,缓冲区满,缓冲区空,缓冲区非空,缓冲区非满(注都是说的内核缓冲区,且这四个术语都是我生造的,仅为解释其原理而造)。这四个I/O事件是进行阻塞同步的根本。(如果不能理解“同步”是什么概念,请学习操作系统的锁,信号量,条件变量等任务同步方面的相关知识)。

阻塞I/O的缺点。但是阻塞I/O模式下,一个线程只能处理一个流的I/O事件。如果想要同时处理多个流,要么多进程(fork),要么多线程(pthread_create),很不幸这两种方法效率都不高。

非阻塞忙轮询的I/O方式,我们发现我们可以同时处理多个流了

while true {
    for i in stream[]
    {
        if i has data
        read until unavailable
    }
}

我们只要不停的把所有流从头到尾问一遍,又从头开始。这样就可以处理多个流了,但这样的做法显然不好,因为如果所有的流都没有数据,那么只会白白浪费CPU。这里要补充一点,阻塞模式下,内核对于I/O事件的处理是阻塞或者唤醒,而非阻塞模式下则把I/O事件交给其他对象(后文介绍的select以及epoll)处理甚至直接忽略。

epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll之会把哪个流发生了怎样的I/O事件通知我们。此时我们对这些流的操作都是有意义的。(复杂度降低到了O(k),k为产生I/O事件的流的个数,也有认为O(1)的[更新 1])

Java在语言中内置了多线程,特别是在创建线程时非常得棒。

大多数的Java Web服务器都会为每个请求启动一个新的执行线程,然后在这个线程中调用开发人员编写的函数。

Java提供了很多在I/O方面开箱即用的功能,但如果遇到创建大量阻塞线程执行大量I/O操作的情况时,Java也没有太好的解决方案。

把非阻塞I/O作为头等大事:Node

http.createServer(function(request, response) {
    fs.readFile('/path/to/file', 'utf8', function(err, data) {
        response.end(data);
    });
});

当请求开始时,第一个函数会被调用,而第二个函数是在文件数据可用时被调用。

这样,Node就能更有效地处理这些回调函数的I/O。有一个更能说明问题的例子:在Node中调用数据库操作。首先,你的程序开始调用数据库操作,并给Node一个回调函数,Node会使用非阻塞调用来单独执行I/O操作,然后在请求的数据可用时调用你的回调函数。这种对I/O调用进行排队并让Node处理I/O调用然后得到一个回调的机制称为“事件循环”。这个机制非常不错。

然而,这个模型有一个问题。在底层,这个问题出现的原因跟V8 JavaScript引擎(Node使用的是Chrome的JS引擎)的实现有关,即:你写的JS代码都运行在一个线程中。请思考一下。这意味着,尽管使用高效的非阻塞技术来执行I/O,但是JS代码在单个线程操作中运行基于CPU的操作,每个代码块都会阻塞下一个代码块的运行。

如果你的主要性能问题是I/O的话,那么这个Node模型能帮到你。但是,它的缺点在于,如果你在一个处理HTTP请求的函数中放入了CPU处理密集型代码的话,一不小心就会让每个连接都出现拥堵。

原生无阻塞:Go

Go语言的一个关键特性是它包含了自己的调度器。它并不会为每个执行线程对应一个操作系统线程,而是使用了“goroutines”这个概念。Go运行时会为一个goroutine分配一个操作系统线程,并控制它执行或暂停。Go HTTP服务器的每个请求都在一个单独的Goroutine中进行处理。

除了回调机制被内置到I/O调用的实现中并自动与调度器交互之外,Go运行时正在做的事情与Node不同。它也不会受到必须让所有的处理代码在同一个线程中运行的限制,Go会根据其调度程序中的逻辑自动将你的Goroutine映射到它认为合适的操作系统线程中。

在大多数情况下,这真正做到了“两全其美”。非阻塞I/O可用于所有重要的事情,但是代码却看起来像是阻塞的,因此这样往往更容易理解和维护。 剩下的就是Go调度程序和OS调度程序之间的交互处理了。这并不是魔法,如果你正在建立一个大型系统,那么还是值得花时间去了解它的工作原理的。同时,“开箱即用”的特点使它能够更好地工作和扩展。

性能评测

对于这些不同模型的上下文切换,很难进行准确的计时。

端到端的HTTP请求/响应性能涉及到的因素有很多。

从单单这一张图中很难得到结论,但我个人认为,在这种存在大量连接和计算的情况下,我们看到的结果更多的是与语言本身的执行有关。请注意,“脚本语言”的执行速度最慢。

PHPJava在 web应用方面 都有 可用 的 非阻塞I/O 的 实现 。但是这些实现并不像上面描述的方法那么使用广泛,并且还需要考虑维护上的开销。更不用说应用程序的代码必须以适合这种环境的方式来构建。

决定网站性能的主要因素是架构,然后是代码水平,最后才是语言。

学习语言无非是语法、库和框架这三者