Go语言TCP Socket编程
2019獨角獸企業重金招聘Python工程師標準>>>
Go語言TCP Socket編程
Golang的 主要 設計目標之一就是面向大規模后端服務程序,網絡通信這塊是服務端 程序必不可少也是至關重要的一部分。在日常應用中,我們也可以看到Go中的net以及其subdirectories下的包均是“高頻+剛需”,而TCP socket則是網絡編程的主流,即便您沒有直接使用到net中有關TCP Socket方面的接口,但net/http總是用到了吧,http底層依舊是用tcp socket實現的。
網絡編程方面,我們最常用的就是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在各個場景下的使用方法、行為特點以及注意事項。
一、模型
從tcp socket誕生后,網絡編程架構模型也幾經演化,大致是:“每進程一個連接” –> “每線程一個連接” –> “Non-Block + I/O多路復用(linux epoll/windows iocp/freebsd darwin kqueue/solaris Event Port)”。伴隨著模型的演化,服務程序愈加強大,可以支持更多的連接,獲得更好的處理性能。
目前主流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端程序大致如下:
//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連接的建立
眾所周知,TCP Socket的連接的建立需要經歷客戶端和服務端的三次握手的過程。連接建立過程中,服務端是一個標準的Listen + Accept的結構(可參考上面的代碼),而在客戶端Go語言使用net.Dial或DialTimeout進行連接建立:
阻塞Dial:
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對于客戶端而言,連接的建立會遇到如下幾種情形:
1、網絡不可達或對方服務未啟動
如果傳給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 refused2、對方服務的listen backlog滿
還有一種場景就是對方服務器很忙,瞬間有大量 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 intfor {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.Connfor 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 ok2015/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端的網絡實現和設置有關。
3、網絡延遲較大,Dial阻塞并超時
如果網絡延遲較大,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三、Socket讀寫
連接建立起來后,我們就要在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的行為特點。
1、Socket中無數據
連接建立后,如果對方未發送數據到socket,接收方(Server)會阻塞在Read操作上,這和前面提到的“模型”原理是一致的。執行該Read操作的goroutine也會被掛起。runtime會監視該socket,直到其有數據才會重新
 調度該socket對應的Goroutine完成read。由于篇幅原因,這里就不列代碼了,例子對應的代碼文件:go-tcpsock/read_write下的client1.go和server1.go。
2、Socket中有部分數據
如果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 connectionvar 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端:
 運行結果:
Client向socket中寫入兩個字節數據(“hi”),Server端創建一個len = 10的slice,等待Read將讀取的數據放入slice;Server隨后讀取到那兩個字節:”hi”。Read成功返回,n =2 ,err = nil。
3、Socket中有足夠數據
如 果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 12345client端發送的內容長度為15個字節,Server端Read buffer的長度為10,因此Server Read第一次返回時只會讀取10個字節;Socket中還剩余5個字節數據,Server再次Read時會把剩余數據讀出(如:情形2)。
4、Socket關閉
如果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。
5、讀取操作超時
有 些場合對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 connectiontime.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.go2015/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遇到的情形一樣不少,我們也逐一看一下。
1、成功寫
前面例子著重于Read,client端在Write時并未判斷Write的返回值。所謂“成功寫”指的就是Write調用返回的n與預期要寫入的數據長度相等,且error = nil。這是我們在調用Write時遇到的最常見的情形,這里不再舉例了。
2、寫阻塞
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 intfor {n, err := conn.Write(data)if err != nil {total += nlog.Printf("write %d bytes, error:%s\n", n, err)break}total += nlog.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 connectiontime.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.go2015/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 .... ...3、寫入部分數據
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字節做特定處理。
4、寫入超時
如果非要給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 safe
基于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 methodsfdmu fdMutex// immutable until Closesysfd intfamily intsotype intisConnected boolnet stringladdr Addrraddr Addr// wait serverpd 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 = 0if 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 intn, 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.ErrUnexpectedEOFbreak}}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屬性
原生Socket API提供了豐富的sockopt設置接口,但Golang有自己的網絡架構模型,golang提供的socket options接口也是基于上述模型的必要的屬性設置。包括
- SetKeepAlive
- SetKeepAlivePeriod
- SetLinger
- SetNoDelay (默認no delay)
- SetWriteBuffer
- SetReadBuffer
不過上面的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 connectionvar 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。
轉載于:https://my.oschina.net/ifraincoat/blog/779811
總結
以上是生活随笔為你收集整理的Go语言TCP Socket编程的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: 防火墙未开启导致通过redis篡改.ss
- 下一篇: Mvc全局过滤器与Action排除
