需求:
1、支持多用户聊天
2、用户上线、退出要在(聊天室通知)
3、修改昵称(不在聊天室通知)
4、规定时间内不活跃的用户强制踢出(聊天室通知)
5、查询在线用户列表(不在聊天室通知)
服务端:
package main
import (
"fmt"
"log"
"net"
"time"
)
func main() {
// 1、监听地址的连接请求,当有新地连接请求到来时,创建Accept实现连接
listen, err := net.Listen("tcp", "127.0.0.1:8080")
ErrorFunc("net.Listen:", err)
defer listen.Close()
// 创建管理者go程,管理map和全局channel,这个是负责广播的,所以要在用户连接前创建好
go Message()
// 2、循环监听客户端连接请求
for {
accept, err := listen.Accept()
ErrorFunc("listen.Accept:", err)
// 启动Go程处理客户端数据请求
go HandlerConnect(accept)
}
}
// 负责读channel的数据,通知所有在线用户***用户上线信息
func Message() {
// 初始化map
OnlineMap = make(map[string]User)
// 循环监听全局channel,读不到就阻塞,不循环的话就只能读一遍,后续的用户上线就读不到了
for {
msg := <-MessageChannel
// 遍历在线用户,通知所有在线用户 clnt:Map的value,也就是User结构体
for _, clnt := range OnlineMap {
clnt.ch <- msg
}
}
}
// 3、创建用户结构体,存储登录用户信息
type User struct {
ch chan string //
Name string // 用户名
Addr string // 用户ip、端口
}
// 4、创建全局map,存储在线的用户 key:用户的ip、端口号 value:用户信息
var OnlineMap map[string]User
// 5、每个用户连接成功后向通道发送数据, 由message函数负责广播上线用户消息
var MessageChannel = make(chan string)
/*
存储用户信息、读取用户信息、修改名称、退出、异常(超时)处理
起个go程 将用户上线信息写到Message通道中
*/
func HandlerConnect(accept net.Conn) {
defer accept.Close()
// 检测用户是否活跃
hasData := make(chan bool)
// 6、获取用户ip、端口
addr := accept.RemoteAddr().String()
// 走到这里说明用户已经连接,因为下面要给子go程写用户信息,所以要初始化user结构体
user := User{make(chan string), "", addr}
// 将新连接的用户添加到在线的map,创建Map由Message处理,key:ip+端口 value:User结构体
OnlineMap[addr] = user
// 创建用来给当前用户发送消息的go程,并负责读取通道中的数据(在线用户)
go WriteMessage(accept, user)
// 发送用户消息到全局channel
MessageChannel <- MakeMessage(user, "上线了")
// 判断用户退出的channel
isQuit := make(chan bool)
// 创建一个go程,读取用户发送的聊天消息,写到全局channel
go func() {
bytes := make([]byte, 4096)
for {
n, err := accept.Read(bytes)
ErrorFunc("accept.Read:", err)
if n == 0 {
// 用户退出后写到channel
isQuit <- true
fmt.Printf("检测到客户端:%s 已退出\n", user.Name)
return
}
// 如果读到用户消息,就广播给所有在线用户,-1是因为使用了netcat模拟用户端,所有的指令都是string,后面会带一个\n,-1是把后面的\n删除掉
msg := string(bytes[:n-1])
// 判断用户端发的消息是否为查询在线用户列表指令,且仅有用户指令,不带有其它字符
if msg == "查询在线用户" && len(msg) == 18 {
accept.Write([]byte("在线用户列表为:\n"))
// 遍历当前OnlineMap
for _, user := range OnlineMap {
userInfo := user.Addr + ":" + user.Name + "\n"
accept.Write([]byte(userInfo))
}
// 有\n 要再加两个字节
} else if len(msg) >= 14 && msg[:12] == "修改昵称" {
user.Name = msg[13:]
// 修改昵称后更新到在线Map
OnlineMap[user.Addr] = user
accept.Write([]byte("修改昵称成功\n"))
} else {
MessageChannel <- MakeMessage(user, msg)
}
// 只要执行到这里,就说明用户活跃
hasData <- true
}
}()
for {
select {
case <-isQuit:
// 将当前用户从在线列表中移除,要先删除用户,不然会广播给自己
delete(OnlineMap, user.Addr)
// 广播给在线用户**用户退出
MessageChannel <- MakeMessage(user, "退出了")
return
case <-hasData:
// 读到数据什么都不做,重置下面的计时器
case <-time.After(time.Second * 1000):
// 将当前用户从在线列表中移除,要先删除用户,不然会广播给自己
delete(OnlineMap, user.Addr)
// 广播给在线用户**用户退出
MessageChannel <- MakeMessage(user, "退出了")
return
}
}
}
func MakeMessage(u User, msg string) (str string) {
str = "【" + u.Addr + "】" + u.Name + " : " + msg
return
}
// 读取用户上线信息
func WriteMessage(accept net.Conn, u User) {
// 监听用户channel上是否有消息
for msg := range u.ch {
// 不加\n容易阻塞,写的消息发不出去
accept.Write([]byte(msg + "\n"))
}
}
func ErrorFunc(info string, err error) {
if err != nil {
log.Println(info, err)
return
}
}
客户端:
客户端不写了,使用Netcat模拟就行了