Go后端开发 -- 即时通信系统

Go后端开发 – 即时通信系统

文章目录

  • Go后端开发 -- 即时通信系统
  • 一、即时通信系统
    • 1.整体框架介绍
    • 2.基础server构建
    • 3.用户上线及广播功能
    • 4.用户消息广播机制
    • 5.用户业务封装
    • 6.用户在线查询
    • 7.修改用户名
    • 8.超时强踢
    • 9.私聊功能
    • 10.完整代码
  • 二、客户端实现
    • 1.建立连接
    • 2.命令行解析
    • 3.菜单显示
    • 4.更新用户名
    • 5.公聊模式
    • 6.私聊模式
    • 7.完整代码


一、即时通信系统

1.整体框架介绍

在这里插入图片描述

  • 虚线框内:server是服务器,user是用户
  • server中:online map用来记录当前那些用户在线;channel用于进行广播;
  • user中:一个用户对应两个go程,一个go程用于阻塞地从channel中读取消息,若有消息,会立即会写给客户端;另一个go程会阻塞等待客户端发消息;这是读写分离模型

2.基础server构建

server.go

  • 定义Server结构体,成员有IP和Port;
  • 定义NewServer函数,用于创建Server接口并返回;
  • 定义Server的成员函数Start,用于启动服务:
    • 监听套接字:
      需要使用net.Listen()函数,network参数传入网络类型(tcp,udp),address参数传入监听的服务端地址,是IP:potr形式(可以使用fmt.Sprintf()进行格式化打印并返回字符串);返回Listener监听套接字对象和error错误信息;
      在这里插入图片描述
    • 关闭listen socket
      在创建完listen socket后,直接defer listener.Close(),关闭listen socket
    • accpet:
      需要使用net.Listener.Accept()函数,返回一个Conn连接(可以进行读写)和error错误信息,若accept成功,Conn就是连接的读写套接字;
      在这里插入图片描述
    • 业务处理:
      使用go程来进行业务处理;
package mainimport ("fmt""net"
)type Server struct {IP   stringPort int
}// 创建一个server的接口
func NewServer(ip string, port int) *Server {server := &Server{IP:   ip,Port: port,}return server
}func (svr *Server) Handler(conn net.Conn) {//当前链接的任务fmt.Println("链接建立成功")
}// 启动服务器的接口
func (svr *Server) Start() {// socket listenlistener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", svr.IP, svr.Port))if err != nil {fmt.Println("listen error :", err)return}//close listen socketdefer listener.Close()//acceptfor {conn, err := listener.Accept()if err != nil {fmt.Println("accpet error :", err)continue}// do handlergo svr.Handler(conn)}
}

main.go

  • main函数创建Server对象,并启动服务
package mainfunc main() {server := NewServer("127.0.0.1", 8888)server.Start()
}

编译运行:
在这里插入图片描述
在这里插入图片描述
使用Linux原生nc指令模拟连接:

nc 127.0.0.1 8888

在这里插入图片描述

3.用户上线及广播功能

user.go

  • 每个user都有一个channel,每个user都创建一个go程,监听channel是否有消息
  • 定义User结构体,包含用户名、地址、channel、连接成员
  • 定义ListenMessage为User的成员方法,用于监听当前User channel,一旦有消息,就直接发送给客户端
  • 定义NewUser方法,用于创建User对象,启动go程调用当前User的ListenMessage方法
package mainimport "net"type User struct {Name stringAddr stringC    chan string //用户channelconn net.Conn    //与客户端的链接
}func NewUser(conn net.Conn) *User {//以地址名作为用户名userAddr := conn.RemoteAddr().String()user := &User{Name: userAddr,Addr: userAddr,C:    make(chan string),conn: conn,}//启动监听当前User channel消息的goroutinego user.ListenMessage()return user
}// 监听当前User channel的方法,一旦有消息,就直接发送给客户端
func (usr *User) ListenMessage() {for {msg := <-usr.Cusr.conn.Write([]byte(msg + "\n")) //使用二进制数组形式写入消息}
}

server.go

  • 需要补充两个属性:online map和message channel
    • online map用于记录当前有哪些用户在线,并创建一个用于同步的读写锁
    • go中关于同步的机制全在sync包中
    • message channel用于广播消息
      在这里插入图片描述
  • Handler函数中,每有一个go程执行Handler函数,就代表有一个用户登陆了,现在Handler函数中创建该用户,再通过读写锁将新用户写入online map,然后广播该用户上线的消息;
  • 定义BroadCast为Server的成员函数,在Handler函数中调用,用于将该用户上线的消息发送到当前Server的Message channel
  • 定义ListenMessage函数为Server的成员函数,用于不断监听Message channel,一旦有数据就会发送给所有的用户channel,该函数在Start中创建一个goroutine执行;
package mainimport ("fmt""net""sync"
)type Server struct {IP   stringPort int//在线用户列表OnlineMap map[string]*User//读写锁mapLock sync.RWMutex //go中同步的机制全在sync包中//消息广播的channelMessage chan string
}// 创建一个server的接口
func NewServer(ip string, port int) *Server {server := &Server{IP:        ip,Port:      port,OnlineMap: make(map[string]*User),Message:   make(chan string),}return server
}// 不断监听Message channel的goroutine,一旦有消息就发送给全部的User
func (svr *Server) ListenMessage() {for {//从Message channel中读取数据msg := <-svr.Message//遍历onlinemap,将消息发送给所有的在线Usersvr.mapLock.Lock()for _, cli := range svr.OnlineMap {cli.C <- msg}svr.mapLock.Unlock()}
}// 广播消息的方法
func (svr *Server) BroadCast(user *User, msg string) {sendMsg := "[" + user.Addr + "]" + user.Name + ":" + msgsvr.Message <- sendMsg
}func (svr *Server) Handler(conn net.Conn) {//当前链接的任务//fmt.Println("链接建立成功")user := NewUser(conn)//用户上线,加入到onlinemap中svr.mapLock.Lock()svr.OnlineMap[user.Name] = usersvr.mapLock.Unlock()//广播当前用户上线消息svr.BroadCast(user, "已上线")//当前Handler阻塞,否则go程就结束了select {}
}// 启动服务器的接口
func (svr *Server) Start() {// socket listenlistener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", svr.IP, svr.Port))if err != nil {fmt.Println("listen error :", err)return}//close listen socketdefer listener.Close()//循环监听Message的go程go svr.ListenMessage()//acceptfor {conn, err := listener.Accept()if err != nil {fmt.Println("accpet error :", err)continue}//accept成功后,代表有个用户上线了// do handlergo svr.Handler(conn)}
}

编译运行:
在这里插入图片描述
用户上线广播功能已实现

4.用户消息广播机制

Server.go

  • 在Handler函数中,增加接收用户消息的go程,由一个字节数组类型的缓冲区接收,使用net.Conn.Read()函数接收链接发送的数据,该函数输入参数是一个字节类型数组,输出数据的长度和错误信息,接收完数据后,再进行广播
func (svr *Server) Handler(conn net.Conn) {//当前链接的任务//fmt.Println("链接建立成功")user := NewUser(conn)//用户上线,加入到onlinemap中svr.mapLock.Lock()svr.OnlineMap[user.Name] = usersvr.mapLock.Unlock()//广播当前用户上线消息svr.BroadCast(user, "已上线")//接收用户发送的消息go func() {buf := make([]byte, 4096)for {n, err := user.conn.Read(buf)//如果读取的消息长度为0,就代表当前用户已下线if n == 0 {svr.BroadCast(user, "已下线")return}//	err不是文件末尾if err != nil && err != io.EOF {fmt.Println("Conn read error:", err)return}// 读取消息成功,提取用户消息,去除\n,然后广播msg := string(buf[:n-1])svr.BroadCast(user, msg)}}()//当前Handler阻塞,否则go程就结束了select {}
}

用户消息广播:
在这里插入图片描述
用户下线(ctrl + c):
在这里插入图片描述

5.用户业务封装

调整Server和User的业务划分:

  • 用户上线、下线和处理消息的功能都应该属于User的业务,我们将这三个业务调整到User业务中
  • 用户上/下线:
    需要向Server的OnlineMap中写入用户,因此当前User需要知道其属于哪个Server,需要在User成员中加入*Server指针
  • 用户处理消息
    也需要放在User的业务中,

在这里插入图片描述
Server.go

package mainimport ("fmt""io""net""sync"
)type Server struct {IP   stringPort int//在线用户列表OnlineMap map[string]*User//读写锁mapLock sync.RWMutex //go中同步的机制全在sync包中//消息广播的channelMessage chan string
}// 创建一个server的接口
func NewServer(ip string, port int) *Server {server := &Server{IP:        ip,Port:      port,OnlineMap: make(map[string]*User),Message:   make(chan string),}return server
}// 不断监听Message channel的goroutine,一旦有消息就发送给全部的User
func (svr *Server) ListenMessage() {for {//从Message channel中读取数据msg := <-svr.Message//遍历onlinemap,将消息发送给所有的在线Usersvr.mapLock.Lock()for _, cli := range svr.OnlineMap {cli.C <- msg}svr.mapLock.Unlock()}
}// 广播消息的方法
func (svr *Server) BroadCast(user *User, msg string) {sendMsg := "[" + user.Addr + "]" + user.Name + ":" + msgsvr.Message <- sendMsg
}func (svr *Server) Handler(conn net.Conn) {//创建新用户user := NewUser(conn, svr)//用户上线user.Online()//接收用户发送的消息go func() {buf := make([]byte, 4096)for {n, err := user.conn.Read(buf)//如果读取的消息长度为0,就代表当前用户已下线if n == 0 {user.Offline()return}//	err不是文件末尾if err != nil && err != io.EOF {fmt.Println("Conn read error:", err)return}// 读取消息成功,提取用户消息,去除\n,然后广播msg := string(buf[:n-1])user.DoMessage(msg)}}()//当前Handler阻塞,否则go程就结束了select {}
}// 启动服务器的接口
func (svr *Server) Start() {// socket listenlistener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", svr.IP, svr.Port))if err != nil {fmt.Println("listen error :", err)return}//close listen socketdefer listener.Close()//循环监听Message的go程go svr.ListenMessage()//acceptfor {conn, err := listener.Accept()if err != nil {fmt.Println("accpet error :", err)continue}//accept成功后,代表有个用户上线了// do handlergo svr.Handler(conn)}
}

User.go

package mainimport "net"type User struct {Name stringAddr stringC    chan string //用户channelconn net.Conn    //与客户端的链接svr *Server //当前用户属于哪个Server
}func NewUser(connect net.Conn, server *Server) *User {//以地址名作为用户名userAddr := connect.RemoteAddr().String()user := &User{Name: userAddr,Addr: userAddr,C:    make(chan string),conn: connect,svr:  server,}//启动监听当前User channel消息的goroutinego user.ListenMessage()return user
}// 监听当前User channel的方法,一旦有消息,就直接发送给客户端
func (usr *User) ListenMessage() {for {msg := <-usr.Cusr.conn.Write([]byte(msg + "\n")) //使用二进制数组形式写入消息}
}// 用户上线
func (usr *User) Online() {//用户上线,加入到onlinemap中usr.svr.mapLock.Lock()usr.svr.OnlineMap[usr.Name] = usrusr.svr.mapLock.Unlock()//广播当前用户上线消息usr.svr.BroadCast(usr, "已上线")
}// 用户下线
func (usr *User) Offline() {//用户上线,加入到onlinemap中usr.svr.mapLock.Lock()delete(usr.svr.OnlineMap, usr.Name)usr.svr.mapLock.Unlock()//广播当前用户上线消息usr.svr.BroadCast(usr, "已下线")
}// 用户处理消息
func (usr *User) DoMessage(msg string) {usr.svr.BroadCast(usr, msg)
}

6.用户在线查询

  • 查询用户在线功能是User的业务,可以在User处理消息的DoMessage函数中进行处理,特定的指令下触发
  • 在查询到当前的在线用户后,需要将该条信息发送给当前User的客户端,因此还需要一个单发当前用户的方法

User.go

// 给当前User对应的客户端发消息(单发)
func (usr *User) SendMsg(msg string) {usr.conn.Write([]byte(msg))
}// 用户处理消息
func (usr *User) DoMessage(msg string) {if msg == "who" {//用户想查询所有在线用户usr.svr.mapLock.Lock()for _, user := range usr.svr.OnlineMap {onlineUser := "[" + user.Addr + "]" + user.Name + ":" + "在线...\n"usr.SendMsg(onlineUser)}usr.svr.mapLock.Unlock()} else {usr.svr.BroadCast(usr, msg)}
}

在这里插入图片描述

7.修改用户名

  • 修改用户名是User的业务,同样是在DoMessage中以特定的消息格式进行触发
  • 首先以rename|...格式作为用户名修改的消息格式
  • 从msg中分离出新的用户名,使用strings.Split(msg, "|")函数,该函数会按第二个参数来分割msg字符串,返回一个字符串数组
  • 再查询online map中该用户是否已经存在,若存在,提醒用户;若不存在,则将原User在Online Map中删除,在加入新的名字的User

User.go

// 用户处理消息
func (usr *User) DoMessage(msg string) {if msg == "who" {//用户想查询所有在线用户usr.svr.mapLock.Lock()for _, user := range usr.svr.OnlineMap {onlineUser := "[" + user.Addr + "]" + user.Name + ":" + "在线...\n"usr.SendMsg(onlineUser)}usr.svr.mapLock.Unlock()} else if len(msg) > 7 && msg[:7] == "rename|" {//修改指令是 rename|...newName := strings.Split(msg, "|")[1]//首先查询onlinemap中是否存在该用户名_, ok := usr.svr.OnlineMap[newName]if ok {fmt.Println("当前用户名已经被使用!")} else {//删除原用户名对应的user,并加入新的用户名usr.svr.mapLock.Lock()delete(usr.svr.OnlineMap, usr.Name)usr.svr.OnlineMap[newName] = usrusr.svr.mapLock.Unlock()//更新User的用户名usr.Name = newNameusr.SendMsg("您已更改用户名为:" + newName + "\n")}} else {usr.svr.BroadCast(usr, msg)}
}

在这里插入图片描述

8.超时强踢

  • 用户长时间不活跃,就强制关闭链接,是Server的业务
  • 在每个用户的Handler中,加入一个定时器time.After(10 * time.Second),该定时器经过10s后会触发,定时器本质是一个channel,在出发后里面是有数据可读的;当再次执行该函数时,定时器就会被重置;
  • 用户每发一次消息,定时器应当被重置,定义一个监听用户是否活跃的channel,用户每发一次消息,都会向该channel中写入数据
  • 重置定时器用到了select语句的一个小技巧,在select监听语句中,将从监听用户是否活跃的channel isLive中读数据的case写在定时器的case上面,每当isLive的case触发,select语句就会触发,下面的case也会尝试执行,进而重置了定时器
    在这里插入图片描述
    Server.go
func (svr *Server) Handler(conn net.Conn) {//创建新用户user := NewUser(conn, svr)//用户上线user.Online()//监听用户是否活跃的channelisLive := make(chan bool)//接收用户发送的消息go func() {buf := make([]byte, 4096)for {//从链接中读取消息n, err := user.conn.Read(buf)//如果读取的消息长度为0,就代表当前用户已下线if n == 0 {user.Offline()return}//	err不是文件末尾if err != nil && err != io.EOF {fmt.Println("Conn read error:", err)return}// 读取消息成功,提取用户消息,去除\n,然后广播msg := string(buf[:n-1])user.DoMessage(msg)//每当用户发一次消息,就代表活跃isLive <- true}}()//当前Handler阻塞,否则go程就结束了for {select {case <-isLive://当从isLive中读到数据时,下面的case也会执行一次,就重置了定时器//不作任何处理,只是为了更新定时器case <-time.After(10 * time.Second)://该case触发,表明超时//将当前的User强制关闭user.SendMsg("你被踢了")//销毁用户资源close(user.C)//关闭链接conn.Close()//退出当前Handlerreturn //或者runtime.Goexit()}}
}

在这里插入图片描述

9.私聊功能

  • User的业务,在DoMessage中处理特定消息,定义消息格式为to|张三|张三你好...

User.go

func (usr *User) DoMessage(msg string) {if msg == "who" {//用户想查询所有在线用户usr.svr.mapLock.Lock()for _, user := range usr.svr.OnlineMap {onlineUser := "[" + user.Addr + "]" + user.Name + ":" + "在线...\n"usr.SendMsg(onlineUser)}usr.svr.mapLock.Unlock()} else if len(msg) > 7 && msg[:7] == "rename|" {//修改指令是 rename|...newName := strings.Split(msg, "|")[1]//首先查询onlinemap中是否存在该用户名_, ok := usr.svr.OnlineMap[newName]if ok {fmt.Println("当前用户名已经被使用!")} else {//删除原用户名对应的user,并加入新的用户名usr.svr.mapLock.Lock()delete(usr.svr.OnlineMap, usr.Name)usr.svr.OnlineMap[newName] = usrusr.svr.mapLock.Unlock()//更新User的用户名usr.Name = newNameusr.SendMsg("您已更改用户名为:" + newName + "\n")}} else if len(msg) > 4 && msg[:3] == "to|" {//消息格式:to|张三|张三你好...//获取对方用户名remoteName := strings.Split(msg, "|")[1]if remoteName == "" {usr.SendMsg("私聊消息格式不正确,请使用\"to|张三|张三你好...\"格式\n")return}//根据用户名找到User对象remoteUser, ok := usr.svr.OnlineMap[remoteName]if !ok {usr.SendMsg(remoteName + "用户不存在\n")return}//获取消息内容,通过User对象发送给对方privateMsg := strings.Split(msg, "|")[2]remoteUser.SendMsg(usr.Name + " : " + privateMsg)} else {usr.svr.BroadCast(usr, msg)}
}

在这里插入图片描述

10.完整代码

现在基本的聊天功能已经实现,以下是完整代码:
server.go

package mainimport ("fmt""io""net""sync""time"
)type Server struct {IP   stringPort int//在线用户列表OnlineMap map[string]*User//读写锁mapLock sync.RWMutex //go中同步的机制全在sync包中//消息广播的channelMessage chan string
}// 创建一个server的接口
func NewServer(ip string, port int) *Server {server := &Server{IP:        ip,Port:      port,OnlineMap: make(map[string]*User),Message:   make(chan string),}return server
}// 不断监听Message channel的goroutine,一旦有消息就发送给全部的User
func (svr *Server) ListenMessage() {for {//从Message channel中读取数据msg := <-svr.Message//遍历onlinemap,将消息发送给所有的在线Usersvr.mapLock.Lock()for _, cli := range svr.OnlineMap {cli.C <- msg}svr.mapLock.Unlock()}
}// 广播消息的方法
func (svr *Server) BroadCast(user *User, msg string) {sendMsg := "[" + user.Addr + "]" + user.Name + ":" + msgsvr.Message <- sendMsg
}func (svr *Server) Handler(conn net.Conn) {//创建新用户user := NewUser(conn, svr)//用户上线user.Online()//监听用户是否活跃的channelisLive := make(chan bool)//接收用户发送的消息go func() {buf := make([]byte, 4096)for {//从链接中读取消息n, err := user.conn.Read(buf)//如果读取的消息长度为0,就代表当前用户已下线if n == 0 {user.Offline()return}//	err不是文件末尾if err != nil && err != io.EOF {fmt.Println("Conn read error:", err)return}// 读取消息成功,提取用户消息,去除\n,然后广播msg := string(buf[:n-1])user.DoMessage(msg)//每当用户发一次消息,就代表活跃isLive <- true}}()//当前Handler阻塞,否则go程就结束了for {select {case <-isLive://当从isLive中读到数据时,下面的case也会执行一次,就重置了定时器//不作任何处理,只是为了更新定时器case <-time.After(60 * time.Second)://该case触发,表明超时//将当前的User强制关闭user.SendMsg("你被踢了")//销毁用户资源close(user.C)//关闭链接conn.Close()//退出当前Handlerreturn //或者runtime.Goexit()}}
}// 启动服务器的接口
func (svr *Server) Start() {// socket listenlistener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", svr.IP, svr.Port))if err != nil {fmt.Println("listen error :", err)return}//close listen socketdefer listener.Close()//循环监听Message的go程go svr.ListenMessage()//acceptfor {conn, err := listener.Accept()if err != nil {fmt.Println("accpet error :", err)continue}//accept成功后,代表有个用户上线了// do handlergo svr.Handler(conn)}
}

user.go

package mainimport ("fmt""net""strings"
)type User struct {Name stringAddr stringC    chan string //用户channelconn net.Conn    //与客户端的链接svr *Server //当前用户属于哪个Server
}func NewUser(connect net.Conn, server *Server) *User {//以地址名作为用户名userAddr := connect.RemoteAddr().String()user := &User{Name: userAddr,Addr: userAddr,C:    make(chan string),conn: connect,svr:  server,}//启动监听当前User channel消息的goroutinego user.ListenMessage()return user
}// 监听当前User channel的方法,一旦有消息,就直接发送给客户端
func (usr *User) ListenMessage() {for {msg := <-usr.Cusr.conn.Write([]byte(msg + "\n")) //使用二进制数组形式写入消息}
}// 用户上线
func (usr *User) Online() {//用户上线,加入到onlinemap中usr.svr.mapLock.Lock()usr.svr.OnlineMap[usr.Name] = usrusr.svr.mapLock.Unlock()//广播当前用户上线消息usr.svr.BroadCast(usr, "已上线")
}// 用户下线
func (usr *User) Offline() {//用户上线,加入到onlinemap中usr.svr.mapLock.Lock()delete(usr.svr.OnlineMap, usr.Name)usr.svr.mapLock.Unlock()//广播当前用户上线消息usr.svr.BroadCast(usr, "已下线")
}// 给当前User对应的客户端发消息(单发)
func (usr *User) SendMsg(msg string) {usr.conn.Write([]byte(msg))
}// 用户处理消息
func (usr *User) DoMessage(msg string) {if msg == "who" {//用户想查询所有在线用户usr.svr.mapLock.Lock()for _, user := range usr.svr.OnlineMap {onlineUser := "[" + user.Addr + "]" + user.Name + ":" + "在线...\n"usr.SendMsg(onlineUser)}usr.svr.mapLock.Unlock()} else if len(msg) > 7 && msg[:7] == "rename|" {//修改指令是 rename|...newName := strings.Split(msg, "|")[1]//首先查询onlinemap中是否存在该用户名_, ok := usr.svr.OnlineMap[newName]if ok {fmt.Println("当前用户名已经被使用!")} else {//删除原用户名对应的user,并加入新的用户名usr.svr.mapLock.Lock()delete(usr.svr.OnlineMap, usr.Name)usr.svr.OnlineMap[newName] = usrusr.svr.mapLock.Unlock()//更新User的用户名usr.Name = newNameusr.SendMsg("您已更改用户名为:" + newName + "\n")}} else if len(msg) > 4 && msg[:3] == "to|" {//消息格式:to|张三|张三你好...//获取对方用户名remoteName := strings.Split(msg, "|")[1]if remoteName == "" {usr.SendMsg("私聊消息格式不正确,请使用\"to|张三|张三你好...\"格式\n")return}//根据用户名找到User对象remoteUser, ok := usr.svr.OnlineMap[remoteName]if !ok {usr.SendMsg(remoteName + "用户不存在\n")return}//获取消息内容,通过User对象发送给对方privateMsg := strings.Split(msg, "|")[2]remoteUser.SendMsg(usr.Name + " : " + privateMsg)} else {usr.svr.BroadCast(usr, msg)}
}

main.go

package mainfunc main() {server := NewServer("127.0.0.1", 8888)server.Start()
}

二、客户端实现

1.建立连接

  • 定义Client结构体,基本属性有:Server的IP、端口、用户名、链接套接字
  • 定义NewClient函数用于创建客户端对象,并连接服务器;
    使用net.Dial()函数连接服务器,输入参数是协议类型和服务器地址(ip:port),输出链接套接字和错误信息
    在这里插入图片描述

client.go

package mainimport ("fmt""net"
)type Client struct {ServerIP   stringServerPort intClientName stringconn       net.Conn
}func NewClient(serverIP string, serverPotr int) *Client {//创建客户端对象client := &Client{ServerIP:   serverIP,ServerPort: serverPotr,}//链接serverconn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", serverIP, serverPotr))if err != nil {fmt.Println("net.Dial error:", err)return nil}client.conn = conn//返回对象return client
}func main() {client := NewClient("127.0.0.1", 8888)if client == nil {fmt.Println(">>>>>>>>链接服务器失败>>>>>>>>")return}fmt.Println(">>>>>>>>链接服务器成功>>>>>>>>>")//启动客户端业务select {}
}

在这里插入图片描述

2.命令行解析

  • go解析命令行需要借助flag库函数:
    flag.StringVar()函数用来将命令行参数绑定string变量,输入参数p为要写入的string类型的变量,name为当前命令行参数的提示符,value为当前参数的默认值,usage是对当前参数的说明;同理int类型参数写入需要使用flag.IntVar()
  • 参数绑定的操作在init()函数中实现,在main()函数中调用flag.Parse()函数进行命令行解析,检测执行该程序时,命令行是否有参数
  • 参数绑定一定要在进程之前,可以在init()函数中实现,也可以在main()函数的第一行实现,之后调用flag.Parse()进行命令行解析
    在这里插入图片描述
    在这里插入图片描述
    client.go
package mainimport ("flag""fmt""net"
)type Client struct {ServerIP   stringServerPort intClientName stringconn       net.Conn
}func NewClient(serverIP string, serverPotr int) *Client {//创建客户端对象client := &Client{ServerIP:   serverIP,ServerPort: serverPort,}//链接serverconn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", serverIP, serverPotr))if err != nil {fmt.Println("net.Dial error:", err)return nil}client.conn = conn//返回对象return client
}var serverIP string
var serverPort intfunc init() {flag.StringVar(&serverIP, "ip", "127.0.0.1", "设置服务器IP地址(默认为127.0.0.1)")flag.IntVar(&serverPort, "port", 8888, "设置服务器端口号(默认为8888)")
}func main() {//命令行解析flag.Parse()client := NewClient("127.0.0.1", 8888)if client == nil {fmt.Println(">>>>>>>>链接服务器失败>>>>>>>>")return}fmt.Println(">>>>>>>>链接服务器成功>>>>>>>>>")//启动客户端业务select {}
}

在这里插入图片描述

3.菜单显示

  • 定义Client的成员函数menu(),用于菜单显示
  • 定义Client的成员函数Run(),用于客户端的启动
package mainimport ("flag""fmt""net"
)type Client struct {ServerIP   stringServerPort intClientName stringconn       net.Connflag       int // 当前客户端的模型
}func NewClient(serverIP string, serverPotr int) *Client {//创建客户端对象client := &Client{ServerIP:   serverIP,ServerPort: serverPort,flag:       999,}//链接serverconn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", serverIP, serverPotr))if err != nil {fmt.Println("net.Dial error:", err)return nil}client.conn = conn//返回对象return client
}func (clt *Client) menu() bool {var flag intfmt.Println("*****************************")fmt.Println("**********1.公聊模式**********")fmt.Println("**********2.私聊模式**********")fmt.Println("**********3.更新用户名********")fmt.Println("**********0.退出**************")fmt.Println("******************************")fmt.Scanln(&flag) // 输入flagif flag >= 0 && flag <= 3 {clt.flag = flagreturn true} else {fmt.Println(">>>>>>请输入合法的flag!<<<<<<")return false}
}func (clt *Client) Run() {for clt.flag != 0 {//模式不对就一直循环菜单for clt.menu() == false {}//根据不同的模式处理不同的业务switch clt.flag {case 1://公聊模式fmt.Println("公聊模式")case 2://私聊模式fmt.Println("私聊模式")case 3://更新用户名fmt.Println("更新用户名")}}
}var serverIP string
var serverPort intfunc init() {flag.StringVar(&serverIP, "ip", "127.0.0.1", "设置服务器IP地址(默认为127.0.0.1)")flag.IntVar(&serverPort, "port", 8888, "设置服务器端口号(默认为8888)")
}func main() {//命令行解析flag.Parse()client := NewClient("127.0.0.1", 8888)if client == nil {fmt.Println(">>>>>>>>链接服务器失败>>>>>>>>")return}fmt.Println(">>>>>>>>链接服务器成功>>>>>>>>>")//启动客户端业务client.Run()
}

在这里插入图片描述

4.更新用户名

  • 定义Client的成员函数UpdateName,用于处理用户名更新业务,将新的用户名封装成更改用户名的指令,并通过conn.Write()函数直接发送给客户端进行处理;
  • 客户端还需要接受服务端返回的消息,由于当前客户端go程阻塞在这个业务处,无法再从连接中读取消息,因此应该由另外的go程来从链接中读取消息
  • 定义Client的成员函数DealResponse,用于不断阻塞读取服务端的回执消息,直接使用io.Copy()函数将套接字conn的数据拷贝到标准输出;该函数在main中以新的go程执行
    在这里插入图片描述
    上面的一行代码等价于下面的代码功能:
    在这里插入图片描述
package mainimport ("flag""fmt""io""net""os"
)type Client struct {ServerIP   stringServerPort intClientName stringconn       net.Connflag       int // 当前客户端的模型
}func NewClient(serverIP string, serverPotr int) *Client {//创建客户端对象client := &Client{ServerIP:   serverIP,ServerPort: serverPort,flag:       999,}//链接serverconn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", serverIP, serverPotr))if err != nil {fmt.Println("net.Dial error:", err)return nil}client.conn = conn//返回对象return client
}func (clt *Client) menu() bool {var flag intfmt.Println("*****************************")fmt.Println("**********1.公聊模式**********")fmt.Println("**********2.私聊模式**********")fmt.Println("**********3.更新用户名********")fmt.Println("**********0.退出**************")fmt.Println("******************************")fmt.Scanln(&flag) // 输入flagif flag >= 0 && flag <= 3 {clt.flag = flagreturn true} else {fmt.Println(">>>>>>请输入合法的flag!<<<<<<")return false}
}func (clt *Client) Run() {for clt.flag != 0 {//模式不对就一直循环菜单for clt.menu() == false {}//根据不同的模式处理不同的业务switch clt.flag {case 1://公聊模式fmt.Println("公聊模式")case 2://私聊模式fmt.Println("私聊模式")case 3://更新用户名clt.UpdateName()}}
}func (clt *Client) UpdateName() bool {fmt.Println(">>>>请输入用户名:")fmt.Scanln(&clt.ClientName)sendMsg := "rename|" + clt.ClientName + "\n"_, err := clt.conn.Write([]byte(sendMsg))if err != nil {fmt.Println("conn.Write error:", err)return false}return true
}func (clt *Client) DealResponse() {//不断阻塞等待套接字conn的数据,并copy到stdout中//一旦clt.conn有数据,就直接拷贝到stdout中,是永久阻塞的io.Copy(os.Stdout, clt.conn)//上面的一行代码等价于下面的代码// for {// 	buf := make([]byte, 4096)// 	clt.conn.Read(buf)// 	fmt.Println(buf)// }
}var serverIP string
var serverPort intfunc init() {flag.StringVar(&serverIP, "ip", "127.0.0.1", "设置服务器IP地址(默认为127.0.0.1)")flag.IntVar(&serverPort, "port", 8888, "设置服务器端口号(默认为8888)")
}func main() {//命令行解析flag.Parse()client := NewClient("127.0.0.1", 8888)if client == nil {fmt.Println(">>>>>>>>链接服务器失败>>>>>>>>")return}fmt.Println(">>>>>>>>链接服务器成功>>>>>>>>>")//单独开启一个goroutine,去处理server的回执消息go client.DealResponse()//启动客户端业务client.Run()
}

在这里插入图片描述

5.公聊模式

  • 定义Client的成员函数PublicChat,用于处理公聊业务,用户的信息打包发送给服务端
func (clt *Client) Run() {for clt.flag != 0 {//模式不对就一直循环菜单for clt.menu() == false {}//根据不同的模式处理不同的业务switch clt.flag {case 1://公聊模式fmt.Println(">>>>>>公聊模式,输入exit退出")clt.PublicChat()case 2://私聊模式fmt.Println("私聊模式")case 3://更新用户名clt.UpdateName()}}
}func (clt *Client) PublicChat() {var chatMsg stringfmt.Println(">>>>>请输入聊天内容:")fmt.Scanln(&chatMsg)for chatMsg != "exit" {//消息不为空则发送给服务器if len(chatMsg) != 0 {sendMsg := chatMsg + "\n"_, err := clt.conn.Write([]byte(sendMsg))if err != nil {fmt.Println("conn.Write error:", err)break}}chatMsg = ""fmt.Println(">>>>>请输入聊天内容:")fmt.Scanln(&chatMsg)}
}

在这里插入图片描述

6.私聊模式

  • 私聊模式的流程:先查询哪些用户在线,在提示用户选择另一个用户进行私聊
  • 定义Client的成员函数PrivateChat ,用于处理私聊业务
  • 定义Client的成员函数SelectUsers() ,用于返回在线用户
func (clt *Client) Run() {for clt.flag != 0 {//模式不对就一直循环菜单for clt.menu() == false {}//根据不同的模式处理不同的业务switch clt.flag {case 1://公聊模式fmt.Println(">>>>>>公聊模式,输入exit退出")clt.PublicChat()case 2://私聊模式fmt.Println(">>>>>>私聊模式,输入exit退出")clt.PrivateChat()case 3://更新用户名clt.UpdateName()}}
}// 查询在线用户
func (clt *Client) SelectUsers() {sendMsg := "who\n"_, err := clt.conn.Write([]byte(sendMsg))if err != nil {fmt.Println("conn.Write error:", err)return}
}func (clt *Client) PrivateChat() {clt.SelectUsers()var remoteName stringvar chatMsg stringfmt.Println(">>>>>>请输入聊天对象(用户名),输入exit退出:")fmt.Scanln(&remoteName)for remoteName != "exit" {fmt.Println(">>>>>>请输入消息内容,输入exit退出:")fmt.Scanln(&chatMsg)for chatMsg != "exit" {//消息不为空则发送给服务器if len(chatMsg) != 0 {sendMsg := "to|" + remoteName + "|" + chatMsg + "\n\n"_, err := clt.conn.Write([]byte(sendMsg))if err != nil {fmt.Println("conn.Write error:", err)break}}chatMsg = ""fmt.Println(">>>>>请输入消息内容,输入exit退出:")fmt.Scanln(&chatMsg)}clt.SelectUsers()fmt.Println(">>>>>>请输入聊天对象(用户名),输入exit退出:")fmt.Scanln(&remoteName)}
}

在这里插入图片描述

7.完整代码

client.go

package mainimport ("flag""fmt""io""net""os"
)type Client struct {ServerIP   stringServerPort intClientName stringconn       net.Connflag       int // 当前客户端的模型
}func NewClient(serverIP string, serverPotr int) *Client {//创建客户端对象client := &Client{ServerIP:   serverIP,ServerPort: serverPort,flag:       999,}//链接serverconn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", serverIP, serverPotr))if err != nil {fmt.Println("net.Dial error:", err)return nil}client.conn = conn//返回对象return client
}func (clt *Client) menu() bool {var flag intfmt.Println("*****************************")fmt.Println("**********1.公聊模式**********")fmt.Println("**********2.私聊模式**********")fmt.Println("**********3.更新用户名********")fmt.Println("**********0.退出**************")fmt.Println("******************************")fmt.Scanln(&flag) // 输入flagif flag >= 0 && flag <= 3 {clt.flag = flagreturn true} else {fmt.Println(">>>>>>请输入合法的flag!<<<<<<")return false}
}func (clt *Client) Run() {for clt.flag != 0 {//模式不对就一直循环菜单for clt.menu() == false {}//根据不同的模式处理不同的业务switch clt.flag {case 1://公聊模式fmt.Println(">>>>>>公聊模式,输入exit退出")clt.PublicChat()case 2://私聊模式fmt.Println(">>>>>>私聊模式,输入exit退出")clt.PrivateChat()case 3://更新用户名clt.UpdateName()}}
}func (clt *Client) UpdateName() bool {fmt.Println(">>>>请输入用户名:")fmt.Scanln(&clt.ClientName)sendMsg := "rename|" + clt.ClientName + "\n"_, err := clt.conn.Write([]byte(sendMsg))if err != nil {fmt.Println("conn.Write error:", err)return false}return true
}func (clt *Client) DealResponse() {//不断阻塞等待套接字conn的数据,并copy到stdout中//一旦clt.conn有数据,就直接拷贝到stdout中,是永久阻塞的io.Copy(os.Stdout, clt.conn)//上面的一行代码等价于下面的代码// for {// 	buf := make([]byte, 4096)// 	clt.conn.Read(buf)// 	fmt.Println(buf)// }
}func (clt *Client) PublicChat() {var chatMsg stringfmt.Println(">>>>>请输入聊天内容:")fmt.Scanln(&chatMsg)for chatMsg != "exit" {//消息不为空则发送给服务器if len(chatMsg) != 0 {sendMsg := chatMsg + "\n"_, err := clt.conn.Write([]byte(sendMsg))if err != nil {fmt.Println("conn.Write error:", err)break}}chatMsg = ""fmt.Println(">>>>>请输入聊天内容:")fmt.Scanln(&chatMsg)}
}// 查询在线用户
func (clt *Client) SelectUsers() {sendMsg := "who\n"_, err := clt.conn.Write([]byte(sendMsg))if err != nil {fmt.Println("conn.Write error:", err)return}
}func (clt *Client) PrivateChat() {clt.SelectUsers()var remoteName stringvar chatMsg stringfmt.Println(">>>>>>请输入聊天对象(用户名),输入exit退出:")fmt.Scanln(&remoteName)for remoteName != "exit" {fmt.Println(">>>>>>请输入消息内容,输入exit退出:")fmt.Scanln(&chatMsg)for chatMsg != "exit" {//消息不为空则发送给服务器if len(chatMsg) != 0 {sendMsg := "to|" + remoteName + "|" + chatMsg + "\n\n"_, err := clt.conn.Write([]byte(sendMsg))if err != nil {fmt.Println("conn.Write error:", err)break}}chatMsg = ""fmt.Println(">>>>>请输入消息内容,输入exit退出:")fmt.Scanln(&chatMsg)}clt.SelectUsers()fmt.Println(">>>>>>请输入聊天对象(用户名),输入exit退出:")fmt.Scanln(&remoteName)}
}var serverIP string
var serverPort intfunc init() {flag.StringVar(&serverIP, "ip", "127.0.0.1", "设置服务器IP地址(默认为127.0.0.1)")flag.IntVar(&serverPort, "port", 8888, "设置服务器端口号(默认为8888)")
}func main() {//命令行解析flag.Parse()client := NewClient("127.0.0.1", 8888)if client == nil {fmt.Println(">>>>>>>>链接服务器失败>>>>>>>>")return}fmt.Println(">>>>>>>>链接服务器成功>>>>>>>>>")//单独开启一个goroutine,去处理server的回执消息go client.DealResponse()//启动客户端业务client.Run()
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/427333.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

蓝桥杯省赛无忧 课件41 选择排序

01 选择排序的思想 02 选择排序的实现 03 例题讲解 #include <iostream> using namespace std; void selectionSort(int arr[], int n) {int i, j, min_index;// 移动未排序数组的边界for (i 0; i < n-1; i) {// 找到未排序的部分中最小元素的索引min_index i;for (…

使用Fiddler进行弱网测试

测试APP、web经常需要用到弱网测试&#xff0c;也就是在信号差、网络慢的情况下进行测试。我们自己平常在使用手机APP时&#xff0c;在地铁、电梯、车库等场景经常会遇到会话中断、超时等情况&#xff0c;这种就属于弱网。 普通的弱网测试可以选择第三方工具对带宽、丢包、延时…

自然语言处理--基于HMM+维特比算法的词性标注

自然语言处理作业2--基于HMM维特比算法的词性标注 一、理论描述 词性标注是一种自然语言处理技术&#xff0c;用于识别文本中每个词的词性&#xff0c;例如名词、动词、形容词等&#xff1b; 词性标注也被称为语法标注或词类消疑&#xff0c;是语料库语言学中将语料库内单词…

面试题之RocketMq

1. RocketMq的组成及各自的作用&#xff1f; 在RocketMq中有四个部分组成&#xff0c;分别是Producer&#xff0c;Consumer&#xff0c;Broker&#xff0c;以及NameServer&#xff0c;类比于生活中的邮局&#xff0c;分别是发信者&#xff0c;收信者&#xff0c;负责暂存&#…

和硕拿下AI Pin代工大单公司 | 百能云芯

和硕公司近日成功中标AI Pin代工大单&#xff0c;AI Pin被认为是继iPhone之后的下一个划时代产品&#xff0c;吸引了全球科技圈的广泛关注。和硕公司对此表示&#xff0c;他们不会只专注于单一客户&#xff0c;而是期望在下半年有更多新品上市&#xff0c;为公司带来丰硕的业绩…

creo草绘3个实例学习笔记

creo草绘3个实例 文章目录 creo草绘3个实例草绘01草绘02草绘03 草绘01 草绘02 草绘03

顺序表的增、删、改、查

小伙伴们好&#xff0c;学完C语言&#xff0c;就要开始学数据结构了&#xff0c;数据结构也是非常重要的&#xff0c;今天我们主要来学习在数据结构中最常用的增删改查操作。话不多说&#xff0c;一起来学习吧 1.数据结构相关概念 1.什么是数据结构&#xff1f; 数据结构是由…

Spring Boot 学习之——@SpringBootApplication注解(自动注解原理)

SpringBootApplication注解 springboot是基于spring的新型的轻量级框架&#xff0c;最厉害的地方当属**自动配置。**那我们就可以根据启动流程和相关原理来看看&#xff0c;如何实现传奇的自动配置 SpringBootApplication//标注在某个类上&#xff0c;表示这个类是SpringBoot…

Supplier 惰性调用和 Future#get 同步等待调用结合

&#x1f4d6;一、背景介绍 关于任务异步执行&#xff0c;两个点不可避免&#xff1a;异步结果和异步回调。 而在我的工程中有这样一段代码&#xff1a;使用 CompletableFuture 进行封装&#xff0c;可以异步执行&#xff0c;异步回调&#xff0c;通过 get() 等待异步任务的结…

JUC-CAS

1. CAS概述 CAS(Compare ans swap/set) 比较并交换&#xff0c;实现并发的一种底层技术。它将预期的值和内存中的值比较&#xff0c;如果相同&#xff0c;就更新内存中的值。如果不匹配&#xff0c;一直重试&#xff08;自旋&#xff09;。Java.util.concurrent.atomic包下的原…

【GitHub项目推荐--一个简单的绘图应用程序(Rust + GTK4)】【转载】

一个用 Rust 和 GTK4 编写的简单的绘图应用程序来创建手写笔记。 Rnote 旨在成为一个简单但实用的笔记应用程序&#xff0c;用于手绘或注释图片或文档。它最终能够导入/导出各种媒体文件格式。而且输出的作品是基于矢量的&#xff0c;这使其在编辑和更改内容时非常灵活。 地址…

微信小程序之全局配置-window和tabBar

学习的最大理由是想摆脱平庸&#xff0c;早一天就多一份人生的精彩&#xff1b;迟一天就多一天平庸的困扰。各位小伙伴&#xff0c;如果您&#xff1a; 想系统/深入学习某技术知识点… 一个人摸索学习很难坚持&#xff0c;想组团高效学习… 想写博客但无从下手&#xff0c;急需…