1. 程式人生 > >Golang伺服器的網路層實現--總結對比常用epoll模型

Golang伺服器的網路層實現--總結對比常用epoll模型

由於最近有接觸到一些長連線的伺服器實現,對網路模型有所學習。對基於C/C++的網路模型實現和基於GoLang的實現對比下來,發現Golang的網路模型程式設計難度大大降低,這得益於Golang的goroutine,可以在程式設計的時候肆無忌憚的建立併發”執行緒”,當伺服器能為每一個客戶端都開啟若干”執行緒”的話,程式設計變的簡單很多。

傳統語言的網路層處理

服務需要同時服務N個客戶端,所以傳統的程式設計方式是採用IO複用,這樣在一個執行緒中對N個套接字進行事件捕獲,當讀寫事件產生後再真正read()或者write(),這樣才能提高吞吐:

上圖中:

  • 綠色執行緒為接受客戶端TCP連結的執行緒,使用阻塞的呼叫socket.accept()
    ,當有新的連線到來後,將socket物件conn加入IO複用佇列。
  • 紫色執行緒為IO複用的阻塞呼叫,通常採用epoll等系統呼叫實現IO複用。當IO複用佇列中的任意socket有資料到來,或者寫緩衝區空閒時可觸發epoll呼叫的返回,否則阻塞epoll呼叫。資料的實際傳送和接收都在紫色執行緒中完成。所以為了提高吞吐,對某個socket的readwrite都應該使用非阻塞的模式,這樣才能最大限度的提高系統吞吐。例如,假設正在對某個socket呼叫阻塞的write,當資料沒有完全傳送完成前,write將無法返回,從而阻止了整個epoll進入下一個迴圈,如果這個時候其他的socket有讀就緒的話,將無法第一時間響應。所以非阻塞的讀寫將在某個fd讀寫較慢的時候,立刻返回,而不會一直等到讀寫結束。這樣才能提高吞吐。然而,採用非阻讀寫將大大提高程式設計難度。
  • 紫色執行緒負責將資料進行解碼並放入佇列中,等待工作執行緒處理;工作執行緒有資料要傳送時,也將資料放入傳送佇列,並通過某種機制通知紫色執行緒對應的socket有資料要寫,進而使得資料在紫色執行緒中寫入socket。

這種模型的程式設計難度主要體現在:

  1. 執行緒少(也不能太多),導致一個執行緒需要處理多個描述符,從而存在對描述符狀態的維護問題。甚至,業務層面的會話等都需要小心維護
  2. 非阻塞IO呼叫,使描述符的狀態更為複雜
  3. 佇列的同步處理

不得不說,能用C或C++來寫伺服器的是真大神!

Golang的goroutine

Golang是一門比較新的語言,正在快速的發展。Golang從語言層面支援一種叫協程

的輕量級執行緒模型,稱為goroutine。當我們建立協程時,實際並不會建立作業系統的執行緒,Golang會使用現有的執行緒來排程協程。也就是說,從程式設計師的角度,協程是併發執行的,好像執行緒一下,而從作業系統的角度來看,程式可能只有幾個執行緒在執行。在同一個應用程式中,協程可以有成千上萬個!所以可以有成千上萬個併發任務,而這些任務的排程又十分輕量,比執行緒排程輕量的多的多。所以從程式設計師的角度,使用Golang就可以在一個應用程式中同時開啟成千上萬個併發任務。簡直逆天!

在Golang中使用go關鍵字來開啟一個goroutine

funcmain(){log.Println("Hello, world")netListen,err:=net.Listen("tcp","localhost:4000")iferr!=nil{fmt.Fprintf(os.Stderr,"Fatal error: %s",err.Error())os.Exit(1)}defernetListen.Close()log.Println("Waiting for clients")for{conn,err:=netListen.Accept()iferr!=nil{continue}log.Println(conn.RemoteAddr().String()," tcp connect success")gohandleConnection(conn)}}funchandleConnection(connnet.Conn){...}

Golang的channel

除了對併發的支援外,Golang中有一種叫channel的併發同步機制。channel類似佇列,是goroutine安全的。所以結合goroutinechannel可以輕而易舉的實現併發程式設計。

Golang如何實現網路層

通過參考多個Golang的開源程式,筆者得出的結論是:肆無忌憚的用goroutine吧。於是一個Golang版的網路模型大致是這樣的:

上圖是單個客戶端連線的伺服器模組結構,同樣的一個顏色代表一個協程:

  • 綠色goroutine依然是接受TCP連結
  • 當完成握手accept返回conn物件後,使用一個單獨的goroutine阻塞讀(紫色),使用一個單獨的goroutine阻塞寫(紅色)
  • 讀到的資料通過解碼後放入讀channel,並由藍色的goroutine來處理
  • 需要寫資料時,藍色的goroutine將資料寫入寫channel,從而觸發紅色的goroutine編碼並寫入conn

可以看到,針對一個客戶端,服務端至少有3個goroutine在單獨為這個客戶端服務。如果從執行緒的角度來看,簡直是浪費啊,然而這就是協程的好處。這個模型很容易理解,因為跟人們的正常思維方式是一致的。並且都是阻塞的呼叫,所以無需維護狀態。

再來看看多個客戶端的情況:

在多個客戶端之間,雖然用了相同的顏色表示goroutine,但實際上他們都是獨立的goroutine,可以想象goroutine的數量將是驚人的。然而,根本不用擔心!這樣的應用程式可能真正的執行緒只有幾個而已。