受權轉載: Tony Bai html
原文鏈接: https://tonybai.com/2015/11/17/tcp-programming-in-golang/linux
Golang的主要 設計目標之一就是面向大規模後端服務程序,網絡通訊這塊是服務端 程序必不可少也是相當重要的一部分。在平常應用中,咱們也能夠看到Go中的net以及其subdirectories下的包均是「高頻+剛需」,而TCP socket則是網絡編程的主流,即使您沒有直接使用到net中有關TCP Socket方面的接口,但net/http老是用到了吧,http底層依舊是用tcp socket實現的。git
網絡編程方面,咱們最經常使用的就是tcp socket編程了,在posix標準出來後,socket在各大主流OS平臺上都獲得了很好的支持。關於tcp programming,最好的資料莫過於W. Richard Stevens 的網絡編程聖經《UNIX網絡 編程 卷1:套接字聯網API》 了,書中關於tcp socket接口的各類使用、行爲模式、異常處理講解的十分細緻。Go是自帶runtime的跨平臺編程語言,Go中暴露給語言使用者的tcp socket api是創建OS原生tcp socket接口之上的。因爲Go runtime調度的須要,golang tcp socket接口在行爲特色與異常處理方面與OS原生接口有着一些差異。這篇博文的目標就是整理出關於Go tcp socket在各個場景下的使用方法、行爲特色以及注意事項。github
從tcp socket誕生後,網絡編程架構模型也幾經演化,大體是:「每進程一個鏈接」 –> 「每線程一個鏈接」 –> 「Non-Block + I/O多路複用(linux epoll/windows iocp/freebsd darwin kqueue/solaris Event Port)」。伴隨着模型的演化,服務程序越發強大,能夠支持更多的鏈接,得到更好的處理性能。golang
目前主流web server通常均採用的都是」Non-Block + I/O多路複用」(有的也結合了多線程、多進程)。不過I/O多路複用也給使用者帶來了不小的複雜度,以致於後續出現了許多高性能的I/O多路複用框架, 好比libevent、libev、libuv等,以幫助開發者簡化開發複雜性,下降心智負擔。不過Go的設計者彷佛認爲I/O多路複用的這種經過回調機制割裂控制流 的方式依舊複雜,且有悖於「通常邏輯」設計,爲此Go語言將該「複雜性」隱藏在Runtime中了:Go開發者無需關注socket是不是 non-block的,也無需親自注冊文件描述符的回調,只需在每一個鏈接對應的goroutine中以「block I/O」的方式對待socket處理便可,這能夠說大大下降了開發人員的心智負擔。一個典型的Go server端程序大體以下:web
//go-tcpsock/server.go func handleConn(c net.Conn) { defer c.Close() for { // read from the connection // ... ... // write to the connection //... ... } } func main() { l, err := net.Listen("tcp", ":8888") if err != nil { fmt.Println("listen error:", err) return } for { c, err := l.Accept() if err != nil { fmt.Println("accept error:", err) break } // start a new goroutine to handle // the new connection. go handleConn(c) } }
用戶層眼中看到的goroutine中的「block socket」,其實是經過Go runtime中的netpoller經過Non-block socket + I/O多路複用機制「模擬」出來的,真實的underlying socket其實是non-block的,只是runtime攔截了底層socket系統調用的錯誤碼,並經過netpoller和goroutine 調度讓goroutine「阻塞」在用戶層獲得的Socket fd上。好比:當用戶層針對某個socket fd發起read操做時,若是該socket fd中尚無數據,那麼runtime會將該socket fd加入到netpoller中監聽,同時對應的goroutine被掛起,直到runtime收到socket fd 數據ready的通知,runtime纔會從新喚醒等待在該socket fd上準備read的那個Goroutine。而這個過程從Goroutine的視角來看,就像是read操做一直block在那個socket fd上似的。具體實現細節在後續場景中會有補充描述。編程
衆所周知,TCP Socket的鏈接的創建須要經歷客戶端和服務端的三次握手的過程。鏈接創建過程當中,服務端是一個標準的Listen + Accept的結構(可參考上面的代碼),而在客戶端Go語言使用net.Dial或DialTimeout進行鏈接創建:ubuntu
阻塞Dial:windows
conn, err := net.Dial("tcp", "google.com:80") if err != nil { //handle error } // read or write on conn
或是帶上超時機制的Dial:後端
conn, err := net.DialTimeout("tcp", ":8080", 2 * time.Second) if err != nil { //handle error } // read or write on conn
對於客戶端而言,鏈接的創建會遇到以下幾種情形:
若是傳給Dial的Addr是能夠當即判斷出網絡不可達,或者Addr中端口對應的服務沒有啓動,端口未被監聽,Dial會幾乎當即返回錯誤,好比:
//go-tcpsock/conn_establish/client1.go ... ... func main() { log.Println("begin dial...") conn, err := net.Dial("tcp", ":8888") if err != nil { log.Println("dial error:", err) return } defer conn.Close() log.Println("dial ok") }
若是本機8888端口未有服務程序監聽,那麼執行上面程序,Dial會很快返回錯誤:
$go run client1.go 2015/11/16 14:37:41 begin dial... 2015/11/16 14:37:41 dial error: dial tcp :8888: getsockopt: connection refused
還有一種場景就是對方服務器很忙,瞬間有大量client端鏈接嘗試向server創建,server端的listen backlog隊列滿,server accept不及時((即使不accept,那麼在backlog數量範疇裏面,connect都會是成功的,由於new conn已經加入到server side的listen queue中了,accept只是從queue中取出一個conn而已),這將致使client端Dial阻塞。咱們仍是經過例子感覺Dial的行爲特色:
服務端代碼:
//go-tcpsock/conn_establish/server2.go ... ... func main() { l, err := net.Listen("tcp", ":8888") if err != nil { log.Println("error listen:", err) return } defer l.Close() log.Println("listen ok") var i int for { time.Sleep(time.Second * 10) if _, err := l.Accept(); err != nil { log.Println("accept error:", err) break } i++ log.Printf("%d: accept a new connection\n", i) } }
客戶端代碼:
//go-tcpsock/conn_establish/client2.go ... ... func establishConn(i int) net.Conn { conn, err := net.Dial("tcp", ":8888") if err != nil { log.Printf("%d: dial error: %s", i, err) return nil } log.Println(i, ":connect to server ok") return conn } func main() { var sl []net.Conn for i := 1; i < 1000; i++ { conn := establishConn(i) if conn != nil { sl = append(sl, conn) } } time.Sleep(time.Second * 10000) }
從程序能夠看出,服務端在listen成功後,每隔10s鍾accept一次。客戶端則是串行的嘗試創建鏈接。這兩個程序在Darwin下的執行 結果:
$go run server2.go 2015/11/16 21:55:41 listen ok 2015/11/16 21:55:51 1: accept a new connection 2015/11/16 21:56:01 2: accept a new connection ... ... $go run client2.go 2015/11/16 21:55:44 1 :connect to server ok 2015/11/16 21:55:44 2 :connect to server ok 2015/11/16 21:55:44 3 :connect to server ok ... ... 2015/11/16 21:55:44 126 :connect to server ok 2015/11/16 21:55:44 127 :connect to server ok 2015/11/16 21:55:44 128 :connect to server ok 2015/11/16 21:55:52 129 :connect to server ok 2015/11/16 21:56:03 130 :connect to server ok 2015/11/16 21:56:14 131 :connect to server ok ... ...
能夠看出Client初始時成功地一次性創建了128個鏈接,而後後續每阻塞近10s才能成功創建一條鏈接。也就是說在server端 backlog滿時(未及時accept),客戶端將阻塞在Dial上,直到server端進行一次accept。至於爲何是128,這與darwin 下的默認設置有關:
$sysctl -a|grep kern.ipc.somaxconn kern.ipc.somaxconn: 128
若是我在ubuntu 14.04上運行上述server程序,咱們的client端初始能夠成功創建499條鏈接。
若是server一直不accept,client端會一直阻塞麼?咱們去掉accept後的結果是:在Darwin下,client端會阻塞大 約1分多鐘纔會返回timeout:
2015/11/16 22:03:31 128 :connect to server ok 2015/11/16 22:04:48 129: dial error: dial tcp :8888: getsockopt: operation timed out
而若是server運行在ubuntu 14.04上,client彷佛一直阻塞,我等了10多分鐘依舊沒有返回。 阻塞與否看來與server端的網絡實現和設置有關。
若是網絡延遲較大,TCP握手過程將更加艱難坎坷(各類丟包),時間消耗的天然也會更長。Dial這時會阻塞,若是長時間依舊沒法創建鏈接,則Dial也會返回「 getsockopt: operation timed out」錯誤。
在鏈接創建階段,多數狀況下,Dial是能夠知足需求的,即使阻塞一小會兒。但對於某些程序而言,須要有嚴格的鏈接時間限定,若是必定時間內沒能成功創建鏈接,程序可能會須要執行一段「異常」處理邏輯,爲此咱們就須要DialTimeout了。下面的例子將Dial的最長阻塞時間限制在2s內,超出這個時長,Dial將返回timeout error:
//go-tcpsock/conn_establish/client3.go ... ... func main() { log.Println("begin dial...") conn, err := net.DialTimeout("tcp", "104.236.176.96:80", 2*time.Second) if err != nil { log.Println("dial error:", err) return } defer conn.Close() log.Println("dial ok") }
執行結果以下(須要模擬一個延遲較大的網絡環境):
$go run client3.go 2015/11/17 09:28:34 begin dial... 2015/11/17 09:28:36 dial error: dial tcp 104.236.176.96:80: i/o timeout
鏈接創建起來後,咱們就要在conn上進行讀寫,以完成業務邏輯。前面說過Go runtime隱藏了I/O多路複用的複雜性。語言使用者只需採用goroutine+Block I/O的模式便可知足大部分場景需求。Dial成功後,方法返回一個net.Conn接口類型變量值,這個接口變量的動態類型爲一個*TCPConn:
//$GOROOT/src/net/tcpsock_posix.go type TCPConn struct { conn }
TCPConn內嵌了一個unexported類型:conn,所以TCPConn」繼承」了conn的Read和Write方法,後續經過Dial返回值調用的Write和Read方法均是net.conn的方法:
//$GOROOT/src/net/net.go type conn struct { fd *netFD } func (c *conn) ok() bool { return c != nil && c.fd != nil } // Implementation of the Conn interface. // Read implements the Conn Read method. func (c *conn) Read(b []byte) (int, error) { if !c.ok() { return 0, syscall.EINVAL } n, err := c.fd.Read(b) if err != nil && err != io.EOF { err = &OpError{Op: "read", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err} } return n, err } // Write implements the Conn Write method. func (c *conn) Write(b []byte) (int, error) { if !c.ok() { return 0, syscall.EINVAL } n, err := c.fd.Write(b) if err != nil { err = &OpError{Op: "write", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err} } return n, err }
下面咱們先來經過幾個場景來總結一下conn.Read的行爲特色。
鏈接創建後,若是對方未發送數據到socket,接收方(Server)會阻塞在Read操做上,這和前面提到的「模型」原理是一致的。執行該Read操做的goroutine也會被掛起。runtime會監視該socket,直到其有數據纔會從新
調度該socket對應的Goroutine完成read。因爲篇幅緣由,這裏就不列代碼了,例子對應的代碼文件:go-tcpsock/read_write下的client1.go和server1.go。
若是socket中有部分數據,且長度小於一次Read操做所指望讀出的數據長度,那麼Read將會成功讀出這部分數據並返回,而不是等待全部指望數據所有讀取後再返回。
Client端:
//go-tcpsock/read_write/client2.go ... ... func main() { if len(os.Args) <= 1 { fmt.Println("usage: go run client2.go YOUR_CONTENT") return } log.Println("begin dial...") conn, err := net.Dial("tcp", ":8888") if err != nil { log.Println("dial error:", err) return } defer conn.Close() log.Println("dial ok") time.Sleep(time.Second * 2) data := os.Args[1] conn.Write([]byte(data)) time.Sleep(time.Second * 10000) }
Server端:
//go-tcpsock/read_write/server2.go ... ... func handleConn(c net.Conn) { defer c.Close() for { // read from the connection var buf = make([]byte, 10) log.Println("start to read from conn") n, err := c.Read(buf) if err != nil { log.Println("conn read error:", err) return } log.Printf("read %d bytes, content is %s\n", n, string(buf[:n])) } } ... ...
咱們經過client2.go發送」hi」到Server端:
運行結果:
$go run client2.go hi 2015/11/17 13:30:53 begin dial... 2015/11/17 13:30:53 dial ok $go run server2.go 2015/11/17 13:33:45 accept a new connection 2015/11/17 13:33:45 start to read from conn 2015/11/17 13:33:47 read 2 bytes, content is hi ...
Client向socket中寫入兩個字節數據(「hi」),Server端建立一個len = 10的slice,等待Read將讀取的數據放入slice;Server隨後讀取到那兩個字節:」hi」。Read成功返回,n =2 ,err = nil。
若是socket中有數據,且長度大於等於一次Read操做所指望讀出的數據長度,那麼Read將會成功讀出這部分數據並返回。這個情景是最符合咱們對Read的期待的了:Read將用Socket中的數據將咱們傳入的slice填滿後返回:n = 10, err = nil。
咱們經過client2.go向Server2發送以下內容:abcdefghij12345,執行結果以下:
$go run client2.go abcdefghij12345 2015/11/17 13:38:00 begin dial... 2015/11/17 13:38:00 dial ok $go run server2.go 2015/11/17 13:38:00 accept a new connection 2015/11/17 13:38:00 start to read from conn 2015/11/17 13:38:02 read 10 bytes, content is abcdefghij 2015/11/17 13:38:02 start to read from conn 2015/11/17 13:38:02 read 5 bytes, content is 12345
client端發送的內容長度爲15個字節,Server端Read buffer的長度爲10,所以Server Read第一次返回時只會讀取10個字節;Socket中還剩餘5個字節數據,Server再次Read時會把剩餘數據讀出(如:情形2)。
若是client端主動關閉了socket,那麼Server的Read將會讀到什麼呢?這裏分爲「有數據關閉」和「無數據關閉」。
「有數據關閉」是指在client關閉時,socket中還有server端未讀取的數據,咱們在go-tcpsock/read_write/client3.go和server3.go中模擬這種狀況:
$go run client3.go hello 2015/11/17 13:50:57 begin dial... 2015/11/17 13:50:57 dial ok $go run server3.go 2015/11/17 13:50:57 accept a new connection 2015/11/17 13:51:07 start to read from conn 2015/11/17 13:51:07 read 5 bytes, content is hello 2015/11/17 13:51:17 start to read from conn 2015/11/17 13:51:17 conn read error: EOF
從輸出結果來看,當client端close socket退出後,server3依舊沒有開始Read,10s後第一次Read成功讀出了5個字節的數據,當第二次Read時,因爲client端 socket關閉,Read返回EOF error。
經過上面這個例子,咱們也能夠猜想出「無數據關閉」情形下的結果,那就是Read直接返回EOF error。
有些場合對Read的阻塞時間有嚴格限制,在這種狀況下,Read的行爲究竟是什麼樣的呢?在返回超時錯誤時,是否也同時Read了一部分數據了呢?這個實驗比較難於模擬,下面的測試結果也未必能反映出全部可能結果。咱們編寫了client4.go和server4.go來模擬這一情形。
//go-tcpsock/read_write/client4.go ... ... func main() { log.Println("begin dial...") conn, err := net.Dial("tcp", ":8888") if err != nil { log.Println("dial error:", err) return } defer conn.Close() log.Println("dial ok") data := make([]byte, 65536) conn.Write(data) time.Sleep(time.Second * 10000) } //go-tcpsock/read_write/server4.go ... ... func handleConn(c net.Conn) { defer c.Close() for { // read from the connection time.Sleep(10 * time.Second) var buf = make([]byte, 65536) log.Println("start to read from conn") c.SetReadDeadline(time.Now().Add(time.Microsecond * 10)) n, err := c.Read(buf) if err != nil { log.Printf("conn read %d bytes, error: %s", n, err) if nerr, ok := err.(net.Error); ok && nerr.Timeout() { continue } return } log.Printf("read %d bytes, content is %s\n", n, string(buf[:n])) } }
在Server端咱們經過Conn的SetReadDeadline方法設置了10微秒的讀超時時間,Server的執行結果以下:
$go run server4.go 2015/11/17 14:21:17 accept a new connection 2015/11/17 14:21:27 start to read from conn 2015/11/17 14:21:27 conn read 0 bytes, error: read tcp 127.0.0.1:8888->127.0.0.1:60970: i/o timeout 2015/11/17 14:21:37 start to read from conn 2015/11/17 14:21:37 read 65536 bytes, content is
雖然每次都是10微秒超時,但結果不一樣,第一次Read超時,讀出數據長度爲0;第二次讀取全部數據成功,沒有超時。反覆執行了屢次,沒能出現「讀出部分數據且返回超時錯誤」的狀況。
和讀相比,Write遇到的情形同樣很多,咱們也逐一看一下。
前面例子着重於Read,client端在Write時並未判斷Write的返回值。所謂「成功寫」指的就是Write調用返回的n與預期要寫入的數據長度相等,且error = nil。這是咱們在調用Write時遇到的最多見的情形,這裏再也不舉例了。
TCP鏈接通訊兩端的OS都會爲該鏈接保留數據緩衝,一端調用Write後,實際上數據是寫入到OS的協議棧的數據緩衝的。TCP是全雙工通訊,所以每一個方向都有獨立的數據緩衝。當發送方將對方的接收緩衝區以及自身的發送緩衝區寫滿後,Write就會阻塞。咱們來看一個例子:client5.go和server.go。
//go-tcpsock/read_write/client5.go ... ... func main() { log.Println("begin dial...") conn, err := net.Dial("tcp", ":8888") if err != nil { log.Println("dial error:", err) return } defer conn.Close() log.Println("dial ok") data := make([]byte, 65536) var total int for { n, err := conn.Write(data) if err != nil { total += n log.Printf("write %d bytes, error:%s\n", n, err) break } total += n log.Printf("write %d bytes this time, %d bytes in total\n", n, total) } log.Printf("write %d bytes in total\n", total) time.Sleep(time.Second * 10000) } //go-tcpsock/read_write/server5.go ... ... func handleConn(c net.Conn) { defer c.Close() time.Sleep(time.Second * 10) for { // read from the connection time.Sleep(5 * time.Second) var buf = make([]byte, 60000) log.Println("start to read from conn") n, err := c.Read(buf) if err != nil { log.Printf("conn read %d bytes, error: %s", n, err) if nerr, ok := err.(net.Error); ok && nerr.Timeout() { continue } } log.Printf("read %d bytes, content is %s\n", n, string(buf[:n])) } } ... ...
Server5在前10s中並不Read數據,所以當client5一直嘗試寫入時,寫到必定量後就會發生阻塞:
$go run client5.go 2015/11/17 14:57:33 begin dial... 2015/11/17 14:57:33 dial ok 2015/11/17 14:57:33 write 65536 bytes this time, 65536 bytes in total 2015/11/17 14:57:33 write 65536 bytes this time, 131072 bytes in total 2015/11/17 14:57:33 write 65536 bytes this time, 196608 bytes in total 2015/11/17 14:57:33 write 65536 bytes this time, 262144 bytes in total 2015/11/17 14:57:33 write 65536 bytes this time, 327680 bytes in total 2015/11/17 14:57:33 write 65536 bytes this time, 393216 bytes in total 2015/11/17 14:57:33 write 65536 bytes this time, 458752 bytes in total 2015/11/17 14:57:33 write 65536 bytes this time, 524288 bytes in total 2015/11/17 14:57:33 write 65536 bytes this time, 589824 bytes in total 2015/11/17 14:57:33 write 65536 bytes this time, 655360 bytes in total
在Darwin上,這個size大約在679468bytes。後續當server5每隔5s進行Read時,OS socket緩衝區騰出了空間,client5就又能夠寫入了:
$go run server5.go 2015/11/17 15:07:01 accept a new connection 2015/11/17 15:07:16 start to read from conn 2015/11/17 15:07:16 read 60000 bytes, content is 2015/11/17 15:07:21 start to read from conn 2015/11/17 15:07:21 read 60000 bytes, content is 2015/11/17 15:07:26 start to read from conn 2015/11/17 15:07:26 read 60000 bytes, content is .... client端: 2015/11/17 15:07:01 write 65536 bytes this time, 720896 bytes in total 2015/11/17 15:07:06 write 65536 bytes this time, 786432 bytes in total 2015/11/17 15:07:16 write 65536 bytes this time, 851968 bytes in total 2015/11/17 15:07:16 write 65536 bytes this time, 917504 bytes in total 2015/11/17 15:07:27 write 65536 bytes this time, 983040 bytes in total 2015/11/17 15:07:27 write 65536 bytes this time, 1048576 bytes in total .... ...
Write操做存在寫入部分數據的狀況,好比上面例子中,當client端輸出日誌停留在「write 65536 bytes this time, 655360 bytes in total」時,咱們殺掉server5,這時咱們會看到client5輸出如下日誌:
... 2015/11/17 15:19:14 write 65536 bytes this time, 655360 bytes in total 2015/11/17 15:19:16 write 24108 bytes, error:write tcp 127.0.0.1:62245->127.0.0.1:8888: write: broken pipe 2015/11/17 15:19:16 write 679468 bytes in total
顯然Write並不是在655360這個地方阻塞的,而是後續又寫入24108後發生了阻塞,server端socket關閉後,咱們看到Wrote返回er != nil且n = 24108,程序須要對這部分寫入的24108字節作特定處理。
若是非要給Write增長一個期限,那咱們能夠調用SetWriteDeadline方法。咱們copy一份client5.go,造成client6.go,在client6.go的Write以前增長一行timeout設置代碼:
conn.SetWriteDeadline(time.Now().Add(time.Microsecond * 10))
啓動server6.go,啓動client6.go,咱們能夠看到寫入超時的狀況下,Write的返回結果:
$go run client6.go 2015/11/17 15:26:34 begin dial... 2015/11/17 15:26:34 dial ok 2015/11/17 15:26:34 write 65536 bytes this time, 65536 bytes in total ... ... 2015/11/17 15:26:34 write 65536 bytes this time, 655360 bytes in total 2015/11/17 15:26:34 write 24108 bytes, error:write tcp 127.0.0.1:62325->127.0.0.1:8888: i/o timeout 2015/11/17 15:26:34 write 679468 bytes in total
能夠看到在寫入超時時,依舊存在部分數據寫入的狀況。
綜上例子,雖然Go給咱們提供了阻塞I/O的便利,但在調用Read和Write時依舊要綜合須要方法返回的n和err的結果,以作出正確處理。net.conn實現了io.Reader和io.Writer接口,所以能夠試用一些wrapper包進行socket讀寫,好比bufio包下面的Writer和Reader、io/ioutil下的函數等。
基於goroutine的網絡架構模型,存在在不一樣goroutine間共享conn的狀況,那麼conn的讀寫是不是goroutine safe的呢?在深刻這個問題以前,咱們先從應用意義上來看read操做和write操做的goroutine-safe必要性。
對於read操做而言,因爲TCP是面向字節流,conn.Read沒法正確區分數據的業務邊界,所以多個goroutine對同一個conn進行read的意義不大,goroutine讀到不完整的業務包反卻是增長了業務處理的難度。對與Write操做而言,卻是有多個goroutine併發寫的狀況。不過conn讀寫是否goroutine-safe的測試不是很好作,咱們先深刻一下runtime代碼,先從理論上給這個問題定個性:
net.conn只是*netFD的wrapper結構,最終Write和Read都會落在其中的fd上:
type conn struct { fd *netFD }
netFD在不一樣平臺上有着不一樣的實現,咱們以net/fd_unix.go中的netFD爲例:
// Network file descriptor. type netFD struct { // locking/lifetime of sysfd + serialize access to Read and Write methods fdmu fdMutex // immutable until Close sysfd int family int sotype int isConnected bool net string laddr Addr raddr Addr // wait server pd pollDesc }
咱們看到netFD中包含了一個runtime實現的fdMutex類型字段,從註釋上來看,該fdMutex用來串行化對該netFD對應的sysfd的Write和Read操做。從這個註釋上來看,全部對conn的Read和Write操做都是有fdMutex互斥的,從netFD的Read和Write方法的實現也證明了這一點:
func (fd *netFD) Read(p []byte) (n int, err error) { if err := fd.readLock(); err != nil { return 0, err } defer fd.readUnlock() if err := fd.pd.PrepareRead(); err != nil { return 0, err } for { n, err = syscall.Read(fd.sysfd, p) if err != nil { n = 0 if err == syscall.EAGAIN { if err = fd.pd.WaitRead(); err == nil { continue } } } err = fd.eofError(n, err) break } if _, ok := err.(syscall.Errno); ok { err = os.NewSyscallError("read", err) } return } func (fd *netFD) Write(p []byte) (nn int, err error) { if err := fd.writeLock(); err != nil { return 0, err } defer fd.writeUnlock() if err := fd.pd.PrepareWrite(); err != nil { return 0, err } for { var n int n, err = syscall.Write(fd.sysfd, p[nn:]) if n > 0 { nn += n } if nn == len(p) { break } if err == syscall.EAGAIN { if err = fd.pd.WaitWrite(); err == nil { continue } } if err != nil { break } if n == 0 { err = io.ErrUnexpectedEOF break } } if _, ok := err.(syscall.Errno); ok { err = os.NewSyscallError("write", err) } return nn, err }
每次Write操做都是受lock保護,直到這次數據所有write完。所以在應用層面,要想保證多個goroutine在一個conn上write操做的Safe,須要一次write完整寫入一個「業務包」;一旦將業務包的寫入拆分爲屢次write,那就沒法保證某個Goroutine的某「業務包」數據在conn發送的連續性。
同時也能夠看出即使是Read操做,也是lock保護的。多個Goroutine對同一conn的併發讀不會出現讀出內容重疊的狀況,但內容斷點是依 runtime調度來隨機肯定的。存在一個業務包數據,1/3內容被goroutine-1讀走,另外2/3被另一個goroutine-2讀 走的狀況。好比一個完整包:world,當goroutine的read slice size < 5時,存在可能:一個goroutine讀到 「worl」,另一個goroutine讀出」d」。
原生Socket API提供了豐富的sockopt設置接口,但Golang有本身的網絡架構模型,golang提供的socket options接口也是基於上述模型的必要的屬性設置。包括
不過上面的Method是TCPConn的,而不是Conn的,要使用上面的Method的,須要type assertion:
tcpConn, ok := c.(*TCPConn) if !ok { //error handle } tcpConn.SetNoDelay(true)
對於listener socket, golang默認採用了 SO_REUSEADDR,這樣當你重啓 listener程序時,不會由於address in use的錯誤而啓動失敗。而listen backlog的默認值是經過獲取系統的設置值獲得的。不一樣系統不一樣:mac 128, linux 512等。
和前面的方法相比,關閉鏈接算是最簡單的操做了。因爲socket是全雙工的,client和server端在己方已關閉的socket和對方關閉的socket上操做的結果有不一樣。看下面例子:
//go-tcpsock/conn_close/client1.go ... ... func main() { log.Println("begin dial...") conn, err := net.Dial("tcp", ":8888") if err != nil { log.Println("dial error:", err) return } conn.Close() log.Println("close ok") var buf = make([]byte, 32) n, err := conn.Read(buf) if err != nil { log.Println("read error:", err) } else { log.Printf("read % bytes, content is %s\n", n, string(buf[:n])) } n, err = conn.Write(buf) if err != nil { log.Println("write error:", err) } else { log.Printf("write % bytes, content is %s\n", n, string(buf[:n])) } time.Sleep(time.Second * 1000) } //go-tcpsock/conn_close/server1.go ... ... func handleConn(c net.Conn) { defer c.Close() // read from the connection var buf = make([]byte, 10) log.Println("start to read from conn") n, err := c.Read(buf) if err != nil { log.Println("conn read error:", err) } else { log.Printf("read %d bytes, content is %s\n", n, string(buf[:n])) } n, err = c.Write(buf) if err != nil { log.Println("conn write error:", err) } else { log.Printf("write %d bytes, content is %s\n", n, string(buf[:n])) } } ... ...
上述例子的執行結果以下:
$go run server1.go 2015/11/17 17:00:51 accept a new connection 2015/11/17 17:00:51 start to read from conn 2015/11/17 17:00:51 conn read error: EOF 2015/11/17 17:00:51 write 10 bytes, content is $go run client1.go 2015/11/17 17:00:51 begin dial... 2015/11/17 17:00:51 close ok 2015/11/17 17:00:51 read error: read tcp 127.0.0.1:64195->127.0.0.1:8888: use of closed network connection 2015/11/17 17:00:51 write error: write tcp 127.0.0.1:64195->127.0.0.1:8888: use of closed network connection
從client1的結果來看,在己方已經關閉的socket上再進行read和write操做,會獲得」use of closed network connection」 error;
從server1的執行結果來看,在對方關閉的socket上執行read操做會獲得EOF error,但write操做會成功,由於數據會成功寫入己方的內核socket緩衝區中,即使最終發不到對方socket緩衝區了,由於己方socket並未關閉。所以當發現對方socket關閉後,己方應該正確合理處理本身的socket,再繼續write已經無任何意義了。
本文比較基礎,但卻很重要,畢竟golang是面向大規模服務後端的,對通訊環節的細節的深刻理解會大有裨益。另外Go的goroutine+阻塞通訊的網絡通訊模型下降了開發者心智負擔,簡化了通訊的複雜性,這點尤其重要。
本文代碼實驗環境:go 1.5.1 on Darwin amd64以及部分在ubuntu 14.04 amd64。
本文demo代碼在這裏能夠找到。