事件驱动编程
文章目录
1 回顾
这里的回顾主要涉及到我自己的学习。如果是初学者的话,可以跳过,直接到2.
1.1 阻塞编程
一般情况下,我们在最开始学习网络编程的时候,这里主要指socket编程的时候。当我们调用read和write的时候,一遍都是使用阻塞模式下的read和write。
对于read来说,当stream中没有数据时,read就会发生阻塞。
对于write来说,当stream满了的时候,write就会发生阻塞。
那么问题来了,就以简单的聊天服务器举例。我的收端是不知道发端在什么时候发送信息的,当收端持有发端的socket后,难道要一直在这里无限等待么?那么其它的连接该什么办?
有人肯能会说多线程啊。
是的。
正如tinyhttpd上的做法那样。
socket
bind
listen
while(1)
{
newConn = accept
pthread_process(newConn);
}
每当我们收到一个客户端连接的时候,我们就启动一个线程来负责它。这被称作one-thread one-connection模式。但是
- 对于C或者C++来说,线程的启动和销毁开销对于我们补鞥呢接收。
- 线程对于C语言来说,虽然比进程轻量级,但是相对于goroutine等语言方面支持并发的来说,还是太重了。不可能去让一个线程只负责一个connection。
- 假设一个线程只负责一个connection,那么在A端给B端发送的内容没有发送完毕之前。B端是没有办法给A端回消息的。因为你需要一直在那里阻塞接收A端的内容,直到收够。
1.2 Tips:read和write的局限
看过《linux系统编程》的人都知道,read和write是不会保证你一定收或者发送多少个字节的。
《unix网络编程卷一》提供给了我们readN和writeN来保证我们一定收到/发送N个字节。
而在没有收够的这个期间,我们会一直停留在这里的while循环中。简单的伪代码如下。
int toRecv = N;
int nread = 0;
while(toRecv != 0)
{
int n = read(dst, src + nread, toRecv);
nread += n;
toRecv -= n;
}
2. 事件驱动编程
什么是事件驱动编程?这里理解比较难点,可以先放着。
事件驱动编程简单来说,就是为对应的事件事先编好事件处理程序,当事件发生时,就会直接调用事件处理程序。
理解这里的关键是理解非事件编程。非事件编程实际上就是需要一个条件判断来不断判断事件有没有发送。就比如查看的你的工资有俩种方法。1. 不断在app上查看余额,来判断你的工资是否到账。这就是非事件编程。2. 短信通知,当工资到账时,直接会有短信通知,这就是事件编程。如果将人的精力比作CPU的话,显然非事件编程就比较耗费CPU。
简而言之,就是阻塞编程不好。很简单,那么就使用非阻塞编程+IO复用就可以了。
对于非阻塞编程来说,就是我read一次,不论stream中有无数据,不论我读到多少,我读完一次,我就结束了,将控制权交还给程序,让程序接着运行后面的事情。
那么问题来了?我怎么知道什么时候去读?
难道是
while(isRead)
{
read_nonblock();
}// 典型的非事件编程
2.1 问题1:什么时候读
我们希望当有读事件到来的时候,有人能告诉我一下,对于程序员来说,只能祈求操作系统来告诉我们了。这就涉及到了IO复用。一般指的是select,epoll。
这里我就不谈如何使用这个函数了。总之,当有读事件/写事件到来的时候,即read-stream有数据,或者write-stream有空位的时候,我们就会被通知到。此时,我们就需要编写对应的事件处理程序。
2.2 问题2:读多少
这个问题实际上已经不是和事件驱动相关了,主要是TCP的分包问题了。
这里我们假设我们的协议为如下格式
字段 | 功能 |
---|---|
len(4字节) | 表示数据长度 |
data | 2进制数据 |
我们知道,我们可以通过len来判断我们接下来要收多少字节。问题是,如果发端在发送len字段的时候,就发送了1+3字节,或者2+2字节。怎么办?
很明显,解决这个问题肯定需要有缓冲区的设置,将我们第一次收到的1或者2字节收取下来。因此对于每个TcpConnection来说,其最起码要有一个接收缓冲区。
我们可以这样做,当读事件触发后,我们先read_nonblock,然后判断是否收到4个字节?,如果没有,就将其存入接收端缓冲区。如果收到4个字节,就读出len长度,然后为这个connection注册一个新的读事件。
实际上,数据endecode(encode/decode)层一般干的就是这事。
3. Reactor模式伪代码
3.1 结构
3.2 简单运行
伪代码如下。
reactor = new Reactor;
Acceptor::void on_process_accept()
{
new_fd = this.accept();
new_handler = new Handler(new_fd);
new_handler.register_handler(on_process_read());
new_handler.register_handler(on_process_write());
reactor.register_handler(new_handler);
}
void main()
{
acceptor = new Acceptor;
acceptor.register_handle(on_process_accept())
reactor.register_handler(acceptor);
reactor.loop(milliseconds(500));
}