一、buffer数据结构
在Boost的asio里,发送数据需要用一个结构去承载,这个结构就是buffer。任何网络库都有提供buffer的数据结构,所谓buffer就是接收和发送数据时缓存数据的结构。(通俗一点说就是,buffer就是包装你要发送或接收的数据的一种结构。发送或者接收一段数据时要转换为buffer这种数据结构去接收或者发送)
boost::asio提供了两种内存缓冲区结构:
asio::mutable_buffer,用于写服务
asio::const_buffer,用于读服务
他们是一段连续的空间,首字节存储了后续数据的长度。但是这两个结构都没有被asio的api直接使用。
对于api的buffer参数,asio提出了MutableBufferSequence和ConstBufferSequence概念,他们是由多个asio::mutable_buffer和asio::const_buffer组成的。也就是说boost::asio为了节省空间,将一部分连续的空间组合起来,作为参数交给api使用。
二、asio::const_buffer
将数据转换为const_buffer对象有两种方法:通过构造函数包装
const_buffer(const void* data, std::size_t size) noexcept
: data_(data),
size_(size)
{
}
void use_const_buffer() {
std::string s = "hello world";
asio::const_buffer asio_buf(s.c_str(), s.length()); //asio::const_buffer的构造函数
std::vector<asio::const_buffer> buffers_sequence; //使用 std::vector<asio::const_buffer> 可以方便地将多个缓冲区合并成一个发送操作。
//虽然你当前只想发送一个字符串,但未来可能会有更多的数据需要发送
buffers_sequence.push_back(asio_buf);
}
通过asio::buffer()函数将数据转换为const_buffer结构
void use_buffer_str() {
asio::const_buffers_1 output_buf = asio::buffer("hello world!"); //asio::const_buffers_1 主要用于表示只有一个 const_buffer 的情况, class const_buffers_1: public const_buffer
//通过调用 asio::buffer(),不需要手动指定字符串的内容和大小。ASIO 会自动处理这些细节并返回一个 const_buffer,这个步骤简化了use_const_buffer()中的步骤。
//我们也可以将其存到数组中
std::vector<asio::const_buffer> buffers_sequence;
buffers_sequence.push_back(output_buf);
//还可以
buffers_sequence.push_back(asio::buffer("你好,世界"));
}
对于数组这种类型数据,怎样转换为buffer结构
void use_buffer_array() {
const size_t BUF_SIZE_BYTES = 20;
std::unique_ptr<char[]> buf(new char[BUF_SIZE_BYTES]);
auto input_but = asio::buffer(static_cast<void*>(buf.get()), BUF_SIZE_BYTES); //取出buf的原始指针,转化为void*类型的指针,并指明大小,
//asio::buffer()就知道你想把buf这个智能指针转换为buffer结构
}
三、同步读写
同步读写就是进行IO操作是程序会被阻塞,直到完成后才会继续往下执行
asio中提供了几种同步写的api:
同步写write_some
write_some可以每次向指定的空间写入固定的字节数,如果写缓冲区满了,就只写一部分,返回写入的字节数。
void write_to_socket(asio::ip::tcp::socket& sock) {
std::string buf = "hello world";
std::size_t total_bytes_written = 0;
//循环发送
//write_som返回每次写入的字节数
while (total_bytes_written != buf.size()) {
total_bytes_written += sock.write_some(asio::buffer(buf.c_str() + total_bytes_written, buf.size() - total_bytes_written));
}
}
同步写send
write_some使用起来比较麻烦,在传递一段数据时可能需要循环多次调用,(因为缓冲区大小有限,可能一次发不完)。asio中提供了send函数,send函数会一次性的将buffer中的内容全部发送给对端,如果有部分字节因为发送缓冲区满了无法发送,则阻塞等待,直到发送缓冲区可用,则继续发送完成。
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::address::from_string(raw_ip_address), port_num);
asio::io_context ioc;
asio::ip::tcp::socket sock(ioc, ep.protocol());//可以省略协议选择的那步,因为endpoint可以隐式推出协议
sock.connect(ep);
std::string buf = "Hello world!";
int send_length = sock.send(asio::buffer(buf.c_str(), buf.length()));
//send_length有三种情况,为0,表示对端关闭
//小于0说明发生系统级错误
//大于0时,则一定等于buf的长度
if (send_length <= 0) {
std::cout << "send failed" << std::endl;
return 0;
}
}
catch (system::system_error& er) {
std::cout << "Error occured! Error code= " << er.code() << " .Message: " << er.what() << std::endl;
return er.code().value();
}
return 0;
}
同步写write
将建立连接后,新创建的socket作为参数传入
void send_data_by_wirte(asio::ip::tcp::socket& sock) {
std::string buf = "Hello World!";
int send_length = asio::write(sock, asio::buffer(buf.c_str(), buf.length()));
//和send发送一样,都有三种
if (send_length <= 0) {
std::cout << "send failed" << std::endl;
return;
}
}
或者
int send_data_by_write() {
std::string raw_ip_address = "192.168.3.11";
unsigned short port_num = 3333;
try {
asio::ip::tcp::endpoint ep(asio::ip::address::from_string(raw_ip_address), port_num);
asio::io_context ioc;
asio::ip::tcp::socket sock(ioc, ep.protocol());//可以省略协议选择的那步,因为endpoint可以隐式推出协议
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& er) {
std::cout << "Error occured! Error code= " << er.code() << " .Message: " << er.what() << std::endl;
return er.code().value();
}
return 0;
}
同步读read_some
std::string read_from_socket(asio::ip::tcp::socket& sock) {
const unsigned int 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);
}
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_context ioc;
asio::ip::tcp::socket sock(ioc, 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;
}
同步读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::address::from_string(raw_ip_address),
port_num);
asio::io_service 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) {
cout << "receive failed" << endl;
}
}
catch (system::system_error& e) {
std::cout << "Error occured! Error code = " << e.code()
<< ". Message: " << e.what();
return e.code().value();
}
return 0;
}
同步读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::address::from_string(raw_ip_address),
port_num);
asio::io_service 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) {
cout << "receive failed" << endl;
}
}
catch (system::system_error& e) {
std::cout << "Error occured! Error code = " << e.code()
<< ". Message: " << e.what();
return e.code().value();
}
return 0;
}
读取直到指定字符
一直读,读到指定字符才停下来
std::string read_data_by_until(asio::ip::tcp::socket& sock) {
asio::streambuf buf;
// Synchronously read data from the socket until
// '\n' symbol is encountered.
asio::read_until(sock, buf, '\n');
std::string message;
// Because buffer 'buf' may contain some other data
// after '\n' symbol, we have to parse the buffer and
// extract only symbols before the delimiter.
std::istream input_stream(&buf);
std::getline(input_stream, message);
return message;
}
四、同步读取demo
客户端
#include <iostream>
#include <boost/asio.hpp>
using namespace boost;
const int MASSAGE_SIZE = 1024;
int main() {
std::string raw_ip_address = "127.0.0.1"; //本机的回路地址,服务器与本机在同一个地址
unsigned short port_num = 10086;
try {
asio::io_context ioc;
asio::ip::tcp::endpoint ep(asio::ip::address::from_string(raw_ip_address), port_num);
asio::ip::tcp::socket sock(ioc, ep.protocol());
system::error_code error=asio::error::host_not_found; //默认的错误,如果没有错误,主机会重置
sock.connect(ep,error);
if (error) {
std::cout << "connect failed , code is " <<error.value()<<"error message is"<< error.message() << std::endl;
return 0;
}
/*std::cout << "Enter message:";
char request[MASSAGE_SIZE];
std::cin.getline(request,MASSAGE_SIZE);
std::size_t length_r = strlen(request);
asio::write(sock,asio::buffer(request, length_r));
char reply[MASSAGE_SIZE];
size_t reply_length = asio::read(sock,asio::buffer(reply, length_r));
std::cout << "Reply is:";
std::cout.write(reply, reply_length);
std::cout << "\n";*/
for (;;) {
std::cout << "Enter message:";
char request[MASSAGE_SIZE];
std::cin.getline(request, MASSAGE_SIZE);
std::size_t length_r = strlen(request);
asio::write(sock, asio::buffer(request, length_r));
char reply[MASSAGE_SIZE];
size_t reply_length = asio::read(sock, asio::buffer(reply, length_r));
std::cout << "Reply is:";
std::cout.write(reply, reply_length);
std::cout << "\n";
}
}
catch (std::exception& e) {
std::cout << "Exception: " << e.what() << std::endl;
}
return 0;
}
服务端
#include<iostream>
#include<boost/asio.hpp>
#include<set>
#include<memory>
using namespace boost;
const int max_length = 1024;
typedef std::shared_ptr<asio::ip::tcp::socket> socket_ptr; //用于管理socket
std::set<std::shared_ptr<std::thread>> thread_set; //用个集合便于管理线程
//会话
void session(socket_ptr sock) {
try {
for (;;) {
char data[max_length];
memset(data, '\0', max_length);
boost::system::error_code error;
//size_t length = asio::read(sock, asio::buffer(data, max_length), error);
size_t length = sock->read_some( asio::buffer(data, max_length), error);
//或者size_t length = (*sock).read_some(asio::buffer(data, max_length), error);
if (error == boost::asio::error::eof) { //对端关闭错误
std::cout << "connection closed by peer" << std::endl;
}
else if (error) {
throw boost::system::system_error(error);
}
std::cout << "receive from" << sock->remote_endpoint().address().to_string() << std::endl;
std::cout << "receive message is:" << data << std::endl;
//回传给对方
boost::asio::write(*sock, boost::asio::buffer(data, length));
}
}
catch (std::exception& e) {
std::cout << "Exception in thread: " << e.what() << std::endl;
}
}
//建立连接
void serve(boost::asio::io_context& ioc,unsigned short port) {
asio::ip::tcp::acceptor a(ioc, asio::ip::tcp::endpoint(asio::ip::tcp::v4(), port)); //监听对应端口,接收,建立连接,相当于打开大门
for (;;) {
socket_ptr socket(new asio::ip::tcp::socket(ioc)); //创建服务人员socket,这里是利用shared_ptr只能直接初始化的语法
a.accept(*socket); //把客户交给socket处理
auto t = std::make_shared<std::thread>(session, socket); //创建线程+专门管理为客户服务
//因为可能这个线程来不及执行就进入下一个for循环了,但这个线程又是局部变量,所以就直接销毁了
//这里利用智能指针的引用集合了做,让出了当前循环后,还能继续存在
thread_set.insert(t);
}
}
int main() {
try {
boost::asio::io_context ioc;
serve(ioc, 10086);
for (auto& t : thread_set) { //防止因为主线程退出,导致子线程还在工作时强制退出造成的不良影响
t->join();
}
}
catch (std::exception& e) {
std::cout << "Exception in thread: " << e.what() << std::endl;
}
return 0;
}
运行
总结
1.同步读写缺陷在于阻塞,如果客户端不发送数据会导致服务端一直阻塞在read操作,这将导致服务器处于阻塞等待状态。
2.在这个demo中,通过开辟新的线程为新生成的连接处理读写,但是一个进程开辟的线程是有限的,约为2048个线程,在Linux环境可以通过unlimit增加一个进程开辟的线程数,但是线程过多也会导致切换消耗的时间片较多。
3.该服务器和客户端为应答式,实际场景为全双工通信模式,发送和接收要独立分开。
4.该服务器和客户端未考虑粘包处理,关于粘包就是用于tcp的缓冲区积累了一些数据,或者收发端的频率不一致导致的。
当然同步读写的方式也有其优点,比如客户端连接数不多,而且服务器并发性不高的场景,可以使用同步读写的方式。使用同步读写能简化编码难度。后面会用异步处理,优化该服务器。
以上内容都是跟着博主学的,一个非常棒的博主!
补充知识
1.强转
1. C 风格的强制转换
double b = (double)(5);
这种括号包裹的方式实际上是 C 风格的强制类型转换,它可以将一个类型强制转换为另一个类型。在 C 和 C++ 中都适用,但在现代 C++ 编程中并不推荐使用,因为它可能导致意想不到的转换行为。C 风格的强制转换会尝试进行所有类型的转换,包括 static_cast
、reinterpret_cast
和 const_cast
,因此可能难以追踪转换的具体性质。
2. static_cast
double b = static_cast<double>(5);
static_cast
是 C++ 中的一种显式类型转换,用于在类型安全的情况下执行转换。它可以用于以下情况:
- 基本数据类型之间的转换(如
int
到double
) - 指针和引用类型之间的转换(如基类指针到派生类指针,但在此情况下需要确保类型安全)。
使用 static_cast
明确表明了转换的意图,并且比 C 风格的转换更易于阅读和理解。
3. reinterpret_cast
reinterpret_cast
用于低级别的指针转换,通常涉及到不同类型之间完全不同的表示,例如将一个指针转换为另一个完全不同类型的指针。这种转换通常是非常危险的,因为它可能破坏对象的类型信息和内存布局。
4. const_cast
const_cast
主要用于去除或添加对象的 const
限定符。这对于某些 API 函数需要非常重要,尤其是在处理与 C 函数接口兼容的代码时。
5. 使用 static_cast<void*>
当你看到 static_cast<void*>(...)
这样的用法时,这是一种明确将某个特定类型的指针(例如 char*
)转换为通用指针类型 void*
的方式。void*
可以指向任何类型的数据,因此在许多库(如 ASIO)中,要求传递的缓冲区是 void*
类型。
示例对比
std::unique_ptr<char[]> buffer(new char[10]);
asio::const_buffer const_buf1 = asio::buffer(buffer.get(), 10); // 隐式转换 asio::const_buffer const_buf2 = asio::buffer(static_cast<void*>(buffer.get()), 10); // 显式转换
在这两个例子中,const_buf1
和 const_buf2
实际上是等效的,但 const_buf1
更为简洁。