c++ 網絡編程(四)TCP/IP LINUX/windows下 socket 基於I/O複用的服務器端代碼 解決多進程服務端創建進程資源浪費問題

原文作者:aircraft

原文鏈接:https://www.cnblogs.com/DOMLX/p/9613861.html

好了,繼上一篇說到多進程服務端也是有缺點的,每創建一個進程就代表大量的運算與內存空間佔用,相互進程數據交換也很麻煩。

本章的I/O模型就是可以解決這個問題的其中一種模型。。。廢話不多說進入主題--

I/O複用技術主要就是select函數的使用。

一.I/O複用預備知識--select()函數用法與作用

select()用來確定一個或多個套接字的狀態(更爲本質一點來講是文件描述符的狀態)。

使用select()所需要包含的頭文件是:#include<sys/select.h>

函數原型爲:int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timeval *timeout);

接下來根據函數原型一點點的介紹一下select()函數。

(1),struct fd_set 這是一個集合,這個集合中存放的是文件描述符(在unix、linux系統中任何的設備、管道、FIFO等都可通過文件描述符的形式來訪問)。當然一個socket也是一個文件描述符啦。相關的操作有:

FD_ZERO(fd_set *)將某一個集合清空

FD_SET(int, fd_set *)將一個給定的文件描述符加入到集合之中

FD_CLR(int, fd_set *)從集合中刪除指定的文件描述符。

FD_ISSET(int, fd_set *)檢查集合中指定的文件描述符是否準備好(可讀或可寫)

(2),struct timeval這是常用的一個結構體,用來表示時間值,有兩個結構體成員:tv_sec表示秒數和tv_usec表示毫秒數。

接下來具體解釋一下select的參數:

nfds:一個整數值,表示的是所要監視的文件描述符的範圍。即你所要監聽的文件描述符的最大值+1(因爲select()函數進行遍歷的時候是從0-文件描述符開始遍歷的)。

readfds:是指向fd_set結構的指針,這個集合中加入我們所需要監視的文件可讀操作的文件描述符。

writefds:指向fd_set結構的指針,這個集合中加入我們所需要監視的文件可寫操作的文件描述符。

exceptfds:指向fd_set結構的指針,這個集合中加入我們所需要監視的文件錯誤異常的文件描述符。

timeout:指向timeval結構體的指針,通過傳入的這個timeout參數來決定select()函數的三種執行方式:

1.傳入的timeout爲NULL,則表示將select()函數置爲阻塞狀態,直到我們所監視的文件描述符集合中某個文件描述符發生變化是,纔會返回結果。

2.傳入的timeout爲0秒0毫秒,則表示將select()函數置爲非阻塞狀態,不管文件描述符是否發生變化均立刻返回繼續執行。

3.傳入的timeout爲一個大於0的值,則表示這個值爲select()函數的超時時間,在timeout時間內一直阻塞,超過時間即返回結果。

然後該說一說select()函數的返回值了:

返回-1:select()函數錯誤,並將所有描述符集合清0,具體的錯誤可以通過errno輸出來查看(在windows下通過GetLastError獲取相應的錯誤代碼)。

返回0:表示select()函數超時。

返回正數:返回的正數值表示已經準備好的描述符數。

注意在每次select()函數調用以後,都需要將集合清空,因爲狀態已經改變,若需要重新監視就需要重新清空後在加入需要監視的文件描述符。

下面通過示例把select函數所有知識點進行整合,希望各位通過如下示例完全理解之前的內容。

linux下監控鍵盤數據:

    #include <sys/time.h>  
    #include <stdio.h>  
    #include <sys/types.h>  
    #include <sys/stat.h>  
    #include <fcntl.h>  
    #include <assert.h>  
    int main ()  
    {  
        int keyboard;  
        int ret,i;  
        char c;  
        fd_set readfd;  
        struct timeval timeout;  
        keyboard = open("/dev/tty",O_RDONLY | O_NONBLOCK);  
        assert(keyboard>0);  
        while(1)  
        {  
            //設置select函數的超時
            timeout.tv_sec=1;  
            timeout.tv_usec=0;
          //初始化fd_set結構體變量
            FD_ZERO(&readfd);  
            FD_SET(keyboard,&readfd);  
      
            ///監控函數  
            ret=select(keyboard+1,&readfd,NULL,NULL,&timeout);  
            if(ret == -1)   //錯誤情況  
                cout<<"error"<<endl ;  
            else if(ret)    //返回值大於0 有數據到來  
                if(FD_ISSET(keyboard,&readfd))  
                {  
                    i=read(keyboard,&c,1);  
                    if('\n'==c)  
                        continue;  
                    printf("hehethe input is %c\n",c);  
                    if ('q'==c)  
                        break;  
                }  
            else    //超時情況  
            {  
                cout<<"time out"<<endl;  
                continue;  
            }  
        }  
    }  

好了大概對select函數有一定的認知了,下面通過select函數實現I/O複用服務端。

二.基於I/O複用的回聲服務端

  • 什麼是I/O複用?通俗點講,其實就是一個事件監聽,只是這個監聽的事件一般是I/O操作裏的讀(read)與寫(write),只要發生了監聽的事件它就會響應。注意與一般服務器的區別,一般服務器是連接請求先進入請求隊列裏,然後,服務端套接字一個個有序去受理。而I/O複用服務器是事件監聽,只要對應監聽事件發生就會響應,是屬於併發服務器的一種。
  • I/O複用的使用 1,I/O複用的使用其實就是對select函數的使用,說select函數是I/O複用的全部內容也不爲過。但這個函數與一般函數不同,它很難使用,我們先來看看它的調用順序,分爲3步: 步驟一:
    • 設置文件描述符,即註冊要監聽的文件描述符,如監聽標準輸入的文件描述符0 -> FD_SET(0, &reads)
    • 指定監視範圍,Linux上創建文件對象生成的對應文件描述符是從0開始遞增的,所以最大監視範圍爲最後創建的文件描述符+1。
    • 設置超時,因爲select函數是一個阻塞函數,只有監視的文件描述符發生變化纔會返回,設置超時就是爲了防止阻塞,如果不想設置超時,則傳遞NULL。

    步驟二:

    • 調用select函數

    步驟三:

    • 查看調用結果,FD_ISSET(0, &reads)發生變化返回真。

下面給出LINUX下基於I/O複用服務端實現代碼:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/select.h>

#define BUF_SIZE 100
void error_handling(char *message);

int main(int argc, const char * argv[]) {
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_adr, clnt_adr;
    struct timeval timeout;
    fd_set reads, cpy_reads;

    socklen_t adr_sz;
    int fd_max, str_len, fd_num;
    char buf[BUF_SIZE];
    if (argc != 2) {
        printf("Usage: %s <port> \n", argv[0]);
        exit(1);
    }

    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = htons(atoi(argv[1]));

    if(bind(serv_sock, (struct sockaddr *) &serv_adr, sizeof(serv_adr)) == -1)
        error_handling("bind() error");
    if(listen(serv_sock, 5) == -1)
        error_handling("listen() error");

    FD_ZERO(&reads);
    //向要傳到select函數第二個參數的fd_set變量reads註冊服務器端套接字
    FD_SET(serv_sock, &reads);
    fd_max = serv_sock;

    while (1)
    {
        cpy_reads = reads;
        timeout.tv_sec = 5;
        timeout.tv_usec = 5000;

        //監聽服務端套接字和與客服端連接的服務端套接字的read事件
        if ((fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout)) == -1)
            break;
        if(fd_num == 0)
            continue;

        if (FD_ISSET(serv_sock, &cpy_reads))//受理客服端連接請求
        {
            adr_sz = sizeof(clnt_adr);
            clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
            FD_SET(clnt_sock, &reads);
            if(fd_max < clnt_sock)
                fd_max = clnt_sock;
            printf("connected client: %d \n", clnt_sock);
        }
        else//轉發客服端數據
        {
            str_len = read(clnt_sock, buf, BUF_SIZE);
            if (str_len == 0)//客服端發送的退出EOF
            {
                FD_CLR(clnt_sock, &reads);
                close(clnt_sock);
                printf("closed client: %d \n", clnt_sock);
            }
            else
            {
                //接收數據爲字符串時執行回聲服務
                write(clnt_sock, buf, str_len);
            }
        }
    }

    close(serv_sock);
    return 0;
}


void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}    

下面給出LINUX下基於I/O複用客戶端實現代碼:

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

#define BUF_SIZE 1024
void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

int main(int argc, const char * argv[]) {
    int sock;
    char message[BUF_SIZE];
    int str_len, recv_len, recv_cnt;
    struct sockaddr_in serv_adr;

    if(argc != 3)
    {
        printf("Usage: %s <IP> <port> \n", argv[0]);
        exit(1);
    }

    sock = socket(PF_INET, SOCK_STREAM, 0);
    if(sock == -1)
        error_handling("socket() error");

    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_adr.sin_port = htons(atoi(argv[2]));

    if (connect(sock, (struct sockaddr *) &serv_adr, sizeof(serv_adr)) == -1)
        error_handling("connect() error");
    else
        puts("Connected ...............");

    while (1) {
        fputs("Input message(Q to quit): ", stdout);
        fgets(message, BUF_SIZE, stdin);
        if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
            break;

        str_len = write(sock, message, strlen(message));

        /*這裏需要循環讀取,因爲TCP沒有數據邊界,不循環讀取可能出現一個字符串一次發送
         但分多次讀取而導致輸出字符串不完整*/
        recv_len = 0;
        while (recv_len < str_len) {
            recv_cnt = read(sock, &message[recv_len], BUF_SIZE - 1);
            if(recv_cnt == -1)
                error_handling("read() error");
            recv_len += recv_cnt;
        }
        message[recv_len] = 0;
        printf("Message from server: %s", message);
    }

    close(sock);
    return 0;
}

下面給出windows下I/O複用socket服務端代碼:

#include<iostream>
#include<WinSock2.h>
#pragma comment(lib,"ws2_32.lib")
#define bufsize 1024
using namespace std;
void main() {
    WSADATA wsadata;
    SOCKET serverSocket,clientSocket;
    int szClientAddr,fdnum,str_len;
    SOCKADDR_IN  serverAddr, clientAddr;
    fd_set reads, cpyReads;
    TIMEVAL timeout;
    char message[bufsize] = "\0";
 
    if(WSAStartup(MAKEWORD(2, 2), &wsadata)!=0)
        cout<<"WSAStartup() error"<<endl;
 
    serverSocket = socket(PF_INET, SOCK_STREAM, 0);
    if(serverSocket == INVALID_SOCKET)
        cout<<"socket()  error"<<endl;
 
    memset(&serverAddr, 0, sizeof(serverAddr));
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serverAddr.sin_port = htons(9999);
 
    if (bind(serverSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
        cout << "bind () error" << endl;
 
    listen(serverSocket, 5);
    cout << "服務器啓動成功!" << endl;
 
    FD_ZERO(&reads);  //所有初始化爲0
    FD_SET(serverSocket, &reads);  //將服務器套接字存入
 
    while (1) {
        cpyReads = reads;   
        timeout.tv_sec = 5;      //5秒
        timeout.tv_usec = 5000;   //5000毫秒
 
        //找出監聽中發出請求的套接字
        if ((fdnum = select(0, &cpyReads, 0, 0, &timeout)) == SOCKET_ERROR)
            break;
        if (fdnum == 0) {
            cout << "time out!" << endl; 
            continue;
        }
        for (unsigned int i = 0; i < reads.fd_count; i++) {
            if (FD_ISSET(reads.fd_array[i], &cpyReads)) { //判斷是否爲發出請求的套接字
                if (reads.fd_array[i] == serverSocket) {  //是否爲服務器套接字
                    szClientAddr = sizeof(clientAddr);
                    clientSocket = accept(serverSocket, (SOCKADDR*)&clientAddr, &szClientAddr);
                    if (clientSocket == INVALID_SOCKET)  cout << "accept() error" << endl;
                    FD_SET(clientSocket, &reads);
                    cout << "連接的客戶端是:" << clientSocket << endl;
                }
                else {//否  就是客戶端
                    str_len = recv(reads.fd_array[i], message, bufsize - 1, 0);
                    if (str_len == 0) {//根據接受數據的大小 判斷是否是關閉
                        FD_CLR(reads.fd_array[i], &reads);  //清除數組中該套接字
                        closesocket(cpyReads.fd_array[i]);
                        cout << "關閉的客戶端是:" << cpyReads.fd_array[i] << endl;
                    }
                    else {
                        send(reads.fd_array[i], message, str_len, 0);
                    }
                }
            }
        }    
    }
    closesocket(clientSocket);
    closesocket(serverSocket);
    WSACleanup();
}

下面給出windows下I/O複用socket客戶端代碼:

#include<iostream>
#include<WinSock2.h>
#pragma comment(lib,"ws2_32.lib")
#define bufsize 1024
using namespace std;
void main() {
    WSADATA wsadata;
    SOCKET clientSocket;
    SOCKADDR_IN  serverAddr;
    int  recvCnt;
 
    char message[bufsize] = "\0";
    if (WSAStartup(MAKEWORD(2, 2), &wsadata) != 0)
        cout << "WSAStartup() error" << endl;
 
    if ((clientSocket = socket(PF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET)
        cout << "socket()  error" << endl;
 
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    serverAddr.sin_port = htons(9999);
 
    if(connect(clientSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr))==SOCKET_ERROR)
        cout<<"connect() error"<<endl;
 
    while (1) {
        cout << "輸入Q或q退出:";
        cin >> message;
        if (!strcmp(message, "Q") || !strcmp(message, "q")) break;
        send(clientSocket, message, strlen(message), 0);
        memset(message, 0, sizeof(message));
        recv(clientSocket, message, bufsize, 0);
        cout << "服務器結果:" << message << endl;
    }
    closesocket(clientSocket);
    WSACleanup();
}

最後說一句啦。本網絡編程入門系列博客是連載學習的,有興趣的可以看我博客其他篇。。。。

參考博客:https://blog.csdn.net/zl908760230/article/details/70257229

參考博客:https://blog.csdn.net/hshl1214/article/details/45872243

參考博客:https://blog.csdn.net/u010223072/article/details/48133725

參考書籍:《TCP/IP 網絡編程 --尹聖雨》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章