Go语言实战: 即时通信系统(未完)
使用Go语言构建一个即时通信系统,旨在锻炼Go语言编程能力
该通信系统至少能够允许用户能够在客户端进行公聊,即所发消息能被所有用户看到,也可发起私聊(即两个用户之间私密通信)。同时,用户能够看到当前有哪些用户在线,强制将某些用户下线。
程序的架构如下:
用户通过客户端去向服务端发起连接,服务端维护一个map,来记录上线的用户,同时定义一个通道,来向每一个用户对应的goroutine发送消息。每有一个用户上线,都会调用一个goroutine去处理。
基础Server构建
首先简单实现一个TCP服务器,将结构体和方法写在一个server.go文件中
创建了一个结构体,为其定义一些方法
// server.go
package main
import (
"bufio"
"fmt"
"log"
"net"
)
type Server struct {
IP string // IP地址
Port int // 端口号
}
// NewServer 创建一个服务器
func NewServer(ip string, port int) *Server {
server := &Server{
IP: ip,
Port: port,
}
return server
}
// handler 处理连接
func (serv *Server) handler(conn net.Conn) {
input := bufio.NewScanner(conn)
who := fmt.Sprintf("(%s)", conn.RemoteAddr().String())
for input.Scan() {
log.Println(who + ": " + input.Text())
fmt.Fprintln(conn, "Repeat " + who + ": "+ input.Text())
}
}
// Start 启动服务器
func (serv *Server) Start() {
// 监听TCP端口
listener, err := net.Listen("tcp",
fmt.Sprintf("%s:%d", serv.IP, serv.Port))
if err != nil {
fmt.Println(err)
return
}
defer listener.Close() // 关闭连接
for {
// 接收连接
conn, err := listener.Accept()
if err != nil {
log.Println(err)
continue
}
go serv.handler(conn)
}
}
在main.go中调用
package main
func main() {
server := NewServer("127.0.0.1", 9091)
server.Start()
}
当有连接请求时,accept接收这个请求,服务器程序会调一个goroutine来为这个请求服务。
goroutine执行handler函数,这个函数在服务端的终端打印出客户端发送的信息,同时将这个信息也发回客户端(就像回声)。
运行该程序,同时使用nc来连接这个服务器,输入"hello"
$ nc 127.0.0.1 9091
hello
Repeat (127.0.0.1:47594): hello
用户上线及广播
因为要记录上线了哪些用户,服务端就要创建一个map来记录当前有哪些用户上线。每次读写这个map都要加锁,避免并发安全问题。
新建user.go,定义一个用户的结构体User
// user.go
package main
import "net"
type User struct {
Name string // 名称
Addr string // 地址
ch chan string // 通道 用来向客户端发送消息
conn net.Conn // 连接
}
// NewUser 创建一个用户
func NewUser(conn net.Conn) *User {
userAddr := conn.RemoteAddr().String()
// 创建结构体
user := &User{
Name: userAddr,
Addr: userAddr,
ch: make(chan string),
conn: conn,
}
go user.ListenMessage()
return user
}
// ListenMessage 监听当前user的通道
func (user *User) ListenMessage() {
for {
msg := <-user.ch // 从通道中取出消息
user.conn.Write([]byte(msg + "\n")) // 发送给客户端
}
}
每个用户对象绑定一个通道ch,这个ch是专门向用户对应的client来发送消息的。User结构体有对应的方法ListenMessage来监听这个通道,若ch中有消息则发送到客户端。
与此同时修改server结构体
type Server struct {
IP string
Port int
OnlineMap map[string]*User // 在线用户列表
mapLock sync.RWMutex // 读写锁
Message chan string // 广播消息
}
于是创建Server的方法也要修改
// NewServer 创建一个服务器
func NewServer(ip string, port int) *Server {
server := &Server{
IP: ip,
Port: port,
OnlineMap: make(map[string]*User),
Message: make(chan string),
}
return server
}
修改原来的handler方法,提醒有用户上线
func (serv *Server) handler(conn net.Conn) {
addr := conn.RemoteAddr().String()
log.Println(addr + " is connecting")
// 创建用户
user := NewUser(conn)
// 加入到map
serv.mapLock.Lock()
serv.OnlineMap[user.Name] = user
serv.mapLock.Unlock()
// 广播上线消息
serv.BroadCast(user, " has arrived")
// 接收消息
input := bufio.NewScanner(conn)
for input.Scan() {
log.Println(addr + ": " + input.Text())
}
}
实现BoardCast方法,作用是广播
func (serv *Server) BroadCast(user *User, msg string) {
sendMsg := "[" + user.Addr + "]" + user.Name + ":" + msg
serv.Message <- sendMsg // 向服务器广播消息通道发送字符串
}
编写一个方法,监听代表广播消息的通道
func (serv *Server) ListenMessage() {
for {
msg := <-serv.Message
// 将msg发送给全部在线的User
serv.mapLock.Lock()
for _, cli := range serv.OnlineMap {
cli.ch <- msg
}
serv.mapLock.Unlock()
}
}
在Start方法中调用这个ListenMessage
// Start 启动服务器
func (serv *Server) Start() {
// 监听TCP端口
listener, err := net.Listen("tcp",
fmt.Sprintf("%s:%d", serv.IP, serv.Port))
if err != nil {
fmt.Println(err)
return
}
defer listener.Close()
// 启动监听Message的goroutine
go serv.ListenMessage()
for {
// 接收连接
conn, err := listener.Accept()
if err != nil {
log.Println(err)
continue
}
go serv.handler(conn)
}
}
此处服务端有一个通道Message,放置广播消息(向所有用户发送的消息),Server的方法ListenMessage会监听这个通道,一旦有消息,便将其写入每一个User的ch通道,达到广播的目的。
上述的两个LisenMessage都是在独立的goroutine中运行的,它们从原先的goroutine中分离出去,时时刻刻监视着指定的通道。
客户端显示
$ nc 127.0.0.1 9091
[127.0.0.1:54312] 127.0.0.1:54312 has arrived
hello
Repeat (127.0.0.1:54312): hello
bye
Repeat (127.0.0.1:54312): bye
再开一个终端
$ nc 127.0.0.1 9091
[127.0.0.1:54332] 127.0.0.1:54332 has arrived
原先的客户端显示
$ nc 127.0.0.1 9091
[127.0.0.1:54312] 127.0.0.1:54312 has arrived
hello
Repeat (127.0.0.1:54312): hello
bye
Repeat (127.0.0.1:54312): bye
[127.0.0.1:54332] 127.0.0.1:54332 has arrived
用户消息广播
之前都是服务端将用户从客户端输入的消息原封不动地发给用户,现在要将用户所发的消息广播到每一个用户,实现"公聊"。
只要稍微修改原先的handler方法,在该方法中调起一个单独的goroutine来专门接收客户端发来的消息,然后将消息广播
func (serv *Server) handler(conn net.Conn) {
addr := conn.RemoteAddr().String()
log.Println(addr + " is connecting")
// 创建用户
user := NewUser(conn)
// 加入到map
serv.mapLock.Lock()
serv.OnlineMap[user.Name] = user
serv.mapLock.Unlock()
// 广播上线消息
serv.BroadCast(user, " has arrived")
// 接收消息
go func() {
input := bufio.NewScanner(conn)
for input.Scan() {
log.Println(addr + ": " + input.Text())
serv.BroadCast(user, input.Text())
}
}()
}
客户端
$ nc 127.0.0.1 9091
[127.0.0.1:55076] 127.0.0.1:55076 has arrived
hello
[127.0.0.1:55076] 127.0.0.1:55076: hello
[127.0.0.1:55078] 127.0.0.1:55078 has arrived
[127.0.0.1:55078] 127.0.0.1:55078: hello
My name is Alice
[127.0.0.1:55076] 127.0.0.1:55076: My name is Alice
[127.0.0.1:55078] 127.0.0.1:55078: My name is Bob
Nice to meet you,Bob
[127.0.0.1:55076] 127.0.0.1:55076: Nice to meet you,Bob
[127.0.0.1:55078] 127.0.0.1:55078: Nice to meet you,too
$ nc 127.0.0.1 9091
[127.0.0.1:55078] 127.0.0.1:55078 has arrived
hello
[127.0.0.1:55078] 127.0.0.1:55078: hello
[127.0.0.1:55076] 127.0.0.1:55076: My name is Alice
My name is Bob
[127.0.0.1:55078] 127.0.0.1:55078: My name is Bob
[127.0.0.1:55076] 127.0.0.1:55076: Nice to meet you,Bob
Nice to meet you,too
[127.0.0.1:55078] 127.0.0.1:55078: Nice to meet you,too
业务封装
将处理User上下线和处理消息的功能都封装到User的方法中
在User结构体中加上serv字段,表示该用户属于哪一个服务器
type User struct {
Name string
Addr string
ch chan string
conn net.Conn
serv *Server
}
在创建该User时就进行赋值
// NewUser 创建一个用户
func NewUser(conn net.Conn, serv *Server) *User {
userAddr := conn.RemoteAddr().String()
user := &User{
Name: userAddr,
Addr: userAddr,
ch: make(chan string),
conn: conn,
serv: serv,
}
go user.ListenMessage() // 调用一个goroutine监听消息
return user
}
添加处理上线的方法
// Offline 用户下线
func (user *User) Offline() {
// 从map去除
user.serv.mapLock.Lock()
delete(user.serv.OnlineMap, user.Name)
user.serv.mapLock.Unlock()
// 广播上线消息
user.serv.BroadCast(user, " has left")
}
然后是下线方法
相对应的,要修改原先的handler方法
func (serv *Server) handler(conn net.Conn) {
addr := conn.RemoteAddr().String()
log.Println(addr + " is connecting")
// 创建用户
user := NewUser(conn, serv)
user.Online() // 用户上线
// 接收消息
go func() {
input := bufio.NewScanner(conn)
for input.Scan() {
log.Println(addr + ": " + input.Text())
// 处理用户消息
user.DoMessage(input.Text())
}
user.Offline() // 用户下线
log.Println(addr + " has been disconnected")
}()
}
客户端
$ nc 127.0.0.1 9091
[127.0.0.1:55464] 127.0.0.1:55464 has arrived
hello
[127.0.0.1:55464] 127.0.0.1:55464: hello
[127.0.0.1:55476] 127.0.0.1:55476 has arrived
[127.0.0.1:55476] 127.0.0.1:55476: hello
bye
[127.0.0.1:55464] 127.0.0.1:55464: bye
^C
$ nc 127.0.0.1 9091
[127.0.0.1:55476] 127.0.0.1:55476 has arrived
hello
[127.0.0.1:55476] 127.0.0.1:55476: hello
[127.0.0.1:55464] 127.0.0.1:55464: bye
[127.0.0.1:55464] 127.0.0.1:55464 has left
在线用户查询
使用户能够查看当前有哪些用户上线
func (user *User) DoMessage(msg string) {
if msg == "$who" {
// 查询在线用户
user.ch <- "Online users:"
user.serv.mapLock.Lock()
for _, u := range user.serv.OnlineMap {
user.ch <- "[" + u.Addr + "] " + u.Name
}
user.serv.mapLock.Unlock()
return
}
user.serv.BroadCast(user, ": "+msg)
}
当用户输入$who就不会被广播,而是将其当作一条指令,显示当前在线的用户
测试
$ nc 127.0.0.1 9091
[127.0.0.1:55648] 127.0.0.1:55648 has arrived
[127.0.0.1:55654] 127.0.0.1:55654 has arrived
$who
Online users:
[127.0.0.1:55646] 127.0.0.1:55646
[127.0.0.1:55648] 127.0.0.1:55648
[127.0.0.1:55654] 127.0.0.1:55654
修改用户名
允许用户修改自己的用户名
只要修改DoMessage方法
func (user *User) DoMessage(msg string) {
// 执行指令
if msg[0] == '$' {
if msg == "$who" {
// 查询在线用户
user.ch <- "Online users:"
user.serv.mapLock.Lock()
for _, u := range user.serv.OnlineMap {
user.ch <- "[" + u.Addr + "] " + u.Name
}
user.serv.mapLock.Unlock()
} else if len(msg) > 7 && msg[:7] == "$rename" {
// 修改用户名称
name := strings.Split(msg, ":")[1] // 字符串切割,以:为分界
_, ok := user.serv.OnlineMap[name] // 检查name是否已经存在
if ok {
user.ch <- "User name already exists"
} else {
user.serv.mapLock.Lock()
delete(user.serv.OnlineMap, user.Name) // 删除原先的name
user.serv.OnlineMap[name] = user // 添加新的name
user.ch <- "Your name has been updated: " + name
user.Name = name
user.serv.mapLock.Unlock()
}
} else {
// 提示信息
user.ch <- "Wrong instruction"
user.ch <- "$who:view online users\n$rename:modify name"
}
return
}
user.serv.BroadCast(user, ": "+msg)
}
如果开头是$那么就将其识别为一条指令
输入按照格式$rename:name
就将用户名修改为name
测试:
$ nc 127.0.0.1 9091
[127.0.0.1:56478] 127.0.0.1:56478 has arrived
$rename:David
Your name has been updated: David
hello
[127.0.0.1:56478] David: hello
$who
Online users:
[127.0.0.1:56478] David
超时强踢
如果一个用户长时间不发消息,那么就会被强行下线,以防无故占用资源。
这将在handler方法中实现
func (serv *Server) handler(conn net.Conn) {
addr := conn.RemoteAddr().String()
log.Println(addr + " is connecting")
// 创建用户
user := NewUser(conn, serv)
user.Online() // 用户上线
active := make(chan struct{}) // 是否活跃
// 接收消息
go func() {
input := bufio.NewScanner(conn)
for input.Scan() {
log.Println(addr + ": " + input.Text())
// 处理用户消息
user.DoMessage(input.Text())
// 用户的任意消息 都代表用户当前活跃
active <- struct{}{}
}
user.Offline()
log.Println(addr + " has been disconnected")
}()
for {
select {
case <-active:
// 激活select便会 重置定时器
case <-time.After(time.Second * 10):
// 超时
user.conn.Write([]byte("You've been kicked out\n"))
delete(serv.OnlineMap, user.Name) // 在map中删除
close(user.ch) // 关闭通道
conn.Close() // 关闭连接
return // 返回这个函数
}
}
}
使用for+select来对通道进行监视,在此之前创建了一个struct{}类型(该类型一般用作信号的传递)的通道active,每次用户有消息时,便会往其中写入一个struct{}{},那么在select中,active就不会被阻塞,于是整个select就不会阻塞,此时刷新定时器,重新计时。如果没有消息,那么active就会阻塞,导致整个select会被阻塞,从而开始定时器开始计时,达到阈值时,便会释放该用户的资源。最后一定要将对应的handler返回,否则会引发panic。
测试
$ nc 127.0.0.1 9091
[127.0.0.1:57332] 127.0.0.1:57332 has arrived
[127.0.0.1:57334] 127.0.0.1:57334 has arrived
You've been kicked out
目前的完整代码
server.go 文件内容
// server.go
package main
import (
"bufio"
"fmt"
"log"
"net"
"sync"
"time"
)
type Server struct {
IP string
Port int
OnlineMap map[string]*User
mapLock sync.RWMutex
Message chan string
}
// NewServer 创建一个服务器
func NewServer(ip string, port int) *Server {
server := &Server{
IP: ip,
Port: port,
OnlineMap: make(map[string]*User),
Message: make(chan string),
}
return server
}
func (serv *Server) ListenMessage() {
for {
msg := <-serv.Message
// 将msg发送给全部在线的User
serv.mapLock.Lock()
for _, cli := range serv.OnlineMap {
cli.ch <- msg
}
serv.mapLock.Unlock()
}
}
func (serv *Server) BroadCast(user *User, msg string) {
sendMsg := "[" + user.Addr + "] " + user.Name + msg
serv.Message <- sendMsg
}
func (serv *Server) handler(conn net.Conn) {
addr := conn.RemoteAddr().String()
log.Println(addr + " is connecting")
// 创建用户
user := NewUser(conn, serv)
user.Online() // 用户上线
active := make(chan struct{}) // 是否活跃
// 接收消息
go func() {
input := bufio.NewScanner(conn)
for input.Scan() {
log.Println(addr + ": " + input.Text())
// 处理用户消息
user.DoMessage(input.Text())
// 用户的任意消息 都代表用户当前活跃
active <- struct{}{}
}
user.Offline()
log.Println(addr + " has been disconnected")
}()
for {
select {
case <-active:
// 激活select便会 重置定时器
case <-time.After(time.Second * 10):
// 超时
user.conn.Write([]byte("You've been kicked out\n"))
// 释放资源
delete(serv.OnlineMap, user.Name)
close(user.ch)
conn.Close()
return // 返回这个函数
}
}
}
// Start 启动服务器
func (serv *Server) Start() {
// 监听TCP端口
listener, err := net.Listen("tcp",
fmt.Sprintf("%s:%d", serv.IP, serv.Port))
if err != nil {
fmt.Println(err)
return
}
defer listener.Close()
// 启动监听Message的goroutine
go serv.ListenMessage()
for {
// 接收连接
conn, err := listener.Accept()
if err != nil {
log.Println(err)
continue
}
go serv.handler(conn)
}
}
user.go 文件内容
package main
import (
"net"
"strings"
)
type User struct {
Name string
Addr string
ch chan string
conn net.Conn
serv *Server
}
// NewUser 创建一个用户
func NewUser(conn net.Conn, serv *Server) *User {
userAddr := conn.RemoteAddr().String()
user := &User{
Name: userAddr,
Addr: userAddr,
ch: make(chan string),
conn: conn,
serv: serv,
}
go user.ListenMessage()
return user
}
// Online 用户上线
func (user *User) Online() {
// 加入到map
user.serv.mapLock.Lock()
user.serv.OnlineMap[user.Name] = user
user.serv.mapLock.Unlock()
// 广播上线消息
user.serv.BroadCast(user, " has arrived")
}
// Offline 用户下线
func (user *User) Offline() {
// 从map去除
user.serv.mapLock.Lock()
delete(user.serv.OnlineMap, user.Name)
user.serv.mapLock.Unlock()
// 广播上线消息
user.serv.BroadCast(user, " has left")
}
func (user *User) DoMessage(msg string) {
// 执行指令
if msg[0] == '$' {
if msg == "$who" {
// 查询在线用户
user.ch <- "Online users:"
user.serv.mapLock.Lock()
for _, u := range user.serv.OnlineMap {
user.ch <- "[" + u.Addr + "] " + u.Name
}
user.serv.mapLock.Unlock()
} else if len(msg) > 7 && msg[:7] == "$rename" {
// $rename:name
name := strings.Split(msg, ":")[1]
_, ok := user.serv.OnlineMap[name]
if ok {
user.ch <- "User name already exists"
} else {
user.serv.mapLock.Lock()
delete(user.serv.OnlineMap, user.Name)
user.serv.OnlineMap[name] = user
user.ch <- "Your name has been updated: " + name
user.Name = name
user.serv.mapLock.Unlock()
}
} else {
user.ch <- "Wrong instruction"
user.ch <- "$who:view online users\n$rename:modify name"
}
return
}
user.serv.BroadCast(user, ": "+msg)
}
// ListenMessage 监听当前user
func (user *User) ListenMessage() {
for {
msg := <-user.ch
user.conn.Write([]byte(msg + "\n"))
}
}
main.go文件内容
package main
func main() {
server := NewServer("127.0.0.1", 9091)
server.Start()
}
私聊功能
目前用户所发的消息都被广播,下面要实现一个私聊功能,一个用户可以私发消息给指定的用户。
在DoMessage中添加一个分支
当用户按照$to name:message
格式输入便会向name对应的用户的通道发送message。
func (user *User) DoMessage(msg string) {
// 执行指令
if msg[0] == '$' {
if msg == "$who" {
// 查询在线用户
user.ch <- "Online users:"
user.serv.mapLock.Lock()
for _, u := range user.serv.OnlineMap {
user.ch <- "[" + u.Addr + "] " + u.Name
}
user.serv.mapLock.Unlock()
} else if len(msg) > 7 && msg[:7] == "$rename" {
// $rename:name
name := strings.Split(msg, ":")[1]
_, ok := user.serv.OnlineMap[name]
if ok {
user.ch <- "User name already exists"
} else {
user.serv.mapLock.Lock()
delete(user.serv.OnlineMap, user.Name)
user.serv.OnlineMap[name] = user
user.ch <- "Your name has been updated: " + name
user.Name = name
user.serv.mapLock.Unlock()
}
} else if len(msg) > 4 && msg[:4] == "$to " {
name := strings.Split(msg, ":")[0][4:] // 接收者
send := strings.Split(msg, ":")[1] // 消息内容
receiver, ok := user.serv.OnlineMap[name]
if ok {
receiver.ch <- user.Name + " to you: " + send
user.ch <- "send ok"
} else {
user.ch <- "The user does not exist"
}
} else {
user.ch <- "Wrong instruction"
user.ch <- "$who:view online users\n$rename:modify name"
}
return
}
user.serv.BroadCast(user, ": "+msg)
}
测试
$ nc 127.0.0.1 9091
[127.0.0.1:43752] 127.0.0.1:43752 has arrived
[127.0.0.1:43760] 127.0.0.1:43760 has arrived
$rename:David
Your name has been updated: David
$who
Online users:
[127.0.0.1:43752] David
[127.0.0.1:43760] Alice
$to Alice:Hello
send ok
Alice to you: Hello
$ nc 127.0.0.1 9091
[127.0.0.1:43760] 127.0.0.1:43760 has arrived
$rename:Alice
Your name has been updated: Alice
David to you: Hello
$to David:Hello
send ok
目前对用户的指令输入的处理并不可靠,用户意外的错误输入容易引发panic。
下面不再使用nc命令用于连接,而是编写一个客户端用于连接。
客户端实现
简单实现发起请求
package main
import (
"fmt"
"log"
"net"
)
type Client struct {
ServIP string // 服务器IP
ServPort int // 端口号
Name string // 名称
conn net.Conn // 连接
}
func NewClient(addr string, port int) *Client {
// 创建客户端对象
clnt := &Client{
ServIP: addr,
ServPort: port,
}
// 发起请求 拼接地址和端口
conn, err := net.Dial("tcp",
fmt.Sprintf("%s:%d", addr, port))
if err != nil {
log.Println(err)
return nil
}
clnt.conn = conn
return clnt
}
func main() {
clnt := NewClient("127.0.0.1", 9091)
if clnt == nil {
return
}
fmt.Println("connect ok")
}
从命令行获取参数
使用flag包,这个模块的作用主要是解析命令行
package main
import (
"flag"
"fmt"
"log"
"net"
)
type Client struct {
ServIP string // 服务器IP
ServPort int // 端口号
Name string // 名称
conn net.Conn // 连接
}
var (
addr string
port int
)
func init() {
// flag.TypeVar(Type 指针, flag 名, 默认值, 帮助信息)
flag.StringVar(&addr, "IP", "127.0.0.1",
"Set the server IP address")
flag.IntVar(&port, "port", 9091,
"Set the server port number")
}
func NewClient(addr string, port int) *Client {
// 创建客户端对象
clnt := &Client{
ServIP: addr,
ServPort: port,
}
// 发起请求
conn, err := net.Dial("tcp",
fmt.Sprintf("%s:%d", addr, port))
if err != nil {
log.Println(err)
return nil
}
clnt.conn = conn
return clnt
}
func main() {
flag.Parse()
clnt := NewClient(addr, port)
if clnt == nil {
return
}
fmt.Println("connect ok")
}
init函数会比main函数先执行
flag.StringVar和flag.IntVar都满足格式:
flag.TypeVar(Type 指针, flag 名, 默认值, 帮助信息)
命令行中运行时,在程序之后加上-h就会显示帮助信息
$ ./client -h
Usage of ./client:
-IP string
Set the server IP address (default "127.0.0.1")
-port int
Set the server port number (default 9091)
菜单显示
实现一个菜单,更好的与用户交互,同时也能将用户的输入规范化
func (clnt *Client) menu() bool {
var choice int
fmt.Println("1.Public chat mode")
fmt.Println("2.Private chat mode")
fmt.Println("3.Update the user name")
fmt.Println("0.Exit")
fmt.Scanf("%d", &choice)
if choice >= 0 && choice <= 3 {
clnt.choice = choice
return true
} else {
fmt.Println("Illegal input")
return false
}
}
封装了主业务的方法
func (clnt *Client) Run() {
for clnt.choice != 0 {
for clnt.menu() != true {} // 如果不为true,则一直循环在这里
// 根据不同的模式处理不同业务
switch clnt.choice {
case 1: // 公聊模式
break
case 2: // 私聊模式
break
case 3: // 更新用户名
break
case 0: // 为0则循环结束
}
}
}
实现对应方法之前,先实现一个读取服务器消息的方法
// DoResponce 处理服务器消息
func (clnt *Client) DoResponce() {
io.Copy(os.Stdout, clnt.conn)
}
更新用户名
// UpdateName 更新用户名
func (clnt *Client) UpdateName() bool {
fmt.Println("Enter your new username: ")
fmt.Scanln(&clnt.Name)
msg := "$rename:" + clnt.Name + "\n"
_, err := clnt.conn.Write([]byte(msg))
if err != nil {
fmt.Println(err)
return false
}
return true
}
然后实现公聊
// PublicChat 公聊
func (clnt *Client) PublicChat() {
// 提示用户输入消息
var msg string
fmt.Println("[Public Mode]\nEnter '$exit' to exit")
for msg != "$exit" {
if len(msg) > 0 {
send := msg + "\n"
_, err := clnt.conn.Write([]byte(send))
if err != nil {
log.Println(err)
break
}
}
msg = ""
fmt.Scanln(&msg)
}
}
直接向服务器发送信息即可,客户端输入$exit则
实现私聊
// 显示所有在线用户
func (clnt *Client) SelectUser() {
send := "$who\n"
_, err := clnt.conn.Write([]byte(send))
if err != nil {
log.Println(err)
return
}
}
// PriavteChat 私聊
func (clnt *Client) PriavteChat() {
var name string
var msg string
fmt.Println("[Private Mode] Enter '$exit' to exit")
clnt.SelectUser()
fmt.Scanln(&name)
for msg != "$exit" {
if len(msg) > 0 {
send := "$to " + name + ":" + msg + "\n"
_, err := clnt.conn.Write([]byte(send))
if err != nil {
log.Println(err)
break
}
}
msg = ""
fmt.Scanln(&msg)
}
}
进入私聊模式时,先显示线上有哪些用户,然后客户端输入用户名,指定要私发给哪个用户。
功能测试
改名
connect ok
1.Public chat mode
2.Private chat mode
3.Update the user name
0.Exit
[127.0.0.1:49448] 127.0.0.1:49448 has arrived
3
Enter your new username: Bob
1.Public chat mode
2.Private chat mode
3.Update the user name
0.Exit
Your name has been updated: Bob
公聊
connect ok
1.Public chat mode
2.Private chat mode
3.Update the user name
0.Exit
[127.0.0.1:49438] 127.0.0.1:49438 has arrived
1
[Public Mode] Enter '$exit' to exit
hello,everyone
[127.0.0.1:49438] 127.0.0.1:49438: hello,everyone
私聊
connect ok
1.Public chat mode
2.Private chat mode
3.Update the user name
0.Exit
[127.0.0.1:49332] 127.0.0.1:49332 has arrived
3
Enter your new username: Bob
1.Public chat mode
2.Private chat mode
3.Update the user name
0.Exit
Your name has been updated: Bob
[127.0.0.1:49336] 127.0.0.1:49336 has arrived
2
[Private Mode] Enter '$exit' to exit
Online users:
[127.0.0.1:49336] Alice
[127.0.0.1:49332] Bob
Alice
Hello,Alice
send ok
$ nc 127.0.0.1 9091
[127.0.0.1:49336] 127.0.0.1:49336 has arrived
$rename:Alice
Your name has been updated: Alice
Bob to you: Hello,Alice
目前客户端的完整代码
package main
import (
"flag"
"fmt"
"io"
"log"
"net"
"os"
)
type Client struct {
ServIP string
ServPort int
Name string
conn net.Conn
choice int
}
var (
addr string
port int
)
func (clnt *Client) menu() bool {
var choice int
fmt.Println("1.Public chat mode")
fmt.Println("2.Private chat mode")
fmt.Println("3.Update the user name")
fmt.Println("0.Exit")
fmt.Scanf("%d", &choice)
if choice >= 0 && choice <= 3 {
clnt.choice = choice
return true
} else {
fmt.Println("Illegal input")
return false
}
}
// PublicChat 公聊
func (clnt *Client) PublicChat() {
// 提示用户输入消息
var msg string
fmt.Println("[Public Mode] Enter '$exit' to exit")
for msg != "$exit" {
if len(msg) > 0 {
send := msg + "\n"
_, err := clnt.conn.Write([]byte(send))
if err != nil {
log.Println(err)
break
}
}
msg = ""
fmt.Scanln(&msg)
}
}
func (clnt *Client) SelectUser() {
send := "$who\n"
_, err := clnt.conn.Write([]byte(send))
if err != nil {
log.Println(err)
return
}
}
// PriavteChat 私聊
func (clnt *Client) PriavteChat() {
var name string
var msg string
fmt.Println("[Private Mode] Enter '$exit' to exit")
clnt.SelectUser()
fmt.Scanln(&name)
for msg != "$exit" {
if len(msg) > 0 {
send := "$to " + name + ":" + msg + "\n"
_, err := clnt.conn.Write([]byte(send))
if err != nil {
log.Println(err)
break
}
}
msg = ""
fmt.Scanln(&msg)
}
}
// UpdateName 更新用户名
func (clnt *Client) UpdateName() bool {
fmt.Print("Enter your new username: ")
fmt.Scanln(&clnt.Name)
msg := "$rename:" + clnt.Name + "\n"
_, err := clnt.conn.Write([]byte(msg))
if err != nil {
fmt.Println(err)
return false
}
return true
}
// DoResponce 处理服务器消息
func (clnt *Client) DoResponce() {
io.Copy(os.Stdout, clnt.conn)
}
func (clnt *Client) Run() {
for clnt.choice != 0 {
for clnt.menu() != true {
}
// 根据不同的模式处理不同业务
switch clnt.choice {
case 1: // 公聊模式
clnt.PublicChat()
break
case 2: // 私聊模式
clnt.PriavteChat()
break
case 3: // 更新用户名
clnt.UpdateName()
break
case 0:
}
}
}
func init() {
flag.StringVar(&addr, "IP", "127.0.0.1",
"Set the server IP address")
flag.IntVar(&port, "port", 9091,
"Set the server port number")
}
func NewClient(addr string, port int) *Client {
// 创建客户端对象
clnt := &Client{
ServIP: addr,
ServPort: port,
choice: 999,
}
// 发起请求
conn, err := net.Dial("tcp",
fmt.Sprintf("%s:%d", addr, port))
if err != nil {
log.Println(err)
return nil
}
clnt.conn = conn
return clnt
}
func main() {
flag.Parse()
clnt := NewClient(addr, port)
if clnt == nil {
return
}
go clnt.DoResponce()
fmt.Println("connect ok")
clnt.Run()
select {}
}
反思
虽然粗糙地实现了想要的功能,但是问题却有很多。首先有三个比较明显的问题:
当有用户下线,服务端的CPU使用率飙升
由于并发,终端的输出存在问题,并发控制没有做好
用户输入合法性
上述是首先要解决的问题,将在后续进行处理
Go语言实战: 即时通信系统(未完)的更多相关文章
- 003 Python与类C语言的区别(未完)
#写在前面的话:重点记录Python的特点 Python特点: 1. 无分号断句 2. 不用担心溢出问题 3. if-else的用法不同 #if或else后面都要添加冒号: import random ...
- C++语言体系设计哲学的一些随想(未完待续)
对于静态类型语言,其本质目标在于恰当地操作数据,得到期望的值.具体而言,需要: (1)定义数据类型 你定义的数据是什么,是整形还是浮点还是字符.该类型的数据可以包含的值的范围是什么. (2)定义操作的 ...
- R语言实战(第2版)PDF完整版带书签目录
<R语言实战2>PDF+源代码 下载:https://pan.baidu.com/s/1gP_16Xq9eVmLJ1yOsWD9FA 提取码:l8dx 分享更多python数据分析相关电子 ...
- R 语言实战-Part 4 笔记
R 语言实战(第二版) part 4 高级方法 -------------第13章 广义线性模型------------------ #前面分析了线性模型中的回归和方差分析,前提都是假设因变量服从正态 ...
- Go web编程学习笔记——未完待续
1. 1).GOPATH设置 先设置自己的GOPATH,可以在本机中运行$PATH进行查看: userdeMacBook-Pro:~ user$ $GOPATH -bash: /Users/user/ ...
- 《Go语言实战》读书笔记
<Go语言实战>中文版pdf 百度网盘: https://pan.baidu.com/s/1kr-gMzaPAn8BFZG0P24Oiw 提取码: r6rt 书籍源码:https://gi ...
- MVC丶 (未完待续······)
希望你看了此小随 可以实现自己的MVC框架 也祝所有的程序员身体健康一切安好 ...
- R语言实战(二)数据管理
本文对应<R语言实战>第4章:基本数据管理:第5章:高级数据管理 创建新变量 #建议采用transform()函数 mydata <- transform(mydata, sumx ...
- swift语言实战晋级-第9章 游戏实战-跑酷熊猫-9-10 移除平台与视差滚动
9.9 移除场景之外的平台 用为平台是源源不断的产生的,如果不注意销毁,平台就将越积越多,虽然在游戏场景中看不到.几十个还看不出问题,那几万个呢?几百万个呢? 所以我们来看看怎么移除平台,那什么样的平 ...
- Swift语言实战晋级-第9章 游戏实战-跑酷熊猫-2 创建熊猫类
当我们创建好项目文件后我们就可以开始一步一步的按照我们之前列入的清单来编写我们的游戏.现在就让我们来创建一个熊猫这个类Panda.swift.我们将采取分解的方式,一步一步的完成Panda.swift ...
随机推荐
- 使用generator进行代码自动生成
在使用generator 进行逆向工程时,一下为步骤 添加mybatis generator的maven插件依赖 添加mybatis generator的配置文件:generatorConfig.xm ...
- 第二次python作业
#3.1 print("今有物不知其数,三三数之剩二,五五数之剩三,七七数之剩二,问几何?\n") number = int(input("请输入你认为符合条件的数: & ...
- 【前端工程化】使用Nexus 搭建前端 npm 私服
参考文献1 - https://blog.51cto.com/lookingdream/3609619 参考文献2 - https://blog.csdn.net/u013278374/article ...
- Ansible 工具参数详解自动化运维
一.Ansible基本概述: Ansible是一个配置管理系统(configuration management sysytem )你只需要可以使用ssh访问你的服务器或设备就行 Ansible 是近 ...
- HTTP 认证授权技术归纳
原文:https://coolshell.cn/articles/19395.html
- Hive启动留下的RunJar进程不能使用Kill -9 杀不掉怎么办?
1.问题示例 [Hadoop@master Logs]$ jps 3728 ResourceManager 6976 RunJar 7587 Jps 4277 Master 3095 NameNode ...
- Windows 10 输入法(仅桌面) %100 解决
大家好,今天我遇到了一件非常难受的一件事,那就是 WIndows 自带的输入发问题,无法输入中文!!! 这时我去网上找找解决方案,总结了一下几个: 文件检查 步骤 以管理员身份运行PowerShell ...
- C#中字符数组,字节数组和string之间的转化(转)
原文链接:http://hi.baidu.com/endyli/item/7bf074945de35e1f934f41fe 来源: NDC(NetworkDiskClient)的界面和后台程序之间用S ...
- 实验2 数组、指针与C++标准库
实验任务5: Info.hpp #ifndef INFO_HPP #define INFO_HPP #include<iostream> #include<iomanip> # ...
- Redis在线安装+三种启动方式(自启配置)
1.下载 1)检查是否安装wget插件 $ wget --version 2)如未安装,使用yum进行安装 $ yum install -y wget 3)下载redis安装包 $ wge ...