鍍金池/ 教程/ C/ 0x15-套接字編程-HTTP服務(wù)器(3)
0x0E-單線程備份(下)
0x11-套接字編程-1
0x05-C語言指針:(Volume-1)
0x13-套接字編程-HTTP服務(wù)器(1)
0x0C-開始行動(dòng)
C 語言進(jìn)階
第一部分
0x05-C語言指針(Volume-2)
0x08-C語言效率(下)
0x07-C語言效率(上)
0x04 C代碼規(guī)范
0x0F-多線程備份
0x05-C語言變量
第四部分
0x16-套接字編程-HTTP服務(wù)器(4)
0x0D-單線程備份(上)
總結(jié)
0x01-C語言序言
0x15-套接字編程-HTTP服務(wù)器(3)
0x14-套接字編程-HTTP服務(wù)器(2)
0x17-套接字編程-HTTP服務(wù)器(5)
第三部分
我的C語言
0x06-C語言預(yù)處理器
0x09-未曾領(lǐng)略的新風(fēng)景
0x0A-C線程和Glib的視角
第二部分
0x10-網(wǎng)絡(luò)的世界
0x12-套接字編程-2
0x03-C代碼
0x0B-C語言錯(cuò)誤處理

0x15-套接字編程-HTTP服務(wù)器(3)

0x15-套接字編程-HTTP服務(wù)器(3)

  • 在一切開始之前,我們需要設(shè)想一下,為了讓自己的HTTP服務(wù)器變得更加靈活,我們可以讓某些參數(shù)不必硬編碼進(jìn)程序中,而是用配置文件的方式讀取
  • 一個(gè)HTTP服務(wù)器的基本配置無非是

    • IP地址端口號(hào), 根目錄路徑
    • 額外增加一個(gè) 線程數(shù)
    • 實(shí)際上,<IP, Port>應(yīng)該不需要我們?nèi)藶橹付ǎ珵榱苏{(diào)試方便,所以選擇放在配置文件中
  • 接下來我們寫一個(gè)可以解析配置文件的小模塊函數(shù)

      struct init_config_from_file {
          int  core_num;               /* CPU Core numbers */
      #define PORT_SIZE 10
          char listen_port[PORT_SIZE]; /*  */
      #define ADDR_SIZE IPV6_LENGTH_CHAR
          char use_addr[ADDR_SIZE];    /* NULL For Auto select(By Operating System) */
      #define PATH_LENGTH 256
          char root_path[PATH_LENGTH]; /* page root path */
      };
      typedef struct init_config_from_file wsx_config_t;

    這個(gè)是配置文件的所有屬性,可以將讀取的參數(shù),存進(jìn)這個(gè)結(jié)構(gòu)體中,與主線程交互

      /*
      * Read the config file "wsx.conf" in particular path
      * and Put the data to the config object
      * @param  config is aims to be a parameter set
      * @return 0 means Success
      * */
      int init_config(wsx_config_t * config);

    交互的接口,我的配置文件叫做 wsx.conf

對(duì)于配置文件存放位置而言,可以靈活一些,例如可以額外添加一個(gè)命令行參數(shù),用來指定本次需要使用的配置文件路徑: ./httpd -f /path/to/wsx.conf 當(dāng)然這用在開發(fā)版本可以方便調(diào)試,實(shí)際上的HTTP服務(wù)器并不行,參見守護(hù)進(jìn)程的定義

最經(jīng)典的做法還是指定默認(rèn)路徑,將配置文件都存放在某個(gè)地方,可以多設(shè)定幾個(gè),并設(shè)定優(yōu)先級(jí)

  • 想想,我們需要什么功能,我給自己的配置文件添加了注釋功能,以#開頭的都是注釋,這點(diǎn)十分容易做到。
  • 上代碼

      static const char * config_path_search[] = {CONFIG_FILE_PATH, "./wsx.conf", "/etc/wushxin/wsx.conf", NULL};
    
      int init_config(wsx_config_t * config){
          const char ** roll = config_path_search;
          FILE * file;
          for (int i = 0; roll[i] != NULL; ++i) {
              file = fopen(roll[i], "r");
              if (file != NULL)
                  break;
          }
          if (NULL == file) {
      #if defined(WSX_DEBUG)
              fprintf(stderr, "Check For the Config file, does it stay its life?\n"
              "In Such Path: \n%s\n%s\n%s\n", config_path_search[0], config_path_search[1], config_path_search[2]);
      #endif
              exit(-1);
          }
      ...未結(jié)束

    這是很簡單的文件操作,包括打開文件,驗(yàn)證是否成功,可以選擇將其封裝成一個(gè)inline函數(shù),來模塊化這個(gè)邏輯。

          char buf[PATH_LENGTH] = {"\0"};
          char * ret;
          ret = fgets(buf, PATH_LENGTH, file);
          while (ret != NULL) {
              char * pos = strchr(buf, ':');
              char * check = strchr(buf, '#'); /* Start with # will be ignore */
              if (check != NULL)
                  *check = '\0';
    
              if (pos != NULL) {
                  *pos++ = '\0';
                  if (0 == strncasecmp(buf, "thread", 6)) {
                      sscanf(pos, "%d", &config->core_num);
                  }
                  else if (0 == strncasecmp(buf, "root", 4)) {
                      sscanf(pos, "%s", &config->root_path);
                      /* End up without "/", Add it */
                      if ((config->root_path)[strlen(config->root_path)-1] != '/') {
                          strncat(config->root_path, "/", 1);
                      }
                  }
                  else if (0 == strncasecmp(buf, "port", 4)) {
                      sscanf(pos, "%s", &config->listen_port);
                  }
                  else if (0 == strncasecmp(buf, "addr", 4)) {
                      sscanf(pos, "%s", &config->use_addr);
                  }
              } /* if pos != NULL */
          ret = fgets(buf, PATH_LENGTH, file);
          } /* while */
          fclose(file);
          return 0;
      }

    真正的核心代碼沒幾行,四個(gè)if,使用strncasecmp函數(shù),檢測(cè)參數(shù)。但是并沒有 驗(yàn)證參數(shù)的正確性

    當(dāng)然你也可以寫成 json 的形式,再用第三方庫,比如c-json之類的解析,但 那不是要依賴第三方了嗎?所以我的建議還是自己寫一個(gè)解析的函數(shù)。

    如果沒能理解這小段代碼,建議翻一下C語言的入門教材,回顧一下語法。

  • 配置文件的樣式

      # Just Edit this Config file Or 
      # You can Create a new one and save the Old to 
      # Back up
      # But Remember that , that file can only parse 
      # the FOUR CONFIGURATION :
      # thread root port address
      # Watch out the case sensitive !!!
      # thread -- For the Worker thread number
      # root   -- For the WebSite's root path
      # port   -- Listen Port
      # address -- Host's address(Note it If you can)
      #            Or empty For the auto select by Operating System
      thread:8
      # Using shell Command (pwd) to show your root Path!
      root:/root/ClionProjects/httpd3/
      port:9998 # That is a port
      address:192.168.141.149
  • 配置文件讀取完成了,我們是時(shí)候設(shè)計(jì)一下主函數(shù)的流程了,回想一下流程圖,下一步就應(yīng)該創(chuàng)建套接字,綁定,并監(jiān)聽(listen)了!(流程圖中沒有畫出listen,過于冗余,但卻必不可少)
  • 可以將 創(chuàng)建,綁定合并成一個(gè)函數(shù),在成功之后,再執(zhí)行listen

      /*
       * Open The Listen Socket With the specific host(IP address) and port
       * That must be compatible with the IPv6 And IPv4
       * host_addr could be NULL
       * port MUST NOT BE NULL !!!
       * sock_type is the pointer to a memory ,which comes from the Outside(The Caller)
       * */
      int open_listenfd(const char * restrict host_addr,const char * restrict port, int * restrict sock_type);

    可以看出來,需要一個(gè)IP, 一個(gè)PORT, 第三個(gè)參數(shù)是套接字類型擔(dān)不是傳入?yún)?shù),而是傳出參數(shù)。

      int open_listenfd(const char * restrict host_addr, const char *     restrict port, int * restrict sock_type){
          int listenfd = 0; /* listen the Port, To accept the new Connection */
          struct addrinfo info_of_host;
          struct addrinfo * result;
          struct addrinfo * p;
    
          /* 實(shí)際上這一行完全可以在上面使用 初始化來達(dá)到目的。
           * struct addrinfo info_of_host = {0}; 需要c99
           */
          memset(&info_of_host, 0, sizeof(info_of_host));
          info_of_host.ai_family   = AF_UNSPEC; /* Unknown Socket Type */
          info_of_host.ai_flags    = AI_PASSIVE; /* Let the Program to help us fill the Message we need */
          info_of_host.ai_socktype = SOCK_STREAM; /* TCP */
    
          int error_code;
          if(0 != (error_code = getaddrinfo(host_addr, port,  &info_of_host, &result))){
              fputs(gai_strerror(error_code), stderr);
              return ERR_GETADDRINFO; /* -2 */
          }
    
          for(p = result; p != NULL; p = p->ai_next) {
              listenfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
    
              if(-1 == listenfd)
                  continue; /* Try the Next Possibility */
              optimizes(listenfd);
              if(-1 == bind(listenfd, p->ai_addr, p->ai_addrlen)){
                  close(listenfd);
                  continue; /* Same Reason */
              }
              break; /* If we get here, it means that we have succeed     to do all the Work */
          }
          freeaddrinfo(result);
          if (NULL == p) {
              fprintf(stderr, "In %s, Line: %d\nError Occur while Open/   Binding the listen fd\n",__FILE__, __LINE__);
              return ERR_BINDIND;
          }
          fprintf(stderr, "DEBUG MESG: Now We(%d) are in : %s , listen    the %s port Success\n", listenfd,
          inet_ntoa(((struct sockaddr_in *)p->ai_addr)->sin_addr), port);
          *sock_type = p->ai_family;
          set_nonblock(listenfd);
          return listenfd;
      }

    其中有一個(gè)optimizes,是用來設(shè)置一些套接字選項(xiàng)的,現(xiàn)在只需要知道有這些選項(xiàng)就行

    套接字選項(xiàng)分別是TCP_NODELAYSO_REUSEADDR

    細(xì)看之下,和前面介紹的幾個(gè)接口幾乎是完全一致的用法。但如果認(rèn)為網(wǎng)絡(luò)編程就是這樣接口調(diào)用的話,那就是大錯(cuò)特錯(cuò)。

    就這樣,如果你的配置文件中,<IP, PORT>沒什么差錯(cuò)的話,我們就完成了打開服務(wù)器套接字的工作,這時(shí)候你可以組織并且運(yùn)行一下前面說的這些代碼,看看是否如此。

運(yùn)行成功與否可以通過你的終端是否顯示上述的調(diào)試信息看出來:

DEBUG MESG: Now We(x) are in : %s , listen the xx port Success

  • 寫到這里,實(shí)際上整個(gè)主函數(shù)的代碼已經(jīng)接近尾聲,來看看全部的過程調(diào)用

      int main(int argc, char * argv[]) {
          wsx_config_t config = {0};
          init_config(&config)
    
          int sock_type = 0;
          int listenfd = open_listenfd(config.use_addr, config.listen_port, &sock_type);
          listen(listenfd, SOMAXCONN);
          signal(SIGPIPE, SIG_IGN);
          handle_loop(listenfd, sock_type, &config);
          return 0;
      }

    這個(gè)邏輯已經(jīng)十分清晰,為了方便我省去了錯(cuò)誤檢查,在代碼中應(yīng)該自己添加,這里面有兩個(gè)新事物: signal(), handle_loop()

  • 來解釋一下signal(SIGPIPE, SIG_IGN)是什么以及為什么

    • signal是信號(hào)函數(shù),還記得之前的章節(jié)用它來當(dāng)做函數(shù)指針類型的一個(gè)練習(xí)思考題嗎?它的作用就是在本進(jìn)程/線程接收到該信號(hào)(SIGPIPE)時(shí)候,會(huì)進(jìn)行這樣的(SIG_IGN)處理
    • 當(dāng)然它有更好更推薦的做法sigation,比較復(fù)雜但是也比較推薦你用它,這里為了減少概念,就用了最原始的signal。
    • SIGPIPE是一個(gè)關(guān)于寫的錯(cuò)誤,觸發(fā)條件是向一個(gè)發(fā)送了RST的對(duì)端進(jìn)行寫操作,默認(rèn)行為就是結(jié)束本進(jìn)程,我們當(dāng)然不愿意結(jié)束了,明明是對(duì)方的錯(cuò),怎么要我們死。最基本的做法就是忽略它SIG_IGN
    • 稍微解釋一下SIGPIPE,模擬一下情形,這里需要對(duì)TCP的工作方式有一定了解,不了解的可以跳過:
      • TCP是全雙工的,意味著可讀可寫,假設(shè)有A,B端,本來工作的好好的,突然B端崩潰退出了,那自然聯(lián)系A(chǔ),B端的套接字連接就斷了,但是A端并不懂啊,它這時(shí)候只知道B端不會(huì)再發(fā)送消息給自己了(因?yàn)榻拥搅薆發(fā)給自己的FIN,自己回復(fù)了ACK,關(guān)閉了接收通道),并不懂自己還能不能發(fā)消息給B啊(所以A當(dāng)做自己能發(fā)給B端)
      • 然而實(shí)際上,現(xiàn)在哪里還能發(fā)消息給B啊,這就回到了上面,如果向一個(gè)發(fā)送了RST的對(duì)端進(jìn)行寫操作的話,就會(huì)觸發(fā)SIGPIPE,信號(hào)這個(gè)東西就是全局的,所以如果你想知道哪個(gè)線程觸發(fā)了這個(gè)信號(hào),還需要檢查寫操作是否返回了EPIPE錯(cuò)誤
    • 看不懂也無所謂,來日方長,細(xì)水長流。這就是這一行代碼的意義,就是為了忽略這個(gè)信號(hào)。
  • handle_loop 是一個(gè)事件循環(huán)的入口

    • 就是所有的事務(wù)處理準(zhǔn)備都在里面,回想一下流程圖,我們接下來該干什么
    • 使用epoll監(jiān)聽服務(wù)器套接字,用來建立新連接
    • 分配新連接給子線程,在其中處理各種事件。
    • 吶,實(shí)際上handle_loop就干了兩件事
      • 準(zhǔn)備一下服務(wù)器資源(包括存儲(chǔ)新連接的各種信息)
      • 創(chuàng)建子線程用來 監(jiān)聽服務(wù)器套接字處理新連接事件
  • 幾個(gè)全局變量

      static int * epfd_group = NULL;   /* Workers' epfd set */
      static int   epfd_group_size = 0; /* Workers' epfd set size */
      static int   workers = 0;         /* Number of Workers */
      static int   listeners = MAX_LISTEN_EPFD_SIZE; /* Number of Listenner */
      static conn_client * clients;   /* Client set */
  • handle_loop()

      void handle_loop(int file_dsption, int sock_type, const wsx_config_t * config) {
          workers = config->core_num - listeners;
          int listen_epfd = epoll_create1(0);
          { /* Register listen fd to the listen_epfd */
              struct epoll_event event;
              event.data.fd = file_dsption;
              event.events = EPOLLET | EPOLLERR | EPOLLIN;
              /* 以ET方式監(jiān)聽file_dsption的讀事件,錯(cuò)誤事件 */
              epoll_ctl(listen_epfd, EPOLL_CTL_ADD, file_dsption, &event);
          }
          /* Prepare Workers Sources */
          prepare_workers(config);
          pthread_t listener_set[listeners];
          pthread_t worker_set[workers];
          for (int i = 0; i < listeners; ++i) 
              pthread_create(&listener_set[i], NULL, listen_thread, (void*)listen_epfd);
          for (int j = 0; j < workers; ++j) {
              pthread_create(&worker_set[j], NULL, workers_thread, (void*)(epfd_group[j]));
              pthread_detach(worker_set[j]);
          }
          for (int k = 0; k < listeners; ++k) 
              pthread_join(listener_set[k], NULL);
          destroy_resouce();
      }

    使用了最原始的線性數(shù)組來存儲(chǔ)所有的連接信息(conn_client),這其實(shí)弊端很大,比如最明顯的數(shù)量以及預(yù)分配的資源過大。但關(guān)鍵是夠簡單,且效率最高。

    整個(gè)的原理就是,在接受到新連接以后,按照某種規(guī)則分配給第i個(gè)子線程,每個(gè)子線程中有一個(gè)工作epoll(epoll_group[i-1]),用來監(jiān)聽新連接的事件,并處理。

    prepare_workers 就是分配內(nèi)存空間的相關(guān)工作。這段代碼,同樣省略了錯(cuò)誤檢查,希望自己添加。

    {}里面可以看出來怎么向epoll實(shí)例中注冊(cè)監(jiān)聽實(shí)體,以及監(jiān)聽事件。

    整段代碼的后半部分,是關(guān)于線程的啟動(dòng),操作,銷毀。pthread_detach意味著放棄線程的資源回收權(quán),用通俗的話來說就是:“撒丫子跑吧,我管不著你了!”。

  • 這就是完整的一個(gè)主函數(shù)邏輯,實(shí)際上非常簡單,到現(xiàn)在為止也沒出現(xiàn)過十分復(fù)雜的東西,就像在做繁瑣的準(zhǔn)備工作一樣。

下一節(jié)將會(huì)詳細(xì)講解

  1. 連接信息都有哪些需要存儲(chǔ)的
  2. 如何處理讀事件,字符數(shù)據(jù)的管理呢?
上一篇:我的C語言下一篇:0x05-C語言變量