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
:
| 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()
方法就可以发送给服务端。
| 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()
就是接受服务端的响应。
| func (c *Client) dealResponse() {
for {
buf := make([]byte, 1024)
c.conn.Read(buf)
fmt.Println(buf)
}
}
|
但是我们也可以用 io.Copy()
来替代,而且io.Copy()
是永久阻塞的。
| // 与上面例子等价
func (c *Client) dealResponse() {
io.Copy(os.Stdout, c.conn)
}
|
总结
- Server 启动后监听 8088 端口
- Client 启动后向 Server 请求,通过 8088 端口
- Server 接收请求后开启一个 user goroutine,和 Client 建立起连接
- Server 接着启动一个 handler goroutine 处理 Client 发来的请求
- handler 中做着各种具体的处理,例如群发,handler 会向 Message chan 发送一条用户上线的消息
- broadcast 会一直监听 Message chan,一有消息就循环把消息放到每一个 User chan 中
- User goroutine 会一直监听 User chan,收到消息就通过 conn.Wrtie() 响应给 Client
- Client 的 delRes goroutine 收到响应后就把消息打印在控制台上。
--- 系列完结 ---