Golang: 并发聊天室综合案例

栏目: Go · 发布时间: 5年前

内容简介:一个小的并发案例,网络聊天室,在线统计,消息转发。专栏的介绍可以参考《CaseGolang专栏》,代码可以看这里主要是一步步实现一个并发的聊天室,很简单的案例。

一个小的并发案例,网络聊天室,在线统计,消息转发。

专栏的介绍可以参考《CaseGolang专栏》,代码可以看 《宝库-Case》

这里主要是一步步实现一个并发的聊天室,很简单的案例。

设计

实现细节可能会冗杂一些,但是大致的实现思路却非常简单。

//总体流程
//server -(处理连接 或者 转发)-> message --->Client.C ---> 读conn.Write给Client端

具体的功能可以按照客户端发送的消息不同,而处理方式不同:

  • 已经连接时,向所有客户端显示已经登录
  • 客户端发消息要求更改客户端显示名字
  • 发送聊天消息则简单转发给所有用户
  • 离线时,向所有客户端显示某客户端已经离线
  • 查看在线人数或者哪些用户在线

大致思路如下:

Golang: 并发聊天室综合案例

案例

上线功能

先测试一下服务器端的功能: (之后再完善)

Golang: 并发聊天室综合案例

此阶段性代码如下: (当前只管功能完成,不管是否高效)

//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
}

消息转发功能

客户端发送的消息,可以全局显示:

Golang: 并发聊天室综合案例

代码也很简单,即 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 :

Golang: 并发聊天室综合案例

代码实现,就是对读取的字符串细化处理:

//转发读到的消息
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|骨傲天 :

Golang: 并发聊天室综合案例

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 读到的信息是对方重置了连接,即表明离线了:

Golang: 并发聊天室综合案例

此时怎么办?简单处理如下:(检测到下线了,给大家写一下消息)。

但是实际上,应该更加细分,如果对方是主动退出,如果是连接断开导致的退出。

其实也可能是超时连接,或者长时间连接但不发送消息导致了。( 超时退出,看后面 )

如此一来,考虑用 slelect 比较好:

Golang: 并发聊天室综合案例

效果如下:

Golang: 并发聊天室综合案例

超时退出

就是在 select 里面加一个分支检测超时,即其他分支不执行,就执行这个分支:

Golang: 并发聊天室综合案例

代码如下:

Golang: 并发聊天室综合案例

总结

  • 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元

跨境电商作为中国电子商务发展的最新趋势,受到了全社会越来越多的重视,大量中国卖家借助阿里巴巴速卖通平台,将产品直接售卖到全球的消费者手中,通过这条短得不能再短的交易链,获得了丰厚的回报。 但同时,跨境电商这一贸易形式,对卖家的综合素质要求比较高:卖家要对海外市场比较熟悉,对跨境物流有所把握,能够用外语进行产品介绍和客户交流,通过跨境结算拿到货款……诸如此类的门槛,让不少新卖家心生畏难,而所有......一起来看看 《跨境电商——阿里巴巴速卖通宝典》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

html转js在线工具
html转js在线工具

html转js在线工具