在第7节中,我们知道,一个简单的服务器端程序可由两部分构成,一部分是用accept(2)函数来与客户端进行连接,另一部分是与已连接的客户端(即是TCP连接)进行服务处理等等。那么,这一节我们分离出与已连接的客户端(即是TCP连接)进行服务处理的这部分,可以添加一个类,叫做Connection。 其内容基本是按照muduo中的TcpConnection类来设计。
首先看看这一节,我们所使用的服务端的代码例子。
#include"src/Server.h"
#include<stdio.h>
void onMessage(const ConnectionPtr& conn, Buffer* buf) {
std::string msg(buf->retrieveAllAsString());
printf("onMessage() %ld bytes reveived:%s\n", msg.size(), msg.c_str());
conn->send(msg); // 回送接受的数据
}
int main()
{
InetAddr servAddr(10000);
EventLoop loop;
Server server(servAddr, &loop);
//比上一节多了这句代码和上面的onMessage函数
server.setMessageCallback([=](const ConnectionPtr& conn, Buffer* buf) {onMessage(conn, buf); });
loop.loop();
return 0;
}
在这节中Server类中多了函数setMessageCallback(),这是设置业务逻辑函数,即是可以自定义业务逻辑。这里还是回显服务器的业务逻辑,也可以用户自定定义其他的业务逻辑。我们需要记住这个函数。
onMessage函数中的ConnectionPtr是std::shared_ptr<Connection>的别名。
Connection class
class Connection:public std::enable_shared_from_this<Connection>
{
public:
enum class StateE { kDisconnected, kConnecting, kConnected, kDisconnecting };
public:
Connection(EventLoop* loop,int sockfd);
void setMessageCallback(const MessageCallback& cb)
{
messageCallback_ = cb;
}
void setCloseCallback(const CloseCallback& cb)
{
closeCallback_ = cb;
}
void setState(StateE state) { state_ = state; }
void send(Buffer* message);
void send(const std::string& messgage);
void connectEstablished();
void connectDestroyed();
int fd()const { return socket_->fd(); }
private:
void handleRead();
void handleWrite();
void handleClose();
private:
EventLoop* loop_;
StateE state_; //连接的状态
std::unique_ptr<Socket> socket_;
std::unique_ptr<Channel> channel_;
MessageCallback messageCallback_;
CloseCallback closeCallback_;
Buffer inputBuffer_;
Buffer outputBuffer_;
};
using ConnectionPtr = std::shared_ptr<Connection>;
class Connection继承的类先不解释了,之后会说。
先说说 TCP连接的4个连接状态吧。
enum class States { kDisconnected, kConnecting, kConnected, kDisconnecting };
分别代表:已经断开、初始状态、已连接、正在断开。
Connection是一个TCP连接,必然会有sockfd,那就会有封装了fd的Socket类对象,这里使用智能指针std::unique_ptr管理,std::unique_ptr<Socket>。
那也必然会有一个Channel,也是通过Channel来获得socket上的IO事件。
还有两个缓冲区:输入缓冲区,输出缓冲区,使用Buffer类。
还有两个回调函数messageCallback_,closeCallback_。
这两个回调先按下不表,后面会细说。
1.Connection构造函数
Connection::Connection(EventLoop* loop, int sockfd)
:loop_(loop)
,state_(StateE::kConnecting)
,socket_(std::make_unique<Socket>(sockfd))
,channel_(std::make_unique<Channel>(loop,sockfd))
{
//channel_设置好 读,写,的回调函数,当发生读或写事件时,就调用对应的事件
channel_->SetReadCallback([this]() {handleRead(); });
channel_->setWriteCallback([this]() {handleWrite(); });
}
到现在了,应该是熟悉这些设置回调函数的操作了的。
之前说了服务器主要分成两大部分嘛,所以Acceptor和Connection是同一等级的。那来看看会在什么时刻生成Connection类对象。
2.Connection类对象的创建时刻
先看看Server类中如何管理Connection的。
class Server
{
public:
//......还有其他的,省略
using MessageCallback=std::function<void()>;
using ConnectionPtr=std::shared_ptr<Connection>;
using connectionMap=std::unordered_map<int,ConnectionPtr>;//key是该连接的sockfd,value是该连接的Connection的智能指针
void newConnection(int sockfd);
void setMessageCallback(MessageCallback& cb);
private:
EventLoop* loop_;
std::unique_ptr<Acceptor> acceptor_;
connectionMap connetions_;
MessageCallback messageCallback_;
}
Server中是使用std::unordered_map管理该Connection的智能指针。
先要回想下第七节中Acceptor在Server构造函数中设置的newConnection(),这是绑定了Server::newConnection(int sockfd)。
所以是当accpetor_可读时候,即是有新客户端想建立连接时,其就会调用Accpetor::handleRead(),该函数内部会调用Server::newConnection(int sockfd)函数。
void Server::newConnection(int sockfd)
{
setNonblock(sockfd);
auto conn = std::make_shared<Connection>(loop_, sockfd);
connections_[sockfd] = conn;
conn->setMessageCallback(messageCallback_); //设置其业务处理回调函数
conn->setCloseCallback([this](const ConnectionPtr& connection) {removeConnection(connection); });
conn->connectEstablished(); //建立连接,这个函数目前和channel->enableReading()是差不多的
//这是之前的,可以对比学习
//Channel* channel = new Channel(loop_, sockfd); //这版本没有delete,会内存泄漏,之后版本会修改
//auto cb = [this,channel]() {handleEvent(channel); };
//channel->SetReadCallback(cb); //设置回调
//channel->enableReading();
}
//connectEstablished函数的实现
void Connection::connectEstablished()
{
assert(state_ == StateE::kConnecting);
setState(StateE::kConnected);
channel_->enableReading();
}
创建了Connection对象后,设置其业务处理回调函数和关闭连接回调函数,并让其Channel可读。
业务处理的回调函数和关闭连接的回调函数比较重要,后面会细说。
3.创建Connection的相关函数的调用顺序图
现在有了Connection类,来看看创建出Connection的相关函数的调用顺序图,这省略了epoll的调用(即是epoll_wait())。其中Channel::handleEvent()的触发条件是listening socket可读,表示有新连接到来。Server会为新连接创建对应的Connection对象。
4.messageCallback_的设置流程。
messageCallback_是用户设置的业务逻辑函数,是通过最外层Server类调用Server::setMessageCallback()来设置绑定给Server::messageCallback_,接着Connection类再把Server::messageCallback_通过Connection::setMessageCallback()设置为自己的messageCallback_。
那可能有疑惑了哈,为什么不就Connection对象一步到位,直接调用Connection::setMessageCallback()设置给自己呢,为什么需要先通过Server设置呢???
这是因为Connecton类对于用户来说是看不见的,用户是不知道Connection类的。对外使用的是Server。你再看看使用例子,是不是没有Connecton类供你对外使用(除了那onMessage()函数)。所以就需要通过Server类来间接设置,接着内部的Connection类才能设置。
可能说的有点别捏,要结合上面的代码看才好理解。
5. handleRead()函数
void Connection::handleRead()
{
int savedErrno = 0;
auto n = inputBuffer_.readFd(fd(), &savedErrno);
if (n > 0) {
messageCallback_(shared_from_this(), &inputBuffer_); //这个是用户设置好的函数
/*
可以把messageCallback_(shared_from_this(), &inputBuffer_)当做下面的onMessage()函数
void onMessage(const ConnectionPtr& conn, Buffer* buf) {
std::string msg(buf->retrieveAllAsString());
printf("onMessage() %ld bytes reveived:%s\n", msg.size(), msg.c_str());
conn->send(msg); // 回送接受的数据
}*/
}
else if (n == 0) {
handleClose(); //表示客户端关闭了连接
}
else {
printf("readFd error\n"); //表示出错了,现在不处理
}
}
当有读事件发生了,就回调handleRead()函数,其内先经过成员变量inputBuffer_调用readFd()函数读取数据。
若读取数据长度大于0,就调用Connection设置好的messageCallback_函数。若长度为0则表明客户端断开了连接,那就调用handlClose()函数。
6.Connection::handleClose()函数
其内部是调用了Server::removeConnection()函数。
void Connection::handleClose()
{
setState(StateE::kDisconnected);
channel_->disableAll();
ConnectionPtr guardThis(shared_from_this()); //shared_from_this()先按下不表
closeCallback_(guardThis); //closeCallback_就是Server::removeConnection()函数
}
void Server::removeConnection(const ConnectionPtr& conn)
{
auto n = connections_.erase(conn->fd()); //connections_是Server类的成员
conn->connectDestroyed();
}
这里可能会有疑惑,为什么不能一步到位,直接就在Connection类中调用函数关闭呢,为什么还需要使用到Server类的成员函数呢??
因为是Server类管理着Connection连接,要删除Connection对象需要在Server中进行部分处理。也就是说是Server类管理着Connection。那要删除Connection对象,就要经过Server这个管理者去删除嘛, 所以才需要有设置回调,才会有使用到Server类的函数。
其回调中也有使用了其Connection类自己的成员函数connectDestroyed。
void Connection::connectDestroyed()
{
if (state_ == StateE::kConnected) {
setState(StateE::kDisconnected);
channel_->disableAll(); //注销读写事件,所以什么事件都不往epoll中注册
}
channel_->remove(); //在epoll中移走其channel,即是使用EPOLL_CTL_DEL删除其channel对应的sockfd
}
Connection::connectDestroyed()先判断连接的状态是否是已连接,若是就执行以上的操作,并最后移走其channel。
7.触发写事件的时刻
说了这么多,那还剩下epoll是如何去触发EPOLLOUT的,即是服务器端是会在什么时候设置EPOLLOUT的。这里就要看看服务端的主动发送数据函数Connection::send()。
void Connection::send(const std::string& message)
{
if (state_ == StateE::kDisconnected)
return;
bool faultError = false;
ssize_t nwrote = 0;
size_t reamining = message.size();
//如果当前channel没有写事件发生,并且发送缓冲区无待发送的数据,那就可以直接发送
if (!channel_->isWrite() && outputBuffer_.readableBytes() == 0) {
nwrote = ::write(fd(), message.data(), message.size());
if (nwrote >= 0) {
reamining = message.size() - nwrote;
if (reamining == 0) {
//表示数据已完全发送出去,没有操作,之后可以设置数据发送完毕,可以通知用户的事件
}
}
else { //nwrote<0
nwrote = 0;
if (errno != EWOULDBLOCK) {
if (errno == EPIPE || errno == ECONNRESET) {
faultError = true;
}
}
}
}
assert(reamining <= message.size());
if (!faultError && reamining > 0) { //表明数据还没有完全发送完毕
outputBuffer_.append(message.data() + nwrote, reamining);
if (!channel_->isWrite()) {
channel_->enableWriting(); //设置可写,注册EPOLLOUT事件
}
}
}
若当前channel没有写事件发生,并且发送缓冲区中无待发送的数据,那就可以直接调用::write()函数去发送数据。若write()发送的f返回的数据长度是小于要发送的数据的长度,即是表明数据还没有完全发送完毕,这时就把剩余未发送的数据append()到输出Buffer(outputBuffer_)中,接着设置其channel可写,即是注册EPOOLOUT事件。这样用户就无需关注数据要发送多少次才能完全发送完毕,只要是调用了Connection::send()就一定会发送完毕。
那接着下一个疑问就出来了,那要是我所有的数据都发送完毕了,那EPOLLOUT事件还在触发,就会一直busy loop。
我们在Connection构造函数是设置了写事件的,当socket变得可写时候,channel会调用Connection::handleWrite()函数,在其函数内,我们会继续发送outputBuffer_中的数据。当数据发送完毕,即是outputBuffer_.readableBytes() == 0,就立即停止观察writeable事件,这样就可以避免busy loop。
void Connection::handleWrite()
{
if (!channel_->isWrite()) {
printf("Connection fd = % d is down, no more writing\n", channel_->Fd());
return;
}
auto n = ::write(fd(), outputBuffer_.peek(), outputBuffer_.readableBytes());
if (n > 0) {
//更新readerIndex
outputBuffer_.retrieve(n);
if (outputBuffer_.readableBytes() == 0) {//表明要发送的数据已全部发送完毕,所以取消写事件
channel_->disableWriting(); //取消写事件
}
else {
printf("read to write more data\n");
}
}
else {
printf("handleWrite error\n");
}
}
8. Connection断开连接的相关函数调用顺序图
目前只有被动关闭这种方式,即是对方先关闭连接,本地read(2)返回0,触发了关闭逻辑。
现在的Connection类已是基本成型,这一节的程序是可以使用的了。还有Channel类中会有些变化,如添加了enableWriting()等等,还有Connection继承std::enable_shared_from_this<Connection>等等问题,会在下一节进行讲解。
完整源代码:https://github.com/liwook/CPPServer/tree/main/code/server_v9