文章目录
以下是本篇文章正文内容
一、同步写
boost::asio提供了几种同步写的api:
write_some
可以每次向指定的空间写入固定的字节数,如果写缓冲区满了,就只写一部分,返回写入的字节数。send
函数会一次性将buffer中的内容发送给对端,如果有部分字节因为发送缓冲区满无法发送,则阻塞等待,直到发送缓冲区可用,则继续发送完成。write
函数,可以一次性将所有数据发送给对端,如果发送缓冲区满了则阻塞,直到发送缓冲区可用,将数据发送完成。
1.1 write_some
write_some
可以每次向指定的空间写入固定的字节数,如果写缓冲区满了,就只写一部分,返回写入的字节数。
void wirte_to_socket(asio::ip::tcp::socket& sock) {
std::string buf = "Hello World!";
std::size_t total_bytes_written = 0;
//循环发送
//write_some会返回每次写入的字节数
//total_bytes_written是已经发送的字节数。
//每次发送buf.length()- total_bytes_written)字节数据
while (total_bytes_written != buf.length()) {
// write_some(),第一个参数是地址,第二个参数是长度
total_bytes_written += sock.write_some(asio::buffer(buf.c_str() + total_bytes_written,
buf.length() - total_bytes_written));
}
}
1.2 send
write_some
使用起来比较麻烦,需要多次调用,asio提供了send
函数。send
函数会一次性将buffer中的内容发送给对端,如果有部分字节因为发送缓冲区满无法发送,则阻塞等待,直到发送缓冲区可用,则继续发送完成。
int send_data_by_send() {
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;
// Step 1. Allocating and opening the socket.
asio::ip::tcp::socket sock(ios, ep.protocol());
sock.connect(ep);
std::string buf = "Hello World!";
/*
三种返回值:
1 小于0,出现socket系统级错误
2 大于0,是buffer的长度,即如果发送不完则一直阻塞等待
3 等于0,对端关闭
*/
int send_length = sock.send(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;
}
1.3 write
类似
send
方法,asio还提供了一个write
函数,可以一次性将所有数据发送给对端,如果发送缓冲区满了则阻塞,直到发送缓冲区可用,将数据发送完成。
在这里插入代码片
二、 同步读
- read_some
- receive
- read
2.1 read_some
读取指定字节数的接口read_some, 数据在不超过指定的长度的时候有多少读多少,读完直接向后运行,没有数据则会一直等待。
所以一般情况下:我们读取数据都需要采用循环读的方式读取数据,因为一次read_some
完毕不能保证读到我们需要长度的数据,read 完一次需要判断读到的数据长度再决定是否还需要再次读取。
代码逻辑:
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) {
// 和write_some类似,返回读取字节的长度,如果读不完则循环下次接着读
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 {
// 创建endpoint
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_some逻辑函数
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;
}
2.2 receive
可以一次性同步接收对方发送的数据,与
send
函数对应
nt 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];
/*
三种返回值:
1 小于0,出现socket系统级错误
2 大于0,是buffer的长度,即如果发送不完则一直阻塞等待
3 等于0,对端关闭
*/
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;
}
2.3 read
一次性同步读取对方发送的数据
asio::read
通常用于同步操作,即调用线程会阻塞直到数据被读取或发生错误。
socket::receive
也可以用于同步操作,但它是socket类的成员函数,可能在某些情况下提供更直接的访问。
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];
// 读操作,参数为socket和buffer,会阻塞知道读满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;
}
三、 同步读写服务器和客户端实例
客户端和服务器采用阻塞的同步读写方式完成通信
3.1 客户端设计
客户端设计基本思路:根据服务器对端的IP和端口创建一个
endpoint
,然后创建socke
t连接这个endpoint
,之后就可以用同步读写的方式发送和接收数据了。
#include <iostream>
#include <boost/asio.hpp>
using namespace std;
using namespace boost::asio::ip;
const int MAX_LENGTH = 1024;
// @file:SyncClient
// @author:IdealSanX_T
// @date:2024/8/8 15:43:46
// @brief:同步读写客户端实现
int main()
{
try {
//创建上下文服务
boost::asio::io_context ioc;
// 构造endpoint,因为此示例中服务器和客户端在同一个主机下,所以用127.0.0.1回环地址,此地址为服务器地址
tcp::endpoint remote_ep(address::from_string("127.0.0.1"), 10086);
// 创建socket
tcp::socket sock(ioc);
// 创建错误代码
boost::system::error_code error = boost::asio::error::host_not_found;
// socket连接endpoint
sock.connect(remote_ep, error);
// 如果出错,打印错误
if (error) {
cout << "connect failed, code is " << error.value() << " error msg is " << error.message();
return 0;
}
// 连接成功处理逻辑
// 客户端发送数据
std::cout << "Enter message: ";
char request[MAX_LENGTH];
// 接收用户输入消息
std::cin.getline(request, MAX_LENGTH);
// 获取消息长度
size_t request_length = strlen(request);
// 同步写发送数据
boost::asio::write(sock, boost::asio::buffer(request, request_length));
// 客户端读取服务器发回的数据
char reply[MAX_LENGTH];
size_t reply_length = boost::asio::read(sock, boost::asio::buffer(reply, request_length));
std::cout << "Reply is: ";
std::cout.write(reply, reply_length);
std::cout << "\n";
}
catch (std::exception& e) {
std::cerr << "Exception: " << e.what() << endl;
}
return 0;
}
3.2 服务器设计
创建session函数,该函数为服务器处理客户端请求,每当我们获取客户端连接后就调用该函数。在session函数里里进行
echo
方式的读写,所谓echo
就是应答式的处理
void session(socket_ptr sock) {
try {
for (;;) {
char data[max_length];
memset(data, '\0', max_length);
boost::system::error_code error;
// 返回读取长度
size_t length = sock->read_some(boost::asio::buffer(data, max_length), error);
// 出错处理,对端关闭的错误
if (error == boost::asio::error::eof) {
std::cout << "connection closed by peer" << endl;
break;
}
else if (error) {
throw boost::system::system_error(error);
}
cout << "receive from " << sock->remote_endpoint().address().to_string() << endl;
cout << "receive message is " << data << endl;
//回传信息值
boost::asio::write(*sock, boost::asio::buffer(data, length));
}
}
catch (std::exception& e) {
std::cerr << "Exception in thread: " << e.what() << "\n" << std::endl;
}
}
完整代码
#include <iostream>
#include <boost/asio.hpp>
#include <set>
#include <memory>
using namespace std;
using namespace boost::asio::ip;
const int MAX_LENGTH = 1024;
using socket_ptr = std::shared_ptr<tcp::socket>; // 智能指针别名
std::set<std::shared_ptr<std::thread>> thread_set; // 线程集合
// @file:SyncServer
// @author:IdealSanX_T
// @date:2024/8/28 15:43:46
// @brief:同步读写服务端实现
/*
*┌────────────────────────────────────────────────┐
*│ FunctionName:session(socket_ptr sock)
*│ Brief :为服务器处理客户端请求,每当我们获取客户端连接后就调用该函数。
*│ 在session函数里里进行echo方式的读写,所谓echo就是应答式的处理。
*└────────────────────────────────────────────────┘
*/
void session(socket_ptr sock) {
try {
for (;;) {
// 存储收到的数据
char data[MAX_LENGTH];
memset(data, '\0', MAX_LENGTH); // 初始化
boost::system::error_code error; // 错误码
// 返回读取长度,read_some读取,注意read_some和read的区别,合理使用
size_t length = sock->read_some(boost::asio::buffer(data, MAX_LENGTH), error);
// 出错处理,对端关闭的错误
if (error == boost::asio::error::eof) {
std::cout << "connection closed by peer" << endl;
break;
}
else if (error) {
throw boost::system::system_error(error);
}
// 打印数据
cout << "receive from " << sock->remote_endpoint().address().to_string() << endl; // 打印地址
cout << "receive message is " << data << endl; // 打印消息
//回传信息值
boost::asio::write(*sock, boost::asio::buffer(data, length));
}
}catch (std::exception& e) {
std::cerr << "Exception in thread: " << e.what() << "\n" << std::endl;
}
}
/*
*┌────────────────────────────────────────────────┐
*│ FunctionName:server(boost::asio::io_context& io_context, unsigned short port)
*│ Brief :根据服务器ip和端口创建服务器acceptor用来接收数据,用socket接收新的连接,然后为这个socket创建session
*└────────────────────────────────────────────────┘
*/
void server(boost::asio::io_context& io_context, unsigned short port) {
// 创建一个接收器,接收客户端TCP连接。这个对象被初始化为监听IPv4地址的指定端口。
tcp::acceptor a(io_context, tcp::endpoint(tcp::v4(), port));
for (;;) {
// 创建一个socket的智能指针
socket_ptr socket(new tcp::socket(io_context));
a.accept(*socket);
// 创建线程,在线程中进行通信的收发
auto t = std::make_shared<std::thread>(session, socket);
// 插入到线程集合中
thread_set.insert(t);
}
}
// 服务器逻辑实现
int main()
{
try {
//创建上下文服务
boost::asio::io_context ioc;
// 调用server
server(ioc, 10086);
// 遍历集合,等待所有线程的通信结束后,主线程才会结束,防止主线程提前结束,对客户端造成不良影响
for (auto& t : thread_set) {
t->join();
}
}
catch (std::exception& e) {
std::cerr << "Exception: " << e.what() << endl;
}
return 0;
}
四、 同步读写优劣
- 劣势
- 同步读写的缺陷在于读写是阻塞的,如果客户端对端不发送数据服务器的read操作是阻塞的,这将导致服务器处于阻塞等待状态。
- 可以通过开辟新的线程为新生成的连接处理读写,但是一个进程开辟的线程是有限的,约为2048个线程,在Linux环境可以通过unlimit增加一个进程开辟的线程数,但是线程过多也会导致切换消耗的时间片较多。
- 该服务器和客户端为应答式,实际场景为全双工通信模式,发送和接收要独立分开。
- 该服务器和客户端未考虑粘包处理。
综上所述,这个服务器和客户端存在的问题,为解决上述问题,后续主要以异步读写改进上述方案。
- 优势
客户端连接数不多,而且服务器并发性不高的场景,可以使用同步读写的方式。使用同步读写能简化编码难度。