第一课:同步IO
首先,协议有很多层,这个你在网络课上应该了解过吧 。物理层 数据链路层 协议层 之类。从上层来说,常见协议一般有两种,一种是流式的,一种是包式的 。UDP 我们暂时不讨论,先说TCP 它是一个流式协议 。
一个tcp的connection就相当于一个basic_iostream的子类,可以不断地read和write 。可以不断地read和write的东西,我们就认为是一个i/o对象 。 在同步io的观念里,读一个东西,只要还有东西可读,就一定能读出来 。就像如果我们有个fstream,只要没feof,就可以不断地read 。也就是说一个tcp的connection 能被 read and write? 发送数据相当于write,接受数据相当于read。文件、TCP连接、控制台输入输出 各自都是一种形式的iostream ,从read和write这两个方法上来说,没有什么区别 。
同步IO,也应该是你所熟悉的I/O形式,也就是一个stream f,就可以f.read(),f.write() 只要文件没有结束,就可以读出来。但是,除了“文件结束”这种读不出数据的情况下,还有另一个类似的情况,叫做“数据还未就绪”。比如说,硬盘资源占满了,导致暂时不能去读写硬盘,或者网络连接对方还未发送数据的时候我们读,或者发送缓冲区满的时候我们去写。
这种情况下,传统的同步IO模型,就让这个方法一直处在等待态,直到硬盘恢复,或者对方发送了数据,然后就返回 。也就是在“数据就绪”前,是阻塞在这个方法内部的 。
设想一个双人聊天系统,A给B发送消息,同时B也给A发送消息。A这边,要能发送A输入的消息,也就是:
while (cin>>s)
{
sock<<s;
}
B这边要能接受A发过来的消息,就是:
while (sock>>s)
{
cout<<s;
}
另外,反过来说,A和B 是一样的 ,A也要接受B发送的消息 。所以A这边两段代码都要有。
这个读写的循环怎么构建呢? 答案是没办法 。唯一的办法是:
_beginthread(sendmsg);
_beginthread(recvmsg);
也就是对读写分别建立进程(或者线程) 然后操作系统去维护。 在linux/unix下,建进程的比较多,因为那边进程的消耗比较小 ;在windows下,大部分都是用线程实现 。
其实作为一个经典的客户端程序,这样是很好的,没什么问题。但是作为服务器,就有个很大的问题了。因为给每个用户要建立两个线程,每个socket 都可能同时要读和写(我问:“厄~ 读 可以把所有用户都统一到一个里面么?”,其实这就是跟异步IO的不同点,参考第二课)。且不说线程切换的代价,线程总数在许多操作系统下都是有限制的。另外每个线程要有自己的运行栈,这个栈所占用的内存也是相当可怕的。所以,我们要减少线程/进程数。
ok,第一课:同步I/O讲完了。
第一课其实就是提出了个问题:服务器不能用传统的IO方式 怎么办?其实同步I/O也有很多经典的模型,不过我略过去了,讲的话要讲很久。其实许多服务器都是用同步模型的IIS、Apache都是多线程/多进程。IIS、Apache并没有解决第一课提出的问题。
这个叫同步模型也就是,每个线程同步的自己做自己的事 。同步模型的麻烦主要是一些多线程机制,比如不是request-response only的服务器,要加锁,搞buffer什么的。
如果我们同时进行1000个逻辑任务(比如接受1000个用户的请求),那么我们就要准备1000个线程。一般作为服务器只要
while (read())
{
write();
}
所以一个用户对应一个线程,然后阻塞就阻塞好了。这个模型中 服务器只有响应用户请求的角色,而有些服务 服务器有可能主动发给用户东西,这个就是 request/response和request/response/notify的区别了。首先,IIS和Apache,这两个例子都是HTTP的,都只是request/response的 ,没有notify的需求。其次,我接触过的大部分包含notify机制的服务器,都是用异步服务实现的,如果用同步服务,也有解决方案,用一个加了线程锁的bufferedsocket。
在这里,同步和异步指的就是“是否N个逻辑任务,就对应着N个并行的线程/进程 ”。同步的就是“是”,异步的就是“否”
第二课:异步IO
刚才说到那个同步模型,每个逻辑任务对应一个线程。但是我们会发现,大部分时候所有线程都在等待态,真正处理事务的时候并不多。所以我在无数人已经商业运用了异步IO之后,回想初中时古老的程序,再次重复发明了异步IO(上面这句话是自我讽刺,可以忽略 )。总之,回忆我当时的一个程序,是这样的 :
while (true)
{
//画动画
}
但是为了能够获取输入 ,那时候并不会线程的我,这么写的
while (true)
{
if (has_input)
{
process_input();
}
draw();
}
当然,作为我们服务器来说,没有draw,但是有一大堆的input和output ,所以可以这么写:
异步IO模型1,传统异步IO:
while (true)
{
for (sock in socks)
{
if (sock.has_input())
process(sock.input());
}
}
这就是最最基本的一种异步模型。
ok.. 这是1个线程了,但是处理的东西还是1000sock 的 socks。实际应用的空间不变,但额外开销少了很多很多,这就是第一种形式。
从实现上来说,我们可以给一个socket设置一个属性,使得read在没有数据的时候,返回一个false,而非等待。
然后提出第二种模型,也是在几年以前(包括我读过的Google的源代码里)最常见的一种模型,叫select模型,它的结构是类似这样的:
异步IO模型2,select模型:
while (true)
{
sock = select who has events from socks;
process(sock.read());
}
这个select,它的意义是:如果一堆socks里有至少一个sock可以读,就返回其中一个可以读的sock,否则,阻塞等待。应该来说,已经很完美了——因为很多人都在用 ,但是显然可以做的更完美.
select的缺点是 :如果有1000个sock连着在了,就是有1000个sock,每次拿出一个可读的sock的时候,select实际上遍历了这个socket数组。换句话说,一次select的复杂度是O(N)的。
实际上一个服务器的性能,要求有数据来时总能立即处理。只有特殊的有其他方面性能压力的服务器,才需要用堆来维护请求列表。但是,维护的是请求列表,不是sock ,sock总是假设能够立即处理的。就是,总是能及时read(sock),但不一定能及时process(请求列表)。
好,select模型的缺陷是每次select都是O(N)。换言之,处理N个用户的M次“数据就绪”,复杂度是O(N*M)的,这是它的缺陷 。
另外你要知道TCP所依赖的底层协议是基于数据包的,如果需要对TCP结构进行优化,必须对TCP如何通过数据包来实现有所了解 ,就像上次FM说的那个问题(网卡接收速率不及客户端发送速率)。好,先不谈那个,我们继续讨论异步IO。
所以下面列出两种模型,一种是WSAEvent/Epoll模型,一种是IOCP模型。你要理解这两种在哲学上是可以互相转化的,这个我们稍候讨论。
前面一种模型其实就是event模型,是这样
异步IO模型3,WSAEvent/Epoll模型(event 模型):
while (GetSockEvent(&sock, &msgtype))
{
switch(msgtype)
{
case CANREAD: sock.read(); break;
case CANWRITE: //xxx(); break;
case CLOSE: //xxx(); break;
}
}
首先,当一个sock发生事件的时候,网卡本身是知道的,并且网卡驱动是能找到对应的socket的。如果我们不去select它,而是让它把这样的事件push到一个event queue里,我们从event queue里取出event,直接处理对应的sock。这就是Event模型。在WSA里,用窗口消息实现。MFC的CSocket就是这么做的 ,在linux下epoll也实现了同样的功能 。这也是公认最优雅的解决方案,最适合做面向对象封装 。
看上去好像就是把一部分事情 放到低层去做了,但其实不算是 。是本来做了很多明明根本不需要做的事情,现在咱不做了。
然后说IOCP模型,这个模型在Windows下用的比较多,因为在windows下比较高效。全称是Input/Output Completion Port,中文是完成端口 。
它不同于事件模型,它的事件不是“可以读”或“可以写”,而是“读完成了”或“写完成了” 也就是说,你先调sock.read();,然后得到一个E_PENDING返回值,ok,然后去你的队列等这个消息,你就会等到一个消息说这个read完成了,这个模型应该说是最高效的。
它最重大的也是最受争议的特性之一是,缓冲区不再由系统提供,而是完全由调用者提供,恩……简单的说,和event对比:如果我们要用异步I/O实现一个同步I/O,
event的方法是:
WaitForEvent(&sock);
sock.read();
IOCP的方法是:
sock.read();
WaitForReadComplete();
总之就是,这个read过程,是从你调用开始,一直持续到你收到Complete事件。在这种机制下,read只是把你提供的缓冲区交给驱动,读的过程你并不知晓,读完以后,驱动再给你提供一个通知 。而event模型事实上是在socket建立好就开始读了 ,由驱动提供了缓冲区,当驱动给你提供通知的时候,事实上数据已经在缓冲区里了,然后你再去读的时候,实际上是进行了一次缓冲区复制,IOCP省掉了这次复制。
因为我说了,event模型是最优雅、最适合封装成面向对象形式的模型,所以,大部分IOCP的面向对象封装,都在事实上用IOCP模型实现了一个event模型。
实际上select event iocp 三种模型,都是可以互相实现的。只是select天生慢,用select实现的event和iocp不能优化掉select需要优化的那一部分——就是每次select都要遍历整个socks列表。
select模型跟传统异步IO模型的区别只在没有请求的时候:传统模型CPU 100%,select CPU 0% 。
select模型:如果一堆socks没有sock可以读则阻塞等待,不占用CPU;而传统异步IO模型:某个sock不可读 就去找下一个,都不可读,就一直都在循环里。。。
硬件实质上是IOCP模型。select、event、甚至连同步,都是在IOCP之上进行封装而实现的,区别是在驱动层还是应用软件进行封装而已
附加两个定理:
1. Event模型和IOCP模型可以互相实现
2. 异步IO可以实现同步IO,同步IO在允许额外线程的情况下可以提供异步IO
作业:去网上搜索WSAEvent、IOCP、Epoll三种实现的源代码,阅读