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

时间:2022-09-25 22:29:27

本文为大家分享了Go语言多人聊天室项目实战,供大家参考,具体内容如下

功能需求

  • 实现单撩
  • 实现群撩
  • 实现用户上线的全网通知
  • 实现用户昵称
  • 实现聊天日志的存储和查看

服务端实现

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
type Client struct {
 conn net.Conn
 name string
 addr string
}
 
var (
 //客户端信息,用昵称为键
 //clientsMap = make(map[string]net.Conn)
 clientsMap = make(map[string]Client)
)
 
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 clientsMap {
 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()}
 clientsMap[clientName] = client
 
 //TODO:给已经在线的用户发送上线通知——使用昵称
 for _, client := range clientsMap {
 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 {
 msg := string(buffer[:n])
 fmt.Printf("%s:%s\n", client.name, msg)
 
 //将客户端说的每一句话记录在【以他的名字命名的文件里】
 writeMsgToLog(msg, client)
 
 strs := strings.Split(msg, "#")
 if len(strs) > 1 {
 //all#hello
 //zqd#hello
 
 //要发送的目标昵称
 targetName := strs[0]
 targetMsg := strs[1]
 
 //TODO:使用昵称定位目标客户端的Conn
 if targetName == "all" {
  //群发消息
  for _, c := range clientsMap {
  c.conn.Write([]byte(client.name + ":" + targetMsg))
  }
 } else {
  //点对点消息
  for key, c := range clientsMap {
  if key == targetName {
  c.conn.Write([]byte(client.name + ":" + targetMsg))
 
  //在点对点消息的目标端也记录日志
  go writeMsgToLog(client.name + ":" + targetMsg,c)
  break
  }
  }
 }
 
 } else {
 
 //客户端主动下线
 if msg == "exit" {
  //将当前客户端从在线用户中除名
  //向其他用户发送下线通知
  for name, c := range clientsMap {
  if c == client {
  delete(clientsMap, 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 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))
}

客户端实现

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
import (
 "net"
 "fmt"
 "os"
 "bufio"
 "io"
 "flag"
)
 
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()
 
 //发送到服务端
 _, err := conn.Write(lineBytes)
 CHandleError(err, "conn.Write")
 
 //正常退出
 if string(lineBytes) == "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
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。

原文链接:https://blog.csdn.net/itcastcpp/article/details/84133929