5.7 Go语言项目实战:多人聊天室2.0

Amiee7 · 2019-03-17 17:40:25 · 3700 次点击 · 大约8小时之前 开始浏览    置顶
这是一个创建于 2019-03-17 17:40:25 的主题,其中的信息可能已经有所发展或是发生改变。

本篇文章IT兄弟连GO语言学院小美

给读者们分享一下Go语言项目实战:多人聊天室2.0

对GO语言感兴趣想要学习Golang开发技术的小伙伴就随小编来了解一下吧。

需求描述

  • 申请建群:向服务端发送聊天消息“建群#群昵称”
  • 服务端审核:如果没有同名群存在,就返回“建群成功”否则返回“群已存在”
  • 创建群结构体:属性包括群主、群昵称、群成员;
  • 查看群信息:任何人可以通过向服务端发送聊天信息“群信息#所有”或者“群信息#昵称”查看群信息
  • 服务端返回:服务端返回格式

    • [x] 群昵称:xxx
    • [x] 群主:xxx
    • [x] 群人数:xxx
  • 申请加群:任何人可以通过向服务端发送聊天信息“加群#群昵称”申请加群
  • 加群审核:服务端将加群信息转发给群主,由群主确认,有服务端转发结果
  • 群聊:群成员向服务端发送消息“群信息-群昵称#信息内容”,服务端将群信息以“成员昵称:信息内容”广播给群里所有人
  • 实现文件上传

客户端实现

package main

/*导入类库*/
import (
    "net"
    "fmt"
    "os"
    "bufio"
    "io"
    "flag"
    "strings"
    "io/ioutil"
)

/*全局常量*/
var (
    //退出命令管道
    chanQuit = make(chan bool, 0)

    //服务端连接对象
    conn     net.Conn
)

/*错误处理逻辑:有错误就暴力退出程序*/
func CHandleError(err error, why string) {
    if err != nil {
        fmt.Println(why, err)
        os.Exit(1)
    }
}

/*客户端主程序*/
func main() {

    //TODO:在命令行参数中携带昵称
    nameInfo := [3]interface{}{"name", "无名氏", "昵称"}
    retValuesMap := GetCmdlineArgs(nameInfo)
    name := retValuesMap["name"].(string)

    //拨号连接,获得connection
    var e error
    conn, e = net.Dial("tcp", "127.0.0.1:8888")
    CHandleError(e, "net.Dial")
    defer func() {
        conn.Close()
    }()

    //在一条独立的协程中输入,并发送消息
    go handleSend(conn, name)

    //在一条独立的协程中接收服务端消息
    go handleReceive(conn)

    //设置优雅退出逻辑
    <-chanQuit

}

/*处理收到的消息*/
func handleReceive(conn net.Conn) {
    buffer := make([]byte, 1024)
    for {
        n, err := conn.Read(buffer)
        if err != io.EOF {
            CHandleError(err, "conn.Read")
        }

        if n > 0 {
            msg := string(buffer[:n])
            fmt.Println(msg)
        }
    }

}

/*处理消息发送*/
func handleSend(conn net.Conn, name string) {
    //TODO:发送昵称到服务端
    _, err := conn.Write([]byte(name))
    CHandleError(err, "conn.Write([]byte(name))")

    reader := bufio.NewReader(os.Stdin)
    for {
        //读取标准输入
        lineBytes, _, _ := reader.ReadLine()
        lineStr := string(lineBytes)

        //上传文件
        //upload#文件名#文件路径
        if strings.Index(lineStr,"upload") == 0{
            //拿到文件名和文件路径
            strs := strings.Split(lineStr,"#")
            if len(strs)!=3{
                fmt.Println("上传信息格式有误!")
                continue
            }
            fileName := strs[1]
            filePath := strs[2]

            //构造数据包
            dataPack := make([]byte, 0)

            //写入数据包头部(upload#文件名)
            header := make([]byte, 100)
            copy(header,[]byte("upload#"+fileName+"#"))
            dataPack = append(dataPack, header...)

            //写入数据包身体(文件字节)
            fileBytes, _ := ioutil.ReadFile(filePath)
            dataPack = append(dataPack,fileBytes...)

            //写给服务端
            conn.Write(dataPack)

        }else{
            //发送到服务端
            _, err := conn.Write(lineBytes)
            CHandleError(err, "conn.Write")

            //正常退出
            if lineStr == "exit" {
                os.Exit(0)
            }
        }

    }
}

/*获取命令行参数*/
func GetCmdlineArgs(argInfos ...[3]interface{}) (retValuesMap map[string]interface{}) {

    fmt.Printf("type=%T,value=%v\n", argInfos, argInfos)

    //初始化返回结果
    retValuesMap = map[string]interface{}{}

    //预定义【用户可能输入的各种类型的指针】
    var strValuePtr *string
    var intValuePtr *int

    //预定义【用户可能输入的各种类型的指针】的容器
    //用户可能输入好几个string型的参数值,存放在好几个string型的指针中,将这些同种类型的指针放在同种类型的map中
    //例如:flag.Parse()了以后,可以根据【strValuePtrsMap["cmd"]】拿到【存放"cmd"值的指针】
    var strValuePtrsMap = map[string]*string{}
    var intValuePtrsMap = map[string]*int{}

    /*    var floatValuePtr *float32
        var floatValuePtrsMap []*float32
        var boolValuePtr *bool
        var boolValuePtrsMap []*bool*/

    //遍历用户需要接受的所有命令定义
    for _, argArray := range argInfos {

        /*
        先把每个命令的名称和用法拿出来,
        这俩货都是string类型的,所有都可以通过argArray[i].(string)轻松愉快地获得其字符串
        一个叫“cmd”,一个叫“你想干嘛”
        "cmd"一会会用作map的key
        */
        //[3]interface{}
        //["cmd" "未知类型" "你想干嘛"]
        //["gid"     0     "要查询的商品ID"]
        //上面的破玩意类型[string 可能是任意类型 string]
        nameValue := argArray[0].(string)  //拿到第一个元素的string值,是命令的name
        usageValue := argArray[2].(string) //拿到最后一个元素的string值,是命令的usage

        //判断argArray[1]的具体类型
        switch argArray[1].(type) {
        case string:
            //得到【存放cmd的指针】,cmd的值将在flag.Parse()以后才会有
            //cmdValuePtr = flag.String("cmd", argArray[1].(string), "你想干嘛")
            strValuePtr = flag.String(nameValue, argArray[1].(string), usageValue)

            //将这个破指针以"cmd"为键,存在【专门放置string型指针的map,即strValuePtrsMap】中
            strValuePtrsMap[nameValue] = strValuePtr

        case int:
            //得到【存放gid的指针】,gid的值将在flag.Parse()以后才会有
            //gidValuePtr = flag.String("gid", argArray[1].(int), "商品ID")
            intValuePtr = flag.Int(nameValue, argArray[1].(int), usageValue)

            //将这个破指针以"gid"为键,存在【专门放置int型指针的map,即intValuePtrsMap】中
            intValuePtrsMap[nameValue] = intValuePtr
        }

    }

    /*
    程序运行到这里,所有不同类型的【存值指针】都放在对相应类型的map中了
    flag.Parse()了以后,可以从map中以参数名字获取出【存值指针】,进而获得【用户输入的值】
    */

    //用户输入完了,解析,【用户输入的值】全都放在对应的【存值指针】中
    flag.Parse()

    /*
    遍历各种可能类型的【存值指针的map】
    */
    if len(strValuePtrsMap) > 0 {
        //从【cmd存值指针的map】中拿取cmd的值,还以cmd为键存入结果map中
        for k, vPtr := range strValuePtrsMap {
            retValuesMap[k] = *vPtr
        }
    }
    if len(intValuePtrsMap) > 0 {
        //从【gid存值指针的map】中拿取gid的值,还以gid为键存入结果map中
        for k, vPtr := range intValuePtrsMap {
            retValuesMap[k] = *vPtr
        }
    }

    //返回结果map
    return
}

服务端实现

客户端数据模型

package main

import "net"

type Client struct {
    //客户端连接
    conn net.Conn
    //昵称
    name string
    //远程地址
    addr string
}

聊天群数据模型和相关方法

package main

import (
    "strconv"
)

/*
·创建群结构体:属性包括群主、群昵称、群成员;
*/
type Group struct {
    //群昵称
    Name string
    //群主
    Owner *Client
    //群成员
    Members []*Client
}

/*
群昵称:xxx
群主:xxx
群人数:xxx
*/
func (g *Group) String() string {
    info := "群昵称:" + g.Name + "\n"
    info += "群  主:" + g.Owner.name + "\n"
    info += "群人数:" + strconv.Itoa(len(g.Members)) + "人\n"
    return info
}

/*添加新成员*/
func (g *Group) AddClient(client *Client) {
    g.Members = append(g.Members, client)
}

/*建群工厂方法*/
func NewGroup(name string, owner *Client) *Group {
    group := new(Group)
    group.Name = name
    group.Owner = owner
    group.Members = make([]*Client, 0)
    group.Members = append(group.Members, owner)
    return group
}

/*加群申请回复*/
type GroupJoinReply struct {
    //发送人
    fromWhom *Client
    //申请人
    toWhom *Client

    //申请的群
    group *Group
    //同意与否
    answer string
}

/*加群申请工厂方法*/
func NewGroupJoinReply(fromWhom, toWhom *Client, group *Group, answer string) *GroupJoinReply {
    reply := new(GroupJoinReply)
    reply.fromWhom = fromWhom
    reply.toWhom = toWhom
    reply.group = group
    reply.answer = answer
    return reply
}

//加群审核的自动执行
func (reply *GroupJoinReply) AutoRun() {
    if reply.group.Owner == reply.fromWhom {
        //回复是群主发的
        if reply.answer == "yes" {
            reply.group.AddClient(reply.toWhom)
            SendMsg2Client("你已成功加入"+reply.group.Name, reply.toWhom)
        } else {
            SendMsg2Client(reply.group.Name+"群主已经拒绝了您的加群请求,fuckoff!", reply.toWhom)
        }
    } else {
        //不是群主发的可以将“伪群主”封号
        SendMsg2Client("根据《中华人民共和国促进装逼法》,你已获得《葵花宝典》的练习权,执法人员将送书上门并监督练习", reply.fromWhom)
    }
}

服务端主程序

package main

import (
    "net"
    "fmt"
    "os"
    "io"
    "strings"
    "io/ioutil"
    "time"
    "bytes"
)

var (
    //客户端信息,用昵称为键
    //allClientsMap = make(map[string]net.Conn)
    allClientsMap = make(map[string]*Client)

    //所有群
    allGroupsMap map[string]*Group

    //basePath
    basePath = "C:/Users/sirouyang/Desktop/W4/day5/02飞狗聊天/server/uploads/"
)

func init() {
    allGroupsMap = make(map[string]*Group)
    allGroupsMap["示例群"] = NewGroup("示例群", &Client{name: "系统管理员"})
}

func SHandleError(err error, why string) {
    if err != nil {
        fmt.Println(why, err)
        os.Exit(1)
    }
}

func main() {

    //建立服务端监听
    listener, e := net.Listen("tcp", "127.0.0.1:8888")
    SHandleError(e, "net.Listen")
    defer func() {
        for _, client := range allClientsMap {
            client.conn.Write([]byte("all:服务器进入维护状态,大家都洗洗睡吧!"))
        }
        listener.Close()
    }()

    for {
        //循环接入所有女朋友
        conn, e := listener.Accept()
        SHandleError(e, "listener.Accept")
        clientAddr := conn.RemoteAddr()

        //TODO:接收并保存昵称
        buffer := make([]byte, 1024)
        var clientName string
        for {
            n, err := conn.Read(buffer)
            SHandleError(err, "conn.Read(buffer)")
            if n > 0 {
                clientName = string(buffer[:n])
                break
            }
        }
        fmt.Println(clientName + "上线了")

        //TODO:将每一个女朋友丢入map
        client := &Client{conn, clientName, clientAddr.String()}
        allClientsMap[clientName] = client

        //TODO:给已经在线的用户发送上线通知——使用昵称
        for _, client := range allClientsMap {
            client.conn.Write([]byte(clientName + "上线了"))
        }

        //在单独的协程中与每一个具体的女朋友聊天
        go ioWithClient(client)
    }

}

//与一个Client做IO
func ioWithClient(client *Client) {
    //clientAddr := conn.RemoteAddr().String()
    buffer := make([]byte, 1024)

    for {
        n, err := client.conn.Read(buffer)
        if err != io.EOF {
            SHandleError(err, "conn.Read")
        }

        if n > 0 {
            msgBytes := buffer[:n]
            if bytes.Index(msgBytes,[]byte("upload"))==0{
                /*处理文件上传*/

                //拿到数据包头(文件名)
                msgStr := string(msgBytes[:100])
                fileName := strings.Split(msgStr, "#")[1]

                //拿到数据包身体(文件字节)
                fileBytes := msgBytes[100:]

                //将文件字节写入指定位置
                err := ioutil.WriteFile(basePath+fileName, fileBytes, 0666)
                SHandleError(err,"ioutil.WriteFile")
                fmt.Println("文件上传成功")
                SendMsg2Client("文件上传成功",client)

            }else{
                /*处理字符消息*/
                //拿到客户端消息
                msg := string(msgBytes)
                fmt.Printf("%s:%s\n", client.name, msg)

                //将客户端说的每一句话记录在【以他的名字命名的文件里】
                writeMsgToLog(msg, client)

                strs := strings.Split(msg, "#")
                if len(strs) > 1 {
                    //要发送的目标昵称
                    header := strs[0]
                    body := strs[1]

                    switch header {

                    //世界消息
                    case "all":
                        handleWorldMsg(client, body)

                        //建群申请
                    case "group_setup":
                        handleGroupSetup(body, client)

                        //查看群信息
                    case "group_info":
                        handleGroupInfo(body, client)

                        //加群申请
                    case "group_join":
                        group, ok := allGroupsMap[body]
                        //如果群不存在
                        if !ok {
                            SendMsg2Client("查无此群,fuckoff", client)
                            continue
                        }

                        //发出加群申请
                        SendMsg2Client(client.name+"申请加入群"+body+",是否同意?", group.Owner)
                        SendMsg2Client("申请已发送,请等待群主审核",client)

                        //处理群主的回复
                    case "group_joinreply":
                        //group_joinreply#no<a href="/user/zhangsan" title="@zhangsan">@zhangsan</a>@东方艺术殿堂交流群

                        //拿到回复、申请人昵称、群昵称、
                        strs := strings.Split(body, "@")
                        answer := strs[0]
                        applicantName := strs[1]
                        groupName := strs[2]

                        //判断是否群昵称和申请人是否合法
                        group,ok1:=allGroupsMap[groupName]
                        toWhom,ok2:=allClientsMap[applicantName]

                        //自动执行加群申请
                        if ok1 && ok2{
                            NewGroupJoinReply(client,toWhom,group,answer).AutoRun()
                        }

                    default:
                        //点对点消息
                        handleP2PMsg(header, client, body)
                    }

                } else {

                    //客户端主动下线
                    if msg == "exit" {
                        //将当前客户端从在线用户中除名
                        //向其他用户发送下线通知
                        for name, c := range allClientsMap {
                            if c == client {
                                delete(allClientsMap, name)
                            } else {
                                c.conn.Write([]byte(name + "下线了"))
                            }
                        }
                    } else if strings.Index(msg, "log@") == 0 {
                        //log@all
                        //log@张全蛋
                        filterName := strings.Split(msg, "@")[1]
                        //向客户端发送它的聊天日志
                        go sendLog2Client(client, filterName)
                    } else {
                        client.conn.Write([]byte("已阅:" + msg))
                    }

                }
            }

        }
    }

}

/*处理点对点消息*/
func handleP2PMsg(header string, client *Client, body string) {
    for key, c := range allClientsMap {
        if key == header {
            c.conn.Write([]byte(client.name + ":" + body))

            //在点对点消息的目标端也记录日志
            go writeMsgToLog(client.name+":"+body, c)
            break
        }
    }
}

/*处理查看群信息*/
func handleGroupInfo(body string, client *Client) {
    if body == "all" {
        //查看所有群信息
        info := ""
        for _, group := range allGroupsMap {
            info += group.String() + "\n"
        }
        SendMsg2Client(info, client)
    } else {
        //查看单个群信息
        if group, ok := allGroupsMap[body]; ok {
            SendMsg2Client(group.String(), client)
        } else {
            SendMsg2Client("查无此群,stupid!", client)
        }

    }
}

/*处理建群申请*/
func handleGroupSetup(body string, client *Client) {
    if _, ok := allGroupsMap[body]; !ok {
        //建群
        newGroup := NewGroup(body, client)

        //将新群添加到所有群集合
        allGroupsMap[body] = newGroup

        //通知群主建群成功
        SendMsg2Client("建群成功", client)
    } else {
        //要创建的群已经存在
        SendMsg2Client("要创建的群已经存在", client)
    }
}

/*处理世界消息*/
func handleWorldMsg(client *Client, body string) {
    for _, c := range allClientsMap {
        c.conn.Write([]byte(client.name + ":" + body))
    }
}

func SendMsg2Client(msg string, client *Client) {
    client.conn.Write([]byte(msg))
}

//向客户端发送它的聊天日志
func sendLog2Client(client *Client, filterName string) {
    //读取聊天日志
    logBytes, e := ioutil.ReadFile("D:/BJBlockChain1801/demos/W4/day1/01ChatRoomII/logs/" + client.name + ".log")
    SHandleError(e, "ioutil.ReadFile")

    if filterName != "all" {
        //查找与某个人的聊天记录
        //从内容中筛选出带有【filterName#或filterName:】的行,拼接起来
        logStr := string(logBytes)
        targetStr := ""
        lineSlice := strings.Split(logStr, "\n")
        for _, lineStr := range lineSlice {
            if len(lineStr) > 20 {
                contentStr := lineStr[20:]
                if strings.Index(contentStr, filterName+"#") == 0 || strings.Index(contentStr, filterName+":") == 0 {
                    targetStr += lineStr + "\n"
                }
            }
        }
        client.conn.Write([]byte(targetStr))
    } else {
        //查询所有的聊天记录
        //向客户端发送
        client.conn.Write(logBytes)
    }

}

//将客户端说的一句话记录在【以他的名字命名的文件里】
func writeMsgToLog(msg string, client *Client) {
    //打开文件
    file, e := os.OpenFile(
        "D:/BJBlockChain1801/demos/W4/day1/01ChatRoomII/logs/"+client.name+".log",
        os.O_CREATE|os.O_WRONLY|os.O_APPEND,
        0644)
    SHandleError(e, "os.OpenFile")
    defer file.Close()

    //追加这句话
    logMsg := fmt.Sprintln(time.Now().Format("2006-01-02 15:04:05"), msg)
    file.Write([]byte(logMsg))
}

想要了解更多关于GO语言开发方面内容的小伙伴,

请关注IT兄弟连官网、公众号:GO语言研习社,

IT兄弟连教育有专业的微软、谷歌讲师为您指导,

此外IT兄弟连老师精心推出的GO语言教程定能让你快速掌握GO语言从入门到精通开发实战技能。


有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

3700 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传