众所周知,直到目前为止,C/C++依旧没有标准网络库
每次我们在网络编程中,就不得直接调用系统API来进行编程,但很多步骤都是重复不变的的,却又不得不写,非常繁琐
但其实作为C/C++准标准库的boost库,早就有了一个网络库,名为asio
,也许在下一个C++标准,就加入了标准库也说不一定
官网下载地址:
sourceforge.net/projects/boost/
下载可能有点慢,因为是国外的网站
如果你还没有配置好boost
库,可以参考我的这篇文章:boost 编译
除此之外,我还写了一篇基于asio库实现的聊天室(群聊、单聊以及文件传输),可以点击这里查看
该项目的服务器因为使用的asio库,所以完全跨平台,可以直接在windows、linux平台进行编译
既然是网络编程,那么就必然少不了TCP
与UDP
的身影(虽然这个网络模块还可以控制的更加深入,也支持其它协议)
但这个库也有很不友好的地方,那就是层次太多了!所以我们首先来理一下各个类的层次关系
比如首先,boost
这个库有很多模块,asio
就是其中的一个网络模块,这些所有模块都是在boost
这个命名空间下:
using namespace boost;
然后我们这里是使用asio
这个网络模块里面的各个类,所以就是:
using namespace boost::asio;
而asio
空间中,我们首先不可避免的就是类io_service
或io_context
注意,
io_context
这个类是用来替代io_service
的,所以建议以后都直接使用io_context
即可
这个类非常重要,它相当于我们程序与系统之间I/O
操作的中介,我们所有的接受或发送数据操作,都是通过将需求提交给这个类,然后这个类再交给计算机来执行的
基于这个理念,基本所有
asio
网络库中有读写I/O
需求的类,其构造函数的第一个参数就是它,比如后面要讲的收发数据的socket
类,以及tcp
服务器用于接受用户连接的acceptor
类等
而这个io_context
就在asio
里面,所以在using namespace boost::asio;
之后,就可以直接用它实例化对象:
io_context io;
除了io_context
外,asio
里面还有一个函数非常重要,那就是buffer
函数,它的作用其实就是构造一个结构体,大致如下:
struct{
void* buf;
s_size len;
}
该网络模块中所有的收发数据操作,都不接受单独的字符串,而是这样一个结构体,分别为缓存区的首地址以及缓存区的大小
总结一下就是,asio里面,直接用到的就是一个类:io_context
与一个函数:buffer
然后继续深入,紧接着就是asio
里面进一步的命名空间ip
,我们的TCP
和UDP
相关类,就在这个ip
里面
比如我们想使用tcp
,其socket
类,就是:ip::tcp::socket
,而udp
的socket
类就是:ip::udp::socket
由于我们通常程序用中可能只使用其中某一个协议,比如只使用TCP
,那就可以这样写:
using asio::ip::tcp;
作为TCP服务器,用于接受客户端连接的类acceptor
也在其中
这样就不用每次都加前面那一大长串了(如果tcp与udp都会使用,那就别这样写了,会混淆)
除了socket
类,我们在网络通信中还需要对方的ip与端口才行,这就用到了类endpoint
,它同样在tcp
与udp
中都有
还有就是地址处理类:address
,直接就在ip
里面,其最常用的就是它的静态函数from_string
,将十进制的ip地址转化为网络字节序
最后总体总结一下常用的类所在位置:
空间 | 类或函数 |
---|---|
boost::asio | io_context,buffer |
boost::asio::ip | address |
boost::asio::ip::tcp | socket,acceptor,endpoint |
boost::asio::ip::udp | socket,endpoint |
这个网络库不仅支持同步,而且还支持异步,所以下面我们分别进行讲解
#include
#include"boost/asio.hpp"
using namespace std;
using namespace boost;
using asio::ip::tcp;
int main() {
cout << "server start ……" << endl;
asio::io_context io;
tcp::acceptor acptr(io, tcp::endpoint(tcp::v4(), 6688));
tcp::socket sock(io);
acptr.accept(sock);
cout << "client:" << sock.remote_endpoint().address() << endl;
try {
while (true) {
char buf[0xFF];
sock.receive(asio::buffer(buf));
sock.send(asio::buffer(buf));
}
}
catch(std::exception& e) {
cout << e.what();
}
sock.close();
::system("pause");
}
这里写的是一个只接受一个客户端连接的回声服务器
可以看到,这里用到的
acceptor
与socket
,第一个参数都为这个io_context
注意一下逻辑的转化,比如以前我们使用纯系统网络api
时,是必须要先有一个监听socket
,但使用了asio
就有点不一样了,acceptor
类就封装了一个监听socket
,通过其构造参数的第二个,endpoint
,来确定监听地址与端口
tcp::acceptor acptr(io, tcp::endpoint(tcp::v4(), 6688));
而这里的endpoint
第一个参数为tcp::v4()
,这个函数返回一个tcp
协议类型,由于没有找到说明,我理解的就是监听ipv4
类型本机所有地址,其第二个参数就是要监听的端口号
然后就是调用accept
函数,用于接受客户端连接,其参数就是一个申请好的socket
,用于保存连接上来的客户端信息
cout << "client:" << sock.remote_endpoint().address() << endl;
同时这里我还通过调用remote_endpoint
函数,可以获取客户端连接上来的终端,再调用address
函数,就可以得到地址信息,并打印出来
然后通过一个try
和catch
结构来捕获错误信息,比如客户端中断连接之类的
在while循环里面,我们就可以通过receive
函数与send
函数,进行同步接受与发送消息
注意缓存区都需要通过buffer
函数来构造函数可接受的缓存区结构体,否则会报错误
#include
#include"boost/asio.hpp"
using namespace std;
using namespace boost::asio;
int main() {
io_context io;
ip::tcp::socket sock(io);
sock.connect(ip::tcp::endpoint(ip::address::from_string("127.0.0.1"),6688));
char buf[0xFF];
while (true) {
cin >> buf;
sock.send(buffer(buf));
memset(buf, 0, 0xFF);
sock.receive(buffer(buf));
cout << buf<<endl;
}
sock.close();
::system("pause");
}
客户端就更加简单了,直接构造一个socket
,然后调用connect
函数连接即可,其唯一的参数就是一个终端类endpoint
这里使用address
类的静态函数from_string
将十进制的地址转化,得到第一个参数,第二个参数为要连接的端口
紧接着就是进入while
循环,先发送,后接收信息即可
这里同样写的是一个只接受一个客户端连接的回声服务器
由于异步大多是用于服务器上,而且客户端使用方法和服务器类似,所以客户端代码还是上面的同步代码,服务器异步通信代码如下:
#include
#include"boost/asio.hpp"
#include"boost/bind.hpp"
using namespace std;
using namespace boost;
using asio::ip::tcp;
void sock_accept(tcp::socket* sockCli);
void sock_Recv(char* buf, tcp::socket *sockCli);
void sock_Send(char* buf, tcp::socket* sockCli);
int main() {
cout << "server start ……" << endl;
asio::io_context io;
tcp::acceptor acptr(io, tcp::endpoint(tcp::v4(), 6688));
tcp::socket *sock=new tcp::socket(io);
acptr.async_accept(*sock, boost::bind(sock_accept, sock));
io.run();
}
void sock_Send(char* buf, tcp::socket* sockCli) {
try {
sockCli->async_receive(asio::buffer(buf, 0xFF), boost::bind(sock_Recv, buf, sockCli));
}
catch (std::exception& e) {
cout << "";
cout << e.what();
delete sockCli;
delete[] buf;
}
}
void sock_accept(tcp::socket* sockCli) {
char* buf = new char[0xFF];
cout << "client ip:" << sockCli->remote_endpoint().address() << endl;
sockCli->async_receive(asio::buffer(buf, 0xFF), boost::bind(&sock_Recv, buf, sockCli));
}
void sock_Recv(char* buf, tcp::socket* sockCli) {
try {
sockCli->async_send(asio::buffer(buf, 0xFF), boost::bind(sock_Send, buf, sockCli));
}
catch (std::exception& e) {
cout << "";
cout << e.what();
delete sockCli;
delete[] buf;
}
}
从main
函数看起,这里异步等待客户端连接的是async_accept
函数,并且注意这里的socket
我用的指针,这是为了便于函数传值
它的第一个参数就是保存连接上来的客户端套接字类,第二个参数是如果客户端连接上来后,调用哪个函数进行处理
所以就有了我写的sock_accept
这个函数,但由于这个函数参数的格式有要求,无法包含void sock_accept(tcp::socket *sockCli)
这种形式的函数
所以我用到了boost库里面的另一个强大的函数:bind
这个bind函数,其主要作用就是更改函数特性,比如我这里,第一个参数为要更改的函数,第二参数是我想要传给这个函数的参数,bind
函数接收到这两个参数,就会返回一个无参函数,就可以符合async_accept
第二个参数的要求
每当程序调用这个返回的无参函数时,都会将这个sock
变量传入实际的sock_accept
函数进行调用,是不是相当强大!
然后我们就调用io_context
的run
函数,等待执行即可
注意,异步的情况下,程序不会卡在
async_accept
这里,它仅仅只是提交了一个接受客户端连接的请求,等待系统执行完成后,调用对应的处理函数就行了
而run
函数,只要还有一个请求没有完成,它就不会返回
所以一旦有客户端连接上来了,我们就要在sock_accept
函数里面,通过async_receive
函数来提交一个接受客户端数据的请求
void sock_accept(tcp::socket *sockCli) {
char *buf=new char[0xFF];
cout << "client ip:" << sockCli->remote_endpoint().address() << endl;
sockCli->async_receive(asio::buffer(buf,0xFF), boost::bind(&sock_Recv,buf,sockCli));
}
其第一个参数为缓存区,由于我们这里使用的是堆内存,所以buffer
函数无法自动推导缓存区大小,就得我们自己传入
而第二个参数也是类似,由于不接受void sock_Recv(char *buf,tcp::socket *sockCli)
这种类型的函数,我们又的确需要传入这些参数,所以就调用了bind
函数进行构造
而在sock_Recv
函数中,因为我们是回声服务器,所以又调用了async_send
函数,来提交发送数据的请求
void sock_Recv(char *buf,tcp::socket *sockCli) {
try {
sockCli->async_send(asio::buffer(buf, 0xFF), boost::bind(sock_Send, buf, sockCli));
}
catch (std::exception& e) {
cout << "";
cout << e.what();
delete sockCli;
delete[] buf;
}
}
这样这两个发送与接受数据的函数就形成了互相调用,请求就永远不会终止,直到出现异常,比如客户端断开了连接,跳出这个互相调用的循环
没有了请求需要处理,io_context
的run
函数就会返回,从而程序结束
#include
#include"boost/asio.hpp"
#include"boost/bind.hpp"
using namespace std;
using namespace boost;
using asio::ip::udp;
int main() {
cout << "server start ……" << endl;
asio::io_context io;
udp::socket sock(io, udp::endpoint(udp::v4(), 6688));
char buf[0xFF];
udp::endpoint cliPoint;
while (1) {
sock.receive_from(asio::buffer(buf), cliPoint);
sock.send_to(asio::buffer(buf),cliPoint);
}
}
由于udp不需要等待客户端来连接,所以这里服务器同样使用的socket
,不同的是我们需要给它传入第二个参数,即监听的终端
然后就可以调用receive_from
与send_to
来接收与发送数据了,逻辑和用系统api相同,接受的数据来源的客户端地址,保存在传入的cliPoint
中
#include
#include"boost/asio.hpp"
using namespace std;
using namespace boost;
using boost::asio::ip::udp;
int main() {
asio::io_context io;
udp::socket sock(io);
sock.open(asio::ip::udp::v4());
udp::endpoint serPoint(asio::ip::address::from_string("127.0.0.1"),6688);
while (1) {
char buf[0xFF];
cin >> buf;
sock.send_to(asio::buffer(buf), serPoint);
memset(buf, 0, 0xFF);
sock.receive_from(asio::buffer(buf), serPoint);
cout << buf << endl;
}
::system("pause");
}
udp客户端的socket
也有点不一样,它需要调用open
函数,使用ipv4
协议
然后其它的都一样,就是调用send_to
与receive_from
函数接受与发送数据即可
客户端与服务器的方法都一样,所以这里同样只改动服务器的代码:
#include
#include"boost/asio.hpp"
#include"boost/bind.hpp"
using namespace std;
using namespace boost;
using asio::ip::udp;
void sock_recv(char* buf, udp::socket* sock, udp::endpoint* cliPoint);
void sock_send(char* buf, udp::socket* sock, udp::endpoint* cliPoint);
int main() {
cout << "server start ……" << endl;
asio::io_context io;
udp::socket *sock=new udp::socket(io, udp::endpoint(udp::v4(), 6688));
char *buf=new char[0xFF];
udp::endpoint *cliPoint=new udp::endpoint;
sock->async_receive_from(asio::buffer(buf,0xFF),*cliPoint,boost::bind(sock_recv,buf,sock,cliPoint));
io.run();
}
void sock_send(char* buf, udp::socket* sock, udp::endpoint* cliPoint) {
try
{
sock->async_receive_from(asio::buffer(buf, 0xFF), *cliPoint, boost::bind(sock_recv, buf, sock, cliPoint));
}
catch (const std::exception& e)
{
cout << e.what();
}
}
void sock_recv(char* buf, udp::socket* sock, udp::endpoint* cliPoint) {
try
{
sock->async_send_to(asio::buffer(buf, 0xFF), *cliPoint, boost::bind(sock_send, buf, sock, cliPoint));
}
catch (const std::exception& e)
{
cout << e.what();
}
}
步骤同样差不多,只是这里调用的都是异步函数async_receive_from
与async_send_to
同时这里的socket
与endpoint
都采用的指针形式,方便传值,还有就是bind
函数,和前面一样,是为了适配这个参数,同时还要将我们想要传入的值给传进去
至于我写的接收与发送的回调函数,同样只是互相调用而已,形成了一个事件循环,这样io_context
的run
函数才不会返回