Golang实战-一个聊天室的实现二

本文是Golang实战系列的第二篇,介绍如何为之前的简单聊天室添加更多功能,如新建、切换房间及管理操作。通过创建房间对象,实现了包括列表、进入、退出、创建和删除房间等命令。文章附带了完整代码,但指出存在一个BUG,当客户端发送'list'命令时,所有房间成员都会收到响应,且创建的channel未关闭。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在上一篇《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没关闭

剩下的几个命令逻辑和其他几个命令的逻辑差不多,就不写了。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值