(P17)socket編程(十二)

1.select限制

  • 用select實現的併發服務器,能達到的併發數,受到2個方面的限制
    (1)一個進程能打開的最大文件描述符的限制。這個可以通過調整內核參數實現。
    (2)select中的fd_set集合容量的限制(FD_SETSIZE)。這需要重新編譯內核。
    FD_SETSIZE是在頭文件中定義的,而且需要重新編譯內核

  • 方法1,使用命令:
    解決一個進程能打開的最大文件描述符的限制

ulimit -n 2048,進程打開的文件描述符的數量
  • 方法2,編程解決:
    解決一個進程能打開的最大文件描述符的限制

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

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

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

//僅僅改變當前進程最大文件描述符的限制
int main(void)
{
    //man getrlimit
    struct rlimit rl;
    if (getrlimit(RLIMIT_NOFILE, &rl) <0)
        ERR_EXIT("getrlimit"):

    printf("%d\n", (int)rl.rlim_max);

    rl.rlim_cur = 2048;
    rl.rlim_max = 2048;

    if (setrlimit(RLIMIT_NOFILE, &rl) <0)
        ERR_EXIT("setrlimit"):    

    if (getrlimit(RLIMIT_NOFILE, &rl) <0)
        ERR_EXIT("getrlimit"):

    printf("%d\n", (int)rl.rlim_max);    
    return 0;
}
  • eg:select併發量的測試
    客戶端代碼:NetworkProgramming-master (1)\LinuxNetworkProgramming\P17\P17echocli.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);

//客戶端僅作連接
int main(int argc, char** argv) {
    
    int count = 0;
    while(1)
    {
        // 1. 創建套接字
        int sockfd;
        if ((sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) 
        {
            sleep(4);//當客戶端fd超過上限,先sleep4s再退出來,推遲close
            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));

        printf("count = %d\n", ++count);
    }
    return 0;
}
  • eg:select併發量的測試
    服務端代碼:NetworkProgramming-master (1)\LinuxNetworkProgramming\P17\P17echoserv.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) {

    int count = 0;
    //測試服務端收到了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);

    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
    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));

            printf("count = %d\n", c++count);
            FD_SET(connfd, &allset);//將connfd加入到allset集合,以便下次關心connfd的可讀事件
            if (connfd > maxfd)//更新maxfd
                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);
                
                
                writen(connfd, recvbuf, strlen(recvbuf));
                if (--nready <= 0)//所有的事件處理完畢,就break
                {
                    break;
                }
            }
        }
    }


    return 0;
}
  • 測試結果:客戶端沒有sleep
    服務端
    在這裏插入圖片描述
    客戶端
    在這裏插入圖片描述

  • 測試結果:客戶端加了sleep
    服務端:
    在這裏插入圖片描述
    客戶端:
    在這裏插入圖片描述

  • 解釋如下
    (1)客戶端
    0 1 2已經被標準輸入,標準輸出,標準錯誤輸出所佔用
    1021個連接
    創建第1022個套接字的時候失敗了,若客戶端沒有調用sleep(4),會給服務端發送FIN段,
    若客戶端客戶端調用了sleep(4),就不會發送FIN段給對方了
    (2)服務端
    收到這1021個連接,會在已完成連接隊列中維護1021個條目,若客戶端沒有sleep(4),可能還有300個fd沒有處理,此時創建第1022個套接字的時候失敗了,失敗了就意味着退出進程,導致客戶端發送了FIN段給服務端,而此時服務端還沒有完全接受連接,所以有可能先收到了客戶端的FIN段,所以會出現client close和accept混在一起了

  • 沒有sleep的服務端解釋:
    close已經收到了,但是已完成連接隊列還沒有完全接收完,一旦套接字關閉,就能騰出來足夠的文件描述符來接受連接了,所以這裏的1021並不代表有1021個併發,因爲有一些客戶端已經關閉了,所以客戶端有個sleep(4)。

  • 有sleep的客戶端解釋:
    爲啥客戶端是1020個併發?
    除了0,1,2外,還有監聽套接字3,所以併發數量爲1024-4=1020.
    當fd爲1021時,創建套接字就會失敗,因爲超出了進程創建的fd的限制,所以顯示Too many open files

2.poll:與select差不多

  • poll
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

fds:關心的套接口以及事件,通常指向一個數組;
nfds:加入到該數組中的io的個數;
timeout:超時時間;
  • struct pollfd結構體
struct pollfd {
	 int   fd;         /* file descriptor */
	 short events;     /* requested events */要請求的事件
	 short revents;    /* returned events */返回的事件
};
  • eg客戶端:代碼如上:NetworkProgramming-master (1)\LinuxNetworkProgramming\P17\P17echocli.c
  • eg服務端:NetworkProgramming-master (1)\LinuxNetworkProgramming\P17\P17pollsrv.c
//
// Created by jxq on 19-8-7.
//

// socket編程 12 poll模型

#include <iostream>
#include <stdio.h>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.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));
    }

}

int main(int argc, char** argv) {
    // 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;
    int maxi = 0;//最大不空閒的位置爲0
    int connfd;
    int i;
    struct pollfd client[2048];//struct pollfd結構體保存客戶端的信息
    int ret;
    int Max = 0;

    for (i = 0; i < 2048; ++i)
    {
        client[i].fd = -1;//-1表示客戶端空閒
    }
    
    client[0].fd = listenfd;
    client[0].events = POLLIN;//POLLIN:表示監聽套接字listenfd的可讀事件感興趣
    while (1)
    {
        nready = poll(client, maxi+1, -1);//-1表示永遠等待,等到有事件才返回
        //nready = select(maxfd + 1, &rset, NULL, NULL, NULL);
        if (nready == -1)//爲-1表示出錯
        {
            if (errno == EINTR)
            {
                continue;
            }
            ERR_EXIT("select");
        }

        if (nready == 0)//爲0表示超時了
        {
            continue;
        }

        //if (FD_ISSET(listenfd, &rset))
        if (client[0].revents & POLLIN)//監聽套接口是client[0]
        {
            peerlen = sizeof(peeraddr);
            connfd = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen);
            if (connfd == -1)
            {
                ERR_EXIT("accept");
            }
            for (i = 0; i < 2048; i++)
            {
                if (client[i].fd < 0)
                {
                    client[i].fd = connfd;
                    if (i > maxi)
                        maxi = i;
                    break;
                }
            }
            if (i == 2048)
            {
                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));
            client[i].events = POLLIN;//得到新的fd,就得將其添加進去
            if (--nready <= 0)
            {
                continue;
            }
        }

        //已連接套接口從1到maxi進行遍歷
        for (i = 1; i < = maxi; ++i)
        {
            connfd = client[i].fd;
            if (connfd == -1)
            {
                continue;
            }
            //if (FD_ISSET(connfd, &rset))
            if (client[0].events & POLLIN)//表示發生了可讀事件
            {
                char recvbuf[1024] = {0};
                ret = readline(connfd, recvbuf, 1024);
                if (ret == -1)
                {
                    ERR_EXIT("readline");
                }
                if (ret == 0)
                {
                    printf("client close\n");
                    client[i].fd = -1;
                    //FD_CLR(connfd, &allset);
                    maxi--;
                }
                fputs(recvbuf, stdout);
                //sleep(4);
                writen(connfd, recvbuf, strlen(recvbuf));
                if (--nready <= 0)
                {
                    break;
                }
            };
        }
    }

    // 7. 斷開連接
    close(listenfd);


    return 0;
}
  • 測試:ulimit -n 2048
    服務端:去掉0,1,2,監聽fd,爲2048-4=2044
    在這裏插入圖片描述
    客戶端:去掉0,1,2,爲2048-3=2045,即打開2045個fd
    在這裏插入圖片描述

  • struct pollfd結構體中的events
    (1)POLLIN與select的可讀事件相對應
    (2)POLLPRI與select的緊急事件相對應
    (3)POLLOUT與select的可寫事件相對應

在這裏插入圖片描述

  • Makefile
.PHONY:clean all
CC=gcc
CFLAGS=-Wall -g
BIN=echosrv echocli nofile_limit contest pollsrv
all:$(BIN)
%.o:%.c
	$(CC) $(CFLAGS) -c $< -o $@
clean:
	rm -f *.o $(BIN)


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