主要內容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接口的各類使用、行爲模式、異常處理講解的十分細緻。
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 }
(重點)用戶層眼中看到的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 }
服務端:
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 }
阻塞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:
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的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上測試,下同。
還有一種場景就是對方服務器很忙,瞬間有大量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 }
客戶端代碼:
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初始時成功地一次性創建了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創建一個連接。
若是網絡延遲較大,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 }
執行結果以下,須要模擬一個網絡延遲大的環境:
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 }
服務器端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 }
從Conn接口中有Read,Write,Close等方法。
1)conn.Read的特色
鏈接創建後,若是對方未發送數據到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 }
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 }
若是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 }
服務端:
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 }
經過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中有數據,且長度大於等於一次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)。
若是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 }
服務端:
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 }
執行結果:
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 }
服務端:
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 }
在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 }
服務端:
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 }
執行結果:
[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 }
啓動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 }
每次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 }
服務端:
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 }
上述例子的執行結果以下:
[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 }
(9)小結
本文比較基礎,但卻很重要,畢竟golang是面向大規模服務後端的,對通訊環節的細節的深刻理解會大有裨益。另外Go的goroutine+阻塞通訊的網絡通訊模型下降了開發者心智負擔,簡化了通訊的複雜性,這點尤其重要。
注:上面例子出現(root@centos)表示是在Centos6.5上運行,其餘是在Windows上運行,go version go1.8 windows/amd64。
特別注意:
2. Redis使用
(1)Redis簡介
(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
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 }
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 }
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 }
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 }
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 }
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 }
上面只列出了redis幾個基本操做,Redis更加詳細操做能夠看個人這篇博客(用Python API): https://www.cnblogs.com/xuejiale/p/10460468.html
先看實現鏈接池的例子:
在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 }
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 }
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 }
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 }
主要看這幾個參數:
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 }
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存儲數據完善以前的圖書管理系統?
參考文獻: