视频:https://www.bilibili.com/video/BV1jm4y1r7AY 作者文档:https://llfc.club/category?catid=225RaiVNI8pFDD5L4m807g7ZwmF#!aid/2LfzYBkRCfdEDrtE6hWz8VrCLoS
传统的阻塞模型
网络编程的基本流程对于服务端是这样的
服务端
- socket——创建socket对象。
- bind——绑定本机ip+port。
- listen——监听来电,若在监听到来电,则建立起连接。
- accept——再创建一个socket对象给其收发消息。原因是现实中服务端都是面对多个客户端,那么为了区分各个客户端,则每个客户端都需再分配一个socket对象进行收发消息。
- read、write——就是收发消息了。
客户端
- socket——创建socket对象。
- connect——根据服务端ip+port,发起连接请求。
- write、read——建立连接后,就可发收消息了。
图示如下
相关的网络编程技术可以看看我之前写的文章
https://llfc.club/articlepage?id=2LXIKWJtKGblnWtHT7TplLKK6ze
现代化模型
当然,现在的都是Proactor模式、Reactor模式这种,使用epoll等多路复用技术,而且是异步的
- Reactor模式,是同步模型
- Proactor模式,是异步模型
接下来按照上述流程,我们用boost::asio逐步介绍。这里会有本书:《Boost.Asio C++ Network Programming Cookbook》,博客很多地方也按这本书写的
终端节点的创建
所谓 终端节点
就是用来通信的端对端的节点,可以通过ip地址和端口构造,其的节点可以连接这个终端节点做通信.
如果我们是客户端,我们可以通过对端的ip和端口构造一个endpoint,用这个endpoint和其通信。
客户端
int client_end_point() {
// Step 1. 假设客户端应用程序已经获得了ip地址和协议端口号。
std::string raw_ip_address = "127.0.0.1";
unsigned short port_num = 3333;
// 用于存储解析原始ip地址时发生的错误信息。
boost::system::error_code ec;
// Step 2. 使用独立于IP协议版本的地址表示。
asio::ip::address ip_address = asio::ip::address::from_string(raw_ip_address, ec);
if (ec.value() != 0) {
// 提供的IP地址无效。打破执行
std::cout
<< "Failed to parse the IP address. Error code = "
<< ec.value() << ". Message: " << ec.message();
return ec.value();
}
// Step 3. 准备端点
asio::ip::tcp::endpoint ep(ip_address, port_num);
// Step 4. 端点已经准备好,可以用来指定客户机想要与之通信的网络中的特定服务器。
return 0;
}
服务端,如果是服务端,则只需根据本地地址绑定就可以生成endpoint
int server_end_point(){
// Step 1. 这里我们假设服务器应用程序已经获得了协议端口号。
unsigned short port_num = 3333;
// Step 2. 创建asio::ip::address类的特殊对象,指定主机上所有可用的ip地址。注意,这里我们假设服务器工作在IPv6协议上。
asio::ip::address ip_address = asio::ip::address_v6::any();
// Step 3. 创建端点
asio::ip::tcp::endpoint ep(ip_address, port_num);
// Step 4. 端点被创建后,并可用于指定IP地址和端口号,服务器应用程序希望在这些地址和端口号上侦听传入的连接。
return 0;
}
总结
- 创建ip地址类,
asio::ip::address
- 创建端点类,
asio::ip::tcp::endpoint
创建socket
创建socket分为4步
- 创建上下文iocontext
- 选择协议
- 生成socket
- 打开socket
具体点来讲是:
- 创建IO服务类,
asio::io_service
- 创建tcp类,
asio::ip::tcp
- 创建socket类
- 客户端:普通 socket类,
asio::ip::tcp::socket
- 服务端:accept socket类,
asio::ip::tcp::acceptor
- 客户端:普通 socket类,
- 打开socket类,open
客户端
// 创建TCP套接字
int create_tcp_socket() {
// Step 1. 套接字构造函数需要'io_service'类的实例
asio::io_context ios;
// Step 2. 创建一个tcp类的对象,以IPv4作为底层协议,表示tcp协议
asio::ip::tcp protocol = asio::ip::tcp::v4();
// Step 3. 实例化一个活动的TCP套接字对象
asio::ip::tcp::socket sock(ios);
// 用于存储打开套接字时发生的错误信息
boost::system::error_code ec;
// Step 4. 打开 socket
sock.open(protocol, ec);
if (ec.value() != 0) {
// 打开socket失败
std::cout
<< "Failed to open the socket! Error code = "
<< ec.value() << ". Message: " << ec.message();
return ec.value();
}
return 0;
}
服务端。上述socket只是通信的socket,如果是服务端,我们还需要生成一个acceptor的socket,用来接收新的连接。
// 创建TCP Accept套接字
int create_acceptor_socket() {
// Step 1. 套接字构造函数需要'io_service'类的实例。
asio::io_context ios;
// Step 2. 创建一个tcp类的对象,表示一个tcp协议,IPv6作为底层协议。
asio::ip::tcp protocol = asio::ip::tcp::v6();
// Step 3. 实例化一个接受套接字对象。
asio::ip::tcp::acceptor acceptor(ios);
// 用于存储打开接收方套接字时发生的错误信息。
boost::system::error_code ec;
// Step 4. 打开 acceptor socket.
acceptor.open(protocol, ec);
if (ec.value() != 0) {
// 打开socket失败
std::cout
<< "Failed to open the acceptor socket!"
<< "Error code = "
<< ec.value() << ". Message: " << ec.message();
return ec.value();
}
return 0;
}
绑定acceptor
对于acceptor类型的socket,服务器要将其绑定到指定的断点,所有连接这个端点的连接都可以被接收到。
int bind_acceptor_socket() {
// Step 1. 这里我们假设服务器应用程序已经获得了协议端口号。
unsigned short port_num = 3333;
// Step 2. 创建端点
asio::ip::tcp::endpoint ep(asio::ip::address_v4::any(),
port_num);
// 由'acceptor'类构造函数使用。
asio::io_context ios;
// Step 3. 创建并打开一个接收套接字
asio::ip::tcp::acceptor acceptor(ios, ep.protocol());
boost::system::error_code ec;
// Step 4. 绑定接收器套接字
acceptor.bind(ep, ec);
// 处理错误
if (ec.value() != 0) {
// 绑定接收socket失败。打破执行。
std::cout << "Failed to bind the acceptor socket."
<< "Error code = " << ec.value() << ". Message: "
<< ec.message();
return ec.value();
}
return 0;
}
(完整)
流程
- 获取IP和端口等
- 创建端点,
asio::ip::tcp::endpoint
- 创建IO上下文,
asio::io_context
- 创建并打开套接字
- 客户端:
asio::ip::tcp::socket
,connect - 服务端:
asio::ip::tcp::acceptor
,bind,listen,sock,accept
- 客户端:
客户端:连接指定的端点,对连接服务器指定的端点进行连接
int connect_to_end() {
// Step 1. 假设客户端应用程序已经获得了目标服务器的IP地址和协议端口号
std::string raw_ip_address = "127.0.0.1";
unsigned short port_num = 3333;
try {
// Step 2. 创建指定目标服务器应用程序的端点
asio::ip::tcp::endpoint ep(asio::ip::address::from_string(raw_ip_address), port_num);
asio::io_context ios;
// Step 3. 创建并打开套接字
asio::ip::tcp::socket sock(ios, ep.protocol());
// Step 4. 连接套接字
sock.connect(ep);
// 此时,套接字'sock'已连接到服务器应用程序,并可用于向其发送数据或从其接收数据。
}
// 这里使用的asio::ip::address::from_string()和asio::ip::tcp::socket::connect()的过载会在出现错误条件时抛出异常。
catch (system::system_error& e) {
std::cout << "Error occured! Error code = " << e.code()
<< ". Message: " << e.what();
return e.code().value();
}
}
服务器:服务器接收连接。当有客户端连接时,服务器需要接收连接
int accept_new_connection(){
// 包含挂起连接请求的队列的大小。
const int BACKLOG_SIZE = 30;
// Step 1. 这里我们假设服务器应用程序已经获得了协议端口号。
unsigned short port_num = 3333;
// Step 2. 创建服务器端点。
asio::ip::tcp::endpoint ep(asio::ip::address_v4::any(), port_num);
asio::io_context ios;
try {
// Step 3. 实例化并打开一个接受套接字。
asio::ip::tcp::acceptor acceptor(ios, ep.protocol());
// Step 4. 将接收方套接字绑定到服务器端。
acceptor.bind(ep);
// Step 5. 开始监听传入的连接请求。
acceptor.listen(BACKLOG_SIZE);
// Step 6. 创建活动套接字。
asio::ip::tcp::socket sock(ios);
// Step 7. 处理下一个连接请求并将活动套接字连接到客户端
acceptor.accept(sock);
// 此时,'sock'套接字已连接到客户端应用程序,并可用于向其发送数据或从其接收数据
}
catch (system::system_error& e) {
std::cout << "Error occured! Error code = " << e.code()
<< ". Message: " << e.what();
return e.code().value();
}
}
关于buffer
任何网络库都有提供buffer的数据结构,所谓buffer就是接收和发送数据时缓存数据的结构。
boost::asio提供了asio::mutable_buffer 和 asio::const_buffer这两个结构,但是这两个结构都没有被asio的api直接使用
- asio::mutable_buffer用于写服务
- asio::const_buffer用于读服务 他们是一段连续的空间,首字节存储了后续数据的长度。
对于api的buffer参数,asio提出了MutableBufferSequence和ConstBufferSequence概念,他们是由多个asio::mutable_buffer和asio::const_buffer组成的。 也就是说boost::asio为了节省空间,将一部分连续的空间组合起来,作为参数交给api使用。
我们可以理解为MutableBufferSequence的数据结构为std::vectorasio::mutable_buffer
结构如下
每隔vector存储的都是mutable_buffer的地址,每个mutable_buffer的第一个字节表示数据的长度,后面跟着数据内容。
这么复杂的结构交给用户使用并不合适,所以asio提出了buffer()函数,该函数接收多种形式的字节流,该函数返回asio::mutable_buffers_1 o或者asio::const_buffers_1结构的对象。
如果传递给buffer()的参数是一个只读类型,则函数返回asio::const_buffers_1 类型对象。
如果传递给buffer()的参数是一个可写类型,则返回asio::mutable_buffers_1 类型对象。
asio::const_buffers_1和asio::mutable_buffers_1是asio::mutable_buffer和asio::const_buffer的适配器,提供了符合MutableBufferSequence和ConstBufferSequence概念的接口,所以他们可以作为boost::asio的api函数的参数使用。
简单概括一下,我们可以用buffer()函数生成我们要用的缓存存储数据。
比如boost的发送接口send要求的参数为ConstBufferSequence类型
template<typename ConstBufferSequence>
std::size_t send(const ConstBufferSequence & buffers);
我们需要将”Hello Word转化为该类型”
void use_const_buffer() {
std::string buf = "hello world!";
asio::const_buffer asio_buf(buf.c_str(), buf.length());
std::vector<asio::const_buffer> buffers_sequence;
buffers_sequence.push_back(asio_buf);
}
最终buffers_sequence就是可以传递给发送接口send的类型。但是这太复杂了,可以直接用buffer函数转化为send需要的参数类型
void use_buffer_str() {
asio::const_buffers_1 output_buf = asio::buffer("hello world");
}
output_buf可以直接传递给该send接口。我们也可以将数组转化为send接受的类型
void use_buffer_array(){
const size_t BUF_SIZE_BYTES = 20;
std::unique_ptr<char[] > buf(new char[BUF_SIZE_BYTES]);
auto input_buf = asio::buffer(static_cast<void*>(buf.get()), BUF_SIZE_BYTES);
}
对于流式操作,我们可以用streambuf,将输入输出流和streambuf绑定,可以实现流式输入和输出。
void use_stream_buffer() {
asio::streambuf buf;
std::ostream output(&buf);
// Writing the message to the stream-based buffer.
output << "Message1\nMessage2";
// Now we want to read all data from a streambuf
// until '\n' delimiter.
// Instantiate an input stream which uses our
// stream buffer.
std::istream input(&buf);
// We'll read data into this string.
std::string message1;
std::getline(input, message1);
// Now message1 string contains 'Message1'.
}