zoukankan      html  css  js  c++  java
  • Go语言实践_实现一(服务器端)对多(客户端)在线聊天室

    一、目的

    运用Go语言中的goroutine和通道实现一个简单的一个服务器端对多个客户端的在线聊天

    软件环境:Goland,Go1.9

    代码仓库链接

    二、设计思路

    与一对一的设计思路类似,就是加了个线程的操作。

    1,服务器端声明一个map,并打开监听端口;

    2,客户端打开监听端口,同时连入服务器端;

    3,在客户端上给自己起一个昵称,并输出,同时启动一个线程;

    4,服务器端接收一个昵称,并存入map;

    5,声明一个空的字符串,并写入要群发的消息;

    6,服务器端解析发送的消息(msg_str[0]的值):

    • nick:使该客户端加入聊天室并广播连上服务器端的所有其他客户端;
    • say:广播客户端发出的消息;
    • quit:使该客户端退出,断开与服务器端的连接,并将退出消息广播给其他连上服务器端的所有其他客户端;

    三、Go代码

    Server端

    // one sever to more client chat room
    //This is chat sever
    package main
    
    import (
        "fmt"
        "net"
        "strings"
    )
    
    var ConnMap map[string]net.Conn = make(map[string]net.Conn)  //声明一个集合
    
    //ConnMap := make(map[string]net.Conn)
    
    func main() {
        listen_socket, err := net.Listen("tcp", "127.0.0.1:8000")  //打开监听接口
        if err != nil {
            fmt.Println("server start error")
        }
    
        defer listen_socket.Close()
        fmt.Println("server is wating ....")
    
        for {
            conn, err := listen_socket.Accept()  //收到来自客户端发来的消息
            if err != nil {
                fmt.Println("conn fail ...")
            }
            fmt.Println(conn.RemoteAddr(), "connect successed")
    
            go handle(conn)  //创建线程
        }
    }
    
    func handle(conn net.Conn) {
        for {
            data := make([]byte, 255)  //创建字节流 (此处同 一对一 通信)
            msg_read, err := conn.Read(data)  //声明并将从客户端读取的消息赋给msg_read 和err
            if msg_read == 0 || err != nil {
                continue
            }
    
            //解析协议
            msg_str := strings.Split(string(data[0:msg_read]), "|")  //将从客户端收到的字节流分段保存到msg_str这个数组中
    
            switch msg_str[0] {
            case "nick":  //加入聊天室
                fmt.Println(conn.RemoteAddr(), "-->", msg_str[1])  //nick占在数组下标0上,客户端上写的昵称占在数组下标1上
                for k, v := range ConnMap {  //遍历集合中存储的客户端消息
                    if k != msg_str[1] {
                        v.Write([]byte("[" + msg_str[1] + "]: join..."))
                    }
                }
                ConnMap[msg_str[1]] = conn
            case "say":   //转发消息
                for k, v := range ConnMap {  //k指客户端昵称   v指客户端连接服务器端后的地址
                    if k != msg_str[1] {  //判断是不是给自己发,如果不是
                        fmt.Println("Send "+msg_str[2]+" to ", k)  //服务器端将消息转发给集合中的每一个客户端
                        v.Write([]byte("[" + msg_str[1] + "]: " + msg_str[2]))  //给除了自己的每一个客户端发送自己之前要发送的消息
                    }
                }
            case "quit":  //退出
                for k, v := range ConnMap {  //遍历集合中的客户端昵称
                    if k != msg_str[1] {  //如果昵称不是自己
                        v.Write([]byte("[" + msg_str[1] + "]: quit"))  //给除了自己的其他客户端昵称发送退出的消息,并使Write方法阻塞
                    }
                }
                delete(ConnMap, msg_str[1])  //退出聊天室
            }
        }
    }

    Client端

    // one sever to more client chat room
    //This is chat client
    package main
    
    import (
        "fmt"
        "net"
    )
    
    var nick string = ""  //声明聊天室的昵称
    
    func main() {
        conn, err := net.Dial("tcp", "127.0.0.1:8000")  //打开监听端口
        if err != nil {
            fmt.Println("conn fail...")
        }
        defer conn.Close()
        fmt.Println("client connect server successed 
    ")
    
        //给自己取一个聊天室的昵称
        fmt.Printf("Make a nickname:")
        fmt.Scanf("%s", &nick)  //输入昵称
        fmt.Println("hello : ", nick)  //客户端输出
        conn.Write([]byte("nick|" + nick))  //将信息发送给服务器端
    
        go Handle(conn)  //创建线程
    
        var msg string
        for {
            msg = ""  //声明一个空的消息
            fmt.Scan(&msg)  //输入消息
            conn.Write([]byte("say|" + nick + "|" + msg))  //三段字节流 say | 昵称 | 发送的消息
            if msg == "quit" {  //如果消息为quit
                conn.Write([]byte("quit|" + nick))  //将quit字节流发送给服务器端
                break  //程序结束运行
            }
        }
    }
    
    func Handle(conn net.Conn) {
    
        for {
    
            data := make([]byte, 255)  //创建一个字节流
            msg_read, err := conn.Read(data)  //将读取的字节流赋值给msg_read和err
            if msg_read == 0 || err != nil {  //如果字节流为0或者有错误
                break
            }
    
            fmt.Println(string(data[0:msg_read]))  //把字节流转换成字符串
        }
    }

    四、参考资料

    Split

    五、总结与感受

    着重关注收发消息的判定,收消息后的解包过程和开多线程;注意发消息与收消息时字节流与字符串的转换。

    从初学Go到一对一再到一对多,我已经逐渐体会到使用Go语言做服务器端的方便与强大。

    六、补充:还存在的问题

    昨天把代码发给服务器主程大佬看,他看过后提出以下需要考虑和完善的问题,先忽略程序设计上的问题:

    程序正确性无法保证

    1. Read可能一次性收到两个包,也可能收到半包。出现以上两种情况的时候协议解析都会出现问题。
    2. Write不保证一次调用时全部写完,存在短写的情况。
    3. ConnMap非线程安全。func handle(conn net.Conn)是多线程环境运行的。
    4. 连接出错及正常短开的情况未处理。
  • 相关阅读:
    软件架构自学笔记-- 转载“腾讯数据库专家雷海林分享智能运维架构”
    软件架构自学笔记-- 架构设计与安全控制
    软件架构自学笔记——什么样的架构才是好的架构
    软件架构自学笔记----分享“去哪儿 Hadoop 集群 Federation 数据拷贝优化”
    软件架构自学笔记---架构分析
    软件架构自学笔记——非功能特性
    软件架构自学笔记——常见的软件架构(https://jiajunhuang.com/articles/2018_09_16-common_software_archtecture_pattern.md.html)
    微服务化的基石——持续集成
    微软开源大规模数据处理项目 Data Accelerator
    vs2019 cdkey 秘钥
  • 原文地址:https://www.cnblogs.com/OctoptusLian/p/9406144.html
Copyright © 2011-2022 走看看