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



    C/C++实战入门到精通 https://blog.csdn.net/weixin_50964512/article/details/125710864

    前言

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

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

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

    官网下载地址:

    sourceforge.net/projects/boost/
    
    • 1

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

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

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

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

    一、理清层次关系

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

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

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

    using namespace boost;
    
    • 1

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

    using namespace boost::asio;
    
    • 1

    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;
    
    • 1

    除了io_context 外,asio里面还有一个函数非常重要,那就是buffer函数,它的作用其实就是构造一个结构体,大致如下:

    struct{
    void* buf;
    s_size len;
    }
    
    • 1
    • 2
    • 3
    • 4

    该网络模块中所有的收发数据操作,都不接受单独的字符串,而是这样一个结构体,分别为缓存区的首地址以及缓存区的大小

    总结一下就是,asio里面,直接用到的就是一个类:io_context 与一个函数:buffer

    然后继续深入,紧接着就是asio里面进一步的命名空间ip,我们的TCPUDP相关类,就在这个ip里面

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

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

    using asio::ip::tcp;
    
    • 1

    作为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
    #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");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

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

    可以看到,这里用到的acceptorsocket ,第一个参数都为这个io_context

    注意一下逻辑的转化,比如以前我们使用纯系统网络api时,是必须要先有一个监听socket,但使用了asio就有点不一样了,acceptor 类就封装了一个监听socket,通过其构造参数的第二个,endpoint,来确定监听地址与端口

    tcp::acceptor acptr(io, tcp::endpoint(tcp::v4(), 6688));
    
    • 1

    而这里的endpoint第一个参数为tcp::v4(),这个函数返回一个tcp协议类型,由于没有找到说明,我理解的就是监听ipv4类型本机所有地址,其第二个参数就是要监听的端口号

    然后就是调用accept函数,用于接受客户端连接,其参数就是一个申请好的socket,用于保存连接上来的客户端信息

    cout << "client:" << sock.remote_endpoint().address() << endl;
    
    • 1

    同时这里我还通过调用remote_endpoint函数,可以获取客户端连接上来的终端,再调用address函数,就可以得到地址信息,并打印出来

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

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

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

    2)客户端

    #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");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    客户端就更加简单了,直接构造一个socket ,然后调用connect函数连接即可,其唯一的参数就是一个终端类endpoint

    这里使用address类的静态函数from_string将十进制的地址转化,得到第一个参数,第二个参数为要连接的端口

    紧接着就是进入while循环,先发送,后接收信息即可

    2.异步实例

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

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

    #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;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    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));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    其第一个参数为缓存区,由于我们这里使用的是堆内存,所以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;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这样这两个发送与接受数据的函数就形成了互相调用,请求就永远不会终止,直到出现异常,比如客户端断开了连接,跳出这个互相调用的循环

    没有了请求需要处理,io_contextrun函数就会返回,从而程序结束

    3.运行测试

    在这里插入图片描述

    三、UDP编程实例

    1.同步实例

    1)服务器

    #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);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    由于udp不需要等待客户端来连接,所以这里服务器同样使用的socket ,不同的是我们需要给它传入第二个参数,即监听的终端

    然后就可以调用receive_fromsend_to来接收与发送数据了,逻辑和用系统api相同,接受的数据来源的客户端地址,保存在传入的cliPoint

    2)客户端

    #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");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    udp客户端的socket 也有点不一样,它需要调用open函数,使用ipv4协议

    然后其它的都一样,就是调用send_toreceive_from函数接受与发送数据即可

    2.异步实例

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

    #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();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    步骤同样差不多,只是这里调用的都是异步函数async_receive_fromasync_send_to

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

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

    3.运行测试

    在这里插入图片描述

  • 相关阅读:
    计算地球上两点间的方位角和俯仰角【输入经纬度】
    word制作多个单位联合发文的文件头
    Zipkin_Slueth微服务链式追踪
    【开发篇】十七、消息:模拟订单短信通知
    0基础学习VR全景平台篇 第93篇:智慧景区教程
    bp神经网络怎么看结果,bp神经网络结果不一样
    趣谈网络协议_1
    计算机毕业设计(附源码)python中小学在线作业管理小程序
    Redis服务部署
    19-29-k8s-基本命令-yaml-kubectl
  • 原文地址:https://blog.csdn.net/weixin_50964512/article/details/126727297