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的read
和write
都應該使用非阻塞的模式,這樣才能最大限度的提高系統吞吐。例如,假設正在對某個socket呼叫阻塞的write
,當資料沒有完全傳送完成前,write
將無法返回,從而阻止了整個epoll
進入下一個迴圈,如果這個時候其他的socket
有讀就緒的話,將無法第一時間響應。所以非阻塞的讀寫將在某個fd讀寫較慢的時候,立刻返回,而不會一直等到讀寫結束。這樣才能提高吞吐。然而,採用非阻讀寫將大大提高程式設計難度。 - 紫色執行緒負責將資料進行解碼並放入佇列中,等待工作執行緒處理;工作執行緒有資料要傳送時,也將資料放入傳送佇列,並通過某種機制通知紫色執行緒對應的socket有資料要寫,進而使得資料在紫色執行緒中寫入socket。
這種模型的程式設計難度主要體現在:
- 執行緒少(也不能太多),導致一個執行緒需要處理多個描述符,從而存在對描述符狀態的維護問題。甚至,業務層面的會話等都需要小心維護
- 非阻塞IO呼叫,使描述符的狀態更為複雜
- 佇列的同步處理
不得不說,能用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
安全的。所以結合goroutine
和channel
可以輕而易舉的實現併發程式設計。
Golang如何實現網路層
通過參考多個Golang的開源程式,筆者得出的結論是:肆無忌憚的用goroutine
吧。於是一個Golang版的網路模型大致是這樣的:
上圖是單個客戶端連線的伺服器模組結構,同樣的一個顏色代表一個協程:
- 綠色
goroutine
依然是接受TCP連結 - 當完成握手
accept
返回conn
物件後,使用一個單獨的goroutine
來阻塞讀
(紫色),使用一個單獨的goroutine
來阻塞寫
(紅色) - 讀到的資料通過解碼後放入
讀channel
,並由藍色的goroutine
來處理 - 需要寫資料時,藍色的
goroutine
將資料寫入寫channel
,從而觸發紅色的goroutine
編碼並寫入conn
可以看到,針對一個客戶端,服務端至少有3個goroutine
在單獨為這個客戶端服務。如果從執行緒的角度來看,簡直是浪費啊,然而這就是協程的好處。這個模型很容易理解,因為跟人們的正常思維方式是一致的。並且都是阻塞的呼叫,所以無需維護狀態。
再來看看多個客戶端的情況:
在多個客戶端之間,雖然用了相同的顏色表示goroutine
,但實際上他們都是獨立的goroutine
,可以想象goroutine
的數量將是驚人的。然而,根本不用擔心!這樣的應用程式可能真正的執行緒只有幾個而已。