Go websocket 聊天室的詳細實現和詳細分析
阿新 • • 發佈:2019-01-02
websocket 聊天室資料結構分析
- 首先要做一個聊天室我們需要把所有的連線資訊都儲存下來
- 所以就需要有一個客戶端
client
的manager
,manager
裡應該儲存所有的client
資訊 - 所以在我們的程式裡定義了
ClientManager
這個結構體 - 用
clients
這個map
結構來儲存所有的連線資訊 - 遍歷
clients
通過使用broadcast
這個channel
把web
端傳送來的訊息分發給所有的客戶端client
- 其次每個成功建立長連線的
client
開一個read
協程和wrtie
read
協程不斷讀取web
端輸入的meaasge
,並把message
傳遞給boradcast
,讓manager
遍歷clients
把message
通過broadcast
channel
,傳遞給各個客戶端client
的send
channel
write
協程不斷的將send
channel
裡的訊息傳送給web
端
流程圖
具體程式碼
- server.go
package main
import (
"encoding/json"
"fmt"
"net/http"
"github.com/gorilla/websocket"
"github.com/satori/go.uuid"
)
//客戶端管理
type ClientManager struct {
//客戶端 map 儲存並管理所有的長連線client,線上的為true,不在的為false
clients map[*Client]bool
//web端傳送來的的message我們用broadcast來接收,並最後分發給所有的client
broadcast chan []byte
//新建立的長連線client
register chan *Client
//新登出的長連線client
unregister chan *Client
}
//客戶端 Client
type Client struct {
//使用者id
id string
//連線的socket
socket *websocket.Conn
//傳送的訊息
send chan []byte
}
//會把Message格式化成json
type Message struct {
//訊息struct
Sender string `json:"sender,omitempty"` //傳送者
Recipient string `json:"recipient,omitempty"` //接收者
Content string `json:"content,omitempty"` //內容
}
//建立客戶端管理者
var manager = ClientManager{
broadcast: make(chan []byte),
register: make(chan *Client),
unregister: make(chan *Client),
clients: make(map[*Client]bool),
}
func (manager *ClientManager) start() {
for {
select {
//如果有新的連線接入,就通過channel把連線傳遞給conn
case conn := <-manager.register:
//把客戶端的連線設定為true
manager.clients[conn] = true
//把返回連線成功的訊息json格式化
jsonMessage, _ := json.Marshal(&Message{Content: "/A new socket has connected."})
//呼叫客戶端的send方法,傳送訊息
manager.send(jsonMessage, conn)
//如果連線斷開了
case conn := <-manager.unregister:
//判斷連線的狀態,如果是true,就關閉send,刪除連線client的值
if _, ok := manager.clients[conn]; ok {
close(conn.send)
delete(manager.clients, conn)
jsonMessage, _ := json.Marshal(&Message{Content: "/A socket has disconnected."})
manager.send(jsonMessage, conn)
}
//廣播
case message := <-manager.broadcast:
//遍歷已經連線的客戶端,把訊息傳送給他們
for conn := range manager.clients {
select {
case conn.send <- message:
default:
close(conn.send)
delete(manager.clients, conn)
}
}
}
}
}
//定義客戶端管理的send方法
func (manager *ClientManager) send(message []byte, ignore *Client) {
for conn := range manager.clients {
//不給遮蔽的連線傳送訊息
if conn != ignore {
conn.send <- message
}
}
}
//定義客戶端結構體的read方法
func (c *Client) read() {
defer func() {
manager.unregister <- c
c.socket.Close()
}()
for {
//讀取訊息
_, message, err := c.socket.ReadMessage()
//如果有錯誤資訊,就登出這個連線然後關閉
if err != nil {
manager.unregister <- c
c.socket.Close()
break
}
//如果沒有錯誤資訊就把資訊放入broadcast
jsonMessage, _ := json.Marshal(&Message{Sender: c.id, Content: string(message)})
manager.broadcast <- jsonMessage
}
}
func (c *Client) write() {
defer func() {
c.socket.Close()
}()
for {
select {
//從send裡讀訊息
case message, ok := <-c.send:
//如果沒有訊息
if !ok {
c.socket.WriteMessage(websocket.CloseMessage, []byte{})
return
}
//有訊息就寫入,傳送給web端
c.socket.WriteMessage(websocket.TextMessage, message)
}
}
}
func main() {
fmt.Println("Starting application...")
//開一個goroutine執行開始程式
go manager.start()
//註冊預設路由為 /ws ,並使用wsHandler這個方法
http.HandleFunc("/ws", wsHandler)
//監聽本地的8011埠
http.ListenAndServe(":8011", nil)
}
func wsHandler(res http.ResponseWriter, req *http.Request) {
//將http協議升級成websocket協議
conn, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}).Upgrade(res, req, nil)
if err != nil {
http.NotFound(res, req)
return
}
//每一次連線都會新開一個client,client.id通過uuid生成保證每次都是不同的
client := &Client{id: uuid.Must(uuid.NewV4()).String(), socket: conn, send: make(chan []byte)}
//註冊一個新的連結
manager.register <- client
//啟動協程收web端傳過來的訊息
go client.read()
//啟動協程把訊息返回給web端
go client.write()
}
- client.go
package main
import (
"flag"
"fmt"
"github.com/gorilla/websocket"
"net/url"
)
//定義連線的服務端的網址
var addr = flag.String("addr", "localhost:8011", "http service address")
func main() {
u := url.URL{Scheme: "ws", Host: *addr, Path: "/ws"}
var dialer *websocket.Dialer
//通過Dialer連線websocket伺服器
conn, _, err := dialer.Dial(u.String(), nil)
if err != nil {
fmt.Println(err)
return
}
//go timeWriter(conn)
//列印接收到的訊息或者錯誤
for {
_, message, err := conn.ReadMessage()
if err != nil {
fmt.Println("read:", err)
return
}
fmt.Printf("received: %s\n", message)
}
}
//func timeWriter(conn *websocket.Conn) {
// for {
// time.Sleep(time.Second * 2)
// conn.WriteMessage(websocket.TextMessage, []byte(time.Now().Format("2006-01-02 15:04:05")))
// }
//}
- chatroom.html
<html>
<head>
<title>Golang Chat</title>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
<script type="text/javascript">
$(function() {
var conn;
var msg = $("#msg");
var log = $("#log");
function appendLog(msg) {
var d = log[0]
var doScroll = d.scrollTop == d.scrollHeight - d.clientHeight;
msg.appendTo(log)
if (doScroll) {
d.scrollTop = d.scrollHeight - d.clientHeight;
}
}
$("#form").submit(function() {
if (!conn) {
return false;
}
if (!msg.val()) {
return false;
}
conn.send(msg.val());
msg.val("");
return false
});
if (window["WebSocket"]) {
conn = new WebSocket("ws://localhost:12345/ws");
conn.onclose = function(evt) {
appendLog($("<div><b>Connection Closed.</b></div>"))
}
conn.onmessage = function(evt) {
appendLog($("<div/>").text(evt.data))
}
} else {
appendLog($("<div><b>WebSockets Not Support.</b></div>"))
}
});
</script>
<style type="text/css">
html {
overflow: hidden;
}
body {
overflow: hidden;
padding: 0;
margin: 0;
width: 100%;
height: 100%;
background: gray;
}
#log {
background: white;
margin: 0;
padding: 0.5em 0.5em 0.5em 0.5em;
position: absolute;
top: 0.5em;
left: 0.5em;
right: 0.5em;
bottom: 3em;
overflow: auto;
}
#form {
padding: 0 0.5em 0 0.5em;
margin: 0;
position: absolute;
bottom: 1em;
left: 0px;
width: 100%;
overflow: hidden;
}
</style>
</head>
<body>
<div id="log"></div>
<form id="form">
<input type="submit" value="傳送" />
<input type="text" id="msg" size="64"/>
</form>
</body>
</html>
- 本文章是對 一蓑煙雨1989 文章的分析 ,如想了解原文章可以點選超連線檢視原文