Linux中國

並發伺服器(一):簡介

這是關於並發網路伺服器編程的第一篇教程。我計劃測試幾個主流的、可以同時處理多個客戶端請求的伺服器並發模型,基於可擴展性和易實現性對這些模型進行評判。所有的伺服器都會監聽套接字連接,並且實現一些簡單的協議用於與客戶端進行通訊。

該系列的所有文章:

協議

該系列教程所用的協議都非常簡單,但足以展示並發伺服器設計的許多有趣層面。而且這個協議是 有狀態的 —— 伺服器根據客戶端發送的數據改變內部狀態,然後根據內部狀態產生相應的行為。並非所有的協議都是有狀態的 —— 實際上,基於 HTTP 的許多協議是無狀態的,但是有狀態的協議也是很常見,值得認真討論。

在伺服器端看來,這個協議的視圖是這樣的:

總之:伺服器等待新客戶端的連接;當一個客戶端連接的時候,伺服器會向該客戶端發送一個 * 字元,進入「等待消息」的狀態。在該狀態下,伺服器會忽略客戶端發送的所有字元,除非它看到了一個 ^ 字元,這表示一個新消息的開始。這個時候伺服器就會轉變為「正在通信」的狀態,這時它會向客戶端回送數據,把收到的所有字元的每個位元組加 1 回送給客戶端 注1 。當客戶端發送了 $ 字元,伺服器就會退回到等待新消息的狀態。^$ 字元僅僅用於分隔消息 —— 它們不會被伺服器回送。

每個狀態之後都有個隱藏的箭頭指向 「等待客戶端」 狀態,用於客戶端斷開連接。因此,客戶端要表示「我已經結束」的方法很簡單,關掉它那一端的連接就好。

顯然,這個協議是真實協議的簡化版,真實使用的協議一般包含複雜的報文頭、轉義字元序列(例如讓消息體中可以出現 $ 符號),額外的狀態變化。但是我們這個協議足以完成期望。

另一點:這個系列是介紹性的,並假設客戶端都工作的很好(雖然可能運行很慢);因此沒有設置超時,也沒有設置特殊的規則來確保伺服器不會因為客戶端的惡意行為(或是故障)而出現阻塞,導致不能正常結束。

順序伺服器

這個系列中我們的第一個服務端程序是一個簡單的「順序」伺服器,用 C 進行編寫,除了標準的 POSIX 中用於套接字的內容以外沒有使用其它庫。伺服器程序是順序,因為它一次只能處理一個客戶端的請求;當有客戶端連接時,像之前所說的那樣,伺服器會進入到狀態機中,並且不再監聽套接字接受新的客戶端連接,直到當前的客戶端結束連接。顯然這不是並發的,而且即便在很少的負載下也不能服務多個客戶端,但它對於我們的討論很有用,因為我們需要的是一個易於理解的基礎。

這個伺服器的完整代碼在這裡;接下來,我會著重於一些重點的部分。main 函數裡面的外層循環用於監聽套接字,以便接受新客戶端的連接。一旦有客戶端進行連接,就會調用 serve_connection,這個函數中的代碼會一直運行,直到客戶端斷開連接。

順序伺服器在循環里調用 accept 用來監聽套接字,並接受新連接:

while (1) {
  struct sockaddr_in peer_addr;
  socklen_t peer_addr_len = sizeof(peer_addr);

  int newsockfd =
      accept(sockfd, (struct sockaddr*)&peer_addr, &peer_addr_len);

  if (newsockfd < 0) {
    perror_die("ERROR on accept");
  }

  report_peer_connected(&peer_addr, peer_addr_len);
  serve_connection(newsockfd);
  printf("peer donen");
}

accept 函數每次都會返回一個新的已連接的套接字,然後伺服器調用 serve_connection;注意這是一個 阻塞式 的調用 —— 在 serve_connection 返回前,accept 函數都不會再被調用了;伺服器會被阻塞,直到客戶端結束連接才能接受新的連接。換句話說,客戶端按 順序 得到響應。

這是 serve_connection 函數:

typedef enum { WAIT_FOR_MSG, IN_MSG } ProcessingState;

void serve_connection(int sockfd) {
  if (send(sockfd, "*", 1, 0) < 1) {
    perror_die("send");
  }

  ProcessingState state = WAIT_FOR_MSG;

  while (1) {
    uint8_t buf[1024];
    int len = recv(sockfd, buf, sizeof buf, 0);
    if (len < 0) {
      perror_die("recv");
    } else if (len == 0) {
      break;
    }

    for (int i = 0; i < len; ++i) {
      switch (state) {
      case WAIT_FOR_MSG:
        if (buf[i] == &apos;^&apos;) {
          state = IN_MSG;
        }
        break;
      case IN_MSG:
        if (buf[i] == &apos;$&apos;) {
          state = WAIT_FOR_MSG;
        } else {
          buf[i] += 1;
          if (send(sockfd, &buf[i], 1, 0) < 1) {
            perror("send error");
            close(sockfd);
            return;
          }
        }
        break;
      }
    }
  }

  close(sockfd);
}

它完全是按照狀態機協議進行編寫的。每次循環的時候,伺服器嘗試接收客戶端的數據。收到 0 位元組意味著客戶端斷開連接,然後循環就會退出。否則,會逐位元組檢查接收緩存,每一個位元組都可能會觸發一個狀態。

recv 函數返回接收到的位元組數與客戶端發送消息的數量完全無關(^...$ 閉合序列的位元組)。因此,在保持狀態的循環中遍歷整個緩衝區很重要。而且,每一個接收到的緩衝中可能包含多條信息,但也有可能開始了一個新消息,卻沒有顯式的結束字元;而這個結束字元可能在下一個緩衝中才能收到,這就是處理狀態在循環迭代中進行維護的原因。

例如,試想主循環中的 recv 函數在某次連接中返回了三個非空的緩衝:

  1. ^abc$de^abte$f
  2. xyz^123
  3. 25$^ab$abab

服務端返回的是哪些數據?追蹤代碼對於理解狀態轉變很有用。(答案見 2

多個並發客戶端

如果多個客戶端在同一時刻向順序伺服器發起連接會發生什麼事情?

伺服器端的代碼(以及它的名字 「順序伺服器」)已經說的很清楚了,一次只能處理 一個 客戶端的請求。只要伺服器在 serve_connection 函數中忙於處理客戶端的請求,就不會接受別的客戶端的連接。只有當前的客戶端斷開了連接,serve_connection 才會返回,然後最外層的循環才能繼續執行接受其他客戶端的連接。

為了演示這個行為,該系列教程的示例代碼 包含了一個 Python 腳本,用於模擬幾個想要同時連接伺服器的客戶端。每一個客戶端發送類似之前那樣的三個數據緩衝 注3 ,不過每次發送數據之間會有一定延遲。

客戶端腳本在不同的線程中並發地模擬客戶端行為。這是我們的序列化伺服器與客戶端交互的信息記錄:

$ python3.6 simple-client.py  -n 3 localhost 9090
INFO:2017-09-16 14:14:17,763:conn1 connected...
INFO:2017-09-16 14:14:17,763:conn1 sending b&apos;^abc$de^abte$f&apos;
INFO:2017-09-16 14:14:17,763:conn1 received b&apos;b&apos;
INFO:2017-09-16 14:14:17,802:conn1 received b&apos;cdbcuf&apos;
INFO:2017-09-16 14:14:18,764:conn1 sending b&apos;xyz^123&apos;
INFO:2017-09-16 14:14:18,764:conn1 received b&apos;234&apos;
INFO:2017-09-16 14:14:19,764:conn1 sending b&apos;25$^ab0000$abab&apos;
INFO:2017-09-16 14:14:19,765:conn1 received b&apos;36bc1111&apos;
INFO:2017-09-16 14:14:19,965:conn1 disconnecting
INFO:2017-09-16 14:14:19,966:conn2 connected...
INFO:2017-09-16 14:14:19,967:conn2 sending b&apos;^abc$de^abte$f&apos;
INFO:2017-09-16 14:14:19,967:conn2 received b&apos;b&apos;
INFO:2017-09-16 14:14:20,006:conn2 received b&apos;cdbcuf&apos;
INFO:2017-09-16 14:14:20,968:conn2 sending b&apos;xyz^123&apos;
INFO:2017-09-16 14:14:20,969:conn2 received b&apos;234&apos;
INFO:2017-09-16 14:14:21,970:conn2 sending b&apos;25$^ab0000$abab&apos;
INFO:2017-09-16 14:14:21,970:conn2 received b&apos;36bc1111&apos;
INFO:2017-09-16 14:14:22,171:conn2 disconnecting
INFO:2017-09-16 14:14:22,171:conn0 connected...
INFO:2017-09-16 14:14:22,172:conn0 sending b&apos;^abc$de^abte$f&apos;
INFO:2017-09-16 14:14:22,172:conn0 received b&apos;b&apos;
INFO:2017-09-16 14:14:22,210:conn0 received b&apos;cdbcuf&apos;
INFO:2017-09-16 14:14:23,173:conn0 sending b&apos;xyz^123&apos;
INFO:2017-09-16 14:14:23,174:conn0 received b&apos;234&apos;
INFO:2017-09-16 14:14:24,175:conn0 sending b&apos;25$^ab0000$abab&apos;
INFO:2017-09-16 14:14:24,176:conn0 received b&apos;36bc1111&apos;
INFO:2017-09-16 14:14:24,376:conn0 disconnecting

這裡要注意連接名:conn1 是第一個連接到伺服器的,先跟伺服器交互了一段時間。接下來的連接 conn2 —— 在第一個斷開連接後,連接到了伺服器,然後第三個連接也是一樣。就像日誌顯示的那樣,每一個連接讓伺服器變得繁忙,持續了大約 2.2 秒的時間(這實際上是人為地在客戶端代碼中加入的延遲),在這段時間裡別的客戶端都不能連接。

顯然,這不是一個可擴展的策略。這個例子中,客戶端中加入了延遲,讓伺服器不能處理別的交互動作。一個智能伺服器應該能處理一堆客戶端的請求,而這個原始的伺服器在結束連接之前一直繁忙(我們將會在之後的章節中看到如何實現智能的伺服器)。儘管服務端有延遲,但這不會過度佔用 CPU;例如,從資料庫中查找信息(時間基本上是花在連接到資料庫伺服器上,或者是花在硬碟中的本地資料庫)。

總結及期望

這個示例伺服器達成了兩個預期目標:

  1. 首先是介紹了問題範疇和貫徹該系列文章的套接字編程基礎。
  2. 對於並發伺服器編程的拋磚引玉 —— 就像之前的部分所說,順序伺服器還不能在非常輕微的負載下進行擴展,而且沒有高效的利用資源。

在看下一篇文章前,確保你已經理解了這裡所講的伺服器/客戶端協議,還有順序伺服器的代碼。我之前介紹過了這個簡單的協議;例如 串列通信分幀用協程來替代狀態機。要學習套接字網路編程的基礎,Beej 的教程 用來入門很不錯,但是要深入理解我推薦你還是看本書。

如果有什麼不清楚的,請在評論區下進行評論或者向我發送郵件。深入理解並發伺服器!

  • 注1:狀態轉變中的 In/Out 記號是指 Mealy machine
  • 注2:回應的是 bcdbcuf23436bc
  • 注3:這裡在結尾處有一點小區別,加了字元串 0000 —— 伺服器回應這個序列,告訴客戶端讓其斷開連接;這是一個簡單的握手協議,確保客戶端有足夠的時間接收到伺服器發送的所有回復。

via: https://eli.thegreenplace.net/2017/concurrent-servers-part-1-introduction/

作者:Eli Bendersky 譯者:GitFuture 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國