moduo网络库的reactor模式(下):实现非阻塞TCP网络

1、在reactor框架下加入tcp

Unix下的tcp连接也是经由socket文件描述符(sockfd)实现的。此节只是封装了listening sockefd进行监听(accept(2)),得到的新连接(普通sockfd)直接提供给用户让用户自行处理。下一节才进一步地将得到的新连接也封装起来。

1.1、首先将unix下的socket调用api简易封装成Socket类,得到wapper。即将api调用如socket()、bind()、listen()、accept()等裹上对错误返回值的处理。

#ifndef SOCKET_H_
#define SOCKET_H_

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

class Socket
{
public:
  Socket(unsigned short port) : port_(port) 
  {
    sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd_<0)
    {
      perror("socket");
	  exit(-1);
    }
    // non-block
    int flags = ::fcntl(sockfd_, F_GETFL, 0);
    flags |= O_NONBLOCK;
    int ret = ::fcntl(sockfd_, F_SETFL, flags);
    if(ret==-1)
    {
      perror("fcntl");
	  exit(-1);
    }

    // close-on-exec
    flags = ::fcntl(sockfd_, F_GETFD, 0);
    flags |= FD_CLOEXEC;
    ret = ::fcntl(sockfd_, F_SETFD, flags);
    if(ret==-1)
    {
      perror("fcntl");
	  exit(-1);
    }
  }

  void setReuseAddr(bool on)
  {
    int optval = on ? 1 : 0;
    setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR,
                 &optval, sizeof optval);
    // FIXME CHECK
  }

  ~Socket() 
  {
    close(sockfd_); 
  }
  
  int fd() { return sockfd_; }

  void Bind()
  {
    struct sockaddr_in my_addr;
    bzero(&my_addr,sizeof(my_addr));
    my_addr.sin_family = AF_INET;
    my_addr.sin_port = htons(port_);   //port
    my_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    int err_log=bind(sockfd_, (struct sockaddr*)&my_addr, sizeof(my_addr));
    if(err_log!=0)
    {
        perror("bind");
	    close(sockfd_);
	    exit(-1);
    }
  }
  
  void Listen()
  {
    int err_log = listen(sockfd_, 10);
    if(err_log!=0)
    {
        perror("listen");
	    close(sockfd_);
	    exit(-1);
    }
  }

  int Accept(struct sockaddr_in* peeraddr)
  {
    //struct sockaddr_in addr;
    //bzero(&addr, sizeof addr);
    //socklen_t addr_len=sizeof(addr);
    //int connfd = accept(sockfd_, &addr, &addr_len);

    bzero(peeraddr, sizeof(*peeraddr));
    socklen_t peeraddr_len=sizeof(*peeraddr);

    int connfd = accept(sockfd_, (sockaddr*)peeraddr, &peeraddr_len);
    if (connfd < 0)
    {
      perror("accept");  
    }
    return connfd;
  }

private:
  unsigned short port_;
  int sockfd_;
};
    
#endif

1.2、封装listening sockfd为Acceptor类,封装方法与muduo手法类似。负责监听(accept(2))外部是否有新建连接。其中

Acceptor::enableReading()为用户调用函数,使能listening sockfd可读并加入到poll(2)中进行事件循环。若可读(listen到有新连接),则回调Acceptor::handleRead()进行accept(2)并回调用户函数cb_(),而cb_是由用户调用setNewConnectionCallbackFunc()设置。cb_()则是用户自行设计的普通sockfd读、写等操作处理。

#ifndef ACCEPTOR_H_
#define ACCEPTOR_H_

#include "Socket.hpp"
#include "Channel.hpp"
#include "Thread.hpp"
#include 

class EventLoop;

class Acceptor
{
public:
  typedef std::function AcceptCallbackFunc;
  Acceptor(EventLoop* loop, unsigned short port)
   : loop_(loop), socket_(port), socketChannel_(loop_,socket_.fd())
  {}

  ~Acceptor() {}

  void setNewConnectionCallbackFunc(AcceptCallbackFunc cb)
  {
    cb_=cb;
  }

  //user used function
  void enableReading()
  {
    socket_.Bind();
    socket_.Listen();
    socketChannel_.setReadCallback(std::bind(&Acceptor::handleRead,this));
    socketChannel_.enableReading();
  }
 
  void handleRead()
  {
    struct sockaddr_in peeraddr;
    int connfd=socket_.Accept(&peeraddr);
    //std::cout<<"tid "<=0)
      if(cb_)
        cb_(connfd, &peeraddr);
    else
      ::close(connfd);
  }

  void setReuseAddr(bool on)
  {
    socket_.setReuseAddr(true);
  }

private:
  EventLoop* loop_;
  Socket socket_;
  Channel socketChannel_;
  AcceptCallbackFunc cb_;
};

#endif

1.3、测试

server端:

Acceptor server1(loop, 6666),server2(loop,6688);为两个不同的listening sockfd封装类。

Acceptor::setReuseAddr()为设置该listening sockfd关闭后不必等待则可直接再次使用。

client1()和client2()为用户回调函数,此处用于处理普通socketfd的读操作:当有新建连接时,发送一句话给连接者然后close。

#include "EventLoopThread.hpp"
#include "EventLoop.hpp"
#include "Thread.hpp"
#include "Acceptor.hpp"
#include 
    
using namespace std;
   
void client1(int connfd, sockaddr_in* peeraddr)
{     
  cout<<"tid "<loop(); //test "one thread one loop"
  sleep(20);
  loop->quit();

  sleep(3);
  return 0;
}

client端:

  1 #include 
  2 #include 
  3 #include 
  4 #include 
  5 #include 
  6 #include 
  7 #include 
  8 
  9 int main(int argc, char *argv[])
 10 {
 11     unsigned short port = 6666;
 12     char *server_ip = "127.0.0.1";
 13 
 14     int sockfd = socket(AF_INET, SOCK_STREAM, 0);
 15     if(sockfd<0)
 16     {
 17         perror("socket");
 18         exit(-1);
 19     }
 20 
 21     struct sockaddr_in server_addr;
 22     bzero(&server_addr,sizeof(server_addr));
 23     server_addr.sin_family = AF_INET;
 24     server_addr.sin_port = htons(port);
 25     inet_pton(AF_INET, server_ip, &server_addr.sin_addr.s_addr);
 26 
 27     int err_log = connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
 28     if(err_log!=0)
 29     {
 30         perror("connect");
 31         close(sockfd);
 32         exit(-1);
 33     }
 34 
 35     char buff[4096];
 36     int n=recv(sockfd, buff, 4096, 0);
 37     buff[n]='\0';
 38     printf("Port %d: recv msg from server: %s\n", port, buff);
 39     close(sockfd);
 40 
 41     return 0;
 42 }

另外,Linux 的 nc 命令有个 -p 选项(字母 p 是小写),这个选项的作用就是 nc 在模拟客户端程序时,可以使用指定端口号连接到服务器程序上去。我们还是以上面的服务器程序为例,这个我们不用我们的 client 程序,改用 nc 命令来模拟客户端。在 shell 终端输入: 

nc -v -p 9999 127.0.0.1 6666

-v 选项表示输出 nc 命令连接的详细信息,这里连接成功以后,会输出“Ncat: Connected to 127.0.0.1:6666.” 提示已经连接到服务器的 6666 端口上去了。

-p 选项的参数值是 9999 表示,我们要求 nc 命令本地以端口号 9999 连接服务器,注意不要与端口号 6666 混淆,6666 是服务器的侦听端口号,也就是我们的连接的目标端口号,9999 是我们客户端使用的端口号。我们用 lsof 命令来验证一下我们的 nc 命令是否确实以 9999 端口号连接到 server 进程上去了。

 

测试:

server端:

baddy@ubuntu:~/Documents/Reactor/s2.1$ ./testAcceptorDemo 
Main: pid: 56222 tid: 56222
tid 56222: create a new thread
tid 56222: waiting
tid 56223: Thread::func_() started!
tid 56223: notified
tid 56222: received notification
tid 56223: start looping...
tid 56223: server accept a connector
tid 56223: server accept a connector
tid 56223: server accept a connector
tid 56223: end looping...
tid 56223: Thread end!

client端: 

baddy@ubuntu:~/Documents$ ./tcp_send
Port 6666: recv msg from server: How are you
baddy@ubuntu:~/Documents$ ./tcp_send2
Port 6688: recv msg from server: What's up
baddy@ubuntu:~/Documents$ ./tcp_send2
Port 6688: recv msg from server: What's up

 


2 封装TCP网络

在上一节的基础上进一步封装普通sockfd为TcpConnection类,并将listening sockfd类Acceptor和普通sockfd类TcpConnection封装到类TcpServer供用户使用。

封装方式与muduo手法基本一致:把当前对象(Acceptor对象)封装到一个新类(TcpServer类)中成为其数据成员。在构造函数中构造该成员(此操作相当于上节中用户自行构造Acceptor server1(loop,6666)),并在构造函数中使该成员回调新的回调函数(把回调Acceptor::handleRead()改为TcpServer::newConnection())。在新的回调函数中构造TcpConnection对象(用于封装accept(2)得到的普通sockfd),当普通sockfd有事件发生时则回调由用户传入的回调函数(相当于上节中的client1())。

用户通过设置操作普通sockfd读、写等事件的回调函数,然后调用TcpServer::start()使能listening sockfd,进入事件循环则可实现非阻塞TCP网络。

moduo网络库的reactor模式(下):实现非阻塞TCP网络_第1张图片

  • 2.1 封装listening sockfd为Acceptor类,封装方法与muduo手法类似。负责监听(accept(2))外部是否有新建连接。

    Acceptor用于监听,关注连接,建立连接后,由TCPConnection来接管处理;

    这个类没有业务处理,用来处理监听和连接请求到来后的逻辑; 

    所有与事件循环相关的都是Channel,Acceptor不直接和EventLoop打交道,所以在这个类中需要有一个Channel的成员,并包含将Channel挂到事件循环中的逻辑(listen())。

  • 2.2 封装普通socket为TcpConnection类,封装方法与muduo手法类似。负责接收新建的普通sockfd。

    TcpConnection处理连接建立后的收发数据;业务处理回调完成。

  • 2.3 封装供用户使用的TcpServer类,封装负责监听listening socketfd的Acceptor类和普通sockfd的TcpConnection类。

    作为最终用户的接口方,和外部打交道通过TCPServer交互,而业务逻辑处理将回调函数传入到底层,这种传递函数的方式犹如数据的传递一样自然和方便;

    作用Acceptor和TcpConnection的粘合剂,调用Acceptor开始监听连接并设置回调,连接请求到来后,在回调中新建TcpConnection连接,设置TcpConnection的回调(将用户的业务处理回调函数传入,包括:连接建立后,读请求处理、写完后的处理,连接关闭后的处理),从这里可以看到,业务逻辑的传递就跟数据传递一样,多么漂亮。


总结:

(1)通过对象回调机制,一个对象A可以把一个特定工作委托给另一个对象B的一个方法来完成。A不必知道B的名字,也不用知道它的类型,甚至都不需要知道B的存在,只要求B对象具有一个签名正确的方法,就可以通过回调机制把工作交给B的这个方法来执行。在C语言里,这个机制是通过函数指针实现的,所以很自然的,在C++里,我们希望通过指向成员函数的指针(如std::function+std::bind())来解决类似问题。

(2)函数式编程中,类之间的关系主要通过组合来实现,而不是通过派生实现; 这也是函数式编程的一个设计理念,更多的使用组合而不是继承来实现类之间的关系,而支撑其能够这样设计的根源在于function()+bind()带来的函数自由传递,实现回调非常简单; 而OO设计中,只能使用基于虚函数/多态来实现回调,不可避免的使用继承结构。


参考资料

https://github.com/chenshuo/muduo

C++ 工程实践(5):避免使用虚函数作为库的接口

function/bind的救赎(上)

你可能感兴趣的:(linux网络编程)