内容简介:一个小的并发案例,网络聊天室,在线统计,消息转发。专栏的介绍可以参考《CaseGolang专栏》,代码可以看这里主要是一步步实现一个并发的聊天室,很简单的案例。
一个小的并发案例,网络聊天室,在线统计,消息转发。
专栏的介绍可以参考《CaseGolang专栏》,代码可以看 《宝库-Case》 。
这里主要是一步步实现一个并发的聊天室,很简单的案例。
设计
实现细节可能会冗杂一些,但是大致的实现思路却非常简单。
//总体流程 //server -(处理连接 或者 转发)-> message --->Client.C ---> 读conn.Write给Client端
具体的功能可以按照客户端发送的消息不同,而处理方式不同:
- 已经连接时,向所有客户端显示已经登录
- 客户端发消息要求更改客户端显示名字
- 发送聊天消息则简单转发给所有用户
- 离线时,向所有客户端显示某客户端已经离线
- 查看在线人数或者哪些用户在线
大致思路如下:
案例
上线功能
先测试一下服务器端的功能: (之后再完善)
此阶段性代码如下: (当前只管功能完成,不管是否高效)
//filename: server.go package main import ( "net" "log" ) //用于记录全局在线人员 type Client struct { C chan string //用于给该Client转发消息 Name string //用户名 Addr string //网络地址 } var onlineMap map[string]Client var message = make(chan string) //用于操作 onlineMap 同步控制,同时传输消息 //总体流程 //server -(处理连接 或者 转发)-> message --->Client.C ---> 读conn.Write给Client端 func main() { listener, err := net.Listen("tcp", ":8000") if err != nil { log.Fatalln("net.Listen = ", err) } defer listener.Close() //初始化 map onlineMap = make(map[string]Client) //协程1,用于转发消息给 Client.C go sendMessage2ClientChan() //主协程,循环等待连接 for { conn, err := listener.Accept() if err != nil { log.Println("listen.Accept err = ", err) continue } //处理用户连接: // 1. 新连接加入 map // 2. 开启等待读取 Client.C 写回客户端的协程,等待读取Client.C // 3. 广播该客户端上线了 // 4. 新开协程接收用户发送过来的数据 TODO go HandleConn(conn) } } //死循环,阻塞等待,写消息(写入 Client.C) func sendMessage2ClientChan() { for { msg := <- message //阻塞等待读取消息 for _, cli := range onlineMap { cli.C <- msg //写 } } } //读取 Client.C 转发给具体的 CLient 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() //处理完就关闭连接 //获取客户端信息,加入 map cliAddr := conn.RemoteAddr().String() //作为key ////默认情况 Client.Name 就是地址名字 cli := Client{make(chan string), cliAddr, cliAddr} //把结构体添加到 map onlineMap[cliAddr] = cli //等待读取 Client.C,转发给 Client 对端 go writeMsg2Client(cli, conn) //写 mesage 给具体的 cli.C message <- makeMsg(cli, "上线了。") //开一个协程,读取Client发送过来的数据 //TODO for { }//连接还不能关闭 } //工具方法 func makeMsg(cli Client, str string) (buf string) { buf = "[ " + cli.Addr + " ] " + cli.Name + " : " + str return }
消息转发功能
客户端发送的消息,可以全局显示:
代码也很简单,即 hanldeConn 开个协程专门用来读取客户端写过来的内容,通过 message 转发:
//处理新连接 func HandleConn(conn net.Conn) { //处理用户连接 defer conn.Close() //处理完就关闭连接 //获取客户端信息,加入 map cliAddr := conn.RemoteAddr().String() //作为key ////默认情况 Client.Name 就是地址名字 cli := Client{make(chan string), cliAddr, cliAddr} //把结构体添加到 map onlineMap[cliAddr] = cli //等待读取 Client.C,转发给 Client 对端 go writeMsg2Client(cli, conn) //写 mesage 给具体的 cli.C message <- makeMsg(cli, "上线了。") //新开协程接收用户Client的数据 go func() { buf := make([]byte, 1024*2) for { n, err := conn.Read(buf) if n==0 { //对端断开或者其他连接问题 log.Println("conn.Read err = ", err) return //该协程结束,而不是server结束 } //转发读到的消息 msg := string(buf[:n-1]) //读多少,转发多少 message <- makeMsg(cli, msg) } }() for { }//连接还不能关闭 }
提示自己功能
上线向大家提示一下我上线了,同时显示下 whoami
:
//写 mesage 给具体的 cli.C message <- makeMsg(cli, "上线了。") //写给自己端,提示我的用户是谁 ---- 后面的改名功能 cli.C <- makeMsg(cli, cli.Name + "初来乍到,请多指教") //新开协程接收用户Client的数据
查看用户
万一我想查看一下当前在线的用户怎么办? who
:
代码实现,就是对读取的字符串细化处理:
//转发读到的消息 msg := string(buf[:n-1]) //读多少,转发多少 if len(msg) == 3 && msg == "who" { //遍历 map,给当前用户发送所有成员 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|骨傲天
:
else if len >=8 && msg[:6] == "rename" { //rename|骨傲天 newName := strings.Split(msg, "|")[1] cli.Name = newName onlineMap[cliAddr] = cli //因为map存储的是副本 conn.Write([]byte("rename 完毕。\n")) }
离线功能
如果 server 从 client 读到的信息是对方重置了连接,即表明离线了:
此时怎么办?简单处理如下:(检测到下线了,给大家写一下消息)。
但是实际上,应该更加细分,如果对方是主动退出,如果是连接断开导致的退出。
其实也可能是超时连接,或者长时间连接但不发送消息导致了。( 超时退出,看后面 )
如此一来,考虑用 slelect 比较好:
效果如下:
超时退出
就是在 select 里面加一个分支检测超时,即其他分支不执行,就执行这个分支:
代码如下:
总结
- map 为什么可以并发写?
因为这里是以 ip:port 作为 key 的,一定不会重复(无脏数据)。严格意义上,还是要做好同步控制。
- 先写入 message 在写入 Client.C 的原因?
因为这里的 Client 是一个领域模型对象,代表了不同的客户端;但是 message 代表数据队列。
两者本质上是不同的,通过写入数据管道,最终保存在数据结构中,围绕数据结构展开读写,这是本编程案例的思想。
其次: message
是全局的,而 Client.C
这个消息是单个Client,代码中所有的开启,处理连接的协程都是基于个体Client的,阻塞读取也是基于本 client的,单个 client 的消息之所以会群显示,就是因为遍历单独写入了 Client.C :
//死循环,阻塞等待,写消息(写入 Client.C) func sendMessage2ClientChan() { for { msg := <- message //阻塞等待读取消息 for _, cli := range onlineMap { cli.C <- msg //写 } } }
缺陷:容器里面存入引用&指针才是上策—如果修改的话。
map 里面一开始就应该存储 Client 结构的指针,因为没想到后面想修改其中的内容,所以一开始设计成了值副本。
最后,详细代码放在了 github 上 《gopher宝库》 。
Merlin 最后补充 网络部分(网络和并发联系紧密)
以上所述就是小编给大家介绍的《Golang: 并发聊天室综合案例》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
跨境电商——阿里巴巴速卖通宝典
速卖通大学 编著 / 电子工业出版社 / 2015-1 / 69.00元
跨境电商作为中国电子商务发展的最新趋势,受到了全社会越来越多的重视,大量中国卖家借助阿里巴巴速卖通平台,将产品直接售卖到全球的消费者手中,通过这条短得不能再短的交易链,获得了丰厚的回报。 但同时,跨境电商这一贸易形式,对卖家的综合素质要求比较高:卖家要对海外市场比较熟悉,对跨境物流有所把握,能够用外语进行产品介绍和客户交流,通过跨境结算拿到货款……诸如此类的门槛,让不少新卖家心生畏难,而所有......一起来看看 《跨境电商——阿里巴巴速卖通宝典》 这本书的介绍吧!