【Asio网络编程】socket同步读写


以下是本篇文章正文内容

一、同步写

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,然后创建socket连接这个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;
}


四、 同步读写优劣

  • 劣势
  1. 同步读写的缺陷在于读写是阻塞的,如果客户端对端不发送数据服务器的read操作是阻塞的,这将导致服务器处于阻塞等待状态
  2. 可以通过开辟新的线程为新生成的连接处理读写,但是一个进程开辟的线程是有限的,约为2048个线程,在Linux环境可以通过unlimit增加一个进程开辟的线程数,但是线程过多也会导致切换消耗的时间片较多。
  3. 该服务器和客户端为应答式,实际场景为全双工通信模式,发送和接收要独立分开。
  4. 该服务器和客户端未考虑粘包处理。

综上所述,这个服务器和客户端存在的问题,为解决上述问题,后续主要以异步读写改进上述方案。

  • 优势
    客户端连接数不多,而且服务器并发性不高的场景,可以使用同步读写的方式。使用同步读写能简化编码难度。
  • 17
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值