在线观看不卡亚洲电影_亚洲妓女99综合网_91青青青亚洲娱乐在线观看_日韩无码高清综合久久

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

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

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

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

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

    這個是配置文件的所有屬性,可以將讀取的參數(shù),存進這個結(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

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

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

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

      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é)束

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

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

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

    當然你也可以寫成 json 的形式,再用第三方庫,比如c-json之類的解析,但 那不是要依賴第三方了嗎?所以我的建議還是自己寫一個解析的函數(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ù)的流程了,回想一下流程圖,下一步就應該創(chuàng)建套接字,綁定,并監(jiān)聽(listen)了?。鞒虉D中沒有畫出listen,過于冗余,但卻必不可少)
  • 可以將 創(chuàng)建,綁定合并成一個函數(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);

    可以看出來,需要一個IP, 一個PORT, 第三個參數(shù)是套接字類型擔不是傳入?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;
    
          /* 實際上這一行完全可以在上面使用 初始化來達到目的。
           * 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;
      }

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

    套接字選項分別是TCP_NODELAYSO_REUSEADDR。

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

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

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

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

  • 寫到這里,實際上整個主函數(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;
      }

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

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

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

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

      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的讀事件,錯誤事件 */
              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ù)組來存儲所有的連接信息(conn_client),這其實弊端很大,比如最明顯的數(shù)量以及預分配的資源過大。但關(guān)鍵是夠簡單,且效率最高。

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

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

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

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

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

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

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