Linux網路程式設計學習筆記(7)---5種I/O模型及select輪詢
本文主要介紹5種I/O模型,select函式以及利用select實現C/S模型。
1、5種I/O模型
(1)阻塞I/O:
一直等到資料到來,才會將資料從核心中拷貝到使用者空間中。
(2)非阻塞I/O:
每過一段時間就詢問是否有資料到來(輪詢),呼叫recv()函式,若沒有資料到來會返回錯誤。接著繼續詢問。
(3)I/O多路複用:
一個程序可以輪詢多個I/O(檔案描述符),將阻塞過程提前到select/poll/epoll中。
(4)訊號驅動I/O:
當資料到達時,程序會收到訊號SIGIO,在訊號處理函式中,實現資料的讀取。
(5)非同步I/O:
以上四種方式都是同步方式,都是順序執行的。而非同步I/O不一定是順序執行。通過呼叫aio_read函式,無論核心是否準備好資料,都會給使用者程序返回一個值,因此不會使程式產生阻塞。同時,使用者程序可以做其他的事情。若資料準備好了,核心會將資料複製給使用者程序。當一切完成之後,核心會發送完成通知。
2、select函式
select函式原型:
int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);
select函式作用:
select函式用來管理多個I/O,當其中的一個或多個I/O檢測到了我們感興趣的事件,select就返回檢測到的事件個數,並返回哪些I/O產生了事件。
引數介紹:
(1)maxfdp:一個整數,在Linux中,為所有檔案描述符的最大值加1;
(2)readfds:如果I/O(檔案描述符)可讀,就把它放入readfds集合中;
(3)writefds:如果I.O(檔案描述符)可寫,就把它放入writefds集合中;
(4)exceptfds:如果想檢測檔案描述符是否有異常,就把它放入exceptfdsz集合中;
(5)timeout:結構體timeval,select的超時時間。若設定為NULL,則將select置於阻塞狀態;若設定為0,則設定select為純非阻塞函式,不管檔案描述符是否有變換,會立即返回;若設定為大於0,則在timeout時間內,select一直阻塞。
3、利用select的C/S模型
客戶端:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include"common.h"
int main()
{
int sock ;
sock = socket(PF_INET,SOCK_STREAM,0 );
if(sock <0)
{
err_exit("socket");
}
struct sockaddr_in addr;
memset(&addr,0,sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(5888);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
int ret ;
ret = connect (sock , (struct sockaddr*)&addr,sizeof(addr));
if(ret < 0)
{
err_exit("connect");
}
fd_set fds;
FD_ZERO(&fds); //清空集合
int maxfds;
if(STDIN_FILENO > sock)
maxfds=STDIN_FILENO;
else
maxfds=sock;
while(1)
{
FD_SET(STDIN_FILENO,&fds);
FD_SET(sock,&fds);
int nready=select(maxfds+1,&fds,NULL,NULL,NULL);
if(nready==-1) //出錯
err_exit("select");
else if(nready == 0) //無資料
continue;
//鍵盤有輸入
if(FD_ISSET(STDIN_FILENO,&fds))
{
struct packet writebuf;
memset(&writebuf,0,sizeof(writebuf));
//獲取鍵盤輸入
fgets(writebuf.data,sizeof(writebuf.data),stdin);
int n=strlen(writebuf.data);
writebuf.msgLen=htonl(n);
writen(sock,&writebuf,4+n); //傳送
memset(&writebuf,0,sizeof(writebuf));
}
if(FD_ISSET(sock,&fds))
{
struct packet readbuf;
memset(&readbuf,0,sizeof(readbuf));
int ret=readn(sock,&readbuf.msgLen,4);
if(ret == -1)
err_exit("readn");
else if(ret == 0)
{
printf("peer1 close\n");
break;
}
int dataBytes = ntohl(readbuf.msgLen);
int readBytes= readn(sock,readbuf.data,dataBytes);
if(readBytes<0)
err_exit("read");
fputs(readbuf.data,stdout);
}
}
close(sock);
return 0;
}
伺服器:
/*
單程序伺服器
實現簡單點到點的聊天功能
*/
#include<stdlib.h>
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include"common.h"
int main()
{
int listenfd;
listenfd = socket(PF_INET , SOCK_STREAM, 0);
if(listenfd <0)
{
err_exit("socket");
}
struct sockaddr_in addr;
addr.sin_family = PF_INET;
addr.sin_port = htons(5888);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
//設定地址可重複利用
int on = 1;
setsockopt(listenfd, SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
int ret = bind(listenfd , (const struct sockaddr *)&addr,sizeof(addr));
if(ret == -1)
{
err_exit("bind");
}
ret = listen(listenfd , SOMAXCONN);
if(ret < 0)
{
err_exit("listen");
}
struct sockaddr_in peerAddr ;
socklen_t peerlen = sizeof(peerAddr); //peerlen一定要有初始值
int conn;
conn = accept (listenfd ,(struct sockaddr *)&peerAddr,&peerlen);
if(conn <0)
{
err_exit("accept");
}
printf("客戶端的IP地址是:%s,埠號是:%d\n",
inet_ntoa(peerAddr.sin_addr),ntohs(peerAddr.sin_port));
fd_set fds;
FD_ZERO(&fds); //清空集合
int maxfds;
if(STDIN_FILENO > conn)
maxfds=STDIN_FILENO;
else
maxfds=conn;
while(1)
{
FD_SET(STDIN_FILENO,&fds);
FD_SET(conn,&fds);
int nready=select(maxfds+1,&fds,NULL,NULL,NULL);
if(nready==-1) //出錯
err_exit("select");
else if(nready == 0) //無資料
continue;
//鍵盤有輸入
if(FD_ISSET(STDIN_FILENO,&fds))
{
struct packet writebuf;
memset(&writebuf,0,sizeof(writebuf));
//獲取鍵盤輸入
fgets(writebuf.data,sizeof(writebuf.data),stdin);
int n=strlen(writebuf.data);
writebuf.msgLen=htonl(n);
writen(conn,&writebuf,4+n); //傳送
memset(&writebuf,0,sizeof(writebuf));
}
if(FD_ISSET(conn,&fds))
{
struct packet readbuf;
memset(&readbuf,0,sizeof(readbuf));
int ret=readn(conn,&readbuf.msgLen,4);
if(ret == -1)
err_exit("readn");
else if(ret == 0)
{
printf("peer1 close\n");
break;
}
int dataBytes = ntohl(readbuf.msgLen);
int readBytes= readn(conn,readbuf.data,dataBytes);
if(readBytes<0)
err_exit("read");
fputs(readbuf.data,stdout);
}
}
close(listenfd);
close(conn);
return 0;
}
common.h
#ifndef __COMMON__
#define __COMMON__
#include<errno.h>
#include<stdlib.h>
#include<unistd.h>
# define err_exit(m)\
do\
{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
struct packet
{
unsigned int msgLen ;
char data [1024];
};
void handler(int sig)
{
printf("recv a sig = %d \n",sig);
exit(EXIT_SUCCESS);
}
/*
readn 函式
讀取count位元組資料
*/
ssize_t readn(int fd,void *buf, size_t count)
{
int left = count ; //剩下的位元組
char * ptr = (char*)buf ;
while(left>0)
{
int readBytes = read(fd,ptr,left);
if(readBytes< 0)//read函式小於0有兩種情況:1中斷 2出錯
{
if(errno == EINTR)//讀被中斷
{
continue;
}
return -1;
}
if(readBytes == 0)//讀到了EOF
{
return count - left;
}
left -= readBytes;
ptr += readBytes ;
}
return count ;
}
/*
writen 函式
寫入count位元組的資料
*/
ssize_t writen(int fd, void *buf, size_t count)
{
int left = count ;
char * ptr = (char *)buf;
while(left >0)
{
int writeBytes = write(fd,ptr,left);
if(writeBytes<0)
{
if(errno == EINTR)
continue;
return -1;
}
else if(writeBytes == 0)
continue;
left -= writeBytes;
ptr += writeBytes;
}
return count;
}
/*
從緩衝區中讀取指定長度的資料,但不清楚緩衝區內容
*/
ssize_t read_peek(int sockfd, void *buf, size_t len )
{
while(1)
{
int ret = recv (sockfd , buf ,len ,MSG_PEEK);
if(ret == -1)
{
if(errno ==EINTR) //出現中斷
continue ;
}
return ret ;
}
}
/*
按行讀取資料
引數說明:
sockfd:套接字
buf :應用層緩衝區,儲存讀取到的資料
maxline:所規定的一行的長度
返回值說明:
*/
ssize_t readLine (int sockfd , void *buf ,size_t maxline)
{
int ret;
int nRead = 0;
int left = maxline ; //剩下的位元組數
char * pbuf = (char *) buf ;
int count = 0;
while(1)
{
ret = read_peek ( sockfd, pbuf, left); //讀取長度為left的socket緩衝區內容
if(ret <0)
{
return ret;
}
nRead = ret;
int i = 0;
for(;i<nRead;++i)//看看讀取出來的資料中是否有換行符\n
{
if(pbuf[i]=='\n')//如果有換行符
{
ret = readn(sockfd , pbuf , i+1);//讀取一行
if(ret != i+1) //一定會讀到i+1個字元,否則是讀取出錯
{
exit(EXIT_FAILURE);
}
return ret + count ;
}
}
//窺探的資料中並沒有換行符
//把這段沒有\n的讀出來
ret = readn(sockfd , pbuf,nRead);
if(ret != nRead )
{
exit(EXIT_FAILURE);
}
pbuf += nRead;
left -= nRead;
count += nRead;
}
return -1;
}
#endif //__COMMON__