Go語言8-socket和redis
socket 程式設計
在Go裡為我們提供了net包。
下面這篇貌似是官方文件的翻譯:
ofollow,noindex" target="_blank">https://blog.csdn.net/chenbaoke/article/details/42782571上面的轉載,上面的頁面在IE下瀏覽貌似有點問題:
3600" rel="nofollow,noindex" target="_blank">https://studygolang.com/articles/3600Package net provides a portable interface for network I/O, including TCP/IP, UDP, domain name resolution, and Unix domain sockets.
net包對於網路I/O提供了行動式介面,包括TCP/IP,UDP,域名解析以及Unix Socket。
Although the package provides access to low-level networking primitives, most clients will need only the basic interface provided by the Dial, Listen, and Accept functions and the associated Conn and Listener interfaces. The crypto/tls package uses the same interfaces and similar Dial and Listen functions.
儘管net包提供了大量訪問底層的介面,但是大多數情況下,客戶端僅僅只需要最基本的介面,例如Dial,LIsten,Accepte以及分配的conn連線和listener介面。 crypto/tls包使用相同的介面以及類似的Dial和Listen函式。
服務端
服務端的處理流程:
- 監聽埠
- 接收客戶端的連線
- 建立goroutine,處理連線
服務端程式碼:
package main import ( "fmt" "net" ) func main() { fmt.Println("準備開啟Server...") listen, err := net.Listen("tcp", "0.0.0.0:8080") if err != nil { fmt.Println("監聽埠ERROR:", err) return } for { conn, err := listen.Accept() if err != nil { fmt.Println("接收連線ERROR:", err) } go process(conn) } } func process(conn net.Conn) { defer conn.Close() for { buf := make([]byte, 512) n, err := conn.Read(buf) fmt.Println(n)// 這個應該是讀取到的資料的長度 if err != nil { fmt.Println("讀取資料ERROR:", err) return } fmt.Println("READ:", string(buf)) } }
測試,暫時還沒有客戶端,可以先用windows的telnet工具來測試一下:
>telnet 127.0.0.1 8080
進入telnet後按任意鍵盤,server端會做出反應,但是效果不是很友好。
修改一下服務端的process函式如下:
func process(conn net.Conn) { defer conn.Close() for { buf := make([]byte, 10)// 這次一次只收10個 _, err := conn.Read(buf)// 接收的長度就不看了 if err != nil { fmt.Println("讀取資料ERROR:", err) return } fmt.Printf("%v\n", buf)// 檢視buf的字元型別 } }
conn.Read用於讀取收到的資料,把資料存到變數buf裡。buf切片裡的型別應該是字元型別預設就是0,從輸出裡看到其他後面都是0。
conn.Read 返回的第一個引數是接收的長度,那麼可以對buf進行切片,只把收到的資料打印出來:
package main import ( "fmt" "net" ) func main() { fmt.Println("準備開啟Server...") listen, err := net.Listen("tcp", "0.0.0.0:8080") if err != nil { fmt.Println("監聽埠ERROR:", err) return } for { conn, err := listen.Accept() if err != nil { fmt.Println("接收連線ERROR:", err) } go process(conn) } } func process(conn net.Conn) { defer conn.Close() for { buf := make([]byte, 512) n, err := conn.Read(buf) if err != nil { fmt.Println("讀取資料ERROR:", err) return } fmt.Printf(string(buf[0:n]))// 接收了n個字元,就只打印前n個 } }
上面的程式碼,在telnet連線後,鍵盤輸入任何內容,都會在server端打印出來。這裡也能傳中文,但是接收到的的是亂碼,這個主要是telnet的問題(有編碼的問題,一箇中文字元佔2個長度,如果是utf-8是長度3。而且會把中文的2段編碼拆開發出去。不深究了。) ,後面用上客戶端就好了。
客戶端
客戶端的處理流程:
- 建立與服務端的連線
- 進行資料收發
- 關閉連線
客戶端程式碼:
package main import ( "bufio" "fmt" "net" "os" "strings" ) func main() { conn, err := net.Dial("tcp", "127.0.0.1:8080") if err != nil { fmt.Println("建立連線ERROR:", err) return } fmt.Println("建立連線成功") defer conn.Close()// 這裡一定記得要關閉 inputReader := bufio.NewReader(os.Stdin) for { input, err := inputReader.ReadString('\n') input = strings.TrimSpace(input) if err != nil { fmt.Println("終端輸入ERROR:", err) } if input == "Q" { fmt.Println("退出...") return } _, err = conn.Write([]byte(input))// 第一個引數是傳送的字元數 if err != nil { fmt.Println("傳送資料ERROR:", err) return } } }
傳送http請求
這裡需要一點基礎,你得知道Web服務的本質。
這裡只用HTTP/1.1來做個示例,預設:Connection:keep-alive,示例裡設定為:Connection:close。接收的資料的內容會有區別,Connection:close收到的資料會比較簡單(Connection:keep-alive應該是比較新的標準,預設是返回這樣的格式 )。
傳送http請求的程式碼:
package main import ( "fmt" "io" "os" "net" ) func main() { conn, err := net.Dial("tcp", "edu.51cto.com:80") if err != nil { fmt.Println("建立連線ERROR:", err) return } defer conn.Close() // 設定請求頭 msg := "GET / HTTP/1.1\r\n" msg += "Host:edu.51cto.com\r\n" msg += "Connection:close\r\n" msg += "\r\n"// 請求頭結束 _, err = io.WriteString(conn, msg)// 傳送請求 if err != nil { fmt.Println("傳送資料ERROR:", err) return } buf := make([]byte, 1024) fmt.Println("接收資料...") for { n, err := conn.Read(buf) fmt.Println(string(buf[0:n])) if err != nil { if err == io.EOF{ fmt.Println("接收完畢...") break } else { fmt.Println("接收資料ERROR:", err) os.Exit(0) } } } }
Redis
本篇主要是講go如何使用Redis,就是一些簡單的程式碼示例。關於Redis的基礎知識可以看下面這篇的第一章節。
Redis安裝和基礎知識:http://blog.51cto.com/steed/2057706
Redis是一個開源的高效能的key-value的記憶體資料庫,可以把它當成遠端的資料結構。
支援的value型別很多,比如:string、list(連結串列)、set(集合)、hash表等
Redis的效能非常高,單機能夠達到15w qps,通常用來做快取。
使用第三方開源的redis庫:https://github.com/gomodule/redigo
安裝第三方庫:
go get github.com/gomodule/redigo/redis
連線 Redis
package main import ( "fmt" "github.com/gomodule/redigo/redis" ) func main() { conn, err := redis.Dial("tcp", "192.168.3.108:6379") if err != nil { fmt.Println("連線Redis ERROR:", err) return } fmt.Println("連線Redis 成功:", conn) defer conn.Close() }
Set 和 Get
上面的連線沒問題的,接下來就可以通過Redis存(Set)取(Get)資料了。這裡還有個坑,
正常啟動Redis的命令:
$ redis-server
redis預設是以保護模式啟動的,只能本機連。本機以外可能連不上,或者只能連不能改。就不要那麼麻煩再去整配置檔案了,這裡以學習測試為主,推薦這麼啟動:
$ redis-server --protected-mode no
現在Redis可以用了,使用Set和Get進行存取的程式碼:
package main import ( "fmt" "github.com/gomodule/redigo/redis" ) func main() { conn, err := redis.Dial("tcp", "192.168.3.108:6379") if err != nil { fmt.Println("連線Redis ERROR:", err) return } fmt.Println("連線Redis 成功:", conn) defer conn.Close() resSet, err := conn.Do("Set", "age", 18)// 這裡存字串或數值到Redis裡都是一樣的 if err != nil { fmt.Println("Set Redis ERROR:", err) return } fmt.Println("Redis Set:", resSet) resGet, err := redis.Int(conn.Do("Get", "age")) if err != nil { fmt.Println("Get Redis ERROR:", err) return } fmt.Println("Redis Get:", resGet) } /* 執行結果 PS H:\Go\src\go_dev\day9\redis\connect> go run main.go 連線Redis 成功: &{{0 0} 0 <nil> 0xc042004030 0 0xc04203a120 0 0xc042036180 [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]} Redis Set: OK Redis Get: 18 PS H:\Go\src\go_dev\day9\redis\connect> */
批量Set
批量Set是redis原生就支援的功能,這裡就是呼叫新的方法 MSet 和 MGet ,進行批量的操作:
package main import ( "fmt" "github.com/gomodule/redigo/redis" ) func main() { conn, err := redis.Dial("tcp", "192.168.3.108:6379") if err != nil { fmt.Println("連線Redis ERROR:", err) return } fmt.Println("連線Redis 成功:", conn) defer conn.Close() resSet, err := conn.Do("MSet", "k1", "v1", "k2", "v2", "k3", "v3") if err != nil { fmt.Println("Set Redis ERROR:", err) return } fmt.Println("Redis MSet:", resSet) resGet, err := redis.Strings(conn.Do("MGet", "k1", "k2", "k3")) if err != nil { fmt.Println("Get Redis ERROR:", err) return } fmt.Println("Redis MGet:", resGet) for _, v := range resGet { fmt.Println(v) } }
Hash 操作
package main import ( "fmt" "github.com/gomodule/redigo/redis" ) func main() { conn, err := redis.Dial("tcp", "192.168.3.108:6379") if err != nil { fmt.Println("連線Redis ERROR:", err) return } fmt.Println("連線Redis 成功:", conn) defer conn.Close() resSet, err := conn.Do("HSet", "student", "age", 18) if err != nil { fmt.Println("Set Redis ERROR:", err) return } fmt.Println("Redis Set:", resSet) resGet, err := redis.Int(conn.Do("HGet", "student", "age")) if err != nil { fmt.Println("Get Redis ERROR:", err) return } fmt.Println("Redis Get:", resGet) }
設定超時時間
超時時間是對key進行設定的。預設是沒有超時時間的,永不過期。這裡的示例是在設定好key-value之後再對key設定超時時間:
package main import ( "fmt" "github.com/gomodule/redigo/redis" ) func main() { conn, err := redis.Dial("tcp", "192.168.3.108:6379") if err != nil { fmt.Println("連線Redis ERROR:", err) return } fmt.Println("連線Redis 成功:", conn) defer conn.Close() // 假設之前已經設定了name這個key值,設定後name會在10秒後過期 resSet, err := conn.Do("expire", "name", 10) if err != nil { fmt.Println("Set Redis ERROR:", err) return } fmt.Println("Redis Set:", resSet) if int(resSet.(int64)) == 1 { fmt.Println("設定超時時間成功") } }
在Set的時候,就可以加一個引數,其中就有超時時間。這裡的例子是對已有的key重新設定一個超時時間。如果key存在,則返回1,如果key不存在,Redis會返回0。
佇列操作
package main import ( "fmt" "github.com/gomodule/redigo/redis" ) func main() { conn, err := redis.Dial("tcp", "192.168.3.108:6379") if err != nil { fmt.Println("連線Redis ERROR:", err) return } fmt.Println("連線Redis 成功:", conn) defer conn.Close() resPush, err := conn.Do("lpush", "names", "Clark", "Lois", "Kara") if err != nil { fmt.Println("Push Redis ERROR:", err) return } fmt.Println("Redis MSet:", resPush) for { resPop, err := redis.String(conn.Do("lpop", "names")) if err != nil { if err == redis.ErrNil { fmt.Println("佇列已經取完:", err) break } else { fmt.Println("Pop Redis ERROR:", err) return } } fmt.Println("Redis lpop:", resPop) } }
使用連線池
使用場景:
對於一些大物件,或者初始化過程較長的可複用的物件,我們如果每次都new物件出來,那麼意味著會耗費大量的時間。我們可以將這些物件快取起來,當介面呼叫完畢後,不是銷燬物件,當下次使用的時候,直接從物件池中拿出來即可。
使用連線池操作Redis的步驟:
- 首先建立連線池,可以在init函式裡建立。
- 每次要連線的時候,就從池裡獲取一個連線。記得defer關閉連線。
- 獲取到連線後的操作方法就一樣了
程式碼示例:
package main import ( "fmt" "github.com/gomodule/redigo/redis" ) var pool redis.Pool func init() { pool = redis.Pool { MaxIdle: 8, MaxActive: 0, IdleTimeout: 300, Dial: func() (redis.Conn, error) { return redis.Dial("tcp", "192.168.3.108:6379") }, } } func main() { conn := pool.Get()// 從池裡獲取一個連線使用 defer conn.Close()// 還是記得要關閉連線 resCon, err := conn.Do("Set", "school", "SHHS") if err != nil { fmt.Println("Redis Set ERROR:", err) return } fmt.Println(resCon)// 返回值是OK沒什麼用 resGet, err := redis.String(conn.Do("Get", "school")) if err != nil { fmt.Println("Redis Get ERROR:", err) return } fmt.Println(resGet) pool.Close()// 關閉pool }