其實從上學開始就一直想寫一個im。 最近深入go,真是學會了太多,感覺人森雖然苦短,但是也不能只用python。很多知識是不用編譯型語言無法了解的。
該來的還是會來,現在會一步一步用go把這個服務器完善起來 先從這個demo開始。
這個demo 我們要求所有連上服務器的用戶都會知道有用戶的離開,有用戶的加入(除了第一個加入的用戶),每個人說話就像聊天室一樣,房間里的所有人都能看到。
由於接收tcp請求,get accept的conn步驟都差不多所以先上main部分的代碼:
func main() { listener, err := net.Listen("tcp", "0.0.0.0:8888") if err != nil { log.Fatal(err) } go broadcaster() for{ conn, err := listener.Accept() if err != nil { fmt.Fprintf(os.Stdout, "you got something wrong %v", err) continue } go handleConn(conn) } }
使用net包里面提供的Listen監聽tcp來自8888端口的數據。並獲得一個listener對象。
發起一個goroutine用於消息廣播使用
然后進入監聽循環,使用listener對象提供的Accept方法來獲取連接。
每獲得一個連接就重新啟一個goroutine去handle這個鏈接。
main里面寫的代碼非常簡單,其實服務器要做的事情總結一下無非就是獲得listener對象,然后不停的獲取獲取鏈接上來的conn對象,然后把這些對象丟給處理鏈接函數去進行處理。在使用handleConn方法處理conn對象的時候,我們同樣對不同的鏈接都啟一個goroutine去並發處理每個conn這樣則無需等待。
用於要給在線的所有用戶發送消息,而不同的用戶的conn對象都在不同的goroutine里面,我們很容易想到使用隊列這種東西來做消息的傳遞,但是golang里面有channel來處理各不同goroutine之間的消息傳遞,所以在這個demo我選擇使用channel在各不同的goroutine中傳遞廣播消息。
先申明一些要用到的channel
type client chan<- string // send only channel var ( entering = make(chan client) leaving = make(chan client) messages = make(chan string) )
這里要注意一點的是,重新定義了一個client類型,他是一個單向chennel,只能往里面寫消息。
下面申請的entering和leaving都是client類型的channel。
什么意思呢?
就是說下面的entering和leaving都是裝channel的channel。這里有點繞要注意,裝channel的channel在<-的時候,會直接將channel對象裝進去。
這里拓展開說說這個問題,以免下面的代碼難以理解,來看一個例子:
package main import ( "fmt" "time" ) type client chan string var entering = make(chan client) func main() { ch := make(chan string) go func() {ch <- "那你很棒棒哦😯 "}() go func() {entering <- ch}() o := <-entering time.Sleep(2 * time.Second) fmt.Println(<-o) }
這里我們創建了一個client類型,他是一個string類型的channel。
同時申明一個entering,他是一個client類型的channel。這里也可以寫成 make(chan chan string) 但是寫成client更方便清晰有木有。
在執行 entering <- ch的時候,並不是把ch里面裝的string內容吐出去了,而是把自己裝進了entering。
后面寫的都是在驗證這一行為就不繼續贅述了。
繼續回來說broadcaster函數:
func broadcaster() { clients := make(map[client]bool) //all connected clients for { select { case msg := <- messages: // Broadcast incoming message to all // clients' outgoing message channels. for cli := range clients{ cli <- msg } case cli := <- entering: clients[cli] = true case cli := <- leaving: delete(clients, cli) close(cli) } } }
我們在main里面使用goroutine開啟了一個broadcaster函數來負責廣播所有用戶發送的消息。
這里使用一個字典來保存用戶clients,字典的key是各連接申明的單向發隊列。
使用一個select開啟一個多路復用:
每當有廣播消息從messages發送進來,都會循環cliens對里面的每個channel發消息。
每當有消息從entering里面發送過來,就生成一個新的key-value。相當於給clients里面增加一個新的client。
每當有消息從leaving里面發送過來,就刪掉這個key-value對,並關閉對應的channel。
最后我們來看handleConn函數里的邏輯:
func handleConn(conn net.Conn) { ch := make(chan string) go clientWriter(conn, ch) who := conn.RemoteAddr().String() ch <- "You are " + who messages <- who + " has joined us" // 這里不是把數據吐出去。。而是吐出去了本身一個channel這里比較難理解。 entering <- ch input := bufio.NewScanner(conn) for input.Scan() { messages <- who + ": " + input.Text() } leaving <- ch messages <- who + " has left " conn.Close() }
為每個過來處理的conn都創建一個新的channel,開啟一個新的goroutine去把發送給這個channel的消息寫進conn。
獲取連接過來的ip地址和端口號。
先把歡迎信息寫進channel返回給客戶端。
然后生成一條廣播消息寫進messages里。
然后把這個channel加入到客戶端集合 也就是 entering <- ch
然后開始監聽客戶端往conn里寫的數據,每掃描到一條就將這條消息發送到廣播channel中
如果客戶端關閉了標准輸入,那么把隊列離開寫入leaving交給廣播函數去刪除這個客戶端並關閉這個客戶端。
廣播這個人的離開給所有人。
最后關閉這個客戶端的連接Conn.Close()。
最后上clientWriter的代碼:
func clientWriter(conn net.Conn, ch <-chan string) { for msg := range ch { fmt.Fprintln(conn, msg) // NOTE: ignoring network errors } }
沒什么好說的,就是把每個發送過來的消息都寫入到conn中,沒有消息發過來的時候就阻塞。
其實看似簡單餓服務器做了一些細節上的處理,因為golang中字典並不是並發安全的,所以只有一個gonroutine單獨干這件事情,保證了其並發情況也安全。
關於並發安全這個話題,可以寫n篇文章來闡述其細節也不為過,以后可能會有更多機會介紹到。
這么看其實邏輯已經非常清楚了,后續我還會往這個服務器上加更多的功能,包括讓客戶端寫入自己的名字來替代現在用ip地址標記遠端連接的情況。
Reference:
https://github.com/gopl-zh/gopl-zh.github.com The Go Programming Language