同步VS异步
Boost.Asio的作者做了一个很惊艳的工作:它可以让你在同步和异步中自由选择,从而更好的适应你的应用。
在之前的章节中,我们学习了每种类型应用的框架,比如同步客户端,同步服务端,异步客户端,异步服务端。每一个你都可以作为你应用的基础。如果要更加深入地学习各种类型应用的细节,请继续。
混合同步异步编程
Boost.Asio库允许你进行同步和异步的混合编程。我个人认为这是一个坏主意,但是Boost.Asio(就像C++一样)在你需要的时候允许你深入底层。
通常来说,当你写一个异步应用时,你会很容易掉入这个陷阱。比如在响应一个异步write操作时,假设,你做了一个同步read操作:
io_service service; ip::tcp::socket sock(service); ip::tcp::endpoint ep( ip::address::from_string("127.0.0.1"), 8001); void on_write(boost::system::error_code err, size_t bytes) {
char read_buff[512];
read(sock, buffer(read_buff)); }
async_write(sock, buffer("echo"), on_write);
毫无疑问,同步read操作会阻塞当前的线程,从而导致其他任何正在等待的异步操作变成挂起状态(对这个线程)。这是一段糟糕的代码,因为它会导致整个应用变得无响应或者整个被阻塞掉(所有异步运行的端点都必须避免阻塞,而执行一个同步的操作违反了这个原则)。
当你写一个同步应用时,你不大可能执行异步的read或者write操作,因为同步地思考已经意味着用一种线性的方式思考(执行A,然后执行B,再执行C,等等)。
我唯一能想到的同步和异步同时工作的场景就是同步操作和异步操作是完全隔离的,比如,同步和异步从一个数据库进行读写。
从客户端传递信息到服务端VS从服务端传递信息到客户端
成功的客户端/服务端应用一个很重要的部分就是来回传递消息(服务端到客户端和客户端到服务端)。你需要指定用什么来标记一个消息。换句话说,当读取一个输入的消息时,你怎么判断它被完整读取了?
标记消息结尾的方式完全取决与你(标记消息的开始很简单,因为它就是前一个消息之后传递过来的第一个字节),但是要保证消息是简单且连续的。
你可以:
- 消息大小固定(这不是一个很好的主意,如果我们需要发送更多的数据怎么办?)
- 通过一个特殊的字符标记消息的结尾,比如’\n’或者’\0’
- 再消息的头部指定消息的大小
我在整本书中间采用的方式都是“使用’\n’标记消息的结尾”。所以,每次读取一条消息都会如下:
-
char buff_[512]; // 同步读取 read(sock_, buffer(buff_),
boost::bind(&read_complete, this, _1, _2)); // 异步读取
async_read(sock_, buffer(buff_),MEM_FN2(read_complete,_1,_2), MEM_FN2(on_read,_1,_2));
size_t read_complete(const boost::system::error_code & err, size_t bytes) {
if ( err) return 0; already_read_ = bytes; bool found = std::find(buff_, buff_ + bytes, ‘\n‘) < buff_ +
bytes; // 一个一个读,直到读到回车,无缓存 return found ? 0 : 1;
}
我把在消息头部指定消息长度这种方式作为一个练习留给读者;它是非常简单的。
客户端应用中的同步I/O
同步客户端一般都能归类到如下两种情况中的一种:
- 它向服务端请求一些东西,读取结果,然后处理它们。然后请求一些其他的东西,然后一直持续下去。事实上,这很像之前章节里说到的同步客户端。
- 从服务端读取消息,处理它,然后写回结果。然后读取另外一条消息,然后一直持续下去。
两种情况都使用“发送请求-读取结果”的策略。换句话说,一个部分发送一个请求到另外一个部分然后另外一个部分返回结果。这是实现客户端/服务端应用非常简单的一种方式,同时这也是我非常推荐的一种方式。
你可以创建一个Mambo Jambo类型的客户端服务端应用,你可以随心所欲地写它们中间的任何一个部分,但是这会导致一场灾难。(你怎么知道当客户端或者服务端阻塞的时候会发生什么?)。
上面的情况看上去会比较相似,但是它们非常不同:
- 前者,服务端响应请求(服务端等待来自客户端的请求然后回应)。这是一个请求式连接,客户端从服务端拉取它需要的东西。
- 后者,服务端发送事件到客户端然后由客户端响应。这是一个推式连接,服务端推送通知/事件到客户端。
你大部分时间都在做请求式客户端/服务端应用,这也是比较简单,同时也是比较常见的。
你可以把拉取请求(客户端到服务端)和推送请求(服务端到客户端)结合起来,但是,这是非常复杂的,所以你最好避免这种情况。把这两种方式结合的问题在于:如果你使用“发送请求-读取结果”策略。就会发生系下面一系列事情:
客户端写入(发送请求)
服务端写入(发送通知到客户端)
客户端读取服务端写入的内容,然后将其作为请求的结果进行解析
服务端阻塞以等待客户端的返回的结果,这会在客户端发送新请求的时候发生
服务端把发送过来的请求当作它等待的结果进行解析
客户端会阻塞(服务端不会返回任何结果,因为它把客户端的请求当作它通知返回的结果)
在一个请求式客户端/服务端应用中,避免上面的情况是非常简单的。你可以通过实现一个ping操作的方式来模拟一个推送式请求,我们假设每5秒钟客户端ping一次服务端。如果没有事情需要通知,服务端返回一个类似ping ok的结果,如果有事情需要通知,服务端返回一个ping [event_name]。然后客户端就可以初始化一个新的请求去处理这个事件。
复习一下,第一种情况就是之前章节中的同步客户端应用,它的主循环如下:
void loop() { // 对于我们登录操作的结果 write("login " + username_ + "\n"); read_answer(); while ( started_) {
write_request(); read_answer(); ...
} }
我们对其进行修改以适应第二种情况:
void loop() { while ( started_) {
read_notification();
write_answer(); }
} void read_notification() {
already_read_ = 0; read(sock_, buffer(buff_),
boost::bind(&talk_to_svr::read_complete, this, _1, _2)); process_notification();
} void process_notification() {
// ... 看通知是什么,然后准备回复 }
PS:新的迭代再次来袭…