C++ asio网络编程(2)buffer结构和同步读写
前言
第二天学习网络编程,回顾上一天的学习,我们完成的客户端和服务端的socket的创建,以及服务端acceptor的绑定和监听,接下来我们完成剩下的内容
一、Buffer是什么?
!!思路引线:当客户端发送数据给服务端的时候,我们是不是会有一个疑问,发送的数据存储在哪?
此时我们需要来了解一下Buffer这个东西!!!!
任何网络库都有提供buffer的数据结构,所谓buffer就是接收和发送数据时缓存数据的结构。
boost::asio提供了asio::mutable_buffer 和 asio::const_buffer这两个结构,他们是一段连续的空间,首字节存储了后续数据的长度。
asio::mutable_buffer用于写服务,asio::const_buffer用于读服务。但是这两个结构都没有被asio的api直接使用。
对于api的buffer参数,asio提出了MutableBufferSequence和ConstBufferSequence概念,他们是由多个asio::mutable_buffer和asio::const_buffer组成的。也就是说boost::asio为了节省空间,将一部分连续的空间组合起来,作为参数交给api使用。
我们可以理解为MutableBufferSequence的数据结构为std::vector<asio::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函数的参数使用。
拓展内容
在此我要拓展一个知识点,也是自身一开始没弄懂
asio::mutable_buffer和asio::const_buffer是单个缓冲区对象
但许多Asio API(如async_read, async_write)要求参数满足BufferSequence概念(即可以表示一个或多个缓冲区的序列)
适配器的作用:
const_buffers_1/mutable_buffers_1将单个buffer包装成单元素序列
通过适配器,单个buffer可以伪装成符合BufferSequence要求的类型
简单概括一下,我们可以用buffer()函数生成我们要用的缓存存储数据。
比如boost的发送接口send要求的参数为ConstBufferSequence类型
template<typename ConstBufferSequence>
std::size_t send(const ConstBufferSequence & buffers);
!!!记住上面这个send函数的参数噢,下面这个片段会提及!!!
1.利用buffer写一个发送数据的函数(片段)
先用一个笨拙的办法
void use_const_buffer()
{
std::string buf = "hell world";
asio::const_buffer asio_buf(buf.c_str(), buf.length());
std::vector<asio::const_buffer> buffers_sequence;
buffers_sequence.push_back(asio_buf);
}
这里我详细解释一下上部分代码
比如我现在是客户端,我要发送 hello world给服务端,我首先创建了一个 string类型的hello world,然后将他转换为const_buffer类型,在转换的时候,buf.c_str()意思是这个字符串的首地址,后面那个参数就是传入这个字符串的长度,然后我们创建一个const_buffer类型的容器vector 将这个字符串存储进去,此时这个buffers_sequence是一个ConstBufferSequence类型,可以直接被send函数(上文提及的)调用
当看到这里时,我想如果还是对于const_buffer和const_buffers_1两者有所混淆的话,给你打个比方,const_buffers_1比作一个大球,而有无数个小球const_buffer在它里面
2.利用buffer写一个高效版发送数据的函数(片段)
上述版本发送一条数据过于繁琐,这里我们利用buffer函数,传入只读类型,buffer函数会返回一个const_buffers_1类型,这个类型可以直接传入send函数
void use_buffer_str()
{
std::vector<asio::const_buffer> output_buf;
output_buf.push_back(asio::buffer("hello world"));
}
**
二、asio socket同步读写
**
1.同步写
(1).write_some
boost::asio提供了几种同步写的api,write_some可以每次向指定的空间写入固定的字节数,如果写缓冲区满了,就只写一部分,返回写入的字节数。
void wirte_to_socket(asio::ip::tcp::socket& sock)
{
std::string buf = "hello world";
std::size_t total_bytes_written = 0;
while (total_bytes_written != buf.length())
{
total_bytes_written += sock.write_some
(asio::buffer(buf.c_str() + total_bytes_written,
buf.length() - total_bytes_written));
}
}
这里我详细解释一下上部分代码
比如我们客户端向服务端发送数据hello world,通过调用这个write_some函数,这个函数每次返回一部分,不是一次性就把hello world全部发送过去,可能第一次发送hel,第二次发送low,是一部分一部分的发送,这个size_t类型是无符号整型的意思,total_bytes_written用来记录是否全部发送完,然后buffer函数里面第一个传入首地址,然后是长度
(2).小结客户端发送数据(完整代码不是片段讲解)
学到这里,我们一起来写一份客户端向服务端发送数据的完整代码
void wirte_to_socket(asio::ip::tcp::socket& sock)
{
std::string buf = "hello world";
std::size_t total_bytes_written = 0;
while (total_bytes_written != buf.length())
{
total_bytes_written += sock.write_some(asio::buffer(buf.c_str() + total_bytes_written,
buf.length() - total_bytes_written));
}
}
int send_data_by_wirte_some()
{
std::string raw_ip_address = "192.168.3.11";
unsigned short port_num = 3333;
try
{
asio::ip::tcp::endpoint ep(asio::ip::make_address(raw_ip_address), port_num);
asio::io_context ios;
asio::ip::tcp::socket sock(ios, ep.protocol());
sock.connect(ep);
wirte_to_socket(sock);
}
catch (system::system_error& e)
{
std::cout << "Error code = "
<< e.code() << ". Message: " << e.what();
return e.code().value();
}
return 0;
}
这里我详细解释一下上部分代码
首先第一个函数wirte_to_socket(asio::ip::tcp::socket& sock)这里不再讲解,上面说过,下面那个函数一开始我们假设对端服务器的ip地址和端口号,然后进入try——catch,首先创建客户端终端节点endpoint,然后传入服务端的ip地址和端口号,记得这个ip地址要转换类型,不拿直接用string类型,然后创建好endpoint以后我们创建socket,传入上下文io_context ios和协议protocol,然后再连接,连接完后调用上面那个写好的函数进行传输数据,如果这里代码有看不懂的,先回顾上面内容和第一天的内容讲解,因为这里只是把片段整合到了一起,大家可以理清一下思路
(3).图解说明为什么write_some不能一次发完所有数据
如果我们想发送hello world,我们假设tcp发送区域总共一次性只能发12长度,假如我们上一句信息还剩下5个长度没发完,我们这次想要发hello world10个长度,此时BUFFER用户发送区域向tcp缓冲发送区传入这个10个字节,但装不下了,所以只能传入前面7个,这就是为什么write_some无法一次性全部将数据发送完毕的原因,这样发送一部分,然后告诉我们没发送完,然后再发一部分,告诉我们没发送完
(4).介绍send函数来解决write_some问题
这里我们利用send函数来向服务端发送数据,代码90%与第二章第二小节(小结客户端发送数据)一致,因为只是更换了数据传输函数
int send_data_by_send()
{
std::string raw_ip_address = "192.168.3.11";
unsigned short port_num = 3333;
try
{
asio::ip::tcp::endpoint ep(asio::ip::make_address(raw_ip_address), port_num);
asio::io_context ios;
asio::ip::tcp::socket sock(ios, ep.protocol());
sock.connect(ep);
std::string buf = "hello world";
int send_length=sock.send(asio::buffer(buf.c_str(), buf.length()));
if (send_length <= 0)//传输失败
{
return 0;
}
}
catch (system::system_error& e)
{
std::cout << "Error code = "
<< e.code() << ". Message: " << e.what();
return e.code().value();
}
return 0;
}
这里int send_length=sock.send(asio::buffer(buf.c_str(), buf.length()));直接调用send,这里大家可能有个疑问,定义这个send_length是为什么,send函数会返回我们数据传输了多少,要么成功要么失败,成功了就是数据的全部长度,如果 send_length为0 说明对端关闭了,如果小于0 说明出现了系统级的错误
write_some使用起来比较麻烦,需要多次调用,asio提供了send函数。send函数会一次性将buffer中的内容发送给对端,如果有部分字节因为发送缓冲区满无法发送,则阻塞等待,直到发送缓冲区可用,则继续发送完成。
(4).write(全局函数)
类似send方法,asio还提供了一个write函数,可以一次性将所有数据发送给对端,如果发送缓冲区满了则阻塞,直到发送缓冲区可用,将数据发送完成。
int send_data_by_wirte() {
std::string raw_ip_address = "127.0.0.1";
unsigned short port_num = 3333;
try {
asio::ip::tcp::endpoint
ep(asio::ip::make_address(raw_ip_address),
port_num);
asio::io_context ios;
asio::ip::tcp::socket sock(ios, ep.protocol());
sock.connect(ep);
std::string buf = "Hello World!";
int send_length = asio::write(sock, asio::buffer(buf.c_str(), buf.length()));
if (send_length <= 0) {
std::cout << "send failed" << std::endl;
return 0;
}
}
catch (system::system_error& e) {
std::cout << "Error occured! Error code = " << e.code()
<< ". Message: " << e.what();
return e.code().value();
}
return 0;
}
以上代码大部分一致,只是更改了函数调用,大家可以多看看
这的write是全局函数· 不是socket内的成员函数,这里要注意
(5).send与write的区别
1. 阻塞与非阻塞
send函数:
默认情况下是非阻塞的。这意味着调用send时,程序不会因为等待数据发送完成而被阻塞,可以继续执行其他任务。
如果需要阻塞操作,可以通过设置选项来实现。
write函数:
默认情况下是阻塞的。调用write时,程序会一直等待数据发送完成才会继续执行。
适用于简单的、不需要高并发处理的应用场景。
2. 适用场景
send函数:
适合处理多个数据块或需要同时处理多个连接的场景。
支持非阻塞操作,适合高并发应用。
示例:实时聊天应用、在线游戏服务器等。
write函数:
适合处理单个数据块或不需要高并发处理的场景。
使用简单直接,适合初学者或小型项目。
示例:简单的文件传输、控制台应用程序等。
3. 性能和效率
send函数:
由于支持非阻塞操作,在处理高并发场景时可能会有更好的性能表现。
在需要频繁发送多个数据块时,可以减少系统调用的次数,提高效率。
write函数:
在处理单个数据块时,性能与send相当。
但在高并发或需要处理多个数据块的场景下,可能会因为阻塞而导致性能下降。
2.同步读
(1).read_some
同步读和同步写类似,提供了读取指定字节数的接口read_some
std::string read_from_socket(asio::ip::tcp::socket& sock)
{
const unsigned char MESSAGE_SIZE = 7;
char buf[MESSAGE_SIZE];
std::size_t total_bytes_read = 0;
while (total_bytes_read != MESSAGE_SIZE)
{
total_bytes_read += sock.read_some(asio::buffer(buf+ total_bytes_read,MESSAGE_SIZE - total_bytes_read));
}
return std::string(buf, total_bytes_read);
}
这里我详细解释一下上部分代码
代码部分与前文有相同的部分这里不再解释,我来解释一下这些定义,MESSAGE_SIZE 是假设我们传入的数据长度为7, buf[MESSAGE_SIZE]
创建一个这个数组来存储我们的数据
(2).小结客户端读入数据(完整代码不是片段讲解)
int read_data_by_read_some() {
std::string raw_ip_address = "127.0.0.1";
unsigned short port_num = 3333;
try {
asio::ip::tcp::endpoint
ep(asio::ip::address::from_string(raw_ip_address),
port_num);
asio::io_service ios;
asio::ip::tcp::socket sock(ios, ep.protocol());
sock.connect(ep);
read_from_socket(sock);
}
catch (system::system_error& e) {
std::cout << "Error occured! Error code = " << e.code()
<< ". Message: " << e.what();
return e.code().value();
}
return 0;
}
此代码与前文类似,在这不过多讲解,但read_some也有这样的一个问题。就是每次只能返回一段一段的,导致我接收的数据的时候很难受,能不能等返回的数据结束后,一次性告诉我全部完整数据内容呢
(3).receive
可以一次性同步接收对方发送的数据
int read_data_by_receive() {
std::string raw_ip_address = "127.0.0.1";
unsigned short port_num = 3333;
try {
asio::ip::tcp::endpoint
ep(asio::ip::make_address(raw_ip_address),
port_num);
asio::io_context ios;
asio::ip::tcp::socket sock(ios, ep.protocol());
sock.connect(ep);
const unsigned char BUFF_SIZE = 7;
char buffer_receive[BUFF_SIZE];
int receive_length = sock.receive(asio::buffer(buffer_receive, BUFF_SIZE));
if (receive_length <= 0) {
std::cout << "receive failed" << std::endl;
}
}
catch (system::system_error& e) {
std::cout << "Error occured! Error code = " << e.code()
<< ". Message: " << e.what();
return e.code().value();
}
return 0;
}
此代码不再注解,与前文内容大致相同,大家好好梳理一下思路,这里的receive_length也是可以返回三种值,与send函数那部分一样,大家可以回头看看
(4).read(全局函数)
可以一次性同步读取对方发送的数据
int read_data_by_read() {
std::string raw_ip_address = "127.0.0.1";
unsigned short port_num = 3333;
try {
asio::ip::tcp::endpoint
ep(asio::ip::make_address(raw_ip_address),
port_num);
asio::io_context ios;
asio::ip::tcp::socket sock(ios, ep.protocol());
sock.connect(ep);
const unsigned char BUFF_SIZE = 7;
char buffer_receive[BUFF_SIZE];
int receive_length = asio::read(sock, asio::buffer(buffer_receive, BUFF_SIZE));
if (receive_length <= 0) {
std::cout << "receive failed" << std::endl;
}
}
catch (system::system_error& e) {
std::cout << "Error occured! Error code = " << e.code()
<< ". Message: " << e.what();
return e.code().value();
}
return 0;
}
这的read是全局函数· 不是socket内的成员函数,这里要注意
(5).receive与read的区别
我们可能有疑问,在读取数据时候,read和receive有什么区别啊,不都是可以一次性读取全部吗?
1. 阻塞与非阻塞
read函数:
read是一个阻塞式函数。当调用read时,程序会在等待数据到达期间暂停执行其他任务。
它通常用于简单的场景,或者在不需要高并发处理的情况下。
receive函数:
receive可以用于非阻塞式(异步)操作。通过结合Boost.Asio的异步机制,程序可以在等待数据的同时继续执行其他任务。
这使得receive更适合需要处理多个连接或高并发场景的应用。
2. 数据读取方式
read函数:
read函数通常会尝试读取指定数量的数据。如果数据尚未到达,它会一直等待直到读取到指定数量的数据。
这种行为在需要保证每次读取固定大小的数据时非常有用。
receive函数:
receive函数则根据实际到达的数据量来决定读取多少。它不会等待指定数量的数据到达,而是立即返回已经接收到的数据。
这种方式在处理不确定大小的数据流时更加灵活。
3. 适用场景
read函数:
适用于简单的、不需要高并发处理的应用。
当需要保证每次读取固定大小的数据时,使用read可以简化代码逻辑。
receive函数:
适用于需要处理多个连接或高并发场景的应用。
当数据流的大小不确定或变化较大时,使用receive可以提高程序的灵活性和效率。