Go語言學習之9 網絡協議TCP、Redis與聊天室

主要內容html

1. Tcp編程
2. redis使用linux

1. Tcp編程git

(1)簡介github

      Golang是谷歌設計開發的語言,在Golang的設計之初就把高併發的性能做爲Golang的主要特性之一,也是面向大規模後端服務程序。在服務器端網絡通訊是必不可少的也是相當重要的一部分。Golang內置的包例如net、net/http中的底層就是對TCP socket方法的封裝。golang

    TCP簡介:web

1 Golang是谷歌設計開發的語言,在Golang的設計之初就把高併發的性能做爲Golang的主要特性之一,也是面向大規模後端服務程序。在服務器端網絡通訊是必不可少的也是相當重要的一部分。Golang內置的包例如net、net/http中的底層就是對TCP socket方法的封裝。 2 網絡編程方面,咱們最經常使用的就是tcp socket編程了,在posix標準出來後,socket在各大主流OS平臺上都獲得了很好的支持。關於tcp programming,最好的資料莫過於W. Richard Stevens 的網絡編程聖經《UNIX網絡 編程 卷1:套接字聯網API》 了,書中關於tcp socket接口的各類使用、行爲模式、異常處理講解的十分細緻。
TCP簡介

      Go是自帶runtime的跨平臺編程語言,Go中暴露給語言使用者的tcp socket api是創建OS原生tcp socket接口之上的。因爲Go runtime調度的須要,golang tcp socket接口在行爲特色與異常處理方面與OS原生接口有着一些差異。redis

(2)模型數據庫

      從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端程序大體以下:ubuntu

 1 //go-tcpsock/server.go
 2 func HandleConn(conn net.Conn) {  3  defer conn.Close()  4 
 5     for {  6         // read from the connection  7         // ... ...  8         // write to the connection  9         //... ...
10  } 11 } 12 
13 func main() { 14     listen, err := net.Listen("tcp", ":8888") 15     if err != nil { 16         fmt.Println("listen error: ", err) 17         return
18  } 19 
20     for { 21         conn, err := listen.Accept() 22         if err != nil { 23             fmt.Println("accept error: ", err) 24             break
25  } 26 
27         // start a new goroutine to handle the new connection
28  go HandleConn(conn) 29  } 30 }
Go server編程

     (重點)用戶層眼中看到的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上似的。

   關於netpoller能夠看下這爲博主博客:http://www.opscoder.info/golang_netpoller.html

(3)TCP鏈接的創建

      衆所周知,TCP Socket的鏈接的創建須要經歷客戶端和服務端的三次握手的過程。鏈接創建過程當中,服務端是一個標準的Listen + Accept的結構(可參考上面的代碼),而在客戶端Go語言使用net.Dial()或net.DialTimeout()進行鏈接創建。

      服務端的處理流程: a. 監聽端口     b. 接收客戶端的連接     c. 建立goroutine,處理該連接

      客戶端的處理流程: a. 創建與服務端的連接     b. 進行數據收發     c. 關閉連接

      經過客戶端可服務端實現一個簡單的聊天系統?

      客戶端:

 1 package main  2 
 3 import (  4     "bufio"
 5     "fmt"
 6     "net"
 7     "os"
 8     "strings"
 9 ) 10 
11 func main() { 12     fmt.Println("start client...") 13     conn, err := net.Dial("tcp", "localhost:50000") 14     if err != nil { 15         fmt.Println("Error dialing", err.Error()) 16         return
17  } 18 
19  defer conn.Close() 20     inputReader := bufio.NewReader(os.Stdin) 21     for { 22         input, _ := inputReader.ReadString('\n') 23         trimmedInput := strings.Trim(input, "\r\n") 24         if trimmedInput == "Q" { 25             return
26  } 27         _, err = conn.Write([]byte(trimmedInput)) 28         if err != nil { 29             return
30  } 31  } 32 }
client.go

      服務端:

 1 package main  2 
 3 import (  4     "fmt"
 5     "net"
 6     "io"
 7 )  8 func main() {  9     fmt.Println("start server...") 10     listen, err := net.Listen("tcp", "0.0.0.0:50000") 11     if err != nil { 12         fmt.Println("listen failed, err:", err) 13         return
14  } 15     for { 16         conn, err := listen.Accept() 17         if err != nil { 18             fmt.Println("accept failed, err:", err) 19             continue
20  } 21  go process(conn) 22  } 23 } 24 
25 func process(conn net.Conn) { 26  defer conn.Close() 27 
28     for { 29         buf := make([]byte, 10) 30         _, err := conn.Read(buf) 31 
32         if err == io.EOF {  //當客戶端斷開的時候就沒法讀到數據
33             fmt.Println("read end") 34             return
35  } 36 
37         if err != nil { 38             fmt.Println("read err:", err) 39             return
40  } 41         fmt.Println("read: ", string(buf)) 42  } 43 }
server.go

     阻塞Dial:

1 conn, err := net.Dial("tcp", "www.baidu.com:80") 2 if err != nil { 3     //handle error
4 } 5 //read or write on conn
阻塞Dial

      超時機制的Dial:

1 conn, err := net.DialTimeout("tcp", "www.baidu.com:80", 2*time.Second) 2 if err != nil { 3     //handle error
4 } 5 //read or write on conn
超時Dial

      對於客戶端而言,鏈接的創建會遇到以下幾種情形:

  • 網絡不可達或對方服務未啓動

      若是傳給Dial的Addr是能夠當即判斷出網絡不可達,或者Addr中端口對應的服務沒有啓動,端口未被監聽,Dial會幾乎當即返回錯誤,好比:

 1 package main  2 
 3 import (  4     "net"
 5     "log"
 6 )  7 
 8 func main() {  9     log.Println("begin dial...") 10     conn, err := net.Dial("tcp", ":8888") 11     if err != nil { 12         log.Println("dial error:", err) 13         return
14  } 15  defer conn.Close() 16     log.Println("dial ok") 17 }
網絡不可達或對方服務未啓動

      若是本機8888端口未有服務程序監聽,那麼執行上面程序,Dial會很快返回錯誤:

      注:在Centos6.5上測試,下同。

  • 對方服務的listen backlog滿 

      還有一種場景就是對方服務器很忙,瞬間有大量client端鏈接嘗試向server創建,server端的listen backlog隊列滿,server accept不及時((即使不accept,那麼在backlog數量範疇裏面,connect都會是成功的,由於new conn已經加入到server side的listen queue中了,accept只是從queue中取出一個conn而已),這將致使client端Dial阻塞。咱們仍是經過例子感覺Dial的行爲特色:

      服務端代碼:

 1 package main  2 
 3 import (  4     "net"
 5     "log"
 6     "time"
 7 )  8 
 9 func main() { 10     l, err := net.Listen("tcp", ":8888") 11     if err != nil { 12         log.Println("error listen:", err) 13         return
14  } 15  defer l.Close() 16     log.Println("listen ok") 17 
18     var i int
19     for { 20         time.Sleep(time.Second * 10) 21         if _, err := l.Accept(); err != nil { 22             log.Println("accept error:", err) 23             break
24  } 25         i++
26         log.Printf("%d: accept a new connection\n", i) 27  } 28 }
server.go

      客戶端代碼:

 1 package main  2 
 3 import (  4     "net"
 5     "log"
 6     "time"
 7 )  8 
 9 func establishConn(i int) net.Conn { 10     conn, err := net.Dial("tcp", ":8888") 11     if err != nil { 12         log.Printf("%d: dial error: %s", i, err) 13         return nil 14  } 15     log.Println(i, ":connect to server ok") 16     return conn 17 } 18 
19 func main() { 20     var sl []net.Conn 21 
22     for i := 1; i < 1000; i++ { 23         conn := establishConn(i) 24         if conn != nil { 25             sl = append(sl, conn) 26  } 27  } 28 
29     time.Sleep(time.Second * 10000) 30 }
client.go

      通過測試在Client初始時成功地一次性創建了131個鏈接,而後後續每阻塞近1s才能成功創建一條鏈接。也就是說在server端 backlog滿時(未及時accept),客戶端將阻塞在Dial上,直到server端進行一次accept。

      若是server一直不accept,client端會一直阻塞麼?咱們去掉accept後的結果是:在Darwin下,client端會阻塞大 約1分多鐘纔會返回timeout。而若是server運行在ubuntu 14.04上,client彷佛一直阻塞,我等了10多分鐘依舊沒有返回。 阻塞與否看來與server端的網絡實現和設置有關。

      注:在Centos6.5上測試,發現註釋掉server端的accept,client一次創建131個鏈接後,後面還會每隔1s創建一個連接。

  • 網絡延遲較大,Dial阻塞並超時

      若是網絡延遲較大,TCP握手過程將更加艱難坎坷(各類丟包),時間消耗的天然也會更長。Dial這時會阻塞,若是長時間依舊沒法創建鏈接,則Dial也會返回「 getsockopt: operation timed out」錯誤。

      在鏈接創建階段,多數狀況下,Dial是能夠知足需求的,即使阻塞一小會兒。但對於某些程序而言,須要有嚴格的鏈接時間限定,若是必定時間內沒能成功創建鏈接,程序可能會須要執行一段「異常」處理邏輯,爲此咱們就須要DialTimeout了。下面的例子將Dial的最長阻塞時間限制在2s內,超出這個時長,Dial將返回timeout error:

 1 package main  2 
 3 import (  4     "net"
 5     "log"
 6     "time"
 7 )  8 
 9 func main() { 10     log.Println("begin dial...") 11     conn, err := net.DialTimeout("tcp", "192.168.30.134:8888", 2*time.Second) 12     if err != nil { 13         log.Println("dial error:", err) 14         return
15  } 16  defer conn.Close() 17     log.Println("dial ok") 18 }
client_timeout.go

      執行結果以下,須要模擬一個網絡延遲大的環境:

1 $go run client_timeout.go 2 2015/11/17 09:28:34 begin dial... 3 2015/11/17 09:28:36 dial error: dial tcp 104.236.176.96:80: i/o timeout

(4)Socket讀寫

      鏈接創建起來後,咱們就要在conn上進行讀寫,以完成業務邏輯。前面說過Go runtime隱藏了I/O多路複用的複雜性。語言使用者只需採用goroutine+Block I/O的模式便可知足大部分場景需求。Dial成功後,方法返回一個Conn接口類型變量值。

      客戶端Dial創建鏈接:

func Dial(network, address string) (Conn, error)
 1 type Conn interface {  2         // Read reads data from the connection.  3         // Read can be made to time out and return an Error with Timeout() == true  4         // after a fixed time limit; see SetDeadline and SetReadDeadline.
 5         Read(b []byte) (n int, err error)  6 
 7         // Write writes data to the connection.  8         // Write can be made to time out and return an Error with Timeout() == true  9         // after a fixed time limit; see SetDeadline and SetWriteDeadline.
10         Write(b []byte) (n int, err error) 11 
12         // Close closes the connection. 13         // Any blocked Read or Write operations will be unblocked and return errors.
14  Close() error 15 
16         // LocalAddr returns the local network address.
17  LocalAddr() Addr 18 
19         // RemoteAddr returns the remote network address.
20  RemoteAddr() Addr 21 
22         // SetDeadline sets the read and write deadlines associated 23         // with the connection. It is equivalent to calling both 24         // SetReadDeadline and SetWriteDeadline. 25         //
26         // A deadline is an absolute time after which I/O operations 27         // fail with a timeout (see type Error) instead of 28         // blocking. The deadline applies to all future and pending 29         // I/O, not just the immediately following call to Read or 30         // Write. After a deadline has been exceeded, the connection 31         // can be refreshed by setting a deadline in the future. 32         //
33         // An idle timeout can be implemented by repeatedly extending 34         // the deadline after successful Read or Write calls. 35         //
36         // A zero value for t means I/O operations will not time out.
37  SetDeadline(t time.Time) error 38 
39         // SetReadDeadline sets the deadline for future Read calls 40         // and any currently-blocked Read call. 41         // A zero value for t means Read will not time out.
42  SetReadDeadline(t time.Time) error 43 
44         // SetWriteDeadline sets the deadline for future Write calls 45         // and any currently-blocked Write call. 46         // Even if write times out, it may return n > 0, indicating that 47         // some of the data was successfully written. 48         // A zero value for t means Write will not time out.
49  SetWriteDeadline(t time.Time) error 50 }
Conn接口

      服務器端Listen監聽客戶端鏈接:

func Listen(network, address string) (Listener, error)
 1 type Listener interface {  2         // Accept waits for and returns the next connection to the listener.
 3  Accept() (Conn, error)  4 
 5         // Close closes the listener.  6         // Any blocked Accept operations will be unblocked and return errors.
 7  Close() error  8 
 9         // Addr returns the listener's network address.
10  Addr() Addr 11 }
Listener 接口

      從Conn接口中有Read,Write,Close等方法。

 1)conn.Read的特色

  • Socket中無數據

      鏈接創建後,若是對方未發送數據到socket,接收方(Server)會阻塞在Read操做上,這和前面提到的「模型」原理是一致的。執行該Read操做的goroutine也會被掛起。runtime會監視該socket,直到其有數據纔會從新調度該socket對應的Goroutine完成read。例子對應的代碼文件:go-tcpsock/read_write下的client1.go和server1.go。

 1 package main  2 
 3 import (  4     "log"
 5     "net"
 6     "time"
 7 )  8 
 9 func main() { 10     log.Println("begin dial...") 11     conn, err := net.Dial("tcp", ":8888") 12     if err != nil { 13         log.Println("dial error:", err) 14         return
15  } 16  defer conn.Close() 17     log.Println("dial ok") 18     time.Sleep(time.Second * 10000) 19 }
client1.go
 1 //server.go
 2 
 3 package main  4 
 5 import (  6     "log"
 7     "net"
 8 )  9 
10 func handleConn(c net.Conn) { 11  defer c.Close() 12     for { 13         // read from the connection
14         var buf = make([]byte, 10) 15         log.Println("start to read from conn") 16         n, err := c.Read(buf) 17         if err != nil { 18             log.Println("conn read error:", err) 19             return
20  } 21         log.Printf("read %d bytes, content is %s\n", n, string(buf[:n])) 22  } 23 } 24 
25 func main() { 26     l, err := net.Listen("tcp", ":8888") 27     if err != nil { 28         log.Println("listen error:", err) 29         return
30  } 31 
32     for { 33         c, err := l.Accept() 34         if err != nil { 35             log.Println("accept error:", err) 36             break
37  } 38         // start a new goroutine to handle 39         // the new connection.
40         log.Println("accept a new connection") 41  go handleConn(c) 42  } 43 }
server1.go
  • Socket中有部分數據

      若是socket中有部分數據,且長度小於一次Read操做所指望讀出的數據長度,那麼Read將會成功讀出這部分數據並返回,而不是等待全部指望數據所有讀取後再返回。

      客戶端:

 1 //client2.go
 2 package main  3 
 4 import (  5     "fmt"
 6     "log"
 7     "net"
 8     "os"
 9     "time"
10 ) 11 
12 func main() { 13     if len(os.Args) <= 1 { 14         fmt.Println("usage: go run client2.go YOUR_CONTENT") 15         return
16  } 17     log.Println("begin dial...") 18     conn, err := net.Dial("tcp", ":8888") 19     if err != nil { 20         log.Println("dial error:", err) 21         return
22  } 23  defer conn.Close() 24     log.Println("dial ok") 25 
26     time.Sleep(time.Second * 2) 27     data := os.Args[1] 28     conn.Write([]byte(data)) 29 
30     time.Sleep(time.Second * 10000) 31 }
client2.go

      服務端:

 1 //server2.go
 2 package main  3 
 4 import (  5     "log"
 6     "net"
 7 )  8 
 9 func handleConn(c net.Conn) { 10  defer c.Close() 11     for { 12         // read from the connection
13         var buf = make([]byte, 10) 14         log.Println("start to read from conn") 15         n, err := c.Read(buf) 16         if err != nil { 17             log.Println("conn read error:", err) 18             return
19  } 20         log.Printf("read %d bytes, content is %s\n", n, string(buf[:n])) 21  } 22 } 23 
24 func main() { 25     l, err := net.Listen("tcp", ":8888") 26     if err != nil { 27         log.Println("listen error:", err) 28         return
29  } 30 
31     for { 32         c, err := l.Accept() 33         if err != nil { 34             log.Println("accept error:", err) 35             break
36  } 37         // start a new goroutine to handle 38         // the new connection.
39         log.Println("accept a new connection") 40  go handleConn(c) 41  } 42 }
server2.go

      經過client2.go發送」hi」到Server端:

F:\Go\project\src\go_dev\go-tcpsock\read_write>go run client2.go hi 2019/03/04 22:43:41 begin dial... 2019/03/04 22:43:41 dial ok F:\Go\project\src\go_dev\go-tcpsock\read_write>go run server2.go 2019/03/04 22:43:41 accept a new connection 2019/03/04 22:43:41 start to read from conn 2019/03/04 22:43:43 read 2 bytes, content is hi 2019/03/04 22:43:43 start to read from conn
  • Socket中有足夠數據

      若是socket中有數據,且長度大於等於一次Read操做所指望讀出的數據長度,那麼Read將會成功讀出這部分數據並返回。這個情景是最符合咱們對Read的期待的了:Read將用Socket中的數據將咱們傳入的slice填滿後返回:n = 10, err = nil。

      執行結果:

F:\Go\project\src\go_dev\go-tcpsock\read_write>go run client2.go abcdefghij123 2019/03/04 22:50:01 begin dial... 2019/03/04 22:50:01 dial ok F:\Go\project\src\go_dev\go-tcpsock\read_write>go run server2.go 2019/03/04 22:50:01 accept a new connection 2019/03/04 22:50:01 start to read from conn 2019/03/04 22:50:03 read 10 bytes, content is abcdefghij 2019/03/04 22:50:03 start to read from conn 2019/03/04 22:50:03 read 3 bytes, content is 123
2019/03/04 22:50:03 start to read from conn

      結果分析: client端發送的內容長度爲13個字節,Server端Read buffer的長度爲10,所以Server Read第一次返回時只會讀取10個字節;Socket中還剩餘3個字節數據,Server再次Read時會把剩餘數據讀出(如:情形2)。

  • Socket關閉

      若是client端主動關閉了socket,那麼Server的Read將會讀到什麼呢?

      這裏分爲「有數據關閉」和「無數據關閉」:

      有數據關閉是指在client關閉時,socket中還有server端未讀取的數據。當client端close socket退出後,server依舊沒有開始Read,10s後第一次Read成功讀出了全部的數據,當第二次Read時,因爲client端 socket關閉,Read返回EOF error。

      客戶端:

 1 //client3.go
 2 package main  3 
 4 import (  5     "fmt"
 6     "log"
 7     "net"
 8     "os"
 9     "time"
10 ) 11 
12 func main() { 13     if len(os.Args) <= 1 { 14         fmt.Println("usage: go run client3.go YOUR_CONTENT") 15         return
16  } 17     log.Println("begin dial...") 18     conn, err := net.Dial("tcp", ":8888") 19     if err != nil { 20         log.Println("dial error:", err) 21         return
22  } 23  defer conn.Close() 24     log.Println("dial ok") 25 
26     time.Sleep(time.Second * 2) 27     data := os.Args[1] 28     conn.Write([]byte(data)) 29 }
client3.go

          服務端:

 1 //server3.go
 2 
 3 package main  4 
 5 import (  6     "log"
 7     "net"
 8     "time"
 9 ) 10 
11 func handleConn(c net.Conn) { 12  defer c.Close() 13     for { 14         // read from the connection
15         time.Sleep(10 * time.Second) 16         var buf = make([]byte, 10) 17         log.Println("start to read from conn") 18         n, err := c.Read(buf) 19         if err != nil { 20             log.Println("conn read error:", err) 21             return
22  } 23         log.Printf("read %d bytes, content is %s\n", n, string(buf[:n])) 24  } 25 } 26 
27 func main() { 28     l, err := net.Listen("tcp", ":8888") 29     if err != nil { 30         log.Println("listen error:", err) 31         return
32  } 33 
34     for { 35         c, err := l.Accept() 36         if err != nil { 37             log.Println("accept error:", err) 38             break
39  } 40         // start a new goroutine to handle 41         // the new connection.
42         log.Println("accept a new connection") 43  go handleConn(c) 44  } 45 }
server3.go

      執行結果:

F:\Go\project\src\go_dev\go-tcpsock\read_write>go run client3.go hello 2019/03/04 22:55:49 begin dial... 2019/03/04 22:55:49 dial ok F:\Go\project\src\go_dev\go-tcpsock\read_write>go run server3.go 2019/03/04 22:55:49 accept a new connection 2019/03/04 22:55:59 start to read from conn 2019/03/04 22:55:59 read 5 bytes, content is hello 2019/03/04 22:56:09 start to read from conn 2019/03/04 22:56:09 conn read error: EOF

      結果分析:從輸出結果來看,當client端close socket退出後,server3依舊沒有開始Read,10s後第一次Read成功讀出了5個字節的數據,當第二次Read時,因爲client端 socket關閉,Read返回EOF error。

      經過上面這個例子,咱們也能夠猜想出「無數據關閉」情形下的結果,那就是Read直接返回EOF error。

  • 讀取操做超時

      有些場合對Read的阻塞時間有嚴格限制,在這種狀況下,Read的行爲究竟是什麼樣的呢?在返回超時錯誤時,是否也同時Read了一部分數據了呢? 這個實驗比較難於模擬,下面的測試結果也未必能反映出全部可能結果。

      客戶端:

 1 //client4.go
 2 package main  3 
 4 import (  5     "log"
 6     "net"
 7     "time"
 8 )  9 
10 func main() { 11     log.Println("begin dial...") 12     conn, err := net.Dial("tcp", ":8888") 13     if err != nil { 14         log.Println("dial error:", err) 15         return
16  } 17  defer conn.Close() 18     log.Println("dial ok") 19 
20     data := make([]byte, 65536) 21  conn.Write(data) 22 
23     time.Sleep(time.Second * 10000) 24 }
client4.go

      服務端:

 1 //server4.go
 2 
 3 package main  4 
 5 import (  6     "log"
 7     "net"
 8     "time"
 9 ) 10 
11 func handleConn(c net.Conn) { 12  defer c.Close() 13     for { 14         // read from the connection
15         time.Sleep(10 * time.Second) 16         var buf = make([]byte, 65536) 17         log.Println("start to read from conn") 18         //c.SetReadDeadline(time.Now().Add(time.Microsecond * 10))//conn read 0 bytes, error: read tcp 127.0.0.1:8888->127.0.0.1:60763: i/o timeout
19         c.SetReadDeadline(time.Now().Add(time.Microsecond * 10)) 20         n, err := c.Read(buf) 21         if err != nil { 22             log.Printf("conn read %d bytes, error: %s", n, err) 23             if nerr, ok := err.(net.Error); ok && nerr.Timeout() { 24                 continue
25  } 26             return
27  } 28 
29         log.Printf("read %d bytes, content is %s\n", n, string(buf[:n])) 30  } 31 } 32 
33 func main() { 34     l, err := net.Listen("tcp", ":8888") 35     if err != nil { 36         log.Println("listen error:", err) 37         return
38  } 39 
40     for { 41         c, err := l.Accept() 42         if err != nil { 43             log.Println("accept error:", err) 44             break
45  } 46         // start a new goroutine to handle 47         // the new connection.
48         log.Println("accept a new connection") 49  go handleConn(c) 50  } 51 }
server4.go

      在Server端咱們經過Conn的SetReadDeadline方法設置了10微秒的讀超時時間。

      雖然每次都是10微秒超時,但結果不一樣,第一次Read超時,讀出數據長度爲0;第二次讀取全部數據成功,沒有超時。反覆執行了屢次,沒能出現「讀出部分數據且返回超時錯誤」的狀況。 

 2)conn.Write的特色

  • 成功寫

      前面例子着重於Read,client端在Write時並未判斷Write的返回值。所謂「成功寫」指的就是Write調用返回的n與預期要寫入的數據長度相等,且error = nil。這是咱們在調用Write時遇到的最多見的情形,這裏再也不舉例了。

  • 寫阻塞

      TCP鏈接通訊兩端的OS都會爲該鏈接保留數據緩衝,一端調用Write後,實際上數據是寫入到OS的協議棧的數據緩衝的。TCP是全雙工通訊,所以每一個方向都有獨立的數據緩衝。當發送方將對方的接收緩衝區以及自身的發送緩衝區寫滿後,Write就會阻塞。

      客戶端:

 1 //client5.go
 2 package main  3 
 4 import (  5     "log"
 6     "net"
 7     "time"
 8 )  9 
10 func main() { 11     log.Println("begin dial...") 12     conn, err := net.Dial("tcp", ":8888") 13     if err != nil { 14         log.Println("dial error:", err) 15         return
16  } 17  defer conn.Close() 18     log.Println("dial ok") 19 
20     data := make([]byte, 65536) 21     var total int
22     for { 23         n, err := conn.Write(data) 24         if err != nil { 25             total += n 26             log.Printf("write %d bytes, error:%s\n", n, err) 27             break
28  } 29         total += n 30         log.Printf("write %d bytes this time, %d bytes in total\n", n, total) 31  } 32 
33     log.Printf("write %d bytes in total\n", total) 34     time.Sleep(time.Second * 10000) 35 }
client5.go

      服務端:

 1 //server5.go
 2 
 3 package main  4 
 5 import (  6     "log"
 7     "net"
 8     "time"
 9 ) 10 
11 func handleConn(c net.Conn) { 12  defer c.Close() 13     time.Sleep(time.Second * 10) 14     for { 15         // read from the connection
16         time.Sleep(5 * time.Second) 17         var buf = make([]byte, 60000) 18         log.Println("start to read from conn") 19         n, err := c.Read(buf) 20         if err != nil { 21             log.Printf("conn read %d bytes, error: %s", n, err) 22             if nerr, ok := err.(net.Error); ok && nerr.Timeout() { 23                 continue
24  } 25             break
26  } 27 
28         log.Printf("read %d bytes, content is %s\n", n, string(buf[:n])) 29  } 30 } 31 
32 func main() { 33     l, err := net.Listen("tcp", ":8888") 34     if err != nil { 35         log.Println("listen error:", err) 36         return
37  } 38 
39     for { 40         c, err := l.Accept() 41         if err != nil { 42             log.Println("accept error:", err) 43             break
44  } 45         // start a new goroutine to handle 46         // the new connection.
47         log.Println("accept a new connection") 48  go handleConn(c) 49  } 50 }
server5.go        

      執行結果:

[root@centos tcp]# go run client5.go 2019/03/04 23:30:18 begin dial... 2019/03/04 23:30:18 dial ok 2019/03/04 23:30:18 write 65536 bytes this time, 65536 bytes in total 2019/03/04 23:30:18 write 65536 bytes this time, 131072 bytes in total 2019/03/04 23:30:19 write 65536 bytes this time, 196608 bytes in total 2019/03/04 23:30:19 write 65536 bytes this time, 262144 bytes in total 2019/03/04 23:30:19 write 65536 bytes this time, 327680 bytes in total 2019/03/04 23:30:19 write 65536 bytes this time, 393216 bytes in total 2019/03/04 23:30:39 write 65536 bytes this time, 458752 bytes in total 2019/03/04 23:30:39 write 65536 bytes this time, 524288 bytes in total [root@centos tcp]# go run server5.go 2019/03/04 23:30:18 accept a new connection 2019/03/04 23:30:33 start to read from conn 2019/03/04 23:30:33 read 60000 bytes, content is
2019/03/04 23:30:38 start to read from conn 2019/03/04 23:30:38 read 60000 bytes, content is
2019/03/04 23:30:43 start to read from conn 2019/03/04 23:30:43 read 60000 bytes, content is

      Server5在前10s中並不Read數據,所以當client5一直嘗試寫入時,寫到必定量後就會發生阻塞。

      在Centos6.5上測試,這個size大約在 393216 bytes。後續當server5每隔5s進行Read時,OS socket緩衝區騰出了空間,client5就又能夠寫入。

  • 寫入部分數據

      Write操做存在寫入部分數據的狀況,好比上面例子中,當client端輸出日誌停留在「2019/03/04 23:30:39 write 65536 bytes this time, 524288 bytes in total」時,咱們殺掉server5,這時咱們會看到client5輸出如下日誌:

[root@centos tcp]# go run client5.go 2019/03/04 23:30:18 begin dial... 2019/03/04 23:30:18 dial ok 2019/03/04 23:30:18 write 65536 bytes this time, 65536 bytes in total 2019/03/04 23:30:18 write 65536 bytes this time, 131072 bytes in total 2019/03/04 23:30:19 write 65536 bytes this time, 196608 bytes in total 2019/03/04 23:30:19 write 65536 bytes this time, 262144 bytes in total 2019/03/04 23:30:19 write 65536 bytes this time, 327680 bytes in total 2019/03/04 23:30:19 write 65536 bytes this time, 393216 bytes in total 2019/03/04 23:30:39 write 65536 bytes this time, 458752 bytes in total 2019/03/04 23:30:39 write 65536 bytes this time, 524288 bytes in total 2019/03/04 23:30:45 write 49152 bytes, error:write tcp 127.0.0.1:37294->127.0.0.1:8888: write: connection reset by peer 2019/03/04 23:30:45 write 573440 bytes in total

      顯然Write並不是在 524288 bytes 這個地方阻塞的,而是後續又寫入49152 bytes 後發生了阻塞,server端socket關閉後,咱們看到Wrote返回er != nil且n = 49152,程序須要對這部分寫入的49152 字節作特定處理。

  •  寫入超時

      若是非要給Write增長一個期限,那咱們能夠調用SetWriteDeadline方法。咱們copy一份client5.go,造成client6.go,在client6.go的Write以前增長一行timeout設置代碼:      

conn.SetWriteDeadline(time.Now().Add(time.Microsecond * 10))
 1 //client6.go
 2 package main  3 
 4 import (  5     "log"
 6     "net"
 7     "time"
 8 )  9 
10 func main() { 11     log.Println("begin dial...") 12     conn, err := net.Dial("tcp", ":8888") 13     if err != nil { 14         log.Println("dial error:", err) 15         return
16  } 17  defer conn.Close() 18     log.Println("dial ok") 19 
20     data := make([]byte, 65536) 21     var total int
22     for { 23         conn.SetWriteDeadline(time.Now().Add(time.Microsecond * 10)) 24         n, err := conn.Write(data) 25         if err != nil { 26             total += n 27             log.Printf("write %d bytes, error:%s\n", n, err) 28             break
29  } 30         total += n 31         log.Printf("write %d bytes this time, %d bytes in total\n", n, total) 32  } 33 
34     log.Printf("write %d bytes in total\n", total) 35     time.Sleep(time.Second * 10000) 36 }
client6.go

      啓動server6.go,啓動client6.go,咱們能夠看到寫入超時的狀況下,Write的返回結果:

[root@centos tcp]# go run client6.go 2019/03/04 23:46:33 begin dial... 2019/03/04 23:46:33 dial ok 2019/03/04 23:46:33 write 65536 bytes this time, 65536 bytes in total 2019/03/04 23:46:33 write 65536 bytes this time, 131072 bytes in total 2019/03/04 23:46:33 write 49152 bytes, error:write tcp 127.0.0.1:37295->127.0.0.1:8888: i/o timeout 2019/03/04 23:46:33 write 180224 bytes in total

      能夠看到在寫入超時時,依舊存在部分數據寫入的狀況。

      綜上例子,雖然Go給咱們提供了阻塞I/O的便利,但在調用Read和Write時依舊要綜合須要方法返回的n和err的結果,以作出正確處理。net.conn實現了io.Reader和io.Writer接口,所以能夠試用一些wrapper包進行socket讀寫,好比bufio包下面的Writer和Reader、io/ioutil下的函數等。

(5)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 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方法的實現也證明了這一點:

 1 func (fd *netFD) Read(p []byte) (n int, err error) {  2     if err := fd.readLock(); err != nil {  3         return 0, err  4  }  5  defer fd.readUnlock()  6     if err := fd.pd.PrepareRead(); err != nil {  7         return 0, err  8  }  9     for { 10         n, err = syscall.Read(fd.sysfd, p) 11         if err != nil { 12             n = 0
13             if err == syscall.EAGAIN { 14                 if err = fd.pd.WaitRead(); err == nil { 15                     continue
16  } 17  } 18  } 19         err = fd.eofError(n, err) 20         break
21  } 22     if _, ok := err.(syscall.Errno); ok { 23         err = os.NewSyscallError("read", err) 24  } 25     return
26 } 27 
28 func (fd *netFD) Write(p []byte) (nn int, err error) { 29     if err := fd.writeLock(); err != nil { 30         return 0, err 31  } 32  defer fd.writeUnlock() 33     if err := fd.pd.PrepareWrite(); err != nil { 34         return 0, err 35  } 36     for { 37         var n int
38         n, err = syscall.Write(fd.sysfd, p[nn:]) 39         if n > 0 { 40             nn += n 41  } 42         if nn == len(p) { 43             break
44  } 45         if err == syscall.EAGAIN { 46             if err = fd.pd.WaitWrite(); err == nil { 47                 continue
48  } 49  } 50         if err != nil { 51             break
52  } 53         if n == 0 { 54             err = io.ErrUnexpectedEOF 55             break
56  } 57  } 58     if _, ok := err.(syscall.Errno); ok { 59         err = os.NewSyscallError("write", err) 60  } 61     return nn, err 62 }
Read Write

      每次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」。

 (6)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等。

 (7)關閉鏈接

      和前面的方法相比,關閉鏈接算是最簡單的操做了。因爲socket是全雙工的,client和server端在己方已關閉的socket和對方關閉的socket上操做的結果有不一樣。看下面例子:

      客戶端:

 1 package main  2 
 3 import (  4     "log"
 5     "net"
 6     "time"
 7 )  8 
 9 func main() { 10     log.Println("begin dial...") 11     conn, err := net.Dial("tcp", ":8888") 12     if err != nil { 13         log.Println("dial error:", err) 14         return
15  } 16  conn.Close() 17     log.Println("close ok") 18 
19     var buf = make([]byte, 32) 20     n, err := conn.Read(buf) 21     if err != nil { 22         log.Println("read error:", err) 23     } else { 24         log.Printf("read % bytes, content is %s\n", n, string(buf[:n])) 25  } 26 
27     n, err = conn.Write(buf) 28     if err != nil { 29         log.Println("write error:", err) 30     } else { 31         log.Printf("write % bytes, content is %s\n", n, string(buf[:n])) 32  } 33 
34     time.Sleep(time.Second * 1000) 35 }
client.go

      服務端:

 1 //server.go
 2 
 3 package main  4 
 5 import (  6     "log"
 7     "net"
 8 )  9 
10 func handleConn(c net.Conn) { 11  defer c.Close() 12 
13     // read from the connection
14     var buf = make([]byte, 10) 15     log.Println("start to read from conn") 16     n, err := c.Read(buf) 17     if err != nil { 18         log.Println("conn read error:", err) 19     } else { 20         log.Printf("read %d bytes, content is %s\n", n, string(buf[:n])) 21  } 22 
23     n, err = c.Write(buf) 24     if err != nil { 25         log.Println("conn write error:", err) 26     } else { 27         log.Printf("write %d bytes, content is %s\n", n, string(buf[:n])) 28  } 29 } 30 
31 func main() { 32     l, err := net.Listen("tcp", ":8888") 33     if err != nil { 34         log.Println("listen error:", err) 35         return
36  } 37 
38     for { 39         c, err := l.Accept() 40         if err != nil { 41             log.Println("accept error:", err) 42             break
43  } 44         // start a new goroutine to handle 45         // the new connection.
46         log.Println("accept a new connection") 47  go handleConn(c) 48  } 49 }
server.go

      上述例子的執行結果以下:

[root@centos conn_close]# go run client1.go 2019/03/05 00:00:59 begin dial... 2019/03/05 00:00:59 close ok 2019/03/05 00:00:59 read error: read tcp 127.0.0.1:37296->127.0.0.1:8888: use of closed network connection 2019/03/05 00:00:59 write error: write tcp 127.0.0.1:37296->127.0.0.1:8888: use of closed network connection [root@centos conn_close]# go run server1.go 2019/03/05 00:00:59 accept a new connection 2019/03/05 00:00:59 start to read from conn 2019/03/05 00:00:59 conn read error: EOF 2019/03/05 00:00:59 write 10 bytes, content is

      從client的結果來看,在己方已經關閉的socket上再進行read和write操做,會獲得」use of closed network connection」 error;
      從server的執行結果來看,在對方關閉的socket上執行read操做會獲得EOF error,但write操做會成功,由於數據會成功寫入己方的內核socket緩衝區中,即使最終發不到對方socket緩衝區了,由於己方socket並未關閉。所以當發現對方socket關閉後,己方應該正確合理處理本身的socket,再繼續write已經無任何意義了。

(8)發送http請求

Get請求協議的格式以下: 請求首行; // 請求方式 請求路徑 協議和版本,例如:GET /index.html HTTP/1.1
請求頭信息;// 請求頭名稱:請求頭內容,即爲key:value格式,例如:Host:localhost
空行;     // 用來與請求體分隔開
請求體。   // GET沒有請求體,只有POST有請求體。
 例如: GET /books/?sex=man&name=Professional HTTP/1.1 Host: www.wrox.com User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.6) Gecko/20050225 Firefox/1.0.1 Connection: Keep-Alive

   關於Http協議能夠看下博客:http://www.cnblogs.com/yuanchenqi/articles/6000358.html

   Get和Post請求的區別能夠看下博客:https://www.cnblogs.com/logsharing/p/8448446.html

   依照上面Get的請求協議格式,咱們給百度發一個Get請求:

 1 package main  2 
 3 import (  4     "fmt"
 5     "io"
 6     "net"
 7 )  8 func main() {  9 
10     conn, err := net.Dial("tcp", "www.baidu.com:80") 11     if err != nil { 12         fmt.Println("Error dialing", err.Error()) 13         return
14  } 15  defer conn.Close() 16     
17     msg := "GET / HTTP/1.1\r\n"
18     msg += "Host: www.baidu.com\r\n"
19     msg += "Connection: close\r\n"
20     msg += "\r\n\r\n"
21 
22     _, err = io.WriteString(conn, msg) 23     if err != nil { 24         fmt.Println("write string failed, ", err) 25         return
26  } 27     buf := make([]byte, 4096) 28     for { 29         count, err := conn.Read(buf) 30         if err != nil { 31             break
32  } 33         fmt.Println(string(buf[0:count])) 34  } 35 }
Get 訪問百度

(9)小結

      本文比較基礎,但卻很重要,畢竟golang是面向大規模服務後端的,對通訊環節的細節的深刻理解會大有裨益。另外Go的goroutine+阻塞通訊的網絡通訊模型下降了開發者心智負擔,簡化了通訊的複雜性,這點尤其重要。

 注:上面例子出現(root@centos)表示是在Centos6.5上運行,其餘是在Windows上運行,go version go1.8 windows/amd64。

特別注意:

  • 上面內容除一小部分(運行結果及其餘博客連接部分)所有來自 https://tonybai.com/2015/11/17/tcp-programming-in-golang/ 該博主,解釋權歸該博主。
  • 本節用到的例子在該博主github地址:https://github.com/bigwhite/experiments/tree/master/go-tcpsock

2. Redis使用

  (1)Redis簡介

  • redis是個開源的高性能的key-value的內存數據庫,能夠把它當成遠程的數據結構。
  • 支持的value類型很是多,好比string、list(鏈表)、set(集合)、hash表等等。
  • redis性能很是高,單機可以達到15w qps,一般適合作緩存。

  (2)下載並安裝依賴

使用第三方開源的redis庫: github.com/garyburd/redigo/redis go get github.com/garyburd/redigo/redis

   注意:go get 從指定源上面下載或者更新指定的代碼和依賴,並對他們進行編譯和安裝(至關於 clone + install)。

               更多命令使用能夠看:https://www.flysnow.org/2017/03/08/go-in-action-go-tools.html

  (3)操做Redis

  • 鏈接redis
 1 package main  2 
 3 import (  4     "fmt"
 5     "github.com/garyburd/redigo/redis"
 6 )  7 
 8 func main() {  9     fmt.Println("start to connect redis...") 10     c, err := redis.Dial("tcp", "192.168.30.134:6379") 11     if err != nil { 12         fmt.Println("conn redis failed,", err) 13         return
14  } 15 
16  defer c.Close() 17 }
connect redis
  • 字符串 Set 操做
 1 package main  2 
 3 import (  4     "fmt"
 5     "github.com/garyburd/redigo/redis"
 6 )  7 
 8 func main() {  9     c, err := redis.Dial("tcp", "192.168.30.134:6379") 10     if err != nil { 11         fmt.Println("conn redis failed,", err) 12         return
13  } 14 
15  defer c.Close() 16     _, err = c.Do("Set", "abc", 100) 17     if err != nil { 18  fmt.Println(err) 19         return
20  } 21 
22     r, err := redis.Int(c.Do("Get", "abc")) 23     if err != nil { 24         fmt.Println("get abc failed,", err) 25         return
26  } 27 
28  fmt.Println(r) 29 }
String Set
  • Hash表
 1 package main  2 
 3 import (  4     "fmt"
 5     "github.com/garyburd/redigo/redis"
 6 )  7 
 8 func main() {  9     c, err := redis.Dial("tcp", "192.168.30.134:6379") 10     if err != nil { 11         fmt.Println("conn redis failed,", err) 12         return
13  } 14 
15  defer c.Close() 16     _, err = c.Do("HSet", "books", "abc", 200) 17     if err != nil { 18  fmt.Println(err) 19         return
20  } 21 
22     r, err := redis.Int(c.Do("HGet", "books", "abc")) 23     if err != nil { 24         fmt.Println("get abc failed,", err) 25         return
26  } 27 
28  fmt.Println(r) 29 }
Hash
  • 批量Set
 1 package main  2 
 3 import (  4     "fmt"
 5     "github.com/garyburd/redigo/redis"
 6 )  7 
 8 func main() {  9     c, err := redis.Dial("tcp", "192.168.30.134:6379") 10     if err != nil { 11         fmt.Println("conn redis failed,", err) 12         return
13  } 14 
15  defer c.Close() 16     _, err = c.Do("MSet", "abc", 100, "efg", 300) 17     if err != nil { 18  fmt.Println(err) 19         return
20  } 21 
22     r, err := redis.Ints(c.Do("MGet", "abc", "efg")) 23     if err != nil { 24         fmt.Println("get abc failed,", err) 25         return
26  } 27 
28     for _, v := range r { 29  fmt.Println(v) 30  } 31 }
batch set
  • 過時時間
 1 package main  2 
 3 import (  4     "fmt"
 5     "time"
 6     "github.com/garyburd/redigo/redis"
 7 )  8 
 9 func main() { 10     c, err := redis.Dial("tcp", "192.168.30.134:6379") 11     if err != nil { 12         fmt.Println("conn redis failed,", err) 13         return
14  } 15  defer c.Close() 16     
17     _, err = c.Do("Set", "abc", 100) 18     if err != nil { 19  fmt.Println(err) 20         return
21  } 22 
23     r, err := redis.Int(c.Do("Get", "abc")) 24     if err != nil { 25         fmt.Println("get abc failed,", err) 26         return
27  } 28     fmt.Println("abc = ", r) 29 
30     _, err = c.Do("expire", "abc", 5)  //5s後過時
31     if err != nil { 32  fmt.Println(err) 33         return
34  } 35 
36     time.Sleep(5*time.Second) 37 
38     r, err = redis.Int(c.Do("Get", "abc")) 39     if err != nil { 40         fmt.Println("get abc failed,", err) 41         return
42  } 43     fmt.Println("abc = ", r) //get abc failed, redigo: nil returned
44 }
expire
  • 隊列操做
 1 package main  2 
 3 import (  4     "fmt"
 5     "github.com/garyburd/redigo/redis"
 6 )  7 
 8 func main() {  9     c, err := redis.Dial("tcp", "192.168.30.134:6379") 10     if err != nil { 11         fmt.Println("conn redis failed,", err) 12         return
13  } 14 
15  defer c.Close() 16     _, err = c.Do("lpush", "book_list", "abc", "ceg", 300) 17     if err != nil { 18  fmt.Println(err) 19         return
20  } 21 
22     r, err := redis.String(c.Do("lpop", "book_list")) 23     if err != nil { 24         fmt.Println("get abc failed,", err) 25         return
26  } 27 
28  fmt.Println(r) 29 }
push

   上面只列出了redis幾個基本操做,Redis更加詳細操做能夠看個人這篇博客(用Python API): https://www.cnblogs.com/xuejiale/p/10460468.html

  • Redis鏈接池

   先看實現鏈接池的例子:

   在cinfig.go中主要是鏈接池一些參數的設置,在pool.go中實現獲取鏈接池接口,在main.go中是調鏈接池的接口應用。

1 package redisConf 2 
3 var RedisConf = map[string]string{ 4    "name":    "redis", 5    "type":    "tcp", 6    "address": "192.168.30.134:6379", 7    "auth":    "*****",  //若是有密碼,寫成本身的密碼
8 }
conf.go
 1 package redisPool  2 
 3 import (  4    "go_dev/day9/go_redis/redis_poll/redisConf"    //改爲你本身的包目錄
 5    "github.com/garyburd/redigo/redis"
 6    "time"
 7 )  8 
 9 var RedisClient *redis.Pool 10 
11 func init() { 12    // 創建鏈接池
13     RedisClient = &redis.Pool { 14         MaxIdle: 16, 15         MaxActive:   1024, 16         IdleTimeout: 300 * time.Second, 17  Dial: func() (redis.Conn, error) { 18             c, err := redis.Dial(redisConf.RedisConf["type"], redisConf.RedisConf["address"]) 19             if err != nil { 20                 return nil, err 21  } 22             //若是redis設置了密碼,須要下面的驗證 23             // if _, err := c.Do("AUTH", redisConf.RedisConf["auth"]); err != nil { 24             // c.Close() 25             // return nil, err 26             // }
27             return c, nil 28  }, 29         //每次獲取鏈接前作一次check
30  TestOnBorrow: func(c redis.Conn, t time.Time) error { 31             if time.Since(t) < time.Minute { 32                 return nil 33  } 34             _, err := c.Do("PING") 35             return err 36  }, 37  } 38 }
pool.go
 1 package main  2 
 3 import (  4    "go_dev/day9/go_redis/redis_poll/redisPool"  //改爲你本身的包目錄
 5    "fmt"
 6    "github.com/garyburd/redigo/redis"
 7 )  8 
 9 var RedisExpire = 3600 //緩存有效期
10 
11 func main() { 12 
13    // 從池裏獲取鏈接
14    rc := redisPool.RedisClient.Get() 15    // 用完後將鏈接放回鏈接池
16  defer rc.Close() 17 
18    key := "redis.cache"
19    //設置值
20    _, err := rc.Do("Set", key, "1", "EX", RedisExpire) 21    if err != nil { 22  fmt.Println(err) 23       return
24  } 25    //取出值
26    val, err := redis.String(rc.Do("Get", key)) 27    if err != nil { 28  fmt.Println(err) 29  } 30  fmt.Println(val) 31    //刪除
32    _, err = rc.Do("Del", key) 33    if err != nil { 34  fmt.Println(err) 35       return
36  } 37 }
main.go
 1 func initRedis() {  2    // 創建鏈接池
 3     pool := &redis.Pool {  4         MaxIdle: 16,  5         MaxActive:   1024,  6         IdleTimeout: 300 * time.Second,  7  Dial: func() (redis.Conn, error) {  8             return redis.Dial("tcp", "localhost:6379")  9  }, 10  } 11 }
簡寫版 鏈接池

   目錄結構以下:

 

   分析:首先來看Pool這個結構體及結構體中各個變量的含義:

 1 type Pool struct {  2     // Dial is an application supplied function for creating and configuring a  3     // connection.  4     //
 5     // The connection returned from Dial must not be in a special state  6     // (subscribed to pubsub channel, transaction started, ...).
 7  Dial func() (Conn, error)  8 
 9     // TestOnBorrow is an optional application supplied function for checking 10     // the health of an idle connection before the connection is used again by 11     // the application. Argument t is the time that the connection was returned 12     // to the pool. If the function returns an error, then the connection is 13     // closed.
14  TestOnBorrow func(c Conn, t time.Time) error 15 
16     // Maximum number of idle connections in the pool.
17     MaxIdle int
18 
19     // Maximum number of connections allocated by the pool at a given time. 20     // When zero, there is no limit on the number of connections in the pool.
21     MaxActive int
22 
23     // Close connections after remaining idle for this duration. If the value 24     // is zero, then idle connections are not closed. Applications should set 25     // the timeout to a value less than the server's timeout.
26  IdleTimeout time.Duration 27 
28     // If Wait is true and the pool is at the MaxActive limit, then Get() waits 29     // for a connection to be returned to the pool before returning.
30     Wait bool
31 
32     // Close connections older than this duration. If the value is zero, then 33     // the pool does not close connections based on age.
34  MaxConnLifetime time.Duration 35 
36     chInitialized uint32 // set to 1 when field ch is initialized
37 
38     mu     sync.Mutex    // mu protects the following fields
39     closed bool          // set to true when the pool is closed.
40     active int           // the number of open connections in the pool
41     ch     chan struct{} // limits open connections when p.Wait is true
42     idle   idleList      // idle connections
43 }
Pool struct

   主要看這幾個參數:

Dial:是必需要實現的,就是調用普通的的redis.Dial便可。 MaxIdle:最大的空閒鏈接數,表示即便沒有redis鏈接時依然能夠保持N個空閒的鏈接,而不被清除,隨時處於待命狀態。 MaxActive:最大的激活鏈接數,表示同時最多有N個鏈接,也就是併發數。 IdleTimeout:最大的空閒鏈接等待時間,超過此時間後,空閒鏈接將被關閉。 Wait:當鏈接數已滿,是否要阻塞等待獲取鏈接。false表示不等待,直接返回錯誤。 TestOnBorrow:在獲取conn的時候會調用一次這個方法,來保證鏈接可用(其實也不是必定可用,由於test成功之後依然有可能被幹掉),這個方法是可選項,通常這個方法是去調用
一個redis的ping方法,看項目需求了,若是併發很高,想極限提升速度,這個能夠不設置。若是想增長點鏈接可用性,仍是加上比較好。

   Pool中的方法及具體實現能夠看下面的連接:

https://github.com/garyburd/redigo/blob/master/redis/pool.go#L122
https://studygolang.com/articles/9642 (鏈接池代碼分析)
https://blog.csdn.net/xiaohu50/article/details/51606349 (redis.Pool 配置參數調優)重點)GO操做redis更多API能夠看: https://godoc.org/github.com/garyburd/redigo/redis#Ints,結合上面的操做就能夠熟練操做Redis
  • 管道操做

   請求/響應服務能夠實現持續處理新請求,即便客戶端沒有準備好讀取舊響應。這樣客戶端能夠發送多個命令到服務器而無需等待響應,最後在一次讀取多個響應。這就是管道化(pipelining),這個技術在多年就被普遍使用了。距離,不少POP3協議實現已經支持此特性,顯著加速了從服務器下載新郵件的過程。Redis很早就支持管道化,因此不管你使用任何版本,你均可以使用管道化技術
   鏈接支持使用Send(),Flush(),Receive()方法支持管道化操做。

Send(commandName string, args ...interface{}) error Flush() error Receive() (reply interface{}, err error)

   Send向鏈接的輸出緩衝中寫入命令。
   Flush將鏈接的輸出緩衝清空並寫入服務器端。
   Recevie按照FIFO順序依次讀取服務器的響應。

c.Send("SET", "foo", "bar") c.Send("GET", "foo") c.Flush() c.Receive() // reply from SET
v, err = c.Receive() // reply from GET

      上面若是再一次 c.Receive() 則會 hang 住,由於只發送了兩條命令,執行結果也就只有兩條,再去取管道中無輸出,所以會hang住。

 1 package main  2 
 3 import (  4     "fmt"
 5     "github.com/garyburd/redigo/redis"
 6 )  7 
 8 func main() {  9     c, err := redis.Dial("tcp", "192.168.30.134:6379") 10     if err != nil { 11         fmt.Println("conn redis failed, err:", err) 12         return
13  } 14  defer c.Close() 15 
16     c.Send("SET", "foo", "bar") 17     c.Send("GET", "foo") 18 
19  c.Flush() 20 
21     v, err := c.Receive() 22     fmt.Printf("v:%v,err:%v\n", v, err)  // v:OK,err:<nil>
23     v, err = c.Receive() 24     fmt.Printf("v:%s,err:%v\n", v, err)  // v:bar,err:<nil> 25     //fmt.Printf("v:%v,err:%v\n", v, err) //v:[98 97 114],err:<nil> 
26 
27     v, err = c.Receive()    // hang住,一直等待
28     fmt.Printf("v:%v,err:%v\n", v, err) 29 }
hang example

       Do方法結合了Send, Flush and Receive方法的功能。開始Do方法往管道寫入命令而且刷新輸出緩衝。接下來Do方法會接收全部就緒的回覆包括最近發送的命令。若是收到的回覆中有錯誤,則Do就會返回錯誤。若是沒有錯誤,Do會返回最近一次收到的回覆。若是發送的命令是空(""),則Do方法會刷新輸出緩衝收到就緒的回覆而不用發送命令。

       使用發送和Do方法能夠實現一個管道事務:

c.Send("MULTI") c.Send("INCR", "foo") c.Send("INCR", "bar") r, err := c.Do("EXEC") fmt.Println(r) // prints [1, 1]

       一個鏈接支持併發的Receive和併發的Send,Flush,但不支持併發的Do方法。

  • 發佈訂閱

      使用Send, Flush 和 Receive能夠實現發佈訂閱的訂閱者:

c.Send("SUBSCRIBE", "example") c.Flush() for { reply, err := c.Receive() if err != nil { return err } // process pushed message
}

     這 PubSubConn 類型封裝了鏈接(Conn)很方便的實現訂閱者。訂閱,發佈訂閱,不訂閱,發佈不訂閱方法發送而且刷新訂閱管理命令。Receive方法將推送的消息轉換爲特定的類型。

 1 psc := redis.PubSubConn{Conn: c}  2 psc.Subscribe("example")  3 for {  4     switch v := psc.Receive().(type) {  5     case redis.Message:  //單個訂閱subscribe
 6         fmt.Printf("%s: message: %s\n", v.Channel, v.Data)  7     case redis.Subscription:  //模式訂閱psubscribe
 8         fmt.Printf("%s: %s %d\n", v.Channel, v.Kind, v.Count)  9     case error: 10         return v 11  } 12 }
發佈訂閱

圖書管理系統v3:

     使用redis存儲數據完善以前的圖書管理系統?

 參考文獻:

  • https://tonybai.com/2015/11/17/tcp-programming-in-golang/
  • https://godoc.org/github.com/garyburd/redigo/redis#Ints
  • https://blog.csdn.net/guyan0319/article/details/84944059 (Go Redis鏈接池)
  • https://www.jianshu.com/p/2d3db51d5bbe
  • https://studygolang.com/articles/12230 (golang redis鏈接池的使用)
  • https://www.cnblogs.com/suoning/p/7259106.html
相關文章
相關標籤/搜索