Go和 websocket 实现聊天室

websocekt 的导入

  • WebSocket是一种在单个TCP连接上进行全双工通信的协议
  • WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据
  • 在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输
    需要安装第三方包:
    cmd中:go get -u -v github.com/gorilla/websocket

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 端
    在这里插入图片描述

data.go

package main
//会把Message格式化成json
type Data struct {
	Ip       string   `json:"ip"`
	User     string   `json:"user"`
	From     string   `json:"from"`
	Type     string   `json:"type"`
	Content  string   `json:"content"`
	UserList []string `json:"user_list"`
}

connection.go

package main

import (
	"encoding/json"
	"fmt"
	"net/http"

	"github.com/gorilla/websocket"
)

type connection struct {
	//连接的socket
	ws   *websocket.Conn
	//写入的管道,其他client写信息,会写到这里面来
	sc   chan []byte
	//传输数据(包含各种信息)
	data *Data
}

var wu = &websocket.Upgrader{ReadBufferSize: 512,
	WriteBufferSize: 512, CheckOrigin: func(r *http.Request) bool { return true }}
//监听器调用的函数,每次发送时会调用
func myws(w http.ResponseWriter, r *http.Request) {
	//创建一个webSocekt链接
	ws, err := wu.Upgrade(w, r, nil)
	if err != nil {
		return
	}
	//指定链接的具体参数
	c := &connection{sc: make(chan []byte, 256), ws: ws, data: &Data{}}
	//告诉聊天室,这是一个新的链接
	//聊天室接收到之后,就更新hub.c 结构
	h.r <- c
	//把消息返回给web端
	go c.writer()
	//程收web端传过来的消息
	c.reader()
	defer func() {
		//finaly 兜底,出现错误,则断开该链接
		c.data.Type = "logout"
		user_list = del(user_list, c.data.User)
		c.data.UserList = user_list
		c.data.Content = c.data.User
		data_b, _ := json.Marshal(c.data)
		h.b <- data_b
		h.r <- c
	}()
}

func (c *connection) writer() {
	//从管道中读数据,有消息就写入,发送给web端
	for message := range c.sc {
		c.ws.WriteMessage(websocket.TextMessage, message)
	}
	c.ws.Close()
}

var user_list = []string{}

func (c *connection) reader() {
	for {
		//接受web端发过来的信息,然后根据类型填充不同的datatype
		_, message, err := c.ws.ReadMessage()
		if err != nil {
			h.r <- c
			break
		}
		json.Unmarshal(message, &c.data)
		switch c.data.Type {
		case "login":
			c.data.User = c.data.Content
			c.data.From = c.data.User
			user_list = append(user_list, c.data.User)
			c.data.UserList = user_list
			data_b, _ := json.Marshal(c.data)
			h.b <- data_b
		case "user":
			c.data.Type = "user"
			data_b, _ := json.Marshal(c.data)
			h.b <- data_b
		case "logout":
			c.data.Type = "logout"
			user_list = del(user_list, c.data.User)
			data_b, _ := json.Marshal(c.data)
			h.b <- data_b
			h.r <- c
		default:
			fmt.Print("========default================")
		}
	}
}

//删除指定的用户
func del(slice []string, user string) []string {
	count := len(slice)
	if count == 0 {
		return slice
	}
	if count == 1 && slice[0] == user {
		return []string{}
	}
	var n_slice = []string{}
	for i := range slice {
		if slice[i] == user && i == count {
			//如果最后一个,则返回count之前的切片即可
			return slice[:count]
		} else if slice[i] == user {
			//从中间截断,删除第 i 条信息
			n_slice = append(slice[:i], slice[i+1:]...)
			break
		}
	}
	fmt.Println(n_slice)
	return n_slice
}

hub.go

package main

import "encoding/json"

//全局变了,对hub中的属性进行实例化
var h = hub{
	c: make(map[*connection]bool),
	u: make(chan *connection),
	b: make(chan []byte),
	r: make(chan *connection),
}

type hub struct {
	//聊天室 map 储存并管理所有的长连接client,在线的为true,不在的为false
	c map[*connection]bool
	//web端发送来的的message我们用broadcast来接收,并最后分发给所有的client
	b chan []byte
	//新创建的长连接client
	r chan *connection
	//新注销的长连接client
	u chan *connection
}

func (h *hub) run() {
	for {
		//聊天室,不停监听信息
		//select可以同时监听一个或多个channel,直到其中一个channel ready,就执行相关语句
		select {
			//如果有新的连接接入,就通过channel把连接传递给conn
			case c := <-h.r:
				//把客户端的连接设置为true,表示已经上线
				h.c[c] = true
				//把返回连接成功的消息json格式化
				c.data.Ip = c.ws.RemoteAddr().String()
				c.data.Type = "handshake"
				c.data.UserList = user_list
				data_b, _ := json.Marshal(c.data)
				//将信息传送给链接对应的管道
				c.sc <- data_b
			case c := <-h.u:
				//判断连接的状态,如果是true,就关闭当前链接的通道
				if _, ok := h.c[c]; ok {
					delete(h.c, c)
					close(c.sc)
				}
			case data := <-h.b:
				//接收到客户端发过来的信息,我们要广播给所有client
				//遍历所有在线的client
				for c := range h.c {
					select {
					//将数据传递给当前链接的通道
					case c.sc <- data:
					default:
						delete(h.c, c)
						close(c.sc)
					}
				}
		}
	}
}

server.go

package main

import (
"fmt"
"net/http"

"github.com/gorilla/mux"
)

func main() {
	router := mux.NewRouter()
	//开启聊天室,即开启服务端
	go h.run()
	//router 相当于一个路由器,当为 “/ws”时,调用 myws函数
	router.HandleFunc("/ws", myws)
	if err := http.ListenAndServe("127.0.0.1:8080", router); err != nil {
		fmt.Println("err:", err)
	}
}

local.html

<!DOCTYPE html>
<html>
<head>
    <title></title>
    <meta http-equiv="content-type" content="text/html;charset=utf-8">
    <style>
        p {
            text-align: left;
            padding-left: 20px;
        }
    </style>
</head>
<body>
<div style="width: 800px;height: 600px;margin: 30px auto;text-align: center">
    <h1>www.5lmh.comy演示聊天室</h1>
    <div style="width: 800px;border: 1px solid gray;height: 300px;">
        <div style="width: 200px;height: 300px;float: left;text-align: left;">
            <p><span>当前在线:</span><span id="user_num">0</span></p>
            <div id="user_list" style="overflow: auto;">
            </div>
        </div>
        <div id="msg_list" style="width: 598px;border:  1px solid gray; height: 300px;overflow: scroll;float: left;">
        </div>
    </div>
    <br>
    <textarea id="msg_box" rows="6" cols="50" onkeydown="confirm(event)"></textarea><br>
    <input type="button" value="发送" onclick="send()">
</div>
</body>
</html>
<script type="text/javascript">
    var uname = prompt('请输入用户名', 'user' + uuid(8, 16));
    var ws = new WebSocket("ws://127.0.0.1:8080/ws");
    ws.onopen = function () {
        var data = "系统消息:建立连接成功";
        listMsg(data);
    };
    ws.onmessage = function (e) {
        var msg = JSON.parse(e.data);
        var sender, user_name, name_list, change_type;
        switch (msg.type) {
            case 'system':
                sender = '系统消息: ';
                break;
            case 'user':
                sender = msg.from + ': ';
                break;
            case 'handshake':
                var user_info = {'type': 'login', 'content': uname};
                sendMsg(user_info);
                return;
            case 'login':
            case 'logout':
                user_name = msg.content;
                name_list = msg.user_list;
                change_type = msg.type;
                dealUser(user_name, change_type, name_list);
                return;
        }
        var data = sender + msg.content;
        listMsg(data);
    };
    ws.onerror = function () {
        var data = "系统消息 : 出错了,请退出重试.";
        listMsg(data);
    };
    function confirm(event) {
        var key_num = event.keyCode;
        if (13 == key_num) {
            send();
        } else {
            return false;
        }
    }
    function send() {
        var msg_box = document.getElementById("msg_box");
        var content = msg_box.value;
        var reg = new RegExp("\r\n", "g");
        content = content.replace(reg, "");
        var msg = {'content': content.trim(), 'type': 'user'};
        sendMsg(msg);
        msg_box.value = '';
    }
    function listMsg(data) {
        var msg_list = document.getElementById("msg_list");
        var msg = document.createElement("p");
        msg.innerHTML = data;
        msg_list.appendChild(msg);
        msg_list.scrollTop = msg_list.scrollHeight;
    }
    function dealUser(user_name, type, name_list) {
        var user_list = document.getElementById("user_list");
        var user_num = document.getElementById("user_num");
        while(user_list.hasChildNodes()) {
            user_list.removeChild(user_list.firstChild);
        }
        for (var index in name_list) {
            var user = document.createElement("p");
            user.innerHTML = name_list[index];
            user_list.appendChild(user);
        }
        user_num.innerHTML = name_list.length;
        user_list.scrollTop = user_list.scrollHeight;
        var change = type == 'login' ? '上线' : '下线';
        var data = '系统消息: ' + user_name + ' 已' + change;
        listMsg(data);
    }
    function sendMsg(msg) {
        var data = JSON.stringify(msg);
        ws.send(data);
    }
    function uuid(len, radix) {
        var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
        var uuid = [], i;
        radix = radix || chars.length;
        if (len) {
            for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random() * radix];
        } else {
            var r;
            uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
            uuid[14] = '4';
            for (i = 0; i < 36; i++) {
                if (!uuid[i]) {
                    r = 0 | Math.random() * 16;
                    uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
                }
            }
        }
        return uuid.join('');
    }
</script>

运行与测试

  • 运行指令,在cmd下执行
    go run server.go hub.go data.go connection.go
    
  • 双击html页面
    在这里插入图片描述
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值