• Go语言进阶,并发通道机制搭建一个可注册昵称的聊天室


            聊天室的创建,主要是由两部分组成,服务端和客户端, 新增一个客户端相当于新来一个用户,陆续参与进来进行群聊,服务端就是处理所有客户端的操作然后反馈出去。

    服务端具体作用:一直监听所有客户端的连接net.Conn(有进入、发送消息、退出这样的行为操作),然后通过广播器(自定义一个函数)将客户端的这些操作,广播到其他所有的客户端并显示,这样每个客户端就可以看到其余所有人的聊天状态与信息了。

    其中广播器用到了select的多路复用技术,case判断是什么通道从而进行不同的操作,如果是进入通道,相当于新来一个用户,就进行新增用户操作;如果是信息通道,就将信息发送给全部的客户端;如果是退出通道,说明这个用户离开聊天室,那么就删除这个用户并关闭它的连接。

    客户端比较简单,就类似拨号上网,先连接到服务端,然后进行信息的输入和发送即可。

    这里使用了goroutine并发机制,对于看过本人前面文章的读者来说,就会比较熟悉这个并发,通过创建chan多通道(channel),进行互相独立的通信。

    Go语言进阶,闭包、指针、并发https://blog.csdn.net/weixin_41896770/article/details/127547900

    Go语言并发比较二叉树(Binary Tree)https://blog.csdn.net/weixin_41896770/article/details/127569147

    Go语言进阶,详解并发中的通道机制https://blog.csdn.net/weixin_41896770/article/details/127748316

    尤其建议先看完上面最后一篇Go语言进阶,详解并发中的通道机制,这样对于并发通道,就显得要容易点。

    客户端的IP地址来做用户名的示例

    服务端的代码

    server.go

    1. package main
    2. import (
    3. "bufio"
    4. "fmt"
    5. "log"
    6. "net"
    7. "strings"
    8. )
    9. func main() {
    10. listener, err := net.Listen("tcp", "127.0.0.1:65535")
    11. if err != nil {
    12. log.Fatal(err)
    13. }
    14. // 启动广播器
    15. go broadcaster()
    16. for {
    17. // 将下一个连接返回给侦听器
    18. conn, err := listener.Accept()
    19. if err != nil {
    20. log.Print(err)
    21. continue
    22. }
    23. // 不断处理客户端新的连接
    24. go handleConn(conn)
    25. }
    26. }
    27. // 定义一个名为user_ch的只发送的单通道
    28. type user_ch chan<- string
    29. // 进入通道,退出通道以及信息通道
    30. var (
    31. entry_ch = make(chan user_ch)
    32. exit_ch = make(chan user_ch)
    33. msgs_ch = make(chan string)
    34. )
    35. // 广播器
    36. func broadcaster() {
    37. user_chs := make(map[user_ch]bool)
    38. for {
    39. select {
    40. // 如果是进入通道,就新增一个用户
    41. case user := <-entry_ch:
    42. user_chs[user] = true
    43. // 如果是信息通道,将信息遍历发送给所有客户端
    44. case msg := <-msgs_ch:
    45. for user := range user_chs {
    46. user <- msg
    47. }
    48. // 如果是退出通道,需删除这个客户端然后关闭
    49. case user := <-exit_ch:
    50. delete(user_chs, user)
    51. close(user)
    52. }
    53. }
    54. }
    55. // 处理客户端的连接请求、信息发送、连接关闭的操作
    56. // 使用goroutine机制开启客户端的写操作,将信息写入到连接中
    57. // 其中的net.Conn是一个接口,有读写等方法
    58. func handleConn(conn net.Conn) {
    59. ch := make(chan string)
    60. go userWrite(conn, ch)
    61. // 这里将获取的客户端的IP地址+端口当作用户名
    62. userName := conn.RemoteAddr().String()
    63. ch <- fmt.Sprintf("欢迎 %s,进入聊天室", userName)
    64. entry_ch <- ch //进入的通道
    65. msgs_ch <- fmt.Sprintf("%s,上线了", userName)
    66. // 扫描并读取用户的输入内容,发送给信息通道
    67. scanInfo := bufio.NewScanner(conn)
    68. for scanInfo.Scan() {
    69. // 不能发送空消息
    70. if len(strings.TrimSpace(scanInfo.Text())) == 0 {
    71. continue
    72. }
    73. msgs_ch <- userName + ": " + scanInfo.Text()
    74. }
    75. exit_ch <- ch // 退出的通道
    76. msgs_ch <- fmt.Sprintf("%s,下线了", userName)
    77. conn.Close() //下线记得关闭连接
    78. }
    79. func userWrite(conn net.Conn, ch <-chan string) {
    80. for msg := range ch {
    81. fmt.Fprintln(conn, msg)
    82. }
    83. }

    客户端的代码

    client.go

    1. package main
    2. import (
    3. "io"
    4. "log"
    5. "net"
    6. "os"
    7. )
    8. func main() {
    9. conn, err := net.Dial("tcp", "127.0.0.1:65535")
    10. if err != nil {
    11. log.Fatal(err)
    12. }
    13. // 服务端的通道
    14. server_ch := make(chan struct{})
    15. go func() {
    16. io.Copy(os.Stdout, conn)
    17. log.Println("服务端关闭")
    18. server_ch <- struct{}{} // 往服务端通道发送信息
    19. }()
    20. // 将标准输入的信息写入到conn里
    21. copyInfo(conn, os.Stdin)
    22. conn.Close()
    23. //<-server_ch
    24. }
    25. func copyInfo(dst io.Writer, src io.Reader) {
    26. if _, err := io.Copy(dst, src); err != nil {
    27. log.Fatal(err)
    28. }
    29. }

    效果如图:

    这里是使用了IP+端口来标识用户,也可以使用自定义昵称进入,在进入前注册一个,我们仿照handleConn这个函数来写一个注册用户名的函数userRegiste,判断输入的用户名是否重复,然后返回用户名。

    注册用户名的示例

    server.go

    1. package main
    2. import (
    3. "bufio"
    4. "fmt"
    5. "log"
    6. "net"
    7. "strings"
    8. )
    9. func main() {
    10. listener, err := net.Listen("tcp", "127.0.0.1:65535")
    11. if err != nil {
    12. log.Fatal(err)
    13. }
    14. // 启动广播器
    15. go broadcaster()
    16. for {
    17. // 将下一个连接返回给侦听器
    18. conn, err := listener.Accept()
    19. if err != nil {
    20. log.Print(err)
    21. continue
    22. }
    23. // 不断处理客户端新的连接
    24. go handleConn(conn)
    25. }
    26. }
    27. // 定义一个名为user_ch的只发送的单通道
    28. // 修改处:定义个用户名的结构体
    29. type user_ch chan<- string
    30. type userInfo struct {
    31. name string
    32. ch user_ch
    33. }
    34. // 进入通道,退出通道以及信息通道
    35. // 修改处:进入和退出的通道,使用了userInfo结构体类型
    36. var (
    37. entry_ch = make(chan userInfo)
    38. exit_ch = make(chan userInfo)
    39. msgs_ch = make(chan string)
    40. )
    41. // 广播器
    42. func broadcaster() {
    43. user_chs := make(map[string]user_ch)
    44. for {
    45. select {
    46. case user := <-register:
    47. // 先判断新用户名是否有重复
    48. _, ok := user_chs[user.name]
    49. user.ch <- !ok
    50. // 如果是进入通道,就新增一个用户
    51. case user := <-entry_ch:
    52. var usernames []string
    53. for username := range user_chs {
    54. //if username == user.name {
    55. // user.ch <- fmt.Sprintf("其他用户已使用: %s", strings.Join(usernames, "; "))
    56. //}
    57. usernames = append(usernames, username)
    58. }
    59. user_chs[user.name] = user.ch
    60. // 如果是信息通道,将信息遍历发送给所有客户端
    61. case msg := <-msgs_ch:
    62. for _, user := range user_chs {
    63. user <- msg
    64. }
    65. // 如果是退出通道,需删除这个用户然后关闭它的通道
    66. case user := <-exit_ch:
    67. delete(user_chs, user.name)
    68. close(user.ch)
    69. }
    70. }
    71. }
    72. // 处理客户端的连接请求、信息发送、连接关闭的操作
    73. // 使用goroutine机制开启客户端的写操作,将信息写入到连接中
    74. // 其中的net.Conn是一个接口,有读写等方法
    75. func handleConn(conn net.Conn) {
    76. userName := userRegiste(conn) //修改处:注册用户名的操作
    77. ch := make(chan string)
    78. go userWrite(conn, ch)
    79. ch <- fmt.Sprintf("欢迎 %s,进入聊天室", userName)
    80. // 修改处:进入和退出的通道需要使用userInfo的结构体
    81. userinfo := userInfo{userName, ch}
    82. entry_ch <- userinfo //进入的通道
    83. msgs_ch <- fmt.Sprintf("%s,上线了", userName)
    84. // 扫描并读取用户的输入内容,发送给信息通道
    85. scanInfo := bufio.NewScanner(conn)
    86. for scanInfo.Scan() {
    87. // 不能发送空消息
    88. if len(strings.TrimSpace(scanInfo.Text())) == 0 {
    89. continue
    90. }
    91. msgs_ch <- userName + ": " + scanInfo.Text()
    92. }
    93. exit_ch <- userinfo // 退出的通道
    94. msgs_ch <- fmt.Sprintf("%s,下线了", userName)
    95. conn.Close() //下线记得关闭连接
    96. }
    97. func userWrite(conn net.Conn, ch <-chan string) {
    98. for msg := range ch {
    99. fmt.Fprintln(conn, msg)
    100. }
    101. }
    102. // 修改处:定义一个注册用户名的结构体
    103. type registeInfo struct {
    104. name string
    105. ch chan<- bool
    106. }
    107. // 新建一个注册用户名的通道
    108. var register = make(chan registeInfo)
    109. func userRegiste(conn net.Conn) (uname string) {
    110. ch := make(chan bool)
    111. fmt.Fprint(conn, "输入昵称: ")
    112. scanInfo := bufio.NewScanner(conn)
    113. for scanInfo.Scan() {
    114. if len(strings.TrimSpace(scanInfo.Text())) == 0 {
    115. continue
    116. }
    117. uname = scanInfo.Text()
    118. register <- registeInfo{uname, ch}
    119. if <-ch {
    120. break
    121. }
    122. fmt.Fprintf(conn, "昵称:%q,已存在!\r\n换一个昵称: ", uname)
    123. }
    124. return uname
    125. }

  • 相关阅读:
    浏览器工作原理与实践--浏览上下文组:如何计算Chrome中渲染进程的个数
    计算机操作系统 第三章:处理机调度与死锁(2)
    mxnet gluon GRU 文档
    docker(七)SpringBoot集群搭建 Nginx负载
    Python基础
    图论——基础概念
    静态路由与双线BFD热备份
    hdu 1052(田忌赛马 贪心算法,sort排序)
    华为 IoTDA(物联网平台)如何使用Python SDK 实现应用侧连接
    rviz建图拉点导航
  • 原文地址:https://blog.csdn.net/weixin_41896770/article/details/127768054