select模型(一 改進客戶端)

1、改程序使用select來改進客戶端對標準輸入和套接字輸入的處理,不然關閉服務器以後
循環中的內容都要被gets阻塞。原程序中http://www.javashuo.com/article/p-meszdlnu-kd.html,若服務器端先關閉發送FIN,客戶端處於CLOSE WAIT狀態,服務端到FIN_WAIT2。因爲程序阻塞在fgets,所以沒法到readline,也就沒法break循環從而調用close。全部套接口狀態不能再往前推動了。html

 

2、select管理多個I/O,一旦其中一個I/O或者多個I/O檢測出咱們所感興趣的事件,select函數返回
返回值是檢測到的事件個數。而且能夠返回哪些I/O發生了事件,進而遍歷這些事件去處理。服務器

 


3、int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
參數一、讀、寫、異常集合中的文件描述符的最大值+1(例如讀集合放入描述符3,4,5 ,寫集合放入7,9,異常集合填空,那麼nfds爲10
參數二、可讀集合(是一個輸入輸出參數,例如咱們對3,4,5描述符的讀感興趣,當3,5發生讀事件,select函數改變集合內容爲3,5返回)
參數五、超時時間
後四個參數是輸入輸出參數,參數2,3,4返回發生的I/O事件。參數5返回剩餘時間socket

 


4、修改描述符集的宏
void FD_CLR(int fd, fd_set *set);  //將fd描述符從集合set中移除
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);  //將fd描述符添加到集合set中
void FD_ZERO(fd_set *set);函數

 

下面利用select來改造以前的客戶端服務器程序,防止服務器端關閉,客戶端還在等stdin輸入,而不能退出。spa

客戶端程序:指針

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

#define ERR_EXIT(m)\
    do\
    {\
        perror(m);\
        exit(EXIT_FAILURE);\
    }while(0)
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)
            return (count-nleft);
        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;
            return -1;
        }else if(nwritten==0)
            continue;
        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);//從sockfd讀取內容到buf,但不去清空sockfd,偷窺
        if(ret==-1&&errno==EINTR)
            continue;
        return ret;
    }
}
//偷窺方案實現readline避免一次讀取一個字符
ssize_t readline(int sockfd,void * buf,size_t maxline)
{
    int ret;
    int nread;
    size_t nleft=maxline;
    char *bufp=(char*)buf;
    while(1)
    {
        ret=recv_peek(sockfd,bufp,nleft);//不清除sockfd,只是窺看
        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);//讀出sockfd中的一行而且清空
                if(ret!=i+1)
                    exit(EXIT_FAILURE);
                return ret;
            }
        }
        if(nread>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,1024);
        if(ret==-1)
            ERR_EXIT("readline");
        else if(ret==0)
        {
            printf("service closed\n");
            break;
        }
        fputs(recvbuf,stdout);
        memset(sendbuf,0,sizeof(sendbuf));
        memset(recvbuf,0,sizeof(recvbuf));
    }
*/
    char sendbuf[1024]={0};
    char recvbuf[1024]={0};    
    fd_set rset;
    FD_ZERO(&rset);//初始化
    int nready;//準備好的個數
    int maxfd;
    int fd=fileno(stdin);//爲什麼不使用STDIN_FILLENO(0),防止標準輸入被重定向
    if(fd>sock)
        maxfd=fd;
    else 
        maxfd=sock;
    while(1)
    {
        FD_SET(fd,&rset);//循環中。每次要從新設置rset。
        FD_SET(sock,&rset);
        nready=select(maxfd+1,&rset,NULL,NULL,NULL);
        if(nready==-1)
            ERR_EXIT("select error");
        if(nready==0)
            continue;
        if(FD_ISSET(sock,&rset))
        {
            int ret=readline(sock,recvbuf,sizeof(recvbuf));
            if(ret==-1)
                ERR_EXIT("readline error");
            else if(ret==0)
            {
                ERR_EXIT("serve closed");
                break;
            }
            fputs(recvbuf,stdout);
            memset(recvbuf,0,sizeof(recvbuf));
        }
        if(FD_ISSET(fd,&rset))
        {
            if(fgets(sendbuf,sizeof(sendbuf),stdin)==NULL)
                break;
            writen(sock,sendbuf,strlen(sendbuf));
            memset(sendbuf,0,sizeof(sendbuf));
        }
    }
    close(sock);//當服務器端關閉,客戶端readline讀取到FIN退出循環,執行close
    
}
void handle_sigpipe(int sig)
{
    printf("recive a signal=%d\n",sig);

}
int main(void)
{
        signal(SIGPIPE,handle_sigpipe);//捕捉第二次write的SIGPIPE信號,默認終止進程
        int sock;
        if((sock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0)
            ERR_EXIT("socket error");
    
        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");//服務器段地址
        //inet_aton("127.0.0.1",&servaddr.sin_addr);
    
        if(connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
            ERR_EXIT("connect");

        //利用getsockname獲取客戶端自己地址和端口,即爲對方accept中的對方套接口
        struct sockaddr_in localaddr;
        socklen_t addrlen=sizeof(localaddr);
        if(getsockname(sock,(struct sockaddr *)&localaddr,&addrlen)<0)
            ERR_EXIT("getsockname error");
        printf("local IP=%s, local port=%d\n",inet_ntoa(localaddr.sin_addr),ntohs(localaddr.sin_port));    
        //使用getpeername獲取對方地址
    echo_cli(sock);//選擇一個與服務器通訊
    return 0;
}

 

 

 

服務器程序不變,先關閉服務器程序,客戶端也能正常退出了。(客戶端select同時監測套接口和標準輸入的讀事件)code

/*
主動關閉服務器端,客戶端不會再while(fgets())處阻塞,而是會
接收到服務器的FIN從而進入TIME_WAIT狀態

*/

#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
#include<stdlib.h>
#include<stdio.h>
#include<errno.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<signal.h>
#include<sys/wait.h>
#define ERR_EXIT(m)\
    do\
    {\
        perror(m);\
        exit(EXIT_FAILURE);\
    }while(0)
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)
            return (count-nleft);
        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;
            return -1;
        }else if(nwritten==0)
            continue;
        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);//從sockfd讀取內容到buf,但不去清空sockfd,偷窺
        if(ret==-1&&errno==EINTR)
            continue;
        return ret;
    }
}
//偷窺方案實現readline避免一次讀取一個字符
ssize_t readline(int sockfd,void * buf,size_t maxline)
{
    int ret;
    int nread;
    size_t nleft=maxline;
    char *bufp=(char*)buf;
    while(1)
    {
        ret=recv_peek(sockfd,bufp,nleft);//不清除sockfd,只是窺看
        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);//讀出sockfd中的一行而且清空
                if(ret!=i+1)
                    exit(EXIT_FAILURE);
                return ret;
            }
        }
        if(nread>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)
{
        int ret;
        char recvbuf[1024];
        while(1)
        {
            memset(&recvbuf,0,sizeof(recvbuf));
            //使用readn以後客戶端發送的數據不足1024會阻塞
            //在客戶端程序中肯定消息的邊界,發送定長包
            ret=readline(conn,recvbuf,1024);                                                               
            //客戶端關閉
            if(ret==-1)
                ERR_EXIT("readline");            
            else if(ret==0)
            {
                printf("client close\n");
                break;//不用繼續循環等待客戶端數據
            }
            fputs(recvbuf,stdout);
            writen(conn,recvbuf,strlen(recvbuf));
        }
}
void handle_sigchld(int sig)
{
    
    while(waitpid(-1,NULL, WNOHANG)>0)
        ;
        
}
int main(void)
{    
    
    signal(SIGCHLD,handle_sigchld);
    int listenfd;
    if((listenfd=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0)
        ERR_EXIT("socket error");
    //if((listenfd=socket(PF_INET,SOCK_STREAM,0))<0)
    

    //本地協議地址賦給一個套接字
    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);//表示本機地址

    //開啓地址重複使用,關閉服務器再打開不用等待TIME_WAIT
    int on=1;
    if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0)
        ERR_EXIT("setsockopt error");
    //綁定本地套接字
    if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
        ERR_EXIT("bind error");
    if(listen(listenfd,SOMAXCONN)<0)//設置監聽套接字(被動套接字)
        ERR_EXIT("listen error");
    
    struct sockaddr_in peeraddr;//對方套接字地址
    socklen_t peerlen=sizeof(peeraddr);
    int conn;//已鏈接套接字(主動套接字)
    pid_t pid;
    while(1){
        if((conn=accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen))<0)
            ERR_EXIT("accept error");
        //鏈接好以後就構成鏈接,端口是客戶端的。peeraddr是對端
        printf("ip=%s port=%d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
        pid=fork();
        if(pid==-1)
            ERR_EXIT("fork");
        if(pid==0){    
                close(listenfd);
                echo_srv(conn);
                //某個客戶端關閉,結束該子進程,不然子進程也去接受鏈接
                //雖然結束了exit退出,可是內核還保留了其信息,父進程並未爲其收屍。
                exit(EXIT_SUCCESS);
        }else     close(conn);
    }
    return 0;
}
相關文章
相關標籤/搜索