GOlang 网络编程 websocket入门(心得分享)

8 阅读12分钟

TCP UDP 官网教程

package main

import (
	"bufio"
	"fmt"
	"net"
	"os"
	"strings"
)

func main() {
	conn, err := net.Dial("tcp", "127.0.0.1:20000") //建立连接
	if err != nil {
		fmt.Println("err :", err)
		return
	}
	defer conn.Close()                       // 关闭连接
	inputReader := bufio.NewReader(os.Stdin) //`标准输入`
	for {
		input, _ := inputReader.ReadString('\n') // 读取用户输入
		inputInfo := strings.Trim(input, "\r\n")
		if strings.ToUpper(inputInfo) == "Q" { // 如果输入q就退出
			return
		}
		_, err = conn.Write([]byte(inputInfo)) // 发送数据
		if err != nil {
			return
		}
	}
}

package main

import (
	"bufio"
	"fmt"
	"net"
)

func main() {
	listen, err := net.Listen("tcp", "127.0.0.1:20000") //监听端口

	if err != nil {
		fmt.Println("listen failed, err:", err)
		return
	}
	for {
		conn, err := listen.Accept() // 建立连接
		if err != nil {
			fmt.Println("accept failed, err:", err)
			continue
		}
		go process(conn) // 启动一个goroutine处理连接
	}
}

// 处理函数
func process(conn net.Conn) {
	defer conn.Close() // 关闭连接
	for {
		reader := bufio.NewReader(conn)
		var buf [128]byte
		n, err := reader.Read(buf[:]) // 读取数据
		if err != nil {
			fmt.Println("read from client failed, err:", err)
			break
		}
		recvStr := string(buf[:n])
		fmt.Println("收到client端发来的数据:", recvStr)
		conn.Write([]byte(recvStr)) // 发送数据
	}
}

这两个案例demo是官网的 主要是演示了** **

客户端发送服务 conn, err := net.Dial("tcp", "127.0.0.1:20000") //建立连接

服务器端的监听端口 listen, err := net.Listen("tcp", "127.0.0.1:20000") //监听端口

思考

  • 为什么服务端一定要有
	for {
		conn, err := listen.Accept() // 建立连接
		if err != nil {
			fmt.Println("accept failed, err:", err)
			continue
		}
		go process(conn) // 启动一个goroutine处理连接
	}


//服务器 持续监听 新的连接请求;
每来一个客户端,就启动一个 独立的 goroutine 去处理它;
主循环立即回到 Accept(),准备接收下一个客户端;
实现 并发、多客户端支持。

核心包NET

  • <font style="color:rgb(6, 10, 38);">Dial(network, address string)</font>:主动连接(客户端使用)
  • <font style="color:rgb(6, 10, 38);">DialTimeout(network, address string, timeout time.Duration)</font>:带超时的连接
  • <font style="color:rgb(6, 10, 38);">Listen(network, address string)</font>:监听端口(服务端使用)
  • <font style="color:rgb(6, 10, 38);">ResolveTCPAddr(network, address string)</font>:解析地址为 <font style="color:rgb(6, 10, 38);">*TCPAddr</font>
  • <font style="color:rgb(6, 10, 38);">LookupHost(host string)</font>:DNS 查询

UDP案例

package main

import (
	"fmt"
	"net"
)

// UDP 客户端
func main() {
	socket, err := net.DialUDP("udp", nil, &net.UDPAddr{
		IP:   net.IPv4(0, 0, 0, 0),
		Port: 30000,
	})
	if err != nil {
		fmt.Println("连接服务端失败,err:", err)
		return
	}
	defer socket.Close()
	sendData := []byte("Hello server")
	_, err = socket.Write(sendData) // 发送数据
	if err != nil {
		fmt.Println("发送数据失败,err:", err)
		return
	}
	data := make([]byte, 4096)
	n, remoteAddr, err := socket.ReadFromUDP(data) // 接收数据
	if err != nil {
		fmt.Println("接收数据失败,err:", err)
		return
	}
	fmt.Printf("recv:%v addr:%v count:%v\n", string(data[:n]), remoteAddr, n)
}

package main

import (
	"fmt"
	"net"
)

// UDP/server/main.go

// UDP server端
func main() {
	listen, err := net.ListenUDP("udp", &net.UDPAddr{
		IP:   net.IPv4(0, 0, 0, 0), //监听所有网卡
		Port: 30000,                //监听端口
	}) //创建UDP连接
	if err != nil {
		fmt.Println("listen failed, err:", err) //监听失败
		return
	}
	defer listen.Close()
	for {
		var data [1024]byte                         //创建一个1024字节的缓冲区
		n, addr, err := listen.ReadFromUDP(data[:]) // 接收数据
		if err != nil {
			fmt.Println("read udp failed, err:", err)
			continue
		}
		fmt.Printf("data:%v addr:%v count:%v\n", string(data[:n]), addr, n)
		_, err = listen.WriteToUDP(data[:n], addr) // 发送数据
		if err != nil {
			fmt.Println("write to udp failed, err:", err)
			continue
		}
	}
}

这次执行会发现跟TCP不一样 你执行一次以后 就会断开连接 这就是UDP UDP 客户端:多数场景是“问一次,答一次”,用完就走 不会建立长连接

小疑问:?UDP既然是用一次回答一次 那么像直播岂不是这种轮询会很多吗 如何保证呢? 直播我看的就是 点进去直播间 就可以一直看了 也没有什么明显的卡顿

A:直播并不是“轮询”,而是“持续单向流式发送”——服务器不停地往客户端发 UDP 包,客户端不停地收,中间没有“问-答”交互。

  • 你点进直播间 → 客户端告诉服务器:“我要看这个流”
  • 服务器立刻开始 以每秒几十个 UDP 包的速度,源源不断地把视频/音频数据发给你
  • 不需要你每帧都“请求”,也不需要你“确认收到”
  • 你只是默默接收并播放

这叫 单向实时流(Unidirectional Real-time Stream)

GO websocket入门教程

前情提要:本文章只适合想要入门以后 想要加强网络编程相关概念以及使用的 比小白NB 一点的大白。阅读文章之前 相信你已经知道了 TCP UDP WebSocket 的一些基本的概念 但是对于三者之间的联系 还有到底有什么区别...实战到底有什么不同....如果你是抱着这种心态的话,那么恭喜你,你被我恭喜到了(bushi)这篇一系列教程必然适合你。

WebSokcet Tcp Udp 傻傻分不清

最简单的 TCP 有链接 UDP无连接。 而对于websocket来说

举个例子>> 再我们访问网页的时候 每一个请求其实都是你去通过URL -->发GET/POST请求以后 -->服务器给你返回值 -->然后前端渲染--> 响应 -->你看到。 只是tcp/udp这个样子的 你请求 服务器才会给你,你不请求 服务器不给你 。

然而对于WebSocket来说 服务器说:不管你要不要 我全给你。是的 即使你不请求,只要你连接上了这个WebSocket 那么服务器有什么信息就发给你什么信息。

websocket就像是打电话(确实)

因此 tcp与websocket的区别的关键点就是在于 是否是你要了服务器才给你

其他不同 TCP&WebSocket

(1)建立在 TCP 协议之上,服务器端的实现比较容易。

(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。

(3)数据格式比较轻量,性能开销小,通信高效。

(4)可以发送文本,也可以发送二进制数据。

(5)没有同源限制,客户端可以与任意服务器通信。

(6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

websocket**握手过程**

客户端 → 服务器: HTTP Upgrade 请求
        (带 Sec-WebSocket-Key)

服务器 → 客户端: HTTP 101 Switching Protocols
        (返回 Sec-WebSocket-Accept)

连接建立!从此进入 WebSocket 世界

WebSocket 生命周期

握手 → 连接成功 → 持续通信 → 关闭连接
  ↓        ↓          ↓          ↓
onopen  onmessage   send()    onclose

开始代码

首先要先下载 包

go get -u -v github.com/gorilla/websocket

认识一下新朋友

conn *websocket.Conn

Gorilla WebSocket 库中表示一个已建立的 WebSocket 连接的对象。它是你与客户端(比如浏览器)进行双向通信的核心。

核心方法

方法作用
<font style="color:rgb(6, 10, 38);">ReadMessage() (messageType int, p []byte, err error)</font>从客户端读取消息
<font style="color:rgb(6, 10, 38);">WriteMessage(messageType int, data []byte) error</font>向客户端发送消息
<font style="color:rgb(6, 10, 38);">Close() error</font>主动关闭连接(发送 Close 帧)
<font style="color:rgb(6, 10, 38);">SetReadDeadline(t time.Time)</font> / <font style="color:rgb(6, 10, 38);">SetWriteDeadline(t time.Time)</font>设置读写超时
<font style="color:rgb(6, 10, 38);">LocalAddr()</font> / <font style="color:rgb(6, 10, 38);">RemoteAddr()</font>获取本地/客户端网络地址
<font style="color:rgb(6, 10, 38);">Subprotocol()</font>获取协商的子协议(如 <font style="color:rgb(6, 10, 38);">"chat"</font>

写一个echo服务器

Echo 服务器(Echo Server)是一种非常基础的网络服务程序,其核心功能是:将客户端发送过来的数据原样返回(“回显”)给客户端。它常用于网络编程教学、协议测试、连接调试等场景。

package main

import (
	"log"
	"net/http"

	"github.com/gorilla/websocket"
)

// 升级 HTTP 连接到 WebSocket函数
var upgrader = websocket.Upgrader{
	CheckOrigin: func(r *http.Request) bool {
		//todo: 生产环境应该验证来源
		return true //这里直接返回true,表示允许所有来源
	},
}

// w http.ResponseWriter 是一个接口(interface) 用于向客户端(通常是浏览器或其他 HTTP 客户端)写回响应数据。
// r *http.Request 是一个结构体指针 包含客户端发来的 HTTP 请求的所有信息。
func echo(w http.ResponseWriter, r *http.Request) {
	// 1. 升级 HTTP 连接到 WebSocket
	conn, err := upgrader.Upgrade(w, r, nil)
	// upgrader.Upgrade(w, r,  responseHeader http.Header)
	/*
		responseHeader http.Header 是一个结构体 包含响应头信息
		握手响应中添加自定义 HTTP 头。
		这个头只在握手阶段发送一次,后续 WebSocket 数据帧不再包含 HTTP 头。
	*/
	if err != nil {
		log.Println("升级失败:", err)
		return
	}
	defer conn.Close()

	// 2. 循环读取消息
	for {
		// 读取客户端消息
		messageType, message, err := conn.ReadMessage()
		//messageType 有两种类型:文本(TextMessage) 值为 websocket.TextMessage 1
		// 和二进制(BinaryMessage) 于发送 任意二进制数据(如图片、音频、Protobuf、自定义字节流)  websocket.BinaryMessage 2
		if err != nil {
			log.Println("读取失败:", err)
			break
		}
		log.Printf("收到: %s", message)
		retmsg := []byte("Echo: " + string(message))
		// 3. 原样返回(Echo)
		err = conn.WriteMessage(messageType, retmsg)
		if err != nil {
			log.Println("发送失败:", err)
			break
		}
	}
}

func main() {
	http.HandleFunc("/ws", echo)
	log.Println("服务器启动在 :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

HTML(本教程并不交前端 所以我是AI生成的能用就行)

<!DOCTYPE html>
<html>
<head>
    <title>WebSocket Echo 测试</title>
</head>
<body>
    <h1>WebSocket Echo 测试</h1>
    <input id="msg" type="text" placeholder="输入消息">
    <button onclick="send()">发送</button>
    <div id="messages"></div>

    <script>
        let socket;
        
        // 连接
        function connect() {
            socket = new WebSocket('ws://localhost:8080/ws');
            
            socket.onopen = () => {
                appendMessage('✅ 连接成功');
            };
            
            socket.onmessage = (event) => {
                appendMessage('收到: ' + event.data);
            };
            
            socket.onclose = () => {
                appendMessage('❌ 连接关闭');
            };
            
            socket.onerror = (error) => {
                appendMessage('❌ 错误: ' + error);
            };
        }
        
        // 发送消息
        function send() {
            const msg = document.getElementById('msg').value;
            if (msg && socket.readyState === WebSocket.OPEN) {
                socket.send(msg);
                appendMessage('你: ' + msg);
                document.getElementById('msg').value = '';
            }
        }
        
        // 显示消息
        function appendMessage(text) {
            const div = document.createElement('div');
            div.textContent = text;
            document.getElementById('messages').appendChild(div);
        }
        
        // 页面加载时连接
        window.onload = connect;
    </script>
</body>
</html>

或者是写一个client客户端 用来跟服务端建立websocket连接

package main

import (
	"bufio"
	"fmt"
	"log"
	"os"
	"strings"

	"github.com/gorilla/websocket"
)

func main() {
	conn, _, err := websocket.DefaultDialer.Dial("ws://localhost:8080/ws", nil)
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	// 启动一个 goroutine 专门读取服务器消息
	go func() {
		for {
			_, msg, err := conn.ReadMessage()
			if err != nil {
				log.Println("读取出错:", err)
				return
			}
			fmt.Printf("← 收到: %s\n", msg)
		}
	}()

	// 主 goroutine 负责从 stdin 读取用户输入并发送
	scanner := bufio.NewScanner(os.Stdin)
	fmt.Println("请输入消息(输入 'quit' 退出):")
	for scanner.Scan() {
		text := strings.TrimSpace(scanner.Text())
		if text == "quit" {
			break
		}
		if err := conn.WriteMessage(websocket.TextMessage, []byte(text)); err != nil {
			log.Println("发送失败:", err)
			break
		}
		fmt.Printf("→ 已发送: %s\n", text)
	}
}

官方案例demo

框架展示

Server

package main

import (
	"fmt"
	"net/http"

	"github.com/gorilla/mux"
)

func main() {
	router := mux.NewRouter()
	go h.run()
	router.HandleFunc("/ws", myws)
	if err := http.ListenAndServe("127.0.0.1:8080", router); err != nil {
		fmt.Println("err:", err)
	}
}

HUB

package main

import "encoding/json"

var h = hub{
	c: make(map[*connection]bool),//连接列表
	u: make(chan *connection), //注销通道
	b: make(chan []byte), //广播通道
	r: make(chan *connection),//注册通道
}

type hub struct {
	c map[*connection]bool
	b chan []byte
	r chan *connection
	u chan *connection
}

func (h *hub) run() {
	for {
		select {
		case c := <-h.r:
			h.c[c] = true
			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:
			if _, ok := h.c[c]; ok {
				delete(h.c, c)
				close(c.sc)
			}
		case data := <-h.b:
			for c := range h.c {
				select {
				case c.sc <- data:
				default:
					delete(h.c, c)
					close(c.sc)
				}
			}
		}
	}
}

DATA

package main

import (
	"encoding/json"

	"github.com/gorilla/websocket"
)

// Data 是所有 WebSocket 消息的通用结构
type Data struct {
	Ip       string   `json:"ip"`        // 客户端 IP
	User     string   `json:"user"`      // 当前用户
	From     string   `json:"from"`      // 消息来源
	Type     string   `json:"type"`      // 消息类型
	Content  string   `json:"content"`   // 消息内容
	UserList []string `json:"user_list"` // 在线用户列表
}

// 封装发送逻辑
func (d *Data) SendTo(conn *websocket.Conn) error {
	data, err := json.Marshal(d)
	if err != nil {
		return err
	}
	return conn.WriteMessage(websocket.TextMessage, data)
}

Connection

package main

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

	"github.com/gorilla/websocket"
)

type connection struct {
	ws   *websocket.Conn
	sc   chan []byte
	data *Data
}

var user_list = []string{}

var wu = &websocket.Upgrader{ReadBufferSize: 512, WriteBufferSize: 512, CheckOrigin: func(r *http.Request) bool {
	return true
}}

func myws(w http.ResponseWriter, r *http.Request) {
	ws, err := wu.Upgrade(w, r, nil)
	if err != nil {
		return
	}
	c := &connection{sc: make(chan []byte, 256), ws: ws, data: &Data{}}
	h.r <- c
	go c.writer()
	c.reader()
	defer func() {
		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() {
	for message := range c.sc {
		c.ws.WriteMessage(websocket.TextMessage, message)
	}
	c.ws.Close()
}

func (c *connection) reader() {
	for {
		_, 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 {
			return slice[:count]
		} else if slice[i] == user {
			n_slice = append(slice[:i], slice[i+1:]...)
			break
		}
	}
	fmt.Println(n_slice)
	return n_slice
}

小问题 Q&A

Q:为什么myws的write 要启一个协程与写 而Read确是要一个无限for循环就可以?

A: 如果write是for循环 就会卡住Read 所以write必须是协程

Q:为什么read不能也是一个协程而是一个for循环

A:但通常 不需要,因为:myws 的主 goroutine 本来就没别的事干,让它负责 reader 更自然减少一个 goroutine,节省资源生命周期更清晰:myws 退出 = 连接关闭

reader()writer()
是否循环✅必须 for
循环
也用 for range
循环
是否 goroutine不需要(主流程就是它)✅必须 go
启动
驱动方式主动从网络读(阻塞 I/O)被动从 channel 读(事件驱动)
目的接收客户端指令发送服务端消息给客户端

BUT

但是我感觉这个官方的demo有点小问题 字段语义不清晰 而且

c.data.User = c.data.Content            
c.data.From = c.data.User 想这一块就很奇怪

所以我写了一个小demo基于官方的demo 更适合新人宝宝的体质

demo升级版(更容易理解)

server

package main

import (
	"log"
	"net/http"

	"github.com/gorilla/mux"
)

func main() {
	// 1. 启动 hub(消息中心)
	go h.run()

	// 2. 创建路由器
	router := mux.NewRouter()

	// 3. 注册 WebSocket 路由
	router.HandleFunc("/ws", myws)

	// 4. 启动 HTTP 服务器
	log.Println(" 服务器启动在 http://127.0.0.1:2778")
	log.Fatal(http.ListenAndServe("127.0.0.1:2778", router))
}

HUB

package main

import "log"

// hub 是聊天室的核心,管理所有连接和消息广播
type hub struct {
	c map[*connection]bool // 所有活跃连接
	u chan *connection     // 注销连接通道
	b chan []byte          // 广播消息通道
	r chan *connection     // 注册新连接通道
}

var h = hub{
	c: make(map[*connection]bool),
	u: make(chan *connection),
	b: make(chan []byte),
	r: make(chan *connection),
}

// run 是 hub 的主循环,处理所有事件
func (h *hub) run() {
	for {
		select {
		// 处理新连接注册
		case conn := <-h.r:
			h.c[conn] = true
			log.Printf(" 新连接: %p, 当前连接数: %d", conn, len(h.c))

			// 发送握手消息
			data := &Data{
				Ip:   conn.ws.RemoteAddr().String(),
				Type: "handshake",
			}
			data.SendTo(conn.ws)

		// 处理连接注销
		case conn := <-h.u:
			if _, ok := h.c[conn]; ok {
				delete(h.c, conn)
				close(conn.sc)
				log.Printf(" 连接断开: %p, 剩余连接数: %d", conn, len(h.c))
			}

		// 处理广播消息
		case message := <-h.b:
			for conn := range h.c {
				select {
				case conn.sc <- message:
					// 发送成功
				default:
					// 发送失败(channel 满),断开连接
					close(conn.sc)
					delete(h.c, conn)
				}
			}
		}
	}
}

DATA

package main

import (
	"encoding/json"

	"github.com/gorilla/websocket"
)

// Data 是所有 WebSocket 消息的通用结构
type Data struct {
	Ip       string   `json:"ip"`        // 客户端 IP
	User     string   `json:"user"`      // 当前用户
	From     string   `json:"from"`      // 消息来源
	Type     string   `json:"type"`      // 消息类型
	Content  string   `json:"content"`   // 消息内容
	UserList []string `json:"user_list"` // 在线用户列表
}

// 封装发送逻辑
func (d *Data) SendTo(conn *websocket.Conn) error {
	data, err := json.Marshal(d)
	if err != nil {
		return err
	}
	return conn.WriteMessage(websocket.TextMessage, data)
}

CONNECTION

package main

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

	"github.com/gorilla/websocket"
)

// connection 代表一个客户端连接
type connection struct {
	ws   *websocket.Conn // 底层 WebSocket 连接
	sc   chan []byte     // 发送消息的 channel
	data *Data           // 当前处理的消息
}

var upgrader = websocket.Upgrader{
	CheckOrigin: func(r *http.Request) bool {
		return true // TODO: 生产环境应验证来源
	},
}

// myws 是 WebSocket 的 HTTP 处理函数
func myws(w http.ResponseWriter, r *http.Request) {
	// 1. 升级 HTTP 连接到 WebSocket
	ws, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Println("升级失败:", err)
		return
	}

	// 2. 创建 connection
	conn := &connection{
		ws: ws,
		sc: make(chan []byte, 256), // 带缓冲的 channel
	}

	// 3. 注册到 hub
	h.r <- conn

	// 4. 启动发送协程
	go conn.writer()

	// 5. 启动接收循环(阻塞)
	conn.reader()

	// 6. 退出时注销
	h.u <- conn
}

// writer 负责从 sc channel 读取消息并发送
func (c *connection) writer() {
	for message := range c.sc {
		err := c.ws.WriteMessage(websocket.TextMessage, message)
		if err != nil {
			log.Println("发送失败:", err)
			break
		}
	}
	c.ws.Close()
}

// reader 负责读取客户端消息并处理
func (c *connection) reader() {
	defer func() {
		// 确保退出时关闭连接
		c.ws.Close()
	}()

	for {
		// 1. 读取消息
		_, message, err := c.ws.ReadMessage()
		if err != nil {
			log.Println("读取失败:", err)
			break
		}

		// 2. 解析 JSON
		var data Data
		if err := json.Unmarshal(message, &data); err != nil {
			log.Println("解析失败:", err)
			continue
		}
		c.data = &data

		// 3. 根据消息类型处理
		switch data.Type {
		case "login":
			handleLogin(c, &data)
		case "user":
			handleUserMessage(c, &data)
		case "logout":
			handleLogout(c, &data)
		default:
			log.Printf("未知消息类型: %s", data.Type)
		}
	}
}



var userList []string // 全局用户列表(非线程安全!)

// 处理登录
func handleLogin(c *connection, data *Data) {
	// 添加用户
	userList = append(userList, data.Content)

	// 广播新用户上线
	broadcastData := &Data{
		User:     data.Content,
		Type:     "login",
		Content:  data.Content + " 加入了聊天室",
		UserList: userList,
	}

	broadcastToAll(broadcastData)
}

// 处理普通消息
func handleUserMessage(c *connection, data *Data) {
	// 转发消息给所有人
	broadcastData := &Data{
		User:    data.User,
		From:    data.From,
		Type:    "user",
		Content: data.Content,
	}

	broadcastToAll(broadcastData)
}

// 处理登出
func handleLogout(c *connection, data *Data) {
	// 从用户列表移除
	userList = del(userList, data.Content)

	// 广播用户下线
	broadcastData := &Data{
		User:     data.Content,
		Type:     "logout",
		Content:  data.Content + " 离开了聊天室",
		UserList: userList,
	}

	broadcastToAll(broadcastData)
}

// 广播给所有连接
func broadcastToAll(data *Data) {
	jsonData, _ := json.Marshal(data)
	h.b <- jsonData
}

// 从切片中删除元素
func del(slice []string, user string) []string {
	for i, u := range slice {
		if u == user {
			return append(slice[:i], slice[i+1:]...)
		}
	}
	return slice
}