|
1 | 1 | # day07-为我们的服务器添加一个Acceptor
|
2 | 2 |
|
3 |
| -代码已写好 |
| 3 | +在上一天,我们分离了服务器类和事件驱动类,将服务器逐渐开发成Reactor模式。至此,所有服务器逻辑(目前只有接受新连接和echo客户端发来的数据)都写在`Server`类里。但很显然,`Server`作为一个服务器类,应该更抽象、更通用,我们应该对服务器进行进一步的模块化。 |
4 | 4 |
|
5 |
| -分离Acceptor类,用来接受连接 |
| 5 | +仔细分析可发现,对于每一个事件,不管提供什么样的服务,首先需要做的事都是调用`accept()`函数接受这个TCP连接,然后将socket文件描述符添加到epoll。当这个IO口有事件发生的时候,再对此TCP连接提供相应的服务。 |
| 6 | +> 在这里务必先理解TCP的面向连接这一特性,在谢希仁《计算机网络》里有详细的讨论。 |
| 7 | +
|
| 8 | +因此我们可以分离接受连接这一模块,添加一个`Acceptor`类,这个类有以下几个特点: |
| 9 | +- 类存在于事件驱动`EventLoop`类中,也就是Reactor模式的main-Reactor |
| 10 | +- 类中的socket fd就是服务器监听的socket fd,每一个Acceptor对应一个socket fd |
| 11 | +- 这个类也通过一个独有的`Channel`负责分发到epoll,该Channel的事件处理函数`handleEvent()`会调用Acceptor中的接受连接函数来新建一个TCP连接 |
| 12 | + |
| 13 | +根据分析,Acceptor类定义如下: |
| 14 | +```cpp |
| 15 | +class Acceptor{ |
| 16 | +private: |
| 17 | + EventLoop *loop; |
| 18 | + Socket *sock; |
| 19 | + InetAddress *addr; |
| 20 | + Channel *acceptChannel; |
| 21 | +public: |
| 22 | + Acceptor(EventLoop *_loop); |
| 23 | + ~Acceptor(); |
| 24 | + void acceptConnection(); |
| 25 | +}; |
| 26 | +``` |
| 27 | +这样一来,新建连接的逻辑就在`Acceptor`类中。但逻辑上新socket建立后就和之前监听的服务器socket没有任何关系了,TCP连接和`Acceptor`一样,拥有以上提到的三个特点,这两个类之间应该是平行关系。所以新的TCP连接应该由`Server`类来创建并管理生命周期,而不是`Acceptor`。并且将这一部分代码放在`Server`类里也并没有打破服务器的通用性,因为对于所有的服务,都要使用`Acceptor`来建立连接。 |
| 28 | + |
| 29 | +为了实现这一设计,我们可以用两种方式: |
| 30 | +1. 使用传统的虚类、虚函数来设计一个接口 |
| 31 | +2. C++11的特性:std::function、std::bind、右值引用、std::move等实现函数回调 |
| 32 | + |
| 33 | +虚函数使用起来比较繁琐,程序的可读性也不够清晰明朗,而std::function、std::bind等新标准的出现可以完全替代虚函数,所以本教程采用第二种方式。 |
| 34 | +> 关于虚函数,在《C++ Primer》第十五章第三节有详细讨论,而C++11后的新标准可以参考欧长坤《现代 C++ 教程》 |
| 35 | +
|
| 36 | +首先我们需要在Acceptor中定义一个新建连接的回调函数: |
| 37 | +```cpp |
| 38 | +std::function<void(Socket*)> newConnectionCallback; |
| 39 | +``` |
| 40 | +在新建连接时,只需要调用这个回调函数: |
| 41 | +```cpp |
| 42 | +void Acceptor::acceptConnection(){ |
| 43 | + newConnectionCallback(sock); |
| 44 | +} |
| 45 | +``` |
| 46 | +而这个回调函数本身的实现在`Server`类中: |
| 47 | +```cpp |
| 48 | +void Server::newConnection(Socket *serv_sock){ |
| 49 | + // 接受serv_sock上的客户端连接 |
| 50 | +} |
| 51 | +``` |
| 52 | +> 在今天的代码中,Acceptor的Channel使用了ET模式,事实上使用LT模式更合适,将在之后修复 |
| 53 | +
|
| 54 | +新建Acceptor时通过std::bind进行绑定: |
| 55 | +```cpp |
| 56 | +acceptor = new Acceptor(loop); |
| 57 | +std::function<void(Socket*)> cb = std::bind(&Server::newConnection, this, std::placeholders::_1); |
| 58 | +acceptor->setNewConnectionCallback(cb); |
| 59 | +``` |
| 60 | +这样一来,尽管我们抽象分离出了`Acceptor`,新建连接的工作任然由`Server`类来完成。 |
| 61 | +> 请确保清楚地知道为什么要这么做再进行之后的学习。 |
| 62 | +
|
| 63 | +至此,今天的教程已经结束了。在今天,我们设计了服务器接受新连接的`Acceptor`类。测试方法和之前一样,使用`make`得到服务器和客户端程序并运行。虽然服务器功能已经好几天没有变化了,但每一天我们都在不断抽象、不断完善,从结构化、流程化的程序设计,到面向对象程序设计,再到面向设计模式的程序设计,逐渐学习服务器开发的思想与精髓。 |
6 | 64 |
|
7 | 65 | 完整源代码:[https://github.com/yuesong-feng/30dayMakeCppServer/tree/main/code/day07](https://github.com/yuesong-feng/30dayMakeCppServer/tree/main/code/day07)
|
0 commit comments