简易的服务器版本的全双工模式

87 阅读4分钟

为什么加锁: 我们不能保证发送的接口(一般是逻辑层线程),和回调函数的接口(一般在网络层)在一个线程,就存在两个线程对这个队列的共同访问,所以增加一个锁保证队列安全性.

加什么锁?

std::lock_guard 是 C++ 标准库中的一个类模板,用于管理互斥锁(mutex)的获取和释放。它是一个 RAII(Resource Acquisition Is Initialization)工具,确保在构造时获得锁,并在析构时自动释放锁。这有助于避免死锁,因为它保证了锁的释放即使在抛出异常时也会发生。

#include <mutex>
#include <iostream>
#include <thread>
std::mutex mtx;
void threadSafeFunction(int value) {
    std::lock_guard<std::mutex> lock(mtx);
    // 临界区开始
    std::cout << "Value: " << value << std::endl;
    // 临界区结束,lock 被销毁,mtx 自动解锁
}

int main() {
    std::thread t1(threadSafeFunction, 1);
    std::thread t2(threadSafeFunction, 2);

    t1.join();
    t2.join();

    return 0;
}

t.join()方法只会使主线程(或者说调用t.join()的线程)进入等待池并等待t线程执行完毕后才会被唤醒。并不影响同一时刻处在运行状态的其他线程。

设计一个数据节点MsgNode用来存储数据

class MsgNode{    
friend class CSession;
public:
    MsgNode(char * msg, int max_len) {        _
        data = new char[max_len];        
        memcpy(_data, msg, max_len);    }    
        ~MsgNode() {        
        delete[] _data;    }
private:    int _cur_len;    int _max_len;    char* _data;
};

1  _cur_len表示数据当前已处理的长度(已经发送的数据或者已经接收的数据长度),因为一个数据包存在未发送完或者未接收完的情况。
2  _max_len表示数据的总长度。
3  _data表示数据域,已接收或者已发送的数据都放在此空间内。

封装发送接口

首先在Session类里新增一个队列存储要发送的数据,因为我们不能保证每次调用发送接口的时候上一次数据已经发送完,就要把要发送的数据放入队列中,通过回调函数不断地发送。而且我们不能保证发送的接口和回调函数的接口在一个线程,所以要增加一个锁保证发送队列安全性。
同时我们新增一个发送接口Send 修改后的session

``

class Session : public std::enable_shared_from_this<Session>//一个模板类
{
public:
	Session(boost::asio::io_context& ioc,Server* server) :_socket(ioc),_server(server) {
		boost::uuids::uuid a_uuid = boost::uuids::random_generator()();//为什么有两个括号?
		_uuid = boost::uuids::to_string(a_uuid);
	}
	~Session() {
		std::cout << "session desturct delete this" << this << std::endl;
	}
	tcp::socket& Socket() {
		return _socket;
	}

	void Send(char* msg, int max_length);//111111111
	void Start();//监听客户端的读和写
	std::string& GetUuid();
private:
	Server* _server;
	std::string _uuid;
	tcp::socket _socket;
	enum{MAX_LENGTH=1024};
	char _data[MAX_LENGTH];
	void HandleRead(const boost::system::error_code& ec,size_t bytes_transferred, std::shared_ptr<Session> _self_shared);//当监听到客户端发了数据的时候调用读的回调函数
	void HandleWrite(const boost::system::error_code& ec, std::shared_ptr<Session> _self_shared);
	
	std::queue<std::shared_ptr<MsgNode> > _send_que;//2222222222
	std::mutex _send_lock;//3333333333
};


发送接口实现

void Session::Send(char* msg, int max_length) {
	//注意这里是一个局部变量,所以可以在锁的上面,它不会被共享
	bool pending = false;//ing表示发送队列有数据==上一次还没发完
	std::lock_guard<std::mutex> lock(_send_lock);
	if (_send_que.size() > 0) {//有上次的东西就置true,那样下面封装进节点进队列后就可以判断发上次的还是发这次了
		pending = true;
	}
	_send_que.push(make_shared<MsgNode>(msg, max_length));
	if (pending) {//发上次的,直接退出
		return;
	}
	//没东西了发这次的
	boost::asio::async_write(_socket, boost::asio::buffer(msg, max_length),
		std::bind(&Session::HandleWrite, this, std::placeholders::_1, shared_from_this()));
}

回调写调整

void Session::HandleWrite(const boost::system::error_code& ec, shared_ptr<Session> _self_shared) {
	if (!ec) {
		std::lock_guard<std::mutex> lock(_send_lock);
		_send_que.pop();//为什么直接出队了?因为HandleWrite调用成功了,意味着他的外层函数已经将数据发完了
		if (!_send_que.empty()) {//出对一个还有数据节点,那么就拿出来接着发
			auto& msgnode = _send_que.front();
			boost::asio::async_write(_socket, boost::asio::buffer(msgnode->_data, msgnode->_max_len),
				std::bind(&Session::HandleWrite, this, std::placeholders::_1, _self_shared));
		}

	}
	else {
		cout << "handle write failed,error is " << endl;
		//delete this;//把Session销毁掉,实际这么做有隐患
		_server->ClearSession(_uuid);
	}
}

回调函数调整做到一直监听

    void Session::HandleRead(const boost::system::error_code& ec, size_t bytes_transferred,shared_ptr<Session> _self_shared) {
	if (!ec) {//外层异步读函数读完之后,触发读回调,把读到的写回去后初始化数组,继续异步读去监听,这样做到一直监听
		cout << "server recervi data is: " << _data << endl;
		Send(_data, bytes_transferred);
		memset(_data, 0, MAX_LENGTH);
		_socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH),
			std::bind(&Session::HandleRead, this, std::placeholders::_1,std::placeholders::_2, _self_shared));

	}
	else {
		cout << "read error" << endl;
		//delete this;
		_server->ClearSession(_uuid);
	}
}

但仍会有粘包问题