《TCP/IP网络编程》第17章 优于select的epoll

《TCP/IP网络编程》第17章 优于select的epoll

  • epoll
    • select速度慢的原因
    • select优点
    • epoll相关函数和结构体
    • epoll回声服务器端
        • 17.echo_epollserver.c
  • 条件触发和边缘触发
    • 条件触发的事件特性
        • 17.echo_EPLTserver.c
    • 边缘触发服务器必知两点
    • 边缘触发服务器
        • 17.echo_EPETserver.c
    • 条件触发与边缘触发优劣

epoll

select速度慢的原因

  • select函数遍历所有文件描述符
  • 每次都需要向select函数传递监视对象信息

每次循环遍历所有监视对象,找出发生变化的文件描述符。

每次调用select函数时都需要向操作系统传递监视对象信息,实际上select函数是监视套接字变化的函数,而套接字由操作系统管理。

通过“仅向操作系统传递一次监视对象,监视范围或内容发生变化时只通知发生变化的事项。”的方式可以弥补select函数的缺点。

需要操作系统的支持,Linux是epoll,Windows是IOCP。

select优点

  • 服务器端接入者少
  • 程序具有兼容性(多数操作系统都支持select函数)

epoll相关函数和结构体

epoll从Linux2.5.44内核开始引入。

cat /proc/sys/kernel/osrelease

epoll函数优点:

  1. 无需循环以监视所有文件描述符的状态变化
  2. 调用对应select函数的epoll_wait函数时无需每次传递监视对象信息
  • epoll_event
    epoll_event结构体将发生变化(事件)的文件描述符单独集中在一起。
struct epoll_event {
	__uint32_t events;
	epoll_data_t data;
}
typedef union epoll_data {
	void *ptr;
	int fd;
	__uint32_t u32;
	__uint64_t u64;
}
  • epoll_create
    epoll方式下,操作系统负责保存监视对象文件描述符,因此需要使用epoll_create向操作系统请求创建保存epoll文件描述符的空间。
#include 

//size是向操作系统提供建议值
//成功返回epoll例程文件描述符,失败-1
//Linux2.6.8后size无效,系统动态调整epoll例程大小
int epoll_create(int size);
  • epoll_ctl
    通过epoll_ctl请求操作系统添加和删除监视对象文件描述符
#include 

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epoll(A, EPOLL_CTL_ADD, B, C);
epoll例程A注册文件描述符B,监视C中事件。

epoll(A, EPOLL_CTL_ADD, B, NULL);
epoll例程A删除文件描述符B。

  1. epfd,epoll例程文件描述符。

  2. op用于指定监视对象的添加、删除、更改等操作。
    EPOLL_CTL_ADD:注册文件描述符
    EPOLL_CTL_DEL:删除文件描述符(事件为NULL)
    EPOLL_CTL_MOD:更改文件描述符关注事件

  3. event,关注的事件

struct epoll_event event;

event.events=EPOLLIN; //位或运算,多个事件
event.data.fd=sockfd;
epoll(epfd, EPOLL_CTL_ADD, sockfd, &event);

EPOLLIN:需要读取数据
EPOLLOUT:输出缓冲为空,可以立即发送数据
EPOLLPRI:收到OOB数据
EPOLLRDHUP:断开连接或半关闭,边缘触发有用
EPOLLERR:发生错误
EPOLLET:边缘触发得到事件通知
EPOLLONESHOT:发生一次事件后,相应文件描述符不在收到事件通知,需要EPOLL_CTL_MOD再次设置事件

  • epoll_wait
    类似select,等待文件描述符发生变化
#include 

//成功返回发生事件的文件描述符数,失败-1
//timeout,1/1000秒,-1阻塞等待
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
int event_cnt;
struct epoll_event *events;

events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE); //EPOLL_SIZE宏常量
event_cnt=epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);

epoll回声服务器端

17.echo_epollserver.c
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define BUF_SIZE 100
#define EPOLL_SIZE 50

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

int main(int argc, char *argv[])
{
	if (argc != 2)
	{
		printf("Usage : %s \n", argv[0]);
		exit(1);
	}

	int serv_sock = socket(PF_INET, SOCK_STREAM, 0);
	if (serv_sock == -1)
		error_handling("socket() error!");

	socklen_t addr_size = sizeof(struct sockaddr_in);

	struct sockaddr_in serv_addr;
	memset(&serv_addr, 0, serv_addr_size);
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	serv_addr.sin_port = htons(atoi(argv[1]));

	if (bind(serv_sock, (struct sockaddr *)&serv_addr, serv_addr_size) == -1)
		error_handling("bind() error!");

	if (listen(serv_sock, 5) == -1)
		error_handling("listen() error!");

	int epfd = epoll_create(EPOLL_SIZE);
	struct epoll_event *ep_events = (struct epoll_event *)malloc(sizeof(struct epoll_event) * EPOLL_SIZE);

	struct epoll_event event;
	event.events = EPOLLIN; // 读取事件
	eventdata.fd = serv_sock;
	epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);

	while (1)
	{
		int event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
		if (event_cnt == -1)
		{
			puts("epoll_wait() error!");
			break;
		}

		for (int i = 0; i < event_cnt; i++)
		{
			if (ep_events[i].data.fd == serv_sock)
			{
				struct sockaddr_in clnt_addr;
				int clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &addr_size);
				event.events = EPOLLIN; // 读取事件
				eventdata.fd = clnt_sock;
				epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
				printf("connected client: %d\n", clnt_sock);
			}
			else
			{
				char buf[BUF_SIZE];
				int str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
				if (str_len == 0)
				{
					epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
					close(ep_events[i].data.fd);
					printf("closed client: %d\n", ep_events[i].data.fd);
				}
				else
				{
					write(ep_events[i].data.fd, buf, str_len);
				}
			}
		}
	}

	// free(ep_events);
	close(epfd);
	close(serv_sock);

	return 0;
}

// gcc 17.echo_epollserver.c -o 17.echo_epollserver && ./17.echo_epollserver 9190

条件触发和边缘触发

条件触发(Level Trigger)和边缘触发(Edge Trigger)的区别在于发生事件的时间点。

条件触发特性,条件触发方式中,只要输入缓冲有数据就会一直通知该事件,多次注册事件。

边缘触发中输入缓冲收到数据时仅注册1次该事件(触发后失效)。

条件触发,满足条件就触发(比如输入缓冲有数据)。
边缘触发,状态变化才触发(比如无缓冲状态变为有缓冲状态)。

条件触发的事件特性

epoll默认以条件触发方式工作。

17.echo_EPLTserver.c
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define PORT 9999
#define BUF_SIZE 4
#define EPOLL_SIZE 50

void error_handling(const char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

int main(int argc, char *argv[])
{
    int serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if (serv_sock == -1)
        error_handling("socket() error!");

    int opt = 1;
    if (setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, (const void *)&opt, sizeof(opt)) == -1)
        error_handling("setsockopt() error!");

    socklen_t addr_size = sizeof(struct sockaddr_in);

    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, addr_size);
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(PORT);

    if (bind(serv_sock, (struct sockaddr *)&serv_addr, addr_size) == -1)
        error_handling("bind() error!");

    if (listen(serv_sock, 5) == -1)
        error_handling("listen() error!");

    struct epoll_event *ep_events = (struct epoll_event *)malloc(sizeof(struct epoll_event) * EPOLL_SIZE);
    int epfd = epoll_create(EPOLL_SIZE);

    struct epoll_event event;
    event.events = EPOLLIN; // 读取事件
    event.data.fd = serv_sock;

    epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);

    while (1)
    {
        int event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
        if (event_cnt == -1)
        {
            puts("epoll_wait() error!");
            break;
        }

        puts("return epoll_wait");

        for (int i = 0; i < event_cnt; i++)
        {
            if (ep_events[i].data.fd == serv_sock)
            {
                struct sockaddr_in clnt_addr;
                int clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &addr_size);
                event.events = EPOLLIN; // 读取事件
                // event.events = EPOLLIN|EPOLLET;
                event.data.fd = clnt_sock;
                epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
                printf("connected client: %d\n", clnt_sock);
            }
            else
            {
                char buf[BUF_SIZE];
                int str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
                if (str_len == 0)
                {
                    epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
                    close(ep_events[i].data.fd);
                    printf("closed client: %d\n", ep_events[i].data.fd);
                }
                else
                {
                    write(ep_events[i].data.fd, buf, str_len);
                }
            }
        }
    }

    close(serv_sock);

    close(epfd);
    free(ep_events);

    return 0;
}

// gcc 17.echo_EPLTserver.c -o 17.echo_EPLTserver && ./17.echo_EPLTserver

多次输出return epoll_wait


clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &addr_size);
后面
event.events=EPOLLIN;
修改为
event.events=EPOLLIN|EPOLLET;
仅输出一次return epoll_wait

select以条件触发方式工作。

边缘触发服务器必知两点

  • errno变量验证错误原因
  • 套接字特性设为非阻塞(Non-blocking)I/O

read函数发现缓冲中无数据可读时返回-1,同时在errno中保存EAGAIN常量。

#include 
//成功返回cmd参数相关值,失败-1
int fcntl(int filedes, int cmd, ...);
//获取之前设置的属性信息
int flag = fcntl(fd, F_GETFL, 0);//获取文件描述符属性

//添加非阻塞O_NONBLOCK标志
fcntl(fd, F_SETFL, flag|O_NONBLOCK);//设置文件描述符属性

边缘触发服务器

边缘触发方式中,仅注册一次事件。
一旦发生输入事件,就应该读取输入缓冲中全部数据。

read返回-1,同时在errno值为EAGAIN,说明无数据可读。

边缘触发方式中,阻塞方式工作的read&write函数可能引起服务器端的长时间停顿,边缘触发方式中一定要采用非阻塞read&write函数。

17.echo_EPETserver.c
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define PORT 9999
#define BUF_SIZE 4
#define EPOLL_SIZE 50

void error_handling(const char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

// 非阻塞
void setnonblockingmode(int fd)
{
    int flag = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flag | O_NONBLOCK);
}

int main(int argc, char *argv[])
{
    int serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if (serv_sock == -1)
        error_handling("socket() error!");

    int opt = 1;
    if (setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, (const void *)&opt, sizeof(opt)) == -1)
        error_handling("setsockopt() error!");

    socklen_t addr_size = sizeof(struct sockaddr_in);

    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, addr_size);
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(PORT);

    if (bind(serv_sock, (struct sockaddr *)&serv_addr, addr_size) == -1)
        error_handling("bind() error!");

    if (listen(serv_sock, 5) == -1)
        error_handling("listen() error!");

    struct epoll_event *ep_events = (struct epoll_event *)malloc(sizeof(struct epoll_event) * EPOLL_SIZE);
    int epfd = epoll_create(EPOLL_SIZE);

    setnonblockingmode(serv_sock);
    struct epoll_event event;
    event.events = EPOLLIN; // 读取事件
    event.data.fd = serv_sock;

    epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);

    while (1)
    {
        int event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
        if (event_cnt == -1)
        {
            puts("epoll_wait() error!");
            break;
        }

        puts("return epoll_wait");

        for (int i = 0; i < event_cnt; i++)
        {
            if (ep_events[i].data.fd == serv_sock)
            {
                struct sockaddr_in clnt_addr;
                int clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &addr_size);
                setnonblockingmode(clnt_sock);
                event.events = EPOLLIN | EPOLLET; // 读取事件且边缘触发
                event.data.fd = clnt_sock;
                epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
                printf("connected client: %d\n", clnt_sock);
            }
            else
            {
                while (1)
                {

                    char buf[BUF_SIZE];
                    int str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
                    if (str_len == 0)
                    {
                        epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
                        close(ep_events[i].data.fd);
                        printf("closed client: %d\n", ep_events[i].data.fd);
                        break;
                    }
                    else if (str_len < 0)
                    {
                        if (errno == EAGAIN) // 非阻塞模式下,当没有数据可读时,read()返回-1,errno=EAGAIN
                            /*
                            {
                                // 再次注册边缘触发的读取事件,
                                event.events = EPOLLIN | EPOLLET; // 读取事件且边缘触发
                                event.data.fd = ep_events[i].data.fd;
                                epoll_ctl(epfd, EPOLL_CTL_ADD, ep_events[i].data.fd, &event);
                            }
                            else // 读取出错
                            {
                                epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
                                close(ep_events[i].data.fd);
                                printf("closed client: %d\n", ep_events[i].data.fd);
                            }
                            */
                            break;
                    }
                    else
                    {
                        write(ep_events[i].data.fd, buf, str_len);
                    }
                }
            }
        }
    }

    close(serv_sock);

    close(epfd);
    free(ep_events);

    return 0;
}

// gcc 17.echo_EPETserver.c -o 17.echo_EPETserver && ./17.echo_EPETserver

条件触发与边缘触发优劣

边缘触发相对于条件触发的优点:可以分离接收数据和处理数据的时间点。

从实现模型的角度看,边缘触发更有可能带来高性能,但不能简单地认为只要使用边缘触发就一定能提高速度。

你可能感兴趣的:(C/C++,整理,tcp/ip,网络,tcp/ip)