Linux下網絡socket編程——實現服務器(select)與多個客戶端通信


一、關於socket通信

服務器端工作流程:

  • 調用 socket() 函數創建套接字 用 bind() 函數將創建的套接字與服務端IP地址綁定
  • 調用listen()函數監聽socket() 函數創建的套接字,等待客戶端連接 當客戶端請求到來之后
  • 調用 accept()函數接受連接請求,返回一個對應於此連接的新的套接字,做好通信准備
  • 調用 write()/read() 函數和 send()/recv()函數進行數據的讀寫,通過 accept() 返回的套接字和客戶端進行通信 關閉socket(close)

客戶端工作流程:

  • 調用 socket() 函數創建套接字
  • 調用 connect() 函數連接服務端
  • 調用write()/read() 函數或者 send()/recv() 函數進行數據的讀寫
  • 關閉socket(close)

 

二、用select實現服務器端編程:

select函數樓主在之前文章中(select函數用法)已經提及,不在多做綴述。下面貼上服務器端代碼servce.c

#include <stdio.h>
#include <netinet/in.h>   //for souockaddr_in
#include <sys/types.h>      
#include <sys/socket.h>
#include <errno.h>
#include <stdlib.h>

#include <arpa/inet.h>

//for select
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/select.h>

#include <strings.h>   //for bzero
#include <string.h>

#define BUFF_SIZE 1024
#define backlog 7
#define ser_port 11277
#define CLI_NUM 3


int client_fds[CLI_NUM];

int main(int agrc,char **argv)
{
    int ser_souck_fd;
    int i;   
    char input_message[BUFF_SIZE];
    char resv_message[BUFF_SIZE];


    struct sockaddr_in ser_addr;
    ser_addr.sin_family= AF_INET;    //IPV4
    ser_addr.sin_port = htons(ser_port); 
    ser_addr.sin_addr.s_addr = INADDR_ANY;  //指定的是所有地址

    //creat socket
    if( (ser_souck_fd = socket(AF_INET,SOCK_STREAM,0)) < 0 )
    {
        perror("creat failure");
        return -1;
    } 

    //bind soucket
    if(bind(ser_souck_fd, (const struct sockaddr *)&ser_addr,sizeof(ser_addr)) < 0)
    {
        perror("bind failure");
        return -1;
    }

    //listen
    if(listen(ser_souck_fd, backlog) < 0) 
    {
        perror("listen failure"); 
        return -1;
    }


    //fd_set
    fd_set ser_fdset;
    int max_fd=1;
    struct timeval mytime;
    printf("wait for client connnect!\n");

    while(1)
    {
        mytime.tv_sec=27;
        mytime.tv_usec=0;

        FD_ZERO(&ser_fdset);

        //add standard input
        FD_SET(0,&ser_fdset);
        if(max_fd < 0)
        {
            max_fd=0; 
        }

        //add serverce
        FD_SET(ser_souck_fd,&ser_fdset);
        if(max_fd < ser_souck_fd)
        {
            max_fd = ser_souck_fd;
        }

        //add client 
        for(i=0;i<CLI_NUM;i++)  //用數組定義多個客戶端fd
        {
            if(client_fds[i]!=0) 
            {
                FD_SET(client_fds[i],&ser_fdset);
                if(max_fd < client_fds[i])
                {
                    max_fd = client_fds[i]; 
                }
            }
        }

        //select多路復用
        int ret = select(max_fd + 1, &ser_fdset, NULL, NULL, &mytime);

        if(ret < 0)    
        {    
            perror("select failure\n");    
            continue;    
        }    

        else if(ret == 0)
        {
            printf("time out!");
            continue;
        }

        else
        {
            if(FD_ISSET(0,&ser_fdset)) //標准輸入是否存在於ser_fdset集合中(也就是說,檢測到輸入時,做如下事情)
            {
                printf("send message to");
                bzero(input_message,BUFF_SIZE);
                fgets(input_message,BUFF_SIZE,stdin);

                for(i=0;i<CLI_NUM;i++)
                {
                    if(client_fds[i] != 0)
                    {
                        printf("client_fds[%d]=%d\n", i, client_fds[i]);
                        send(client_fds[i], input_message, BUFF_SIZE, 0);
                    }
                }

            }

            if(FD_ISSET(ser_souck_fd, &ser_fdset)) 
            {
                struct sockaddr_in client_address;
                socklen_t address_len;
                int client_sock_fd = accept(ser_souck_fd,(struct sockaddr *)&client_address, &address_len);
                if(client_sock_fd > 0)
                {
                    int flags=-1;
                    //一個客戶端到來分配一個fd,CLI_NUM=3,則最多只能有三個客戶端,超過4以后跳出for循環,flags重新被賦值為-1
                    for(i=0;i<CLI_NUM;i++)
                    {
                        if(client_fds[i] == 0)
                        {
                            flags=i; 
                            client_fds[i] = client_sock_fd;
                            break;
                        }
                    }


                    if (flags >= 0)
                    {
                        printf("new user client[%d] add sucessfully!\n",flags);

                    }

                    else //flags=-1
                    {   
                        char full_message[]="the client is full!can't join!\n";
                        bzero(input_message,BUFF_SIZE);
                        strncpy(input_message, full_message,100);
                        send(client_sock_fd, input_message, BUFF_SIZE, 0);

                    }
                }    
            }

        }

        //deal with the message

        for(i=0; i<CLI_NUM; i++)
        {
            if(client_fds[i] != 0)
            {
                if(FD_ISSET(client_fds[i],&ser_fdset))
                {
                    bzero(resv_message,BUFF_SIZE);
                    int byte_num=read(client_fds[i],resv_message,BUFF_SIZE);
                    if(byte_num > 0)
                    {
                        printf("message form client[%d]:%s\n", i, resv_message);
                    }
                    else if(byte_num < 0)
                    {
                        printf("rescessed error!");
                    }

                    //某個客戶端退出
                    else  //cancel fdset and set fd=0
                    {
                        printf("clien[%d] exit!\n",i);
                        FD_CLR(client_fds[i], &ser_fdset);
                        client_fds[i] = 0;
                       // printf("clien[%d] exit!\n",i);
                        continue;  //這里如果用break的話一個客戶端退出會造成服務器也退出。  
                    }
                }
            }
        }    
    }
    return 0;
}

select實現多路復用,多路復用,顧名思義,就是說各做各的事,標准輸入事件到來,有相關函數處理。服務器處理服務器的事件,客戶端到來時有相關函數對其進行處理,通過select遍歷各fd的讀寫情況,就不用擔心阻塞了。

三、用epoll實現客戶端編程:

1、客戶端程序(epoll_client.c):

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

#include <sys/epoll.h>
#include <errno.h>
#include <fcntl.h>

#define BUFFER_SIZE 1024    

int main(int argc, const char * argv[])    
{   
    int i,n;
    int connfd,sockfd;
    struct epoll_event ev,events[20]; //ev用於注冊事件,數組用於回傳要處理的事件
    int epfd=epoll_create(256);//創建一個epoll的句柄,其中256為你epoll所支持的最大句柄數

    struct sockaddr_in client_addr;
    struct sockaddr_in server_addr;    

    server_addr.sin_family = AF_INET;    
    server_addr.sin_port = htons(11277);    
    server_addr.sin_addr.s_addr =INADDR_ANY;    
    bzero(&(server_addr.sin_zero), 8);    

    int server_sock_fd = socket(AF_INET, SOCK_STREAM, 0);  

    ev.data.fd=server_sock_fd;//設置與要處理的事件相關的文件描述符
    ev.events=EPOLLIN|EPOLLET;//設置要處理的事件類型
    epoll_ctl(epfd,EPOLL_CTL_ADD,server_sock_fd,&ev);//注冊epoll事件

    if(server_sock_fd == -1)    
    {    
        perror("socket error");    
        return 1;    
    }    

    char recv_msg[BUFFER_SIZE];    
    char input_msg[BUFFER_SIZE];    

    if(connect(server_sock_fd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr_in)) == 0)    
    {    
        for(;;)
        {
            int nfds=epoll_wait(epfd,events,20,500);//等待epoll事件的發生
            for(i=0;i<nfds;++i)
            {    
                if(events[i].events&EPOLLOUT) //有數據發送,寫socket
                {
                    bzero(input_msg, BUFFER_SIZE);    
                    fgets(input_msg, BUFFER_SIZE, stdin);    

                    sockfd = events[i].data.fd;
                    write(sockfd, recv_msg, n);

                    ev.data.fd=sockfd;
                    ev.events=EPOLLIN|EPOLLET;
                    epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&ev);
                }   

                else if(events[i].events&EPOLLIN)//有數據到來,讀socket
                {
                    bzero(recv_msg, BUFFER_SIZE);
                    if((n = read(server_sock_fd, recv_msg, BUFFER_SIZE)) <0 )
                    {
                        printf("read error!");
                    }

                    ev.data.fd=server_sock_fd;
                    ev.events=EPOLLOUT|EPOLLET;
                    printf("%s\n",recv_msg);
                }

            }        
        }
    }    
    return 0;    
}   

2、關於epoll函數:

相比於select,epoll最大的好處在於它不會隨着監聽fd數目的增長而降低效率。因為在內核中的select實現中,它是采用輪詢來處理的,輪詢的fd數目越多,自然耗時越多。並且,在linux/posix_types.h頭文件有這樣的聲明:
#define __FD_SETSIZE 1024
表示select最多同時監聽1024個fd

一共三個函數:

1、 int epoll_create (int size); 
創建一個epoll的句柄

size用來告訴內核這個監聽的數目一共有多大。這個參數不同於select()中的第一個參數,給出最大監聽的fd+1的值。需要注意的是,當創建好epoll句柄后,它就是會占用一個fd值,在linux下如果查看/proc/進程id/fd/,是能夠看到這個fd的,所以在使用完epoll后,必須調用close()關閉,否則可能導致fd被耗盡。

2、 int epoll_ctl (int epfd , int op, int fd, struct epoll_event *event);

epoll的事件注冊函數,它不同與select()是在監聽事件時告訴內核要監聽什么類型的事件,而是在這里先注冊要監聽的事件類型。第一個參數是epoll_create()的返回值,第二個參數表示動作,用三個宏來表示:

EPOLL_CTL_ADD:注冊新的fd到epfd中;

EPOLL_CTL_MOD:修改已經注冊的fd的監聽事件;

EPOLL_CTL_DEL:從epfd中刪除一個fd;

第三個參數是需要監聽的fd

第四個參數是告訴內核需要監聽什么事,struct epoll_event結構如下:

struct epoll_event
 {
    __uint32_t events;    /* Epoll events */
    epoll_data_t data;     /* User data variable */
};

  

typedef union epoll_data
 {
    void *ptr;
    int fd;
    __uint32_t u32;
    __uint64_t u64;
} epoll_data_t;

events可以是以下幾個宏的集合:

  • EPOLLIN :表示對應的文件描述符可以讀(包括對端SOCKET正常關閉);
  • EPOLLOUT:表示對應的文件描述符可以寫;
  • EPOLLPRI:表示對應的文件描述符有緊急的數據可讀(這里應該表示有帶外數據到來);
  • EPOLLERR:表示對應的文件描述符發生錯誤;
  • EPOLLHUP:表示對應的文件描述符被掛斷;
  • EPOLLET: 將EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來說的。
  • EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之后,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列里
3、 int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

等待事件的產生,類似於select()調用。

參數events用來從內核得到事件的集合,

maxevents告之內核這個events有多大,這個 maxevents的值不能大於創建epoll_create()時的size,

參數timeout是超時時間(毫秒,0會立即返回,-1將不確定,也有說法說是永久阻塞)。該函數返回需要處理的事件數目,如返回0表示已超時。

使用步驟:

<1>首先通過create_epoll(int maxfds)來創建一個epoll的句柄,其中maxfds為你epoll所支持的最大句柄數。這個函數會返回一個新的epoll句柄,之后的所有操作將通過這個句柄來進行操作。在用完之后,記得用close()來關閉這個創建出來的epoll句柄。

<2>然后每一幀的調用epoll_wait (int epfd, epoll_event events, int max events, int timeout) 來查詢所有的網絡接口。

<3>kdpfd為用epoll_create創建之后的句柄,events是一個epoll_event*的指針,當epoll_wait這個函數操作成功之后,epoll_events里面將儲存所有的讀寫事件。max_events是當前需要監聽的所有socket句柄數。最后一個timeout是 epoll_wait的超時,為0的時候表示馬上返回,為-1的時候表示一直等下去,直到有事件范圍,為任意正整數的時候表示等這么長的時間,如果一直沒有事件,則返回。一般如果網絡主循環是單獨的線程的話,可以用-1來等,這樣可以保證一些效率,如果是和主邏輯在同一個線程的話,則可以用0來保證主循環的效率。 epoll_wait返回之后應該是一個循環,遍歷所有的事件。

基本上都是如下的框架:

for( ; ; )
    {
        nfds = epoll_wait(epfd,events,20,500);
        for(i=0;i<nfds;++i)
        {
            if(events[i].data.fd==listenfd) //有新的連接
            {
                     connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen);    //accept這個連接
                     ev.data.fd=connfd;
                     ev.events=EPOLLIN|EPOLLET;
                     epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //將新的fd添加到epoll的監聽隊列中
            }

           else if( events[i].events&EPOLLIN ) //接收到數據,讀socket
            {
                     n = read(sockfd, line, MAXLINE)) < 0    //讀
                     ev.data.ptr = md;     //md為自定義類型,添加數據
                     ev.events=EPOLLOUT|EPOLLET;
                     epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改標識符,等待下一個循環時發送數據,異步處理的精髓
            }
            else if(events[i].events&EPOLLOUT) //有數據待發送,寫socket
            {
                     struct myepoll_data* md = (myepoll_data*)events[i].data.ptr;    //取數據
                     sockfd = md->fd;
                     send( sockfd, md->ptr, strlen((char*)md->ptr), 0 );        //發送數據
                     ev.data.fd=sockfd;
                     ev.events=EPOLLIN|EPOLLET;
                     epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改標識符,等待下一個循環時接收數據
            }
            else
            {
                     //其他的處理
            }
        }
    }

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM