Linux C++實現一服務器與多客戶端之間的通信


通過網絡查找資料得到的都是一些零碎不成體系的知識點,無法融會貫通。而且需要篩選有用的信息,這需要花費大量的時間。所以把寫代碼過程中用到的相關知識的博客鏈接附在用到的位置,方便回顧。

1.程序流程

  • 服務器端:socker()建立套接字,綁定(bind)並監聽(listen),用accept()等待客戶端連接。
  • 客戶端:socker()建立套接字,連接(connect)服務器,連接上后使用send()和recv(),在套接字上寫讀數據,直至數據交換完畢,close()關閉套接字。

2.實現

具體實現上使用select函數Linux Select
在收發信息的時候,端口是會被占用的,也就是處於阻塞狀態。例如這個例子UDP 組播 實例,只能實現一對一的通信。
在Linux中,我們可以使用select函數實現I/O端口的復用,傳遞給 select函數的參數會告訴內核:

  • 我們所關心的文件描述符

  • 對每個描述符,我們所關心的狀態。(我們是要想從一個文件描述符中讀或者寫,還是關注一個描述符中是否出現異常)

  • 我們要等待多長時間。(我們可以等待無限長的時間,等待固定的一段時間,或者根本就不等待)

從 select函數返回后,內核告訴我們一下信息:

  • 對我們的要求已經做好准備的描述符的個數

  • 對於三種條件哪些描述符已經做好准備.(讀,寫,異常)

有了這些返回信息,我們可以調用合適的I/O函數(通常是 read 或 write),並且這些函數不會再阻塞.

select函數介紹

select函數原型如下:

int select (int maxfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

select系統調用是用來讓我們的程序監視多個文件句柄(socket 句柄)的狀態變化的。程序會停在select這里等待,直到被監視的文件句柄有一個或多個發生了狀態改變。返回:做好准備的文件描述符的個數,超時為0,錯誤為 -1.

具體參數的解釋

  1. intmaxfdp是一個整數值,是指集合中所有文件描述符的范圍,即所有文件描述符的最大值加1,不能錯。

說明:對於這個原理的解釋可以看下邊fd_set的詳細解釋,fd_set是以位圖的形式來存儲這些文件描述符。maxfdp也就是定義了位圖中有效的位的個數。

  1. fd_set*readfds是指向fd_set結構的指針,這個集合中應該包括文件描述符,我們是要監視這些文件描述符的讀變化的,即我們關心是否可以從這些文件中讀取數據了,如果這個集合中有一個文件可讀,select就會返回一個大於0的值,表示有文件可讀;如果沒有可讀的文件,則根據timeout參數再判斷是否超時,若超出timeout的時間,select返回0,若發生錯誤返回負值。可以傳入NULL值,表示不關心任何文件的讀變化。

  2. fd_set*writefds是指向fd_set結構的指針,這個集合中應該包括文件描述符,我們是要監視這些文件描述符的寫變化的,即我們關心是否可以向這些文件中寫入數據了,如果這個集合中有一個文件可寫,select就會返回一個大於0的值,表示有文件可寫,如果沒有可寫的文件,則根據timeout參數再判斷是否超時,若超出timeout的時間,select返回0,若發生錯誤返回負值。可以傳入NULL值,表示不關心任何文件的寫變化。

  3. fd_set*errorfds同上面兩個參數的意圖,用來監視文件錯誤異常文件。

  4. structtimeval* timeout是select的超時時間,這個參數至關重要,它可以使select處於三種狀態,第一,若將NULL以形參傳入,即不傳入時間結構,就是將select置於阻塞狀態,一定等到監視文件描述符集合中某個文件描述符發生變化為止;第二,若將時間值設為0秒0毫秒,就變成一個純粹的非阻塞函數,不管文件描述符是否有變化,都立刻返回繼續執行,文件無變化返回0,有變化返回一個正值;第三,timeout的值大於0,這就是等待的超時時間,即 select在timeout時間內阻塞,超時時間之內有事件到來就返回了,否則在超時后不管怎樣一定返回,返回值同上述。

詳細解釋

首先我們先看一下最后一個參數。它指明我們要等待的時間:

struct timeval{      
        long tv_sec;   /*秒 */
        long tv_usec;  /*微秒 */   
    }

tv_sec 代表多少秒
tv_usec 代表多少微秒 1000000 微秒 = 1秒
有三種情況:

timeout == NULL 等待無限長的時間。等待可以被一個信號中斷。當有一個描述符做好准備或者是捕獲到一個信號時函數會返回。如果捕獲到一個信號, select函數將返回 -1,並將變量 erro設為 EINTR。

timeout->tv_sec == 0 &&timeout->tv_usec == 0不等待,直接返回。加入描述符集的描述符都會被測試,並且返回滿足要求的描述符的個數。這種方法通過輪詢,無阻塞地獲得了多個文件描述符狀態。

timeout->tv_sec !=0 ||timeout->tv_usec!= 0 等待指定的時間。當有描述符符合條件或者超過超時時間的話,函數返回。在超時時間即將用完但又沒有描述符合條件的話,返回 0。對於第一種情況,等待也會被信號所中斷。

中間的三個參數 readset, writset, exceptset,指向描述符集。這些參數指明了我們關心哪些描述符,和需要滿足什么條件(可寫,可讀,異常)。一個文件描述集保存在 fd_set 類型中。fd_set類型變量每一位代表了一個描述符。我們也可以認為它只是一個由很多二進制位構成的數組。如下圖所示:
在這里插入圖片描述
Linux: fd_set用法
對於 fd_set類型的變量我們所能做的就是聲明一個變量,為變量賦一個同種類型變量的值,或者使用以下幾個宏來控制它:

#include <sys/select.h>   
int FD_ZERO(int fd, fd_set *fdset);   
int FD_CLR(int fd, fd_set *fdset);   
int FD_SET(int fd, fd_set *fd_set);   
int FD_ISSET(int fd, fd_set *fdset);

FD_ZERO宏將一個 fd_set類型變量的所有位都設為 0,使用FD_SET將變量的某個位置位。清除某個位時可以使用 FD_CLR,我們可以使用FD_ISSET來測試某個位是否被置位。

當聲明了一個文件描述符集后,必須用FD_ZERO將所有位置零。之后將我們所感興趣的描述符所對應的位置位,操作如下:

fd_set rset;   
int fd;   
FD_ZERO(&rset);   
FD_SET(fd, &rset);   
FD_SET(stdin, &rset);

select返回后,用FD_ISSET測試給定位是否置位:

if(FD_ISSET(fd, &rset)   
 
{ ... }

關於select模型

理解select模型的關鍵在於理解fd_set,為說明方便,取fd_set長度為1字節,fd_set中的每一bit可以對應一個文件描述符fd。則1字節長的fd_set最大可以對應8個fd。

(1)執行fd_set set;FD_ZERO(&set);則set用位表示是0000,0000。

(2)若fd=5,執行FD_SET(fd,&set);后set變為0001,0000(第5位置為1)

(3)若再加入fd=2,fd=1,則set變為0001,0011

(4)執行select(6,&set,0,0,0)阻塞等待

(5)若fd=1,fd=2上都發生可讀事件,則select返回,此時set變為0000,0011。注意:沒有事件發生的fd=5被清空

基於上面的討論,可以輕松得出select模型的特點:

(1)可監控的文件描述符個數取決與sizeof(fd_set)的值。我這邊服務器上sizeof(fd_set)=512,每bit表示一個文件描述符,則我服務器上支持的最大文件描述符是512*8=4096。據說可調,另有說雖然可調,但調整上限受於編譯內核時的變量值。

(2)將fd加入select監控集的同時,還要再使用一個數據結構array保存放到select監控集中的fd,一是用於再select返回后,array作為源數據和fd_set進行FD_ISSET判斷。二是select返回后會把以前加入的但並無事件發生的fd清空,則每次開始 select前都要重新從array取得fd逐一加入(FD_ZERO最先),掃描array的同時取得fd最大值maxfd,用於select的第一個參數。

(3)可見select模型必須在select前循環array(加fd,取maxfd),select返回后循環array(FD_ISSET判斷是否有時間發生)。

這種實現方式是有缺點的,一是能監聽的端口數量有限,二是采用輪詢的方法當套接字多了以后效率很低。三是需要有一個存儲大量fd的數據結構,用戶空間和內核空間在傳遞該結構時復制開銷大

關於上面所說的I/O,select相關的問題這篇博客說的很明白Linux Select
網絡字節轉換inet_aton、inet_nota、inet_addr
C++基礎--htons(),htonl(),ntohs(),ntohl()

3.實現代碼

Socket原理及實踐(Java/C/C++)
sockaddr詳解
C語言網絡編程:bind函數詳解
c++ Socket學習——使用listen(),accept(),write(),read()函數
STDIN_FILENO
C語言文件操作之fgets()因為gets()的不安全,所以使用fgets()代替
socklen_t 類型
socket編程之accept()函數

服務器端:

#include<stdio.h>  
#include<stdlib.h>  
#include<netinet/in.h>  
#include<sys/socket.h>  
#include<arpa/inet.h>  
#include<string.h>  
#include<unistd.h>  
#define BACKLOG 5     //完成三次握手但沒有accept的隊列的長度  
#define CONCURRENT_MAX 8   //應用層同時可以處理的連接  
#define SERVER_PORT 11332  
#define BUFFER_SIZE 1024  
#define QUIT_CMD ".quit"  
int client_fds[CONCURRENT_MAX];  //聲明一個數組來存儲狀態
int main(int argc, const char * argv[])  
{  
    char input_msg[BUFFER_SIZE];  //限定最大值
    char recv_msg[BUFFER_SIZE];    
    struct sockaddr_in server_addr;  
    server_addr.sin_family = AF_INET;  
    server_addr.sin_port = htons(SERVER_PORT);  
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  
    bzero(&(server_addr.sin_zero), 8);  
    //創建socket  
    int server_sock_fd = socket(AF_INET, SOCK_STREAM, 0);  
    if(server_sock_fd == -1)  
    {  
        perror("socket error");  
        return 1;  
    }  
    //綁定socket  
    int bind_result = bind(server_sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));  
    if(bind_result == -1)  
    {  
        perror("bind error");  
        return 1;  
    }  
    //listen  
    if(listen(server_sock_fd, BACKLOG) == -1)  
    {  
        perror("listen error");  
        return 1;  
    }  
    //fd_set  
    fd_set server_fd_set;  /*fd_set實際上是一long類型的數組,每一個數組元素都能與一打開的文件句柄 (不管是socket句柄,還是其他文件或命名管道或設備句柄) 建立聯系,建立聯系的工作由程序員完成,當調用select()時,由內核根據IO狀態修改fe_set的內容,由此來通知執行了select()的進程哪一socket或文件可讀。*/
    int max_fd = -1;  
    struct timeval tv;  //超時時間設置  
    while(1)  
    {  
        tv.tv_sec = 20;  
        tv.tv_usec = 0;  
        FD_ZERO(&server_fd_set);  
        FD_SET(STDIN_FILENO, &server_fd_set);  
        if(max_fd <STDIN_FILENO)  //STDIN_FILENO 標准輸入設備的文件描述符(鍵盤)
        {  
            max_fd = STDIN_FILENO;  
        }  
        //printf("STDIN_FILENO=%d\n", STDIN_FILENO);  
    //服務器端socket  
        FD_SET(server_sock_fd, &server_fd_set);  
       // printf("server_sock_fd=%d\n", server_sock_fd);  
        if(max_fd < server_sock_fd)  
        {  
            max_fd = server_sock_fd;  
        }  
    //客戶端連接  
        for(int i =0; i < CONCURRENT_MAX; i++)  
        {  
            //printf("client_fds[%d]=%d\n", i, client_fds[i]);  
            if(client_fds[i] != 0)  
            {  
                FD_SET(client_fds[i], &server_fd_set);  
                if(max_fd < client_fds[i])  
                {  
                    max_fd = client_fds[i];  
                }  
            }  
        }  
        int ret = select(max_fd + 1, &server_fd_set, NULL, NULL, &tv);  
        if(ret < 0)  
        {  
            perror("select 出錯\n");  
            continue;  
        }  
        else if(ret == 0)  
        {  
            printf("select 超時\n");  
            continue;  
        }  
        else  
        {  
            //ret 為未狀態發生變化的文件描述符的個數  
            if(FD_ISSET(STDIN_FILENO, &server_fd_set))  
            {  
                printf("發送消息:\n");  
                bzero(input_msg, BUFFER_SIZE);  
                fgets(input_msg, BUFFER_SIZE, stdin);  
                //輸入“.quit"則退出服務器  
                if(strcmp(input_msg, QUIT_CMD) == 0)  
                {  
                    exit(0);  
                }  
                for(int i = 0; i < CONCURRENT_MAX; i++)  
                {  
                    if(client_fds[i] != 0)  
                    {  
                        printf("client_fds[%d]=%d\n", i, client_fds[i]);  
                        send(client_fds[i], input_msg, BUFFER_SIZE, 0);  
                    }  
                }  
            }  
            if(FD_ISSET(server_sock_fd, &server_fd_set))  
            {  
                //有新的連接請求  
                struct sockaddr_in client_address;  
                socklen_t address_len;  
                int client_sock_fd = accept(server_sock_fd, (struct sockaddr *)&client_address, &address_len);  
                printf("new connection client_sock_fd = %d\n", client_sock_fd);  
                if(client_sock_fd > 0)  
                {  
                    int index = -1;  
                    for(int i = 0; i < CONCURRENT_MAX; i++)  
                    {  
                        if(client_fds[i] == 0)  
                        {  
                            index = i;  
                            client_fds[i] = client_sock_fd;  
                            break;  
                        }  
                    }  
                    if(index >= 0)  
                    {  
                        printf("新客戶端(%d)加入成功 %s:%d\n", index, inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));  
                    }  
                    else  
                    {  
                        bzero(input_msg, BUFFER_SIZE);  
                        strcpy(input_msg, "服務器加入的客戶端數達到最大值,無法加入!\n");  
                        send(client_sock_fd, input_msg, BUFFER_SIZE, 0);  
                        printf("客戶端連接數達到最大值,新客戶端加入失敗 %s:%d\n", inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));  
                    }  
                }  
            }  
            for(int i =0; i < CONCURRENT_MAX; i++)  
            {  
                if(client_fds[i] !=0)  
                {  
                    if(FD_ISSET(client_fds[i], &server_fd_set))  
                    {  
                        //處理某個客戶端過來的消息  
                        bzero(recv_msg, BUFFER_SIZE);  
                        long byte_num = recv(client_fds[i], recv_msg, BUFFER_SIZE, 0);  
                        if (byte_num > 0)  
                        {  
                            if(byte_num > BUFFER_SIZE)  
                            {  
                                byte_num = BUFFER_SIZE;  
                            }  
                            recv_msg[byte_num] = '\0';  
                            printf("客戶端(%d):%s\n", i, recv_msg);  
                        }  
                        else if(byte_num < 0)  
                        {  
                            printf("從客戶端(%d)接受消息出錯.\n", i);  
                        }  
                        else  
                        {  
                            FD_CLR(client_fds[i], &server_fd_set);  
                            client_fds[i] = 0;  
                            printf("客戶端(%d)退出了\n", i);  
                        }  
                    }  
                }  
            }  
        }  
    }  
    return 0;  
} 

客戶端

connect函數詳解
Linux下Socket網絡編程send和recv使用注意事項

#include<stdio.h>  
#include<stdlib.h>  
#include<netinet/in.h>  
#include<sys/socket.h>  
#include<arpa/inet.h>  
#include<string.h>  
#include<unistd.h>  
#define BUFFER_SIZE 1024  
  
int main(int argc, const char * argv[])  
{  
    struct sockaddr_in server_addr;  
    server_addr.sin_family = AF_INET;  
    server_addr.sin_port = htons(11332);  
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  
    bzero(&(server_addr.sin_zero), 8);  
  
    int server_sock_fd = socket(AF_INET, SOCK_STREAM, 0);  
    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)  
    {  
    fd_set client_fd_set;  
    struct timeval tv;  
  
    while(1)  
    {  
        tv.tv_sec = 20;  
        tv.tv_usec = 0;  
        FD_ZERO(&client_fd_set);  
        FD_SET(STDIN_FILENO, &client_fd_set);  
        FD_SET(server_sock_fd, &client_fd_set);  
  
       select(server_sock_fd + 1, &client_fd_set, NULL, NULL, &tv);  
        if(FD_ISSET(STDIN_FILENO, &client_fd_set))  
        {  
            bzero(input_msg, BUFFER_SIZE);  
            fgets(input_msg, BUFFER_SIZE, stdin);  
            if(send(server_sock_fd, input_msg, BUFFER_SIZE, 0) == -1)  
            {  
                perror("發送消息出錯!\n");  
            }  
        }  
        if(FD_ISSET(server_sock_fd, &client_fd_set))  
        {  
            bzero(recv_msg, BUFFER_SIZE);  
            long byte_num = recv(server_sock_fd, recv_msg, BUFFER_SIZE, 0);  
            if(byte_num > 0)  
            {  
            if(byte_num > BUFFER_SIZE)  
            {  
                byte_num = BUFFER_SIZE;  
            }  
            recv_msg[byte_num] = '\0';  
            printf("服務器:%s\n", recv_msg);  
            }  
            else if(byte_num < 0)  
            {  
            printf("接受消息出錯!\n");  
            }  
            else  
            {  
            printf("服務器端退出!\n");  
            exit(0);  
            }  
        }  
        }  
    //}  
    }  
    return 0;  
} 


免責聲明!

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



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