linux網絡編程之IO複用-epoll用法

上一篇文章中,我們講解了select的用法和弊端.

https://blog.csdn.net/zhangkai19890929/article/details/95165596

select的最大弊端就是:
就是每次都要遍歷整個數據,來知道這個數組裏到底哪些sockfd可以讀寫,這樣的效率會導致我們處理高併發的瓶頸C10K(client 10k,也就是最大處理併發數量爲10000)

所以linux內核後面又提出了改進的網絡IO複用模型:epoll.

假設有10000路併發,某一個時刻只有1000個用戶發送了數據

相對於select io我們需要遍歷這10000路併發,並判斷其中哪些sock是可讀的,但是改進後的epoll,我們可以直接知道,是這1000路來了數據,我們只管遍歷這1000路就可以了.

epll優勢:

1.使用鏈表實現,理論上同時可以的高併發數量是無數個.
2.只需要關心活躍的鏈接,不需要關係所有的鏈接.

epoll api介紹:

epoll_create
epoll_ctl

int efd = epoll_create
創建鏈表,內核在底層創建鏈表,efd可以理解爲鏈表的頭.

有了鏈表那麼我們就需要往鏈接裏添加數據,鏈表裏的元素是:epoll_event,讓我們看下結構體 :

struct epoll_event
{
  uint32_t events;	/* Epoll events */
  epoll_data_t data;	/* User data variable */
} __EPOLL_PACKED;

對應的epoll_data_t結構體爲:

typedef union epoll_data
{
  void *ptr;
  int fd;
  uint32_t u32;
  uint64_t u64;
} epoll_data_t;

OK,那麼我們來看看如何向鏈表中添加和刪除事件:

向鏈表裏添加:
epoll_ctl(efd, EPOLL_CTL_ADD, cfd, &event_usercoming);

向鏈表裏刪除:
epoll_ctl(efd, EPOLL_CTL_DEL, event.data.fd, &event);

下面看我們寫的demo,進行完整的測試工程分析:

epoll_server.c
編譯命令: gcc -o epoll_server epoll_server.c

#include <sys/epoll.h>
#include <stdio.h>
#include <sys/types.h>          
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <pthread.h>
#include <time.h>

#define SERVER_PORT			9995
#define MAX_SOCKETS 		256
#define MAX_EPOLL_EVENTS	1000

int main(int argc,char *argv[])
{

	int sfd = socket(AF_INET, SOCK_STREAM, 0);
	int ret = 0;

	struct sockaddr_in addr;
	addr.sin_family = AF_INET;
	addr.sin_addr.s_addr = INADDR_ANY;
	addr.sin_port = htons(SERVER_PORT);

	ret = bind(sfd, (struct sockaddr *)&addr, sizeof(struct sockaddr_in));
	if (ret < 0)
	{
		fprintf(stderr, "Error bind(). \n");
		goto close;
	}

	ret = listen(sfd, MAX_SOCKETS);
	if (ret < 0)
	{
		fprintf(stderr, "Error listen(). \n");
		goto close;
	}

	int efd  = epoll_create(1); //創建鏈表,efd表示是鏈表的頭
	if (efd < 0)
	{
		fprintf(stderr, "epoll create fail . \n");
		goto close;
	}

	//添加事件
	struct epoll_event newEvent;
	newEvent.events = EPOLLIN;//代表有用戶進來了!
	newEvent.data.fd = sfd;

	epoll_ctl(efd, EPOLL_CTL_ADD, sfd, &newEvent);

	struct epoll_event *events;
	int cnt_event;
	struct timespec waittime;
	int epoll_waittime = 0;

	uint8_t recvbuf[1024];

	while (1)
	{
		clock_gettime(CLOCK_MONOTONIC, &waittime);
		epoll_waittime = (waittime.tv_sec + 10) * 1000;//忽略了納秒,這種寫法不是特別的精準.

		cnt_event = epoll_wait(efd, events, MAX_EPOLL_EVENTS, epoll_waittime);
		if (cnt_event == -1)
		{
			fprintf(stderr, "errno:%d \n", errno);
			break;
		}
		fprintf(stderr,"epoll wait cnt_vent:%d \n" , cnt_event);
		
		//遍歷事件
		for (int i = 0 ; i < cnt_event ; i++)
		{
			struct epoll_event event = events[i];
			
			if (event.events & EPOLLIN) //代表可讀
			{
				if (event.data.fd == sfd)//new user coming
				{
					fprintf(stderr, "new user coming .... \n");
					struct sockaddr_in addr;
					int len = 0;//這裏必須設置初始值,不然會返回errno = 22的錯誤提示.
					
					int cfd = accept(sfd, (struct sockaddr *)&addr , (socklen_t*)&len);//這裏爲何會返回-1呢? errno 22
					if (cfd<0)
					{
						fprintf(stderr, "accept fail , errno:%d \n" , errno );
						continue;
					}

					struct epoll_event event_usercoming;
					event_usercoming.events = EPOLLIN ;
					event_usercoming.data.fd = cfd;

					epoll_ctl(efd, EPOLL_CTL_ADD, cfd, &event_usercoming);

					//怎麼通知內核,這個事件我已經處理好了???



				}
				else
				{
					if (read(event.data.fd, recvbuf, sizeof(recvbuf)) <= 0)
					{
						epoll_ctl(efd, EPOLL_CTL_DEL, event.data.fd, &event);
					}
					else
					{
						fprintf(stderr, "receive content:%s \n" , recvbuf);
					}
					
				}
			}
			else if (event.events & EPOLLOUT)//代表可寫 --- 這裏有一個疑問,理論上來說,用戶終端是永遠可寫的,所以這個。。。。
			{
				//理論上寫都是隨時的,但是一般要根據協議來決定什麼時候寫,給對方寫client需要的數據.
				fprintf(stderr, "you can send data to sock fd:%d \n" , event.data.fd);
			}

		}

	}


close:
	close(efd);

	return 0;
}

現在要重點介紹epoll裏的兩種事件模型:LT/ET.

LT: – epoll 默認的處理方式.
ev.events = EPOLLIN;

ET:
ev.events = EPOLLIN | EPOLLET;

LT/ET的主要區別在於事件通知的方式不同:

LT的意思就是,有事件觸發,我通知你了,你就必須處理,如果你不處理,我下次來還要通知你.
ET的意思是,有事件觸發了,我通知你了,你處理不處理那都是你的事情,下一次我是不會再通知你了.

怎麼來測試,很簡單:

針對上面的例子,如果有客戶端連接了:

我們在LT模式下,不去執行accept事件,那麼epoll_wait每次都會觸發.
在ET模式下,我們不執行accept事件,那麼epoll_wait就只會觸發一次.

實測下:
實測發現,如果代碼中沒寫accept模塊,epoll_wait會直接返回-1,erro 返回 14,操蛋呀。。。。。

有可能是ubuntu在編譯環節就針對這種情況做了優化,目前本人使用的ubuntu系統爲18.04.

針對read操作呢?

1.客戶端發送10字節
2.服務器每次讀取2個字節

LT:
在這裏插入圖片描述

epoll_wait在LT模式下持續通知了四次,直到客戶端把這客戶端發送過來的數據讀完!

ET:
在這裏插入圖片描述
實打實的真的就只獲取了一次.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章