使用epoll()进行socket编程处理多客户连接的TCP服务器实例

Search for a command to run...

No comments yet. Be the first to comment.
在这个系列中,主要讨论在Linux下的socket编程,有基本的socket编程文章,有讨论raw socket编程的文章,有在应用层编程的文章,也有一些面向数据链路层的编程文章;部分文章涉及IPv6和netlink,大多数文章均附有完整的C语言源代码。
在网络编程中,使用 select() 处理多客户端的连接是非常常用的方法,select() 是一个非常古老的方法,在大量连接下会显得效率不高,而且其对描述符的数值还有一些限制,Linux内核从 2.1.13 版以后提供了 poll() 替代 select(),本文介绍 poll() 在网络编程中的使用方法,并着重介绍 poll() 在编程行与 select() 的区别,旨在帮助熟悉 select() 编程的程序员可以很容易地使用 poll() 编程,本文提供了一个具体的实例,并附有完整的源代码,...
双向链表是一种比单向链表更为灵活的数据结构,与单向链表相比可以有更多的应用场景,本文讨论双向链表的基本概念及实现方法,并着重介绍使用GLib的GList实现单向链表的方法及步骤,本文给出了多个实际范例源代码,旨在帮助学习基于GLib编程的读者较快地掌握GList的使用方法,本文程序在 ubuntu 20.04 下编译测试完成,gcc 版本号 9.4.0;本文适合初学者阅读。 1 双向链表及其实现 在文章《单向链表以及如何使用GLib中的GSList实现单向链表》中,介绍了单向链表以及基于 G...

C 编程框架是开发人员必不可少的工具,编程框架可以为构建强大且性能优异的应用程序提供结构化的基础,本文将对 8 个最佳 C 编程框架和库做出简要的介绍,如果您正在寻找适合初学者的 C 编程框架或旨在进行 C 编程框架比较,相信本文可以给您一定的帮助。 顶级 C 编程框架 – 概述 本文将介绍以下 8 个 C 语言编程框架: 序号框架名称主要特点易于集成下载链接 1GTK全面的小部件集,跨平台支持中等的下载 2Qt跨平台支持,集成开发环境中等的下载 3CMocka轻量级,模...

单向链表是一种基础的数据结构,也是一种简单而灵活的数据结构,本文讨论单向链表的基本概念及实现方法,并着重介绍使用GLib的GSList实现单向链表的方法及步骤,本文给出了多个实际范例源代码,旨在帮助学习基于GLib编程的读者较快地掌握GSList的使用方法,本文程序在 ubuntu 20.04 下编译测试完成,gcc 版本号 9.4.0;本文适合初学者阅读。 1 单向链表及其实现 在文章《使用GLib进行C语言编程的实例》中,简单介绍了 GLib,建议阅读本文前先阅读这篇文章; 单向链表是一...

本文将讨论使用GLib进行编程的基本步骤,GLib是一个跨平台的,用C语言编写的3个底层库(以前是5个)的集合,GLib提供了多种高级的数据结构,如内存块、双向和单向链表、哈希表等,GLib还实现了线程相关的函数、多线程编程以及相关的工具,例如原始变量访问、互斥锁、异步队列等,GLib主要由GNOME开发;本文是使用GLib编程的入门文章,旨在通过实例帮助希望学习GLib编程的读者较快地入门,本文将给出多个使用GLib库编程范例的源代码,本文程序在 ubuntu 20.04 下编译测试完成,gc...

打开电脑连接wifi是一件很平常的事情,但这些事情通常都是操作系统下的wifi管理程序替我们完成的,如何在程序中扫描wifi信号其实资料并不多,前面已经有两篇文章介绍了如何使用ioctl()扫描wifi信号,但其实在Linux下有一个简单的库对这些ioctl()的操作进行了封装,这个库就是libiw,使用libiw可以简化编程,本文介绍了如果使用libiw对wifi信号进行扫描的基本方法,本文将给出完整的源代码,本文程序在 ubuntu 20.04 下编译测试完成,gcc 版本号 9.4.0;尽...

在网络编程中,当需要使用单线程处理多客户端的连接时,常使用select()或者poll()来处理,但是当并发数量非常大时,select()和poll()的性能并不好,epoll()的性能大大好于select()和poll(),在编写大并发的服务器软件时,epoll()应该是首选的方案,本文介绍epoll()在网络编程中的使用方法,本文提供了一个具体的实例,并附有完整的源代码,本文实例在 ubuntu 20.04 下编译测试完成,gcc 版本号 9.4.0。
select() 处理多个 socket 连接:
poll() 处理多个 socket 连接:
poll() 和 select() 的编程方法非常相似,但 epoll 有较大区别;poll() 相似的工作:监视多个文件描述符看它们是否可以进行 I/O 操作;epoll_create1() - 用于建立一个 epoll 实例;epoll_ctl() - 用于向 epoll 实例的 Interest List 中添加要监视的文件描述符,或者修改/删除 Interest List 中的文件描述符;epoll_wait() - 用于监视已经登记的文件描述符集,当有一个或多个被监视的文件描述符可以进行 I/O 操作时返回;epoll_wait() 后,有两种触发方式可以使 epoll_wait() 返回,边沿触发(Edge-Triggered)和电平触发(Level-Triggered),这两个词是从电子电路中引申过来的,熟悉电子电路的或者做嵌入式编程的读者应该对此有些了解;epoll_wait() 会返回文件描述符 rfd,表示在 rfd 上有数据可以读取;epoll_wait();epoll_wait() 就会被触发返回,由于写入了 2kb 数据但只读出了 1kb,所以在第 5 步时,epoll_wait() 会返回 rfd 有数据可读;epoll_wait() 返回,虽然读缓冲区中仍有 1kb 的数据没有被读出,但在第 5 步时 epoll_wait() 是不会返回的;poll(),可以在任何使用 poll() 的地方使用电平触发方式的 epoll,epoll 真正的意义在于其边缘触发方式;epoll_wait() 被触发后必须将读缓冲区的数据全部读出,否则可能会有数据丢失,所以当使用边沿触发方式时,通常需要将文件描述符设置成非阻塞方式,然后循环读取,直至出现 EAGAIN 错误代码为止,如下 ......
int done = 0; // not done
int nbytes = 0; // how many bytes to read
do {
nbytes = recv(fd, buffer, sizeof(buffer), 0);
if (nbytes > 0) {
buffer[nbytes] = '\0';
...
continue;
} else if (rc == 0) {
// the socket discinnected
break;
} else if (errno == EINTR) {
// if errno==EINTR, it means socket is not closed, just because some network errors happened
continue;
} else if (errno == EAGAIN) {
done = 1;
break;
} else {
perror("recv() failed");
break;
}
} while (1);
......
epoll_create1() - 创建一个 epoll 实例
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_create1(int flags);
epoll_ctl() - epoll 文件描述符的控制接口
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
struct epoll_event 的定义如下:
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
下面代码将一个文件描述符 fd 加入到 epoll 实例 epfd 的 Interest List 中,使用边沿触发方式,当可以进行读操作时触发 epoll_wait() 返回:
......
int epfd = epoll_create1(0);
...
struct epoll_event event;
memset(&event, 0 , sizeof(struct epoll_event));
// Set up the structure epoll_event
event.data.fd = fd;
event.events = EPOLLIN | EPOLLET;
// Add a new descriptor to the interest list
if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event) == -1) {
perror("EPOLL_CTL_ADD failed");
}
......
epoll_wait() - 等待 epoll 文件描述符上的 I/O 事件
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epoll_wait() 后,当 epoll 实例中被监视的文件描述符有事件产生或者超时时间 timeout 到,该函数将返回;epoll_create1() 返回的 epoll 实例的文件描述符;events->data.fd 为产生事件的文件描述符,events->events 为实际产生的事件(位掩码);epoll_wait() 在参数 events 中返回的事件不会大于 maxevents;epoll_wait() 等待 timeout 时长后不论是否有事件产生都会返回,将 timeout 设为 -1,epoll_wait() 将一直等待直至有事件产生,将 timeout 设为 0,epoll_sait() 将立即返回,不论是否有事件产生;epoll_wait() 调用成功时,返回一个正整数,表示在参数 events 中有多少个事件;epoll_wait() 因超时返回时,将返回 0;epoll_wait() 调用失败将返回 -1,errno 中为错误代码;epoll_wait() 可以被信号打断,此时,错误代码为 EINTR,通常情况下如果 errno 为 EINTR 时可以重新调用 epoll_wait();epoll 进行 socket 编程的基本步骤:
socket() 建立需要侦听的 socket;setsockopt() 设置 socket 为可重复使用;ioctl() 设置 socket 为非阻塞;bind() 绑定服务器的地址和端口;listen() 开始侦听端口;select()/poll() 编程时是一致的;epoll_create1() 构建一个 epoll 实例 epfd; struct epoll_event ev,将服务器侦听 socket 加入到加入到结构中,并设置 EPOLLIN 事件及边沿触发方式(EPOLLET);epoll_ctl() 的 EPOLL_CTL_ADD 方法将侦听 socket 加入到 epoll 实例 epfd 的 Interest List 中;启动 epoll_wait();
epoll_wait();<0 表示 epoll_wait() 出错,errno 中为错误代码;返回 >0 表示有需要处理的 socket,进行处理;
要处理的 socket 通常又分为两种,一种是正在侦听的 socket,如果有
EPOLLIN事件表示有客户端发出了连接请求,使用accept()接受连接将产生一个新的 socket,这个新的 socket 要按照步骤 7、8 的方法加入到 epoll 实例的 Interest List 中,以便在 epoll 中可以被监视,因为我们使用的边沿触发方式,所以还要记得使用ioctl()将这个新的 socket 设置成非阻塞;另一类 socket 就是已经和服务器建立连接的一个或多个客户端的 socket,这类 socket 有 EPOLLIN 事件产生可能是有数据发送回来,也可能是因为连接中断,在调用
recv()从 socket 中接收数据时,如果返回值>0表示确实有数据发送回来,要做出相应处理,如果返回值为 0 则表示这个连接已经中断,此时只需将该 socket 关闭即可,理论上说,当一个 socket 被关闭后,epoll 会自动地将该 socket 从 Interest List 中删除,所以通常我们不需要显式地使用 epoll_ctl() 的 EPOLL_CTL_DEL 方法从 epoll 实例的 Interest List 中删除这个 socket;
回到步骤 9,再次启动 epoll_wait();
gcc -Wall -g epoll-server.c -o epoll-server./epoll-serverepoll_wait() 监视 socket;epoll_wait() 监视 socket;ctrl + c 产生,服务进程在收到这个信号后将退出进程;运行截图:

欢迎访问我的博客:https://whowin.cn
email: hengch@163.com
