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

鍍金池/ 教程/ C/ 0x16-套接字編程-HTTP服務(wù)器(4)
0x0E-單線程備份(下)
0x11-套接字編程-1
0x05-C語言指針:(Volume-1)
0x13-套接字編程-HTTP服務(wù)器(1)
0x0C-開始行動
C 語言進階
第一部分
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語言錯誤處理

0x16-套接字編程-HTTP服務(wù)器(4)

0x16-套接字編程-HTTP服務(wù)器(4)

新連接

  1. 一個新晉連接,有哪些信息是值得我們關(guān)注的?
  2. 該如何存儲它們?

這里將會敘述的并不會很完整,因為不同目的的網(wǎng)絡(luò)程序,需要關(guān)注的信息也大不相同

特別是這個程序關(guān)注的是如何使用C語言編寫一個服務(wù)器

  1. 我們最關(guān)心的,還是對端通過這個新連接所發(fā)來的信息
    • 簡單來說就是我們read到的信息。進行過系統(tǒng)編程的都應(yīng)該會知道這個函數(shù),與之對應(yīng)的是write。與 C標(biāo)準(zhǔn)庫 為我們提供的標(biāo)準(zhǔn)格式化輸入輸出不同的地方在于其操作的對象。read/write操作的是一個在叫做 文件描述符(file description)int類型的東西,而標(biāo)準(zhǔn)庫的函數(shù)(printf/scanf)操作的則是一個FILE*特殊的結(jié)構(gòu)體指針,這兩者之間可以互相轉(zhuǎn)換,通過fdopen(fd-->FILE*)/fileno(FILE*-->fd)具體相關(guān)知識,查閱相關(guān)信息,如著名的APUE。
  2. 其次我們對這個信息做相應(yīng)處理,中間會有很多狀態(tài),也就是常常聽到的HTTP狀態(tài)機
    • 實際上也就是幾個狀態(tài)值在轉(zhuǎn)換和過渡,只是名字專業(yè)了一些
  3. 最后我們會生成一個信息,用來回復(fù)對端
    1. 這個也叫做響應(yīng)報文

*nix下的文件描述符(file description)Windows下近似相當(dāng)于 文件句柄(file handler),只不過前者是有規(guī)律的遞增,而后者則不是。

  1. 如何存儲?

      typedef unsigned char boolean;
      struct connection {
          int  file_dsp;
      #define CONN_BUF_SIZE 512
          int r_buf_offset;
          int w_buf_offset;
          string_t r_buf;
          string_t w_buf;
          struct {
              /* Is it Keep-alive in Application Layer */
              boolean conn_linger  : 1;
              boolean set_ep_out   : 1;
              boolean is_read_done : 1; /* Read from Peer Done? */
              boolean request_http_v : 2; /* HTTP/1.1 1.0 0.9 2.0 */
              boolean request_method : 2; /* GET HEAD POST */
              int content_type : 4;  /* 2 ^ 4 -> 16 Types */
              int content_length; /* For POST */
              string_t requ_res_path; /* / */
          }conn_res;
      };
      typedef struct connection conn_client;

    其中有一個陌生的事物,string_t,這個是用來進行字符串操作的一個自己寫的結(jié)構(gòu),用于簡化操作,可以把它看成一個可以自動增長的字符串類型。

    再者就是,內(nèi)嵌結(jié)構(gòu)體中使用到了 位域 這個方式,主要是因為C中沒有原生的bool類型,使用int來表示又太過奢侈

    這個位域的寫法在某些人看來似乎不太感冒,實際上還有替代的方法可以用,也就是使用掩碼的思想,在一個int型中的不同位包含不同的信息,實際上和我這個的原理是相同的,只不過我將它拆開了,這樣就可以不寫各種處理宏

      /* 另一種寫法 */
      ...
      struct {
          int status_set;
          int content_length;
          string_t request_length;
      }conn_res
      ...
      enum {
          SET_CONN_LINGGER = 1,
          SET_EPOLLOUT = 1 << 1,
          ...
      }
      /* 幾乎對于每一個位置的操作都有三個,設(shè)置,復(fù)位,檢測 */
      #define SET_CONN_LINGER(MASK_SET) (MASK_SET &= SET_CONN_LINGER)
      #define CLR_CONN_LINGER(MASK_SET) (MASK_SET &= (~SET_CONN_LINGER)&0xFFFF)
      #define IS_CONN_LINGER(MASK_SET) (MASK_SET & SET_CONN_LINGER)

    依此類推。

實際上,對于這個string_t 的設(shè)計是一個想當(dāng)然的失敗,當(dāng)時是想嘗試使用面向?qū)ο蟮南敕?,但是沒有考慮到其使用時候的冗余,后面會看到這個小麻煩,但是總體上還是可以得。

這次總結(jié)出來的就是,在C里面使用面向?qū)ο蟮乃季S實在有點勉強,具體等后方說到這個string_t時會再提到。

2016-08-28 將其修改為正常的C風(fēng)格。

  1. 所以實際上來看一看,我存儲了哪些狀態(tài)信息

    • 一個新連接的 file description file_dsp: 這個肯定是必要的,不然你怎么對這個新連接進行操作。
    • 一個讀緩沖配著一個讀位移(r_bufr_buf_offset) :
      • 之所以需要位移,是因為你要牢牢記住,尤其是在網(wǎng)絡(luò)通信中,總會出現(xiàn)網(wǎng)絡(luò)不穩(wěn)的狀況,這會導(dǎo)致某時候你的信息不能完全一次新的讀取到,也就是需要分次讀取,所以你需要知道上次你讀到哪里
      • 另一個原因是因為,在解析讀取的信息的時候,你要時刻知道自己處理到哪里了,是否接收到數(shù)據(jù)不完整?是否接收的數(shù)據(jù)有錯?等等。
    • 一個寫緩沖配著一個寫位移('w_buf'和w_buf_offset)
      • 寫事件要比讀事件簡單許多。
    • 一個包含HTTP狀態(tài)的屬性結(jié)構(gòu)conn_res
      • conn_linger : 是否保持連接(keep-alive)
      • set_ep_out : 是否設(shè)置監(jiān)聽寫事件(EPOLLOUT)
      • is_read_done : 是否已經(jīng)讀取信息完畢
      • request_http_v : HTTP協(xié)議版本
      • request_method : HTTP請求方法
      • content_type : 響應(yīng)報文 中的 屬性
      • content_length : 同上
      • requ_res_path : 對端想請求的資源
  2. 所以這也從另一個方面回答了上面的第二個問題 該如何存儲它們?
  3. 了解過,要存儲那些信息,該如何存儲這些信息之后,就能繼續(xù)服務(wù)器的編寫

事件循環(huán)

  • 前面我們的進度,已經(jīng)到了handle_loop里面,并且將總體流程已經(jīng)過了一遍
  • handle_loop 就是一個事件循環(huán),我們整個程序的編程模型就是一個 事件驅(qū)動 的編程體系,什么是事件驅(qū)動,可以查閱相關(guān)資料,如 UNP 等書籍。在這個事件循環(huán)中,我們使用兩個事件驅(qū)動我們的流程 : 讀事件寫事件
  • 即,一旦某個連接可讀(回憶一下TCP連接可讀可寫)我就處理讀事件,寫事件也是如此。
  • 在這個循環(huán)中,我們啟動了兩種線程,一種專門用于接受建立新連接,一種專門用來處理新連接的讀寫事件,分別是listen_threadworkers_thread,常理來說前者一個就夠了,后者可以酌情處理。
  • 先說說比較簡單的listen_thread

listen_thread

  • 回到handle_loop的代碼中可以看到有一個獨立的代碼塊{},這個代碼塊的作用就是將我們之前創(chuàng)建的服務(wù)器套接字,添加到一個epoll實例中,準(zhǔn)備傳給listen_thread。在該epoll實例中,我們監(jiān)聽了它的讀事件,以及錯誤事件 EPOLLERR

      { /* Register listen fd to the listen_epfd */
          struct epoll_event event;
          event.data.fd = file_dsption;
          event.events = EPOLLET | EPOLLERR | EPOLLIN;
          epoll_ctl(listen_epfd, EPOLL_CTL_ADD, file_dsption, &event);
      }
  • 緊接著,我們需要創(chuàng)建線程,用來完成接受創(chuàng)建新連接, 分配新連接, 處理新連接
  • 先說前兩個

  • listen_thread

      /* Listener's Thread
      * @param arg will be a epoll instance
      * */
      static void * listen_thread(void * arg) {
          int listen_epfd = (int)arg;
          struct epoll_event new_client = {0};
          /* Adding new Client Sock to the Workers' thread */
          int balance_index = 0;
          while (terminal_server != CLOSE_SERVE) {

    這是一個永不停止的循環(huán),除非在外部傳入了一個信號CTRL+C,其實沒什么意義,不過還是寫了

              //這是監(jiān)聽的阻塞地點,在此處會返回有多少個事件發(fā)生了,當(dāng)然這里只有一個
              int is_work = epoll_wait(listen_epfd, &new_client, 1, 2000);
              int sock = 0;
              // 如果不是因為超時才到了這里
              while (is_work > 0) { /* New Connect */
                  //接受并創(chuàng)建新連接
                  sock = accept(new_client.data.fd, NULL, NULL);
                  if (sock > 0) {
                      // 如果沒有意外的話
                      set_nonblock(sock);
                      clear_clients(&clients[sock]);
                      clients[sock].file_dsp = sock;
                      // 分配新連接給各個workers_thread
                      add_event(epfd_group[balance_index], sock, EPOLLIN);
                      balance_index = (balance_index+1) % workers;
                  } else /* sock == -1 means nothing to accept */
                      break;
              } /* new Connect */
          }/* main while */
          close(listen_epfd);
          pthread_exit(0);
      }

其實在上面的 acceptset_nonblock 可以用一個系統(tǒng)調(diào)用來解決,accept4,而不需要使用兩個不同的系統(tǒng)調(diào)用來完成這個功能,具體可以查詢文檔。

  • 可以看出,這個listen_thread 的職責(zé)非常簡單,就只是單純的接受創(chuàng)建新連接,設(shè)置一些屬性,并且分配給workers_thread,所以真正復(fù)雜的工作還是在后者身上

workers_thread

  • 這是整個程序的核心部分,但還是按照庖丁解牛的方法,一步步分解
  • 整個的代碼有點冗長,但是邏輯十分清晰,大體可以分成讀寫兩部分

      static void * workers_thread(void * arg) {
          int deal_epfd = (int)arg;
          struct epoll_event new_apply = {0};
          while(terminal_server != CLOSE_SERVE) {
              int is_apply = epoll_wait(deal_epfd, &new_apply, 1, 2000);
              if(is_apply > 0) { /* New Apply */
                  int sock = new_apply.data.fd;
                  conn_client * new_client = &clients[sock];

    到此處為止,前面的邏輯和listen_thread 十分相似,需要額外說的就是 epoll_wait 接口中的第二,三個參數(shù) , 代表著有事件改變狀態(tài)的新連接(new_apply[i]),和有多少個這樣的新連接(i)。代碼中寫的是(,&new_apply,1,)代表著我每次只想得到一個,說明及替代方案在后面會提到,跳過也無所謂。

                  /* 讀事件 */
                  if (new_apply.events & EPOLLIN) { /* Reading Work */
                      /* handle_read 是接收并解析HTTP請求報文的地方 */
                      int err_code = handle_read(new_client);
                      /* 此處省略一個很重要的分片錯誤處理 */
                      else if (err_code != HANDLE_READ_SUCCESS) {
                          /* Read Bad Things */
                          close(sock);
                          continue;
                      }
                  } // Read Event

    以上便是簡化的讀事件的處理,拋開來看,一切的核心就是handle_read這個函數(shù),后放會詳細(xì)講解。

                  /* 寫事件 */
                  else if (new_apply.events & EPOLLOUT) { /* Writing Work */
                      int err_code = handle_write(new_client);
                      /* TCP's Write buffer is Busy */
                      if (HANDLE_WRITE_AGAIN == err_code) 
                          mod_event(deal_epfd, sock, EPOLLONESHOT | EPOLLOUT);
                      else if (HANDLE_WRITE_FAILURE == err_code) { /* Peer Close */
                          close(sock);
                          continue;
                      }
                      /* if Keep-alive */
                      if(1 == new_client->conn_res.conn_linger)
                          mod_event(deal_epfd, sock, EPOLLIN);
                      else{
                          close(sock);
                          continue;
                      }
                  } /* EPOLLOUT */

    所謂clear_clients其實就是清除一些現(xiàn)有狀態(tài),不然下次有別的連接占用的時候就會錯亂了。

                  else { /* EPOLLRDHUG EPOLLERR EPOLLHUG */
                      close(sock);
                  }
              } /* New Apply */
          } /* main while */
          return (void*)0;
      }
  • 看起來有點長,實際上模塊十分清楚。從上往下看,由三個if - else 分支組成,分別處理 讀事件,寫事件,錯誤事件
  • 這其中省略了一些十分重要的錯誤處理,以及某些優(yōu)化,希望可以自己補全,但這都無所謂,因為已經(jīng)將這種編程模型全盤托出,接下來就是細(xì)節(jié)方面的處理了。

handle_read

  • 這應(yīng)該是這個 HTTP服務(wù)器 真正的重點所在,用一個詞來形容就是 核心技術(shù),當(dāng)然沒那么高端,就是個程序而已。
  • 前面提到一個名詞,叫做 HTTP狀態(tài)機,指的就是狀態(tài)的轉(zhuǎn)換,在C語言中,可以使用enum來實現(xiàn)

      typedef enum {
          HANDLE_READ_SUCCESS = -(1 << 1),
          HANDLE_READ_FAILURE = -(1 << 2),
          ...
      }HANDLE_STATUS;

    代表了,handle_read 是成功還是失敗,有一個額外的 MESSAGE_IMCOMPLETE 狀態(tài)也輸一這個范疇內(nèi),但是設(shè)計的時候出現(xiàn)了差錯,可以選擇將其放在里面。

    MESSAGE_IMCOMPLETE 是為了應(yīng)對TCP分片問題,所以在顯示網(wǎng)絡(luò)中很常見,但是本地測試的時候可能不容易發(fā)現(xiàn),可以使用工具 tc 來模擬弱環(huán)境。

  • HANDLE_STATUS handle_read(conn_client * client)

      HANDLE_STATUS handle_read(conn_client * client) {
          int err_code = 0;
          /* Reading From Socket */
          err_code = read_n(client);
          if (err_code != READ_SUCCESS) { /* If read Fail then End this connect */
              return HANDLE_READ_FAILURE;
          }

    到這里為止是讀取所有可以讀到的數(shù)據(jù)

          /* Parsing the Reading Data */
          err_code = parse_reading(client);
          if (err_code == MESSAGE_INCOMPLETE)
              return MESSAGE_INCOMPLETE;
          if (err_code != PARSE_SUCCESS) { /* If Parse Fail then End this connect */
              return HANDLE_READ_FAILURE;
          }

    到這里為止是處理所有已經(jīng)讀到的數(shù)據(jù)

          return HANDLE_READ_SUCCESS;
      }

    到了這里,就證明讀和處理都已經(jīng)正確完成了。

巧用gdb能讓你輕松理解整個狀態(tài)機的邏輯

  • 從函數(shù)接口上看,它接受一個conn_client類型的指針,回想一下,這就是我們存儲每個新連接的各種信息的地方,返回值就是這個動作的狀態(tài)了。
  • 從功能上看,這個函數(shù)主要的工作就是將handle_read拆分成兩大部分:

    1. 讀取數(shù)據(jù) (read_n)
      1. 首先讀取所有能讀取的數(shù)據(jù)(從socket中)
      2. 驗證數(shù)據(jù)是否完整
        1. 對于GET 而言就是是否讀取到了一個空行\r\n
        2. 對于POST 來說就是是否一句Content-length屬性的值將 body 讀取完整了
    2. 處理數(shù)據(jù) (parse_reading)
      1. 處理HTTP請求報文第一行狀態(tài)行
      2. 處理剩余的頭屬性,如Connection
      3. 生成響應(yīng)報文,你可以考慮將這一步劃分出去,因為這一步涉及到了磁盤I/O
  • 先說第一部分,讀取數(shù)據(jù)(read_n)
  • static int read_n(conn_client * client)

  • 實現(xiàn)一個read函數(shù)的加強版

      __thread char read_buf2[CONN_BUF_SIZE] = {0};
      static int read_n(conn_client * client) {
          int    read_offset2 = 0;
          int    fd        = client->file_dsp;
          char * buf       = &read_buf2[0];
          int    buf_index = read_offset2;
          int read_number = 0;
          int less_capacity = 0;

    從前往后依次是讀緩沖區(qū)位移, 處理的連接套接字, buf純粹多此一舉還可能阻礙編譯器優(yōu)化,但我還是寫了,強迫癥吧, buf_index同理,read_number是本次讀的字符個數(shù),less_capacity是緩沖區(qū)的容量余量

          while (1) {
              /* 因為是非阻塞,所以要不停地讀,直到`read`返回-1,且errno為EAGAIN */
              less_capacity = CONN_BUF_SIZE - buf_index;
              if (less_capacity <= 1) {/* Overflow Protection */
                  /* 萬一這本地的緩沖區(qū)容量不夠了,就刷新進 conn_client 中 */
                  buf[buf_index] = '\0'; /* Flush the buf to the r_buf String */
                  /* 對于 STRING 宏,可以看看我的源碼中的 wsx_string.h */
                  cappend_string(client->r_buf, STRING(buf));
                  client->r_buf_offset += read_offset2;//- client->read_offset;
                  read_offset2 = 0;
                  buf_index = 0;
                  less_capacity = CONN_BUF_SIZE - buf_index;
                  /* 清空緩沖區(qū)成功 */
              }

    上面的代碼中,有一個APPEND宏,是用來簡化代碼的,功能是 #define APPEND(str) str,(strlen(str)+1)

              read_number = (int)read(fd, buf+buf_index, less_capacity);
              /* 0代表對端關(guān)閉了連接或者說是已經(jīng)讀完了 EOF(對端調(diào)用close()/shutdown()) */
              if (0 == read_number) { /* We must close connection */
                  return READ_FAIL;
              }
              /* -1 代表現(xiàn)在沒東西可以讀了 */
              else if (-1 == read_number) { /* Nothing to read */
                  if (EAGAIN == errno || EWOULDBLOCK == errno) {
                      /* 這個時候,我們該做的就是將緩沖區(qū)的東西,存儲起來 */
                      buf[buf_index] = '\0';
                      append_string(client->r_buf, STRING(buf));
                      client->r_buf_offset += read_offset2;//client->read_offset;
                      return READ_SUCCESS;
                  }
                  return READ_FAIL;
              }
              else { /* Continue to Read */
                  /* 能讀取到信息,就繼續(xù)讀 */
                  buf_index += read_number;
                  read_offset2 = buf_index;
              } 
          } /* while(1) */
      }

__thread 關(guān)鍵字是多線程編程里一個挺有用的一個關(guān)鍵字,具體可以查詢資料,簡單來說,就是讓每個線程擁有一個自己的全局變量。

  • 經(jīng)過read_n之后,我們就(可能)獲取到了完整的數(shù)據(jù)了,接下來就是解析它們,引入一個狀態(tài)
  • PARSE_STATUS

      typedef enum {
      /* Parse the Reading Success, set the event to Write Event */
          PARSE_SUCCESS    = 1 << 1,
      /* Parse the Reading Fail, for the Wrong Syntax */ 
          PARSE_BAD_SYNTAX = 1 << 2, 
      /* Parse the Reading Success, but Not Implement OR No Such Resources*/
          PARSE_BAD_REQUT  = 1 << 3, 
      }PARSE_STATUS;

    解釋的很清楚了,不再贅述。

  • PARSE_STATUS parse_reading(conn_client * client)

      PARSE_STATUS parse_reading(conn_client * client) {
          int err_code = 0;
          requ_line line_status = {0};
          client->r_buf_offset = 0; /* Set the real Storage offset to 0, the end of buf is '\0' */

    requ_line是一個結(jié)構(gòu)體,用來存儲狀態(tài)行所含有的三個信息: 請求方法, 請求資源, HTTP版本號

          /* Get Request line */
          err_code = deal_requ(client, &line_status);
          /* 回想一下這個狀態(tài),TCP分片的情況 */
          if (MESSAGE_INCOMPLETE == err_code)  /* Incompletely reading */
              return MESSAGE_INCOMPLETE;
          if (DEAL_LINE_REQU_FAIL == err_code) /* Bad Request */
              return PARSE_BAD_REQUT;

    到這里為止是處理狀態(tài)行的代碼

          /* Get Request Head Attribute until /r/n */
          err_code = deal_head(client);  /* The second line to the Empty line */
          if (DEAL_HEAD_FAIL == err_code)
              return PARSE_BAD_SYNTAX;

    到這里為止是處理完了所有的頭屬性

          /* Response Page maker */
          err_code = make_response_page(client);  
          if (MAKE_PAGE_FAIL == err_code)
              return PARSE_BAD_REQUT;
    
          return PARSE_SUCCESS;
      }
  • 對于deal_requdeal_head來說,只是一個很簡單的從大字符串中識別出小字符串,并存儲起來的問題,不想過多的敘述。在這個處理過程中,自己實現(xiàn)了一個get_line按行讀取的函數(shù),同樣會被后面的deal_head使用

    • 這其中有一些問題需要注意一下,那就是你需要考慮TCP分片問題,這是我第三次提到這個東西,也就是用狀態(tài)機監(jiān)測好這個問題是否發(fā)生,并及時處理。
    • deal_head中,可以按行進行循環(huán)讀取(get_line),知道你發(fā)現(xiàn)空行,那么你就處理完成了,如果是POST方法,你還需要繼續(xù)讀取,直到讀取完它的body?,F(xiàn)在想想,conn_client這個結(jié)構(gòu)體中的那些屬性是干什么的,就是從這里解析出來的。
  • 讀取解析完成之后,就能進行響應(yīng)報文的生成了。在下一節(jié)中詳述

題外話

  • 和上一個部分不同,再上一個部分我盡可能的不落下一絲一毫的細(xì)節(jié),將自己如何寫程序的想法分享給諸位
  • 但這章節(jié),無論怎么看,從思維,從代碼都不再像之前那般面面俱到,我認(rèn)為也沒有必要,這一章大家應(yīng)該就具備了自我獨立思考的能力,實際上在給出了結(jié)構(gòu)圖之后,后面的章節(jié)就不怎么必要了
  • 但我想把自己的想法寫出來,想想求學(xué)的這幾年無人引導(dǎo),苦苦尋找資料的那些日子,我覺得我有必要把自己從網(wǎng)絡(luò)上得來的知識,再次回饋給網(wǎng)絡(luò),這才是生生不息,自我進步的道理。

最后

  • 額外的補充

  • 這個經(jīng)驗分享系列馬上就要到頭了,下一步的我也許就該畢業(yè)了

    • 也許在最后一年,我會用最后的時間完成額外的章節(jié)
    • 額外的章節(jié)有過想法,就是寫一個完整可用的數(shù)據(jù)庫系統(tǒng)
    • 這個工程量遠(yuǎn)超前方章節(jié),如果有想法我會及時在本書中更新動態(tài)
    • 希望大家也能夠?qū)⒆约褐赖?,學(xué)到的知識貢獻(xiàn)出來
  • 如果覺得我說的還行,可以給我來一點鼓勵呀
    • 1

      下一節(jié)

  • 講述如何生成響應(yīng)報文,以及本章的收尾。