文章目录
在linux的网络编程中,很长的时间都在使用select来做事件触发。在linux新的内核中,有了一种替换它的机制,就是epoll。
相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。因为在内核中的select实现中,它是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。并且,在linux/posix_types.h头文件有这样的声明:
#define __FD_SETSIZE 1024
表示select最多同时监听1024个fd,当然,可以通过修改头文件再重编译内核来扩大这个数目,但这似乎并不治本。
详细可以看下面博客,介绍了多路复用中各种实现的优缺点:
linux编程之select、poll、epoll区别
1. Linux中提供的epoll相关函数如下:
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
int epoll_create(int size);
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大,从Linux2.6.8版以来,size参数被忽略不用
。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
系统调用epoll_ctl()
能够修改由文件描述符epfd
所代表的epoll
实例中的兴趣列表。若成功返回0,若出错返回-1。
第一个参数epfd
是epoll_create()
的返回值;
第二个参数op
表示动作,用三个宏来表示:
- EPOLL_CTL_ADD:注册新的fd到epfd中;
- EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
- EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数fd
指明了要修改兴趣列表中的哪一个文件描述符的设定。该参数可以是代表管道、FIFO、套接字、POSIX消息队列、inotify实例、终端、设备,甚至是另一个epoll实例的文件描述符。但是,这里fd不能作为普通文件或目录的文件描述符;
第四个参数event
是指向结构体epoll_event
的指针,结构体的定义如下:
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
events可以是以下几个宏的集合:
EPOLLIN //表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT //表示对应的文件描述符可以写;
EPOLLPRI //表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR //表示对应的文件描述符发生错误;
EPOLLHUP //表示对应的文件描述符被挂断;
EPOLLET //将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT //只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
系统调用epoll_wait()返回epoll实例中处于就绪态的文件描述符信息,单个epoll_wait()调用能够返回多个就绪态文件描述符的信息。调用成功后epoll_wait()返回数组events中的元素个数,如果在timeout超时间隔内没有任何文件描述符处于就绪态的话就返回0,出错时返回-1并在errno中设定错误码以表示错误原因。
第一个参数epfd
是epoll_create()
的返回值;
第二个参数events
所指向的结构体数组中返回的是有关就绪态文件描述符的信息,数组events的空间由调用者负责申请;
第三个参数maxevents
指定所evlist
数组里包含的元素个数;
第四个参数timeout
用来确定epoll_wait()
的阻塞行为,有如下几种:
- 如果timeout等于-1,调用将一直阻塞,直到兴趣列表中的文件描述符上有事件产生或者直到捕获到一个信号为止。
- 如果timeout等于0,执行一次非阻塞式地检查,看兴趣列表中的描述符上产生了哪个事件。
- 如果timeout大于0,调用将阻塞至多timeout毫秒,直到文件描述符上有事件发生,或者直到捕获到一个信号为止。
epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
select、poll、epoll的区别可参考: linux编程之select、poll、epoll区别
2. 服务器要求和功能分析都类似,不再赘述,可参考下面链接:
3. 代码模块分析
与除server_poll.c
(主函数) 不同之外,其余代码模块都与select服务器相同,直接调用即可:
select服务器代码:select代码模块的复用
create_database 创建数据库模块
create_database.h
创建数据库的头文件
#ifndef CREATE_DATABASE_H
#define CREATE_DATABASE_H
int create_database(void);
#endif
create_database.c
创建数据的c文件,为了防止多次创建数据库,每次启动服务器时,直接创建数据库,多个客户端连接时,直接打开数据库,然后插入数据即可。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <ctype.h>
#include <time.h>
#include <libgen.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/resource.h>
#include <fcntl.h>
#include <sqlite3.h>
#include "create_database.h"
#define database_name "temper.db"
int create_database(void)
{
sqlite3 *db=NULL;
char *zErrMsg = NULL;
int rc;
int len;
//char sql ="create table if not exists temperature(ID char(10), datetime char(50), temperature char(10))";
char *sql ="create table temperature(ID char(10), datetime char(50), temperature char(10))"; //描述创建数据库中表的信息
/* Open database 若没有则创建*/
len = sqlite3_open(database_name, &db);
if(len != SQLITE_OK)
{
sqlite3_close(db);
fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
return -1; //子线程不能调用exit(0)
}
printf("Opened database successfully\n");
/* Execute SQL statement */
rc = sqlite3_exec(db, sql, 0, 0, &zErrMsg);
if( rc != SQLITE_OK )
{
sqlite3_close(db);
fprintf(stderr, "Create table error: %s\n", zErrMsg);
return 0;
}
printf("Table created successfully\n");
sqlite3_close(db);
return 1;
}
socket_server_init服务器端初始化
socket_server_init.h
初始化的头文件
#ifndef SOCKET_SERVER_INIT_H
#define SOCKET_SERVER_INIT_H
int socket_server_init(char *listenip, int listen_port);
#endif
socket_server_init.c
文件主要是实现创建服务器的socket,bind,listen,每次创建服务器端都必须调用这些函数。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <ctype.h>
#include <time.h>
#include <libgen.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/resource.h>
#include <fcntl.h>
#include <sqlite3.h>
#include "server_socket_init.h"
int server_socket_init(char *listenip, int listen_port)
{
int listenfd;
int on = 1;
int rv = 0;
struct sockaddr_in serv_addr;
if((listenfd=socket(AF_INET, SOCK_STREAM, 0)) < 0 )
{
printf("create socket failure: %s\n", strerror(errno));
return -1;
}
//Set socket port reuseable, fix 'Address already in use' bug when socket server restart
//setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(listen_port);
if( !listenip)
{
/* 监听所有的客户端的IP地址 */
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
}
else
{
if(inet_pton(AF_INET,listenip, &serv_addr.sin_addr) <= 0)
{
printf("inet_pton() set listen IP failure: %s\n", strerror(errno));
rv = -2;
goto Cleanup;
}
}
//Set socket port reuseable, fix 'Address already in use' bug when socket server restart
if((setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (void*)&on, sizeof(on))) < 0 )
{
printf("setsockopt failure: %s\n", strerror(errno));
return -2;
}
if(bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
{
printf("bind socket[%d] failure: %s\n", listenfd, strerror(errno));
rv = -3;
goto Cleanup;
}
if(listen(listenfd, 64) < 0)
{
printf("listen() socket[%d] failure: %s\n", listenfd, strerror(errno));
rv = -4;
goto Cleanup;
}
Cleanup:
if(rv < 0)
{
close(listenfd);
}
else
rv = listenfd;
return rv;
}
server_epoll主函数
server_epoll.c
文件创建服务器端温度采集实时上报,因为每次运行服务器程序都会创建数据库,插入表。如果已存在数据库,那么在插入表时会失败,导致程序退出,所有在主函数中一定要判断数据是否存在,存在则不用插入表,在while循环中直接插入数据即可,下面167行代码
有注释。
/*********************************************************************************
* Copyright: (C) 2019 Tang Zhiqiang<t_zhiqiang@163.com>
* All rights reserved.
*
* Filename: server_epoll.c
* Description: This file is socket server RPI temperature
*
* Version: 1.0.0(11/04/2019)
* Author: Tang Zhiqiang <t_zhiqiang@163.com>
* ChangeLog: 1, Release initial version on "11/04/2019 08:15:46 PM"
*
********************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <ctype.h>
#include <time.h>
#include <pthread.h>
#include <getopt.h>
#include <libgen.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/resource.h>
#include <signal.h>
#include <syslog.h>
#include <fcntl.h>
#include <sys/time.h>
#include <sqlite3.h>
#include <sys/epoll.h>
#include "server_socket_init.h"
#include "create_database.h"
#define MAX_EVENTS 512
#define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0])) //计算结构体/数组元素个数
#define database_name "temper.db"
//void set_socket_rlimit(void);
int create_database(void);
int server_socket_init(char *listenip, int listen_port);
int g_sigstop = 0;
void signal_stop(int signum)
{
if(SIGTERM == signum)
{
printf("SIGTERM signal detected\n");
g_sigstop = 1;
}
}
static inline void print_usage(char *progname)
{
printf("Usage: %s [OPTION]...\n", progname);
printf(" %s is a socket server program, which used to verify client and echo back string from it\n",progname);
printf("\nMandatory arguments to long options are mandatory for short options too:\n");
printf(" -b[daemon ] set program running on background\n");
printf(" -p[port ] Socket server port address\n");
printf(" -h[help ] Display this help information\n");
printf("\nExample: %s -b -p 8900\n", progname);
return ;
}
int main(int argc, char *argv[])
{
int listenfd,connfd;
int serv_port = 0;
int daemon_run = 0;
char *progname = NULL;
int opt;
int rv;
int log_fd;
int i;
//int found;
char buf[1024];
char sql1[28];
int epollfd;
struct epoll_event event;
struct epoll_event event_array[MAX_EVENTS];
int events;
char *zErrMsg = NULL;
int ret;
char delim[]="/";
char id[20];
char data_time[50];
char temper[10];
char *ptr=NULL;
sqlite3 *db;
int len;
//char *p=NULL;
struct option long_options[]=
{
{"daemon", no_argument, NULL, 'd'},
{"port", required_argument, NULL, 'p'},
{"help", no_argument, NULL, 'h'},
{NULL, 0, NULL, 0}
};
progname = basename(argv[0]);
//Parser the command line parameters
while((opt = getopt_long(argc, argv, "dp:h", long_options, NULL)) != -1)
{
switch (opt)
{
case 'd':
daemon_run = 1;
break;
case 'p':
serv_port = atoi(optarg);
break;
case 'h':
print_usage(progname);
break;
default:
break;
}
}
if(!serv_port)
{
print_usage(progname);
return -1;
}
signal(SIGTERM, signal_stop); //15号信号(SIGTERM)可被捕捉
if(daemon_run)
{
printf("program %s running at the backgroud now\n", argv[0]);
//建立日志系统
log_fd=open("receive_temper.log", O_CREAT|O_RDWR, 0666);
if(log_fd < 0)
{
printf("Open the logfile failure: %s\n", strerror(errno));
return 0;
}
//标准输出、标准出错重定向
dup2(log_fd, STDOUT_FILENO);
dup2(log_fd, STDERR_FILENO);
//程序后台运行
if(daemon(1, 1) <0 )
{
printf("Deamon failure: %s", strerror(errno));
return 0;
}
}
if((listenfd=server_socket_init(NULL, serv_port)) < 0)
{
printf("ERROR: %s server listen on port %d failure\n", argv[0], serv_port);
return -2;
}
printf("server socket[%d] start to listen on port %d\n", listenfd, serv_port);
printf("Start create database\n");
if (access(database_name, W_OK) < 0)
{
if(rv=create_database() < 0)
{
printf("Create database failure: %s\n", strerror(errno));
return -1;
}
}
printf("Create database OK\n");
/* 将加入 listenfd 到兴趣数组中 */
if((epollfd=epoll_create(MAX_EVENTS)) < 0) //这里缺少括号,优先级出问题了 已改
{
printf("%s create epoll farilure: %s\n", argv[0], strerror(errno));
return -3;
}
printf("programe create epoll[%d] OK.\n", epollfd);
/* 必须将文件描述符赋值给event.data.fd */
event.events = EPOLLIN;
event.data.fd = listenfd;
if(epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &event) < 0 )
{
printf("epoll add listen socket[%d] failure: %s\n", listenfd, strerror(errno));
return -4;
}
//printf("programe will start running...\n");
while(!g_sigstop)
{
//program will blocked here
printf("programe will blocked here...\n");
events=epoll_wait(epollfd, event_array, MAX_EVENTS, -1); //成功则返回数据元素个数
if(events < 0)
{
printf("epoll failure: %s\n", strerror(errno));
break;
}
else if(events==0)
{
printf("epoll timeout\n");
continue;
}
//events>0 is the active events count
for(i = 0; i < events; i++)
{
//for循环的events与结构体成员中的events不同
//按位与求的是结果,逻辑与求的是真或假
if((event_array[i].events&EPOLLERR) || (event_array[i].events&EPOLLHUP))
{
printf("epoll_wait get error on fd[%d]: %s\n",event_array[i].data.fd, strerror(errno));
epoll_ctl(epollfd, EPOLL_CTL_DEL, event_array[i].data.fd, NULL);
close(event_array[i].data.fd);
}
//listen socket get event means new client start connect now
if(event_array[i].data.fd == listenfd) //已将listenfd加入到epoll感兴趣事件集合
{
if((connfd=accept(listenfd, (struct sockaddr *)NULL, NULL)) < 0) //不保存客户端信息(结构体指针为NULL)
{
printf("accept new client failure: %s\n", strerror(errno));
continue;
}
event.data.fd = connfd;
event.events = EPOLLIN;
if(epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &event) < 0 )
{
printf("epoll add client socket failure: %s\n", strerror(errno));
close(event_array[i].data.fd);
continue;
}
printf("accept new client[%d] OK\n", connfd);
}
else //data arriver from alrady connected client
{
memset(buf, 0, sizeof(buf));
if((rv=read(event_array[i].data.fd, buf, sizeof(buf))) <= 0 ) //同上
{
printf("socket[%d] read data failure or disconnect and will be remove.\n", event_array[i].data.fd);
epoll_ctl(epollfd, EPOLL_CTL_DEL, event_array[i].data.fd, NULL);
close(event_array[i].data.fd);
continue;
}
else
{
printf("socket[%d] read data: %s\n", event_array[i].data.fd, buf);
/* Open database */
len = sqlite3_open(database_name, &db);
if(len != SQLITE_OK)
{
sqlite3_close(db);
fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
epoll_ctl(epollfd, EPOLL_CTL_DEL, event_array[i].data.fd, NULL);
close(event_array[i].data.fd); //结束当前链接客户端
continue;
}
printf("Opened database successfully\n");
ptr = strtok(buf, delim);
while(ptr != NULL)
{
strncpy(id, ptr, sizeof(id));
ptr=strtok(NULL, delim);
strncpy(data_time, ptr, sizeof(data_time));
ptr=strtok(NULL, delim);
strncpy(temper, ptr, sizeof(temper));
ptr=strtok(NULL, delim);
}
memset(sql1, 0, sizeof(sql1));
snprintf(sql1, 128, "insert into temperature values('%s', '%s', '%s');", id, data_time, temper);
//保证了数组sql1的内容为字符串
sql1[127] = '\0';
printf("%s\n", sql1);
//调用sqlite3_exec();将数据存储至temperature表中
ret = sqlite3_exec(db, sql1, 0 , 0, &zErrMsg);
if (ret != SQLITE_OK) //判断返回值,如果不等于SQLITE_OK,即插入记录失败
{
sqlite3_close(db);
printf("insert data failure: %s!\n", zErrMsg);
epoll_ctl(epollfd, EPOLL_CTL_DEL, event_array[i].data.fd, NULL);
close(event_array[i].data.fd);
continue;
}
printf("insert data successfully!\n");
}//read data from cilent fd
} //for(i=0; i<ARRAY_SIZE(fds_array); i++)
}//data arriver from alrady connected client
}//while(!g_sigstop)
close(listenfd);
sqlite3_close(db); //关闭数据库
return 0;
}
4. 服务器运行结果
5. 客户端代码相同,方便查看直接贴出运行结果
详细的客户端代码如下连接: