1. 程式人生 > >Linux學習之網路程式設計(select)

Linux學習之網路程式設計(select)

言之者無罪,聞之者足以戒。 - “詩序”

1、阻塞式I/O

下面看一下實現的邏輯:

2、非阻塞式I/O

下面看一下實現的邏輯:

3、I/O複用(select/epoll)

(1)  int  select (int maxfdp, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout)

 第一個引數maxfdp :是一個整數值,是指集合中所有檔案描述符的範圍,即所有檔案描述符的最大值加1,不能錯!
             第二個引數readfds

: 是指向fd_set結構的指標,這個集合中應該包括檔案描述符,我們是要監視這些檔案描述符的讀變化的,即我們關心是否可以從這些檔案中讀取資料了,如果這個集合中有一個檔案可讀,select就會返回一個大於0的值,表示有檔案可讀,如果沒有可讀的檔案,則根據timeout引數再判斷是否超時,若超出timeout的時間,select返回0,若發生錯誤返回負值。可以傳入NULL值,表示不關心任何檔案的讀變化。
             第三個引數writefds :是指向fd_set結構的指標,這個集合中應該包括檔案描述符,我們是要監視這些檔案描述符的寫變化的,即我們關心是否可以向這些檔案中寫入資料了,如果這個集合中有一個檔案可寫,select就會返回一個大於0的值,表示有檔案可寫,如果沒有可寫的檔案,則根據timeout引數再判斷是否超時,若超出timeout的時間,select返回0,若發生錯誤返回負值。可以傳入NULL值,表示不關心任何檔案的寫變化。     
             第四個引數errorfds
: 同上面兩個引數的意圖,用來監視檔案是否發生錯誤異常。
             第五個引數timeout : 是select的超時時間,這個引數至關重要。它可以使select處於三種狀態,第一,若將NULL以形參傳入,即不傳入時間結構,就是將select置於永久阻塞狀態,一定等到監視檔案描述符集合中某個檔案描述符發生變化為止;第二,若將時間值設為0秒0毫秒,就變成一個純粹的非阻塞函式,不管檔案描述符是否有變化,都立刻返回繼續執行,檔案無變化返回0,有變化返回一個正值;第三,timeout的值大於0,這就是等待的超時時間,即select在timeout時間內阻塞,超時時間之內有事件到來返回正值,超時返回0。                

返回值:負值:select錯誤,正值:某些檔案可讀寫或出錯,0:等待超時,沒有可讀寫或錯誤的檔案。

(2)四個操作描述字的巨集定義

    FD_ZERO(&set);      /* 將set清零 */
    FD_SET(fd, &set);   /* 將fd加入set */
    FD_CLR(fd, &set);   /* 將fd從set中清除 */
    FD_ISSET(fd, &set); /* 如果fd在set中則真 */

    關於FD_ISSET多說一句,select返回時會將沒有準備就緒的檔案描述符從set中清除,所以FD_ISSET(fd, &set)判斷fd是否在set中,如果在說明他沒有被清除,該描述符的狀態發生了變化(可讀、可寫或者異常)。

下面看一下邏輯:

這裡給出一張程式的邏輯圖:

下面來看一下伺服器程式的程式碼:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>


#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>


#include <unistd.h>
#include <fcntl.h>


#define SERV_PORT 8888
#define MAX_LISTEN_QUE 5

#define MAX_BUFFER_SIZE 1024

#define RT_ERR (-1)
#define RT_OK  0
//建立套接字的函式
int Ipv4_tcp_create_socket(void)
{
	int listenfd,sockfd,opt=1;
	struct sockaddr_in server,client;
	socklen_t len;
	int temp;
	int ret;
	//建立套接字
	listenfd = socket(AF_INET,SOCK_STREAM,0);//ipv4,全雙工
	if(listenfd < 0)
	{
		perror("Create socket fail\n");	
		return RT_ERR;
	}
	//設定地址重用
	if((ret = setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt))) < 0)
	{
		perror("Error , set socket reuse addr failued\n");	
		return RT_ERR;
	}
	//初始化伺服器端

	bzero(&server,sizeof(server));
	server.sin_family = AF_INET;
	server.sin_port = htons(SERV_PORT);
	server.sin_addr.s_addr = htonl(INADDR_ANY);//連線所有的客戶端

	len = sizeof(struct sockaddr);
	if(bind(listenfd,(struct sockaddr *)&server,len) < 0){
		perror("bind error\n");
		return RT_ERR;
	}

	listen(listenfd,MAX_LISTEN_QUE);

	return listenfd;
	
}

int main(int argc,char *argv[])
{
	int listenfd,sockfd;
	struct sockaddr_in server,client;
	socklen_t len;
	int bytes = 0;
	fd_set g_rdfs,cur_rdfs;
	int maxfd;
	int i;
	char buf[MAX_BUFFER_SIZE];

	len = sizeof(struct sockaddr_in);
	//呼叫建立套接字函式
	listenfd = Ipv4_tcp_create_socket();
	//將c_rdfs清零
	FD_ZERO(&g_rdfs);
	//將listendfd新增進c_rdfs中
	FD_SET(listenfd,&g_rdfs);
	maxfd = listenfd;
	
	while(1)
	{
		cur_rdfs = g_rdfs;
		//監控套接字
		if(select(maxfd + 1,&cur_rdfs,NULL,NULL,NULL) < 0){
			perror("select error\n");
			return RT_ERR;
		}
		for(i=0;i <= maxfd;i++){
			//判斷i是否在cur_rdfs中
			if(FD_ISSET(i,&cur_rdfs)){
				//判斷是不是我們監聽的套接字
				if(listenfd == i){
					//接收套接字(返回的是通訊套接字)
					if((sockfd = accept(listenfd,(struct sockaddr*)&client,(socklen_t*)&len)) < 0){
						perror("accept error\n");
						return RT_ERR;
					}
					printf("sockfd:%d\n",sockfd);
					//清除我們新增進去的套接字,以防下次迴圈再次檢測到
					FD_CLR(i,&cur_rdfs);
					//得到最大的套接字的個數
					maxfd = maxfd > sockfd ? maxfd : sockfd;
					//將通訊套接字加入關注的套接字集
					FD_SET(sockfd,&g_rdfs);
					//如果不是監聽套接字
				}else{
						printf("read socket :%d\n",i);
						//如果不是監聽套接字我就直接讀取資料
						bytes = recv(i,buf,MAX_BUFFER_SIZE,0);
						if(bytes < 0){
							perror("recv error\n");
							return RT_ERR;
						}
						if(bytes == 0){
							//客戶端退出,從我關注的套接字集中把它清掉
							FD_CLR(i,&g_rdfs);
							//關閉套接字
							close(i);
							continue;
						}
						//列印讀取到的內容
						printf("buf:%s\n",buf);
						//把客戶端傳送的資料,傳送給客戶端
						send(i,buf,strlen(buf),0);
					}
			}
		}
	}
}

有的朋友可能會問怎麼這兩篇文章都沒有客戶端的程式,因為這兩篇文章我是用windows下的命令提示符來完成和Linux編寫的伺服器程式通訊的。所以沒有給出客戶端的程式碼。

直接找到windows下的命令提示符視窗,輸入:telnet 192.168.177.128 8888回車就好了。(192.168.177.128是地址,8888是埠號)

注意:先執行伺服器程式在連線。

上面程式的程式碼還是有很多不足的,有很多地方可以優化,優化程式碼是每一個程式設計師都應該思考的問題,我自己也想著優化了一下,下面直接粘貼出程式碼:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>


#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>


#include <unistd.h>
#include <fcntl.h>


#define SERV_PORT 8888
#define MAX_LISTEN_QUE 5

#define MAX_BUFFER_SIZE 1024

#define RT_ERR (-1)
#define RT_OK  0
//建立套接字的函式
int Ipv4_tcp_create_socket(void)
{
	int listenfd,sockfd,opt=1;
	struct sockaddr_in server,client;
	socklen_t len;
	int temp;
	int ret;
	//建立套接字
	listenfd = socket(AF_INET,SOCK_STREAM,0);//ipv4,全雙工
	if(listenfd < 0)
	{
		perror("Create socket fail\n");	
		return RT_ERR;
	}
	//設定地址重用
	if((ret = setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt))) < 0)
	{
		perror("Error , set socket reuse addr failued\n");	
		return RT_ERR;
	}
	//初始化伺服器端

	bzero(&server,sizeof(server));
	server.sin_family = AF_INET;
	server.sin_port = htons(SERV_PORT);
	server.sin_addr.s_addr = htonl(INADDR_ANY);//連線所有的客戶端

	len = sizeof(struct sockaddr);
	if(bind(listenfd,(struct sockaddr *)&server,len) < 0){
		perror("bind error\n");
		return RT_ERR;
	}

	listen(listenfd,MAX_LISTEN_QUE);

	return listenfd;
	
}

int main(int argc,char *argv[])
{
	int listenfd,sockfd;
	struct sockaddr_in server,client;
	socklen_t len;
	int bytes = 0;
	fd_set g_rdfs,cur_rdfs;
	int maxfd;
	int i;
	char buf[MAX_BUFFER_SIZE];
	//獲得套接字的極限值
	int client_fd[FD_SETSIZE];
	
	printf("FD_SETSIZE:%d\n",FD_SETSIZE);
	len = sizeof(struct sockaddr_in);
	//呼叫建立套接字函式
	listenfd = Ipv4_tcp_create_socket();
	//將c_rdfs清零
	FD_ZERO(&g_rdfs);
	//將listendfd新增進c_rdfs中
	FD_SET(listenfd,&g_rdfs);
	maxfd = listenfd;
	//賦初值
	for(i = 0;i < FD_SETSIZE; i++)
	{
		client_fd[i] = -1;
	}
	while(1)
	{
		cur_rdfs = g_rdfs;
		//監控套接字
		if(select(maxfd + 1,&cur_rdfs,NULL,NULL,NULL) < 0){
			perror("select error\n");
			return RT_ERR;
		}
		//判斷監聽套接字是否在檢測集中
		if(FD_ISSET(listenfd,&cur_rdfs)){
			//接收套接字(返回的是通訊套接字)
			if((sockfd = accept(listenfd,(struct sockaddr*)&client,(socklen_t*)&len)) < 0){
				perror("accept error\n");
				return RT_ERR;
			}
			printf("sockfd:%d\n",sockfd);
			//清除我們新增進去的套接字,以防下次迴圈再次檢測到
			FD_CLR(listenfd,&cur_rdfs);
			//得到最大的套接字的個數
			maxfd = maxfd > sockfd ? maxfd : sockfd;
			//將通訊套接字加入關注的套接字集
			FD_SET(sockfd,&g_rdfs);
            //下面的for語句整體的作用是找到我們的通訊套接字並將它的值賦值給儲存套接字的陣列
			//(大家都應該知道前三個套接字都不是我們的通訊套接字,第四個才是)
			for(i = 0; i < maxfd; i++){
				if(-1 == client_fd[i]){
					client_fd[i] = sockfd;
					break;
				}
			}
		}
		for(i=0;i <= maxfd;i++){
			if(-1 == client_fd[i]){
				continue;
			}
			//判斷cur_rdfs是不是在我們關注的讀寫套接字中
			if(FD_ISSET(client_fd[i],&cur_rdfs)){
				printf("read socket :%d\n",client_fd[i]);
				//如果不是監聽套接字我就直接讀取資料
				bytes = recv(client_fd[i],buf,MAX_BUFFER_SIZE,0);
				if(bytes < 0){
					perror("recv error\n");
					return RT_ERR;
				}
				if(bytes == 0){
					//客戶端退出,從我關注的套接字集中把它清掉
					FD_CLR(client_fd[i],&g_rdfs);
					//關閉套接字
					close(client_fd[i]);
					client_fd[i] = -1;
					continue;
				}
				//列印讀取到的內容
				printf("buf:%s\n",buf);
				//把客戶端傳送的資料,傳送給客戶端
				send(client_fd[i],buf,strlen(buf),0);
					
			}
		}
	}
}