1. 程式人生 > >TCP/IP網路程式設計 學習筆記_13 --基於I/O複用的服務端

TCP/IP網路程式設計 學習筆記_13 --基於I/O複用的服務端

前言:前面我們講了多程序的併發服務端,只要有客服端連線請求就會建立新程序,這雖然也是一種解決方案,但建立程序是需要付出極大代價的,這需要大量運算和記憶體空間,而且每個程序間具有獨立的記憶體空間,所以相互間的資料交換也相對複雜(管道)。
本章將討論併發伺服器的第二種實現方法——基於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)發生變化返回真。

    2,再來講講select函式,首先,來看看它的定義:

    int select(int maxfd, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
    maxfd:監視範圍
    readset:監視對應檔案描述符的接收事件,不監視這個事件則傳0
    writeset:監視對應檔案描述符的傳輸事件,不監視則傳0
    exceptset:監視異常事件,不監視則傳0
    timeout:設定超時時間,不設定則傳NULL
    返回值:發生錯誤返回-1,超時返回0,發生引數2-4事件返回事件發生的對應檔案描述符。

    然後,再來看看fd_set陣列,它是一個存有0和1的位陣列,這個陣列中的值0表不監視,1表監視。而每個值對應在陣列中的位置與檔案描述符一一對應。如Linux上的標準輸入的檔案描述符是0,則要監聽這個檔案描述符,就只需把fd_set陣列的fd_set[0]設定為1即可。下面是對fd_set陣列操作的一些巨集:

    FD_ZERO(fd_set *fdset):將fd_set變數的所有位初始化為0
    FD_SET(int fd, fd_set *fdset):設定檔案描述符fd為監聽狀態
    FD_CLR(int fd, fd_set *fdset):取消檔案描述符fd的監聽狀態
    FD_ISSET(int fd, fd_set *fdset):檔案描述符fd是否發生相應的監視事件,發生則返回真。

    3,select函式呼叫示例:

//
//  main.cpp
//  hello_client
//
//  Created by app05 on 15-8-31.
//  Copyright (c) 2015年 app05. All rights reserved.
//

#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/select.h>
#define BUF_SIZE 30

int main(int argc, const char * argv[]) {
    fd_set reads, temps;
    int result, str_len;
    char buf[BUF_SIZE];
    struct timeval timeout;
    /*初始化狀態設定*/
    FD_ZERO(&reads);   //將位陣列reads初始化為0,即不監聽任何檔案描述符
    FD_SET(0, &reads);  //設定監聽,監聽檔案描述符為0的物件(標準輸入)

    while (1)
    {
        //因為select呼叫後,timeout和初始化狀態會發生變化,所以需要每次迴圈前再初始化一次
        temps = reads;
        timeout.tv_sec = 5;
        timeout.tv_usec = 5000;
        result = select(1, &temps, 0, 0, &timeout);
        if(result == -1)
        {
            puts("select() error");
            break;
        }
        else if (result == 0)
        {
            puts("Time-out!");
        }
        else
        {
            //監聽的檔案描述符發生接收監聽事件
            if (FD_ISSET(0, &temps)) {
                str_len = read(0, buf, BUF_SIZE);
                buf[str_len] = 0;  //字串輸出結束符
                printf("message from console: %s", buf);
            }
        }
    }

    return 0;
}

這裡寫圖片描述

實現I/O複用的服務端

用I/O複用的方式實現前面的回聲程式

  • 服務端程式碼
//
//  main.cpp
//  hello_server
//
//  Created by app05 on 15-8-31.
//  Copyright (c) 2015年 app05. All rights reserved.
//

#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);
    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);
}
  • 客服端程式碼用以前寫的,隨便copy一個來
//
//  main.cpp
//  hello_client
//
//  Created by app05 on 15-7-6.
//  Copyright (c) 2015年 app05. All rights reserved.
//

#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;
}

這裡寫圖片描述
這裡寫圖片描述