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

Amiee7 · · 3530 次点击 · 开始浏览    置顶
这是一个创建于 的主题,其中的信息可能已经有所发展或是发生改变。

本篇文章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

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