websocket 搭建直播聊天室

烟雨 发布于 2025-07-22 60 次阅读


下载地址:olahol/melody: :notes: Go 的极简 websocket 框架

展示用宝塔搭建websocket 我第一次用go部署东西 有很多也不熟悉 走个流程即可

开始搭建

#先搭建好宝塔 我已经搭建好了宝塔 进入下载地址下载zip格式源码
#找到网站 GO项目 SDK管理 常用版本 安装1.24.5 (稳定版)
#下载melody-master.zip并上传到宝塔目录www/wwwroot/websocket并解压
#没有websocket文件夹就新建一个 解压出来的文件夹为melody-master
#进入目录 
cd /www/wwwroot/websocket
#初始化模块(如果没做过)
go mod init melody-master
#临时使用国内代理 这会设置 Go 使用阿里云的 goproxy 国内代理,速度更快且更稳定。
go env -w GOPROXY=https://goproxy.cn,direct

#假设入口文件是 main.go,运行: websocket_server 是你编译成go的名字 随便改
#在www/wwwroot创建main.go 复制下面代码并保存 这个地址#https://github.com/olahol/melody/tree/master/examples 
#里面有好多扩展main.go 自己去复制或者用我这个现成的

复制以下代码放进main.go并保存

package main

import (
	"net/http"

	"github.com/olahol/melody"
)

func main() {
	m := melody.New()

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		http.ServeFile(w, r, "index.html")
	})

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

	m.HandleMessage(func(s *melody.Session, msg []byte) {
		m.Broadcast(msg)
	})

	http.ListenAndServe(":5000", nil)
}

自用app引用代码

package main

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

	"github.com/olahol/melody"
)

// 定义一个结构来存储每个用户的信息
type User struct {
	ID   int64  `json:"id"`
	Name string `json:"name"`
}

// 用一个线程安全的Map来存储所有在线用户
var (
	users      = make(map[*melody.Session]*User)
	usersMutex = &sync.RWMutex{}
	idCounter  int64
)

// App端期望收到的用户列表消息结构
type UserListMessage struct {
	Type  string   `json:"type"`
	Users []string `json:"users"`
}

func main() {
	m := melody.New()

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		http.ServeFile(w, r, "index.html")
	})

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

	// 当一个新用户连接时
	m.HandleConnect(func(s *melody.Session) {
		idCounter++
		newUser := &User{ID: idCounter, Name: fmt.Sprintf("User-%d", idCounter)}

		usersMutex.Lock()
		users[s] = newUser
		usersMutex.Unlock()

		fmt.Printf("User %s (ID: %d) connected.\n", newUser.Name, newUser.ID)
	})

	// 当一个用户断开连接时
	m.HandleDisconnect(func(s *melody.Session) {
		usersMutex.Lock()
		if user, ok := users[s]; ok {
			delete(users, s)
			fmt.Printf("User %s (ID: %d) disconnected.\n", user.Name, user.ID)
		}
		usersMutex.Unlock()
	})

	// 当收到任何消息时
	m.HandleMessage(func(s *melody.Session, msg []byte) {
		var data map[string]interface{}
		if err := json.Unmarshal(msg, &data); err != nil {
			fmt.Println("Error decoding message:", err)
			return
		}

		// 根据消息类型做不同处理
		msgType, _ := data["type"].(string)
		switch msgType {
		
		case "set_username":
			// 这是App连接成功后发的“自我介绍”
			usersMutex.Lock()
			if user, ok := users[s]; ok {
				if newName, ok := data["username"].(string); ok {
					fmt.Printf("User %s (ID: %d) is now known as %s.\n", user.Name, user.ID, newName)
					user.Name = newName // 更新用户名
				}
			}
			usersMutex.Unlock()

		case "get_user_list":
			// 这是我们的App定时发来的请求
			usersMutex.RLock()
			var userNames []string
			for _, user := range users {
				userNames = append(userNames, user.Name)
			}
			usersMutex.RUnlock()

			// 构造符合App期望的响应
			response := UserListMessage{
				Type:  "user_list",
				Users: userNames,
			}
			
			// 只把响应发回给请求者
			jsonResponse, _ := json.Marshal(response)
			s.Write(jsonResponse)

		case "chat_message", "sticker_message":
			// 对于聊天和表情,我们依然需要更新用户名,以防用户中途改名
			usersMutex.Lock()
			if user, ok := users[s]; ok {
				if newName, ok := data["username"].(string); ok {
					if user.Name != newName {
						fmt.Printf("User %s (ID: %d) has changed name to %s.\n", user.Name, user.ID, newName)
						user.Name = newName
					}
				}
			}
			usersMutex.Unlock()

			// 将聊天/表情消息广播给所有人
			m.Broadcast(msg)
		
		default:
			// 如果是未知类型,就直接广播
			fmt.Printf("Broadcasting unknown message type: %s\n", msgType)
			m.Broadcast(msg)
		}
	})

	fmt.Println("WebSocket server started at :5000")
	http.ListenAndServe(":5000", nil)
}
#下载依赖
go mod tidy
#编译 websocket_server 是要编译出来的名字  编译方法在main.go
go build -o websocket_server main.go

测试脚本 index.html

随便找的 作者也有我没用我放在下面 把index.html 放到/www/wwwroot/

这个地址里面有很多index.html 你可以用我的也可以用他的反正是测试

https://github.com/olahol/melody/tree/master/examples

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <title>Melody Chat</title>
</head>
<body>
  <h1>WebSocket Chat</h1>
  <input id="msg" placeholder="输入消息" />
  <button onclick="send()">发送</button>
  <ul id="chat"></ul>

  <script>
    const ws = new WebSocket(`ws://${location.host}/ws`);
    ws.onmessage = e => {
      const li = document.createElement("li");
      li.textContent = e.data;
      document.getElementById("chat").appendChild(li);
    };
    function send() {
      const input = document.getElementById("msg");
      ws.send(input.value);
      input.value = "";
    }
  </script>
</body>
</html>

APP专用测试index.html

<!DOCTYPE html>
<html>
<head>
    <title>WebSocket Test Client</title>
    <style>
        body { font-family: sans-serif; display: flex; flex-direction: column; height: 100vh; margin: 0; }
        #header { padding: 10px; border-bottom: 1px solid #ccc; background-color: #f8f8f8; display: flex; align-items: center; }
        #header input { margin-right: 10px; }
        #content { display: flex; flex: 1; overflow: hidden; }
        #chat-container { flex: 3; display: flex; flex-direction: column; border-right: 1px solid #ccc; }
        #messages { flex: 1; list-style-type: none; margin: 0; padding: 10px; overflow-y: auto; }
        #messages li { padding: 8px; border-bottom: 1px solid #eee; }
        #messages li.sticker img { width: 48px; height: 48px; }
        #form { display: flex; padding: 10px; border-top: 1px solid #ccc; }
        #form input { flex: 1; padding: 10px; border: 1px solid #ccc; border-radius: 3px; }
        #form button { margin-left: 10px; }
        #user-list-container { flex: 1; padding: 10px; overflow-y: auto; }
        #user-list-container h3 { margin-top: 0; }
        #users { list-style-type: none; padding: 0; }
    </style>
</head>
<body>

    <div id="header">
        <input id="username" value="WebAppUser" />
        <button id="connectBtn">Connect</button>
    </div>

    <div id="content">
        <div id="chat-container">
            <ul id="messages"></ul>
            <form id="form" action="">
                <input id="input" autocomplete="off" /><button>Send</button>
            </form>
        </div>
        <div id="user-list-container">
            <h3>Online Users (<span id="user-count">0</span>)</h3>
            <ul id="users"></ul>
        </div>
    </div>

    <script>
        const usernameInput = document.getElementById('username');
        const connectBtn = document.getElementById('connectBtn');
        const form = document.getElementById('form');
        const input = document.getElementById('input');
        const messages = document.getElementById('messages');
        const usersList = document.getElementById('users');
        const userCount = document.getElementById('user-count');

        let ws;
        let pollingInterval;

        const stickerMap = {
            "ic_sticker_smile": `<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"><path fill="#FFC107" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z"/><path fill="#000000" d="M12 17.5c-2.49 0-4.5-2.01-4.5-4.5h9c0 2.49-2.01 4.5-4.5 4.5z"/><path fill="#000000" d="M8.5 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/><path fill="#000000" d="M15.5 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/></svg>`,
            "ic_sticker_laugh": `<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"><path fill="#FFC107" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z"/><path fill="#000000" d="M12 17.5c-3.04 0-5.5-2.46-5.5-5.5h11c0 3.04-2.46 5.5-5.5 5.5z"/><path fill="#000000" d="M8.5 9C7.67 9 7 8.33 7 7.5S7.67 6 8.5 6s1.5.67 1.5 1.5S9.33 9 8.5 9z"/><path fill="#000000" d="M15.5 9c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5S16.33 9 15.5 9z"/></svg>`
        };

        function connect() {
            const host = window.location.host;
            ws = new WebSocket(`ws://${host}/ws`);

            ws.onopen = function() {
                console.log("Connected to server");
                connectBtn.textContent = 'Disconnect';
                connectBtn.onclick = disconnect;
                
                // Introduce ourselves
                const username = usernameInput.value || "WebAppUser";
                ws.send(JSON.stringify({ type: 'set_username', username: username }));

                // Start polling for user list
                requestUserList();
                pollingInterval = setInterval(requestUserList, 5000);
            };

            ws.onmessage = function (e) {
                const data = JSON.parse(e.data);
                
                switch(data.type) {
                    case "user_list":
                        updateUserList(data.users);
                        break;
                    case "chat_message":
                        addMessage(`${data.username}: ${data.message}`);
                        break;
                    case "sticker_message":
                        addSticker(data.username, data.sticker_name);
                        break;
                }
            };

            ws.onclose = function() {
                console.log("Disconnected from server");
                clearInterval(pollingInterval);
                connectBtn.textContent = 'Connect';
                connectBtn.onclick = connect;
                updateUserList([]);
            };
        }

        function disconnect() {
            if (ws) {
                ws.close();
            }
        }

        function requestUserList() {
            if (ws && ws.readyState === WebSocket.OPEN) {
                ws.send(JSON.stringify({ type: 'get_user_list' }));
            }
        }

        function updateUserList(users) {
            userCount.textContent = users.length;
            usersList.innerHTML = '';
            users.forEach(user => {
                const item = document.createElement('li');
                item.textContent = user;
                usersList.appendChild(item);
            });
        }
        
        function addMessage(text) {
            const item = document.createElement('li');
            item.textContent = text;
            messages.appendChild(item);
            messages.scrollTop = messages.scrollHeight;
        }

        function addSticker(username, stickerName) {
            const item = document.createElement('li');
            item.classList.add('sticker');
            const stickerSVG = stickerMap[stickerName] || 'Sticker';
            item.innerHTML = `<strong>${username}:</strong> ${stickerSVG}`;
            messages.appendChild(item);
            messages.scrollTop = messages.scrollHeight;
        }

        connectBtn.onclick = connect;

        form.addEventListener('submit', function(e) {
            e.preventDefault();
            if (ws && ws.readyState === WebSocket.OPEN && input.value) {
                const username = usernameInput.value || "WebAppUser";
                const message = {
                    type: "chat_message",
                    username: username,
                    message: input.value
                };
                ws.send(JSON.stringify(message));
                input.value = '';
            }
        });

    </script>
</body>
</html>

启动你的 Go 服务

#启动你的 Go 服务
cd /www/wwwroot/websocket
./websocket_server
#我用宝塔的就不从这里启动了
点击网站 GO项目 添加GO项目

项目执行文件
/www/wwwroot/websocket/websocket_server
项目名称
websocket_server
项目端口
5000
执行命令
/www/wwwroot/websocket/websocket_server
应用变量 无
运行用户
www
开机启动 √
在添加一个域名和外网映射 就可以测试了
http://你的ip:5000

此作者没有提供个人介绍。
最后更新于 2025-07-23