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 {}
}

反思

虽然粗糙地实现了想要的功能,但是问题却有很多。首先有三个比较明显的问题:

  1. 当有用户下线,服务端的CPU使用率飙升

  2. 由于并发,终端的输出存在问题,并发控制没有做好

  3. 用户输入合法性

上述是首先要解决的问题,将在后续进行处理

posted @ 2022-05-13 23:17  N3ptune  阅读(383)  评论(0编辑  收藏  举报