《http篇》boost.asio实现http服务

发布时间 2023-07-30 13:30:12作者: Fusio

模块详解及TCP实例

下载和编译参考之前文章

参考链接:https://zhuanlan.zhihu.com/p/634878993

层次关系

首先,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地址转化为网络字节序。

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

image

TCP实例

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

同步实例

服务器端

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

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

可以看到,这里用到的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<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循环,先发送,后接收信息即可。

实现HTTP服务端

下载和编译参考之前文章

参考链接:https://blog.csdn.net/luchengtao11/article/details/97814106

代码

main函数很简单,就是启动了一个server实例、并运行。

int main(int argc, char* argv[])
{
	try
	{
		// Check command line arguments.
		//if (argc != 4)
		//{
		//	std::cerr << "Usage: http_server <address> <port> <doc_root>\n";
		//	std::cerr << "  For IPv4, try:\n";
		//	std::cerr << "    receiver 0.0.0.0 80 .\n";
		//	std::cerr << "  For IPv6, try:\n";
		//	std::cerr << "    receiver 0::0 80 .\n";
		//	return 1;
		//}

		//官网的例子是通过命令行传参的,这里为方便,直接在代码中写死

		// Initialise the server.
		std::string ip_address = "0.0.0.0";
		std::string port = "8080";
		std::string doc_root = "../../../docs";
		http::server::server s(ip_address, port, doc_root);

		// Run the server until stopped.
		s.run();
	}
	catch (std::exception& e)
	{
		std::cerr << "exception: " << e.what() << "\n";
	}

	return 0;
}

下面还没看