HTML5 WebSocket
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。
现在,很多网站为了实现推送技术,所用的技术都是 Ajax 轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。
HTML5 定义的 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯
浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。
当你获取 Web Socket 连接后,你可以通过 send() 方法来向服务器发送数据,并通过 onmessage 事件来接收服务器返回的数据。
以下 API 用于创建 WebSocket 对象。
介绍
首先要使用WebSocket那么肯定需要服务器端有一个连接的地方,那么用golang来做就是将普通的一个接口,通过声明升级,来将这个接口升级为ws协议而不是http协议。
在golang中使用WebSocket有很多可以使用的包,这里使用github.com/gorilla/websocket这个包,我们可以去这个地址看一下,有详细的项目介绍,以及使用方法。
安装
在项目中执行go get github.com/gorilla/websocket 将这个依赖包拉到我们的项目中
实现
// Manager 所有 websocket 信息type Manager struct {IdClientMap map[int32]*Client // 用户连接数组Lock sync.RWMutex // 锁MonitoringRealData, Register, UnRegister chan *Client // 监控实时数据,注册连接,删除连接Message chan *MessageData // 定义一个消息通道GroupMessage chan *GroupMessageData // 消息组通道BroadCastMessage chan *BroadCastMessageData // 广播消息incr int32 // 自增数}
首先定义一个结构体,来存储记录我们的websocket中的内容,每个用户请求接口连接WebSocket都是一个单独的进程,那么如果想要用户之间可以进行交互那么就需要将连接都存储起来,这里就
用一个IdClientMap来存储每个用户的连接,每次用户连接进来 incr都是自增+1 这个就代表当前用户的一个连接ID,用连接ID存储当前连接到数组中,就可以使用IdClientMap[1].client就可以做交互了。
定义了之后需要将这个结构体初始化,那么定义一个变量来初始化结构体中的各个属性。
var (// WebsocketManager 初始化 wsManager 管理器WebsocketManager = Manager{IdClientMap: make(map[int32]*Client),Register: make(chan *Client, 128),UnRegister: make(chan *Client, 128),MonitoringRealData: make(chan *Client, 128),Message: make(chan *MessageData, 128),GroupMessage: make(chan *GroupMessageData, 128),BroadCastMessage: make(chan *BroadCastMessageData, 128),incr: 1,}SystemId int32 = 1)// Client 单个 websocket 信息type Client struct {Id int32GroupList []int32Socket *websocket.ConnMessage chan []byte}// messageData 单个发送数据信息type MessageData struct {Id int32Group intMessage []byte}// groupMessageData 组广播数据信息type GroupMessageData struct {Group int32Message []byte}// 广播发送数据信息type BroadCastMessageData struct {Message []byte}// 读取消息type ReadMessageRequest struct {Type int `json:"type"`Message string `json:"message"`AcceptId int32 `json:"accept_id"`}// 消息返回type MessageResponse struct {Status int `json:"status"`Type int `json:"type"`Message string `json:"message"`FromId int32 `json:"from_id;"`}
这里将各个属性都make初始化,再定义好所需的对应结构体,读取消息,存储用户连接发送消息,广播消息,群组消息。这里定义完之后,创建一个接口来访问。
// webSocket连接func (manager *Manager) WsClient(ctx *gin.Context) {upGrader := websocket.Upgrader{// cross origin domainCheckOrigin: func(r *http.Request) bool {return true},// 处理 Sec-WebSocket-Protocol HeaderSubprotocols: []string{ctx.GetHeader("Sec-WebSocket-Protocol")},}conn, err := upGrader.Upgrade(ctx.Writer, ctx.Request, nil)if err != nil {log.Printf("websocket connect error: %s", ctx.Param("channel"))return}client := &Client{Id: atomic.AddInt32(&manager.incr, 1),Socket: conn,Message: make(chan []byte, 1024),}go client.Read(manager)go client.Write()manager.RegisterClient(client)}
这里创建了一个Manage结构体下的成员方法WsClient ,在方法中调用拉取的websocket包的方法websocket.Upgrader 定义升级http到ws的参数,再调用upGrader.Upgrade(ctx.Writer, ctx.Request, nil)将当前接口升级为WebSocket接口。再初始化Client结构体,设置当前用户的连接,以及唯一的ID,再初始化一个消息的通道,
下面使用了go client.Read(manager) 这里是开启了golang的一个goroutine协程,这个是读取WebSocket消息,另外还有一个go client.Write() 这里是将消息传输到客户端,然后调用manager.RegisterClient(client) 注册一个连接到组里
// 读取websocket发送来的数据func (c *Client) Read(manager *Manager) {defer func() {log.Printf("close:[%s],date:%s", c.Id, time.Now())WebsocketManager.UnRegister <- c}()for {messageType, message, err := c.Socket.ReadMessage()if err != nil || messageType == websocket.CloseMessage {break}var ResponseData []byteReadMessage := ReadMessageRequest{}if err := json.Unmarshal(message, &ReadMessage); err != nil {ResponseData, _ = json.Marshal(MessageResponse{Status: 201, Message: "消息发送失败", Type: 1, FromId: SystemId})c.Message <- ResponseData} else {acceptClient, ok := manager.IdClientMap[ReadMessage.AcceptId]fmt.Printf("ok:%s,acceptId:%s", ok, ReadMessage.AcceptId)if !ok {ResponseData, _ = json.Marshal(MessageResponse{Status: 201, Message: "消息发送失败,对方已离线", Type: 1, FromId: SystemId})c.Message <- ResponseData} else {ResponseData, _ = json.Marshal(MessageResponse{Status: 200, Message: ReadMessage.Message, Type: 1, FromId: c.Id})acceptClient.Message <- ResponseData}}fmt.Printf("[clientId:]%v,[message:]%s\n", c.Id, message)}}// 写信息,从 channel 变量 Send 中读取数据写入 websocket 连接func (c *Client) Write() {defer func() {log.Printf("clientWrite [%s] disconnect", c.Id)if err := c.Socket.Close(); err != nil {log.Printf("clientWrite [%s] disconnect err: %s", c.Id, err)}}()for {select {case message, ok := <-c.Message:if !ok {_ = c.Socket.WriteMessage(websocket.CloseMessage, []byte{})return}err := c.Socket.WriteMessage(websocket.TextMessage, message)if err != nil {log.Printf("client [%s] writemessage err: %s", c.Id, err)}}}}// 注册用户进程func (manager *Manager) RegisterClient(c *Client) {manager.Register <- c}// 注销用户进程func (manager *Manager) UnRegisterClient(c *Client) {manager.UnRegister <- c}// 监听用户进程注册,用户进程注销func (manager *Manager) Start() {for {select {case client := <-manager.Register:log.Printf("client [%s] connect", client.Id)/* log.Printf("register client [%s] to group [%s]", client.Id, client.Group)*/manager.Lock.RLock()if _, ok := manager.IdClientMap[client.Id]; ok {manager.IdClientMap[client.Id].Socket.Close()}manager.IdClientMap[client.Id] = clientResponseData, _ := json.Marshal(MessageResponse{Status: 200, Message: fmt.Sprintf("连接成功,您的会话ID为[%v]", client.Id), Type: 1, FromId: SystemId})client.Message <- ResponseDatamanager.Lock.RUnlock()// 注销case client := <-manager.UnRegister:manager.Lock.Lock()delete(manager.IdClientMap, client.Id)manager.Lock.Unlock()close(client.Message)}}}
这里一共三个方法,Read,Write,Start 分别是读,写,启动。
通过c.Socket.ReadMessage() 分别返回3个参数,消息内容,消息类型,错误,这里我们定义全部使用json来传输消息,那么消息类型就是文本类型,判断一下err是否有错误,如果没有错误那么将收到的消息解析json,就可以拿到对应传输过来的参数,处理过后通过c.Message <- ResponseData 将消息传输到通道中。在Write中开启了一个循环,通过select case来接受一个channel通道的数据,然后通过c.Socket.WriteMessage(websocket.TextMessage, message) 来将数据传输给客户端。
调用

首先将WebSocket启动。然后将我们实现的接口WsClient注册一个路由/user/connect 将服务启动,那么就可以通过WebSocket连接到服务器
可以通过http://www.websocket-test.com/ 这个地址是一个WebSocket的测试网站。我们可以将地址粘贴进去连接进行一个测试。

这里填入地址ws://127.0.0.1:8088/user/connect就是我们实现的WebSocket接口,这里的协议是必须ws://,否则连接就会不成功,下面就可以发送消息到服务器。

这里发送消息{"type":1,"message":"xiaoxi ","accept_id":2} 定义类型是文本,消息内容,接受人ID,这里发送过去之后返回消息发送失败,对方已离线,那是因为id为2的已经不在线 断开连接了,那么重新打开一个网页 在来连接
这里新开浏览器连接的ID为4,那么我们在这边发送消息给连接ID为3的用户

这边发送之后,打开3号的窗口
可以看到这里多了一条消息,状态200 成功,消息内容,以及发送人的ID,这样一个简单的聊天WebSocket就完成了。
