脚本之家,脚本语言编程技术及教程分享平台!
分类导航

Python|VBS|Ruby|Lua|perl|VBA|Golang|PowerShell|Erlang|autoit|Dos|bat|

服务器之家 - 脚本之家 - Golang - Go语言多人聊天室项目实战

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

2020-05-27 10:21尹成 Golang

这篇文章主要为大家详细介绍了Go语言多人聊天室项目实战,实现单撩或多撩等多种功能,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

本文为大家分享了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

延伸 · 阅读

精彩推荐
  • Golanggolang如何使用struct的tag属性的详细介绍

    golang如何使用struct的tag属性的详细介绍

    这篇文章主要介绍了golang如何使用struct的tag属性的详细介绍,从例子说起,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看...

    Go语言中文网11352020-05-21
  • Golanggolang 通过ssh代理连接mysql的操作

    golang 通过ssh代理连接mysql的操作

    这篇文章主要介绍了golang 通过ssh代理连接mysql的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    a165861639710342021-03-08
  • Golanggo日志系统logrus显示文件和行号的操作

    go日志系统logrus显示文件和行号的操作

    这篇文章主要介绍了go日志系统logrus显示文件和行号的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    SmallQinYan12302021-02-02
  • GolangGolang通脉之数据类型详情

    Golang通脉之数据类型详情

    这篇文章主要介绍了Golang通脉之数据类型,在编程语言中标识符就是定义的具有某种意义的词,比如变量名、常量名、函数名等等,Go语言中标识符允许由...

    4272021-11-24
  • GolangGolang中Bit数组的实现方式

    Golang中Bit数组的实现方式

    这篇文章主要介绍了Golang中Bit数组的实现方式,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    天易独尊11682021-06-09
  • Golanggolang的httpserver优雅重启方法详解

    golang的httpserver优雅重启方法详解

    这篇文章主要给大家介绍了关于golang的httpserver优雅重启的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,...

    helight2992020-05-14
  • Golanggo语言制作端口扫描器

    go语言制作端口扫描器

    本文给大家分享的是使用go语言编写的TCP端口扫描器,可以选择IP范围,扫描的端口,以及多线程,有需要的小伙伴可以参考下。 ...

    脚本之家3642020-04-25
  • Golanggolang json.Marshal 特殊html字符被转义的解决方法

    golang json.Marshal 特殊html字符被转义的解决方法

    今天小编就为大家分享一篇golang json.Marshal 特殊html字符被转义的解决方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧 ...

    李浩的life12792020-05-27