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
-
构造时候,就需要将handleRead注册到Reactor中,这样到Message到来的时候,回调handleRead。
-
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复杂的地方在于,连接断开的时候该如何处理。
- 主动断开连接,即服务器断开连接。对于服务器主动断开的连接,我们应当将数据发送完毕之后,然后断开连接,即软断开。
- 被动断开连接,即客户端断开连接。被动断开的话,客户端已经不会在接收我们的数据了,应此我们即使有数据要发送给客户端,这时候也应该全部抛弃。
当前先不考虑这些情况。
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的目的自然是为了简化使用网络库,让我们更加专注于业务上去。这里我们主要就是看一下调用栈。
- 设置完毕Acceptor后,当有新连接到来时,我们回调的是
void
Acceptor::OnAccept()
{
int cfd = Socket::Accept(...);
onAcceptCallback(cfd);
}
- onAcceptCallback显然是需要上层注册的,那么自然是TcpServer中某个成员函数。TcpServer::OnNewConnection(int);
void
TcpServer::OnNewConnection(int cfd)
{
TcpConnectionPtr ptr = make_shared<TcpConnection>(cfd,...);
// TcpConnection需要注册自己的回调函数
ptr->SetMessageCallback(onMessage);
ptr->SerConnectionCallback(onConnection);
}
- 显然,这个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