(P15)socket編程(十)

1.close與shutdown

  • close終止了數據傳送的兩個方向
  • shutdown可以有選擇的終止某個方向的數據傳送或者終止數據傳送的2個方向
  • shutdown how=1就可以保證對等方接收到一個EOF字符(EOF,相當於給對方發送了一個FIN段),而不管其它進程是否已經打開了套接字(與引用計數無關)。
    而close不能保證,直到套接字引用計數減爲0時才發送,也就是說直到所有的進程都關閉了套接字。(若多個多個進程打開了該套接字,直到最後一個進程調用close纔會向對等方發送一個FIN段)
man 2 shutdown

int shutdown(int sockfd, int how);

how=1,,也就是SHUT_WR =1,表示終止寫的方向
SHUT_WR =1,終止寫的方向
SHUT_RD=0,終止讀的方向

how=2:表示兩者都終止
  • eg:前提:socketA調用了close,socketB沒有調用close
    (1)socketA向socketB傳送數據,socketA調用close是關閉了socketA的發送和接收數據這2個方向(即:socketA不能從套接字中讀數據和也不能從套接字中寫數據)。
    (2)相當於socketA向B發送了FIN段,socketB收到後,read返回爲0,但是這並不意味着socketB不能通過socket向socketA發送數據,僅僅意味着socketA到socketB的數據傳送終止了,並不意味着socketB向socketA的數據傳送終止了,此時socketA會向socketB發送一個RST段(含義:連接重置),socketB再次調用write,會產生SIGPIPE信號
    (3)eg:前段時間寫過的多進程服務端的代碼
int conn;
conn = accept(sock, NULL. NULL);
pid_t pid=fork();
if (pid == -1)
	ERR_EXIT("fork");

if (pid == 0)
{
	close(sock);
	//通信
	close(conn);//這時纔會向對方發送FIN段(因爲這時conn引用計數減爲0)
}
else if ()
{
	shutdown(conn,shut_WR);//不用理會引用計數,直接向對方發送FIN段
	close(conn);//不會向客戶端發送FIN段,僅僅只是將套接字的引用計數減1
}
  • 爲啥要使用shutdown?
    (1)可以將TCP看成是全雙工的管道
    (2)假設server端收到這些字節,還沒有回射到client端,還處於全雙工的管道中,若此時client關閉,server端收到了FIN終止段,那麼此時這些數據無法回射給client端,因爲client端調用了close,意味着套接字終止了數據傳送的2個方向,不能接收管道中的數據了
    對於client端而言,只想關閉管道中寫入的這一半,不想關閉讀入的這一半,此時使用close就無能爲力了。
    在這裏插入圖片描述

2.進一步改進回射客戶程序

  • eg:服務端:NetworkProgramming-master (1)\LinuxNetworkProgramming\P15echosrv.c
//
// Created by wangji on 19-8-7.
//

// socket編程 9

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


using namespace std;

struct packet
{
    int len;
    char buf[1024];
};

#define ERR_EXIT(m) \
        do  \
        {   \
            perror(m);  \
            exit(EXIT_FAILURE); \
        } while(0);

ssize_t readn(int fd, void *buf, size_t count)
{
    size_t nleft = count;   // 剩餘字節數
    ssize_t nread;
    char *bufp = (char*) buf;

    while (nleft > 0)
    {
        nread = read(fd, bufp, nleft);
        if (nread < 0)
        {
            if (errno == EINTR)
            {
                continue;
            }
            return  -1;
        } else if (nread == 0)
        {
            return count - nleft;
        }

        bufp += nread;
        nleft -= nread;
    }
    return count;
}

ssize_t writen(int fd, const void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nwritten;
    char* bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nwritten = write(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
            {
                continue;
            }
            return -1;
        }
        else if (nwritten == 0)
        {
            continue;
        }
        bufp += nwritten;
        nleft -= nwritten;
    }
    return count;
}

ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
    while (1)
    {
        int ret = recv(sockfd, buf, len, MSG_PEEK); // 查看傳入消息
        if (ret == -1 && errno == EINTR)
        {
            continue;
        }
        return ret;
    }
}

ssize_t readline(int sockfd, void *buf, size_t maxline)
{
    int ret;
    int nread;
    char *bufp = (char*)buf;    // 當前指針位置
    int nleft = maxline;
    while (1)
    {
        ret = recv_peek(sockfd, buf, nleft);
        if (ret < 0)
        {
            return ret;
        }
        else if (ret == 0)
        {
            return ret;
        }
        nread = ret;
        int i;
        for (i = 0; i < nread; i++)
        {
            if (bufp[i] == '\n')
            {
                ret = readn(sockfd, bufp, i+1);
                if (ret != i+1)
                {
                    exit(EXIT_FAILURE);
                }
                return ret;
            }
        }
        if (nread > nleft)
        {
            exit(EXIT_FAILURE);
        }
        nleft -= nread;
        ret = readn(sockfd, bufp, nread);
        if (ret != nread)
        {
            exit(EXIT_FAILURE);
        }
        bufp += nread;
    }
    return -1;
}

void echo_srv(int connfd)
{
    char recvbuf[1024];
    // struct packet recvbuf;
    int n;
    while (1)
    {
        memset(recvbuf, 0, sizeof recvbuf);
        int ret = readline(connfd, recvbuf, 1024);
        if (ret == -1)
        {
            ERR_EXIT("readline");
        }
        if (ret == 0)
        {
            printf("client close\n");
            break;
        }

        fputs(recvbuf, stdout);
        writen(connfd, recvbuf, strlen(recvbuf));
    }

}

void handle_sigpipe(int sig)
{
    printf("recv a sig=%d\n", sig);
}


int main(int argc, char** argv) {


    //測試服務端收到了SIGPIPE信號
    signal(SIGPIPE, SIG_IGN);
    signal(SIGPIPE, handle_sigpipe);

    // 1. 創建套接字
    int listenfd;
    if ((listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
        ERR_EXIT("socket");
    }

    // 2. 分配套接字地址
    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof servaddr);
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(6666);
    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;
    // 確保time_wait狀態下同一端口仍可使用
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof on) < 0) {
        ERR_EXIT("setsockopt");
    }

    // 3. 綁定套接字地址
    if (bind(listenfd, (struct sockaddr *) &servaddr, sizeof servaddr) < 0) {
        ERR_EXIT("bind");
    }
    // 4. 等待連接請求狀態
    if (listen(listenfd, SOMAXCONN) < 0) {
        ERR_EXIT("listen");
    }
    // 5. 允許連接
    struct sockaddr_in peeraddr;
    socklen_t peerlen;


    // 6. 數據交換
    int nready;//檢測到的事件個數
    int maxfd = listenfd;//默認套接口1,2,3已經打開了,所以listenfd爲3
    fd_set rset;
    fd_set allset;
    FD_ZERO(&rset);
    FD_ZERO(&allset);
    FD_SET(listenfd, &allset);
    int connfd;
    int i;
    int client[FD_SETSIZE];//rset集合中最大容量爲FD_SETSIZE
    int maxi;//最大空閒位置初始值爲0
    // int ret;
    // int Max = 0;
    // memset(client, -1, sizeof(client));
    for (i = 0; i < FD_SETSIZE; i++)
    {   
        client[i] = -1;//等於-1表示空閒的
    }
    while (1)
    {
        rset = allset;//若沒有把所有感興趣的fd保存至allset中,那麼下一次select,rset裏面其實是已經改變過的fd的集合
        //rset只保留當前改變事件的IO集合,並沒有監聽所有的套接口
        nready = select(maxfd + 1, &rset, NULL, NULL, NULL);
        if (nready == -1)
        {
            if (errno == EINTR)//被信號中斷,還可以執行
            {
                continue;
            }
            ERR_EXIT("select");
        }

        if (nready == 0)
        {
            continue;
        }

        //監聽套接口發生可讀事件,意味着對方connect已經完成,這邊已完成連接隊列的條目不爲空
        //此時,調用accept方法就不再阻塞
        if (FD_ISSET(listenfd, &rset))//rset是輸入輸出參數:輸出參數表示:哪些fd產生了事件,輸入參數表示:我們關心哪些文件描述符fd
        {
            peerlen = sizeof(peeraddr);
            //peerlen是輸入輸出參數,要有初始值,返回的是對方地址的長度
            connfd = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen);
            if (connfd == -1)
            {
                ERR_EXIT("accept");
            }
            for (i = 0; i < FD_SETSIZE; i++)
            {
                if (client[i] < 0)
                {
                    client[i] = connfd;//將connfd保存到client中的空閒位置
                    if (i > maxi)//最大不空閒位置可能發生改變
                        maxi = i;//最大空閒位置發生了改變
                    break;
                }
            }
            if (i == FD_SETSIZE)//找不到空閒位置
            {
                fprintf(stderr, "too many clients\n");
                exit(EXIT_FAILURE);
            }
            printf("id = %s, ", inet_ntoa(peeraddr.sin_addr));
            printf("port = %d\n", ntohs(peeraddr.sin_port));
            FD_SET(connfd, &allset);//將connfd加入到allset集合,以便下次關心connfd的可讀事件
            if (connfd > maxfd)//更新maxfd
                maxfd = connfd;
            // Max++;
            // maxfd = max(maxfd, connfd);
            if (--nready <= 0)//說明檢測的事件已經處理完畢,沒必要往下走
            {
                continue;
            }
        }
        //connfd產生事件
        for (i = 0; i <= maxi; ++i)
        {
            connfd = client[i];
            if (connfd == -1)
            {
                continue;
            }
            if (FD_ISSET(connfd, &rset))//已連接套接口有可讀事件
            {
                char recvbuf[1024] = {0};
                int ret = readline(connfd, recvbuf, 1024);
                if (ret == -1)
                {
                    ERR_EXIT("readline");
                }
                if (ret == 0)
                {
                    printf("client close\n");
                    
                    FD_CLR(connfd, &allset);//從allset中清除
                    // Max--;
                    //補充(未做):實際上,還應該改變maxi的值,如果i是maxi,則maxi應該等於第二大的值,麻煩,先算了
                    client[i] = -1;//一旦套接口關閉。位置空閒了,爲-1
                    close(connfd);//關閉套接口,讓客戶端接收到通知
                }
                fputs(recvbuf, stdout);
                
                //測試客戶端close:客戶端輸入一些數據後,再輸入ctrl+D,由於服務端sleep 4s,服務端再次
                //發送數據給客戶端,客戶端會給服務端發送RST段
                //服務端會收到RST段,會收到SIGPIPE信號
                sleep(4);
                
                writen(connfd, recvbuf, strlen(recvbuf));
                if (--nready <= 0)//所有的事件處理完畢,就break
                {
                    break;
                }
            }
        }
    }
//    pid_t pid;
//    while (1) {
//        int connfd;
//        if ((connfd = accept(listenfd, (struct sockaddr *) &peeraddr, &peerlen)) < 0) {
//            ERR_EXIT("accept");
//        }
//
//        printf("id = %s, ", inet_ntoa(peeraddr.sin_addr));
//        printf("port = %d\n", ntohs(peeraddr.sin_port));
//
//        pid = fork();
//
//        if (pid == -1) {
//            ERR_EXIT("fork");
//        }
//        if (pid == 0)   // 子進程
//        {
//            close(listenfd);
//            echo_srv(connfd);
//            //printf("child exit\n");
//            exit(EXIT_SUCCESS);
//        } else {
//            //printf("parent exit\n");
//            close(connfd);
//        }
//
//
//    }
    // 7. 斷開連接
    close(listenfd);


    return 0;
}
  • eg:客戶端:NetworkProgramming-master (1)\LinuxNetworkProgramming\P15echocli.c
//
// Created by wangji on 19-8-6.
//

// socket編程 8 select模型

#include <iostream>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>


using namespace std;

struct packet
{
    int len;
    char buf[1024];
};

#define ERR_EXIT(m) \
        do  \
        {   \
            perror(m);  \
            exit(EXIT_FAILURE); \
        } while(0);

ssize_t readn(int fd, void *buf, size_t count)
{
    size_t nleft = count;   // 剩餘字節數
    ssize_t nread;
    char *bufp = (char*) buf;

    while (nleft > 0)
    {
        nread = read(fd, bufp, nleft);
        if (nread < 0)
        {
            if (errno == EINTR)
            {
                continue;
            }
            return  -1;
        } else if (nread == 0)
        {
            return count - nleft;
        }

        bufp += nread;
        nleft -= nread;
    }
    return count;
}

ssize_t writen(int fd, const void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nwritten;
    char* bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nwritten = write(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
            {
                continue;
            }
            return -1;
        }
        else if (nwritten == 0)
        {
            continue;
        }
        bufp += nwritten;
        nleft -= nwritten;
    }
    return count;
}


ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
    while (1)
    {
        int ret = recv(sockfd, buf, len, MSG_PEEK); // 查看傳入消息
        if (ret == -1 && errno == EINTR)
        {
            continue;
        }
        return ret;
    }
}

ssize_t readline(int sockfd, void *buf, size_t maxline)
{
    int ret;
    int nread;
    char *bufp = (char*)buf;    // 當前指針位置
    int nleft = maxline;
    while (1)
    {
        ret = recv_peek(sockfd, buf, nleft);
        if (ret < 0)
        {
            return ret;
        }
        else if (ret == 0)
        {
            return ret;
        }
        nread = ret;
        int i;
        for (i = 0; i < nread; i++)
        {
            if (bufp[i] == '\n')
            {
                ret = readn(sockfd, bufp, i+1);
                if (ret != i+1)
                {
                    exit(EXIT_FAILURE);
                }
                return ret;
            }
        }
        if (nread > nleft)
        {
            exit(EXIT_FAILURE);
        }
        nleft -= nread;
        ret = readn(sockfd, bufp, nread);
        if (ret != nread)
        {
            exit(EXIT_FAILURE);
        }
        bufp += nread;
    }
    return -1;
}

void ehco_cli(int sockfd)
{
//    char recvbuf[1024];
//    char sendbuf[1024];
//    // struct packet recvbuf;
//    // struct packet sendbuf;
//    memset(recvbuf, 0, sizeof recvbuf);
//    memset(sendbuf, 0, sizeof sendbuf);
//    int n = 0;
//    while (fgets(sendbuf, sizeof sendbuf, stdin) != NULL)   // 鍵盤輸入獲取
//    {
//        writen(sockfd, sendbuf, strlen(sendbuf)); // 寫入服務器
//
//        int ret = readline(sockfd, recvbuf, sizeof recvbuf);    // 服務器讀取
//        if (ret == -1)
//        {
//            ERR_EXIT("readline");
//        }
//        if (ret == 0)
//        {
//            printf("server close\n");
//            break;
//        }
//
//        fputs(recvbuf, stdout); // 服務器返回數據輸出
//
//        // 清空
//        memset(recvbuf, 0, sizeof recvbuf);
//        memset(sendbuf, 0, sizeof sendbuf);
//    }
    fd_set rset;
    FD_ZERO(&rset);

    int nready;//檢測到的事件個數
    int maxfd;
    int fd_stdin = fileno(stdin);//fileno:獲取標準輸入的文件描述符
    if (fd_stdin > sockfd)
    {
        maxfd = fd_stdin;
    } else {
        maxfd = sockfd;
    }

    char sendbuf[1024] = {0};
    char recvbuf[1024] = {0};
    
    int stdineof = 0;

    while (1)//檢測標準輸入是否有可讀事件,套接口是否有可讀事件
    {
        //當標準輸入終止時,fgets=NULL時,fd_stdin不能加入rset集中進行監聽
        if (stdineof == 0)
            FD_SET(fd_stdin, &rset);//將fd_stdin放到讀的集合rset中,關心fd_stdin文件描述符的事件
        FD_SET(sockfd, &rset);//將sockfd放到讀的集合rset中
        nready = select(maxfd+1, &rset, NULL, NULL, NULL);//讀集合中最大文件描述+1
        //到底是哪一個套接口檢測到事件?rset集合是會改變的!!返回的是哪一些io或者套接口檢測到了事件
        //說明rset是輸入輸出參數
        if (nready == -1)
        {
            ERR_EXIT("select");
        }
        if (nready == 0)
        {
            continue;
        }
        
        //標準輸入IO和網絡IO
        //判斷標準輸入產生的可讀事件,還是套接口產生的可讀事件?
        if (FD_ISSET(sockfd, &rset))    // 套接口產生的可讀事件
        {
            int ret = readline(sockfd, recvbuf, sizeof(recvbuf));    // 服務器讀取
            if (ret == -1)
            {
                ERR_EXIT("readline");
            }
            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)   //等於NULL,說明客戶端已經得到一個EOF結束符
            {
                stdineof = 1;
                //測試客戶端給服務端發數據,服務端sleep 4s後,客戶端無法讀取服務端回射的數據
                close(sockfd);
                eixt(EXIT_FAILURE);
                /*
                shutdown(sockfd, SHUT_WR);
                //僅關閉寫入的一半,並不意味着不能讀取數據
                //實際還能讀取數據,還能讀取到對方的關閉通知,即readline還能讀取回射的數據
                */
            }
            else
            {
                writen(sockfd, sendbuf, strlen(sendbuf)); // 寫入服務器
                memset(sendbuf, 0, sizeof(sendbuf));
            }
            

        }
    }
    // close(sockfd);
};

void handle_sigchld(int sig)
{
    // wait(NULL);
    while (waitpid(-1, NULL, WNOHANG) > 0);
}


int main(int argc, char** argv) {
    
    // signal(SIGCHLD, SIG_IGN);
    signal(SIGCHLD, handle_sigchld);
    // 1. 創建套接字
    int sockfd;
    if ((sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
        ERR_EXIT("socket");
    }

    // 2. 分配套接字地址
    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof servaddr);
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(6666);
    // 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);

    // 3. 請求鏈接
    if (connect(sockfd, (struct sockaddr *) &servaddr, sizeof servaddr) < 0) {
        ERR_EXIT("connect");
    }

    struct sockaddr_in localaddr;
    socklen_t addrlen = sizeof localaddr;
    if (getsockname(sockfd, (struct sockaddr*)&localaddr, &addrlen) < 0)
    {
        ERR_EXIT("getsockname");
    }
    printf("id = %s, ", inet_ntoa(localaddr.sin_addr));
    printf("port = %d\n", ntohs(localaddr.sin_port));

    // 4. 數據交換
    ehco_cli(sockfd);

    // 5. 斷開連接
    close(sockfd);


    return 0;
}
  • 測試1:
    客戶端執行2次數:
    第一次輸入完數據,再輸入客戶端快速輸入ctrl+D,
    第二次再打開一個客戶端,再次輸入數據,服務端無法將數據回射給客戶端
    在這裏插入圖片描述
    客戶端關閉後,服務端的管道進程並沒有銷燬,對服務端是無害的,但是服務端無法將數據回射給客戶端了
    在這裏插入圖片描述

  • 測試2:在NetworkProgramming-master (1)\LinuxNetworkProgramming\P15echocli.c中的代碼使用shutdown
    (1)即使客戶端關閉套接字,服務器也能將數據回射回去
    (2)客戶端使用shutdown,如果使用close,第四行和第五行的回射數據就無法收到了
    (3)客戶端輸入aaaa,bbbb,ctrl+D
    在這裏插入圖片描述
    在這裏插入圖片描述

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