内容简介:一个小的并发案例,网络聊天室,在线统计,消息转发。专栏的介绍可以参考《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: 并发聊天室综合案例》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
数据压缩导论(第4版)
[美] Khalid Sayood / 贾洪峰 / 人民邮电出版社 / 2014-1 / 129.00
数据压缩已经成为信息革命的一门支撑技术,这场革命已经改变了我们的生活,而在此过程中,数据压缩也变得几乎无处不在。从MP3播放器到智能手机,再到数字电视和数字电影,数据压缩几乎成了所有信息技术的必备要素。 近年来,以大数据为标志的互联网技术高歌猛进。数据规模大、产生速度快、来源多样等特性,导致数据存储和处理都前所未有地复杂。《数据压缩导论(第4版)》作为迄今为止数据压缩领域最全面而深入的著作,......一起来看看 《数据压缩导论(第4版)》 这本书的介绍吧!