go語言聊天服務器

服務端程序

       服務端程序中包含4個goroutine,分別是一個主goroutine和廣播(broadcaster),每一個連接裏面又包含一個來連接處理(handleConn)goroutine 和一個客戶寫入(clientwriter)goroutine。廣播器(broadcaster)是用於如何使用 select 的一個規範說明,因爲它需要對三種不同的消息進行響應。主 goroutine 的工作是監聽端口,接受連接客戶端的網絡連接,對每一個連接,它將創建一個新的 handleConn goroutine。

完整的實例代碼

package main

 

import (

        "bufio"

        "fmt"

        "log"

        "net"

)

 

func main() {

        listener, err := net.Listen("tcp", "localhost:8001")

        if err != nil {

                log.Fatal(err)

        }

        go broadcaster()

        for {

                conn, err := listener.Accept()

                if err != nil {

                        log.Print(err)

                        continue

                }

                go hanleConn(conn)

        }

}

 

type client chan<- string //對外發送消息的通道

var (

        entering = make(chan client)

        leaving  = make(chan client)

        messages = make(chan string) //所有連接的客戶端

)

 

func broadcaster() {

        clients := make(map[client]bool)

        for {

                select {

                case msg := <-messages:

                        //把所有接收到的消息廣播給所有客戶端

                        //發送消息通道

                        for cli := range clients {

                                cli <- msg

                        }

                case cli := <-entering:

                        clients[cli] = true

                case cli := <-leaving:

                        delete(clients, cli)

                        close(cli)

                }

        }

}

 

func hanleConn(conn net.Conn) {

        ch := make(chan string)

        go clientWriter(conn, ch)

        who := conn.RemoteAddr().String()

        ch <- "歡迎" + who

        messages <- who + "上線"

        entering <- ch

        input := bufio.NewScanner(conn)

        for input.Scan() {

                messages <- who + ":" + input.Text()

        }

        //注意:忽略input.Err()中可能的錯誤

        leaving <- ch

        messages <- who + "下線"

        conn.Close()

}

func clientWriter(conn net.Conn, ch <-chan string) {

        for msg := range ch {

                fmt.Fprintln(conn, msg) //注意,忽略網絡層面的錯誤

        }

}

代碼中main函數裏寫的代碼非常簡單,其實服務器要做的事情總結寫一下無非就獲得listener對象,然後不停的獲取鏈接上來的conn對象,最後把這些對象丟給處理鏈接函數去進行處理,在使用 handleConn 方法處理 conn 對象的時候,對不同的鏈接都啓一個 goroutine 去併發處理每個 conn 這樣則無需等待。由於要給所有在線的用戶發送消息,而不同用戶的 conn 對象都在不同的 goroutine 裏面,但是Go語言中有 channel 來處理各不同 goroutine 之間的消息傳遞,所以在這裏我們選擇使用 channel 在各不同的 goroutine 中傳遞廣播消息。

下面來介紹一下 broadcaster 廣播器,它使用局部變量 clients 來記錄當前連接的客戶集合,每個客戶唯一被記錄的信息是其對外發送消息通道的 ID,下面是細節:

 

type client chan<- string //對外發送消息的通道

var (

        entering = make(chan client)

        leaving  = make(chan client)

        messages = make(chan string) //所有連接的客戶端

)

 

func broadcaster() {

        clients := make(map[client]bool)

        for {

                select {

                case msg := <-messages:

                        //把所有接收到的消息廣播給所有客戶端

                        //發送消息通道

                        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 是各連接申明的單向併發隊列。

使用一個selsect開啓一個多路複用:
每當有廣播消息從messages發送出來,都會循環clients對裏面的每個channel發消息。

每當有消息從entering裏面發送過來,就生成一個新的key-value,相當於給clients裏面增加一個新的client.

每當有消息從leaving裏面發送過來,就刪掉這個key-value對,並關閉對應的channel。

下面再來看一下每個客戶自己goroutine.

handleConn 函數創建一個對外發送消息的新通道,然後通過 entering 通道通知廣播者新客戶到來,接着它讀取客戶發來的每一行文本,通過全局接收消息通道將每一行發送給廣播者,發送時在每條消息前面加上發送者 ID 作爲前綴。一旦從客戶端讀取完畢消息,handleConn 通過 leaving 通道通知客戶離開,然後關閉連接。

 

func hanleConn(conn net.Conn) {

        ch := make(chan string)

        go clientWriter(conn, ch)

        who := conn.RemoteAddr().String()

        ch <- "歡迎" + who

        messages <- who + "上線"

        entering <- ch

        input := bufio.NewScanner(conn)

        for input.Scan() {

                messages <- who + ":" + input.Text()

        }

        //注意:忽略input.Err()中可能的錯誤

        leaving <- ch

        messages <- who + "下線"

        conn.Close()

}

hanleConn函數會爲每個過來處理的conn都創建一個新的channel,開啓一個新的goroutine去把發送給這個channel的消息寫進conn。

hanleConn函數的執行過程可以簡單總結如下幾個步驟:

1、獲取鏈接過來的ip地址和端口號;

2、把歡迎信息寫進channel返回給客戶端;

3、生成一條廣播消息寫進messages裏面;

4、把這個channel加入到客戶端集合,也就是entering<-ch;

5、監聽客戶端con裏寫的數據,每列掃描到一條就將這條消息發送到廣播channel中;

6、如果關閉客戶端,那麼把隊列離開寫入leaving 交給廣播函數去刪除這個客戶端並關閉客戶端;

7、廣播通知其他客戶端該關閉的客戶端已關閉;

8、最後關閉這個客戶端的連接Conn.Close()。

 

客戶端程序

 

package main

 

import (

        "io"

        "log"

        "net"

        "os"

)

 

func main() {

        conn, err := net.Dial("tcp", "localhost:8001")

        if err != nil {

                log.Fatal(err)

        }

        done := make(chan struct{})

        go func() {

                io.Copy(os.Stdout, conn) //注意:忽略錯誤

                log.Println("done")

                done <- struct{}{} //向Goroutinue發出信號

        }()

        mustCopy(conn, os.Stdin)

        conn.Close()

        <-done //等待後代goroutinue完成

}

func mustCopy(dst io.Writer, src io.Reader) {

        if _, err := io.Copy(dst, src); err != nil {

                log.Fatal(err)

        }

}

 

當有n個客戶session在連接的時候,程序併發運行着2n+2個相互通信的goroutine,它不需要隱式的枷鎖操作。Clients map限制在廣播器這一個goroutine中被訪問,所以不會併發訪問它,唯一被多個goroutine共享的變量是通道以及net.Conn的實例,他們又多是併發安全的。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章