歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux管理 >> Linux網絡

linux網絡編程之socket(九) 使用select函數改進客戶端/服務器端程序

一、當我們使用單進程單連接且使用readline修改後的客戶端程序,去連接使用readline修改後的服務器端程序,會出 現一個有趣的現象,先來看輸出:

先運行服務器端,再運行客戶端,

simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./echoser_recv_peek

recv connect ip=127.0.0.1 port=54005

simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./echocli_recv_peek

local ip=127.0.0.1 port=54005

可以先查看一下網絡狀態,

simba@ubuntu:~$ netstat -an | grep tcp | grep 5188

tcp        0      0 0.0.0.0:5188            0.0.0.0:*               LISTEN

tcp        0      0 127.0.0.1:54005         127.0.0.1:5188          ESTABLISHED

tcp        0      0 127.0.0.1:5188          127.0.0.1:54005         ESTABLISHED

可以看出建立了連接,服務器端有兩個進程,一個父進程處於監聽狀態,另一子進程正在對客戶端進行服務 。

再ps 出服務器端的子進程,並kill掉它,

simba@ubuntu:~$ ps -ef | grep echoser

simba     4549  3593  0 15:57 pts/0    00:00:00 ./echoser_recv_peek

simba     4551  4549  0 15:57 pts/0    00:00:00 ./echoser_recv_peek

simba     4558  4418  0 15:57 pts/6    00:00:00 grep --color=auto echoser

simba@ubuntu:~$ kill -9 4551

這時再查看一下網絡狀態,

simba@ubuntu:~$ netstat -an | grep tcp | grep 5188

tcp        0      0 0.0.0.0:5188            0.0.0.0:*               LISTEN

tcp        1      0 127.0.0.1:54005         127.0.0.1:5188          CLOSE_WAIT

tcp        0      0 127.0.0.1:5188          127.0.0.1:54005         FIN_WAIT2

來分析一下,我們將server子 進程  kill掉,則其終止時,socket描述符會自動關閉並發FIN段給client,client收到FIN後處於CLOSE_WAIT狀態, 但是client並沒有終止,也沒有關閉socket描述符,因此不會發FIN給 server子進程,因此server 子進程的TCP連接處於 FIN_WAIT2狀態。

為什麼會出現這種情況呢,來看client的部分程序:

void do_echocli(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, sizeof(recvbuf)); //按行讀取
        if (ret == -1)
            ERR_EXIT("readline error");
        else if (ret  == 0)   //服務器關閉
        {
            printf("server close\n");
            break;
        }
    
        fputs(recvbuf, stdout);
    
        memset(sendbuf, 0, sizeof(sendbuf));
        memset(recvbuf, 0, sizeof(recvbuf));
    
    }
    
    close(sock);
}

客戶端程序阻塞在了fgets 那裡,即從標准輸入讀取數據,所以不能執行到下面的readline,也即不能返回0,不會退出 循環,不會調用close關閉sock,所以出現上述的情況,即狀態停滯,不能向前推進。具體的狀態變化可以參見這裡。

出現上述問題的根本原因在於客戶端程序不能並發處理從標准輸入讀取數據和從套接字讀取數據兩個事件,我們可 以使用前面講過的select函數來完善客戶端程序,如下所示:

void do_echocli(int sock)
{
    fd_set rset;
    FD_ZERO(&rset);
    
    int nready;
    int maxfd;
    int fd_stdin = fileno(stdin); //
    if (fd_stdin > sock)
        maxfd = fd_stdin;
    else
        maxfd = sock;
    
    char sendbuf[1024] = {0};
    char recvbuf[1024] = {0};
    
    while (1)
    {
    
        FD_SET(fd_stdin, &rset);
        FD_SET(sock, &rset);
        nready = select(maxfd + 1, &rset, NULL, NULL, NULL); //select返回表示檢測到可讀事件
        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("read error");
            else if (ret  == 0)   //服務器關閉
            {
                printf("server close\n");
                break;
            }
    
            fputs(recvbuf, stdout);
            memset(recvbuf, 0, sizeof(recvbuf));
        }
    
        if (FD_ISSET(fd_stdin, &rset))
        {
    
            if (fgets(sendbuf, sizeof(sendbuf), stdin) == NULL)
                break;
    
            writen(sock, sendbuf, strlen(sendbuf));
            memset(sendbuf, 0, sizeof(sendbuf));
        }
    }
    
    close(sock);
}

即將兩個事件都添加進可讀事件集合,在while循環中,如果select返回說明有事件發生,依次判斷是哪些事件發生,如 果是標准輸入有數據可讀,則讀取後再次回到循環開頭select阻塞等待事件發生,如果是套接口有數據可讀,且返回為0則 說明對方已經關閉連接,退出循環並調用close關閉sock。

重復前面的實驗過程,把客戶端換成使用select函數修改 後的程序,可以看到最後的輸出:

simba@ubuntu:~$ netstat -an | grep tcp | grep 5188

tcp        0      0 0.0.0.0:5188            0.0.0.0:*               LISTEN

tcp        0      0 127.0.0.1:5188          127.0.0.1:54007         TIME_WAIT

即 client 關閉socket描述符,server 子進程的TCP連接收到client發的FIN段後處於TIME_WAIT狀態,此時會再發生一個ACK段 給client,client接收到之後就處於CLOSED狀態,這個狀態存在時間很短,所以看不到客戶端的輸出條目,TCP協議規定, 主動關閉連接的一方要處於TIME_WAIT狀態,等待兩個MSL(maximumsegment lifetime)的時間後才能回到CLOSED狀態,需 要有MSL 時間的主要原因是在這段時間內如果最後一個ack段沒有發送給對方,則可以重新發送。

過一小會再次查看 網絡狀態,

simba@ubuntu:~$ netstat -an | grep tcp | grep 5188

tcp        0      0 0.0.0.0:5188            0.0.0.0:*               LISTEN

可以發現只剩下服務器端父進程的監聽狀態了,由TIME_WAIT狀態轉入 CLOSED狀態,也很快會消失。

二、前面我們實現的能夠並發服務的服務器端程序是使用fork出多個子進程來實現的 ,現在學習了select函數,可以用它來改進服務器端程序,實現單進程並發服務。先看如下程序,再來解釋:

/*************************************************************************
    > File Name: echoser.c
    > Author: Simba
    > Mail: [email protected] 
    > Created Time: Fri 01 Mar 2013 06:15:27 PM CST
 ************************************************************************/
    
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<string.h>
#include<signal.h>
#include<sys/wait.h>
#include "read_write.h"
    
#define ERR_EXIT(m) \
    do { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while (0)
    
    
int main(void)
{
        
    signal(SIGPIPE, SIG_IGN);
    int listenfd; //被動套接字(文件描述符),即只可以accept, 監聽套接字
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
//  listenfd = socket(AF_INET, SOCK_STREAM, 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 = htonl(INADDR_ANY); 
    /* servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); */
    /* inet_aton("127.0.0.1", &servaddr.sin_addr); */
        
    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) //listen應在socket和bind之後,而在accept之前
        ERR_EXIT("listen error");
        
    struct sockaddr_in peeraddr; //傳出參數
    socklen_t peerlen = sizeof(peeraddr); //傳入傳出參數,必須有初始值
        
    int conn; // 已連接套接字(變為主動套接字,即可以主動connect)
    int i;
    int client[FD_SETSIZE];
    int maxi = 0; // client數組中最大不空閒位置的下標
    for (i = 0; i < FD_SETSIZE; i++)
        client[i] = -1;
    
    int nready;
    int maxfd = listenfd;
    fd_set rset;
    fd_set allset;
    FD_ZERO(&rset);
    FD_ZERO(&allset);
    FD_SET(listenfd, &allset);
    
    while (1) {
        rset = allset;
        nready = select(maxfd + 1, &rset, NULL, NULL, NULL);
        if (nready == -1) {
            if (errno == EINTR)
                continue;
            ERR_EXIT("select error");
        }
    
        if (nready == 0)
            continue;
    
        if (FD_ISSET(listenfd, &rset)) {
            
            conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen);  //accept不再阻塞
            if (conn == -1)
                ERR_EXIT("accept error");
                
            for (i = 0; i < FD_SETSIZE; i++) {
                if (client[i] < 0) {
                    client[i] = conn;
                    if (i > maxi)
                        maxi = i;
                    break;
                } 
            }
                
            if (i == FD_SETSIZE) {
                fprintf(stderr, "too many clients\n");
                exit(EXIT_FAILURE);
            }
    
            printf("recv connect ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr),
                ntohs(peeraddr.sin_port));
    
            FD_SET(conn, &allset);
            if (conn > maxfd)
                maxfd = conn;
    
            if (--nready <= 0)
                continue;
        }
    
        for (i = 0; i <= maxi; i++) {
            conn = client[i];
            if (conn == -1)
                continue;
    
            if (FD_ISSET(conn, &rset)) {
                    
                char recvbuf[1024] = {0};
                int ret = readline(conn, recvbuf, 1024);
                if (ret == -1)
                    ERR_EXIT("readline error");
                else if (ret  == 0) { //客戶端關閉 
                    printf("client close \n");
                    FD_CLR(conn, &allset);
                    client[i] = -1;
                    close(conn);
                }
            
                fputs(recvbuf, stdout);
                writen(conn, recvbuf, strlen(recvbuf));
                    
                if (--nready <= 0)
                    break; 
            }
        }
    
    
    }
            
    return 0;
}
    
/* select所能承受的最大並發數受
 * 1.一個進程所能打開的最大文件描述符數,可以通過ulimit -n來調整
 *   但一個系統所能打開的最大數也是有限的,跟內存有關,可以通過cat /proc/sys/fs/file-max 查看
 * 2.FD_SETSIZE(fd_set)的限制,這個需要重新編譯內核                                                          

                
 */

程序有點長,但邏輯並不復雜,我們按照正常運行的狀況走一下就清晰了。

前面調用socket,listen,bind等函 數等初始化工作就不說了。程序第一次進入while 循環,只把監聽套接字加入關心的事件,select返回說明監聽套接字有可 讀事件,即已完成連接隊列不為空,這時調用accept不會阻塞,返回一個已連接套接字,將這個套接字加入allset,因為第 一次運行則nready = 1,直接continue跳回到while 循環開頭,再次調用select,這次會關心監聽套接字和一個已連接套接 字的可讀事件,如果繼續有客戶端連接上來則繼續將其加入allset,這次nready = 2,繼續執行下面的for 循環,然後對客 戶端進行服務。服務完畢再次回到while 開頭調用select 阻塞時,就關心一個監聽套接字和2個已連接套接字的可讀事件了 ,一直循環下去。

程序大概邏輯就這樣,一些細節就大家自己想想了,比如client數組是用來保存已連接套接字的 ,為了避免每次都得遍歷到FD_SETSIZE-1,保存一個最大不空閒下標maxi,每次遍歷到maxi就可以了。每次得到一個conn, 要判斷一下conn與maxfd的大小。

當得知某個客戶端關閉,則需要將conn在allset中清除掉。之所以要有allset 和 rset 兩個變量是因為rset是傳入傳出參數,在select返回時rset可能被改變,故需要每次在回到while 循環開頭時需要將 allset 重新賦予rset 。

Copyright © Linux教程網 All Rights Reserved