只用golang完成简单的并发聊天室

流程图如下

image-20220924093450972

聊天室模块划分:

主go程:
创建监听socket。 for 循环 Accept() 客户端连接 —— conn。 启动 go 程 HandlerConnect:

HandlerConnect:

​ 创建用户结构体对象。 存入 onlineMap。发送用户登录广播、聊天消息。处理查询在线用户、改名、下线、超时提出。

Manager:
监听 全局 channel message, 将读到的消息 广播给 onlineMap 中的所有用户。

WriteMsgToClient:

​ 读取 每个用户自带 channel C 上消息(由Manager发送该消息)。回写给用户。

全局数据模块:

​ 用户结构体: Client { C、Name、Addr string }

​ 在现用户列表: onlineMap[string]Client key: 客户端IP+port value: Client

​ 消息通道: message

HandlerConnect不直接读取map中的channel是因为channel主要功能是创建新用户结构体存入map中如果同时负责读,那么没有信息的时候会造成堵塞,不能创建新用户。


广播用户上线:

  1. 主go程中,创建监听套接字。 记得defer

  2. for 循环监听客户端连接请求。Accept()

  3. 有一个客户端连接,创建新 go 程 处理客户端数据 HandlerConnet(conn) defer

  4. 定义全局结构体类型 C 、Name、Addr

  5. 创建全局map、channel

  6. 实现HandlerConnet, 获取客户端IP+port —— RemoteAddr()。 初始化新用户结构体信息。 name == Addr

  7. 创建 Manager 实现管理go程。 —— Accept() 之前。

  8. 实现 Manager 。 初始化 在线用户 map。 循环 读取全局 channel,如果无数据,阻塞。 如果有数据, 遍历在线用户 map ,将数据写到 用户的 C 里

  9. 将新用户添加到 在线用户 map 中 。 Key == IP+port value= 新用户结构体

  10. 创建 WriteMsgToClient go程,专门给当前用户写数据。 —— 来源于 用户自带的 C 中

  11. 实现 WriteMsgToClient(clnt,conn) 。遍历自带的 C ,读数据,conn.Write 到 客户端。

  12. HandlerConnet中,结束位置,组织用户上线信息, 将 用户上线信息 写 到全局 channel —— Manager 的读就被激活(原来一直阻塞)

  13. HandlerConnet中,结尾 加 for

// 创建用户结构体类型
type User struct {
C chan string
Name string
Addr string
}
// 创建全局map存储在线用户
var onlineMap map[string]User
// 创建全局channel传递用户信息
var message = make(chan string)
func Manager() {
// 初始化map
onlineMap = make(map[string]User)
//监听全局channel中是否有数据
for {
msg := <-message
// 循环发送信息个所有在线用户
for _, user := range onlineMap {
user.C <- msg
}
}
}
func WriteMsgToUser(user User, conn net.Conn) {
//遍历用户map中的channel看看是否有信息 有信息发送给用户(同时的情况下每个user的channnel信息是一样的 )
for msg := range user.C {
conn.Write([]byte(msg + "\n"))
}
}
func HandlerConnect(conn net.Conn) {
// 将关闭加入栈
defer conn.Close()
//获取连接用户的信息
netAddr := conn.RemoteAddr().String()
//创建用户的结构体信息,默认用户名和密码是IP+port
clnt := User{make(chan string), netAddr, netAddr}
// 将新连接用户添加到map中,key-IP+port value-User结构体
onlineMap[netAddr] = clnt
// 创建专门用来给当前用户发送消息的go程
go WriteMsgToUser(clnt, conn)
// 发送用户上线消息到全局channel中
message <- "[" + netAddr + "]" + clnt.Name + "login"
for {
}
}
func main() {
// 创建监听socket
listener, err := net.Listen("tcp", "127.0.0.1:8005")
if err != nil {
fmt.Println("net.Listen err :", err)
return
}
defer listener.Close()
//创建管理者go程Manager 管理map和全局channel
go Manager()
// 并发 三次握手conn
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("listener.Accept err :", err)
return
}
//创建go程处理客户端请求
go HandlerConnect(conn)
}
}

广播用户信息

和广播用户上线信息基本上同理,也是由HandlerConnect监听接收用户数据传递给Message,只是单个用户是可以多次发送消息的所以需要循环监听这里直接使用了匿名go程(当然可以用具体的函数)

同时考虑到信息的格式基本是一样的只是内容不一样所以封装了一个消息格式化函数MakeMsg,用于格式化消息

具体添加代码如下

HandlerConnect:

message <- MakeMsg(clnt, "login")
//创建一个匿名go程专门处理用户发送的消息
go func() {
buf := make([]byte, 4096)
for {
n, err := conn.Read(buf)
if n == 0 {
fmt.Println("检测到" + clnt.Name + "已断开连接")
return
}
if err != nil {
fmt.Println("conn.Read err :", err)
return
}
//将用户的信息写入到message中
message <- MakeMsg(clnt, string(buf[:n]))
}
}()

MakeMsg:

// 制作个消息格式化函数
func MakeMsg(user User, msg string) string {
buf := "[" + user.Addr + "]" + user.Name + ": " + msg
return buf
}

查询在线用户

用户可以根据具体的请求看到目前的在写列表(只有他自己能看)

其实想想很简单,当HandlerConnect:在循环监听信息时(将切片后自带的\n切掉),监听到了关键词-wio则直接遍历map回写给socket否则写给全局message

代码修改如下:

//切掉自带的\n
msg := string(buf[:n-1])
//提取在线用户列表,将信息写到对应的socket中
if msg == "wio" && len(msg) == 3 {
conn.Write([]byte("online user list:\n"))
for _, value := range onlineMap {
userInfo := value.Addr + ": " + value.Name + "\n"
conn.Write([]byte(userInfo))
}
} else {
//将用户的信息写入到message中
message <- MakeMsg(clnt, msg)
}

image-20220925192214089

聊天室在线改名

和查询用户差不多,根据特有的指令给自己的用户名改名

监听到rename|+name后

1.先截取新名字

2.将新名字存到现有用户对应的结构体中

3.将结构体更新到map中(相同的key会覆盖原内容)

代码如下:

// 判断用户是否发送了改名命令
} else if msg[:6] == "rename" && len(msg) >= 8 {
//slip返回切片这里选取切片中的第二个
newName := strings.Split(msg, "|")[1] //msg[8:]
clnt.Name = newName //修改结构体成员name
onlineMap[netAddr] = clnt //更新map中的对应值
conn.Write([]byte("rename successful:" + newName + "\n"))
} else {
//将用户的信息写入到message中

image-20220925194900409

用户退出

用户关闭窗口后,我们之前判断的即读到的信息是零

image-20220925195844156

只会在控制台打印信息

现在我们完善其功能:

1.将用户从在线列表移除delete()函数

2.广播退出信息

3.结束对应的go程

代码如下:

在 用户成功登陆之后, 创建监听 用户退出的 channel —— isQuit

  1. 当 conn.Read == 0 , isQuit <- true

  2. 在 HandlerConnet 结尾 for 中, 添加 select 监听 <-isQuit

  3. 条件满足。 将用户从在线列表移除。 组织用户下线消息,写入 message (广播)

HandlerConnect

//开头定义管道控制退出
isquit := make(chan bool)
if n == 0 {
isquit <- true
fmt.Println("检测到" + clnt.Name + "已断开连接")
return
}
for {
select {
case <-isquit:
delete(onlineMap, clnt.Addr) //将用户从在线列表map移除-删除key
message <- MakeMsg(clnt, "logout") //广播用户退出
return //结束相关的所有go程
}
}

image-20220925201352606

超时处理

长时间不操作就强制退出

  1. 在 select 中 监听定时器。(time.After())计时到达。将用户从在线列表移除。 组织用户下线消息,写入 message (广播)

  2. 创建监听 用户活跃的 channel —— hasData

  3. 只用户执行:聊天、改名、who 任意一个操作,hasData<- true

  4. 在 select 中 添加监听 <-hasData。 条件满足,不做任何事情。 目的是重置计时器。

//创建channel判断用户是否活跃
hasMsg := make(chan bool)
} else {
//将用户的信息写入到message中
message <- MakeMsg(clnt, msg)
}
hasMsg <- true
case <-hasMsg:
//什么都不做,目的是重置下方计时器
case <-time.After(time.Second * 10):
delete(onlineMap, clnt.Addr) //将用户从在线列表map移除-删除key
message <- MakeMsg(clnt, "logout") //广播用户退出
return //结束相关的所有go程//结束相关的所有go程

最后实现代码如下

服务器:

package main
import (
"fmt"
"net"
"strings"
"time"
)
// 创建用户结构体类型
type User struct {
C chan string
Name string
Addr string
}
// 创建全局map存储在线用户
var onlineMap map[string]User
// 创建全局channel传递用户信息
var message = make(chan string)
func Manager() {
// 初始化map
onlineMap = make(map[string]User)
//监听全局channel中是否有数据
for {
msg := <-message
// 循环发送信息个所有在线用户
for _, user := range onlineMap {
user.C <- msg
}
}
}
func WriteMsgToUser(user User, conn net.Conn) {
//遍历用户map中的channel看看是否有信息 有信息发送给用户(同时的情况下每个user的channnel信息是一样的 )
for msg := range user.C {
conn.Write([]byte(msg + "\n"))
}
}
// 制作个消息格式化函数
func MakeMsg(user User, msg string) string {
buf := "[" + user.Addr + "]" + user.Name + ": " + msg
return buf
}
func HandlerConnect(conn net.Conn) {
// 将关闭加入栈
defer conn.Close()
isquit := make(chan bool)
//创建channel判断用户是否活跃
hasMsg := make(chan bool)
//获取连接用户的信息
netAddr := conn.RemoteAddr().String()
//创建用户的结构体信息,默认用户名和密码是IP+port
clnt := User{make(chan string), netAddr, netAddr}
// 将新连接用户添加到map中,key-IP+port value-User结构体
onlineMap[netAddr] = clnt
// 创建专门用来给当前用户发送消息的go程
go WriteMsgToUser(clnt, conn)
// 发送用户上线消息到全局channel中
//message <- "[" + netAddr + "]" + clnt.Name + "login"
message <- MakeMsg(clnt, "login")
//创建一个匿名go程专门处理用户发送的消息
go func() {
buf := make([]byte, 4096)
for {
n, err := conn.Read(buf)
if n == 0 {
isquit <- true
fmt.Println("检测到" + clnt.Name + "已断开连接")
return
}
if err != nil {
fmt.Println("conn.Read err :", err)
return
}
msg := string(buf[:n-1])
//提取在线用户列表,将信息写到对应的socket中
if msg == "wio" && len(msg) == 3 {
conn.Write([]byte("online user list:\n"))
for _, value := range onlineMap {
userInfo := value.Addr + ": " + value.Name + "\n"
conn.Write([]byte(userInfo))
}
// 判断用户是否发送了改名命令
} else if len(msg) >= 8 && msg[:6] == "rename" {
//slip返回切片这里选取切片中的第二个
newName := strings.Split(msg, "|")[1] //msg[8:]
clnt.Name = newName //修改结构体成员name
onlineMap[netAddr] = clnt //更新map中的对应值
conn.Write([]byte("rename successful:" + newName + "\n"))
} else {
//将用户的信息写入到message中
message <- MakeMsg(clnt, msg)
}
hasMsg <- true
}
}()
for {
select {
case <-isquit:
delete(onlineMap, clnt.Addr) //将用户从在线列表map移除-删除key
message <- MakeMsg(clnt, "logout") //广播用户退出
return
case <-hasMsg:
//什么都不做,目的是重置下方计时器
case <-time.After(time.Second * 10):
delete(onlineMap, clnt.Addr) //将用户从在线列表map移除-删除key
message <- MakeMsg(clnt, "logout") //广播用户退出
return //结束相关的所有go程//结束相关的所有go程
}
}
}
func main() {
// 创建监听socket
listener, err := net.Listen("tcp", "127.0.0.1:8005")
if err != nil {
fmt.Println("net.Listen err :", err)
return
}
defer listener.Close()
//创建管理者go程Manager 管理map和全局channel
go Manager()
// 并发 三次握手conn
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("listener.Accept err :", err)
return
}
//创建go程处理客户端请求
go HandlerConnect(conn)
}
}

流程总结如下

image-20220926102436756

补充

一个go程中的子go程不会随着主go程的关闭而关闭,所以昨天我们在select中忽略了HandlerConnect中的子go程

image-20220926103528268

只要让其跳出循环就好了

image-20220926103602380

posted @   Ember00  阅读(51)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
历史上的今天:
2021-10-27 微信小程序开发基础1-全局页面配置
点击右上角即可分享
微信分享提示