Redis的实现二: c、c++的网络通信编程技术,让服务器处理多个client

看过上期的都知道,我是搞java的,所以对这些可能理解不是很清楚,各位看完可以尽情发言。

事件循环和非阻塞IO

在服务器端网络编程中,有三种处理并发连接的方法。

它们是:分叉多线程事件循环 。分叉为每个客户端连接创建新进程,以实现并发性。多线程使用线程而不是进程。事件循环使用轮询和非阻塞IO,通常在单个线程上运行。由于进程和线程的开销,大多数现代生产级软件使用事件循环进行网络连接。

我们服务器的事件循环的简化伪代码是:

cpp 复制代码
all_fds = [...]
while True:
    active_fds = poll(all_fds)
for each fd in active_fds:
    do_something_with(fd)
def do_something_with(fd):
    if fd is a listening socket:
        add_new_client(fd)
   elif fd is a client connection:
   while work_not_done(fd):
       do_something_to_client(fd)
  def do_something_to_client(fd):
  if should_read_from(fd):
     data = read_until_EAGAIN(fd)
    process_incoming_data(data)
  while should_write_to(fd):
  write_until_EAGAIN(fd)
if should_close(fd):
  destroy_client(fd)
  1. all_fds 是一个包含所有需要监控的文件描述符的列表。
  2. while True: 是一个无限循环,它会一直运行,直到程序被外部中断。
  3. active_fds = poll(all_fds) 调用poll函数,返回一个包含所有准备好进行I/O操作的文件描述符的列表。
  4. for each fd in active_fds: 遍历这个列表,对每个文件描述符执行相应的操作。
  5. do_something_with(fd) 是一个函数,根据文件描述符的类型执行相应的操作。
  6. def do_something_with(fd): 定义了这个函数。
  7. if fd is a listening socket: 如果文件描述符是一个监听套接字,那么调用 add_new_client(fd) 函数添加新的客户端连接。
  8. elif fd is a client connection: 如果文件描述符是一个客户端连接,那么进入一个循环,直到工作完成为止。在这个循环中,调用 do_something_to_client(fd) 函数处理客户端的数据。
  9. def do_something_to_client(fd): 定义了这个函数。
  10. if should_read_from(fd): 如果应该从文件描述符读取数据,那么调用 read_until_EAGAIN(fd) 函数读取数据,然后调用 process_incoming_data(data) 函数处理数据。
  11. while should_write_to(fd): 如果应该向文件描述符写入数据,那么调用 write_until_EAGAIN(fd) 函数写入数据。
  12. if should_close(fd): 如果应该关闭文件描述符,那么调用 destroy_client(fd) 函数销毁客户端连接。

我们不只是对fd做一些事情(读、写或接受),而是使用轮询操作来告诉我们哪些fd可以立即操作而不阻塞。当我们在fd上执行IO操作时,该操作应该在非阻塞模式下执行。

epoll的使用

在Linux上,除了poll系统调用,还有select和epoll。古老的select系统调用基本上与轮询相同,只是最大fd数被限制为一个小数目,这使得它在现代应用程序中已经过时了。epoll API由3个系统调用组成:epoll_create、epoll_wait和epoll_ctl。epoll API是有状态的,而不是提供一组fd作为系统调用参数,epoll_ctl用于操作由epoll_create创建的fd集,epoll_wait正在对其进行操作。

epoll API执行与poll()类似的任务:监视多个文件描述符,以查看其中任何一个文件是否可以进行I/O操作。epoll API可以用作边缘触发或水平触发接口,并且可以很好地扩展到大量监视的文件描述符。以下提供系统调用来创建和管理epoll实例:

**epoll_create()**创建一个epoll实例并返回一个引用该实例的文件描述符。

然后通过**epoll_ctl()**注册对特定文件描述符的兴趣。当前在epoll上注册的文件描述符集,实例有时称为epoll集。

**epoll_wait()**等待I/O事件,如果当前没有事件可用,则阻塞调用线程。

cpp 复制代码
           #define MAX_EVENTS 10
           struct epoll_event ev, events[MAX_EVENTS];
           int listen_sock, conn_sock, nfds, epollfd;

           /* Set up listening socket, 'listen_sock' (socket(),
              bind(), listen()) */

           epollfd = epoll_create(10);
           if (epollfd == -1) {
               perror("epoll_create");
               exit(EXIT_FAILURE);
           }

           ev.events = EPOLLIN;
           ev.data.fd = listen_sock;
           if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
               perror("epoll_ctl: listen_sock");
               exit(EXIT_FAILURE);
           }
               for (;;) {
               nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
               if (nfds == -1) {
                   perror("epoll_pwait");
                   exit(EXIT_FAILURE);
               }

               for (n = 0; n < nfds; ++n) {
                   if (events[n].data.fd == listen_sock) {
                       conn_sock = accept(listen_sock,
                                       (struct sockaddr *) &local, &addrlen);
                       if (conn_sock == -1) {
                           perror("accept");
                           exit(EXIT_FAILURE);
                       }
                       setnonblocking(conn_sock);
                       ev.events = EPOLLIN | EPOLLET;
                       ev.data.fd = conn_sock;
                       if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,
                                   &ev) == -1) {
                           perror("epoll_ctl: conn_sock");
                           exit(EXIT_FAILURE);
                       }
                   } else {
                       do_use_fd(events[n].data.fd);
                   }
               }
           }

这段代码的主要功能是创建一个监听套接字,并使用epoll机制来处理客户端连接请求。以下是代码的详细解析:

  1. #define MAX_EVENTS 10:定义了一个宏常量MAX_EVENTS,表示最多可以处理的事件数量为10。
  2. struct epoll_event ev, events[MAX_EVENTS];:定义了一个结构体数组events,用于存储epoll事件。每个事件由一个epoll_event结构体表示。
  3. int listen_sock, conn_sock, nfds, epollfd;:定义了四个整型变量,分别表示监听套接字、新连接的套接字、返回的文件描述符数量和epoll实例的文件描述符。
  4. epollfd = epoll_create(10);:创建了一个epoll实例,最大文件描述符数量为10。如果创建失败,将打印错误信息并退出程序。
  5. ev.events = EPOLLIN;:设置事件类型为可读事件(EPOLLIN)。
  6. ev.data.fd = listen_sock;:将监听套接字的文件描述符设置为事件的关联数据。
  7. if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1):将监听套接字添加到epoll实例中,以便接收新的连接请求。如果添加失败,将打印错误信息并退出程序。
  8. for (;;):无限循环,不断处理事件。
  9. nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);:调用epoll_wait函数等待事件发生。参数-1表示等待所有类型的事件。返回值表示实际发生的事件数量。如果发生错误,将打印错误信息并退出程序。
  10. for (n = 0; n < nfds; ++n):遍历所有发生的事件。
  11. if (events[n].data.fd == listen_sock):判断事件是否与监听套接字相关联。如果是,表示有新的连接请求。
  12. conn_sock = accept(listen_sock, (struct sockaddr *) &local, &addrlen);:接受新的连接请求,并将新连接的套接字赋值给conn_sock。如果接受失败,将打印错误信息并退出程序。
  13. setnonblocking(conn_sock);:将新连接的套接字设置为非阻塞模式,以便后续处理。
  14. ev.events = EPOLLIN | EPOLLET;:设置事件类型为可读事件(EPOLLIN),并启用边缘触发(EPOLLET)。
  15. ev.data.fd = conn_sock;:将新连接的套接字的文件描述符设置为事件的关联数据。
  16. if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock, &ev) == -1):将新连接的套接字添加到epoll实例中,以便后续处理。如果添加失败,将打印错误信息并退出程序。
  17. else:如果事件与监听套接字无关,表示是一个已连接的客户端发送的数据。调用do_use_fd函数进行处理。
相关推荐
秋野酱1 小时前
如何在 Spring Boot 中实现自定义属性
java·数据库·spring boot
weisian1511 小时前
Mysql--实战篇--@Transactional失效场景及避免策略(@Transactional实现原理,失效场景,内部调用问题等)
数据库·mysql
AI航海家(Ethan)1 小时前
PostgreSQL数据库的运行机制和架构体系
数据库·postgresql·架构
Bunny02121 小时前
SpringMVC笔记
java·redis·笔记
远方 hi4 小时前
linux如何修改密码,要在CentOS 7系统中修改密码
linux·运维·服务器
Kendra9194 小时前
数据库(MySQL)
数据库·mysql
希忘auto5 小时前
详解Redis的Zset类型及相关命令
redis
时光书签5 小时前
Mongodb副本集群为什么选择3个节点不选择4个节点
数据库·mongodb·nosql
资讯分享周5 小时前
过年远控家里电脑打游戏,哪款远控软件最好用?
运维·服务器·电脑
chaodaibing5 小时前
记录一次k8s起不来的排查过程
运维·服务器·k8s