当前位置: 首页 > article >正文

使用 Go 语言实现简单聊天系统

在互联网时代,聊天系统是常见的应用场景之一。无论是即时通讯、在线客服还是多人游戏中的消息系统,聊天功能的实现都是必不可少的。本文将使用 Go 语言,结合 WebSocket 来构建一个简单的多人聊天室系统。

一、项目结构

首先,我们设计一个简单的项目结构,文件结构如下:

go-chat/
│
├── main.go          // 主程序
├── client.go        // 处理客户端连接
└── hub.go           // 消息管理

WebSocket 简介

WebSocket 是一种基于 TCP 的网络协议,允许客户端和服务端建立持久的全双工通信连接。相比于传统的 HTTP 请求-响应模型,WebSocket 更加适合实时通信场景,因此它是实现聊天系统的理想选择。

二、实现思路

  1. 客户端连接管理:每个客户端通过 WebSocket 连接到服务器,服务器会为每个连接的客户端分配一个唯一的 connection
  2. 消息广播:当某个客户端发送消息时,服务器将该消息广播给所有连接的客户端。
  3. 并发处理:Go 原生支持并发编程,通过 Goroutine 和 Channel 可以轻松处理并发消息传递。

三、详细实现

1. main.go - 启动 WebSocket 服务

package main

import (
    "log"
    "net/http"
)

func main() {
    hub := newHub()
    go hub.run()

    http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
        serveWs(hub, w, r)
    })

    log.Println("服务器启动,监听端口 8080...")
    err := http.ListenAndServe(":8080", nil)
    if err != nil {
        log.Fatal("监听失败:", err)
    }
}

main.go 文件的作用是启动 HTTP 服务器,并在 /ws 路径上处理 WebSocket 连接请求。

2. hub.go - 消息管理中心

Hub 负责管理所有的客户端连接,以及消息的广播。

package main

// Hub 负责管理所有客户端的注册、注销及消息广播
type Hub struct {
    clients    map[*Client]bool // 已连接的客户端
    broadcast  chan []byte      // 从客户端接收的广播消息
    register   chan *Client     // 注册请求
    unregister chan *Client     // 注销请求
}

func newHub() *Hub {
    return &Hub{
        clients:    make(map[*Client]bool),
        broadcast:  make(chan []byte),
        register:   make(chan *Client),
        unregister: make(chan *Client),
    }
}

func (h *Hub) run() {
    for {
        select {
        case client := <-h.register:
            h.clients[client] = true
        case client := <-h.unregister:
            if _, ok := h.clients[client]; ok {
                delete(h.clients, client)
                close(client.send)
            }
        case message := <-h.broadcast:
            for client := range h.clients {
                select {
                case client.send <- message:
                default:
                    close(client.send)
                    delete(h.clients, client)
                }
            }
        }
    }
}
  • clients:保存当前连接的所有客户端。
  • broadcast:一个通道,用于广播消息给所有客户端。
  • register/unregister:用于客户端连接和断开的注册和注销。

3. client.go - 处理客户端连接

每个客户端的连接由 Client 结构体表示,并包含了 WebSocket 连接和发送消息的通道。

package main

import (
    "github.com/gorilla/websocket"
    "log"
    "net/http"
    "time"
)

const (
    writeWait = 10 * time.Second
    pongWait  = 60 * time.Second
    pingPeriod = (pongWait * 9) / 10
)

var upgrader = websocket.Upgrader{
    ReadBufferSize:  1024,
    WriteBufferSize: 1024,
}

type Client struct {
    hub  *Hub
    conn *websocket.Conn
    send chan []byte
}

func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Println("升级到 WebSocket 失败:", err)
        return
    }
    client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)}
    client.hub.register <- client

    go client.writePump()
    go client.readPump()
}

func (c *Client) readPump() {
    defer func() {
        c.hub.unregister <- c
        c.conn.Close()
    }()
    c.conn.SetReadLimit(512)
    c.conn.SetReadDeadline(time.Now().Add(pongWait))
    c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })

    for {
        _, message, err := c.conn.ReadMessage()
        if err != nil {
            if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
                log.Printf("读取错误: %v", err)
            }
            break
        }
        c.hub.broadcast <- message
    }
}

func (c *Client) writePump() {
    ticker := time.NewTicker(pingPeriod)
    defer func() {
        ticker.Stop()
        c.conn.Close()
    }()

    for {
        select {
        case message, ok := <-c.send:
            c.conn.SetWriteDeadline(time.Now().Add(writeWait))
            if !ok {
                c.conn.WriteMessage(websocket.CloseMessage, []byte{})
                return
            }

            w, err := c.conn.NextWriter(websocket.TextMessage)
            if err != nil {
                return
            }
            w.Write(message)

            n := len(c.send)
            for i := 0; i < n; i++ {
                w.Write(<-c.send)
            }

            if err := w.Close(); err != nil {
                return
            }

        case <-ticker.C:
            c.conn.SetWriteDeadline(time.Now().Add(writeWait))
            if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
                return
            }
        }
    }
}
  • serveWs:处理每个 WebSocket 连接请求,并为每个连接创建一个客户端实例。
  • readPump:从 WebSocket 连接中读取消息,并将消息广播到所有客户端。
  • writePump:负责将消息发送给客户端,并定期发送心跳检测(ping)消息以保持连接。

四、运行项目

  1. 首先,安装 WebSocket 依赖:

    go get github.com/gorilla/websocket
    
  2. 编写前端 HTML 页面(可用于测试),例如 index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Go 聊天室</title>
    </head>
    <body>
        <div id="chatbox"></div>
        <input id="msg" type="text" />
        <button onclick="sendMessage()">发送</button>
    
        <script>
            var ws = new WebSocket("ws://localhost:8080/ws");
            ws.onmessage = function(event) {
                var chatbox = document.getElementById('chatbox');
                chatbox.innerHTML += event.data + "<br/>";
            };
    
            function sendMessage() {
                var msg = document.getElementById("msg").value;
                ws.send(msg);
            }
        </script>
    </body>
    </html>
    
  3. 运行 Go 服务:

    go run main.go
    
  4. 打开浏览器,访问 index.html,即可体验多人聊天室的功能。


http://www.kler.cn/news/313594.html

相关文章:

  • 排序算法-归并排序
  • 深入解析 JVM 运行时数据区:实战与面试指南
  • Qt clicked()、clicked(bool)、toggled(bool)信号的区别和联系
  • C#基础(11)函数重载
  • 使用jenkins打包unity工程
  • LeetCode118:杨辉三角
  • Spring Boot- 配置文件问题
  • 【JavaScript】数据结构之链表(双指针、滑动窗口)
  • 切换淘宝最新镜像源npm详细讲解
  • 计算机毕业设计选题推荐-4S店试驾平台-小程序/App
  • 过采样和欠采样
  • C++ 字符串最后一个单词的长度(牛客网)
  • # wps必须要登录激活才能使用吗?
  • 摄影学习平台
  • 【Linux】简易日志系统
  • Web前端开发
  • PHP 数组排序类型介绍
  • 基于微信小程序的剧本杀游玩一体化平台
  • [数据结构]算法复杂度详解
  • 代码随想录算法训练营Day7
  • 基于MySQL全量备份+GTID同步的主从架构恢复数据至指定时间点
  • Linux--禁止root用户通过ssh直接登录
  • Java项目实战II基于Java+Spring Boot+MySQL的网上租贸系统设计与实现(开发文档+源码+数据库)
  • 情感AI:科技赋能情感计算的新时代
  • SpringBoot:token是用来鉴权的,那session的作用是什么?
  • 笔记:将WPF中可视化元素(Visual)保存为图像,如PNG,JPEG或BMP的方法简介
  • 设计模式七大原则
  • 毕业设计选题:基于ssm+vue+uniapp的农产品自主供销小程序
  • 与转录组结合,开发下一代诊断技术,或许是医学AI领域的下一个热点|个人观点·24-09-21
  • 中国电子学会202303青少年软件编程(Python)等级考试试卷(四级)真题