從零開始構建一個服務器【2】

服務器的目標是爲了服務大量用戶,但是在【1】中所寫的最基本的服務器很顯然無法滿足這種要求。

因此,我們引入了multiplexing技術,也就是所謂的IO多路複用。

Linux下的IO多路複用技術有三種,select,poll,epoll。

IO多路複用技術能做到什麼呢?

我們普通的監聽套接字,一次只能夠監聽一個網絡套接字,而IO複用技術,可以讓我們同時監聽多個套接字。當某個套接字上有事件發生,IO多路複用的技術,會通過某種方式告訴我們,讓上層來進行處理。

首先第一個就是select,它的接口簽名如下:

int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);
  • 這個存放的是當前select所處理的最大的文件描述符+1
  • 接下來的三個分別代表的是讀事件,寫事件,異常事件
  • 最後一個參數是超時時間

fd_set的定義可以在**/usr/include/sys/select.h**路徑下進行查看,文件總共一百多行代碼,寫的十分精巧,大意是用位圖來實現的。

我們將它進行充分的簡化之後,就只剩一行代碼:

typedef struct 
{
    long int __fds_bits[16];
}fd_set;

long int在這裏有8個字節,8 * 16 = 128個字節,每個字節有8位,總共1024位,select就是通過這1024個位來管理某一個所關心的事件集合上是否有事件發生。也就意味着select通常所管理的事件集合的大小是1024個。可以通過修改內核參數來修改這個大小,但事實上這並不會對性能有額外的好處,也許會是負擔。

與select相關的宏有如下一些:

void FD_ZERO(fd_set* set);   //將一個fdset事件集合全部清理

void FD_SET(int fd, fd_set* set);   //將fd放置進set集合中,表示關心這個事件的發生狀況

int FD_ISSET(int fd, fd_set* set);  //判斷fd在set上是否有事件發生

struct timeval
{
    long tv_sec;    //seconds
    long tc_usec;   //microseconds
}; //這是超時參數的定義

接下來給出一個由select編寫的服務器實例:

#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <iostream>
#include <vector>

using namespace std;

int main()
{
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if(listenfd < 0)
    {
        cerr << "socket" << endl;
        return 1;
    }

    int ret = -1;

    /*int oldSocketFlag = fcntl(listenfd, F_GETFL, 0);
      int newSocketFlag = oldSocketFlag | O_NONBLOCK;
      int ret = fcntl(listenfd, F_SETFL, newSocketFlag);
      if(ret < 0)
      {
      cerr << "fcntl" << endl;
      return 2;
      }*/

    int on = 1;
    //這代表的是ip地址和端口號的複用
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (char*)&on, sizeof(on));
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, (char*)&on, sizeof(on));

    struct sockaddr_in localaddr;
    localaddr.sin_family = AF_INET;
    localaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    localaddr.sin_port = htons(20000);
    ret = bind(listenfd, (struct sockaddr*)&localaddr, sizeof(localaddr));
    if(ret < 0)
    {
        cerr << "bind" << endl;
        return 3;
    }

    ret = listen(listenfd, SOMAXCONN);
    if(ret < 0)
    {
        cerr << "listen" << endl;
        return 4;
    }

    int maxfd = listenfd;
    vector<int> clientfds;

    while(true)
    {
        fd_set readfds;
        FD_ZERO(&readfds);  //將可讀時間全部清零

        FD_SET(listenfd, &readfds);  //將監聽fd加入可讀事件中

        size_t clientfd_len = clientfds.size();
        for(size_t i = 0; i < clientfd_len; ++i)
        {
            if(clientfds[i] != -1)
            {
                FD_SET(clientfds[i], &readfds);
            }
        }

        timeval timeout;
        timeout.tv_sec = 1;
        timeout.tv_usec = 0;
        ret = select(maxfd + 1, &readfds, nullptr,nullptr, &timeout);
        if(ret < 0)
        {
            cerr << "select" << endl;
            return 5;
        }
        else if(ret == 0)
        {
            //timeout
            continue;
        }
        else
        {
            if(FD_ISSET(listenfd, &readfds))  //來了新連接
            {
                struct sockaddr_in clientaddr;
                clientaddr.sin_family = AF_INET;
                socklen_t len = sizeof(clientaddr);
                int client;
                if((client = accept(listenfd, (struct sockaddr*)&clientaddr, &len)) < 0)
                {
                    cerr << "acceppt" << endl;
                    continue;
                }

                clientfds.push_back(client);
                if(maxfd < client)
                    maxfd = client;
            }
            else
            {
                size_t len = clientfds.size();
                for(int i = 0; i < len; ++i)
                {
                    if(clientfds[i] != -1 && FD_ISSET(clientfds[i], &readfds))
                    {
                        char buf[64] = { 0 };
                        ret = read(clientfds[i], buf, sizeof(buf));
                        //事實上這裏應該close每個ret==0的連接,
                        //但是這裏爲了讓打印的客戶端clientfd遞增的明顯一點,
                        //沒有關閉,而是置爲-1
                        if(ret <= 0 && errno != EINTR)
                        {
                            cerr << "read" << endl;
                            clientfds[i] = -1;
                            continue;
                        }
                        cout << "From clientfd: " << clientfds[i] << " data : " << buf << endl;
                    }
                }
            }
        }
    }

    for(size_t i = 0; i < clientfds.size(); ++i)
        close(clientfds[i]);

    close(listenfd);
    return 0;
}

這個服務器只會對客戶端發送過來的數據做打印處理,這裏主要是介紹select的參數。

select目前最大的優勢就是跨平臺,但事實上,在不同平臺上,由於對select的實現不一致,故而在參數細節上,需要注意。比如timeval的超時參數,Linux下這個參數每調用一次需要重新設定,因爲內核會對它進行改動,有興趣可以使用memcmp對傳入前和傳入後的參數進行一個位級別的比較,就可以發現這個細節,但Windows下不會對這個參數進行修改,因此我們在編寫跨平臺的代碼時候,需要額外注意。

這份代碼沒有提供客戶端出來,我們可以使用nc命令來進行測試。

nc -v 127.0.0.1 20000

這裏的端口號寫死了,如果想要修改,可以通過修改位傳參或者是直接在代碼裏修改的的方式來進行修改。

整個代碼的流程思路如下:

在這裏插入圖片描述

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