這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
原文:Golang伺服器的網路層實現
由於最近有接觸到一些長已連線的服務器實現,對網路模型有所學習。對基於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的read
和write
都應該使用非阻塞的模式,這樣才能最大限度的提高系統吞吐。例如,假設正在對某個socket調用阻塞的write
,當資料沒有完全發送完成前,write
將無法返回,從而阻止了整個epoll
進入下一個迴圈,如果這個時候其他的socket
有讀就緒的話,將無法第一時間響應。所以非阻塞的讀寫將在某個fd讀寫較慢的時候,立刻返回,而不會一直等到讀寫結束。這樣才能提高吞吐。然而,採用非阻讀寫將大大提高編程難度。
紫色線程負責將資料進行解碼並放入隊列中,等待背景工作執行緒處理;背景工作執行緒有資料要發送時,也將資料放入發送隊列,並通過某種機制通知紫色線程對應的socket有資料要寫,進而使得資料在紫色線程中寫入socket。
這種模型的編程難度主要體現在:
線程少(也不能太多),導致一個線程需要處理多個描述符,從而存在對描述符狀態的維護問題。甚至,業務層面的會話等都需要小心維護
非阻塞IO調用,使描述符的狀態更為複雜
隊列的同步處理
不得不說,能用C或C++來寫伺服器的是真大神!
Golang的goroutine
Golang
是一門比較新的語言,正在快速的發展。Golang
從語言層面支援一種叫協程
的輕量級執行緒模式,稱為goroutine
。當我們建立協程時,實際並不會建立作業系統的線程,Golang會使用現有的線程來調度協程。也就是說,從程式員的角度,協程是並發執行的,好像線程一下,而從作業系統的角度來看,程式可能只有幾個線程在運行。在同一個應用程式中,協程可以有成千上萬個!所以可以有成千上萬個並發任務,而這些任務的調度又十分輕量,比線程調度輕量的多的多。所以從程式員的角度,使用Golang就可以在一個應用程式中同時開啟成千上萬個並發任務。簡直逆天!
在Golang中使用go
關鍵字來開啟一個goroutine
:
func main() { log.Println("Hello, world") netListen, err := net.Listen("tcp", "localhost:4000") if err != nil { fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error()) os.Exit(1) } defer netListen.Close() log.Println("Waiting for clients") for { conn, err := netListen.Accept() if err != nil { continue } log.Println(conn.RemoteAddr().String(), " tcp connect success") go handleConnection(conn) }}func handleConnection(conn net.Conn) { ...}
Golang的channel
除了對並發的支援外,Golang中有一種叫channel
的並發同步機制。channel
類似隊列,是goroutine
安全的。所以結合goroutine
和channel
可以輕而易舉實現並發編程。
Golang如何?網路層
通過參考多個Golang的開來源程式,筆者得出的結論是:肆無忌憚的用goroutine
吧。於是一個Golang版的網路模型大致是這樣的:
是單個用戶端已連線的服務器模組結構,同樣的一個顏色代表一個協程:
綠色goroutine
依然是接受TCP連結
當完成握手accept
返回conn
對象後,使用一個單獨的goroutine
來阻塞讀
(紫色),使用一個單獨的goroutine
來阻塞寫
(紅色)
讀到的資料通過解碼後放入讀channel
,並由藍色的goroutine
來處理
需要寫資料時,藍色的goroutine
將資料寫入寫channel
,從而觸發紅色的goroutine
編碼並寫入conn
可以看到,針對一個用戶端,服務端至少有3個goroutine
在單獨為這個用戶端服務。如果從線程的角度來看,簡直是浪費啊,然而這就是協程的好處。這個模型很容易理解,因為跟人們的正常思維方式是一致的。並且都是阻塞的調用,所以無需維護狀態。
再來看看多個用戶端的情況:
在多個用戶端之間,雖然用了相同的顏色表示goroutine
,但實際上他們都是獨立的goroutine
,可以想象goroutine
的數量將是驚人的。然而,根本不用擔心!這樣的應用程式可能真正的線程只有幾個而已。