一、在使用select時,我們需要了解linux的五種IO模型和TCP的11種狀態:
1、阻塞IO:recv接收緩衝區有數據後,就會解除阻塞。
2、非阻塞IO:忙等待 fcntl(fd,F_SETFL,flag|O_NONBLOCK);內核中沒有數據時,recv會返回-1,不會阻塞
3、IO複用(select和poll):一旦有一個文件描述符檢測到有文件過來,select就返回,(阻塞提前到selete處)recv就可以直接從內核空間得到數據。
4、信號驅動IO:以信號方式通知應用進程,有數據到來(信號是異步處理一種方式)應用進程調用recv將數據從內核空間拉到用戶空間--效率沒有異步IO高
5、異步IO :aio_read沒有數據到來,這個函數也會立刻返回,有數據到來,內核則將數據拷貝到應用層緩衝區,拷貝完成通過信號通知用戶。
TCP的11種狀態:
還有一種叫CLOSING狀態,產生原因是雙方同時關閉,客戶端會處於FIN_WAIT_1狀態,服務器端也處於FIN_WAIT_1狀態,雙方均在等待的狀態就是CLOSING狀態,收到對方ACK後,就會處於TIME_WAIT狀態,
二、select也會阻塞,相比於阻塞IOselect優點在哪裏?
當我們kill掉服務端的連接進程後,發現服務端處於FIN_WAIT2,不能立刻結束。
原因是客戶端程序阻塞在了標準輸入位置,沒有機會調用close,因此導致服務端不能立刻結束。本質就是因爲從鍵盤接收數據和從網絡接收數據沒有辦法同時處理。這時用selete來進行管理,管理標準輸入IO和套接口IO。
用select便可以管理多個IO,一旦其中一個IO或者多個IO檢測到我們所感興趣的時間,select函數返回,返回值是檢測到的事件個數,並且返回那些IO發生了事件。這樣用戶可以遍歷這些事件去處理這些事件。
其次,服務端使用多個進程處理多個客戶端連接,能不能使用一個進程來處理?
三、select使用
參數含義:
nfds:讀、寫、異常集合中最大文件描述符值+1
readfds 可讀的集合,輸入輸出參數
writefds 可寫的集合,輸入輸出參數
exceptfds 異常集合,輸入輸出參數
timeout 超時時間結構體 填NULL,只有檢測到某個事件才返回,填寫超時時間,沒有事件到來,超時時間到後就返回事件個數0,失敗返回-1,輸入輸出參數。
FD_CLR:將文件描述符從集合中移除
FD_ISSET:判斷fd是否在集合中
FD_SET:將fd添加到集合中
FD_ZERO:清空集合
四、代碼
客戶端:
#include<unistd.h>//read/write
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>//信號
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) \
do\
{\
perror(m);\
exit(EXIT_FAILURE);\
}while (0)
//ssize_t 有符號整數
//size_t 無符號整數
ssize_t readn(int fd,void *buf,size_t count)
{
size_t nleft = count;//剩餘字節數
ssize_t nread;//已接收字節數
char *bufp = (char*) buf;
while(nleft>0)
{
if((nread = read(fd,bufp,nleft))<0)
{
if(errno == EINTR)//被信號中斷
{
continue;
}
else
return -1;
}
else if(nread == 0)//對等方關閉
{
count = count - nleft;//已經讀取的字節數
break;
}
else
{
bufp += nread;
nleft -= nread;
}
}
return count;
}
ssize_t writen(int fd,const void *buf,size_t count)
{
size_t nleft = count;//剩餘字節數
ssize_t nwritten;//已接收字節數
char *bufp = (char*) buf;
while(nleft>0)
{
if((nwritten = write(fd,bufp,nleft))<0)
{
if(errno == EINTR)//被信號中斷
{
continue;
}
else
return -1;
}
else if(nwritten == 0)//對等方關閉
{
continue;
}
else
{
bufp += nwritten;
nleft -= nwritten;
}
}
return count;
}
//從套接口接收數據,但並不把數據從緩衝區清除
ssize_t recv_peek(int sockfd,void *buf,int len)
{
while(1)
{
int ret = recv(sockfd,buf,len,MSG_PEEK);
if((ret == -1) && (errno == EINTR))
continue;
printf("recv_peek :ret = %d,errno = %d\n",ret,errno);
return ret;
}
}
//偷窺方案:
ssize_t readline(int sockfd,void *buf,size_t maxlen)//一行最大的字節數
{//只要遇到/n就返回
int ret;
int nread;
char *bufp = (char *)buf;
int nleft = maxlen;
while(1)
{
ret = recv_peek(sockfd,bufp,nleft);
if(ret<0)
{
return ret;
}
else if(ret == 0)//表示對方關閉了套接口
{
return ret;
}
nread = ret;
int i;
for(i =0; i<nread;i++)
{
if(bufp[i] == '\n')//如果找到了結束符就將數據讀取出來
{
ret = readn(sockfd,bufp,i+1);//下標i,總共有i+1個字符
if(ret != i+1)
exit(EXIT_FAILURE);
return ret;
}
}
//如果沒有找到結束符,就讀出來先緩存起來
if(nread > nleft)//ret = recv_peek(sockfd,bufp,nleft);不可能
exit(EXIT_FAILURE);
nleft -= nread;//剩餘的字節數
ret = readn(sockfd,bufp,nread);
if(ret != nread)//偷窺到的數據是可以全部讀取出來的
{
exit(EXIT_FAILURE);
}
bufp += nread;
}
return -1;
}
void echo_cli(int sock)
{
/* char sendbuf[1024] = {0};
char recvbuf[1024] = {0};
while(fgets(sendbuf,sizeof(sendbuf),stdin)!=NULL)
{
writen(sock,sendbuf,strlen(sendbuf));//發送
int ret = readline(sock,recvbuf,sizeof(recvbuf));
if(ret == -1)
{
ERR_EXIT("readline");
}
else if(ret == 0)
{
printf("client_close\n");
break;
}
fputs(recvbuf,stdout);//打印
memset(recvbuf,0,sizeof(recvbuf));
memset(sendbuf,0,sizeof(sendbuf));
}
close(sock);
*/
//用select統一管理標準輸入IO與套接口IO
fd_set rset;
FD_ZERO (&rset);
int nready;
int fd_stdin = fileno(stdin);
//標準輸入的文件描述符,通過fileno獲取,
//不能直接用STD_FILENO這個宏,
//因爲不能確保標準輸入不被重定向
//還有一個文件描述符爲sock
int maxfd = fd_stdin?fd_stdin>sock:sock;
char sendbuf[1024] = {0};
char recvbuf[1024] = {0};
while(1)
{
FD_SET(fd_stdin,&rset);
FD_SET(sock,&rset);
nready = select(maxfd+1,&rset,NULL,NULL,NULL);
if(nready == -1)
ERR_EXIT("select");
if(nready == 0)
continue;
if(FD_ISSET(sock,&rset))
{
int ret = readline(sock,recvbuf,sizeof(recvbuf));
if(ret == -1)
{
ERR_EXIT("readline");
}
else if(ret == 0)
{
printf("srv_close\n");
break;
}
fputs(recvbuf,stdout);//打印
memset(recvbuf,0,sizeof(recvbuf));
}
if(FD_ISSET(fd_stdin,&rset))
{
if(fgets(sendbuf,sizeof(sendbuf),stdin)==NULL)
break;
writen(sock,sendbuf,strlen(sendbuf));//發送
memset(sendbuf,0,sizeof(sendbuf));
}
}
close(sock);//顯示關閉套接口
}
void handle_sigpipe(int sig)
{
printf("recv a sig = %d\n",sig);
}
int main(void)
{
/*
signal(SIGPIPE,handle_sigpipe);
*/
signal(SIGPIPE,SIG_IGN);//當服務端down掉,客戶端發送數據後tcp協議棧會產生該信號
int sock;//被動套接字
if( (sock = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0)//創建套接字小於0表示失敗
ERR_EXIT("socket");
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");//指定服務器端地址
if(connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
ERR_EXIT("connect");
echo_cli(sock);
return 0;
}
服務端:
#include<unistd.h>//read/write
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>//信號
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) \
do\
{\
perror(m);\
exit(EXIT_FAILURE);\
}while (0)
//ssize_t 有符號整數
//size_t 無符號整數
ssize_t readn(int fd,void *buf,size_t count)
{
size_t nleft = count;//剩餘字節數
ssize_t nread;//已接收字節數
char *bufp = (char*) buf;
while(nleft>0)
{
if((nread = read(fd,bufp,nleft))<0)
{
if(errno == EINTR)//被信號中斷
{
continue;
}
else
return -1;
}
else if(nread == 0)//對等方關閉
{
count = count - nleft;//已經讀取的字節數
break;
}
else
{
bufp += nread;
nleft -= nread;
}
}
return count;
}
ssize_t writen(int fd,const void *buf,size_t count)
{
size_t nleft = count;//剩餘字節數
ssize_t nwritten;//已接收字節數
char *bufp = (char*) buf;
while(nleft>0)
{
if((nwritten = write(fd,bufp,nleft))<0)
{
if(errno == EINTR)//被信號中斷
{
continue;
}
else
{
return -1;
}
}
else if(nwritten == 0)//對等方關閉
{
continue;
}
else
{
bufp += nwritten;
nleft -= nwritten;
}
}
return count;
}
//從套接口接收數據,但並不把數據從緩衝區清除
ssize_t recv_peek(int sockfd,void *buf,size_t len)
{
while(1)
{
int ret = recv(sockfd,buf,len,MSG_PEEK);
if((ret == -1) && (errno == EINTR))
continue;
printf("recv_peek :ret = %d,errno = %d\n",ret,errno);
return ret;
}
}
//偷窺方案:
ssize_t readline(int sockfd,void *buf,size_t maxline)//一行最大的字節數
{//只要遇到/n就返回
int ret;
int nread;
char *bufp = (char *)buf;
int nleft = maxline;
while(1)
{
ret = recv_peek(sockfd,bufp,nleft);
if(ret<0)
return ret;
else if(ret == 0)//表示對方關閉了套接口
return ret;
nread = ret;
int i;
for(i =0; i<nread;i++)
{
if(bufp[i] == '\n')//如果找到了結束符就將數據讀取出來
{
ret = readn(sockfd,bufp,i+1);//下標i,總共有i+1個字符
if(ret != i+1)
exit(EXIT_FAILURE);
return ret;
}
}
//如果沒有找到結束符,就讀出來先緩存起來
if(nread > nleft)//ret = recv_peek(sockfd,bufp,nleft);不可能
exit(EXIT_FAILURE);
nleft -= nread;//剩餘的字節數
ret = readn(sockfd,bufp,nread);
if(ret != nread)//偷窺到的數據是可以全部讀取出來的
{
exit(EXIT_FAILURE);
}
bufp += nread;
}
return -1;
}
void echo_srv(int conn)
{
char recvbuf[1024];
while(1)
{
memset(recvbuf,0,sizeof(recvbuf));
int ret = readline(conn,recvbuf,1024);//按行接收
if(ret == -1)
{
ERR_EXIT("readline");
}
if(ret == 0)
{
printf("client_close\n");
break;
}
fputs(recvbuf,stdout);//打印
writen(conn,recvbuf,strlen(recvbuf));//回射-這裏!!
}
}
int main(void)
{
signal(SIGCHLD,SIG_IGN);//處理殭屍進程
int listenfd;//被動套接字
if( (listenfd = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0)//創建套接字小於0表示失敗
/* if( (listenfd = socket(PF_INET,SOCK_STREAM,0))<0);*///讓內核自己選定協議
ERR_EXIT("socket");
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//表示本機的任意地址
/*servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");*/
/*inet_aton("127.0.0.1",&servaddr.sin_addr);*/
int on = 1;//開啓地址重複利用
if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0)
ERR_EXIT("setsockopt");
if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
ERR_EXIT("bind");
if(listen(listenfd,SOMAXCONN)<0)//監聽後變爲被動套接字
ERR_EXIT("listen");
struct sockaddr_in peeraddr;
socklen_t peerlen;
int conn;//主動套接字
/*
//父子進程可以共享文件描述符
pid_t pid;
while(1)
{
if((conn = accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen))<0)
ERR_EXIT("accept");
printf("ip=%s,port=%d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
//也可以使用select實現併發服務器
pid = fork();//創建進程實現併發處理
if(pid == -1)
ERR_EXIT("frok");
if(pid == 0)
{//子進程不需要處理監聽套接字
close(listenfd);
echo_srv(conn);
exit(EXIT_SUCCESS);//如果通信結束(客戶端關閉)直接結束進程,否則子進程也會去accept
}
else
{//父進程不需要處理連接套接字
close(conn);
}
}
*/
//select單進程處理併發
int i;
//select中fd_set集合的限制FD_SETSIZE
int client[FD_SETSIZE];//緩存select返回的有時間到來的套接口
for(i = 0;i<FD_SETSIZE;i++)
{
client[i] = -1;//空閒
}
int nready;
/*文件描述符
0 標準輸入
1 標準輸出
2 標準錯誤*/
int maxfd = listenfd;//第3個套接字就是監聽套接字,也是最大的套接字
fd_set rset;//讀的集合
fd_set allset;//所有集合
FD_ZERO(&rset);
FD_ZERO(&allset);
//將監聽套接口放到allset中
FD_SET(listenfd,&allset);
while(1)
{
rset = allset;
nready = select(maxfd+1,&rset,NULL,NULL,NULL);//寫、異常、超時均不關心//超時時間爲NULL不可能返回零
if(nready == -1)
{
if(errno == EINTR)
continue;
ERR_EXIT("select");
}
if(nready == 0)
continue;
if(FD_ISSET(listenfd,&rset))//是否是監聽套接口產生事件
{//起初集合中只有一個監聽套接口,不用循環
peerlen = sizeof(peeraddr);//一定要有初始值
//這裏有個問題,就是雖然可以接收多個conn,但是多個conn會覆蓋
conn = accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen);
if(conn == -1)
ERR_EXIT("accept");
for(i = 0;i<FD_SETSIZE;i++)
{
if(client[i]<0)//找到空閒位置將conn存儲進去
{
client[i] = conn;
break;
}
}
if(i == FD_SETSIZE)
{
fprintf(stderr,"too many clients\n");
exit(EXIT_FAILURE);
}
printf("ip=%s,port=%d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
//獲得了套接口conn,下次循環我們也要關心conn的可讀事件
FD_SET(conn,&allset);//不斷運行,添加到數組中的套接口就會越來越多
if(conn>maxfd)
maxfd = conn;
if(--nready <= 0)
continue;
}
for(i = 0;i<FD_SETSIZE;i++)
{
conn = client[i];//已連接套接口
if(conn == -1)
continue;
if(FD_ISSET(conn,&rset))//產生可讀事件
{
char recvbuf[1024] = {0};
int ret = readline(conn,recvbuf,1024);//按行接收
if(ret == -1)
{
ERR_EXIT("readline");
}
if(ret == 0)
{
printf("client_close\n");
//如果對方關閉,則從集合中清除,不再關心它的可讀事件
FD_CLR(conn,&allset);
client[i] = -1;
}
fputs(recvbuf,stdout);//打印
writen(conn,recvbuf,strlen(recvbuf));//回射-這裏!!
if(--nready <= 0)//所有事件都處理完畢退出。
break;
}
}
}
return 0;
}
我們改造客戶端的echo_cli函數使得客戶端可以同時處理多個IO事件,改造服務端,使得服務端可以以一個進程處理多個客戶端連接,最終