1、背景
项目中用到了通信服务器,最开始对于通信服务器的认识还非常浅薄。一直理不清楚通信服务器到底是如何工作的。于是花了些时间来专门研究。
根据百度百科的定义,“通信服务器(Communication Server,可简称CS),是一个专用系统,为网络上需要通过远程通信链路传送文件或访问远地系统或网络上信息的用户提供通信服务。通信服务器根据软件和硬件能力为一个或同时为多个用户提供通信信道”。这么看上去还是有些复杂,简单来说,通信服务器的作用就是做请求转发。
项目中有两套方案来实现通信服务器,一套是简单的HTTP服务器,同时提供代理和反向代理功能,请求数据响应数据直接透传;另一套则是每个接口在通信服务器上必须有对应的接口来转发,以应对更复杂的业务部署场景。从实现上来说,前者在功能相对后者更加简单。那就先从第一套方案开始说起。
2、开始之前
项目中使用C++配合boost库的异步编程来实现。boost的异步编程能提高服务器的使用效率,当然代价就是编码复杂度上升了很多。先花一些篇幅来说说几个基本知识点。
2.1 boost::bind
笔者在开始走读源码之前发现代码有些理解不了,其实不是C++不能理解,而是各种boost::bind理解不了。有关boost::bind的用法可以参看这两篇文章传送门1、传送门2。根据第二条参考:
对自由方法来说,直接boost::bind(函数名, 参数1,参数2,...)
对类方法来说,直接boost::bind(&类名::方法名,类实例指针,参数1,参数2)
一般在boost编程中,常把bind绑定类方法当成回调来用。为什么要使用回调?因为在异步操作中,你不知道调用什么时候结束。传递一个boost::bind仿函数当作参数。这个仿函数内部包含了一个智能指针,指向调用的实例。只要有一个异步操作等待时,Boost.Asio就会保存boost::bind仿函数的拷贝,这个拷贝保存了指向连接实例的一个智能指针,从而保证调用实例保持活动
,从而解决问题。
那在走读源码的时候如何快速理解某个回调的作用呢?根据boost::bind绑定类方法的定义,只需先关注调用这个boost::bind的方法,然后再看boost::bind的具体实现,这是因为因为boost::bind的具体实现是回调,而调用boost::bind的方法完成之后,才会走回调流程。
2.2 boost网络编程中的一些关键API
2.2.1 Boost.Asio命名空间
Boost.Asio的所有内容都包含在boost::asio命名空间或者其子命名空间内。
boost::asio:这是核心类和函数所在的地方。重要的类有io_service和streambuf。类似read, read_at, read_until方法,它们的异步方法,它们的写方法和异步写方法等自由函数也在这里。
boost::asio::ip:这是网络通信部分所在的地方。重要的类有address, endpoint, tcp, udp和icmp,重要的自由函数有connect和async_connect。要注意的是在boost::asio::ip::tcp::socket中间,socket只是boost::asio::ip::tcp类中间的一个typedef关键字。
2.2.2 端点
endpoint,即通信端点,是使用某个端口连接到的一个地址;是通信监听的接口,也是具体的socket接收处理类。不同类型的socket有它自己的endpoint类,比如ip::tcp::endpoint、ip::udp::endpoint和ip::icmp::endpoint
2.2.3 socket
2.2.3.1 连接相关函数
bind(endpoint):这个函数绑定到一个地址
connect(endpoint):这个函数用同步的方式连接到一个地址
async_connect(endpoint):这个函数用异步的方式连接到一个地址
示例:
ip::tcp::endpoint ep( ip::address::from_string("127.0.0.1"), 80);
ip::tcp::socket sock(service);
sock.open(ip::tcp::v4()); n
sock.connect(ep);
sock.write_some(buffer("GET /index.html\r\n"));
char buff[1024]; sock.read_some(buffer(buff,1024));
sock.shutdown(ip::tcp::socket::shutdown_receive);
sock.close();
2.2.3.2 读写函数
async_receive(buffer, [flags,] handler):这个函数启动从套接字异步接收数据的操作。
async_read_some(buffer,handler):这个函数和async_receive(buffer, handler)功能一样。
async_receive_from(buffer, endpoint[, flags], handler):这个函数启动从一个指定端点异步接收数据的操作。
async_send(buffer [, flags], handler):这个函数启动了一个异步发送缓冲区数据的操作。
async_write_some(buffer, handler):这个函数和async_send(buffer, handler)功能一致。
async_send_to(buffer, endpoint, handler):这个函数启动了一个异步send缓冲区数据到指定端点的操作。
receive(buffer [, flags]):这个函数异步地从所给的缓冲区读取数据。在读完所有数据或者错误出现之前,这个函数都是阻塞的。
read_some(buffer):这个函数的功能和receive(buffer)是一致的。
receive_from(buffer, endpoint [, flags])*:这个函数异步地从一个指定的端点获取数据并写入到给定的缓冲区。在读完所有数据或者错误出现之前,这个函数都是阻塞的。
send(buffer [, flags]):这个函数同步地发送缓冲区的数据。在所有数据发送成功或者出现错误之前,这个函数都是阻塞的。
write_some(buffer):这个函数和send(buffer)的功能一致。
send_to(buffer, endpoint [, flags]):这个函数同步地把缓冲区数据发送到一个指定的端点。在所有数据发送成功或者出现错误之前,这个函数都是阻塞的。
示例,在一个TCP套接字上进行同步读写:
ip::tcp::endpoint ep( ip::address::from_string("127.0.0.1"), 80);
ip::tcp::socket sock(service);
sock.connect(ep);
sock.write_some(buffer("GET /index.html\r\n"));
std::cout << "bytes available " << sock.available() << std::endl;
char buff[512];
size_t read =
sock.read_some(buffer(buff));
示例,在一个UDP服务套接字中异步读取数据:
using namespace boost::asio;
io_service service;
ip::udp::socket sock(service);
boost::asio::ip::udp::endpoint sender_ep;
char buff[512];
void on_read(const boost::system::error_code & err, std::size_t read_bytes) {
std::cout << "read " << read_bytes << std::endl;
sock.async_receive_from(buffer(buff), sender_ep, on_read);
}
int main(int argc, char* argv[]) {
ip::udp::endpoint ep(ip::address::from_string("127.0.0.1"),8001);
sock.open(ep.protocol());
sock.set_option(boost::asio::ip::udp::socket::reuse_address(true));
sock.bind(ep);
sock.async_receive_from(buffer(buff,512), sender_ep, on_read);
service.run();
}
2.2.4 自由函数
2.2.4.1 connect方法
这些方法把套接字连接到一个端点。
connect(socket, begin [, end] [, condition]):这个方法遍历队列中从start到end的端点来尝试同步连接。begin迭代器是调用socket_type::resolver::query的返回结果(你可能需要回顾一下端点这个章节)。特别提示end迭代器是可选的;你可以忽略它。你还可以提供一个condition的方法给每次连接尝试之后调用。用法是Iterator connect_condition(const boost::system::error_code & err,Iterator next);。你可以选择返回一个不是next的迭代器,这样你就可以跳过一些端点。
async_connect(socket, begin [, end] [, condition], handler):这个方法异步地调用连接方法,在结束时,它会调用完成处理方法。用法是void handler(constboost::system::error_code & err, Iterator iterator);。传递给处理方法的第二个参数是连接成功端点的迭代器(或者end迭代器)。
using namespace boost::asio::ip;
tcp::resolver resolver(service);
tcp::resolver::iterator iter = resolver.resolve(tcp::resolver::query("www.yahoo.com","80"));
tcp::socket sock(service);
connect(sock, iter);
2.2.4.2 read/write方法(非常重要)
async_read(stream, buffer [, completion] ,handler)
:这个方法异步地从一个流读取。结束时其处理方法被调用。处理方法的格式是:void handler(const boost::system::error_ code & err, size_t bytes);。你可以选择指定一个完成处理方法。完成处理方法会在每个read操作调用成功之后调用,然后告诉Boost.Asio async_read操作是否完成(如果没有完成,它会继续读取)。它的格式是:size_t completion(const boost::system::error_code& err, size_t bytes_transfered) 。当这个完成处理方法返回0时,我们认为read操作完成;如果它返回一个非0值,它表示了下一个async_read_some操作需要从流中读取的字节数。接下来会有一个例子来详细展示这些。async_write(stream, buffer [, completion], handler)
:这个方法异步地向一个流写入数据。参数的意义和async_read是一样的。read(stream, buffer [, completion])
:这个方法同步地从一个流中读取数据。参数的意义和async_read是一样的。write(stream, buffer [, completion])
: 这个方法同步地向一个流写入数据。参数的意义和async_read是一样的。
注意第一个参数变成了流,而不单是socket。这个参数包含了socket但不仅仅是socket。
2、简单的实现方案
2.1 架构框图
方案架构如图所示。通信服务器可部署于DMZ区域中单独的服务器上;也可以和业务服务器部署于一台服务器上,这时候的通信服务器也可以称为安全网关。需要说明的是,在这种部署情况下,从通信服务器接收到的数据是可以直接透传给业务服务器的。
根据业务场景的不同,通信服务器同时提供代理和反向代理的功能。工作在代理模式时,客户端到通信服务器、通信服务器到业务服务器走的都是HTTP(S)协议;工作在反向代理模式时,通信服务器到客户端是HTTP(S)的长连接,通信服务器到业务服务器走的都是HTTP(S)协议。反向代理有地址和范围限制,这个范围和限制有通信服务器定时从业务服务器获取。
2.2 数据请求实现
图中虚线箭头代表回调走向。
重要流程说明:
1、在实例化server的过程中,开启监听:
……
boost::asio::ip::tcp::resolver resolver(acceptor_.get_executor());
boost::asio::ip::tcp::resolver::query query(address, port);
boost::asio::ip::tcp::endpoint endpoint = *resolver.resolve(query);
……
boost::asio::ip::tcp::acceptor.set_option(boost::asio::ip::tcp::acceptor::reuse_address(true));
boost::asio::ip::tcp::acceptor.bind(endpoint);
boost::asio::ip::tcp::acceptor.listen();
……
2、首先接收到客户端连接请求后,实例化各个场景的连接,时序图里以UpstreamHandler为例。实际代码里的实现有四、五种,根据不同的业务场景来重载。server_connection.cpp、client_connection.cpp都是connection的封装,而connection实际上是一路连接的抽象。之后就是标准的boost异步网络编程。
2.2 反向代理数据请求实现
图中虚线箭头代表回调走向。
重要流程说明:
http_tunnel_handle.cpp之前的流程和2.1一致。区别在于:proxy_connection.cpp与server_connection.cpp之间,对于所有的请求转到指定的服务器IP及端口。之后依然是标准的boost异步网络编程。
3、复杂的实现方案
3.1 架构
架构如图所示。看上去,架构似乎简单了些。其实这是一种错觉而已。这种架构的部署更加复杂。通信服务器可以部署于公有云上;可以同简单方案架构一致:部署于DMZ区域中或者当作安全网关来使用。复杂之处恰恰就在部署于公有云上,通信服务器和业务服务器直接的数据没办法透传,需要建立数据通道。它们之间传递数据采用的C/S架构,具体来说通信服务器作为S端,业务服务器作为C端。所以,客户端的请求接口在通信服务器和业务服务器直接也必须实现一遍。
3.2 具体实现
初始化流程与简单方案一致,这里不再赘述。虚线箭头仍然代表回调走向。
重要流程说明:
1、server.cpp开启线程之后,循环等待连接到来。
2、第二个回调handleRequest()表示有连接到来,将数据传递个相应的处理类。
3、这里以mdmc_handler.cpp为例,具体实现还有其他类;mdmc_handler.cpp这一类****handler.cpp代表客户端请求的处理类。图中的第二个回调走向handleRequest()编码较有技巧,这部分花了很多时间来确认。
4、在第三个回调handleRead()之后,通信服务继续进入循环等待状态,等待下一次客户端的请求连接到来。此时,通信服务器继续将从业务服务器得到的请求结果返回给客户端。
3、server_connection.cpp和client_connection.cpp都是connect.cpp的实例。
3.3 额外的场景
额外的场景是指业务服务器上后台管理的有些接口,需要从业务服务器发起经由通信服务器转发给业务服务器本身。
重要的流程说明:
1、mdms_handler.cpp代表业务服务器请求的处理类。
2、通信服务器只需把请求原封不动地传递给业务服务器。
4、总结
行文至此,通信服务器的有关知识就介绍得差不多了。这当然只是代码走读得出的概览,其实C++ boost的网络编程还有很多需要注意的细节。纸上得来终觉浅,绝知此事要躬行,希望认真读过本文的童靴能有所收获,当然,如果能评论提出哪个地方讲的不够透彻,那就再好不过啦。