本篇文章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语言从入门到精通开发实战技能。
有疑问加站长微信联系(非本文作者)