Accepter
:用于接受TCP连接,它是TcpServer
的成员TcpServer
:用于编写网络服务器,接受客户连接TcpConnection
:整个网络库的核心,封装一次TCP连接Buffer
:socket数据的读写通过buffer,用户不需要调用read或write,只需要处理收到的数据和准备好要发送的数据。对sockaddr_in的简单封装,能自动转换字节序。
class InetAddress{
public:
// Constructs an endpoint with given port number.
// Mostly used in TcpServer listening.
explicit InetAddress(uint16_t port);
// Constructs an endpoint with given ip and port.
// ip should be "1.2.3.4"
InetAddress(const std::string& ip, uint16_t port);
// Constructs an endpoint with given struct sockaddr_in
// Mostly used when accepting new connections
InetAddress(const struct sockaddr_in& addr) : addr_(addr) {}
std::string toHostPort() const;
const struct sockaddr_in& getSockAddrInet() const { return addr_; }
void setSockAddrInet(const struct sockaddr_in& addr) { addr_ = addr; }
private:
struct sockaddr_in addr_;
};
Socket
类是对socket文件描述符的封装,也包括一些对socket操作的工具函数。
int createNonblocking();
struct sockaddr_in getLocalAddr(int sockfd);
int getSocketError(int sockfd);
class Socket : noncopyable{
public:
explicit Socket(int sockfd) : sockfd_(sockfd) {}
~Socket();
int fd() const {return sockfd_;}
void bindAddress(const InetAddress& localaddr);
void listen();
// 返回已经设置了non-blocking and close-on-exec的fd
int accept(InetAddress* peeraddr);
void setReuseAddr(bool on);
void shutdownWrite();
void setTcpNoDelay(bool on);
private:
const int sockfd_;
};
Acceptor用于接受新TCP连接,并通过回调通知使用者,它是内部class,供TcpServer
所用,生命期由后者控制。
class Acceptor : noncopyable{
public:
typedef std::function NewConnectionCallback;
Acceptor(EventLoop* loop, const InetAddress& listenAddr);
void setNewConnectionCallback(const NewConnectionCallback& cb) {newConnectionCallback_ = cb;}
bool listenning() const { return listenning_; }
void listen();
private:
void handleRead();
EventLoop* loop_;
Socket acceptSocket_;
Channel acceptChannel_;
NewConnectionCallback newConnectionCallback_;
bool listenning_;
};
Acceptor
在构造时会将其handleRead()
注册为其channel的readCallback_
,而在handleRead()
中则会调用TcpServer
注册的回调函数newConnectionCallback_
。
Tcpserver
类的功能是管理accept获得的TcpConnection
,Tcpserver
是供用户直接使用的,生命期由用户管理。
class TcpServer : noncopyable{
public:
TcpServer(EventLoop* loop, const InetAddress& listenAddr);
~TcpServer();
void start();
void setConnectionCallback(const ConnectionCallback& cb) { connectionCallback_ = cb; }
void setMessageCallback(const MessageCallback& cb) { messageCallback_ = cb; }
void setWriteCompleteCallback(const WriteCompleteCallback& cb) { writeCompleteCallback_ = cb; }
private:
void newConnection(int sockfd, const InetAddress& peerAddr);
void removeConnection(const TcpConnectionPtr& conn);
typedef std::map ConnectionMap;
EventLoop* loop_; // the acceptor loop
const std::string name_;
std::unique_ptr acceptor_;
ConnectionCallback connectionCallback_;
MessageCallback messageCallback_;
WriteCompleteCallback writeCompleteCallback_;
bool started_;
int nextConnId_;
ConnectionMap connections_;
};
在新连接到达时,Acceptor会回调newConnection()
,后者会创建TcpConnection
对象conn,并把它加入ConnectionMap
,设置好callback,再调用conn->connectEstablished()
,其中会回调用户提供的ConnectionCallback
void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr){
loop_->assertInLoopThread();
char buf[32];
snprintf(buf, sizeof(buf), "#%d", nextConnId_);
++nextConnId_;
std::string connName = name_ + buf;
InetAddress localAddr(getLocalAddr(sockfd));
TcpConnectionPtr conn(new TcpConnection(loop_, connName, sockfd, localAddr, peerAddr));
connections_[connName] = conn;
conn->setConnectionCallback(connectionCallback_);
conn->setMessageCallback(messageCallback_);
conn->setCloseCallback(std::bind(&TcpServer::removeConnection, this, std::placeholders::_1));
conn->connectEstablished();
}
TcpConnection
类是最核心也是最复杂的类,它继承了enable_shared_from_this
。我们一步一步逐渐实现
其数据成员如下。TcpConnection
类使用Channel来获取socket上的IO事件,它会自己处理writable事件,而把readable事件通过MessageCallback
传达给用户。TcpConnection
拥有Tcp socket,它会在析构函数中close(fd)。
enum StateE { kConnecting, kConnected, };
void setState(StateE s) { state_ = s; }
void handleRead();
void handleWrite();
void handleClose();
void handleError();
EventLoop* loop_;
std::string name_;
StateE state_;
std::unique_ptr socket_;
std::unique_ptr channel_;
InetAddress localAddr_;
InetAddress peerAddr_;
ConnectionCallback connectionCallback_;
MessageCallback messageCallback_;
CloseCallback closeCallback_;
TcpConnection::handleRead()
会根据read函数的返回值,分别调用messageCallback_
,handleClose()
,handleError()
。
TcpConnection::handleClose()
的主要功能是调用closeCallback_
,这个回调绑定到TcpServer::removeConnection()
。
TcpConnection::connectDestroyed()
是TcpConnection
对象在析构前最后一个调用的函数,它通知用户连接断开。
这里稍微总结一下连接关闭的流程:
TcpConnection::handleRead()
TcpConnection::handleRead()
中read返回0,执行TcpConnection::handleclose()
TcpConnection::handleclose
中首先将连接套接字从epoll中移除(此时并没有在epoller中删除channel),再调用closeCallback_
closeCallback_
绑定到TcpServer::removeConnection()
TcpServer::removeConnection()
将TcpConnection从自己的map中删除,然后将TcpConnection::connectDestroyed()
注册到IO线程中执行TcpConnection::connectDestroyed()
,将channel从epoller
中删除channel。然后TcpConnection
会被析构,相应的channel也被析构。Buffer是非阻塞TCP网络编程必不可少的东西,因为我们的IO线程只能阻塞在IO复用上,而对于应用程序,它只管生成数据,不关心数据是一次性发送还是分成几次发送,所以需要 output buffer,在接收数据时,可能遇到数据不完整,需要 input buffer暂存,等构成一条完整的消息再通知应用程序。
muduo的buffer比较代码比较直观,不在这里过多介绍。
至此,我们基本实现了一个单线程的Reactor网络库,我们通过一个echo server的例子来具体看看各种回调函数是如何被调用的。
EventLoop:
持有一个Epoller
对象poller_
,一个TimerQueue
对象timerQueue_
和一个Channel
对象wakeupChannel_
。
在构造时,会将自己的handleRead()
注册到wakeupChannel_
中(其实就是一个discard函数)。
EventLoop
的指针会被很多运行在该线程的对象持有,有Epoller
,TcpServer
,Acceptor
,TcpConnection
,EventLoop
,主要目的是为了将一些事务放到IO线程中运行,这样可以避免很多同步的问题。
Epoller:
持有EventLoop
的指针ownerLoop_
,自身所有epollfd_
,和由其管理的Channel
的表ChannelMap
,这个表是fd->Channel*
的映射。
Channel:
持有EventLoop
的指针loop_
,负责的fd_
,同时有四个回调函数接口供其使用者注册readCallback_
,writeCallback_
,errorCallback_
,closeCallback_
。这些回调函数是一切回调函数的起点,当有事件到来时,会通过调用Channel::handleEvent
,进行事件分发,调用不同的回调函数。
TcpServer:
持有EventLoop
的指针loop_
,一个Acceptor
对象acceptor_
,和由其管理的TcpConnection
的表TcpConnection
,这个表保存TcpConnection::name_
到TcpConnectionPtr
的映射,还有三个回调函数接口供用户设置connectionCallback_
,messageCallback_
,writeCompleteCallback_
。
在构造时会将TcpServer::newConnection()
注册到acceptor_
的newConnectionCallback_
,而在TcpServer::newConnection()
中会创建一个新的TcpConnection
对象,并将上面三个回调函数接口和TcpServer::removeConnection
4个回调函数注册到TcpConnection
对象中。
Acceptor:
持有EventLoop
的指针loop_
,一个Socket
对象acceptSocket_
,管理acceptSocket_.fd
的Channel
对象acceptChannel_
,一个回调函数接口newConnectionCallback_
供其拥有者TcpServer
注册。
在构造函数中会将acceptSocket_
和指定地址进行绑定,并将Acceptor::handleRead()
注册到acceptChannel_
的readCallback_
,而在Acceptor::handleRead()
中会调用newConnectionCallback_
。
TcpConnection:
持有EventLoop
的指针loop_
,一个Socket
对象socket_
,管理socket_.fd
的Channel
对象channel_
,这点和Acceptor
很像,其实Acceptor
可以看作一个特殊的TcpConnection
。除此之外还会保存双方的地址localAddr_
和peerAddr_
,四个回调函数接口connectionCallback_
,messageCallback_
,writeCompleteCallback_
,closeCallback_
供管理它的TcpServer
注册。最后还有两个Buffer
对象inputBuffer_
和outputBuffer_
。
在构造函数中,会将handleRead(),handleWrite(),handleClose(),handleError()
注册到channel_
中,而这四个函数又会调用上面由TcpServer
注册的回调函数。
TimerQueue:
持有EventLoop
的指针loop_
,自身所有timerfd_
,管理timerfd_
的Channel
对象timerfdChannel_
,还有保存定时器的列表timers_
。
在构造函数中,会将TimerQueue::handleRead()
注册到timerfdChannel_
的readCallback_
,而在TimerQueue::handleRead()
会取出定时器的列表中到时的定时器,并执行对应的定时函数。
#include "TcpServer.h"
#include "EventLoop.h"
#include "InetAddress.h"
#include "utils.h"
#include
void onConnection(const TcpConnectionPtr& conn)
{
if (conn->connected())
{
printf("onConnection(): new connection [%s] from %s\n",
conn->name().c_str(),
conn->peerAddress().toHostPort().c_str());
}
else
{
printf("onConnection(): connection [%s] is down\n",
conn->name().c_str());
}
}
void onMessage(const TcpConnectionPtr& conn,
Buffer* buf,
int64_t receiveTime)
{
printf("onMessage(): received %zd bytes from connection [%s] at %s\n",
buf->readableBytes(),
conn->name().c_str(),
TimeToString(receiveTime).c_str());
conn->send(buf->retrieveAsString());
}
int main()
{
printf("main(): pid = %d\n", getpid());
InetAddress listenAddr(9981);
EventLoop loop;
TcpServer server(&loop, listenAddr);
server.setConnectionCallback(onConnection);
server.setMessageCallback(onMessage);
server.start();
loop.loop();
}
陈硕认为,TCP网络编程的本质是处理三个半事件,即:
我们来讨论一下,当我们运行上面的 echo server 程序之后,使用 netcat 命令与其连接,发送信息,最后断开的整个过程网络库是怎么实现的(主要是各个回调函数是怎么被调用的)。
我们知道,我们调用server.start()
后,Acceptor就开始监听了,调用loop.loop()
就开始处理事件,此时epoller处理的channel就只有Acceptor持有的channel。当有新连接到来,epoll_wait
返回,这个channel可读,所以调用channel的readCallback_
,这个回调接口被Acceptor注册了函数,所以本质上,当有新连接到来时,第一个被调用的回调函数是Acceptor::handleRead()
。
void Acceptor::handleRead(){
loop_->assertInLoopThread();
InetAddress peerAddr(0);
int connfd = acceptSocket_.accept(&peerAddr);
if(connfd >= 0){
if(newConnectionCallback_){
newConnectionCallback_(connfd, peerAddr);
}
else{
close(connfd);
}
}
}
我们看看这个函数执行了什么,它accept后调用newConnectionCallback_
,这个函数接口被TcpServer
注册了函数,所以当有新连接到来时,第二个被调用的回调函数是TcpServer::newConnection()
void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr){
loop_->assertInLoopThread();
char buf[32];
snprintf(buf, sizeof(buf), "#%d", nextConnId_);
++nextConnId_;
std::string connName = name_ + buf;
InetAddress localAddr(getLocalAddr(sockfd));
TcpConnectionPtr conn(new TcpConnection(loop_, connName, sockfd, localAddr, peerAddr));
connections_[connName] = conn;
conn->setConnectionCallback(connectionCallback_);
conn->setMessageCallback(messageCallback_);
conn->setCloseCallback(std::bind(&TcpServer::removeConnection, this, std::placeholders::_1));
conn->connectEstablished();
}
在这个函数中,首先给这个新连接定义了一个name,然后建立了一个TcpConnection
管理新连接,在设置好TcpConnection
的回调接口后,调用了conn->connectEstablished()
void TcpConnection::connectEstablished(){
loop_->assertInLoopThread();
assert(state_ == kConnecting);
setState(kConnected);
channel_->enableReading();
connectionCallback_(shared_from_this());
}
这个函数调用channel_->enableReading()
,将Channel
对象放入poller_
,具体过程是修改channel_
关心的IO事件,然后调用Channel::update()
,这个函数会调用loop_->updateChannel(this)
,也就是EventLoop::updateChannel()
,这个函数又会调用poller_->updateChannel(channel)
,最终修改poller_
。
然后调用了第三个回调函数connectionCallback_
,这个函数是由用户注册到TcpSerer
再由TcpSerer
注册到TcpConnection
中的,也就是 echo server 程序中的void onConnection(const TcpConnectionPtr& conn)
。
至此,一个新连接就被建立完成,并由一个TcpConnection
对象管理,其对应的Channel
对象也放到了epoller
中。
这部分比较简单,和连接的过程比较类似。客户端程序发送信息后,对应的文件描述符可读,调用TcpConnection
持有的channel的readCallback_
,也就是TcpConnection::handleRead()
void TcpConnection::handleRead(int64_t receiveTime){
int savedErrno = 0;
ssize_t n = inputBuffer_.readFd(channel_->fd(), &savedErrno);
if(n > 0){
messageCallback_(shared_from_this(), &inputBuffer_, receiveTime);
}
else if(n == 0){
handleClose();
}
else{
errno = savedErrno;
handleError();
}
}
这个函数在往inputBuffer_
中读入数据后,调用messageCallback_
,这个函数是由用户注册到TcpSerer
再由TcpSerer
注册到TcpConnection
中的,也就是 echo server 程序中的void onMessage(const TcpConnectionPtr& conn, Buffer* buf, int64_t receiveTime)
。它会调用conn->send(buf->retrieveAsString())
,将接收到的数据回显回去。
对用户来说,信息发送的工作在成功调用conn->send(buf->retrieveAsString())
就结束了,剩下的工作由网络库负责。虽然目前是一个单线程的网络库,但是其很多模块是可以运用于多线程场景的,比如TcpConnection::send()
void TcpConnection::send(const std::string& message){
if(state_ == kConnected){
if(loop_->isInLoopThread()){
sendInLoop(message);
}
else{
loop_->runInLoop(std::bind(&TcpConnection::sendInLoop, this, message));
}
}
}
muduo很多同步问题都是通过runInLoop
解决的,send函数如果是在本线程被调用,就会尝试直接发送,否则就调用runInLoop
,这就避免了加锁的操作,如果没有这一步,有可能多个线程尝试对这个TcpConnection
执行send操作,就会发生数据的混乱。
因为这是在本线程中的操作所以会直接调用TcpConnection::sendInLoop()
,如果 output buffer 中没内容,会尝试直接向socket fd中写,如果没写完,或者output buffer本来就有东西,那么向output buffer中写,并channel_->enableWriting()
,之所以需要这一步是因为采用LT模式,所以在需要写的时候才注册写事件,写完后需要注销写事件。
当客户端关闭连接(注意这里不考虑异常情况,比如断电或拔网线,这种情况只能通过主动探测得知连接是否断开,但是通过exit或kill是可以的,都会发送FIN),会发送一个0字节的数据报,通过回调会调用TcpConnection::handleRead()
(具体过程同上),因为读入的字节数为0,所以调用TcpConnection::handleClose()
void TcpConnection::handleClose(){
loop_->assertInLoopThread();
assert(state_ == kConnected || state_ == kDisconnecting);
channel_->disableAll();
closeCallback_(shared_from_this());
}
之后的具体过程在前文中介绍TcpConnection
中有介绍,这里就不重复。
https://github.com/ZhaoxuWang/simple-muduo/tree/main/stage2