在使用网络socket的时候,由于操作系统的差别很大,其底层实现会有各种不同,其中最大量使用的非阻塞的tcp和udp更是各有不同,其中Linux用epoll,BSD用kqueue,windows用完成端口等方法。这会给网络编程带来一定的麻烦,实现通讯接口层代码级兼容的方法,最常听到的是ACE,这个框架无疑把网络通讯,线程通讯等方法都封装得很好很完美,代码亦非常优秀。但其复杂程度过高,并不适合于初学者或者追求简单为美的开发人员。而libev,libevnet这类库则是跨平台方面相对麻烦。
故,若你喜欢匕首而非砍刀的时候,boost的asio库,会是你最好的选择。
非阻塞IO一般用途是作为服务器端的大并发连接,或者客户端非阻塞的方式运行(试想一下,客户端总不能等网络数据过来的时候,一直卡死不动吧),下面选了boost中关于客户端编程的asio例子进行分析说明。
选用的是chat_client,其实国内大部分tcp/ip的编程教程都选用echo server/client作为首选,简单倒是简单,但往往不是正确的选择。因为网络通讯中,除了RR模式(request/reply一问一答)之外,还存在大量的不确定的谁主动发送谁接收情况。这种情况最典型的就是聊天室,你永远不知道什么时候对方会说话,也就无法判断何时该调用recv。故,chat_client其实最适合作为案例解读
在阅读客户端代码之前,我们首先了解一下基础的几个概念,以便于理解,写着写着突然觉得有很多东西要写,如同步异步,阻塞非阻塞,线程,线程池的使用场合等,这里只能做几个假设,假设读者完全了解以上概念及应用场合,否则说起来就很罗嗦。
1. io_service类,官网上说是你的程序和OS之间的一个接口层,不妨把它看作你的socket操作类,通过操作io_serivce对socket进行发送,接收,关闭等操作。它是线程安全的。
2. boost::asio::ip::tcp::socket,真正的socket类,你可以通过下面的方法把socket绑定再io_service中,目前用到的是一对一的使用,一对多还没探究。
boost::asio::ip::tcp::socket socket(io_service);
3. boost::system::error_code 类,错误代码类,基本上OS返回的错误都会通过这个返回错误码,不会抛出异常
4. boost::bind()这个函数是用来绑定的,如F(b,c,d)的写法,用bind就是boost::bind(F, b,c,d),看代码时记得先查查bind的用法
chat_client,看代码之前,说一下此类的流程和结构
1. 定义好接收处理缓冲和发送队列
2. 注册(回调)连接处理函数
3. 连接成功后,注册(回调)包头处理函数
4. 接收到包头后,注册(回调)包体处理函数,即收包后的处理逻辑
大家会问,都是收包的逻辑,发包的呢,别急,继续看main()函数就知道了。
1. 实例化client的实例,并连接到服务上
2. 生成一个新的线程对象,把生成的io_service::run()方法作为新线程的执行入口,把io_service作为参数传入run()中,注意,此时整个程序已经有2个不同的线程,即主线程及刚生成的线程对象t,线程t绑定了io_service::run(),则主要为收包及处理收包的作用。
3. 主线程继续执行,就会看到死循环,不断的阻塞,输入,发送,即主线程中的主逻辑,我们可以需要执行的操作都写在这里。
4. 现在我们拥有了2个独立执行的线程,一个处理主逻辑,一个等待网络中发过来的数据包。即完成了从用户角度上看的,同时进行收发工作。我们的客户端算是可用鸟:)
程序如下,chat_client.cpp,至于chat_server的代码就不贴了,server的代码到处都是,没什么需要分析的。需要测试的同志们可以到boost主页下载编译执行。chat_client与chat_server,
编译的方法为
g++ chat_client.cpp -o chat_client -lboost_system -lboost_thread
g++ chat_server.cpp -o chat_server -lboost_system -lboost_thread
#include <cstdlib>
#include <deque>
#include <iostream>
#include <boost/bind.hpp>
#include <boost/asio.hpp>
#include <boost/thread/thread.hpp>
#include "chat_message.hpp"
using boost::asio::ip::tcp;
//定义了一个发送队列
typedef std::deque<chat_message> chat_message_queue;
class chat_client
{
private:
boost::asio::io_service& io_service_;
tcp::socket socket_;
chat_message read_msg_;
chat_message_queue write_msgs_;
public:
chat_client(boost::asio::io_service& io_service,
tcp::resolver::iterator endpoint_iterator)
: io_service_(io_service), socket_(io_service) {
//构造函数时期即对连接成功进行回调注册
boost::asio::async_connect(socket_, endpoint_iterator,
boost::bind(&chat_client::handle_connect, this,
boost::asio::placeholders::error));
}
void write(const chat_message& msg) {
//Post向队列中投递任务,然后激活空闲线程执行任务。
io_service_.post(boost::bind(&chat_client::do_write, this, msg));
}
void close() {
io_service_.post(boost::bind(&chat_client::do_close, this));
}
private:
/**
* 连接处理函数,一旦连接上来,直接会调用此函数
*/
void handle_connect(const boost::system::error_code& error) {
if (!error) {
//先保证读取足够整个头部,read_msg_.data()实质上是返回一个分配了缓冲的char *指针。
boost::asio::async_read(socket_,
boost::asio::buffer(read_msg_.data(), chat_message::header_length),
//注册连接成功后,回调handle_read_header()函数
boost::bind(&chat_client::handle_read_header, this,
boost::asio::placeholders::error));
}
}
/**
* 连接成功后,调用此函数读取头部信息
*/
void handle_read_header(const boost::system::error_code& error) {
if (!error && read_msg_.decode_header()) {
//然后读取剩余的包体长度,并注册读取包体的函数,read_msg_.body()实际上是返回了一个同样分配了缓冲的char *指针
boost::asio::async_read(socket_,
boost::asio::buffer(read_msg_.body(), read_msg_.body_length()),
//注册读取头部完成后,回调handle_read_body函数
boost::bind(&chat_client::handle_read_body, this,
boost::asio::placeholders::error));
} else {
//出现错误则关闭
do_close();
}
}
void handle_read_body(const boost::system::error_code& error) {
if (!error) {
//注意,这2句就是收到包之后直接显示内容,这里应该替换你所需要的解包逻辑及包处理逻辑2部分。
std::cout.write(read_msg_.body(), read_msg_.body_length());
std::cout << "\n";
//回调完毕后,再次注册读取头部函数,这是必须的,保证每次处理完之后,重新注册读取头部函数
boost::asio::async_read(socket_,
boost::asio::buffer(read_msg_.data(), chat_message::header_length),
boost::bind(&chat_client::handle_read_header, this,
boost::asio::placeholders::error));
} else {
do_close(); }
}
void do_write(chat_message msg) {
bool write_in_progress = !write_msgs_.empty();
write_msgs_.push_back(msg);
if (!write_in_progress) {
//把队列为空作为发送完成的标记,以保证发送出错的时候,还能按发送顺序进行
boost::asio::async_write(socket_,
boost::asio::buffer(write_msgs_.front().data(),
write_msgs_.front().length()),
boost::bind(&chat_client::handle_write, this,
boost::asio::placeholders::error));
}
}
void handle_write(const boost::system::error_code& error) {
if (!error) {
write_msgs_.pop_front();
if (!write_msgs_.empty()) {
//注册回调,若有问题会在error 上返回,一旦出错就会断开连接
boost::asio::async_write(socket_,
boost::asio::buffer(write_msgs_.front().data(),
write_msgs_.front().length()),
boost::bind(&chat_client::handle_write, this,
boost::asio::placeholders::error));
} else {
}
} else {
do_close();
}
}
void do_close() {
socket_.close();
}
};
int main(int argc, char* argv[]) {
try {
if (argc != 3) {
std::cerr << "Usage: chat_client <host> <port>\n";
return 1;
}
//定义io_service,可以把它看作是socket类
boost::asio::io_service io_service;
//provides the ability to resolve a query to a list of endpoints.
tcp::resolver resolver(io_service);
//The ip::basic_resolver_query class template
tcp::resolver::query query(argv[1], argv[2]);
//An iterator over the entries produced by a resolver.
tcp::resolver::iterator iterator = resolver.resolve(query);
//初始化一个对象c,作为client的实例
chat_client c(io_service, iterator);
//生成线程对象t, 绑定在io_service::run方法中, 把io_service作为桥梁在2个进程间进行通讯
//目的是用另外一个线程对socket进行非阻塞回调的接收及处理,不影响主线程操作。
boost::thread t(boost::bind(&boost::asio::io_service::run, &io_service));
char line[chat_message::max_body_length + 1];
//注意,下面的while循环则应该是程序的主循环,也应该封装成一个函数供调用为好
while (std::cin.getline(line, chat_message::max_body_length + 1)) {
using namespace std; // For strlen and memcpy.
chat_message msg;
msg.body_length(strlen(line));
memcpy(msg.body(), line, msg.body_length());
//构造包头(主要是长度,这是最简单实用的方法)
msg.encode_header();
//直接写入数据到socket
c.write(msg);
}
c.close();
t.join();
} catch (std::exception& e) {
std::cerr << "Exception: " << e.what() << "\n";
}
return 0;
}
补充:注意事项,由于asio库需要跨平台,某些OS是通过启动额外线程来实现非阻塞的。故有2点要求
1. 不要直接调用封装层内部的代码(asio)
2. 不要使用信号(这点必须注意)
后记:asio确实相当不错,支持TCP,UDP,ICMP,TIMER,串口和SSL等,对我来说已经足够使用了。若有问题留言即可。
下篇写点啥呢?