国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

Go語言TCP Socket編程

這篇具有很好參考價(jià)值的文章主要介紹了Go語言TCP Socket編程。希望對大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問。

Golang的主要 設(shè)計(jì)目標(biāo)之一就是面向大規(guī)模后端服務(wù)程序,網(wǎng)絡(luò)通信這塊是服務(wù)端 程序必不可少也是至關(guān)重要的一部分。在日常應(yīng)用中,我們也可以看到Go中的net以及其subdirectories下的包均是“高頻+剛需”,而TCP socket則是網(wǎng)絡(luò)編程的主流,即便您沒有直接使用到net中有關(guān)TCP Socket方面的接口,但net/http總是用到了吧,http底層依舊是用tcp socket實(shí)現(xiàn)的。

網(wǎng)絡(luò)編程方面,我們最常用的就是tcp socket編程了,在posix標(biāo)準(zhǔn)出來后,socket在各大主流OS平臺上都得到了很好的支持。關(guān)于tcp programming,最好的資料莫過于W. Richard Stevens 的網(wǎng)絡(luò)編程圣經(jīng)《UNIX網(wǎng)絡(luò) 編程 卷1:套接字聯(lián)網(wǎng)API》 了,書中關(guān)于tcp socket接口的各種使用、行為模式、異常處理講解的十分細(xì)致。Go是自帶runtime的跨平臺編程語言,Go中暴露給語言使用者的tcp socket api是建立OS原生tcp socket接口之上的。由于Go runtime調(diào)度的需要,golang tcp socket接口在行為特點(diǎn)與異常處理方面與OS原生接口有著一些差別。這篇博文的目標(biāo)就是整理出關(guān)于Go tcp socket在各個(gè)場景下的使用方法、行為特點(diǎn)以及注意事項(xiàng)。

一、模型

從tcp socket誕生后,網(wǎng)絡(luò)編程架構(gòu)模型也幾經(jīng)演化,大致是:“每進(jìn)程一個(gè)連接” –> “每線程一個(gè)連接” –> “Non-Block + I/O多路復(fù)用(linux epoll/windows iocp/freebsd darwin kqueue/solaris Event Port)”。伴隨著模型的演化,服務(wù)程序愈加強(qiáng)大,可以支持更多的連接,獲得更好的處理性能。

目前主流web server一般均采用的都是”Non-Block + I/O多路復(fù)用”(有的也結(jié)合了多線程、多進(jìn)程)。不過I/O多路復(fù)用也給使用者帶來了不小的復(fù)雜度,以至于后續(xù)出現(xiàn)了許多高性能的I/O多路復(fù)用框架, 比如libevent、libev、libuv等,以幫助開發(fā)者簡化開發(fā)復(fù)雜性,降低心智負(fù)擔(dān)。不過Go的設(shè)計(jì)者似乎認(rèn)為I/O多路復(fù)用的這種通過回調(diào)機(jī)制割裂控制流 的方式依舊復(fù)雜,且有悖于“一般邏輯”設(shè)計(jì),為此Go語言將該“復(fù)雜性”隱藏在Runtime中了:Go開發(fā)者無需關(guān)注socket是否是 non-block的,也無需親自注冊文件描述符的回調(diào),只需在每個(gè)連接對應(yīng)的goroutine中以“block I/O”的方式對待socket處理即可,這可以說大大降低了開發(fā)人員的心智負(fù)擔(dān)。一個(gè)典型的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”,實(shí)際上是通過Go runtime中的netpoller通過Non-block socket + I/O多路復(fù)用機(jī)制“模擬”出來的,真實(shí)的underlying socket實(shí)際上是non-block的,只是runtime攔截了底層socket系統(tǒng)調(diào)用的錯(cuò)誤碼,并通過netpoller和goroutine 調(diào)度讓goroutine“阻塞”在用戶層得到的Socket fd上。比如:當(dāng)用戶層針對某個(gè)socket fd發(fā)起read操作時(shí),如果該socket fd中尚無數(shù)據(jù),那么runtime會(huì)將該socket fd加入到netpoller中監(jiān)聽,同時(shí)對應(yīng)的goroutine被掛起,直到runtime收到socket fd 數(shù)據(jù)ready的通知,runtime才會(huì)重新喚醒等待在該socket fd上準(zhǔn)備read的那個(gè)Goroutine。而這個(gè)過程從Goroutine的視角來看,就像是read操作一直block在那個(gè)socket fd上似的。具體實(shí)現(xiàn)細(xì)節(jié)在后續(xù)場景中會(huì)有補(bǔ)充描述。

二、TCP連接的建立

眾所周知,TCP Socket的連接的建立需要經(jīng)歷客戶端和服務(wù)端的三次握手的過程。連接建立過程中,服務(wù)端是一個(gè)標(biāo)準(zhǔn)的Listen + Accept的結(jié)構(gòu)(可參考上面的代碼),而在客戶端Go語言使用net.Dial或DialTimeout進(jìn)行連接建立:

阻塞Dial:

conn, err := net.Dial("tcp", "google.com:80")
if err != nil {
    //handle error
}
// read or write on conn

或是帶上超時(shí)機(jī)制的Dial:

conn, err := net.DialTimeout("tcp", ":8080", 2 * time.Second)
if err != nil {
    //handle error
}
// read or write on conn

對于客戶端而言,連接的建立會(huì)遇到如下幾種情形:

1、網(wǎng)絡(luò)不可達(dá)或?qū)Ψ椒?wù)未啟動(dòng)

如果傳給Dial的Addr是可以立即判斷出網(wǎng)絡(luò)不可達(dá),或者Addr中端口對應(yīng)的服務(wù)沒有啟動(dòng),端口未被監(jiān)聽,Dial會(huì)幾乎立即返回錯(cuò)誤,比如:

//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")
}

如果本機(jī)8888端口未有服務(wù)程序監(jiān)聽,那么執(zhí)行上面程序,Dial會(huì)很快返回錯(cuò)誤:

$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

2、對方服務(wù)的listen backlog滿

還有一種場景就是對方服務(wù)器很忙,瞬間有大量client端連接嘗試向server建立,server端的listen backlog隊(duì)列滿,server accept不及時(shí)((即便不accept,那么在backlog數(shù)量范疇里面,connect都會(huì)是成功的,因?yàn)閚ew conn已經(jīng)加入到server side的listen queue中了,accept只是從queue中取出一個(gè)conn而已),這將導(dǎo)致client端Dial阻塞。我們還是通過例子感受Dial的行為特點(diǎn):

服務(wù)端代碼:

//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)
}

從程序可以看出,服務(wù)端在listen成功后,每隔10s鐘accept一次??蛻舳藙t是串行的嘗試建立連接。這兩個(gè)程序在Darwin下的執(zhí)行 結(jié)果:

$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初始時(shí)成功地一次性建立了128個(gè)連接,然后后續(xù)每阻塞近10s才能成功建立一條連接。也就是說在server端 backlog滿時(shí)(未及時(shí)accept),客戶端將阻塞在Dial上,直到server端進(jìn)行一次accept。至于為什么是128,這與darwin 下的默認(rèn)設(shè)置有關(guān):

$sysctl -a|grep kern.ipc.somaxconn
kern.ipc.somaxconn: 128

如果我在ubuntu 14.04上運(yùn)行上述server程序,我們的client端初始可以成功建立499條連接。

如果server一直不accept,client端會(huì)一直阻塞么?我們?nèi)サ鬭ccept后的結(jié)果是:在Darwin下,client端會(huì)阻塞大 約1分多鐘才會(huì)返回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運(yùn)行在ubuntu 14.04上,client似乎一直阻塞,我等了10多分鐘依舊沒有返回。 阻塞與否看來與server端的網(wǎng)絡(luò)實(shí)現(xiàn)和設(shè)置有關(guān)。

3、網(wǎng)絡(luò)延遲較大,Dial阻塞并超時(shí)

如果網(wǎng)絡(luò)延遲較大,TCP握手過程將更加艱難坎坷(各種丟包),時(shí)間消耗的自然也會(huì)更長。Dial這時(shí)會(huì)阻塞,如果長時(shí)間依舊無法建立連接,則Dial也會(huì)返回“ getsockopt: operation timed out”錯(cuò)誤。

在連接建立階段,多數(shù)情況下,Dial是可以滿足需求的,即便阻塞一小會(huì)兒。但對于某些程序而言,需要有嚴(yán)格的連接時(shí)間限定,如果一定時(shí)間內(nèi)沒能成功建立連接,程序可能會(huì)需要執(zhí)行一段“異常”處理邏輯,為此我們就需要DialTimeout了。下面的例子將Dial的最長阻塞時(shí)間限制在2s內(nèi),超出這個(gè)時(shí)長,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")
}

執(zhí)行結(jié)果如下(需要模擬一個(gè)延遲較大的網(wǎng)絡(luò)環(huán)境):

$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上進(jìn)行讀寫,以完成業(yè)務(wù)邏輯。前面說過Go runtime隱藏了I/O多路復(fù)用的復(fù)雜性。語言使用者只需采用goroutine+Block I/O的模式即可滿足大部分場景需求。Dial成功后,方法返回一個(gè)net.Conn接口類型變量值,這個(gè)接口變量的動(dòng)態(tài)類型為一個(gè)*TCPConn:

//$GOROOT/src/net/tcpsock_posix.go
type TCPConn struct {
    conn
}

TCPConn內(nèi)嵌了一個(gè)unexported類型:conn,因此TCPConn”繼承”了conn的Read和Write方法,后續(xù)通過Dial返回值調(diào)用的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
}

下面我們先來通過幾個(gè)場景來總結(jié)一下conn.Read的行為特點(diǎn)。

1、Socket中無數(shù)據(jù)

連接建立后,如果對方未發(fā)送數(shù)據(jù)到socket,接收方(Server)會(huì)阻塞在Read操作上,這和前面提到的“模型”原理是一致的。執(zhí)行該Read操作的goroutine也會(huì)被掛起。runtime會(huì)監(jiān)視該socket,直到其有數(shù)據(jù)才會(huì)重新

調(diào)度該socket對應(yīng)的Goroutine完成read。由于篇幅原因,這里就不列代碼了,例子對應(yīng)的代碼文件:go-tcpsock/read_write下的client1.go和server1.go。

2、Socket中有部分?jǐn)?shù)據(jù)

如果socket中有部分?jǐn)?shù)據(jù),且長度小于一次Read操作所期望讀出的數(shù)據(jù)長度,那么Read將會(huì)成功讀出這部分?jǐn)?shù)據(jù)并返回,而不是等待所有期望數(shù)據(jù)全部讀取后再返回。

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發(fā)送”hi”到Server端:

運(yùn)行結(jié)果:

$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中寫入兩個(gè)字節(jié)數(shù)據(jù)(“hi”),Server端創(chuàng)建一個(gè)len = 10的slice,等待Read將讀取的數(shù)據(jù)放入slice;Server隨后讀取到那兩個(gè)字節(jié):”hi”。Read成功返回,n =2 ,err = nil。

3、Socket中有足夠數(shù)據(jù)

如果socket中有數(shù)據(jù),且長度大于等于一次Read操作所期望讀出的數(shù)據(jù)長度,那么Read將會(huì)成功讀出這部分?jǐn)?shù)據(jù)并返回。這個(gè)情景是最符合我們對Read的期待的了:Read將用Socket中的數(shù)據(jù)將我們傳入的slice填滿后返回:n = 10, err = nil。

我們通過client2.go向Server2發(fā)送如下內(nèi)容:abcdefghij12345,執(zhí)行結(jié)果如下:

$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端發(fā)送的內(nèi)容長度為15個(gè)字節(jié),Server端Read buffer的長度為10,因此Server Read第一次返回時(shí)只會(huì)讀取10個(gè)字節(jié);Socket中還剩余5個(gè)字節(jié)數(shù)據(jù),Server再次Read時(shí)會(huì)把剩余數(shù)據(jù)讀出(如:情形2)。

4、Socket關(guān)閉

如果client端主動(dòng)關(guān)閉了socket,那么Server的Read將會(huì)讀到什么呢?這里分為“有數(shù)據(jù)關(guān)閉”和“無數(shù)據(jù)關(guān)閉”。

“有數(shù)據(jù)關(guān)閉”是指在client關(guān)閉時(shí),socket中還有server端未讀取的數(shù)據(jù),我們在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

從輸出結(jié)果來看,當(dāng)client端close socket退出后,server3依舊沒有開始Read,10s后第一次Read成功讀出了5個(gè)字節(jié)的數(shù)據(jù),當(dāng)?shù)诙蜶ead時(shí),由于client端 socket關(guān)閉,Read返回EOF error。

通過上面這個(gè)例子,我們也可以猜測出“無數(shù)據(jù)關(guān)閉”情形下的結(jié)果,那就是Read直接返回EOF error。

5、讀取操作超時(shí)

有些場合對Read的阻塞時(shí)間有嚴(yán)格限制,在這種情況下,Read的行為到底是什么樣的呢?在返回超時(shí)錯(cuò)誤時(shí),是否也同時(shí)Read了一部分?jǐn)?shù)據(jù)了呢?這個(gè)實(shí)驗(yàn)比較難于模擬,下面的測試結(jié)果也未必能反映出所有可能結(jié)果。我們編寫了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方法設(shè)置了10微秒的讀超時(shí)時(shí)間,Server的執(zhí)行結(jié)果如下:

$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微秒超時(shí),但結(jié)果不同,第一次Read超時(shí),讀出數(shù)據(jù)長度為0;第二次讀取所有數(shù)據(jù)成功,沒有超時(shí)。反復(fù)執(zhí)行了多次,沒能出現(xiàn)“讀出部分?jǐn)?shù)據(jù)且返回超時(shí)錯(cuò)誤”的情況。

和讀相比,Write遇到的情形一樣不少,我們也逐一看一下。

1、成功寫

前面例子著重于Read,client端在Write時(shí)并未判斷Write的返回值。所謂“成功寫”指的就是Write調(diào)用返回的n與預(yù)期要寫入的數(shù)據(jù)長度相等,且error = nil。這是我們在調(diào)用Write時(shí)遇到的最常見的情形,這里不再舉例了。

2、寫阻塞

TCP連接通信兩端的OS都會(huì)為該連接保留數(shù)據(jù)緩沖,一端調(diào)用Write后,實(shí)際上數(shù)據(jù)是寫入到OS的協(xié)議棧的數(shù)據(jù)緩沖的。TCP是全雙工通信,因此每個(gè)方向都有獨(dú)立的數(shù)據(jù)緩沖。當(dāng)發(fā)送方將對方的接收緩沖區(qū)以及自身的發(fā)送緩沖區(qū)寫滿后,Write就會(huì)阻塞。我們來看一個(gè)例子: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數(shù)據(jù),因此當(dāng)client5一直嘗試寫入時(shí),寫到一定量后就會(huì)發(fā)生阻塞:

$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上,這個(gè)size大約在679468bytes。后續(xù)當(dāng)server5每隔5s進(jìn)行Read時(shí),OS socket緩沖區(qū)騰出了空間,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、寫入部分?jǐn)?shù)據(jù)

Write操作存在寫入部分?jǐn)?shù)據(jù)的情況,比如上面例子中,當(dāng)client端輸出日志停留在“write 65536 bytes this time, 655360 bytes in total”時(shí),我們殺掉server5,這時(shí)我們會(huì)看到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這個(gè)地方阻塞的,而是后續(xù)又寫入24108后發(fā)生了阻塞,server端socket關(guān)閉后,我們看到Wrote返回er != nil且n = 24108,程序需要對這部分寫入的24108字節(jié)做特定處理。

4、寫入超時(shí)

如果非要給Write增加一個(gè)期限,那我們可以調(diào)用SetWriteDeadline方法。我們copy一份client5.go,形成client6.go,在client6.go的Write之前增加一行timeout設(shè)置代碼:

conn.SetWriteDeadline(time.Now().Add(time.Microsecond * 10))

啟動(dòng)server6.go,啟動(dòng)client6.go,我們可以看到寫入超時(shí)的情況下,Write的返回結(jié)果:

$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

可以看到在寫入超時(shí)時(shí),依舊存在部分?jǐn)?shù)據(jù)寫入的情況。

綜上例子,雖然Go給我們提供了阻塞I/O的便利,但在調(diào)用Read和Write時(shí)依舊要綜合需要方法返回的n和err的結(jié)果,以做出正確處理。net.conn實(shí)現(xiàn)了io.Reader和io.Writer接口,因此可以試用一些wrapper包進(jìn)行socket讀寫,比如bufio包下面的Writer和Reader、io/ioutil下的函數(shù)等。

Goroutine safe
基于goroutine的網(wǎng)絡(luò)架構(gòu)模型,存在在不同goroutine間共享conn的情況,那么conn的讀寫是否是goroutine safe的呢?在深入這個(gè)問題之前,我們先從應(yīng)用意義上來看read操作和write操作的goroutine-safe必要性。

對于read操作而言,由于TCP是面向字節(jié)流,conn.Read無法正確區(qū)分?jǐn)?shù)據(jù)的業(yè)務(wù)邊界,因此多個(gè)goroutine對同一個(gè)conn進(jìn)行read的意義不大,goroutine讀到不完整的業(yè)務(wù)包反倒是增加了業(yè)務(wù)處理的難度。對與Write操作而言,倒是有多個(gè)goroutine并發(fā)寫的情況。不過conn讀寫是否goroutine-safe的測試不是很好做,我們先深入一下runtime代碼,先從理論上給這個(gè)問題定個(gè)性:

net.conn只是netFD的wrapper結(jié)構(gòu),最終Write和Read都會(huì)落在其中的fd上:

type conn struct {
    fd *netFD
}

netFD在不同平臺上有著不同的實(shí)現(xiàn),我們以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中包含了一個(gè)runtime實(shí)現(xiàn)的fdMutex類型字段,從注釋上來看,該fdMutex用來串行化對該netFD對應(yīng)的sysfd的Write和Read操作。從這個(gè)注釋上來看,所有對conn的Read和Write操作都是有fdMutex互斥的,從netFD的Read和Write方法的實(shí)現(xiàn)也證實(shí)了這一點(diǎn):

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保護(hù),直到此次數(shù)據(jù)全部write完。因此在應(yīng)用層面,要想保證多個(gè)goroutine在一個(gè)conn上write操作的Safe,需要一次write完整寫入一個(gè)“業(yè)務(wù)包”;一旦將業(yè)務(wù)包的寫入拆分為多次write,那就無法保證某個(gè)Goroutine的某“業(yè)務(wù)包”數(shù)據(jù)在conn發(fā)送的連續(xù)性。

同時(shí)也可以看出即便是Read操作,也是lock保護(hù)的。多個(gè)Goroutine對同一conn的并發(fā)讀不會(huì)出現(xiàn)讀出內(nèi)容重疊的情況,但內(nèi)容斷點(diǎn)是依 runtime調(diào)度來隨機(jī)確定的。存在一個(gè)業(yè)務(wù)包數(shù)據(jù),1/3內(nèi)容被goroutine-1讀走,另外2/3被另外一個(gè)goroutine-2讀 走的情況。比如一個(gè)完整包:world,當(dāng)goroutine的read slice size < 5時(shí),存在可能:一個(gè)goroutine讀到 “worl”,另外一個(gè)goroutine讀出”d”。

四、Socket屬性

原生Socket API提供了豐富的sockopt設(shè)置接口,但Golang有自己的網(wǎng)絡(luò)架構(gòu)模型,golang提供的socket options接口也是基于上述模型的必要的屬性設(shè)置。包括

SetKeepAlive
SetKeepAlivePeriod
SetLinger
SetNoDelay (默認(rèn)no delay)
SetWriteBuffer
SetReadBuffer

不過上面的Method是TCPConn的,而不是Conn的,要使用上面的Method的,需要type assertion:

tcpConn, ok := c.(*TCPConn)
if !ok {
    //error handle
}

tcpConn.SetNoDelay(true)

對于listener socket, golang默認(rèn)采用了 SO_REUSEADDR,這樣當(dāng)你重啟 listener程序時(shí),不會(huì)因?yàn)閍ddress in use的錯(cuò)誤而啟動(dòng)失敗。而listen backlog的默認(rèn)值是通過獲取系統(tǒng)的設(shè)置值得到的。不同系統(tǒng)不同:mac 128, linux 512等。

五、關(guān)閉連接

和前面的方法相比,關(guān)閉連接算是最簡單的操作了。由于socket是全雙工的,client和server端在己方已關(guān)閉的socket和對方關(guān)閉的socket上操作的結(jié)果有不同。看下面例子:

//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]))
    }
}
... ...

上述例子的執(zhí)行結(jié)果如下:

$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的結(jié)果來看,在己方已經(jīng)關(guān)閉的socket上再進(jìn)行read和write操作,會(huì)得到”use of closed network connection” error;

從server1的執(zhí)行結(jié)果來看,在對方關(guān)閉的socket上執(zhí)行read操作會(huì)得到EOF error,但write操作會(huì)成功,因?yàn)閿?shù)據(jù)會(huì)成功寫入己方的內(nèi)核socket緩沖區(qū)中,即便最終發(fā)不到對方socket緩沖區(qū)了,因?yàn)榧悍絪ocket并未關(guān)閉。因此當(dāng)發(fā)現(xiàn)對方socket關(guān)閉后,己方應(yīng)該正確合理處理自己的socket,再繼續(xù)write已經(jīng)無任何意義了。

六、小結(jié)

本文比較基礎(chǔ),但卻很重要,畢竟golang是面向大規(guī)模服務(wù)后端的,對通信環(huán)節(jié)的細(xì)節(jié)的深入理解會(huì)大有裨益。另外Go的goroutine+阻塞通信的網(wǎng)絡(luò)通信模型降低了開發(fā)者心智負(fù)擔(dān),簡化了通信的復(fù)雜性,這點(diǎn)尤為重要。

本文代碼實(shí)驗(yàn)環(huán)境:go 1.5.1 on Darwin amd64以及部分在ubuntu 14.04 amd64。文章來源地址http://www.zghlxwxcb.cn/news/detail-552616.html

到了這里,關(guān)于Go語言TCP Socket編程的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • TCP/IP(十一)TCP的連接管理(八)socket網(wǎng)絡(luò)編程

    TCP/IP(十一)TCP的連接管理(八)socket網(wǎng)絡(luò)編程

    一? socket網(wǎng)絡(luò)編程 ?socket 基本操作函數(shù) bind、listen、connect、accept、recv、send、select、close ①??針對 TCP 應(yīng)該如何 Socket 編程? ②? ?listen 時(shí)候參數(shù) backlog 的意義? ③??accept 發(fā)生在三次握手的哪一步? ④? ?客戶端調(diào)用 close 了,連接是斷開的流程是什么? ⑤??沒有 accept,能建立 T

    2024年02月07日
    瀏覽(29)
  • Go語言(Golang)編寫 TCP 端口掃描器

    TCP,也就是傳輸控制協(xié)議(Transmission Control Protocol)。 建立 TCP連接(或者叫打開端口),需要3次握手 客戶端 - 端口打開 -服務(wù)器 syn (請求建立新連接) syn-ack (同意創(chuàng)建新連接) ack (表示響應(yīng)) 服務(wù)端端口關(guān)閉 Closed Port client -syn- Server Server -rst- Client 如果存在防火墻 Fi

    2024年02月03日
    瀏覽(27)
  • Python網(wǎng)絡(luò)編程基礎(chǔ)之ip地址,端口號,TCP,socket

    Python網(wǎng)絡(luò)編程基礎(chǔ)之ip地址,端口號,TCP,socket

    IP地址 IP地址 (Internet Protocol Address)是指互聯(lián)網(wǎng)協(xié)議地址,又譯為網(wǎng)際協(xié)議地址。 IP地址是IP協(xié)議提供的一種統(tǒng)一的地址格式,它為互聯(lián)網(wǎng)上的每一個(gè)網(wǎng)絡(luò)和每一臺主機(jī)分配一個(gè)邏輯地址,以此來屏蔽物理地址的差異。 換而言之,IP 地址就是標(biāo)識網(wǎng)絡(luò)中設(shè)備的一個(gè)地址,好比

    2024年02月02日
    瀏覽(22)
  • C語言Socket編程TCP簡單聊天室

    C語言Socket編程TCP簡單聊天室

    這是一個(gè)使用C語言進(jìn)行套接字編程實(shí)現(xiàn)的簡單聊天室, 使用Pthread庫進(jìn)行多線程執(zhí)行 服務(wù)端: svr.c 客戶端: cli.c Makefile: Makefile 執(zhí)行編譯 啟動(dòng)服務(wù)器 啟動(dòng)客戶端 聊天 退出

    2024年02月03日
    瀏覽(27)
  • Socket網(wǎng)絡(luò)編程(TCP/IP)實(shí)現(xiàn)服務(wù)器/客戶端通信。

    Socket網(wǎng)絡(luò)編程(TCP/IP)實(shí)現(xiàn)服務(wù)器/客戶端通信。

    一.前言 回顧之前進(jìn)程間通信(無名管道,有名管道,消息隊(duì)列,共享內(nèi)存,信號,信號量),都是在同一主機(jī)由內(nèi)核來完成的通信。 那不同主機(jī)間該怎么通信呢? 可以使用Socket編程來實(shí)現(xiàn)。 Socket編程可以通過網(wǎng)絡(luò)來實(shí)現(xiàn)實(shí)現(xiàn)不同主機(jī)之間的通訊。 二.Socket編程的網(wǎng)絡(luò)模型如

    2024年02月08日
    瀏覽(37)
  • Qt6教程之三(13) TCP/IP通訊與socket編程

    Qt6教程之三(13) TCP/IP通訊與socket編程

    目錄 一 前言 二 TCP/IP協(xié)議架構(gòu)和通信原理 三?TCP/IP的連接與斷開過程 四 Qt中開發(fā)TCP/IP原理概述 五 完整實(shí)例代碼示范 在軟件開發(fā)中,常用的技術(shù)體系里面網(wǎng)絡(luò)通信屬于最重要的 “聯(lián)通” 技術(shù),是必須要掌握的技術(shù)。 那為什么網(wǎng)絡(luò)通信如此重要呢,我想大概有以下幾點(diǎn): 不

    2024年02月12日
    瀏覽(24)
  • 【Linux】socket 編程(socket套接字介紹、字節(jié)序、socket地址、IP地址轉(zhuǎn)換函數(shù)、套接字函數(shù)、TCP通信實(shí)現(xiàn))

    【Linux】socket 編程(socket套接字介紹、字節(jié)序、socket地址、IP地址轉(zhuǎn)換函數(shù)、套接字函數(shù)、TCP通信實(shí)現(xiàn))

    橙色 所謂套接字,就是對網(wǎng)絡(luò)中不同主機(jī)上的應(yīng)用進(jìn)程之間進(jìn)行雙向通信的端點(diǎn)的抽象。 一個(gè)套接字就是網(wǎng)絡(luò)上進(jìn)程通信的一端,提供了應(yīng)用層進(jìn)程利用網(wǎng)絡(luò)協(xié)議交換數(shù)據(jù)的機(jī)制。從所處的地位來講,套接字上聯(lián)應(yīng)用進(jìn)程,下聯(lián)網(wǎng)絡(luò)協(xié)議棧,是應(yīng)用程序通過網(wǎng)絡(luò)協(xié)議進(jìn)程通

    2024年02月09日
    瀏覽(19)
  • Socket編程詳解:從基本概念到實(shí)例應(yīng)用(TCP|UDP C語言實(shí)例詳解)

    簡介: Socket編程是網(wǎng)絡(luò)編程中至關(guān)重要的一部分,它提供了一種在不同主機(jī)之間進(jìn)行數(shù)據(jù)通信的方式。本篇博客將詳細(xì)介紹Socket編程的基本概念、原理和實(shí)例應(yīng)用,幫助讀者深入理解和掌握這一重要技術(shù)。 正文: 一、Socket編程概述 Socket是一種通信機(jī)制,通過它可以在不同主

    2024年02月14日
    瀏覽(14)
  • C++開發(fā)基礎(chǔ)之網(wǎng)絡(luò)編程WinSock庫使用詳解TCP/UDP Socket開發(fā)

    Winsock是Windows操作系統(tǒng)提供的用于網(wǎng)絡(luò)編程的API庫。它是Windows Sockets的簡稱,也就是套接字庫。Winsock可以讓開發(fā)人員使用TCP/IP協(xié)議族中的各種協(xié)議,如TCP、UDP等,在Windows平臺下進(jìn)行網(wǎng)絡(luò)編程。 Winsock提供了一組函數(shù)和數(shù)據(jù)結(jié)構(gòu),這些函數(shù)和數(shù)據(jù)結(jié)構(gòu)可以讓開發(fā)人員創(chuàng)建和管理

    2024年01月23日
    瀏覽(25)
  • 【socket】從計(jì)算機(jī)網(wǎng)絡(luò)基礎(chǔ)到socket編程——Windows && Linux C語言 + Python實(shí)現(xiàn)(TCP+UDP)

    【socket】從計(jì)算機(jī)網(wǎng)絡(luò)基礎(chǔ)到socket編程——Windows && Linux C語言 + Python實(shí)現(xiàn)(TCP+UDP)

    簡單講一下基礎(chǔ)知識,便于后面代碼的理解,建議大概瀏覽一下這一小節(jié)內(nèi)容。這里講的只是冰山一角,建議大家學(xué)習(xí)計(jì)算機(jī)網(wǎng)絡(luò)相關(guān)知識,推薦幾本書: 《計(jì)算機(jī)網(wǎng)絡(luò)》(謝希仁) 《計(jì)算機(jī)網(wǎng)絡(luò) 自頂向下方法》 《計(jì)算機(jī)網(wǎng)絡(luò)技術(shù)》 《計(jì)算機(jī)網(wǎng)絡(luò)基礎(chǔ)及應(yīng)用》 《Linux C從入

    2024年02月08日
    瀏覽(22)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包