从零开始写一个Redis-3

Redis-03

1. Timestamp

时间点。
如何理解时间点呢,对于点来说,一定要有参考点,单独一个点的值是没有意义的,对于距离点来说,比如5,这实际上是相对于0偏移+5。而时间点也是,就是距离epoch时间(参考时间)一段时长就是事件点。

这里,我将距离epoch时间多少ms作为时间点,因此我们的时间精度就认为是ms。

因此利用C++11中的chrono来做了时间点。

1.1 源代码

using std::chrono::milliseconds;
class Timestamp
{
public:
  explicit Timestamp() = default;
  explicit Timestamp(const milliseconds &ms)
    : epochMilliseconds_(ms)
  {
  }
  Timestamp & operator+=(const Timestamp& ts)
  {
    this->epochMilliseconds_ += ts.epochMilliseconds_;
    return *this;
  }
  Timestamp & operator+(const Timestamp& ts)
  {
    return (*this)+=ts;
  }
  Timestamp & operator-=(const Timestamp& ts)
  {
    this->epochMilliseconds_ -= ts.epochMilliseconds_;
    return *this;
  }
  Timestamp & operator-(const Timestamp& ts)
  {
    return (*this)-=ts;
  }
  

  std::chrono::milliseconds Milliseconds()const 
  { 
    return epochMilliseconds_;
  }
  static Timestamp Now(){
    return Timestamp(std::chrono::duration_cast<milliseconds>(std::chrono::system_clock::now().time_since_epoch()));
  }
private:
  std::chrono::milliseconds epochMilliseconds_;
};

2. TcpConnection

TcpConnection和用户层的读写Buffer配合是非常绝妙的搭配。如果看过muduo手册中,谈到用户区的Buffer意义。一定明白这个重要性。

因为Tcp是一个字节流协议,所以我们是没办法预测我们收到的字节流是否满足一个完整的包。即使收到一个没有完整的包,我们也不能随意丢弃,因此就存储在ReadBuffer。当我们的确收到一个完整的包后,再去解包。
因此对于TcpConnection来说,当读事件就绪的时候,就是很简单的将收到的数据从内核缓冲区搬到用户缓冲区。然后再去调用上次注册的回调函数。

因此这里最重要的就是如何处理收发了。

2.1 类简要

class TcpConnection
{
public:
private:
    void handleRead();
    void handleWrite();
    
    Socket sock_;
    FileEvent fe_;
}

拥有一个Socket用来管理fd。
拥有一个FileEvent用来管理事件。

2.2 handleRead

  1. 构造时候,就需要将handleRead注册到Reactor中,这样到Message到来的时候,回调handleRead。

  2. handleRead主要就是将收到的消息存放到readBuffer中。然后再去回调上层的回调函数。

其大致思路应该是这个样子的。

class TcpConnection
{
public:
    TcpConnection()
        : ...//初始化
    {
        fe_.RegisterReadHandler(handleRead);
        fe_.EnableReading();
    }
    void SetMessageCallback();
private:
    void handleRead()
    {
        int n = readBuffer_.read(sock.FD());
        if(n > 0){
            messageCallback_(...);
        }
        else if(!n){
            handleClose();
        }
        else{
            handleError();
        }
    }
    void handleWrite();
    
    MessageCallback messageCallback_;
    Buffer readBuffer_;
    Socket sock_;
    FileEvent fe_;
}

2.3 Send和handleWrite

对于发送消息来说,做的比较简单。实际上就是先将消息发送过去,如果没有发送完毕的话,即还残留有数据的话,就存放到writeBuffer中去,然后注册一个写回调函数去处理。

void
TcpConnection::Send(const char *data, int len)
{
    int n = write(sock_.Fd(), ...);
    if(n > 0)
    {
        if(n < len)
        {
            // 注册写回调函数
            fe_.RegisterWriteHandler(handleWrite);
            fe_.EnableWritiing();
        }
    }
    else{
    }
}

因此,我们知道,当我们Send数据的时候,数据实际上可能并没有发送出去,而是交给网络库去发送。那么,如果网络库正在发送消息的话,我当前也就没有必要发送了。直接全交给网络库就好了。

void
TcpConnection::Send(const char *data, int len)
{
    if(!fe_.IsWriting())
    {
    int n = write(sock_.Fd(), data, len);
    if(n > 0)
    {
        if(n < len)
        {
            // 注册写回调函数
            writeBuffer_.append(data + n, len - n);
            fe_.RegisterWriteHandler(handleWrite);
            fe_.EnableWritiing();
        }
    }
    else{
    }
    }
}

handleWrite复杂的地方在于,连接断开的时候该如何处理。

  1. 主动断开连接,即服务器断开连接。对于服务器主动断开的连接,我们应当将数据发送完毕之后,然后断开连接,即软断开。
  2. 被动断开连接,即客户端断开连接。被动断开的话,客户端已经不会在接收我们的数据了,应此我们即使有数据要发送给客户端,这时候也应该全部抛弃。

当前先不考虑这些情况。

void 
TcpConnection::handleWrite()
{
    int n = send(sock_.FD(), writeBuffer_.data(), writeBuffer_.readable());
    writeBuffer_.updateIndex(n);
    if(writeBuffer_.readable() == 0)
    {
        // 取消事件
        fe_.DisableWriting();
    }
    else{
    }
}

3. TcpServer

最后一个类计时TcpServer,完成这个类后,我们这个用于Redis的单线程Reactor网络库基本就完成了。

TcpServer的目的自然是为了简化使用网络库,让我们更加专注于业务上去。这里我们主要就是看一下调用栈。

  1. 设置完毕Acceptor后,当有新连接到来时,我们回调的是
void
Acceptor::OnAccept()
{
    int cfd = Socket::Accept(...);
    onAcceptCallback(cfd);
}
  1. onAcceptCallback显然是需要上层注册的,那么自然是TcpServer中某个成员函数。TcpServer::OnNewConnection(int);
void
TcpServer::OnNewConnection(int cfd)
{
    TcpConnectionPtr ptr = make_shared<TcpConnection>(cfd,...);
    
    // TcpConnection需要注册自己的回调函数
    ptr->SetMessageCallback(onMessage);
    ptr->SerConnectionCallback(onConnection);
}
  1. 显然,这个onMessage和onConnection里面涉及真正的业务代码,因此,我们需要TcpServer提供给我们设置这俩个回调函数的接口。从而新连接到来的时候,其回调函数被自动设置。

因此对于每个连接上有消息到来时,其调用栈如下。

TcpConnection::handleRead先被调用
然后调用TcpConnection下的onMessageCallback_。这个回调函数是通过TcpServer注册得到的。

4. EchoServer

还是以一个简单的Echo服务器来看下如何使用。

class EchoServer
{
public:
  explicit EchoServer(const SocketAddr& addr)
    : server_(addr)
    , addr_(addr)
  {
    server_.SetConnectionCallback(std::bind(&EchoServer::OnConnection, this, std::placeholders::_1));
    server_.SetMessageCallback(std::bind(&EchoServer::OnMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
  }

  void Start()
  {
    server_.Start();
  }
  void OnConnection(TcpConnectionPtr connPtr)
  {
    cout << ">>>>>> new connection" << endl;
  }

  void OnMessage(TcpConnectionPtr connPtr, Buffer *buff, Timestamp ts)
  {
    string recv = buff->returnAllStringAndUpdate();
    connPtr->Send(recv.data(), recv.size());
  }

private:
  TcpServer server_;
  SocketAddr addr_;
};

int main()
{
  Logger::setLogLevel(Logger::LogLevel::DEBUG);
  SocketAddr addr("127.0.0.1:8888");
  EchoServer server(addr);
  server.Start();
}

5. 一些补充

有关FileEvent的类的修改。

对于FileEvent,实际上我有这么一个需求,就是我希望其只能被智能指针管理,为什么?

请查看我的这篇文章

这里的代码主要集中在Redis_Server_0.3中
github

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值