C/C++ 网络库 boost asio 使用详解

前言

本文原文为:Boost asio库使用详解

众所周知,直到目前为止,C/C++依旧没有标准网络库

每次我们在网络编程中,就不得直接调用系统API来进行编程,但很多步骤都是重复不变的的,却又不得不写,非常繁琐

但其实作为C/C++准标准库的boost库,早就有了一个网络库,名为asio,也许在下一个C++标准,就加入了标准库也说不一定

官网下载地址:

sourceforge.net/projects/boost/

下载可能有点慢,因为是国外的网站

如果你还没有配置好boost库,可以参考我的这篇文章:boost 编译

除此之外,我还写了一篇基于asio库实现的聊天室(群聊、单聊以及文件传输),可以点击这里查看

该项目的服务器因为使用的asio库,所以完全跨平台,可以直接在windows、linux平台进行编译

一、理清层次关系

既然是网络编程,那么就必然少不了TCPUDP的身影(虽然这个网络模块还可以控制的更加深入,也支持其它协议)

但这个库也有很不友好的地方,那就是层次太多了!所以我们首先来理一下各个类的层次关系

比如首先,boost这个库有很多模块,asio就是其中的一个网络模块,这些所有模块都是在boost这个命名空间下:

using namespace boost;

然后我们这里是使用asio这个网络模块里面的各个类,所以就是:

using namespace boost::asio;

asio空间中,我们首先不可避免的就是类io_serviceio_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,我们的TCPUDP相关类,就在这个ip里面

比如我们想使用tcp,其socket类,就是:ip::tcp::socket,而udpsocket类就是:ip::udp::socket

由于我们通常程序用中可能只使用其中某一个协议,比如只使用TCP,那就可以这样写:

using asio::ip::tcp;

作为TCP服务器,用于接受客户端连接的类acceptor也在其中

这样就不用每次都加前面那一大长串了(如果tcp与udp都会使用,那就别这样写了,会混淆)

除了socket类,我们在网络通信中还需要对方的ip与端口才行,这就用到了类endpoint,它同样在tcpudp中都有

还有就是地址处理类:address,直接就在ip里面,其最常用的就是它的静态函数from_string,将十进制的ip地址转化为网络字节序

最后总体总结一下常用的类所在位置:

空间类或函数
boost::asioio_context,buffer
boost::asio::ipaddress
boost::asio::ip::tcpsocket,acceptor,endpoint
boost::asio::ip::udpsocket,endpoint

二、TCP编程实例

这个网络库不仅支持同步,而且还支持异步,所以下面我们分别进行讲解

1.同步实例

1)服务器

#include<iostream>
#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");
}

这里写的是一个只接受一个客户端连接的回声服务器

可以看到,这里用到的acceptorsocket ,第一个参数都为这个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函数,就可以得到地址信息,并打印出来

然后通过一个trycatch结构来捕获错误信息,比如客户端中断连接之类的

在while循环里面,我们就可以通过receive函数与send函数,进行同步接受与发送消息

注意缓存区都需要通过buffer函数来构造函数可接受的缓存区结构体,否则会报错误

2)客户端

#include<iostream>
#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循环,先发送,后接收信息即可

2.异步实例

这里同样写的是一个只接受一个客户端连接的回声服务器

由于异步大多是用于服务器上,而且客户端使用方法和服务器类似,所以客户端代码还是上面的同步代码,服务器异步通信代码如下:

#include<iostream>
#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_contextrun函数,等待执行即可

注意,异步的情况下,程序不会卡在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_contextrun函数就会返回,从而程序结束

3.运行测试

在这里插入图片描述

三、UDP编程实例

1.同步实例

1)服务器

#include<iostream>
#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_fromsend_to来接收与发送数据了,逻辑和用系统api相同,接受的数据来源的客户端地址,保存在传入的cliPoint

2)客户端

#include<iostream>
#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_toreceive_from函数接受与发送数据即可

2.异步实例

客户端与服务器的方法都一样,所以这里同样只改动服务器的代码:

#include<iostream>
#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_fromasync_send_to

同时这里的socketendpoint 都采用的指针形式,方便传值,还有就是bind函数,和前面一样,是为了适配这个参数,同时还要将我们想要传入的值给传进去

至于我写的接收与发送的回调函数,同样只是互相调用而已,形成了一个事件循环,这样io_contextrun函数才不会返回

3.运行测试

在这里插入图片描述

  • 31
    点赞
  • 146
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

余识-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值