Golang的聊天服務器實踐(群聊,廣播)(一)


其實從上學開始就一直想寫一個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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM