1. 程式人生 > >【網路】Select伺服器的實現

【網路】Select伺服器的實現

五種I/O模型

Unix下共有五種I/O模型,分別是

(1)阻塞式I/O;

(2)非阻塞I/O;

(3)I/O複用(select和(e)poll);

(4)訊號驅動I/O(SIGIO);

(5)非同步I/O(Posix.1的aio_系列函式);

阻塞I/O模型

應用程式呼叫一個IO函式,導致應用程式阻塞,等待資料準備好。如果資料沒有準備好,一直等待。資料準備好了,從核心拷貝到使用者空間,表示IO結束,IO函式返回成功指示。

非阻塞I/O模型

我們把一個套介面設定為非阻塞就是告訴核心,當所請求的I/O操作無法完成時,不要將程序睡眠,而是返回一個錯誤。這樣我們的I/O操作函式將不斷的測試 資料是否已經準備好,如果沒有準備好,繼續測試,直到資料準備好為止。在這個不斷測試的過程中,會大量的佔用CPU的時間。

I/O複用模型

該種模型又被稱作是多路轉接,I/O複用模型會用到select或者poll函式,這兩個函式也會使程序阻塞,但是和阻塞I/O所不同的的,這兩個函式可以同時阻塞多個I/O操作。而且可以同時對多個讀操作,多個寫操作的I/O函式進行檢測,直到有資料可讀或可寫時,才真正呼叫I/O操作函式。

訊號驅動I/O模型

首先我們允許套介面進行訊號驅動I/O,並安裝一個訊號處理函式,程序繼續執行並不阻塞。當資料準備好時,程序會收到一個SIGIO訊號,可以在訊號處理函式中呼叫I/O操作函式處理資料。

非同步I/O模型

呼叫aio_read函式,告訴核心描述字,緩衝區指標,緩衝區大小,檔案偏移以及通知的方式,然後立即返回。當核心將資料拷貝到緩衝區後,再通知應用程式。也就是它只需要發起這個讀寫事件,不要等待與資料搬遷,只需要在結束之後得到成果。

舉例

(1)張三在釣魚,當魚沒有上鉤時,便一直進行等待;當魚上鉤後,將魚調出。這便是基本的阻塞式I/O

(2)張三在釣魚,這次,當魚沒有上鉤時,他便翻著看《C語言入門》這本書,也可以玩玩手機;當魚上鉤後,將魚調出。這是非阻塞I/O

(3)張三在釣魚,這次他放了一百個魚竿。依次檢查這些魚竿,當一個魚竿有魚上鉤後,將魚調出,然後繼續檢查所有魚竿。這是I/O多路複用

(4)張三釣魚時,將魚竿上放一個鈴鐺,當魚上鉤後會響。然後他便可以幹自己的事情,當領響時,他知道上鉤了,再去收魚

(5)張三準備釣魚,這次。。他直接僱了個人給自己釣魚。這便是非同步I/O

相關函式介紹

重定向的dup函式

標頭檔案

#include<unistd.h>

函式原型

int dup(int oldfd);

int dup2(int oldfd, int newfd);

功能

dup 

將oldfd檔案描述符進行一份拷貝,返回值為最新拷貝生成的檔案描述符(最小的未被使用的檔案描述符)

dup2 

使用newfd對oldfd檔案描述符做一份拷貝,必要是可以先關閉newfd檔案描述符

呼叫dup2之後,oldfd檔案描述符不變,newfd和oldfd相等。

程式碼測試

#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
#include<string.h>

int main()
{
	umask(0);

	int fd = open("test.txt",O_CREAT|O_RDWR,0666);
	const char* msg = "hello world";
	printf("fd -> %d\n",fd);
	write(fd,msg,strlen(msg));
	int new_fd = dup(fd);//將fd進行拷貝,儲存到new_fd中
	printf("new_fd -> %d\n",new_fd);
	write(new_fd,msg,strlen(msg));
	int cpfd = dup(1);//拷貝標準輸出檔案描述符
	dup2(fd,1);
	printf("nice\n");
	dup2(cpfd,1);
	printf("niec\n");
	return 0;
}

執行結果



select函式

標頭檔案

#include<sys/time.h>

#include<sys/types.h>

#include<unistd.h>

函式原型

int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *expectfds,struct timeval *timeout);

函式功能

同時等待多個檔案描述符

引數

nfds

表示的是等待的檔案描述符中的最大的那個+1;

fd_set

表示的是是否需要等待某個檔案描述符,所以這裡的fd_set底層是用點陣圖實現的,所以我們最多可以等待的檔案描述符的個數為sizeof(fd_set)*8;

readfds

表示的是需要等待的讀事件的檔案描述符集;

writefds

表示的是需要等待的寫事件的檔案描述符集;

exceptfds

表示的是需要等待的異常事件的檔案描述符集;

timeout

表示的是每次select的時間為ty_sec秒

返回值

返回集合,該集合表示了收到訊息的檔案描述符

與fd_set有關的函式

fd_set為一個集合,底層是用點陣圖進行實現的

它的每一位都可以用來表示對應的檔案描述符是否收到了事件訊息

#include <sys/time.h>  
#include <sys/types.h>  
#include <unistd.h>  
  
void FD_CLR(int fd, fd_set *set);//將檔案描述符中的fd位去掉  
int  FD_ISSET(int fd, fd_set *set);//檢測檔案描述符集set中的fd位是否存在  
void FD_SET(int fd, fd_set *set);//為set檔案描述符集設定fd為設定  
void FD_ZERO(fd_set *set);//將set檔案描述符集清空  

Select模型

理解select模型的關鍵在於理解fd_set

假設fd_set長度為1位元組

fd_set中的每⼀一bit

可以對應⼀一個⽂檔案描述符fd

則1位元組長的fd_set最⼤大可以對應8個fd

步驟:
(1)執⾏行fd_set set; FD_ZERO(&set);則set⽤用位表⽰示是0000,0000。

(2)若fd=5,執⾏行FD_SET(fd,&set);後set變為0001,0000(第5位置為1)

(3)若再加⼊入fd=2,fd=1,則set變為0001,0011

(4)執⾏行select(6,&set,0,0,0)阻塞等待

(5)若fd=1,fd=2上都發⽣生可讀事件,則select返回,此時set變為0000,0011。注意:沒有事件
發⽣生的fd=5被清空

也就是說,readfds[]陣列來表示連線的客戶端,該陣列大小表示最多可以連線的數量

而rfds是一個集合,底層用點陣圖實現

表示的是對應的檔案描述符有沒有收到對方發來的訊息

若有,則對應的位上被置為1

程式碼實現

select伺服器

#include<stdio.h>
#include<stdlib.h>
#include<error.h>
#include<unistd.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<sys/time.h>
#include<string.h>

int readfds[sizeof(fd_set)*8];
int writefds[sizeof(fd_set)*8];

static void Usage()
{
	printf("Usage: [ipaddr] [port]\n");
}

int startUp(char* ip, int port)
{
	int sockfd = socket(AF_INET,SOCK_STREAM,0);
	if(sockfd < 0)
	{
		perror("socket");
		exit(2);
	}

	struct sockaddr_in addr;
	addr.sin_family = AF_INET;
	addr.sin_port = htons(port);
	addr.sin_addr.s_addr = inet_addr(ip);

	if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr)) < 0)
	{
		perror("bind");
		exit(3);
	}

	if(listen(sockfd,5)<0)
	{
		perror("listen");
		exit(4);
	}

	return sockfd;
}

int main(int argc, char* argv[])
{
	int i = 1;
	if(argc != 3)
	{
		Usage();
		exit(1);
	}

	int listen_sock = startUp(argv[1],atoi(argv[2]));

	int num = sizeof(fd_set)*8;
	writefds[0] = -1;
	readfds[0] = listen_sock;

	for(; i<num; i++)
	{
		writefds[i] = -1;
		readfds[i] = -1;
	}

	fd_set rfds,wfds;
	while(1)
	{
		int maxfd = -1;
		FD_ZERO(&rfds);
		FD_ZERO(&wfds);

		for(i = 0; i<num; ++i)
		{
			if(readfds[i] != -1)
			{
				FD_SET(readfds[i],&rfds);
			}

			if(writefds[i] != -1)
			{
				FD_SET(writefds[i],&wfds);
			}

			maxfd = readfds[i] > maxfd ? readfds[i] : maxfd;
			maxfd = writefds[i] > maxfd ? writefds[i] : maxfd;
		}

		struct timeval time = {1,0};
	
		int n = select(maxfd+1,&rfds,&wfds,NULL,&time);
		
		switch(n)
		{
			case 0:
				printf("time out...\n");
				break;
			case -1:
				break;
			default:
			{
				for(i = 0; i<num; ++i)
				{
					if(FD_ISSET(readfds[i],&rfds))
					{
						if(i == 0)
						{//監聽伺服器就緒
							struct sockaddr_in client;
							socklen_t len = sizeof(client);
							int client_sock = accept(listen_sock,(struct sockaddr*)&client,&len);
						
							if(client_sock < 0)
							{
								perror("accpet");
								exit(5);
							}
							else
							{
								int tmp = 0;
								for(; tmp < num; ++tmp)
								{
									if(readfds[tmp] == -1)
									{
										readfds[tmp] = client_sock;
										break;
									}
								}

								if(tmp == num)
								{
									printf("readfds 滿了\n");
									exit(6);
								}
							}
						}
						else
						{//等待的普通檔案描述符就緒
							char buf[1024];	
							ssize_t s = read(readfds[i],buf,sizeof(buf));
							if(s < 0)
							{
								perror("read");
								exit(7);
							}
							else if(s == 0)
							{
								printf("客戶端退出..\n");
								close(readfds[i]);
								readfds[i] = -1;
								continue;
							}
							else
							{
								buf[s] = 0;
								printf("#client: %s",buf);
								fflush(stdout);
								write(readfds[i],buf,strlen(buf));
							}
						}
					}
				}
			}
		}
	}

	return 0;
}

select客戶端

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

static void Usage()
{
	printf("Usage: [ipaddr] [port]\n");
}

int main(int argc,char* argv[])
{
	if(argc != 3)
	{
		Usage();
		exit(1);
	}

	int sockfd = socket(AF_INET,SOCK_STREAM,0);
	if(sockfd < 0)
	{
		perror("socket");
		exit(2);
	}

	struct sockaddr_in addr;
	addr.sin_family = AF_INET;
	addr.sin_port = htons(atoi(argv[2]));
	addr.sin_addr.s_addr = inet_addr(argv[1]);

	if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))<0)
	{
		perror("connect");
		exit(3);
	}

	printf("連線成功...\n");

	char buf[1024];
	while(1)
	{
		//發資料
		printf("#client: ");
		fflush(stdout);
		ssize_t s = read(0,buf,sizeof(buf)-1);
		if(s <= 0)
		{
			perror("read");
			exit(4);
		}

		int fd = dup(1);
		dup2(sockfd,1);
		printf("%s",buf);
		fflush(stdout);
		dup2(fd,1);
		
		//收資料
		s = read(sockfd,buf,sizeof(buf)-1);
		if(s == 0)
		{
			printf("伺服器退出...\n");
			break;
		}
		else if(s < 0)
		{
			perror("read");
			exit(5);
		}
		else
		{
			buf[s-1] = '\0';
			printf("#server: %s\n",buf);
		}
	}
	close(sockfd);
	return 0;
}

select伺服器優缺點

優點

1、不需要fork或者pthread_create就可以實現一對多的通訊,簡化了程序執行緒的使用

2、同時等待多個檔案描述符,效率相對較高

缺點

1、每次呼叫select,都需要把fd集合從使用者態拷貝到核心態,在fd很多的情況下,迴圈次數多;

2、每次呼叫select都需要在核心遍歷傳遞進來的所有fd,開銷比較大

3、select支援的檔案描述符數量過小,預設是1024