前言 刚入门go
语言和beego
框架,通过一个简单聊天室的实现,来趁热练习。
详细代码见github 。
一、WebSocket协议 在实现之前,我们需要解决一个底层问题。
总所周知,HTTP协议
是单向传输协议,只能由客户端主动 向服务端发送信息,反之则不行。而在聊天室中,一个用户发送一条消息,服务器则需要将该条消息广播到聊天室中的所有用户,这想通过HTTP协议实现是不可能的。
除非,让每个用户每隔一段时间便请求一次服务器获取新消息。这种方式称为长轮询 。但其缺点十分明显,非常消耗资源。
为了解决这个问题,WebSocket协议
应运而生。
那什么是WebSocket协议
呢?百度百科
WebSocket协议
与HTTP协议
同属于应用层协议。不同的是,WebSocket
是双向传输协议,弥补了这个缺点,在该协议下,服务端也能主动向客户端发送信息 。同时,一旦连接,客户端会与服务端保持长时间的通讯。
WebSocket协议
的标识符是ws
,如:ws://localhost:8080/chatRoom/WS
二、go语言并发特性 go
语言的一大特性,便是内置的并发功能(goroutine
)。以及,在并发个体之间传递数据的“通道”(chan
)。
具体细节不在此赘述。
三、beego框架 一个开源的轻量级web server
框架,实现了典型的MVC
模型,和先进的api
接口模型(前后端分离模型)。
聊天室的实现,便基于其MVC
模型。
四、实现步骤 1.需求分析 1)数据分析 聊天室中主要物体分为两种:用户和消息。
用户的主要属性为:姓名、客户端与服务端之间的WebSocket
连接指针。
消息则分为三种:用户发消息、有用户加入、有用户离开。若将加入和离开也视为用户发出的消息内容,那消息的主要属性就有:消息类型、消息内容、发消息者。
2)功能分析 前端:
后端:
客户端(即前端js)若要与服务端建立WebSocket
连接,需要调用WebSocket
连接API,详细内容见大神博客 。
服务端(即后端go)实现
2.数据结构 用户:
1 2 3 4 type Client struct { conn *websocket.Conn name string }
消息:
1 2 3 4 5 6 7 type Message struct { EventType byte `json:"type"` Name string `json:"name"` Message string `json:"message"` }
用户组:
1 clients = make (map [Client] bool )
此处使用映射而不是数组,是为了方便判断某个用户是否已经加入或者已经退出了。
用于goroutine
通道:
1 2 3 4 5 join = make (chan Client, 10 ) leave = make (chan Client, 10 ) message = make (chan Message, 10 )
3.功能实现 1)前端WebSocket
连接实现: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var socket = new WebSocket ('ws://' +window .location .host +'/chatRoom/WS?name=' + $('#name' ).text ());socket.onopen = function ( ) { console .log ("webSocket open" ); connected = true ; }; socket.onclose = function ( ) { console .log ("webSocket close" ); connected = false ; };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 socket.onmessage = function (event ) { var data = JSON .parse (event.data ); console .log ("revice:" , data); var name = data.name ; var type = data.type ; var msg = data.message ; var $messageDiv; if (type == 0 ) { var $usernameDiv = $('<span style="margin-right: 15px;font-weight: 700;overflow: hidden;text-align: right;"/>' ) .text (name); if (name == $("#name" ).text ()) { $usernameDiv.css ('color' , nameColor); } else { $usernameDiv.css ('color' , getUsernameColor (name)); } var $messageBodyDiv = $('<span style="color: gray;"/>' ) .text (msg); $messageDiv = $('<li style="list-style-type:none;font-size:25px;"/>' ) .data ('username' , name) .append ($usernameDiv, $messageBodyDiv); } else { var $messageBodyDiv = $('<span style="color:#999999;">' ) .text (msg); $messageDiv = $('<li style="list-style-type:none;font-size:15px;text-align:center;"/>' ) .append ($messageBodyDiv); } $messageArea.append ($messageDiv); $messageArea[0 ].scrollTop = $messageArea[0 ].scrollHeight ; }
1 2 3 4 5 6 7 8 9 10 11 function sendMessage () { var inputMessage = $inputArea.val (); if (inputMessage && connected) { $inputArea.val ('' ); socket.send (inputMessage); console .log ("send message:" + inputMessage); } }
2)后端WebSocket
连接接口 继承beego
框架的Controller
类型:
1 2 3 type ServerController struct { beego.Controller }
编写ServerController
类型中用于WebSocket
连接的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 func (c *ServerController) WS() { name := c.GetString("name" ) if len (name) == 0 { beego.Error("name is NULL" ) c.Redirect("/" , 302 ) return } conn, err := (&websocket.Upgrader{}).Upgrade(c.Ctx.ResponseWriter, c.Ctx.Request, nil ) if _, ok := err.(websocket.HandshakeError); ok { beego.Error("Not a websocket connection" ) http.Error(c.Ctx.ResponseWriter, "Not a websocket handshake" , 400 ) return } else if err != nil { beego.Error("Cannot setup WebSocket connection:" , err) return } var client Client client.name = name client.conn = conn if !clients[client] { join <- client beego.Info("user:" , client.name, "websocket connect success!" ) } defer func () { leave <- client client.conn.Close() }() for { _, msgStr, err := client.conn.ReadMessage() if err != nil { break } beego.Info("WS-----------receive: " +string (msgStr)) var msg Message msg.Name = client.name msg.EventType = 0 msg.Message = string (msgStr) message <- msg } }
3)后端广播功能 将发消息、用户加入、用户退出三种情况都广播给所有用户。后两种情况经过处理,转换为第一种情况。真正发送信息给客户端的,只有第一种情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 func broadcaster () { for { select { case msg := <-message: str := fmt.Sprintf("broadcaster-----------%s send message: %s\n" , msg.Name, msg.Message) beego.Info(str) for client := range clients { data, err := json.Marshal(msg) if err != nil { beego.Error("Fail to marshal message:" , err) return } if client.conn.WriteMessage(websocket.TextMessage, data) != nil { beego.Error("Fail to write message" ) } } case client := <-join: str := fmt.Sprintf("broadcaster-----------%s join in the chat room\n" , client.name) beego.Info(str) clients[client] = true var msg Message msg.Name = client.name msg.EventType = 1 msg.Message = fmt.Sprintf("%s join in, there are %d preson in room" , client.name, len (clients)) message <- msg case client := <-leave: str := fmt.Sprintf("broadcaster-----------%s leave the chat room\n" , client.name) beego.Info(str) if !clients[client] { beego.Info("the client had leaved, client's name:" +client.name) break } delete (clients, client) var msg Message msg.Name = client.name msg.EventType = 2 msg.Message = fmt.Sprintf("%s leave, there are %d preson in room" , client.name, len (clients)) message <- msg } } }
在后端服务启动时,便开启广播功能:
1 2 3 func init () { go broadcaster() }
此处需要利用goroutine
并发模式,使得该函数能独立在额外的一个线程上运作。
五、参考文档