构建一个简单的网络聊天室:从零到运行

构建一个简单的网络聊天室:从零到运行

一、项目概述

我们的聊天室项目主要包含以下几个核心功能:

  1. 用户注册与登录:用户通过输入唯一的网名进入聊天室,如果网名已被占用,则提示重新输入。
  2. 实时消息传输:用户可以发送和接收消息,所有消息都会实时显示在聊天窗口中。
  3. 心跳机制:客户端定期向服务器发送心跳信号,以保持连接状态。
  4. 排行榜功能:用户可以查询当前聊天室内的排行榜信息。
  5. 优雅断开连接:用户离开时,系统会自动更新状态并通知其他用户。

整个项目分为客户端和服务器端两部分。客户端负责用户交互,服务器端负责处理用户连接、消息转发和状态管理。

二、技术栈

  • 编程语言:Go
  • 网络通信:TCP 协议
  • 数据编码:自定义二进制协议
  • 并发处理:Go 协程
  • 存储:Redis(用于排行榜功能)

三、客户端实现

客户端是用户与聊天室交互的入口。它负责处理用户输入、发送消息、接收消息以及与服务器的通信。

(一)网名注册流程

用户首次进入聊天室时,需要输入一个唯一的网名。客户端将网名发送到服务器进行验证。如果网名已被占用,服务器会返回提示信息,客户端会要求用户重新输入。

for {
    fmt.Printf("输入你的网名: ")
    text := bufio.NewReader(os.Stdin)
    nickname, _ := text.ReadString('\n')
    nickname = strings.TrimSpace(nickname)

    if nickname == "" {
        fmt.Println("网名不能为空,请重新输入!")
        continue
    }

    encodedNickname, err := proto.Encode(nickname)
    if err != nil {
        fmt.Println("Error encoding nickname:", err)
        return
    }

    if _, err = conn.Write(encodedNickname); err != nil {
        fmt.Println("Error sending nickname:", err)
        return
    }

    reader := bufio.NewReader(conn)
    response, err := proto.Decode(reader)
    if err != nil {
        fmt.Println("Error reading response:", err)
        return
    }

    if response == "网名已被占用,请重新输入。\n" {
        fmt.Println("网名已被占用,请重新输入。")
        continue
    }
    fmt.Println("成功进入")
    break
}

(二)心跳机制

为了保持客户端与服务器的连接状态,客户端会定期发送心跳信号。心跳信号是一个简单的字符串 "HEARTBEAT",每隔 15 秒发送一次。

go func() {
    ticker := time.NewTicker(15 * time.Second)
    defer ticker.Stop()
    for {
        select {
        case <-ticker.C:
            heartbeat, err := proto.Encode("HEARTBEAT")
            if err != nil {
                fmt.Println("心跳编码错误:", err)
                return
            }
            if _, err := conn.Write(heartbeat); err != nil {
                fmt.Println("发送心跳失败,连接可能已关闭")
                return
            }
        }
    }
}()

(三)消息接收与发送

客户端通过协程实时接收服务器发送的消息,并将其显示在聊天窗口中。同时,用户输入的消息会被编码并发送到服务器。

// 消息接收协程
go func() {
    reader := bufio.NewReader(conn)
    for {
        message, err := proto.Decode(reader)
        if err != nil {
            fmt.Println("\n与服务器的连接已断开")
            os.Exit(1)
        }
        fmt.Print(message)
    }
}()

// 消息发送循环
for {
    text, _ := bufio.NewReader(os.Stdin).ReadString('\n')
    if strings.TrimSpace(text) == "exit" {
        fmt.Println("退出聊天室")
        return
    }
    if strings.TrimSpace(text) == "/ranking" {
        // 发送查询命令
        proto.EncodeAndSend(conn, "/ranking")
        continue
    }
    if strings.TrimSpace(text) == "" {
        continue
    }

    encodedMsg, err := proto.Encode(text)
    if err != nil {
        fmt.Println("消息编码错误:", err)
        continue
    }

    if _, err := conn.Write(encodedMsg); err != nil {
        fmt.Println("消息发送失败:", err)
        return
    }
}

四、服务器端实现

服务器端是聊天室的核心,负责处理客户端的连接、消息转发和状态管理。服务器端的代码较为复杂,涉及多个功能模块,包括用户管理、消息广播、排行榜维护等。以下是服务器端的关键代码片段:

(一)用户连接管理

服务器端使用一个 clients 映射来管理所有连接的客户端。每当有新的客户端连接时,将其添加到映射中;客户端断开连接时,从映射中移除。

func handleDisconnect(nickname string, err error) {
    mu.Lock()
    delete(clients, nickname)
    mu.Unlock()

    // 删除排行榜中的用户数据
    if _, zremErr := rdb.ZRem(ctx, RankingKey, nickname).Result(); zremErr != nil {
        fmt.Printf("删除用户 %s 排行榜信息失败: %v\n", nickname, zremErr)
    }

    // 构造离开消息
    leaveMsg := ChatMessage{
        Nickname: "系统",
        Content:  fmt.Sprintf("%s 离开了聊天室", nickname),
        SentAt:   time.Now().Unix(),
    }
    if msgData, err := json.Marshal(leaveMsg); err == nil {
        rdb.LPush(ctx, "chat_messages", msgData)
    }

    fmt.Printf("%s 断开连接: %v\n", nickname, err)
}

(二)消息广播

服务器端接收客户端发送的消息,并将其广播给所有连接的客户端。同时,服务器会将消息存储到 Redis 中,以便后续查询。

func broadcastMessage(msg ChatMessage) {
    msgData, err := json.Marshal(msg)
    if err != nil {
        fmt.Println("消息编码失败:", err)
        return
    }

    rdb.LPush(ctx, "chat_messages", msgData)

    for _, client := range clients {
        if _, err := client.Write(msgData); err != nil {
            fmt.Println("消息发送失败:", err)
        }
    }
}

(三)排行榜功能

服务器端使用 Redis 的有序集合(Sorted Set)来维护排行榜信息。每当用户发送消息时,服务器会更新用户的排名。

func updateRanking(nickname string) {
    score, err := rdb.ZScore(ctx, RankingKey, nickname).Result()
    if err != nil {
        fmt.Printf("获取用户 %s 排名失败: %v\n", nickname, err)
        return
    }

    newScore := score + 1
    if _, err := rdb.ZAdd(ctx, RankingKey, &redis.Z{
        Score:  newScore,
        Member: nickname,
    }).Result(); err != nil {
        fmt.Printf("更新用户 %s 排名失败: %v\n", nickname, err)
    }
}

五、数据传输协议

为了高效地传输数据,我们设计了一个简单的二进制协议。协议的格式如下:

  • 长度字段:4 字节,表示消息内容的长度。
  • 消息内容:可变长度,实际消息内容。

编码和解码函数如下:

func Encode(message string) ([]byte, error) {
    msgBytes := []byte(message)
    length := int32(len(msgBytes))

    pkg := new(bytes.Buffer)
    if err := binary.Write(pkg, binary.LittleEndian, length); err != nil {
        return nil, err
    }

    if err := binary.Write(pkg, binary.LittleEndian, msgBytes); err != nil {
        return nil, err
    }
    return pkg.Bytes(), nil
}

func Decode(reader *bufio.Reader) (string, error) {
    lengthBytes, err := reader.Peek(4)
    if err != nil {
        return "", err
    }

    lengthBuff := bytes.NewBuffer(lengthBytes)
    var length int32
    if err := binary.Read(lengthBuff, binary.LittleEndian, &length); err != nil {
        return "", err
    }

    if int32(reader.Buffered()) < 4+length {
        return "", errors.New("数据不完整")
    }

    packet := make([]byte, 4+length)
    if _, err := reader.Read(packet); err != nil {
        return "", err
    }

    return string
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值