前言
使用Go语言开发一个网络群聊天室
部署在Linux环境 CentOS系统
类似于QQ群的效果
功能:
用户上线时通知所有人
用户下线时通知所有人
用户发送消息 所有在线用户会接收到
用户挂机超时会被自动下线
用户可以查询当前所有在线的用户 \who命令
用户可以更改自己的昵称 \rename|new_name命令
核心思路
一、启动主GO程 用于接收各个客户端发来的连接请求
二、对于每一个成功连接的客户端 启动一个子GO程进行业务处理
1.通过MAP数据结构管理所有在线的用户信息
2.每一个连接(用户)都有一个私有管道msg用于接收数据
3.由一个全局管道message和配套的全局广播函数实现群发功能
细节处理
一、如何使所有在线的用户(客户端)接收到消息?
先在主GO程中启动全局广播函数:
创建一个全局管道变量message
全局广播函数遍历MAP数据结构 确定所有在线用户
将message管道中的信息依次发送给每一个用户的私有msg管道
每一个用户接收群发消息:
在处理每一个连接的子GO程中
再启动一个Go程负责监听自己的msg私有管道
并将写入msg管道的数据输出至连接对应的客户端
二、如何实现用户下线功能和超时退出?
在每一个连接的子GO程中
再启动一个GO程负责检查退出状态
使用select语句 检测主动退出
使用计时器实现超时退出
且在有新的消息产生时 重置计时器
三、对MAP的操作应当使用读写锁
完整代码
package main
import (
"fmt"
"net"
"strings"
"sync"
"time"
)
type User struct {
//姓名
name string
//唯一的ID
id string
//管道
msg chan string
}
//全局的MAP结构 存储所有用户的信息
//MAP不能同时读写 如果有不同的GO程同时操作MAP 需要对MAP上锁
var lock sync.RWMutex
var allUsers = make(map[string]User)
//定义一个message全局通道 用于接收任何人发来的消息
var message = make(chan string, 64)
func handler(conn net.Conn) {
//服务端提取客户端连接的时候
//会获取客户端的IP和PORT
clientAddr := conn.RemoteAddr().String()
//为本次连接的客户端创建用户
newUser := User{
id: clientAddr, //ID作为MAP中的KEY值
name: clientAddr, //用户可以自定义 默认初始值与ID相同
msg: make(chan string, 64), //有缓冲的管道 非阻塞
}
//将User变量添加入allUsers
lock.Lock()
allUsers[newUser.id] = newUser
lock.Unlock()
//定义一个退出信号 用于监听对应client是否退出
var isQuit = make(chan bool)
//创建一个重置计数器的管道
//告知watchExit函数 当前用户仍在输入
var resetTimer = make(chan bool)
//启动GO程负责监听退出的信号
go watchExit(&newUser, conn, isQuit, resetTimer)
//向message写入数据 通知所有人 当前用户已经上线
loginInfo := fmt.Sprintf("[%s] is online\n", newUser.name)
message <- loginInfo
//每个连接启动一个GO程从服务端写回客户端
go writeBackToClient(&newUser, conn)
//业务逻辑 多次读取客户端发送的消息
for {
buf := make([]byte, 1024)
cnt, err := conn.Read(buf)
if cnt == 0 {
//发送一个退出信号 由相关GO程负责处理
isQuit <- true
return
}
if err != nil {
fmt.Println("conn.Read err:", err)
return
}
userInput := string(buf[:cnt-1]) //去掉最后一个回车
if len(userInput) == 4 && string(userInput) == "\\who" {
//用户输入的是who命令
//查询当前所有在线的用户
fmt.Println("Checking all the users online...")
//创建切片存储所有在线用户的name
var userInfos []string
lock.RLock()
for _, user := range allUsers {
userInfo := fmt.Sprintf(user.name)
userInfos = append(userInfos, userInfo)
}
lock.RUnlock()
resWho := strings.Join(userInfos, "\n")
resWho += "\n"
newUser.msg <- resWho
} else if len(userInput) > 9 && userInput[:7] == "\\rename" {
//用户输入的是rename命令
//修改自己的用户名
newUser.name = strings.Split(userInput, "|")[1]
lock.Lock()
allUsers[newUser.id] = newUser
lock.Unlock()
newUser.msg <- "rename successfully\n"
} else {
//用户输入的不是命令 是普通消息 则进行转发即可
fmt.Println("receive data from", newUser.name, userInput)
bufName := fmt.Sprintf("[%s] : %s\n", newUser.name, userInput)
message <- bufName
}
//仍有来自客户端的输入
//发送定时器重置信号 不能超时退出
resetTimer <- true
}
}
func writeBackToClient(user *User, conn net.Conn) {
for data := range user.msg {
_, _ = conn.Write([]byte(data))
}
}
//向所有的用户广播消息 全局唯一的GO程
func broadcast() {
for {
//从message中读取数据
info := <-message
//将数据写入到每一个用户的msg管道中
lock.RLock()
for _, user := range allUsers {
//有缓冲的msg不会阻塞
user.msg <- info
}
lock.RUnlock()
}
}
//启动一个GO程负责监听退出信号
//触发后进行清理工作 delete map close con
func watchExit(user *User, conn net.Conn, isQuit <-chan bool, resetTimer <-chan bool) {
for {
select {
case <-isQuit:
lock.RLock()
logoutInfo := fmt.Sprintf("[%s] is offline\n", allUsers[user.id].name)
fmt.Println("user proactive exit:", allUsers[user.id].name)
delete(allUsers, user.id)
conn.Close()
message <- logoutInfo
lock.RUnlock()
return
case <-time.After(60 * time.Second): //60秒没有输入就超时退出
lock.RLock()
logoutInfo := fmt.Sprintf("[%s] is offline\n", allUsers[user.id].name)
fmt.Println("user timeout exit:", allUsers[user.id].name)
delete(allUsers, user.id)
conn.Close()
message <- logoutInfo
lock.RUnlock()
return
case <-resetTimer:
}
}
}
func main() {
//创建服务器 监听
listener, err := net.Listen("tcp", ":666")
if err != nil {
fmt.Println("net.Listen err:", err)
return
}
//启动全局广播GO程
go broadcast()
for {
fmt.Println("Waiting for a new client...")
conn, err := listener.Accept()
if err != nil {
fmt.Println("listen.Accept err:", err)
return
}
fmt.Println("Accept success!")
// 启动GO程处理业务
go handler(conn)
}
}