找工作小项目:day16-重构核心库、使用智能指针(1)

day16-重构核心库、使用智能指针

今天是该项目开源在gthub的最后一天,我这里只是将我自己对于这个项目的理解进行总结,如有错误敬请包含指正,今天会整体理一遍代码,并使用智能指针管理整个项目。

1、common

头文件

定义宏用于禁用类的拷贝构造函数、拷贝赋值运算符、移动构造函数和移动赋值运算符。这里注意对右值引用、左值引用、万能引用的理解。

之后这里定义了一些状态,用以防止在发生错误时直接崩溃(这样的程序不够健壮):

RC_UNDEFINED:未定义的状态。

RC_SUCCESS:成功状态。

RC_SOCKET_ERROR:套接字错误。

RC_POLLER_ERROR:轮询器错误。

RC_CONNECTION_ERROR:连接错误。

RC_ACCEPTOR_ERROR:接收器错误。

RC_UNIMPLEMENTED:未实现的功能。

cpp 复制代码
#define DISALLOW_COPY(cname)     \
  cname(const cname &) = delete; \
  cname &operator=(const cname &) = delete;
#define DISALLOW_MOVE(cname) \
  cname(cname &&) = delete;  \
  cname &operator=(cname &&) = delete;
#define DISALLOW_COPY_AND_MOVE(cname) \
  DISALLOW_COPY(cname);               \
  DISALLOW_MOVE(cname);
enum RC {
  RC_UNDEFINED,
  RC_SUCCESS,
  RC_SOCKET_ERROR,
  RC_POLLER_ERROR,
  RC_CONNECTION_ERROR,
  RC_ACCEPTOR_ERROR,
  RC_UNIMPLEMENTED
};

2、Socket

在这里我们仅完成服务端的socket、bind、listen以及accept,客户端的socket以及connect。

头文件

首先在头文件中禁用了类的拷贝构造函数、拷贝赋值运算符、移动构造函数和移动赋值运算符。之后依此就是构造析构函数,设置套接字文件描述符 fd_ 的值,返回当前套接字文件描述符 fd_ 的值,获取与当前连接的对端地址,获取 socket 接收缓冲区中的数据大小,创建 socket,绑定 socket 到指定的 IP 和端口,将 socket 设置为监听状态,接受客户端连接请求,发起与服务器的连接请求,设置 socket 为非阻塞模式,检查 socket 是否为非阻塞模式。

cpp 复制代码
class Socket {
 public:
  DISALLOW_COPY_AND_MOVE(Socket);
  Socket();
  ~Socket();
  void set_fd(int fd);
  int fd() const;
  std::string get_addr() const;
  RC Create();
  RC Bind(const char *ip, uint16_t port) const;
  RC Listen() const;
  RC Accept(int &clnt_fd) const;
  RC Connect(const char *ip, uint16_t port) const;
  RC SetNonBlocking() const;
  bool IsNonBlocking() const;
  size_t RecvBufSize() const;

 private:
  int fd_;
};

实现

实现上我们一步一步来看如何完成的。

构造析构函数,没啥好说的对属性初始化和释放资源:

cpp 复制代码
Socket::Socket() : fd_(-1) {}

Socket::~Socket() {
  if (fd_ != -1) {
    close(fd_);
    fd_ = -1;
  }
}

设置获取fd_属性:

cpp 复制代码
void Socket::set_fd(int fd) { fd_ = fd; }

int Socket::fd() const { return fd_; }

获取与当前连接的对端地址,getpeername用来获取与某个套接字关联的外地协议地址。

cpp 复制代码
std::string Socket::get_addr() const {
  struct sockaddr_in addr;
  memset(&addr, 0, sizeof(addr));
  socklen_t len = sizeof(addr);
  if (getpeername(fd_, (struct sockaddr *)&addr, &len) == -1) {
    return "";
  }
  std::string ret(inet_ntoa(addr.sin_addr));
  ret += ":";
  ret += std::to_string(htons(addr.sin_port));
  return ret;
}

接下来是将socket设置为无阻塞模式以及判断是否为无阻塞状态,首先通过fcntl(fd_, F_GETFL)获取socket的属性并设置为O_NONBLOCK,之后将socket通过fcntl(fd_, F_SETFL, ...)写入到属性中。

cpp 复制代码
RC Socket::SetNonBlocking() const {
  if (fcntl(fd_, F_SETFL, fcntl(fd_, F_GETFL) | O_NONBLOCK) == -1) {
    perror("Socket set non-blocking failed");
    return RC_SOCKET_ERROR;
  }
  return RC_SUCCESS;
}
bool Socket::IsNonBlocking() const { return (fcntl(fd_, F_GETFL) & O_NONBLOCK) != 0; }

获取接收缓冲区的大小,通过ioctl获取文件描述符socket接收缓冲区中的待读取数据大小。

cpp 复制代码
size_t Socket::RecvBufSize() const {
  size_t size = -1;
  if (ioctl(fd_, FIONREAD, &size) == -1) {
    perror("Socket get recv buf size failed");
  }
  return size;
}

创建一个套接字socket。

cpp 复制代码
RC Socket::Create() {
  assert(fd_ == -1);
  fd_ = socket(AF_INET, SOCK_STREAM, 0);
  if (fd_ == -1) {
    perror("Failed to create socket");
    return RC_SOCKET_ERROR;
  }
  return RC_SUCCESS;
}

用于在指定的 IP 地址和端口上绑定 socket 。创建地址sockaddr_in并将套接字socket通过bind绑定到对应地址上。

cpp 复制代码
RC Socket::Bind(const char *ip, uint16_t port) const {
  assert(fd_ != -1);
  struct sockaddr_in addr;
  memset(&addr, 0, sizeof(addr));
  addr.sin_family = AF_INET;
  addr.sin_addr.s_addr = inet_addr(ip);
  addr.sin_port = htons(port);
  if (::bind(fd_, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
    perror("Failed to bind socket");
    return RC_SOCKET_ERROR;
  }
  return RC_SUCCESS;
}

开始将套接字转变为被动连接监听的套接字。

cpp 复制代码
RC Socket::Listen() const {
  assert(fd_ != -1);
  if (::listen(fd_, SOMAXCONN) == -1) {
    perror("Failed to listen socket");
    return RC_SOCKET_ERROR;
  }
  return RC_SUCCESS;
}

从处于 established 状态的连接队列头部取出一个与服务器进行连接。

cpp 复制代码
RC Socket::Accept(int &clnt_fd) const {
  // TODO: non-blocking
  assert(fd_ != -1);
  clnt_fd = ::accept(fd_, NULL, NULL);
  if (clnt_fd == -1) {
    perror("Failed to accept socket");
    return RC_SOCKET_ERROR;
  }
  return RC_SUCCESS;
}

将服务器的IP以及端口作为参数传入,建立同服务器的连接。

cpp 复制代码
RC Socket::Connect(const char *ip, uint16_t port) const {
  // TODO: non-blocking
  struct sockaddr_in addr;
  memset(&addr, 0, sizeof(addr));
  addr.sin_family = AF_INET;
  addr.sin_addr.s_addr = inet_addr(ip);
  addr.sin_port = htons(port);
  if (::connect(fd_, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
    perror("Failed to connect socket");
    return RC_SOCKET_ERROR;
  }
  return RC_SUCCESS;
}

3、Poller

不要忘记Poller在整个项目上的作用是Epoll,一个多路复用 I/O 事件通知接口。其是由一颗红黑树和一个双向链表构成的,处理流程如下:

1、通过 epoll_ctl 函数向 epoll 实例注册一个文件描述符及其感兴趣的事件,文件描述符和事件类型会被存储在 epoll 的红黑树上。

2、当内核检测到某个注册的文件描述符上发生了感兴趣的事件(如可读、可写等),这个文件描述符会被添加到一个内部的双向链表中,这个链表专门存储那些已经就绪的文件描述符。

3、当应用程序调用 epoll_wait 时,epoll 会检查这个双向链表,将其中的就绪事件返回给应用程序。

头文件

可以看到仍旧将类设置为禁止类复制和类移动,通过对树上事件的注册、更新以及删除方法看到我们的操作是针对Channel的,Channel是一个包含事件套接字和事件类型的类。Poll是调用 epoll_wait获得事件的方法。这里实现了Linux和macOS两种方法。

cpp 复制代码
class Poller {
 public:
  DISALLOW_COPY_AND_MOVE(Poller);
  Poller();
  ~Poller();

  RC UpdateChannel(Channel *ch) const;
  RC DeleteChannel(Channel *ch) const;

  std::vector<Channel *> Poll(long timeout = -1) const;

 private:
  int fd_;

#ifdef OS_LINUX
  struct epoll_event *events_{nullptr};
#endif

#ifdef OS_MACOS
  struct kevent *events_;
#endif
};

实现

首先是构造/析构函数,注意万物皆是文件这句话,所以epoll也有属于自己的socket,在构造和析构的过程中不要忘了释放,创建epoll中还要初始化分配关于events_ 的空间,他就是那个双向链表。

cpp 复制代码
Poller::Poller() {
  fd_ = epoll_create1(0);
  ErrorIf(fd_ == -1, "epoll create error");
  events_ = new epoll_event[MAX_EVENTS];
  memset(events_, 0, sizeof(*events_) * MAX_EVENTS);
}

Poller::~Poller() {
  if (fd_ != -1) {
    close(fd_);
  }
  delete[] events_;
}

通过epoll_wait将树上的事件读入到就绪事件链表中,之后根据发生的事件类型将就绪事件的类型进行记录。注意,只有发生与预期事件相关的事件时才会将事件加入就绪队列中。

cpp 复制代码
std::vector<Channel *> Poller::Poll(int timeout) {
  std::vector<Channel *> active_channels;
  int nfds = epoll_wait(fd_, events_, MAX_EVENTS, timeout);
  ErrorIf(nfds == -1, "epoll wait error");
  for (int i = 0; i < nfds; ++i) {
    Channel *ch = (Channel *)events_[i].data.ptr;
    int events = events_[i].events;
    if (events & EPOLLIN) {
      ch->SetReadyEvents(Channel::READ_EVENT);
    }
    if (events & EPOLLOUT) {
      ch->SetReadyEvents(Channel::WRITE_EVENT);
    }
    if (events & EPOLLET) {
      ch->SetReadyEvents(Channel::ET);
    }
    active_channels.push_back(ch);
  }
  return active_channels;
}

获取事件的socket,并将预期的事件类型进行记录,当然如果该事件不存在在红黑树上,我们需要将事件记录到红黑树上。注意是epoll_event中data部分的ptr指向Channel,所以需要Channel中的期待类型来更新epoll_event中的事件类型,之后pool根据这个。

获取事件的socket,并将通道从红黑树上删除,将通道是否在树上的标志位置false。

cpp 复制代码
void Poller::UpdateChannel(Channel *ch) {
  int sockfd = ch->GetSocket()->fd();
  struct epoll_event ev {};
  ev.data.ptr = ch;
  if (ch->GetListenEvents() & Channel::READ_EVENT) {
    ev.events |= EPOLLIN | EPOLLPRI;
  }
  if (ch->GetListenEvents() & Channel::WRITE_EVENT) {
    ev.events |= EPOLLOUT;
  }
  if (ch->GetListenEvents() & Channel::ET) {
    ev.events |= EPOLLET;
  }
  if (!ch->GetExist()) {
    ErrorIf(epoll_ctl(fd_, EPOLL_CTL_ADD, sockfd, &ev) == -1, "epoll add error");
    ch->SetExist();
  } else {
    ErrorIf(epoll_ctl(fd_, EPOLL_CTL_MOD, sockfd, &ev) == -1, "epoll modify error");
  }
}

void Poller::DeleteChannel(Channel *ch) {
  int sockfd = ch->GetSocket()->fd();
  ErrorIf(epoll_ctl(fd_, EPOLL_CTL_DEL, sockfd, nullptr) == -1, "epoll delete error");
  ch->SetExist(false);
}

之后是在macOS上的代码,逻辑相似就是库函数的调用有差别。

cpp 复制代码
#ifdef OS_MACOS

Poller::Poller() {
  fd_ = kqueue();
  assert(fd_ != -1);
  events_ = new struct kevent[MAX_EVENTS];
  memset(events_, 0, sizeof(*events_) * MAX_EVENTS);
}
Poller::~Poller() {
  if (fd_ != -1) {
    close(fd_);
    fd_ = -1;
  }
}
std::vector<Channel *> Poller::Poll(long timeout) const {
  std::vector<Channel *> active_channels;
  struct timespec ts;
  memset(&ts, 0, sizeof(ts));
  if (timeout != -1) {
    ts.tv_sec = timeout / 1000;
    ts.tv_nsec = (timeout % 1000) * 1000 * 1000;
  }
  int nfds = 0;
  if (timeout == -1) {
    nfds = kevent(fd_, NULL, 0, events_, MAX_EVENTS, NULL);
  } else {
    nfds = kevent(fd_, NULL, 0, events_, MAX_EVENTS, &ts);
  }
  for (int i = 0; i < nfds; ++i) {
    Channel *ch = (Channel *)events_[i].udata;
    int events = events_[i].filter;
    if (events == EVFILT_READ) {
      ch->set_ready_event(ch->READ_EVENT | ch->ET);
    }
    if (events == EVFILT_WRITE) {
      ch->set_ready_event(ch->WRITE_EVENT | ch->ET);
    }
    active_channels.push_back(ch);
  }
  return active_channels;
}
RC Poller::UpdateChannel(Channel *ch) const {
  struct kevent ev[2];
  memset(ev, 0, sizeof(*ev) * 2);
  int n = 0;
  int fd = ch->fd();
  int op = EV_ADD;
  if (ch->listen_events() & ch->ET) {
    op |= EV_CLEAR;
  }
  if (ch->listen_events() & ch->READ_EVENT) {
    EV_SET(&ev[n++], fd, EVFILT_READ, op, 0, 0, ch);
  }
  if (ch->listen_events() & ch->WRITE_EVENT) {
    EV_SET(&ev[n++], fd, EVFILT_WRITE, op, 0, 0, ch);
  }
  int r = kevent(fd_, ev, n, NULL, 0, NULL);
  if (r == -1) {
    perror("kqueue add event error");
    return RC_POLLER_ERROR;
  }
  return RC_SUCCESS;
}
RC Poller::DeleteChannel(Channel *ch) const {
  struct kevent ev[2];
  int n = 0;
  int fd = ch->fd();
  if (ch->listen_events() & ch->READ_EVENT) {
    EV_SET(&ev[n++], fd, EVFILT_READ, EV_DELETE, 0, 0, ch);
  }
  if (ch->listen_events() & ch->WRITE_EVENT) {
    EV_SET(&ev[n++], fd, EVFILT_WRITE, EV_DELETE, 0, 0, ch);
  }
  int r = kevent(fd_, ev, n, NULL, 0, NULL);
  if (r == -1) {
    perror("kqueue delete event error");
    return RC_POLLER_ERROR;
  }
  return RC_SUCCESS;
}
#endif

4、Channel

Channel主要是将事件的socket和事件类型进行联系,并加入相应的回调函数,即Channel中包含了事件的socket、状态、处理、轮询等信息。他就是一个集大成socket。

头文件

同样禁止了复制移动类构造函数。其中包含了需要Channel注册的事件循环EventLoop,事件socket,期待事件类型,就绪事件类型以及对应的读写回调函数。

cpp 复制代码
class Channel {
 public:
  DISALLOW_COPY_AND_MOVE(Channel);
  Channel(int fd, EventLoop *loop);
  ~Channel();

  void HandleEvent() const;
  void EnableRead();
  void EnableWrite();

  int fd() const;
  short listen_events() const;
  short ready_events() const;
  bool exist() const;
  void set_exist(bool in = true);
  void EnableET();

  void set_ready_event(short ev);
  void set_read_callback(std::function<void()> const &callback);
  void set_write_callback(std::function<void()> const &callback);

  static const short READ_EVENT;
  static const short WRITE_EVENT;
  static const short ET;

 private:
  int fd_;
  EventLoop *loop_;
  short listen_events_;
  short ready_events_;
  bool exist_;
  std::function<void()> read_callback_;
  std::function<void()> write_callback_;
};

实现

简单的构造和析构函数,析构函数中调用的EventLoop中的DeleteChannel应该是Poller中的DeleteChannel,将事件从树上删除。

cpp 复制代码
Channel::Channel(int fd, EventLoop *loop) : fd_(fd), loop_(loop), listen_events_(0), ready_events_(0), exist_(false) {}

Channel::~Channel() { loop_->DeleteChannel(this); }

根据不同的就绪事件类型调用不同的事件处理函数,注意根据Poller中,如果事件类型不为EPOLLIN、EPOLLOUT、EPOLLET中任何一种,那么事件不会设置就绪事件类型,在这里也就不会调用任何处理函数。

cpp 复制代码
void Channel::HandleEvent() const {
  if (ready_events_ & READ_EVENT) {
    read_callback_();
  }
  if (ready_events_ & WRITE_EVENT) {
    write_callback_();
  }
}

将事件期待的类型进行设置,并通过EventLoop调用Poller中的UpdateChannel。

cpp 复制代码
void Channel::EnableRead() {
  listen_events_ |= READ_EVENT;
  loop_->UpdateChannel(this);
}

void Channel::EnableWrite() {
  listen_events_ |= WRITE_EVENT;
  loop_->UpdateChannel(this);
}
相关推荐
没耳朵的Rabbit35 分钟前
RedHat运维-Ansible自动化运维基础7-管理变量与模块结果
linux·运维·自动化·ansible
深鱼~1 小时前
Linux系统部署MongoDB开源文档型数据库并实现无公网IP远程访问
linux·数据库·mongodb
kinlon.liu1 小时前
Web应用安全实用建议
前端·网络·网络协议·安全·centos
威斯盾科技1 小时前
电力设备巡检管理系统
运维·网络·信息可视化
云计算_Later1 小时前
mycat读写分离 | MHA高可用
linux·mysql·云计算
zhishengwangxiao1 小时前
职升网:一级计量师证书含金量有多少?
运维·服务器
酷酷学!!!1 小时前
C++第二弹 -- C++基础语法下(引用 内联函数 auto关键字 范围for 指针空值)
开发语言·c++
cpp_learners2 小时前
Linux 程序卡死的特殊处理
linux·shell·c/c++·程序卡死·守护程序
windxgz2 小时前
FFmpeg——视频拼接总结
c++·ffmpeg·音视频
科学的发展-只不过是读大自然写的代码2 小时前
ubuntu 进入命令行
linux·运维·ubuntu