一、模块简述
- (1)主go程:负责监听、接收用户(客户端)连接请求,建立通信关系;同时启动相应的协程处理任务
- (2)处理用户连接协程HandleConnect:负责新上线用户的存储,用户消息读取、发送,用户改名、下线处理及超时处理;为了提高并发效率,同时给一个用户维护多个协程来并行处理上述任务
- (3)用户消息广播协程Manager:负责在线用户遍历,用户消息广播发送,需要与HandleConnect协程及用户子协程协作完成
- (4)协程间应用数据及通信
- map:存储所有登录聊天室的用户信息, key:用户的ip+port。Value:Client结构体
- Client结构体:包含成员:用户名Name,网络地址Addr(ip+port),发送消息的通道C(channel)
- 通道message:协调并发协程间消息的传递
- (5)广播用户上线
- 首先,服务器启动,等待用户建立通信连接。当有用户连接上来,将其存储到map中,这样就维护了一个“在线用户”的列表。当再有新用户连接上来时,应向该列表中所有用户进行广播通知,提示xxx用户上线
- 在go语言中,我们利用协程轻便、高效、并发性好的特性,给每个登录用户维护多个协程来进行数据通信,借助channel不需要使用同步锁,就可以实现高效的并发通信
二、流程分析
- 用户结构体:全局位置定义用户结构体类型 Client,存储登录用户信息。成员包含channel、Name、Addr
type Client struct {
C chan string
Name string
Addr string
}
- 定义全局通道message:处理消息
- 定义全局map:存储在线用户信息。Key为用户网络地址。Value为用户结构体
- 主协程:监听客户端连接请求,当有新的客户端连接,创建新协程handleConnet处理用户连接
- handleConnet协程:获取用户网络地址(Ip+port),创建新用户结构体,包含成员C、Name、Addr。新用户的Name和Addr初值都是用户网络地址(Ip+port)。将用户结构体存入map中。并创建WriteMsgToClient协程,专门负责给当前用户发送消息。组织新用户上线广播消息内容,写入全局通道message中
- WriteMsgToClient协程:读取用户结构体C中的数据,没有则阻塞等待,有数据写出给登录用户
- Manager协程:给map分配空间。循环读取 message 通道中是否有数据。没有,阻塞等待。有则解除阻塞,将message通道中读到的数据写到用户结构体中的C通道
三、源码实现
package main
import (
"fmt"
"net"
"strings"
"time"
)
type Client struct {
C chan string
Name string
Addr string
}
var onlineMap map[string]Client
var message = make(chan string)
func WriteMsgToClient(clnt Client, conn net.Conn) {
for msg := range clnt.C {
conn.Write([]byte(msg + "\n"))
}
}
func MakeMsg(clnt Client, msg string) (buf string) {
buf = "[" + clnt.Addr + "]" + clnt.Name + ": " + msg
return
}
func HandlerConnect(conn net.Conn) {
defer conn.Close()
hasData := make(chan bool)
netAddr := conn.RemoteAddr().String()
clnt := Client{make(chan string), netAddr, netAddr}
onlineMap[netAddr] = clnt
go WriteMsgToClient(clnt, conn)
message <- MakeMsg(clnt, "login")
isQuit := make(chan bool)
go func() {
buf := make([]byte, 4096)
for {
n, err := conn.Read(buf)
if n == 0 {
isQuit <- true
fmt.Printf("检测到客户端:%s退出\n", clnt.Name)
return
}
if err != nil {
fmt.Println("conn.Read err:", err)
return
}
msg := string(buf[:n-1])
if msg == "who" && len(msg) == 3 {
conn.Write([]byte("online user list:\n"))
for _, user := range onlineMap {
userInfo := user.Addr + ":" + user.Name + "\n"
conn.Write([]byte(userInfo))
}
} else if len(msg) >= 8 && msg[:6] == "rename" {
newName := strings.Split(msg, "|")[1]
clnt.Name = newName
onlineMap[netAddr] = clnt
conn.Write([]byte("rename successful\n"))
} else {
message <- MakeMsg(clnt, msg)
}
hasData <- true
}
}()
for {
select {
case <-isQuit:
delete(onlineMap, clnt.Addr)
message <- MakeMsg(clnt, "logout")
return
case <-hasData:
case <-time.After(time.Second * 60):
delete(onlineMap, clnt.Addr)
message <- MakeMsg(clnt, "time out leaved")
return
}
}
}
func Manager() {
onlineMap = make(map[string]Client)
for {
msg := <-message
for _, clnt := range onlineMap {
clnt.C <- msg
}
}
}
func main() {
listener, err := net.Listen("tcp", "127.0.0.1:8000")
if err != nil {
fmt.Println("Listen err", err)
return
}
defer listener.Close()
go Manager()
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("Accept err", err)
return
}
go HandlerConnect(conn)
}
}