網絡編程——C++實現socket通信(TCP)高併發之epoll模式

相關鏈接:TCP連接與釋放網絡編程——C++實現socket通信(TCP)

相關函數:

服務端:
socket()
bind()
listen()
epoll_create()  高併發poll模式
epoll_ctl()
epoll_wait()
accept()
read()recv()write()send()close()

客戶端:
socket()
connect()
write()send()read()recv()close()

着重說明下epoll函數用法。

epoll是增強版的poll,可以看做event poll,通過紅黑樹和內核維護的等待隊列等結構實現的事件觸發等機制實現的高併發,解決了select和poll未能解決的遍歷所有fd來查找響應的缺陷,同時可以通過非阻塞IO模式實現更高的服務性能。

/proc/sys/fs/epoll/max_user_watches:這個文件中的值表示用戶能註冊到epoll實例中的最大文件描述符的數量限制(也就是硬件本身支持的最大數量限制)

關鍵函數
int epoll_create(int size);	//創建一個epoll實例(本質是紅黑樹),也佔用個文件描述符,所以在使用完epoll後,必須調用close()關閉,否則可能導致fd被耗盡。
				//size用來告訴內核這個監聽的數目一共有多大,自從Linux 2.6.8開始,size參數被忽略,但是依然要大於0。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
	將監聽的文件描述符添加到epoll實例中,實例代碼爲將標準輸入文件描述符添加到epoll中
	第一個參數是epoll_create()的返回值,
	第二個參數表示動作,用三個宏來表示:
		EPOLL_CTL_ADD:註冊新的fd到epfd中;
		EPOLL_CTL_MOD:修改已經註冊的fd的監聽事件;
		EPOLL_CTL_DEL:從epfd中刪除一個fd;
	第三個參數是需要監聽的fd,
	第四個參數是告訴內核需要監聽什麼事,struct epoll_event結構如下:
		struct epoll_event {
			__uint32_t events; // Epoll events 
			epoll_data_t data; // User data variable 
		};
		events可以是以下幾個宏的集合(常用的IN/OUT/ERR/ET):
			EPOLLIN :表示對應的文件描述符可以讀(包括對端SOCKET正常關閉);
			EPOLLOUT:表示對應的文件描述符可以寫;
			EPOLLPRI:表示對應的文件描述符有緊急的數據可讀(這裏應該表示有帶外數據到來);
			EPOLLERR:表示對應的文件描述符發生錯誤;
			EPOLLHUP:表示對應的文件描述符被掛斷;
			EPOLLET: 將EPOLL設爲邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來說的。
			EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列裏。
		epoll_data_t聯合體定義如下:(注意是聯合體)
			typedef union epoll_data
			{
			  void *ptr;		//可以傳遞任意類型數據,常用來傳 回調函數
			  int fd;		//可以直接傳遞客戶端的fd
			  uint32_t u32;
			  uint64_t u64;
			} epoll_data_t;


int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
等待epoll事件從epoll實例中發生, 並返回事件總數以及傳出對應文件描述符
	參數events用來從內核得到事件的集合,
	參數maxevents表示每次能處理的最大事件數,告之內核這個events有多大,這個maxevents的值不能大於創建epoll_create()時的size,
	參數timeout是超時時間(毫秒,0會立即返回,-1將不確定,也有說法說是永久阻塞)。


注意:epoll的兩種觸發模式:
	邊沿觸發vs水平觸發
	epoll事件有兩種模型,邊沿觸發:edge-triggered (ET), 水平觸發:level-triggered (LT)
	水平觸發(level-triggered),是epoll的默認模式
		socket接收緩衝區不爲空 有數據可讀 讀事件一直觸發
		socket發送緩衝區不滿 可以繼續寫入數據 寫事件一直觸發
	邊沿觸發(edge-triggered)
		socket的接收緩衝區狀態變化時觸發讀事件,即空的接收緩衝區剛接收到數據時觸發讀事件
		socket的發送緩衝區狀態變化時觸發寫事件,即滿的緩衝區剛空出空間時觸發讀事件
	邊沿觸發僅觸發一次,水平觸發會一直觸發。
	開源庫:libevent 採用水平觸發, nginx 採用邊沿觸發

注意:每當服務端連接斷開後,進入TIME_WAIT狀態,等待2msl時間之後才能重新使用IP和端口,否則在bind時就會報錯。要解決這個問題可以在程序開始時調用端口複用函數setsockopt。原型如下:

//int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
    /* sockfd:標識一個套接口的描述字。
      level:選項定義的層次;支持SOL_SOCKET、IPPROTO_TCP、IPPROTO_IP和IPPROTO_IPV6。
      optname:需設置的選項。
      optval:指針,指向存放選項值的緩衝區
      optlen:optval緩衝區長度。
      返回值:  成功返回0,失敗返回 -1.  */
      

實際調用:
 setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

廢話不多說,上源碼!

實現的功能:客戶端C向服務端S發送一串字符數據,S端會對字符串做轉大寫操作然後回發給C端。直接在咱們Tcp_Server.cpp基礎上修改代碼

服務端Epoll_Server.cpp

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <ctype.h>
#include <sys/epoll.h>	//epoll頭文件

#define MAXSIZE 1024
#define IP_ADDR "127.0.0.1"
#define IP_PORT 8888

int main()
{
	int i_listenfd, i_connfd;
	struct sockaddr_in st_sersock;
	char msg[MAXSIZE];
	int nrecvSize = 0;

	struct epoll_event ev, events[MAXSIZE];
	int epfd, nCounts;	//epfd:epoll實例句柄, nCounts:epoll_wait返回值

	if((i_listenfd = socket(AF_INET, SOCK_STREAM, 0) ) < 0)	//建立socket套接字
	{
		printf("socket Error: %s (errno: %d)\n", strerror(errno), errno);
		exit(0);
	}

	memset(&st_sersock, 0, sizeof(st_sersock));
	st_sersock.sin_family = AF_INET;  //IPv4協議
	st_sersock.sin_addr.s_addr = htonl(INADDR_ANY);	//INADDR_ANY轉換過來就是0.0.0.0,泛指本機的意思,也就是表示本機的所有IP,因爲有些機子不止一塊網卡,多網卡的情況下,這個就表示所有網卡ip地址的意思。
	st_sersock.sin_port = htons(IP_PORT);

	if(bind(i_listenfd,(struct sockaddr*)&st_sersock, sizeof(st_sersock)) < 0) //將套接字綁定IP和端口用於監聽
	{
		printf("bind Error: %s (errno: %d)\n", strerror(errno), errno);
		exit(0);
	}

	if(listen(i_listenfd, 20) < 0)	//設定可同時排隊的客戶端最大連接個數
	{
		printf("listen Error: %s (errno: %d)\n", strerror(errno), errno);
		exit(0);
	}

	if((epfd = epoll_create(MAXSIZE)) < 0)	//創建epoll實例
	{
		printf("epoll_create Error: %s (errno: %d)\n", strerror(errno), errno);
		exit(-1);
	}
	
	ev.events = EPOLLIN;
	ev.data.fd = i_listenfd;
	if(epoll_ctl(epfd, EPOLL_CTL_ADD, i_listenfd, &ev) < 0)
	{
		printf("epoll_ctl Error: %s (errno: %d)\n", strerror(errno), errno);
		exit(-1);
	}
	printf("======waiting for client's request======\n");
	//準備接受客戶端連接
	while(1)
	{
		if((nCounts = epoll_wait(epfd, events, MAXSIZE, -1)) < 0)
		{
			printf("epoll_ctl Error: %s (errno: %d)\n", strerror(errno), errno);
			exit(-1);
		}
		else if(nCounts == 0)
		{
			printf("time out, No data!\n");
		}
		else
		{
			for(int i = 0; i < nCounts; i++)
			{
				int tmp_epoll_recv_fd = events[i].data.fd;
				if(tmp_epoll_recv_fd == i_listenfd)	//有客戶端連接請求
				{
					if((i_connfd = accept(i_listenfd, (struct sockaddr*)NULL, NULL)) < 0)	//阻塞等待客戶端連接
					{
						printf("accept Error: %s (errno: %d)\n", strerror(errno), errno);
					//	continue;
					}	
					else
					{
						printf("Client[%d], welcome!\n", i_connfd);
					}
	
					ev.events = EPOLLIN;
					ev.data.fd = i_connfd;
					if(epoll_ctl(epfd, EPOLL_CTL_ADD, i_connfd, &ev) < 0)
					{
						printf("epoll_ctl Error: %s (errno: %d)\n", strerror(errno), errno);
						exit(-1);
					}
				}
				else	//若是已連接的客戶端發來數據請求
				{
					//接受客戶端發來的消息並作處理(小寫轉大寫)後回寫給客戶端
					memset(msg, 0 ,sizeof(msg));
					if((nrecvSize = read(tmp_epoll_recv_fd, msg, MAXSIZE)) < 0)
					{
						printf("read Error: %s (errno: %d)\n", strerror(errno), errno);
						continue;
					}
					else if( nrecvSize == 0)	//read返回0代表對方已close斷開連接。
					{
						printf("client has disconnected!\n");
						epoll_ctl(epfd, EPOLL_CTL_DEL, tmp_epoll_recv_fd, NULL);
						close(tmp_epoll_recv_fd);  //
					
						continue;
					}
					else
					{
						printf("recvMsg:%s", msg);
						for(int i=0; msg[i] != '\0'; i++)
						{
							msg[i] = toupper(msg[i]);
						}
						if(write(tmp_epoll_recv_fd, msg, strlen(msg)+1) < 0)
						{
							printf("write Error: %s (errno: %d)\n", strerror(errno), errno);
						}

					}
				}
			}
		}
	}//while
	close(i_listenfd);
	close(epfd);
	return 0;
}

客戶端Epoll_Client.cpp (直接用咱們Tcp_Client.cpp就可以)

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <signal.h>
#include <arpa/inet.h>

#define MAXSIZE 1024
#define IP_ADDR "127.0.0.1"
#define IP_PORT 8888

int i_sockfd = -1;

void SigCatch(int sigNum)	//信號捕捉函數(捕獲Ctrl+C)
{
	if(i_sockfd != -1)
	{
		close(i_sockfd);
	}
	printf("Bye~! Will Exit...\n");
	exit(0);
}

int main()
{
	struct sockaddr_in st_clnsock;
	char msg[1024];
	int nrecvSize = 0;

	signal(SIGINT, SigCatch);	//註冊信號捕獲函數

	if((i_sockfd = socket(AF_INET, SOCK_STREAM, 0) ) < 0)	//建立套接字
	{
		printf("socket Error: %s (errno: %d)\n", strerror(errno), errno);
		exit(0);
	}

	memset(&st_clnsock, 0, sizeof(st_clnsock));
	st_clnsock.sin_family = AF_INET;  //IPv4協議
	//IP地址轉換(直接可以從物理字節序的點分十進制 轉換成網絡字節序)
	if(inet_pton(AF_INET, IP_ADDR, &st_clnsock.sin_addr) <= 0)
	{
		printf("inet_pton Error: %s (errno: %d)\n", strerror(errno), errno);
		exit(0);
	}
	st_clnsock.sin_port = htons(IP_PORT);	//端口轉換(物理字節序到網絡字節序)

	if(connect(i_sockfd, (struct sockaddr*)&st_clnsock, sizeof(st_clnsock)) < 0)	//主動向設置的IP和端口號的服務端發出連接
	{
		printf("connect Error: %s (errno: %d)\n", strerror(errno), errno);
		exit(0);
	}

	printf("======connect to server, sent data======\n");

	while(1)	//循環輸入,向服務端發送數據並接受服務端返回的數據
	{
		fgets(msg, MAXSIZE, stdin);
		printf("will send: %s", msg);
		if(write(i_sockfd, msg, MAXSIZE) < 0)	//發送數據
		{
			printf("write Error: %s (errno: %d)\n", strerror(errno), errno);
			exit(0);
		}

		memset(msg, 0, sizeof(msg));
		if((nrecvSize = read(i_sockfd, msg, MAXSIZE)) < 0)	//接受數據
		{
			printf("read Error: %s (errno: %d)\n", strerror(errno), errno);
		}
		else if(nrecvSize == 0)
		{
			printf("Service Close!\n");
		}
		else
		{
			printf("Server return: %s\n", msg);
		}

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