Skip to content

IM11-发送请求

首先作为一个客户端,我们可以打印一张菜单,告诉用户各种选项,让用户输入选择,然后我们再根据用户的选择执行不同的动作。

目前服务端具备群聊、私聊、重命名、查询在线用户4种功能,那么客户端应该具备发送这 4 种指令的能力。

我们可以先加个菜单显示

菜单

func (c *Client) menu() bool {
    var choice int

    fmt.Println("1. 群聊模式")
    fmt.Println("2. 私聊模式")
    fmt.Println("3. 更新用户名")
    fmt.Println("4. 查看在线用户")
    fmt.Println("0. 退出")
    fmt.Scanln(&choice)

    if !(choice >= 0 && choice <= 4) {
        fmt.Println("请输入合法数字[0-4]:")
        return false
    }

    c.choice = choice
    return true
}

有了菜单,就需要接收用户的输入,所以我们在 Client 结构体中增加一个字段 choice

1
2
3
4
5
6
7
type Client struct {
    Name       string
    ServerIP   string
    ServerPort int
    choice     int    // 增加字段
    conn       net.Conn
}

有了菜单,接下来就可以写一个 run() 一直监听用户的输入,接受到用户的输入后检查是不是在 0-4 之间,然后进入 switch 调用不同的方法。

func (c *Client) run() {
    for {
        for c.menu() == false {
        }
        switch c.choice {
        case 1: // 群聊模式
            c.groupChat()
        case 2: // 私聊模式
            c.privateChat()
        case 3: // 修改用户名
            c.rename()
        case 4: // 打印在线用户
            c.showOnlineUser()
        case 0: // 退出
            os.Exit(1)
        }
    }
}
搞定之后,在 main() 函数中调用 run() 函数就可以进入菜单了。

func main() {

    client := NewClient(serverIP, serverPort)
    if client == nil {
        log.Println(">>>>> 连接服务器失败...")
        return
    }
    log.Println(">>>>> 连接服务器成功...")
    go client.dealResponse()

    // 启动客户端业务
    client.run()
}

菜单部分这样就处理好了,下面就可以开始编写 c.groupChat()、c.privateChat()、c.rename()、c.showOnlineUser() 了。

发送请求

查看在线用户

我们先从查看在线用户的 showOnlineUser() 开始,这个比较简单。

用户输入 "4" 之后会调用 c.showOnlineUser(),我们只需要将之前约定好的(用 nc 命令模拟客户端的时候我们约定过指令,《IM5-在线用户查询功能》)指令 alluser, 通过 conn 对象的 Write() 方法就可以发送给服务端。

1
2
3
4
5
6
7
8
func (c *Client) showOnlineUser() {
    showInstruction := "alluser"    // 定义指令
    _, err := c.conn.Write([]byte(showInstruction)) // 发送给服务端
    if err != nil {
        fmt.Println("查看在线用户操作请求失败!")
        fmt.Println("conn.Write err:", err)
    }
}

重命名

和上面 showOnlineUser() 一个道理,只不过我们要多接受一个参数 NEWNAME。重命名的指令是 rename NEWNAME

第一步要让用户输入新的名字,然后拼接指令,发送,完事儿。

func (c *Client) rename() {
    fmt.Println("请输入用户名:")
    fmt.Scanln(&c.Name)    // 接收用户输入的新名字

    renameInstruction := "rename " + c.Name    // 拼接
    _, err := c.conn.Write([]byte(renameInstruction)) // 发送重命名的请求给服务器
    if err != nil {
        fmt.Println("rename 操作请求失败!")
        fmt.Println("conn.Write err:", err)
    }
}

群发消息和私聊模式

举一反三,私聊的指令是 -> USERNAME MESSAGE,需要两个参数,那就接收两次参数,拼接,发送。

为了不用发每一条消息都输入用户名,我们可以在输入用户名之后进入死循环,不断接收聊天内容,并约定输入 <- 时返回菜单。

func (c *Client) privateChat() {
    var (
        content string
        friend  string
    )

    fmt.Println("请输入好友名称,输入<-返回菜单:")
    fmt.Scanln(&friend)    // 输入好友名称

    for friend != "<-" {
        fmt.Println("请输入内容,输入<-返回菜单:")
        fmt.Scanln(&content)    // 输入聊天内容

        for content != "<-" {
            privChatInstruction := "-> " + friend + " " + content    // 拼接指令

            _, err := c.conn.Write([]byte(privChatInstruction))      // 发送请求
            if err != nil {
                fmt.Println("私聊操作请求失败!")
                fmt.Println("conn.Write err:", err)
                break
            }

            fmt.Println("请输入内容,输入<-返回菜单:")
            fmt.Scanln(&content)    // 输入聊天内容
        }
        break
    }
}
func (c *Client) groupChat() {
    var content string

    fmt.Println("请输入群聊消息, 输入<-返回菜单:")
    fmt.Scanln(&content)    // 输入群发内容

    for content != "<-" {
        if len(content) != 0 {

            _, err := c.conn.Write([]byte(content))    // 发送群发内容
            if err != nil {
                fmt.Println("群聊操作请求失败!")
                fmt.Println("conn.Write err:", err)
                break
            }
        }
        content = ""    // 消息内容变量重置

        fmt.Println("请输入群聊消息, 输入<-返回菜单:")
        fmt.Scanln(&content)    // 输入群发内容
    }
}

监听服务器发来的消息

上面写了很多指令,都是发送请求的,但是我们的客户端还不具备接收服务器的响应的功能,所以我们还需要开启一个 goroutine 来监听服务器的响应。

于是我们可以写一个 dealResponse() 函数,用户打印服务器响应回来的内容。

conn.Write() 是发送请求给服务端,那么conn.Read() 就是接受服务端的响应。

1
2
3
4
5
6
7
func (c *Client) dealResponse() {
    for {
        buf := make([]byte, 1024)
        c.conn.Read(buf)
        fmt.Println(buf)
    }
}

但是我们也可以用 io.Copy() 来替代,而且io.Copy() 是永久阻塞的。

1
2
3
4
// 与上面例子等价
func (c *Client) dealResponse() {
    io.Copy(os.Stdout, c.conn)
}

总结

  1. Server 启动后监听 8088 端口
  2. Client 启动后向 Server 请求,通过 8088 端口
  3. Server 接收请求后开启一个 user goroutine,和 Client 建立起连接
  4. Server 接着启动一个 handler goroutine 处理 Client 发来的请求
  5. handler 中做着各种具体的处理,例如群发,handler 会向 Message chan 发送一条用户上线的消息
  6. broadcast 会一直监听 Message chan,一有消息就循环把消息放到每一个 User chan 中
  7. User goroutine 会一直监听 User chan,收到消息就通过 conn.Wrtie() 响应给 Client
  8. Client 的 delRes goroutine 收到响应后就把消息打印在控制台上。

--- 系列完结 ---