一个小的并发案例,网络聊天室,在线统计,消息转发。
专栏的介绍可以参考 《CaseGolang专栏》,代码可以看《宝库-Case》。
这里主要是一步步实现一个并发的聊天室,很简单的案例。
设计
实现细节可能会冗杂一些,但是大致的实现思路却非常简单。
1 2
| //总体流程 //server -(处理连接 或者 转发)-> message --->Client.C ---> 读conn.Write给Client端
|
具体的功能可以按照客户端发送的消息不同,而处理方式不同:
- 已经连接时,向所有客户端显示已经登录
- 客户端发消息要求更改客户端显示名字
- 发送聊天消息则简单转发给所有用户
- 离线时,向所有客户端显示某客户端已经离线
- 查看在线人数或者哪些用户在线
大致思路如下:
案例
上线功能
先测试一下服务器端的功能: (之后再完善)
此阶段性代码如下: (当前只管功能完成,不管是否高效)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
|
package main
import ( "net" "log" )
type Client struct { C chan string Name string Addr string }
var onlineMap map[string]Client
var message = make(chan string)
func main() { listener, err := net.Listen("tcp", ":8000") if err != nil { log.Fatalln("net.Listen = ", err) } defer listener.Close()
onlineMap = make(map[string]Client)
go sendMessage2ClientChan()
for { conn, err := listener.Accept() if err != nil { log.Println("listen.Accept err = ", err) continue }
go HandleConn(conn)
} }
func sendMessage2ClientChan() { for { msg := <- message
for _, cli := range onlineMap { cli.C <- msg } } }
func writeMsg2Client(cli Client, conn net.Conn) { for msg := range cli.C { conn.Write([]byte(msg + "\n")) } }
func HandleConn(conn net.Conn) { defer conn.Close()
cliAddr := conn.RemoteAddr().String() cli := Client{make(chan string), cliAddr, cliAddr}
onlineMap[cliAddr] = cli
go writeMsg2Client(cli, conn)
message <- makeMsg(cli, "上线了。")
for {
}
}
func makeMsg(cli Client, str string) (buf string) { buf = "[ " + cli.Addr + " ] " + cli.Name + " : " + str return }
|
消息转发功能
客户端发送的消息,可以全局显示:
代码也很简单,即 hanldeConn 开个协程专门用来读取客户端写过来的内容,通过 message 转发:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| func HandleConn(conn net.Conn) { defer conn.Close()
cliAddr := conn.RemoteAddr().String() cli := Client{make(chan string), cliAddr, cliAddr}
onlineMap[cliAddr] = cli
go writeMsg2Client(cli, conn)
message <- makeMsg(cli, "上线了。")
go func() { buf := make([]byte, 1024*2) for { n, err := conn.Read(buf) if n==0 { log.Println("conn.Read err = ", err) return } msg := string(buf[:n-1]) message <- makeMsg(cli, msg) } }()
for {
} }
|
提示自己功能
上线向大家提示一下我上线了,同时显示下 whoami
:
1 2 3 4 5 6
| message <- makeMsg(cli, "上线了。")
cli.C <- makeMsg(cli, cli.Name + "初来乍到,请多指教")
|
查看用户
万一我想查看一下当前在线的用户怎么办?who
:
代码实现,就是对读取的字符串细化处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| msg := string(buf[:n-1])
if len(msg) == 3 && msg == "who" { conn.Write([]byte("-----------\n用户列表如下: \n")) for _, tmp := range onlineMap { usrStr := tmp.Addr + ":" + tmp.Name +"\n" conn.Write([]byte(usrStr)) } conn.Write([]byte("-----------\n"))
} else { message <- makeMsg(cli, msg) }
|
改名功能
我想改名,显示一个吊炸天的名字。发送 rename|骨傲天
:
1 2 3 4 5 6
| else if len >=8 && msg[:6] == "rename" { newName := strings.Split(msg, "|")[1] cli.Name = newName onlineMap[cliAddr] = cli conn.Write([]byte("rename 完毕。\n")) }
|
离线功能
如果 server 从 client 读到的信息是对方重置了连接,即表明离线了:
此时怎么办?简单处理如下:(检测到下线了,给大家写一下消息)。
但是实际上,应该更加细分,如果对方是主动退出,如果是连接断开导致的退出。
其实也可能是超时连接,或者长时间连接但不发送消息导致了。(超时退出,看后面)
如此一来,考虑用 slelect 比较好:
效果如下:
超时退出
就是在 select 里面加一个分支检测超时,即其他分支不执行,就执行这个分支:
代码如下:
总结
因为这里是以 ip:port 作为 key 的,一定不会重复(无脏数据)。严格意义上,还是要做好同步控制。
- 先写入 message 在写入 Client.C 的原因?
因为这里的 Client 是一个领域模型对象,代表了不同的客户端;但是 message 代表数据队列。
两者本质上是不同的,通过写入数据管道,最终保存在数据结构中,围绕数据结构展开读写,这是本编程案例的思想。
其次: message
是全局的,而 Client.C
这个消息是单个Client,代码中所有的开启,处理连接的协程都是基于个体Client的,阻塞读取也是基于本 client的,单个 client 的消息之所以会群显示,就是因为遍历单独写入了 Client.C :
1 2 3 4 5 6 7 8 9 10
| func sendMessage2ClientChan() { for { msg := <- message
for _, cli := range onlineMap { cli.C <- msg } } }
|
缺陷:容器里面存入引用&指针才是上策—如果修改的话。
map 里面一开始就应该存储 Client 结构的指针,因为没想到后面想修改其中的内容,所以一开始设计成了值副本。
最后,详细代码放在了 github 上 《gopher宝库》 。
Merlin 最后补充 网络部分(网络和并发联系紧密)