分类目录归档:网络编程

Unix网络I/O模型

平时讲到I/O时经常会遇到阻塞、非阻塞、同步、异步这些概念和分类方法。

要解释各种I/O模型的区别,需先了解网络I/O过程中的两个阶段:
1. 等待数据就绪。是指数据从网络到达内核缓冲区。
2. 等待数据拷贝。是指数据从内核缓冲区到达进程缓冲区。

按照『UNIX Network Programming Volume 1: The Sockets Networking API』中的定义,Unix下的网络I/O模型可以分为Blocking I/O,Non-blocking I/O, I/O multiplexing, Signal driven I/O和Asynchronous I/O等5种。

1. Blocking I/O是阻塞I/O模型,这个模型里程序在发起I/O请求后就被阻塞,直到读写操作完成。无论等待数据就绪还是等待数据拷贝阶段,程序都阻塞在那里。程序在发出recvfrom系统调用后,一直等到数据从内核缓冲区拷贝到进程空间缓冲区,才会继续往下执行。如图*
Snip20130706_63

2. Non-blocking I/O是非阻塞I/O模型,这种模型下,程序在发出recvfrom系统调用后,会立即收到一个错误码,表示数据未就绪。此后,程序需不停通过recvfrom轮询,直到内核缓冲区中数据就绪,程序recvfrom调用开始阻塞,进行数据拷贝。所以非阻塞I/O模型的第一阶段是非阻塞的,而第二阶段是阻塞的。如图。
Snip20130706_65

3. I/O Multiplexing Model是I/O复用模型,这种模型的具体实现与Unix版本相关,比如BSD中是select模型,System V中是poll模型,Linux 2.6之后是epoll模型,FreeBSD中是kqueue模型。以select模型为例,程序通过select系统调用监控多个打开文件描述符,select调用有一个文件描述符数组作为传入参数,调用返回后,已就绪的文件描述符会被标记,程序只要遍历文件描述符数组,找出已就绪文件,发起recvfrom系统调用进行数据拷贝即可。上述两个过程中,select系统调用是阻塞的(阻塞时间视监控的文件描述符多少而定,时间复杂度O(N)),recvfrom系统调用也是阻塞的,但是select模型比此前两个I/O模型高效的地方在于,select模型能够同时监控多个文件描述符。I/O复用模型的示意图如下:
Snip20130706_66

select调用向内核传递的是一个数组,因此有大小限制,遗憾的是它最多只能监控1024个文件描述符。poll调用将数组改为了链表,因此不再有同时能监控的文件描述符个数限制。epoll相比select/poll的优势在于增加了基于事件的就绪通知方式,当某个文件描述符上I/O已经就绪(数据已到内核缓冲区),就会通知程序调用recvfrom去进行数据拷贝,而select/poll模型是要程序自己遍历数组/链表,根据标志位判定数据是否已就绪,再调用recvfrom拷贝数据。

4. Signal driven I/O是信号驱动I/O模型,其实也算是I/O Multiplexing Model的一种,Linux 2.4的实现里是通过SIGIO信号通知程序哪些文件描述符变为就绪状态(边缘触发,只通知一次,因此如果内核中信号事件太多时,可能造成通知丢失)。信号驱动I/O模型在I/O的第一阶段是非阻塞的,第二阶段是阻塞的。
Snip20130706_67

5. Asynchronous I/O Model是异步I/O模型,它是由POSIX标准中定义的,Unix有一套实现(以aio_开头的I/O函数)。这个模型里程序在调用aio_read时,会把文件描述符、进程缓冲区指针、缓冲区大小、通知方式等参数传入,然后立即返回,继续其他操作。等I/O完成(数据从内核缓冲区拷贝到进程缓冲区)后,操作系统会给程序一个信号,告知I/O已经完成,数据在指定的缓冲区中。异步I/O模型与前四种I/O模型的重要区别在于异步I/O模型I/O的两个阶段都是非阻塞的,而前四种I/O模型都至少有一个阶段是阻塞的。
Snip20130706_68

归纳起来,5中I/O模型可以用下图表示:
Snip20130706_69

『UNIX Network Programming Volume 1: The Sockets Networking API』对异步和同步作了如下定义:
1. 同步I/O操作在I/O完成期间会引起程序阻塞。
2. 异步I/O操作在I/O完成期间不会引起程序阻塞。
按照这样的定义,上面列举的5种I/O模型里前4种都是同步I/O,最后一种异步I/O模型才算是异步I/O。回到开头关于阻塞/非阻塞、异步/同步的概念,实际上,它们修饰的对象并不相同,阻塞/非阻塞是指程序要访问的数据没有准备好时,是否需要等待(注意是程序是否需要等待,不是CPU,CPU不会被阻塞);而同步/异步则是指获取数据的方式,同步I/O会阻塞程序等待数据到达,而异步操作不会阻塞程序,数据到达后系统会通知程序去取。

* 文中所有图片均来自『UNIX Network Programming Volume 1: The Sockets Networking API』。

--EOF--

Reverse Ajax分类

Ajax默认只能从客户端向服务器端发出请求,服务器端进行响应。Reverse Ajax(反向Ajax)是这样概念:可以从服务器端推送数据到客户端。

实现Reverse Ajax有以下几种技术:

 一. 轮询Polling

轮询方式是指普通Ajax请求以固定时间间隔向服务器端发出请求,服务器的事件只有在客户端下次请求时才返回。固定时间间隔越大,服务器资源(带宽、连接数等)消耗越低,但是通信延时高。固定时间间隔越小,服务器资源消耗大,但是通信延时小。轮询不具备伸缩性。

 二. Piggyback捎带轮询

Piggyback方式允许客户端在需要的时候向服务器端发送请求,如果服务器端有更新内容要发送到客户端,它会等待客户端下一次请求,然后把期间所有已经就绪的数据一起返回。Piggyback不具备伸缩性。

 三. Comet

Comet 是一个 Web 应用模型,在该模型中,请求被发送到服务器端并保持一个很长的生存期,直到超时或是有服务器端事件发生,在该请求完成后,发出另一个长生存期的 Ajax 请求去等待另一个服务器端事件。

Comet可分为HTTP流模式和长轮询(long polling)模式两类。
1. HTTP流模式:
    1). 页面中放置一个的隐藏Iframe标签,该标签的src属性指向返回服务器端事件的 servlet 路径。每次在事件到达时,servlet 写入并刷新一个新的 script 标签,该标签内部带有 JavaScript 代码,iframe 的内容被附加上这一 script 标签,标签中的内容就会得到执行。
    2). Servlet 3.0 + ajax multi-part可以实现一个长连接接收多次响应(也可理解为一个响应分多次返回)。一些浏览器支持XMLHttpRequest对象的multi-part标记置为ture,在服务器端需要异步Servlet支持(Servlet 3.0)。缺点:不是所有浏览器都支持multi-part标记。只有Gecko内核浏览器才支持,如Firefox。

2.长轮询(long polling)模式:
一个长连接由服务器端保持着打开的状态,只要一有事件发生,响应就会被提交,然后连接关闭。接下来一个新的长连接会被客户端重新打开,然后重复上述过程。
    1). 利用iframe标签。服务器端挂起连接直到有事件发生,接着把脚本内容发送回浏览器,然后重新打开另一个 script 标签来获取下一个事件。
    2). XMLHttpRequest长轮询:客户端打开一个到服务器端的Ajax请求然后等待响应。服务器端挂起请求,只要一有事件发生,服务器端就会在挂起的请求中返回响应并关闭该请求,以及关闭servlet响应的输出流。然后客户端打开一个新的到服务器端的长生存期的Ajax请求。Ajax长轮询请求Comet能在所有支持简单Ajax请求的浏览器上正常工作。

 四. Websocket

WebSocket支持双向、全双工通信信道,通过HTTP请求(也称为 WebSocket握手)和一些特殊的标头 (Header)使连接一直处于激活状态。客户端可以用JavaScript发送和接收数据,就像使用TCP socket一样,可由浏览器商本地(native)实现 ,或通过将调用委托给称为FlashSockets的隐藏的Flash组件的网桥来实现。服务器端对WebSocket的处理情况复杂些,现在还没有Java规范以标准的方式来支持WebSocket。要使用Web容器(例如Tomcat或Jetty)的WebSocket功能就要把应用代码和容器特定的库紧密耦合。jWebSockets是采用 Java 和 JavaScript 开发的实现了 HTML5 Websocket 协议的开源框架, 包含了jWebSocket Server、jWebSocket Clients和jWebSocket FlashBridge。

目前已经支持WebSocket的浏览器有IE 10+, Firefox 11+, Chrome 14+, Safari 6+, Opera 12.1+, iOS Safari 6.0+, Blackberry Browser 7+, Opera Mobile 12.1+, Chrome for Android 25+, Firefox for Android 19+.

支持Reverse Ajax 的类库最佳选择将是能检测到是否支持WebSocket,如果不支持WebSocket,则回退到Comet(长轮询)。

Reference:
[1] IBM developerWorks, 反向 Ajax,第 1 部分: Comet 简介.
[2] IBM developerWorks, 反向 Ajax,第 2 部分: WebSockets.
[3] jWebSocket, http://jwebsocket.org.

--EOF--

异步socket实时系统的设计

使用WSAAsyncSelect异步选择模型时, MSDN上要求必须使用是一个窗口句柄与通信socket关联。某些工控系统Client和Server间交互频繁,网络中数据量较多,而且往往这些系统对实时性和可靠性要求也高,例如一些灾害预警系统。

现有这样一个系统,Server端程序为RS-485或者CAN总线网络的控制主机软件,Client端程序为监控终端软件,监控终端软件的主要需求是实时监控和配置总线网络中从设备。拓扑结构如下:

1
2
3
4
5
6
7
8
9
10
                            +----------+         +----------+
                            |          |         |          |
                            |  Client  <--------->  Server  |
                            |          |         |          |
                            +----------+         +-----^----+
                                                       |
                                                       |
                            +------//------------------v----+
                            |    RS-484 or CAN network      |
                            +------//-----------------------+

图 系统拓扑结构
(Download ASCII Picture)

Client端程序负责socket消息的发送和接收,并且将接收到的数据交给不同的页面进行可视化展现,同时还要响应用户的不同操作、分析操作类型,并进行命令报文的封装。Server端程序负责接收Client端的socket报文,根据报文内容去总线网络执行相关命令,并将反馈数据返回至Client端程序。无论是Client端程序还是Server端程序,如果只是单纯地跑一个窗口进程,很容易造成消息栈溢出,报文丢失的情况,无法满足工控系统中实时性和可靠性两个主要考核指标。因此这样的系统采用多线程是一种必须的手段。

因为系统采用的是异步socket模型,故socket的收发不必同步完成。对于这类系统我的设计思路是:

1. 在Client端将socket收发和响应用户操作的命令分别放在两个不同的处理线程中,主线程(窗口线程)负责socket接收,分析收到的报文类型,将其dispatch至不同的页面进行展现。副线程负责响应用户操作,例如进行实时监控时定期轮询Server,以及一些较为复杂和费时的配置报文封装等等,并负责将socket报文发送到Server端。

2. 在Server端将socket收发和与总线网络交互分别放在不同的处理线程,主线程 (窗口线程)负责socket接收,副线程负责与RS-485或CAN总线网络进行交互,并将总线网络的反馈数据返回至Client端程序。主线程和副线程之间通过一个报文队列进行通信,每次主线程接收到一个socket报文后,将此报文push进报文队列,而副线程会轮询报文队列,当队列不为空时,pop出队首报文,进行处理。报文队列的线程同步方法采用临界区的方法。此外,有时候监控终端长时间没有指令发出,造成Server端的副线程无意义地轮询空报文队列,为了避免这种情况,主副线程之间同时还采用事件通知的方法进行通信。若当前报文队列已为空,则副线程调用WaitForSingleObject阻塞自己,直到主线程设置event。主线程每收到一个socket报文并放入报文队列后都会SetEvent唤醒副线程(如果此刻副线程被阻塞的话)。

--EOF--

MFC中Winsock异步选择模型编程

默认的Winsock是工作在阻塞模式下的,意思是服务器/客户端的accept/connect、recv/send这样的操作必须成对同步出现,否则程序就会阻塞在函数调用的地方,这种方式的弊端显而易见,程序员必须严格匹配请求和响应、读写的顺序,甚至读写的字节数。有些情况甚至根本不能用阻塞模式的Winsock来实现,比如聊天程序,通讯双方send的顺序和时机都是随机的,这时就要用到Socket的异步选择模型来实现。WSAAsyncSelect(异步选择)是一种非阻塞模式的异步socket I/O模型。它是一个基于消息的异步I/O模型,可以设置基于这个消息的网络事件通知对socket通讯进行异步管理。

异步选择模型的实现需要一个窗口句柄,这个窗口作为接收消息的载体,各种网络事件通知都会附在这个消息上进行传送。调用WSAAsyncSelect()函数可以将新生成的socket与窗口、网络事件建立联系,同时设定消息和感兴趣的网络事件通知。例如:

1
WSAAsyncSelect(sock, hWnd, WM_SOCKET, FD_ACCEPT|FD_READ|FD_CLOSE);

设置好之后函数,调用者就可以在hWnd窗口收到WM_SOCKET消息后,在套接字sock上对对应的接受连接事件(FD_ACCEPT)、接收内容事件(FD_READ)和关闭连接事件(FD_CLOSE)进行处理。具体WSAAsyncSelect()函数的使用可以参看MSDN相关页面

MFC中调用WSAAsyncSelect()实现异步socket I/O非常方便。
1、在头文件中声明一个自定义消息。

1
#define WM_SOCKET (WM_USER+1234)

2、在需要使用到异步选择模型的socket套接字上调用WSAAsyncSelect()函数。

1
WSAAsyncSelect(s, GetSafeHwnd(), WM_SOCKET, FD_ACCEPT|FD_CLOSE|FD_READ);

GetSafeHwnd()表示取得当前的窗口句柄,如果要设置别的窗口来接收WM_SOCKET消息,可以通过FindWindow()函数来得到那个窗口的句柄。

3、添加消息处理函数。这里又分成三步处理:
3.1、窗口类声明中添加声明:

1
afx_msg void OnSocketHandler(WPARAM wParam, LPARAM lParam);

3.2、修改BEGIN_MESSAGE_MAP宏,建立WM_SOCKET消息和消息处理函数OnSocketHandler()之间的映射。

1
2
3
4
5
6
7
8
BEGIN_MESSAGE_MAP(CXxxxxDlg, CDialog)
    //{{AFX_MSG_MAP(CXxxxxDlg)
    ON_WM_SYSCOMMAND()
    ON_WM_PAINT()
    ON_WM_QUERYDRAGICON()
    ON_MESSAGE(WM_SOCKET, OnSocketHandler) //add
    //}}AFX_MSG_MAP
END_MESSAGE_MAP()

3.3、编写消息处理函数,对WSAAsyncSelect()函数注册的网络事件通知分别进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void CXxxxxDlg::OnSocketHandler(WPARAM wParam, LPARAM lParam)
{
    SOCKET sock = (SOCKET)wParam;
    if(WSAGETASYNCERROR(lParam))
    {
        //错误处理
    }else
    {
        switch(WSAGETSELECTEVENT(lParam))
        {
            case FD_ACCEPT:
                //接受客户端连接处理
                break;
            case FD_CLOSE:
                //关闭连接处理
                break;
            case FD_READ:
                //准备接收处理
                break;
        }
    }
}

over~~

--EOF--