东阳的学习笔记
到目前为之,Reactor 事件处理框架已初具规模,本文开始我们用它逐步实现一个非阻塞TCP网络编程库。
从 poll(2)返回到再次调用 poll(2)阻塞称为一次事件循环。
- 传统的 Reactor 实现一般会把 timers 做成循环中单独的一步。
一、Acceptor class
先定义 Acceptor class,用于 accept(2) 新TCP连接,并通过回调通知使用者;供 TcpServer 使用,生命期由后者控制。
下面是Acceptor接口:
class Acceptor : boost::noncopyable { public: typedef boost::function<void (int sockfd, const InetAddress&)> NewConnectionCallback; Acceptor(EventLoop* loop, const InetAddress& listenAddr); void setNewConnectionCallback(const NewConnectionCallback& cb) { newConnectionCallback_ = cb; } bool listenning() const { return listenning_; } void listen();
1.1 Acceptor 的数据成员
Acceptor的数据成员包括 Socket、Channel等
- 其中 Socket 是一个 RAII handle,封装了 socket 文件描述符的生命期;Acceptor的 socket 是
listen socket
- Channel 用于观察此 socket 上的 readable 事件,并回调 Acceptor::handleRead()
- handleRead会调用 accept(2) 来接受新连接,并回调用户
callback
。private: void handleRead(); EventLoop* loop_; Socket acceptSocket_; Channel acceptChannel_; NewConnectionCallback newConnectionCallback_; bool listenning_; };
1.2 构造函数和Acceptor::listen()
Acceptor 的构造函数和
Acceptor::listen()
执行创建TCP服务端的传统步骤
- 即调用socket(2) -> bind() -> listen(2) 等 Socket API,其中任何一个步骤出错都会造成程序终止,因此这里并没有对错误进行处理。
Acceptor::Acceptor(EventLoop* loop, const InetAddress& listenAddr) : loop_(loop), acceptSocket_(sockets::createNonblockingOrDie()), acceptChannel_(loop, acceptSocket_.fd()), listenning_(false) { acceptSocket_.setReuseAddr(true); acceptSocket_.bindAddress(listenAddr); acceptChannel_.setReadCallback( boost::bind(&Acceptor::handleRead, this)); } void Acceptor::listen() { loop_->assertInLoopThread(); listenning_ = true; acceptSocket_.listen(); acceptChannel_.enableReading(); }
1.3 sockets::createNoneblockingOrDie()
Acceptor的构造函数调用 creatNoneblockingOrDie() 来创建非阻塞的 socket,现在的 Linux 可以一步完成。
int sockets::createNonblockingOrDie() { // socket #if VALGRIND int sockfd = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (sockfd < 0) { LOG_SYSFATAL << "sockets::createNonblockingOrDie"; } setNonBlockAndCloseOnExec(sockfd); #else int sockfd = ::socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, IPPROTO_TCP); if (sockfd < 0) { LOG_SYSFATAL << "sockets::createNonblockingOrDie"; } #endif return sockfd; }
1.4 Acceptor::handleRead()
Acceptor::listen() 的最后一步让 acceptChannel_ 在 socket 可读的时候调用 Acceptor::handleRead(),后者会接受(accept(2))并回调 newConnectionCallback_。这里直接把
socket fd
传给 callback,这种传递 int 句柄的做法不够理想。
- 在 C++11 中可以先创建 Socket 对象。 再用移动语义把 Socket 对象 std::move() 给回调函数,确保资源的安全释放。
- 这里没有考虑文件描述符耗尽的情况,muduo 的处理办法见 书本 &7.7。(
限制最大并发连接数的方法
);还有一个方法,在拿到大于等于0的 connfd 之后,非阻塞地 poll(2) 一下,看看 fd 是否可读写。正常情况下,看看 fd 是否可读写。正常情况下 poll(2) 会返回 writeable,表明 connfd 可用。反之,不可用,应关闭连接。Acceptor::handleRead() 的策略很简单:(
后面两种适合做短连接服务
)
- 每次 accept(2) 一个 socket。另外还有两种实现策略:
- 一是每次循环 accept(2),直至没有新的连接到达
- 二是
每次尝试 accept(2) N 个新连接
,N的值一般是10。void Acceptor::handleRead() { loop_->assertInLoopThread(); InetAddress peerAddr(0); //FIXME loop until no more int connfd = acceptSocket_.accept(&peerAddr); if (connfd >= 0) { if (newConnectionCallback_) { newConnectionCallback_(connfd, peerAddr); } else { sockets::close(connfd); } } }
1.5 sockets::accept()
Linux 新增的系统调用可以直接 accept(2) 一步得到非阻塞的 socket。
>int sockets::accept(int sockfd, struct sockaddr_in* addr) { socklen_t addrlen = sizeof *addr; #if VALGRIND int connfd = ::accept(sockfd, sockaddr_cast(addr), &addrlen); setNonBlockAndCloseOnExec(connfd); #else int connfd = ::accept4(sockfd, sockaddr_cast(addr), &addrlen, SOCK_NONBLOCK | SOCK_CLOEXEC); #endif if (connfd < 0) { int savedErrno = errno; LOG_SYSERR << "Socket::accept"; switch (savedErrno) { case EAGAIN: // 这里区分暂时错误和致命错误,并区别对待。 // 对于暂时错误,例如 EAGAIN、EINTR、ECONNABORTED case EOPNOTSUPP: // unexpected errors LOG_FATAL << "unexpected error of ::accept " << savedErrno; break; default: LOG_FATAL << "unknown error of ::accept " << savedErrno; break; } } return connfd; }