Go实现聊天室

前言

用Go语言简单实现一个网络聊天室,首先创建一个chatroom的文件夹,在文件夹下创建chatroom.go文件。

建立tcp socket连接

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
package main

import (
"fmt"
"net"
)

//将所有代码写在一个文件中,不做代码整理
func main() {
//创建服务器
listener,err := net.Listen("tcp",":8080")
if err != nil {
fmt.Println("net.Listen err:",err)
return
}

fmt.Println("服务器启动成功!")

for {
fmt.Println("=======>主go程监听中...")

//监听
conn, err := listener.Accept()
if err != nil {
fmt.Println("listener.Accept err", err)
return
}
//建立连接
fmt.Println("建立连接成功!")
//启动处理业务的go程
go handler(conn)
}
}

//处理具体业务
func handler(conn net.Conn) {
for {
fmt.Println("启动业务...")
//TODO //代表这里以后再实现
buf :=make([]byte,1024)

//读取客户端发送过来的数据
cnt,err := conn.Read(buf)
if err != nil {
fmt.Println("conn.Read err:",err)
return
}
fmt.Println("服务器接受客户端发送过来的数据为:",string(buf[:cnt-1]),",cnt: ",cnt)
}
}

启动程序

1
go run chatroom.go

1

nc连接

2

3

数据流分析

我们程序中有一个主go程,负责监听。客户端以一种数据流的形式建立连接,这个时候服务器就会启动一个handler() go程,在handler() go程中可以接受来自客户端发送的消息。这个时候服务器就应该把消息告诉所有人,因此我们应该在handler()中创建一个通道,它要将消息写入到公共通道message中,这个时候会有另外一个go程,它负责监听message,当message中有数据时,它会将数据发送给所有的用户。对于user用户的创建我们需要在handler()中实现,并将user添加到map中,每个用户都有一个自己的msg管道,当需要发送消息给所有用户的时候,go程会遍历用户map,并将消息写入每个用户的管道中。这个时候,每个用户的管道里都会有来自公共管道的message。之后我们再启动一个go程。每个用户都会拥有这个go程,负责将用户自己的msg回写给终端。

定义User和map结构

1
2
3
4
5
6
7
8
type User struct {
name string
id string
msg chan string
}

//创建一个全局的map结构,用于保存所有的用户
var allUsers = make(map[string]User)

在handler中调用

1
2
3
4
5
6
7
8
9
10
11
//客户端与服务器建立连接的时候,会有ip和port,因此将port当成user id
//创建user
clientAddr := conn.RemoteAddr().String()
//fmt.Println("chiientAddr: ",clientAddr[10:])
newUser := User{
name: "虎扑JR"+clientAddr[10:],//可以修改,会提供rename命令修改
id: clientAddr, //id不会修改,作为map中的key
msg: make(chan string),//注意要make空间,否则无法写数据
}
//添加user到map结构
allUsers[newUser.id] = newUser

定义message通道

1
2
//定义一个message全局通道,用接收任何人发送过来的消息
var message = make(chan string,10)

创建监听广播go程函数

1
2
3
4
5
6
7
8
9
10
11
//向所有的用户广播消息,启动一个全局唯一的go程
func broadcast() {
fmt.Println("广播go程启动成功...")

//1.从message中读取数据
info := <- message
//2.将数据写入到每一个用户的msg管道中
for _,user := range allUsers {
user.msg <- info
}
}

启动广播go程

1
2
//启动全局唯一的go程,负责监听message通道,写给所有的用户
go broadcast()

写入上线数据

1
2
3
//向message写入数据,当前用户上线的消息,用于通知所有人
loginInfo := fmt.Sprintf("[%s]:[%s] ===> 上线了login!!",newUser.id,newUser.name)
message <- loginInfo

当前完整代码

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
package main

import (
"fmt"
"net"
)


//将所有代码写在一个文件中,不做代码整理

type User struct {
name string
id string
msg chan string
}

//创建一个全局的map结构,用于保存所有的用户
var allUsers = make(map[string]User)

//定义一个message全局通道,用接收任何人发送过来的消息
var message = make(chan string,10)

func main() {
//创建服务器
listener,err := net.Listen("tcp",":8080")
if err != nil {
fmt.Println("net.Listen err:",err)
return
}

//启动全局唯一的go程,负责监听message通道,写给所有的用户
go broadcast()

fmt.Println("服务器启动成功!")

for {
fmt.Println("=======>主go程监听中...")

//监听
conn, err := listener.Accept()
if err != nil {
fmt.Println("listener.Accept err", err)
return
}
//建立连接
fmt.Println("建立连接成功!")
//启动处理业务的go程
go handler(conn)
}
}

//处理具体业务
func handler(conn net.Conn) {

fmt.Println("启动业务...")

//客户端与服务器建立连接的时候,会有ip和port,因此将port当成user id
//创建user
clientAddr := conn.RemoteAddr().String()
//fmt.Println("chiientAddr: ",clientAddr[10:])
newUser := User{
name: "虎扑JR"+clientAddr[10:],//可以修改,会提供rename命令修改
id: clientAddr, //id不会修改,作为map中的key
msg: make(chan string,10),//注意要make空间,否则无法写数据
}
//添加user到map结构
allUsers[newUser.id] = newUser

//向message写入数据,当前用户上线的消息,用于通知所有人
loginInfo := fmt.Sprintf("[%s]:[%s] ===> 上线了login!!",newUser.id,newUser.name)
message <- loginInfo
for {
buf :=make([]byte,1024)

//读取客户端发送过来的数据
cnt,err := conn.Read(buf)
if err != nil {
fmt.Println("conn.Read err:",err)
return
}
fmt.Println("服务器接受客户端发送过来的数据为:",string(buf[:cnt-1]),",cnt: ",cnt)
}
}


//向所有的用户广播消息,启动一个全局唯一的go程
func broadcast() {
fmt.Println("广播go程启动成功...")

for {

//1.从message中读取数据
info := <-message
fmt.Println("message接收到的消息:", info)
//2.将数据写入到每一个用户的msg管道中
for _, user := range allUsers {
//如果msg是非缓冲的,那么会在这里阻塞
user.msg <- info
}
}
}

4

5

User监听通道go程

每个用户应该还有一个用来监听自己msg管道的go程,负责将数据返回给客户端。

1
2
3
4
5
6
7
8
9
//每个用户应该还有一个用来监听自己msg管道的go程,负责将数据返回给客户端。
func writeBackToClient(user *User,conn net.Conn) {
fmt.Printf("user : %s的go程正在监听自己的msg管道\n",user.name)
for data := range user.msg {
fmt.Printf("user:%s写回给客户端的数据为:%s\n",user.name,data)
_,_ = conn.Write([]byte(data+"\n"))
}

}

当前完整代码

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
package main

import (
"fmt"
"net"
)


//将所有代码写在一个文件中,不做代码整理

type User struct {
name string
id string
msg chan string
}

//创建一个全局的map结构,用于保存所有的用户
var allUsers = make(map[string]User)

//定义一个message全局通道,用接收任何人发送过来的消息
var message = make(chan string,10)

func main() {
//创建服务器
listener,err := net.Listen("tcp",":8080")
if err != nil {
fmt.Println("net.Listen err:",err)
return
}

//启动全局唯一的go程,负责监听message通道,写给所有的用户
go broadcast()

fmt.Println("服务器启动成功!")

for {
fmt.Println("=======>主go程监听中...")

//监听
conn, err := listener.Accept()
if err != nil {
fmt.Println("listener.Accept err", err)
return
}
//建立连接
fmt.Println("建立连接成功!")
//启动处理业务的go程
go handler(conn)
}
}

//处理具体业务
func handler(conn net.Conn) {

fmt.Println("启动业务...")

//客户端与服务器建立连接的时候,会有ip和port,因此将port当成user id
//创建user
clientAddr := conn.RemoteAddr().String()
//fmt.Println("chiientAddr: ",clientAddr[10:])
newUser := User{
name: "虎扑JR"+clientAddr[10:],//可以修改,会提供rename命令修改
id: clientAddr, //id不会修改,作为map中的key
msg: make(chan string,10),//注意要make空间,否则无法写数据
}
//添加user到map结构
allUsers[newUser.id] = newUser

//启动go程,负责将msg数据返回给客户端
go writeBackToClient(&newUser,conn)

//向message写入数据,当前用户上线的消息,用于通知所有人
loginInfo := fmt.Sprintf("[%s]:[%s] ===> 上线了login!!",newUser.id,newUser.name)
message <- loginInfo

for {
//具体业务逻辑
buf :=make([]byte,1024)

//读取客户端发送过来的数据
cnt,err := conn.Read(buf)
if err != nil {
fmt.Println("conn.Read err:",err)
return
}
fmt.Println("服务器接受客户端发送过来的数据为:",string(buf[:cnt-1]),",cnt: ",cnt)
}
}


//向所有的用户广播消息,启动一个全局唯一的go程
func broadcast() {
fmt.Println("广播go程启动成功...")

for {

//1.从message中读取数据
info := <-message
fmt.Println("message接收到的消息:", info)
//2.将数据写入到每一个用户的msg管道中
for _, user := range allUsers {
//如果msg是非缓冲的,那么会在这里阻塞
user.msg <- info
}
}
}

//每个用户应该还有一个用来监听自己msg管道的go程,负责将数据返回给客户端。
func writeBackToClient(user *User,conn net.Conn) {
fmt.Printf("user : %s的go程正在监听自己的msg管道\n",user.name)
for data := range user.msg {
fmt.Printf("user:%s写回给客户端的数据为:%s\n",user.name,data)
_,_ = conn.Write([]byte(data+"\n"))
}

}

7

6

增加功能

查询用户

查询命令:who 将当前所有登陆的用户,展示出来,id,name返回给当前用户

在handler中处理业务逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//1.查询当前所有的用户 who
//判断接受的数据是不是who ==> 长度&字符串
userInput := string(buf[:cnt-1]) //这是用户输入的数据,最后一个是回车,去掉回车
if len(userInput)==3 && userInput == "who" {
//遍历allUsers这个map:(key: userid value:user本身)
fmt.Println("用户即将查询所有用户信息!")

//这个切片包含所有的用户信息
var userInfos []string

for _,user := range allUsers {
userInfo := fmt.Sprintf("userid:%s,username:%s",user.id,user.name)
userInfos = append(userInfos,userInfo )
}
//最终写到管道中,一定是一个字符串
r := strings.Join(userInfos,"\n")
//将数据返回给查询的客户端
newUser.msg <- r
}else {
//如果用户输入的不是命令,只是普通的聊天信息,那么只需要写入到广播通道中,由其他的go程进行常规转发
message <- userInput
}

8

重命名
1
2
3
4
5
6
7
8
9
else if len(userInput) >9 && userInput[:7]=="\\rename" {
//规则:rename|ddd
//读取数据判断长度,判断字符是rename
//使用|分割,获取|后面的内容,作为名字
//更新用户名字 newUser.name = ddd
newUser.name = strings.Split(userInput,"|")[1]
allUsers[newUser.id] = newUser //更新map中的user
//通知客户端,更新成功
newUser.msg <- "rename successfully!"

9

主动退出

从map中删除,对应的conn要close

每个用户都有自己的watch go程,负责监听退出信号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//启动一个go程,负责监听退出信号,触发后进行清理工作:delete map,close conn都在这里处理
func watch(user *User,conn net.Conn,isQuit <- chan bool) {
fmt.Println("启动监听退出信号的go程...")
defer fmt.Println("watch go程退出!")
for {
select {
case <- isQuit:
logoutInfo := fmt.Sprintf("%s exit already!",user.name)
fmt.Println("删除当前用户:",user.name)
delete(allUsers,user.id)
message <- logoutInfo
conn.Close()
return
}
}
}

在handler函数中定义isQuit

1
2
//定义一个退出信号,用户监听client退出
var isQuit = make(chan bool)

在handler中启动go watch,同时传入相应信息

1
2
//启动go程,负责监听退出信号
go watch(&newUser,conn,isQuit)

conn.Read(buf)之后,通过读取的cnt判断用户退出,如果是ctrl+c主动退出,则触发isQuit

1
2
3
4
5
6
7
if cnt == 0 {
fmt.Println("客户端主动关闭ctrl+c,准备退出!")
//map删除用户,conn close
//服务器还可以主动的退出
//在这里不进行真正的退出动作,而是发送一个退出信号,统一做退出处理,可以使用新的管道做信号传递
isQuit <- true
}

10

11

超时退出

使用定时器来进行超时管理。如果60s内没有发送任何数据,那么直接将这个连接关闭掉

更新watch函数

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
//启动一个go程,负责监听退出信号,触发后进行清理工作:delete map,close conn都在这里处理
func watch(user *User,conn net.Conn,isQuit <- chan bool,restTimer <-chan bool) {
fmt.Println("启动监听退出信号的go程...")
defer fmt.Println("watch go程退出!")
for {
select {
case <- isQuit:
logoutInfo := fmt.Sprintf("%s exit already!\n",user.name)
fmt.Println("删除当前用户:",user.name)
delete(allUsers,user.id)
message <- logoutInfo
conn.Close()
return
case <- time.After(10*time.Second):
logoutInfo := fmt.Sprintf("%s timeout exit already!\n",user.name)
fmt.Println("删除当前用户:",user.name)
delete(allUsers,user.id)
message <- logoutInfo
conn.Close()
return
case <- restTimer:
fmt.Printf("连接%s 重置计数器!\n",user.name)

}
}
}

创建并传入restTimer

1
2
3
4
//创建一个用于重置计数器的管道,用于告知watch函数,当前用户正在输入
var restTimer = make(chan bool)
//启动go程,负责监听退出信号
go watch(&newUser,conn,isQuit,restTimer)

只要有数据写入,每次写入数据之后都要设置restTimer

1
restTimer <- true

聊天室完整代码

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
187
188
189
190
191
192
193
194
195
196
package main

import (
"fmt"
"net"
"strings"
"time"
)


//将所有代码写在一个文件中,不做代码整理

type User struct {
name string
id string
msg chan string
}

//创建一个全局的map结构,用于保存所有的用户
var allUsers = make(map[string]User)

//定义一个message全局通道,用接收任何人发送过来的消息
var message = make(chan string,10)

func main() {
//创建服务器
listener,err := net.Listen("tcp",":8080")
if err != nil {
fmt.Println("net.Listen err:",err)
return
}

//启动全局唯一的go程,负责监听message通道,写给所有的用户
go broadcast()

fmt.Println("服务器启动成功!")

for {
fmt.Println("=======>主go程监听中...")

//监听
conn, err := listener.Accept()
if err != nil {
fmt.Println("listener.Accept err", err)
return
}
//建立连接
fmt.Println("建立连接成功!")
//启动处理业务的go程
go handler(conn)
}
}

//处理具体业务
func handler(conn net.Conn) {

fmt.Println("启动业务...")

//客户端与服务器建立连接的时候,会有ip和port,因此将port当成user id
//创建user
clientAddr := conn.RemoteAddr().String()
//fmt.Println("chiientAddr: ",clientAddr[10:])
newUser := User{
name: "虎扑JR"+clientAddr[10:],//可以修改,会提供rename命令修改
id: clientAddr, //id不会修改,作为map中的key
msg: make(chan string,10),//注意要make空间,否则无法写数据
}
//添加user到map结构
allUsers[newUser.id] = newUser

//定义一个退出信号,用户监听client退出
var isQuit = make(chan bool)
//创建一个用于重置计数器的管道,用于告知watch函数,当前用户正在输入
var restTimer = make(chan bool)
//启动go程,负责监听退出信号
go watch(&newUser,conn,isQuit,restTimer)

//启动go程,负责将msg数据返回给客户端
go writeBackToClient(&newUser,conn)

//向message写入数据,当前用户上线的消息,用于通知所有人
loginInfo := fmt.Sprintf("[%s]:[%s] ===> 上线了login!!\n",newUser.id,newUser.name)
message <- loginInfo

for {
//具体业务逻辑
buf :=make([]byte,1024)

//读取客户端发送过来的数据
cnt,err := conn.Read(buf)
if cnt == 0 {
fmt.Println("客户端主动关闭ctrl+c,准备退出!")
//map删除用户,conn close
//服务器还可以主动的退出
//在这里不进行真正的退出动作,而是发送一个退出信号,统一做退出处理,可以使用新的管道做信号传递
isQuit <- true
}
if err != nil {
fmt.Println("conn.Read err:",err,",cnt:",cnt)
return
}
fmt.Println("服务器接受客户端发送过来的数据为:",string(buf[:cnt-1]),",cnt: ",cnt)

//业务逻辑处理 开始-----------
//1.查询当前所有的用户 who
//判断接受的数据是不是who ==> 长度&字符串
userInput := string(buf[:cnt-1]) //这是用户输入的数据,最后一个是回车,去掉回车
if len(userInput)==4 && userInput == "\\who" {
//遍历allUsers这个map:(key: userid value:user本身)
fmt.Println("用户即将查询所有用户信息!")

//这个切片包含所有的用户信息
var userInfos []string

for _,user := range allUsers {
userInfo := fmt.Sprintf("userid:%s,username:%s",user.id,user.name)
userInfos = append(userInfos,userInfo )
}
//最终写到管道中,一定是一个字符串
r := strings.Join(userInfos,"\n") //连接数字切片,生成字符串
//将数据返回给查询的客户端
newUser.msg <- r
} else if len(userInput) >9 && userInput[:7]=="\\rename" {
//规则:rename|ddd
//读取数据判断长度,判断字符是rename
//使用|分割,获取|后面的内容,作为名字
//更新用户名字 newUser.name = ddd
newUser.name = strings.Split(userInput,"|")[1]
allUsers[newUser.id] = newUser //更新map中的user
//通知客户端,更新成功
newUser.msg <- "rename successfully!"
} else {
//如果用户输入的不是命令,只是普通的聊天信息,那么只需要写入到广播通道中,由其他的go程进行常规转发
message <- userInput
}
restTimer <- true

//业务逻辑处理 结束-----------

}
}


//向所有的用户广播消息,启动一个全局唯一的go程
func broadcast() {
fmt.Println("广播go程启动成功...")

for {

//1.从message中读取数据
info := <-message
fmt.Println("message接收到的消息:", info)
//2.将数据写入到每一个用户的msg管道中
for _, user := range allUsers {
//如果msg是非缓冲的,那么会在这里阻塞
user.msg <- info
}
}
}

//每个用户应该还有一个用来监听自己msg管道的go程,负责将数据返回给客户端。
func writeBackToClient(user *User,conn net.Conn) {
fmt.Printf("user : %s的go程正在监听自己的msg管道\n",user.name)
for data := range user.msg {
fmt.Printf("user:%s写回给客户端的数据为:%s\n",user.name,data)
_,_ = conn.Write([]byte(data+"\n"))
}

}

//启动一个go程,负责监听退出信号,触发后进行清理工作:delete map,close conn都在这里处理
func watch(user *User,conn net.Conn,isQuit <- chan bool,restTimer <-chan bool) {
fmt.Println("启动监听退出信号的go程...")
defer fmt.Println("watch go程退出!")
for {
select {
case <- isQuit:
logoutInfo := fmt.Sprintf("%s exit already!\n",user.name)
fmt.Println("删除当前用户:",user.name)
delete(allUsers,user.id)
message <- logoutInfo
conn.Close()
return
case <- time.After(10*time.Second):
logoutInfo := fmt.Sprintf("%s timeout exit already!\n",user.name)
fmt.Println("删除当前用户:",user.name)
delete(allUsers,user.id)
message <- logoutInfo
conn.Close()
return
case <- restTimer:
fmt.Printf("连接%s 重置计数器!\n",user.name)

}
}
}