7.添加Acceptor class

上一节,我们已经实现了Reactor模式的核心结构了。我们拥有了Server类,而其中很多逻辑都在Server类中。而一个简单的服务器端程序主要由两部分构成,一部分是用accept(2)函数来与客户端进行连接,另一部分是与已连接的客户端(即是TCP连接)进行服务处理等等。

那么,我们就可以把Server分离出上面所说的两部分。那我们可以先分离出建立连接这部分,添加一个类,叫做Acceptor。 

Acceptor class

 很明显,这个类是用来用于accept(2)新TCP连接的。

它是内部类,外界是看不到的,供Server类使用的,Acceptor对象的生命周期由Server控制。

该类应该有个sockfd,其是服务器进行监听的serverFd。

那也应该有一个channel,把该channel添加到epoll实例中,该fd被激活时,该channel的handleEvent()就会执行,即是调用对应的回调函数。那该回调函数是什么?很明显,这是进行监听的serverFd,那就是来建立客户端连接。

class Acceptor
{
public:
	using NewConnectionCallback = std::function<void(int sockfd)>;
public:
	Acceptor(const InetAddr& listenAddr, EventLoop* eventloop);
	~Acceptor();

	void setNewconnectionCallback(const NewConnectionCallback& cb) { newConnectionCallback_ = cb; }

	void listen();
private:
	void handleRead();

	EventLoop* loop_;
	Socket acceptSocket_;    //listen fd,即是服务器fd
	Channel acceptChannel_;

	NewConnectionCallback newConnectionCallback_;    
	bool listen_;
};

1.Acceptor类的构造函数

Acceptor的构造函数和Acceptor::listen()函数会调用socket(2),bind(2),listen(2)等Socket API,也即是把在第6节中Server的构造函数的实现(创建TCP服务端的传统步骤)放到Acceptor构造函数中来实现

Acceptor::Acceptor(const InetAddr& listenAddr, EventLoop* eventloop)
	:loop_(eventloop)
	,acceptSocket_(Socket())
	,acceptChannel_(loop_,acceptSocket_.fd())
	,listen_(false)
{
	acceptSocket_.bind(listenAddr);
	
    //设置回调函数
	auto cb = [this]() {handleRead(); };
	acceptChannel_.SetReadCallback(cb);

	this->listen();
}

void Acceptor::listen()
{
	acceptSocket_.listen();
	acceptChannel_.enableReading();
}

在构造函数中设置好回调函数后,acceptChannel_在可读的时候会回调Acceptor::handleRead(),handleRead()内部会调用accept()来接受新连接,并回调newConnectionCallback_。

2.handleRead()的实现

void Acceptor::handleRead()
{
	InetAddr peerAddr;
	int connfd = acceptSocket_.accept(&peerAddr);
	if (connfd >= 0) {
		if (newConnectionCallback_) {
			newConnectionCallback_(connfd);    //这个是在Server构造函数中进行设置的
            //这即是调用Server::newConnection()
		}
	}
}

//Server类中的newConnection()函数
void Server::newConnection(int sockfd)
{
	setNonblock(sockfd);
	Channel* channel = new Channel(loop_, sockfd);//这版本没有delete,内存泄漏,之后版本会修改
	auto cb = [this,channel]() {handleEvent(channel); };

	channel->SetReadCallback(cb);	//设置回调
	channel->enableReading();
}

这里可能会出现一个疑惑:为什么要在Server构造函数中去设置newConnectionCallback_回调呢?

先来看看上一节的Server::newConnection(Socket* serv_sock)的实现

//上一节的Server::newConnection(Socket* serv_sock)函数
void Server::newConnection(Socket* serv_sock)
{
	InetAddr cliaddr;
	Socket* cli_socket = new Socket(serv_sock->accept(&cliaddr));
	cli_socket->setNonblock();

	Channel* channel = new Channel(loop_, cli_socket->fd());	
	auto cb = [this,channel]() {handleEvent(channel); };

	channel->SetCallback(cb);	//设置回调
	channel->enableReading();
}

上一节的Server::newConnection()函数是不是和现在的Acceptor::handleRead()是差不多的呢。

都是先进行accpet()建立新连接,接着创建新客户对应的channel,并设置其回调函数,之后把channel添加到epoll实例内

那么,我们现在有了Acceptor类,那进行accpet()的操作就可以放在Accpetor类中实现了啦。

为什么不把创建新客户对应的channel这些操作也放到Accpetor类中呢

因为建立连接后的channel都代表一个新客户,这些channel应该由Server类去管理。Acceptor类只负责accpet()建立连接,不负责管理建立连接之后的chananel客户,要分工明确。

前面说了,Server类主要有两部分嘛,一部分是用accept(2)函数来与客户端进行连接,另一部分是与已连接的客户端(即是TCP连接)进行服务处理等等。第一部分对应是Acceptor类,另外的对应就是建立连接之后的channel。

目前这一节Server类还没有把新创建的channel保存下来,之后会修改好这情况(就是可以用一个成员变量保存客户端们的channels)。

所以这个函数需要是放置在Server类中,那Acceptor类对象想要用该函数,那就需要在Server类中去进行绑定,把Server::newConnection()绑定给Acceptor::newConnectionCallback_。

Sever类的改变

首先是添加了Acceptor类变量。

class Server
{
public:
	Server(const InetAddr& serverAddr, EventLoop* eventloop);
	~Server();
	void handleEvent(Channel* ch);
	//void newConnection(Socket* serv_sock);
	void newConnection(int sockfd);

private:
	EventLoop* loop_;

	std::unique_ptr<Acceptor> acceptor_;
	
	/*Socket* serv_socket_;
	Channel* serv_channel_;*/
};

//构造函数的实现
Server::Server(const InetAddr& listenAddr, EventLoop* eventloop)
	:loop_(eventloop)
	//,serv_socket_(new Socket)
	,acceptor_(std::make_unique<Acceptor>(listenAddr,loop_))
{
	auto cb = [this](int sockfd){newConnection(sockfd); };
	acceptor_->setNewconnectionCallback(cb);  //把Server::newConnection()绑定给Acceptor::newConnectionCallback_

    //上一节的做法,可以和现在这节的进行对比学习
	//serv_socket_->bind(listenAddr);
	//serv_socket_->listen();
	//serv_socket_->setNonblock();	//设置非阻塞
	//serv_channel_=new Channel(loop_, serv_socket_->fd());
	//auto cb = [this](){newConnection(serv_socket_); };
	//serv_channel_->SetCallback(cb);	//设置回调
	//serv_channel_->enableReading();	
}

目前可以说,Acceptor类中有两个设置回调的地方,一是在Server构造函数中使用Acceptor类对象时需要设置newConnectionCallback_,二是在Acceptor类内部的成员变量acceptChannel_需要设置回调函数SetReadCallback()。这两点需要记住,分清楚。

Acceptor类主要实现创建TCP服务端的传统步骤,并且接受客户端的连接accept()。这样,Server类更加简洁,也分离出了Acceptor类。

接着我们还可以继续完善,还有连接这部分,下一节再介绍。

完整源代码:https://github.com/liwook/CPPServer/tree/main/code/server_v7

C++中,利用TCP知识编写一个登录系统并记录成绩,可以分为以下几个步骤: 1. **建立网络通信**: - 使用socket编程库(如`boost.asio`或`libcurl`),创建一个服务器端,监听TCP连接。客户端(学生和管理员)也需各自初始化套接字进行通信。 2. **用户认证(登录功能)**: - 客户端发送用户名和密码到服务器。服务器验证数据,如果成功则返回一个确认信息;否则返回错误消息。 3. **成绩录入**: - 登录成功的客户端可以请求记录成绩,通过TCP发送包含学号、课程名以及分数的数据包。服务器接收到后更新数据库。 4. **数据库操作**: - 服务器端需要一个简单的数据库模块(如SQLite、MySQL等),用于存储用户信息和成绩数据。使用SQL查询来插入或更新成绩。 5. **响应处理**: - 服务器对每个请求作出响应,例如更新后的成绩通知或登录状态确认。 6. **错误处理和异常捕获**: - 对于可能出现的网络中断、格式错误等问题,添加适当的错误检查和异常处理机制。 7. **安全性和性能优化**: - 考虑使用SSL/TLS进行加密传输,保护敏感信息。同时,优化网络通信效率,减少延迟。 8. **测试**: - 编写单元测试和集成测试,确保各个功能正常工作,并能处理预期之外的情况。 ```cpp // 示例代码片段(简化版) #include <boost/asio.hpp> using boost::asio::ip::tcp; class Server { public: void start() { io_service.run(); } private: tcp::acceptor acceptor(io_service); std::string handleLogin(tcp::socket socket) { // 处理登录请求... } void handleScoreRequest(tcp::socket socket, const std::string& student_id, int score) { // 更新数据库并回复... } }; int main() { Server server; server.acceptor.open(tcp::v4()); server.acceptor.bind(tcp::endpoint(tcp::v4(), 9000)); server.acceptor.listen(); for (;;) { tcp::socket socket(io_service); server.acceptor.accept(socket); handleLogin(socket); // 分别处理登录和成绩请求 } }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值