服務端程序
服務端程序中包含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的實例,他們又多是併發安全的。