在上一篇《Golang实战-一个聊天室的实现》中,我们按照书上写了个简单的聊天室,今天我们来加点我们自己的东西:
可新增房间,并切换房间。
要想新建房间,得有个房间的对象,保存房间的一些基本信息:
type RoomInfo struct {
Name string `json:"name"`
//删除房间时用,只有房间创建者可删除
CreaterId string `json:"creater_id"`
}
房间有了,下面是房间里的人:
type ClientInfo struct {
Id string `json:"id"`
Nickname string `json:"nickname"`
CurrentRoom RoomInfo `json:"current_room"`
ClientResource chan<- string
}
房间和人的关系应该是一对多的关系:
type Clients []*ClientInfo
type ChatRoom struct {
RoomMap map[RoomInfo]Clients
}
房间和人都定义好了,我们需要对房间有一些基本的增、删操作,将它们独立到一个文件db.go里:
package chat
type ClientInfo struct {
Id string `json:"id"`
Nickname string `json:"nickname"`
CurrentRoom RoomInfo `json:"current_room"`
ClientResource chan<- string
}
type RoomInfo struct {
Name string `json:"name"`
//删除房间时用,只有房间创建者可删除
CreaterId string `json:"creater_id"`
}
type Clients []*ClientInfo
type RoomList []RoomInfo
type ChatRoom struct {
RoomMap map[RoomInfo]Clients
}
func NewChatRoom() *ChatRoom {
return &ChatRoom{
RoomMap: make(map[RoomInfo]Clients),
}
}
func (c *ChatRoom) AddRoom(r RoomInfo) {
//判断房间是否已存在
_, ok := c.RoomMap[r]
if !ok {
c.RoomMap[r] = Clients{}
}
}
func (c *ChatRoom) AddClient(r RoomInfo, cli *ClientInfo) {
clients := c.RoomMap[r]
found := false
//防止重复添加
for _, client := range clients {
if client.Id == cli.Id {
found = true
}
}
if found == false {
cli.CurrentRoom = r
c.RoomMap[r] = append(clients, cli)
}
}
func (c *ChatRoom) RemoveClient(r RoomInfo, id string) (bool, int) {
clients, ok := c.RoomMap[r]
if !ok {
return false, 0
}
removed := false
cleaned := Clients{}
for _, client := range clients {
if client.Id != id {
cleaned = append(cleaned, client)
} else {
removed = true
}
}
c.RoomMap[r] = cleaned
return removed, len(cleaned)
}
func (c *ChatRoom) RemoveRoom(r RoomInfo) {
delete(c.RoomMap, r)
}
func (c *ChatRoom) RoomUsers(r RoomInfo) Clients {
return c.RoomMap[r]
}
func (c *ChatRoom) RoomList() RoomList {
results := RoomList{}
for k, _ := range c.RoomMap {
results = append(results, k)
}
return results
}
func (c *ChatRoom) FindRoom(roomName string) (RoomInfo, bool) {
for k, _ := range c.RoomMap {
if k.Name == roomName {
return k, true
}
}
return RoomInfo{}, false
}
再写个简单的测试用例吧db_test.go:
package chat
import "testing"
func TestDb(t *testing.T) {
r1 := RoomInfo{"room1", "127.0.0.1:46235"}
r2 := RoomInfo{"room2", "127.0.0.1:85650"}
c1 := &ClientInfo{"127.0.0.1:46235", "Tom", RoomInfo{}, make(chan string)}
c2 := &ClientInfo{"127.0.0.1:85650", "Lilei", RoomInfo{}, make(chan string)}
room := NewChatRoom()
room.AddRoom(r1)
room.AddRoom(r2)
room.AddClient(r1, c1)
room.AddClient(r1, c2)
room.AddClient(r1, c2)
room.AddClient(r2, c2)
room.RemoveClient(r1, c2.Id)
clients := room.RoomUsers(r1)
t.Logf("%s", clients)
room.RemoveRoom(r2)
rooms := room.RoomList()
t.Logf("%s", rooms)
r, ok := room.FindRoom("room1")
if ok {
t.Logf("%s", r)
}
}
定义个默认的房间,每个客户端连接到服务端时,都在这个默认房间里。
var defaultRoom = chat.RoomInfo{"default", "0.0.0.0:0"}
程序刚开始运行时,就应该将这个默认的房间建好:
func main() {
listener, err := net.Listen("tcp", "localhost:8000")
room = chat.NewChatRoom()
room.AddRoom(defaultRoom)
if err != nil {
//log.Fatal()打印错误信息并调用os.Exit(1),终止程序
log.Fatal(err)
}
//广播,发送消息到所有客户端
go broadcaster()
for {
conn, err := listener.Accept()
if err != nil {
log.Println(err)
continue
}
//每个客户端一个goroutine
go handleConn(conn)
}
}
因为我们是基于命令行界面的,所以要对客户端输入解析处理:
switch command {
case "list":
case "enter":
case "exit":
case "create":
case "delete":
default:
}
暂时提供5种命令:
list:列出所有房间
enter:进入已有的房间,后面加房间名称,eg: enter room1
exit:退出当前房间,退出后,自动进入默认房间,已经在默认房间中的,退出无效
create:新建房间,后面加房间名称,eg: create room1
delete:删除房间,后面加房间房产,只能删除自己创建的房间,eg: delete room1
贴上完整代码chat_server.go:
package main
import (
"chat-room/chat"
"bufio"
"encoding/json"
"fmt"
"log"
"net"
"strings"
)
var room *chat.ChatRoom
var defaultRoom = chat.RoomInfo{"default", "0.0.0.0:0"}
func main() {
listener, err := net.Listen("tcp", "localhost:8000")
room = chat.NewChatRoom()
room.AddRoom(defaultRoom)
if err != nil {
//log.Fatal()打印错误信息并调用os.Exit(1),终止程序
log.Fatal(err)
}
//广播,发送消息到所有客户端
go broadcaster()
for {
conn, err := listener.Accept()
if err != nil {
log.Println(err)
continue
}
//每个客户端一个goroutine
go handleConn(conn)
}
}
//channel的三种类型(只发送、只接受、即发送也接受)
//这里client只发送不接受
//只接受 type client <-chan string
//即发送也接受 type client chan string
type client chan<- string
type MessageStruct struct {
User *chat.ClientInfo `json:"send_user"`
Msg string `json:"msg"`
}
var (
message = make(chan *MessageStruct)
)
func broadcaster() {
for {
select {
case msgStruct := <-message:
msg := msgStruct.Msg
currentRoom := msgStruct.User.CurrentRoom
users := room.RoomUsers(currentRoom)
for _, user := range users {
user.ClientResource <- msg
}
}
}
}
func handleConn(conn net.Conn) {
ch := make(chan string)
//defer close(ch)
//写入消息到客户端的连接
go writeToCLient(conn, ch)
who := conn.RemoteAddr().String()
cli := &chat.ClientInfo{}
cli.Id = who
//当客户端连接过来时,给客户端一条消息
//注意,这时的ch会立马被writeToCLient goroutine读取,并发送到当前客户端
//所以已连接的其他客户端不会接受到该条消息
ch <- "Your nickname:"
reader := bufio.NewReader(conn)
line, err := reader.ReadString('\n')
if err != nil {
log.Fatal(err)
}
nickname := strings.TrimSpace(line)
cli.Nickname = nickname
cli.CurrentRoom = defaultRoom
cli.ClientResource = ch
msgStruct := &MessageStruct{
User: cli,
}
msgStruct.Msg = nickname + " are arrived"
message <- msgStruct
room.AddClient(defaultRoom, cli)
input := bufio.NewScanner(conn)
//阻塞监听客户端输入
for input.Scan() {
msg := input.Text()
msg = strings.TrimSpace(msg)
params := strings.Split(msg, " ")
switch params[0] {
case "list":
r := room.RoomList()
jsonMsg, err := json.Marshal(r)
if err != nil {
log.Println(err)
}
msgStruct.Msg = string(jsonMsg)
message <- msgStruct
case "enter":
roomName, ok := room.FindRoom(params[1])
if ok {
currentRoom := cli.CurrentRoom
room.RemoveClient(currentRoom, cli.Id)
cli.CurrentRoom = roomName
room.AddClient(roomName, cli)
}
case "exit":
//TODO exit current room
case "create":
roomName := chat.RoomInfo{params[1], who}
room.AddRoom(roomName)
case "delete":
//TODO delete a room
default:
msgStruct.Msg = nickname + ": " + msg
message <- msgStruct
}
}
msgStruct.Msg = nickname + " are left"
message <- msgStruct
conn.Close()
}
func writeToCLient(conn net.Conn, ch <-chan string) {
for msg := range ch {
fmt.Fprintln(conn, msg)
}
}
客户端代码不变,项目的目录结构:
|- chat-room
|- chat
|- db.go
|- db_test.go
|- chat_client.go
|- chat_server.go
程序运行结果:
$ go run chat_client.go Your nickname: Tom Tom are arrived jim are arrived jim are left Jim are arrived Jim: Hello john are arrived john are left John are arrived [{"name":"default","creater_id":"0.0.0.0:0"}] defaultttt Tom: defaultttt Lilei are arrived Lilei are left Lilei are arrived Lilei: Hello ervery one |
$ go run chat_client.go Your nickname: Jim Jim are arrived Hello Jim: Hello john are arrived john are left John are arrived [{"name":"default","creater_id":"0.0.0.0:0"}] Tom: defaultttt enter room1 I'm room111 Jim: I'm room111 John: Hei boy room1 comeing new guy |
$ go run chat_client.go Your nickname: John John are arrived list [{"name":"default","creater_id":"0.0.0.0:0"}] create room1 enter room1 Jim: I'm room111 Hei boy room1 comeing new guy John: Hei boy room1 comeing new guy |
$ go run chat_client.go Your nickname: Lilei Lilei are arrived Hello ervery one Lilei: Hello ervery one |
遗留的一些问题:
程序有个BUG,即当客户端输入list命令时,改客户端所在房间的所有人都会收到返回信息。
ch := make(chan string) 创建的ch channel没关闭
剩下的几个命令逻辑和其他几个命令的逻辑差不多,就不写了。