3.22 Go之聊天服务器

3.22 Go之聊天服务器

项目结构

  • 服务端

    • 一个主goroutine-->监听端口,每一个链接创建一个新的handleConngoroutine

    • 一个广播(broadcaster)goroutine--->使用select对消息进行响应

    • 一个(handleConn)goroutine--->用户连接处理

    • 一个写入(clientWriter)goroutine

  • 客户端

    • 一个简单的TCP服务读/写客户端

  • 同步机制使用通道而非java等程序的多线程

示例代码

服务器端:

广播部分
// 广播goroutine
func broadCaster() {
   // 收集客户端
   clients := make(map[client]bool)

   // 循环结合select选择器将消息发送
   for {
       select {
       case msg := <-messages:
          // 如果成功的读取出客户通道内信息则广播出去
          /* 将收到的信息广播 */
          for cli := range clients {
              // 将消息写入
              cli <- msg
          }
           /* 判断是否进入服务端 */
       case cli := <-entering:
           // 改变客户端的布尔量
           clients[cli] = true
           /* 判断客户端是否离开服务器 */
       case cli := <-leaving:
           // 离开了就删除
           delete(clients, cli)
           // 关闭客户端
           close(cli)
      }
  }
}

分析:

使用字典保护用户clients,字典的key是各连接申明的单向并发队列

使用select开启多路复用:

  • 每当有广播消息从messages发送进来,都会循环clients对里面的每个channel发消息。

  • 每当有消息从entering里面发送过来,就生成一个新的key - value,给clients里面增加一个新的client

  • 每当有消息从leaving里面发送过来,就删掉这个key - value对,并关闭对应的channel

客户端部分
// 客户写入goroutine
func clientWriter(conn net.Conn, ch <-chan string) {
   // 循环获取通道中的值
   for msg := range ch {
       // 打印连接结果和通道信息
       fmt.Fprintln(conn, msg)
  }
}

分析:

  • 创建一个对外发送消息的新通道,然后通过entering通道通知广播者新客户到来

  • 读取客户发来的每一行文本,通过全局接收消息通道将每一行发送给广播者

  • 客户端读取完毕消息,handleConn通过leaving通道通知客户离开,然后关闭连接

客户端连接交互部分
// 用户连接客户端
func handleConn(conn net.Conn) {
   // 对外发送客户信息的通道
   ch := make(chan string)

   // 开启一个客户端协程
   go clientWriter(conn, ch)

   who := conn.RemoteAddr().String()
   // 同时写入三个通道当中--->客户消息、服务端客户记录、进入通道
   ch <- "欢迎:" + who
   messages <- who + "上线"
   entering <- ch

   // 记录输入
   input := bufio.NewScanner(conn)
   // 循环监听输入--->记录聊天内容
   for input.Scan() {
       // 写入信息通道
       messages <- who + ":" + input.Text()
  }

   // 记录下线
   leaving <- ch
   messages <- who + "下线!"
   // 关闭连接
   conn.Close()
}
服务端主程序
// main函数总调用
func main() {
   conn, err := net.Dial("tcp", "localhost:8000")
   // 判断错误信息
   if err != nil {
       // 输出日志
       log.Fatal(err)
  }

   // 创建一个完成通道
   done := make(chan struct{})

   // 开启协程
   go func() {
       // 忽略错误
       io.Copy(os.Stdout, conn)
       // 日志打印
       log.Println("done")
       // 告诉主goroutine
       done <- struct{}{}
  }()

   // 调用读写函数
   mustCopy(conn, os.Stdout)
   // 连接关闭
   conn.Close()
   // 通道接收--->等待后台goroutine完成
   <-done
}

分析:

获得listener对象,然后不停的获取链接上来的conn对象,最后把这些对象丢给处理链接函数去进行处理。

在使用handleConn方法处理conn对象的时候,对不同的链接都启一个goroutine去并发处理每个conn这样无需等待。

posted @ 2022-03-23 10:27  俊king  阅读(78)  评论(0)    收藏  举报