HTTP概念进阶

1.什么是回调?

在Java中,就是类A调用类B中的某个方法b,然后类B又在某个时候反过来调用类A中的某个方法a,对于A来说,这个a方法便叫做回调方法

pubilc interface CallBack{

public void callbackMethod();

}

public class A implements CallBack{ // A实现接口CallBack

B b = new B();

public void do(){

b.doSomething(this); // A运行时调用B中doSomething方法,以自身传入参数,B已取得A,可以随时回调A所实现的CallBack接口中的方法

}

public void callbackMethod(){ // 对A来说,该方法就是回调方法

System.out.println("callbackMethod is executing!");

}

}

public class B{

public void doSomething(CallBack cb){ // B拥有一个参数为CallBack接口类型的方法

System.out.println(“I am processing my affairs… ”);

System.out.println(“then, I need invoke callbackMethod…”);

cb.callbackMethod();

}

}


2.什么是同步/异步

进程同步用来实现程序并发执行时候的可再现性。

一.进程同步及异步的概念

1.进程同步:就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。也就是必须一件一件事做,等前一件做完了才能做下一件事.就像早上起床后,先洗涮,然后才能吃饭,不能在洗涮没有完成时,就开始吃饭.按照这个定义,其实绝大多数函数都是同步调用(例如sin,isdigit等)。但是一般而言,我们在说同步、异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。最常见的例子就是

sendmessage。该函数发送一个消息给某个窗口,在对方处理完消息之前,这个函数不返回。当对方处理完毕以后,该函数才把消息处理函数所返回的lresult值返回给调用者。

2.异步

异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。

以casycsocket类为例(注意,csocket从casyncsocket派生,但是其功能已经由异步转化为同步),当一个客户端通过调用connect函数发出一个连接请求后,调用者线程立刻可以朝下运行。当连接真正建立起来以后,socket底层会发送一个消息通知该对象。

这里提到执行部件和调用者通过三种途径返回结果:状态、通知和回调。可以使用哪一种依赖于执行部件的实现,除非执行部件提供多种选择,否则不受调用者控制。如果执行部件用状态来通知,那么调用者就需要每隔一定时间检查一次,效率就很低(有些初学多线程编程的人,总喜欢用一个循环去检查某个变量的值,这其实是一种很严重的错误)。如果是使用通知的方式,效率则很高,因为执行部件几乎不需要做额外的操作。至于回调函数,其实和通知没太多区别。

进程同步的基本概念

在计算机系统中,由于资源有限而导致了进程之间的资源竞争和共享,因此,进程的并发执行不仅仅是用户程序的执行开始时间的随机性和提高资源利用率的结果,也是资源有限性导致资源的竞争与共享对进程的执行过程进行制约所造成的。那么,在进程的并发执行过程中存在哪些制约呢?

二.同步与异步传输:

1.异步传输

通常,异步传输是以字符为传输单位,每个字符都要附加 1 位起始位和 1 位停止位,以标记一个字符的开始和结束,并以此实现数据传输同步。所谓异步传输是指字符与字符(一个字符结束到下一个字符开始)之间的时间间隔是可变的,并不需要严格地限制它们的时间关系。起始位对应于二进制值 0,以低电平表示,占用 1 位宽度。停止位对应于二进制值 1,以高电平表示,占用 1~2 位宽度。一个字符占用 5~8位,具体取决于数据所采用的字符集。例如,电报码字符为 5 位、ASCII码字符为 7 位、汉字码则为8 位。此外,还要附加 1 位奇偶校验位,可以选择奇校验或偶校验方式对该字符实施简单的差错控制。发送端与接收端除了采用相同的数据格式(字符的位数、停止位的位数、有无校验位及校验方式等)外,还应当采用相同的传输速率。典型的速率有:9 600 b/s、19.2kb/s、56kb/s等。

异步传输又称为起止式异步通信方式,其优点是简单、可靠,适用于面向字符的、低速的异步通信场合。例如,计算机与Modem之间的通信就是采用这种方式。它的缺点是通信开销大,每传输一个字符都要额外附加2~3位,通信效率比较低。例如,在使用Modem上网时,普遍感觉速度很慢,除了传输速率低之外,与通信开销大、通信效率低也密切相关。

2. 同步传输

通常,同步传输是以数据块为传输单位。每个数据块的头部和尾部都要附加一个特殊的字符或比特序列,标记一个数据块的开始和结束,一般还要附加一个校验序列 (如16位或32位CRC校验码),以便对数据块进行差错控制。所谓同步传输是指数据块与数据块之间的时间间隔是固定的,必须严格地规定它们的时间关系。

三.同步阻塞与异步阻塞:

同步是阻塞模式,异步是非阻塞模式。 

我的理解:同步是指两个线程的运行是相关的,其中一个线程要阻塞等待另外一个线程的运行。异步的意思是两个线程毫无相关,自己运行自己的。 

同步是指:发送方发出数据后,等接收方发回响应以后才发下一个数据包的通讯方式。 

异步是指:发送方发出数据后,不等接收方发回响应,接着发送下个数据包的通讯方式。 

举个不太恰当的例子,就像: 

SendMessage(...) 

TRACE0("just  like  send"); 

PostMessage(...) 

TRACE0("just  like  WSASend  using  overlapped"); 

 SendMessage是调用的时候不返回,等消息响应后才执行TRACE0,这就是同步. 

PostMessage是调用后马上返回,不用消息响应就执行TRACE0,这就是异步.

四.其它解释:

 同步和异步的区别

 举个例子:普通B/S模式(同步)AJAX技术(异步)

同步:提交请求->等待服务器处理->处理完毕返回 这个期间客户端浏览器不能干任何事

异步: 请求通过事件触发->服务器处理(这是浏览器仍然可以作其他事情)->处理完毕

同步就是你叫我去吃饭,我听到了就和你去吃饭;如果没有听到,你就不停的叫,直到我告诉你听到了,才一起去吃饭。

异步就是你叫我,然后自己去吃饭,我得到消息后可能立即走,也可能等到下班才去吃饭。

所以,要我请你吃饭就用同步的方法,要请我吃饭就用异步的方法,这样你可以省钱。

举个例子 打电话时同步 发消息是异步


3.什么是I/O?
I/O是 input/output的缩写,即输入输出端口。每个设备都会有一个专用的I/O地址,用来处理自己的输入输出信息。CPU与外部设备、存储器的连接和数据交换都需要通过接口设备来实现,。,习惯上说到接口只是指I/O接口。


4.什么是单线程/多线程?
打个比方,单线程就是你去厨房有烧饭又烧菜,一个人来回跑;多线程就是两个人,一个单做饭,一个单做菜。这样的解释应该比纯理论的好理解一点吧?
再补充一下,多线程就是一个CPU虚拟了几个CPU,而双核就是实际上就有两个线程了,当然,还可以每个核再去虚拟多个线程(也可以理解成多个流水线吧)
 
 
5.什么是阻塞/非阻塞?
阻塞
    阻塞调用是指调用结果返回之前,当前线程会被挂起。函数只有在得到结果之后才会返回。有人也许会把阻塞调用和同步调用等同起来,实际上它们是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。例如,我们在CSocket中调用Receive函数,如果缓冲区中没有数据,这个函数就会一直等待,直到有数据才返回。而此时,当前线程还会继续处理各种各样的消息。如果主窗口和调用函数在同一个线程中,除非你在特殊的界面操作函数中调用,其实主界面还是应该可以刷新。socket接收数据的另外一个函数recv则是一个阻塞调用的例子。当socket工作在阻塞模式的时候, 如果没有数据的情况下调用该函数,则当前线程就会被挂起,直到有数据为止。
 
非阻塞
    非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。
 
对象的阻塞模式和阻塞函数调用
 
    对象是否处于阻塞模式和函数是不是阻塞调用有很强的相关性,但是并不是一一对应的。阻塞对象上可以有非阻塞的调用方式,我们可以通过一定的API去轮询状态,在适当的时候调用阻塞函数,就可以避免阻塞。而对于非阻塞对象,调用特殊的函数也可以进入阻塞调用。函数select就是这样的一个例子。
 
 
6.什么是事件?
事件:样本空间的一些子集稍为随机事件,简称事件. 
= =自己理解:大概就是进行了操作产生了变化对应的这个事

7.什么是事件驱动?
早期程序使用输入-操作-输出的机制,整个流程完全由程序员事先设定好。
面向对象程序设计当中采用的就是事件驱动机制。比如说鼠标左击、双击都是具体事件,根据这些事件启用预先设置的相应动作就是事件驱动机制。



8.什么是基于事件驱动的回调?

这一切都归结于“Node.js是事件驱动的”这一事实。好吧,其实我也不是特别确切的了解这句话的意思。不过我会试着解释,为什么它对我们用Node.js写网络应用(Web based application)是有意义的。

当我们使用 http.createServer 方法的时候,我们当然不只是想要一个侦听某个端口的服务器,我们还想要它在服务器收到一个HTTP请求的时候做点什么。

问题是,这是异步的:请求任何时候都可能到达,但是我们的服务器却跑在一个单进程中。

写PHP应用的时候,我们一点也不为此担心:任何时候当有请求进入的时候,网页服务器(通常是Apache)就为这一请求新建一个进程,并且开始从头到尾执行相应的PHP脚本。

那么在我们的Node.js程序中,当一个新的请求到达8888端口的时候,我们怎么控制流程呢?

嗯,这就是Node.js/JavaScript的事件驱动设计能够真正帮上忙的地方了——虽然我们还得学一些新概念才能掌握它。让我们来看看这些概念是怎么应用在我们的服务器代码里的。

我们创建了服务器,并且向创建它的方法传递了一个函数。无论何时我们的服务器收到一个请求,这个函数就会被调用。

我们不知道这件事情什么时候会发生,但是我们现在有了一个处理请求的地方:它就是我们传递过去的那个函数。至于它是被预先定义的函数还是匿名函数,就无关紧要了。

这个就是传说中的 回调 。我们给某个方法传递了一个函数,这个方法在有相应事件发生时调用这个函数来进行 回调 。

至少对我来说,需要一些功夫才能弄懂它。你如果还是不太确定的话就再去读读Felix的博客文章。

让我们再来琢磨琢磨这个新概念。我们怎么证明,在创建完服务器之后,即使没有HTTP请求进来、我们的回调函数也没有被调用的情况下,我们的代码还继续有效呢?我们试试这个:

var http = require("http"); function onRequest(request, response) {   console.log("Request received.");   response.writeHead(200, {"Content-Type": "text/plain"});   response.write("Hello World");   response.end(); } http.createServer(onRequest).listen(8888); console.log("Server has started.");

注意:在 onRequest (我们的回调函数)触发的地方,我用 console.log 输出了一段文本。在HTTP服务器开始工作之后,也输出一段文本。

当我们与往常一样,运行它node server.js时,它会马上在命令行上输出“Server has started.”。当我们向服务器发出请求(在浏览器访问http://localhost:8888/),“Request received.”这条消息就会在命令行中出现。

这就是事件驱动的异步服务器端JavaScript和它的回调啦!

(请注意,当我们在服务器访问网页时,我们的服务器可能会输出两次“Request received.”。那是因为大部分服务器都会在你访问 http://localhost:8888 /时尝试读取 http://localhost:8888/favicon.ico )


9.什么是时间循环?

在了解node.js之前你首先需要了解的一个基本的论点是:I/O是“昂贵”的。

 

因此对于当前的编程技术而言,最大的浪费来自于等待I/O的完成。下面列出了改善该问题的几种方式,其中的某个可以帮助你提高性能:

 

  • 同步:在某一时刻,一次只处理一个请求。但这种情况下,任何一个请求都会“耽误”(阻塞)所有其他的请求。
  • fork一个新进程:对于每个请求,你启动一个新的进程来处理。这种情况下,无法达到很好的扩展,上百个连接就意味着上百个进程的存在。fork()函数是Unix程序员的锤子,因为使用它很方便,所以每个程序都看起来像个钉子一样(都喜欢用锤子拿来敲敲它)。所以,经常造成过度使用,而有些过往矫正。
  • 线程:开启一个新的线程来处理每个请求。这种方式很简单,并且对于内核来讲使用线程也比fork进程来得“亲切”,因为通常线程花费比进程更少的开销。缺点:你的机子可能不支持基于线程编程,并且基于线程的程序,其复杂度增长得非常快,同时你还会有对访问共享资源的担忧。

 

你需要了解的第二个论点是:被线程处理的每个连接都是“内存昂贵的”。

Apache是采用多线程处理请求的。它对于每个请求“孵化”出一个线程(或者进程,这取决于配置)来处理。你将会看到随着并发连接数的增长以及更多的线程需要服务多个客户端时,那些开销有多消耗内存。Nginx跟Node.js都不是基于多线程模型的,因为线程跟进程都需要非常大的内存开销。他们都是单线程的,但是基于事件的。这种基于单线程的模型消除了为了处理很多请求而创建成百上千个线程或进程带来的开销。

 

Node.js为你的代码保持单线程的运行环境

 

它确实是基于单线程运行的,你无法编写任何代码来执行并发;例如执行一个"sleep"操作将阻塞整个服务器1秒钟。

 

[javascript] view plaincopyprint?
 
  1. while(new Date().getTime() < now + 1000) {  
  2.    // do nothing  
  3. }  
 

 

因此,当代码运行的时候,node.js将不会响应来自客户端的其他请求,因为它只有一个线程来执行你的代码。或者,如果你有某些CPU密集型的操作,比如说,重置图片的尺寸,那也将阻塞所有其他的请求。

 

...然而,除了你的代码之外,其他的一切都是并发执行

 

 

在一个单独的请求里,没有办法可以使得代码并行执行。然而,所有的I/O都是基于时间的并且是异步的,所以接下来的代码将不会阻塞服务器:

[javascript] view plaincopyprint?
 
  1. c.query(  
  2.    'SELECT SLEEP(20);',  
  3.    function (err, results, fields) {  
  4.      if (err) {  
  5.        throw err;  
  6.      }  
  7.      res.writeHead(200, {'Content-Type': 'text/html'});  
  8.      res.end('<html><head><title>Hello</title></head><body><h1>Return from async DB query</h1></body></html>');  
  9.      c.end();  
  10.     }  
  11. );  


如果你在一个请求中这么做,其他请求能够很好得被执行。

 

为什么这是更好的方式?什么时候我们需要从同步转向异步/并发执行?

 

采用同步执行是个不错的方式,因为它使得编码变得容易(对比线程而言,并发问题常常让你陷入万劫不复)。

在node.js中,你不需要去担心你的代码在后端会发生。你只需要在你做I/O操作的时候使用回调就可以了。你会得到保证:你的代码不会被中断,并且I/O操作也不会阻塞其他请求(因为没有了那些线程/进程需要花费的开销,比如在Apache中会发生的内存过高等)。


采用异步I/O也很好,因为I/O比那些执行其他操作更昂贵,我们应该做一些更有意义的事情而不是去等待I/O。

 

一个事件循环指的是——一个实体,它可以处理外部事件并且将它们转化为回调的执行。因此,I/O调用变成了node.js可以从一个请求切换到另外一个请求的“点”,你的代码保存了回调并返回控制权给node.js运行时环境。而回调在最终获得了数据之后被执行。

当然,在node.js内部,仍然是依靠线程和进程来进行数据访问、处理其他任务执行。然而,这些都没有明确地对你的代码暴露出来,所以你不需要额外担心内部如何处理I/O之间的交互。对比Apache的模型,它少去了很多线程以及线程开销,因为对每个连接来讲单独的线程不是必须的。仅仅是当你绝对需要让某个操作并发执行才会需要线程,但即便如此线程也是node.js自己管理的。


除了I/O调用之外,node.js期待所有的请求最好快速返回。比如,那些CPU密集型的工作应该被隔离到另一个进程上去执行(通过与事件交互或者使用像WebWorker一样的抽象)。这很明显意味着当你与事件交互的时候,如果没有另一个线程在后端(node.js运行时),那么你是无法并行化执行代码的。基本上,所有可以emit事件的对象(例如EventEmitter的实例)都支持基于事件的异步交互并且你也可以与“blocking code”交互(例如使用文件、sockets或者在node.js中是EventEmitter的子进程)。使用这种方案的话,就能够很好得利用多核的优势了,可以看看:node-http-proxy。

 

内部实现

 

内部,node.js依赖于libev提供的事件循环,libeio是对于libev的补充,node.js使用池化的线程来提供对于异步I/O的支持。如果你想了解更多细节,你可以看一下libev的文档

 

如何在Node.js中实现异步

 

 

Tim Caswell在其PPT中描述了整个模式:

 

  • First-classfunction:例如我们将function作为数据传递,包裹他们以在需要的时候执行。
  • Function组装:就像你了解的关于异步函数或者闭包一样,在触发了I/O事件之后执行。
  • 回调计数器:对于基于事件的回调,你无法保证对于任何特殊的命令,I/O事件都会被执行。所以,一旦你需要多次查询来完成某个处理,通常你仅需要对任何的并发I/O操作进行计数,然后在你确实需要最后的结果的时候检查是否必要的操作都已全部完成(其中的一个例子是在事件回调中,通过对返回的数据库查询进行计数)。查询会被并发执行,并且I/O也对此提供支持(例如可以通过连接池的方式实现并发查询)。
  • 事件循环:上面已经提到过,你可以将blockingcode包裹进一个基于事件的抽象中去(比如通过运行一个子进程,然后当它执行完成之后再返回)。

再次申明原文出处:http://blog.mixu.net/2011/02/01/understanding-the-node-js-event-loop/

 

posted on 2015-09-11 00:08  求知的木头  阅读(934)  评论(0编辑  收藏  举报

导航