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

news2024/12/23 23:25:22

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 main

import (
	"fmt"
	"net"
)

type Server struct {
	IP   string
	Port 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 listen
	listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", svr.IP, svr.Port))
	if err != nil {
		fmt.Println("listen error :", err)
		return
	}
	//close listen socket
	defer listener.Close()

	//accept
	for {
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("accpet error :", err)
			continue
		}

		// do handler
		go svr.Handler(conn)
	}
}

main.go

  • main函数创建Server对象,并启动服务
package main

func 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 main

import "net"

type User struct {
	Name string
	Addr string
	C    chan string //用户channel
	conn 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消息的goroutine
	go user.ListenMessage()

	return user
}

// 监听当前User channel的方法,一旦有消息,就直接发送给客户端
func (usr *User) ListenMessage() {
	for {
		msg := <-usr.C

		usr.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 main

import (
	"fmt"
	"net"
	"sync"
)

type Server struct {
	IP   string
	Port int

	//在线用户列表
	OnlineMap map[string]*User
	//读写锁
	mapLock sync.RWMutex //go中同步的机制全在sync包中

	//消息广播的channel
	Message 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,将消息发送给所有的在线User
		svr.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 + ":" + msg

	svr.Message <- sendMsg
}

func (svr *Server) Handler(conn net.Conn) {
	//当前链接的任务
	//fmt.Println("链接建立成功")
	user := NewUser(conn)
	//用户上线,加入到onlinemap中
	svr.mapLock.Lock()
	svr.OnlineMap[user.Name] = user
	svr.mapLock.Unlock()

	//广播当前用户上线消息
	svr.BroadCast(user, "已上线")

	//当前Handler阻塞,否则go程就结束了
	select {}
}

// 启动服务器的接口
func (svr *Server) Start() {
	// socket listen
	listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", svr.IP, svr.Port))
	if err != nil {
		fmt.Println("listen error :", err)
		return
	}
	//close listen socket
	defer listener.Close()

	//循环监听Message的go程
	go svr.ListenMessage()

	//accept
	for {
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("accpet error :", err)
			continue
		}

		//accept成功后,代表有个用户上线了
		// do handler
		go 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] = user
	svr.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 main

import (
	"fmt"
	"io"
	"net"
	"sync"
)

type Server struct {
	IP   string
	Port int

	//在线用户列表
	OnlineMap map[string]*User
	//读写锁
	mapLock sync.RWMutex //go中同步的机制全在sync包中

	//消息广播的channel
	Message 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,将消息发送给所有的在线User
		svr.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 + ":" + msg

	svr.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 listen
	listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", svr.IP, svr.Port))
	if err != nil {
		fmt.Println("listen error :", err)
		return
	}
	//close listen socket
	defer listener.Close()

	//循环监听Message的go程
	go svr.ListenMessage()

	//accept
	for {
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("accpet error :", err)
			continue
		}

		//accept成功后,代表有个用户上线了
		// do handler
		go svr.Handler(conn)
	}
}

User.go

package main

import "net"

type User struct {
	Name string
	Addr string
	C    chan string //用户channel
	conn 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消息的goroutine
	go user.ListenMessage()

	return user
}

// 监听当前User channel的方法,一旦有消息,就直接发送给客户端
func (usr *User) ListenMessage() {
	for {
		msg := <-usr.C

		usr.conn.Write([]byte(msg + "\n")) //使用二进制数组形式写入消息
	}
}

// 用户上线
func (usr *User) Online() {
	//用户上线,加入到onlinemap中
	usr.svr.mapLock.Lock()
	usr.svr.OnlineMap[usr.Name] = usr
	usr.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] = usr
			usr.svr.mapLock.Unlock()

			//更新User的用户名
			usr.Name = newName
			usr.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()

	//监听用户是否活跃的channel
	isLive := 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()
			//退出当前Handler
			return //或者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] = usr
			usr.svr.mapLock.Unlock()

			//更新User的用户名
			usr.Name = newName
			usr.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 main

import (
	"fmt"
	"io"
	"net"
	"sync"
	"time"
)

type Server struct {
	IP   string
	Port int

	//在线用户列表
	OnlineMap map[string]*User
	//读写锁
	mapLock sync.RWMutex //go中同步的机制全在sync包中

	//消息广播的channel
	Message 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,将消息发送给所有的在线User
		svr.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 + ":" + msg

	svr.Message <- sendMsg
}

func (svr *Server) Handler(conn net.Conn) {
	//创建新用户
	user := NewUser(conn, svr)

	//用户上线
	user.Online()

	//监听用户是否活跃的channel
	isLive := 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()
			//退出当前Handler
			return //或者runtime.Goexit()
		}
	}
}

// 启动服务器的接口
func (svr *Server) Start() {
	// socket listen
	listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", svr.IP, svr.Port))
	if err != nil {
		fmt.Println("listen error :", err)
		return
	}
	//close listen socket
	defer listener.Close()

	//循环监听Message的go程
	go svr.ListenMessage()

	//accept
	for {
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("accpet error :", err)
			continue
		}

		//accept成功后,代表有个用户上线了
		// do handler
		go svr.Handler(conn)
	}
}

user.go

package main

import (
	"fmt"
	"net"
	"strings"
)

type User struct {
	Name string
	Addr string
	C    chan string //用户channel
	conn 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消息的goroutine
	go user.ListenMessage()

	return user
}

// 监听当前User channel的方法,一旦有消息,就直接发送给客户端
func (usr *User) ListenMessage() {
	for {
		msg := <-usr.C

		usr.conn.Write([]byte(msg + "\n")) //使用二进制数组形式写入消息
	}
}

// 用户上线
func (usr *User) Online() {
	//用户上线,加入到onlinemap中
	usr.svr.mapLock.Lock()
	usr.svr.OnlineMap[usr.Name] = usr
	usr.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] = usr
			usr.svr.mapLock.Unlock()

			//更新User的用户名
			usr.Name = newName
			usr.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 main

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

二、客户端实现

1.建立连接

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

client.go

package main

import (
	"fmt"
	"net"
)

type Client struct {
	ServerIP   string
	ServerPort int
	ClientName string
	conn       net.Conn
}

func NewClient(serverIP string, serverPotr int) *Client {
	//创建客户端对象
	client := &Client{
		ServerIP:   serverIP,
		ServerPort: serverPotr,
	}

	//链接server
	conn, 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 main

import (
	"flag"
	"fmt"
	"net"
)

type Client struct {
	ServerIP   string
	ServerPort int
	ClientName string
	conn       net.Conn
}

func NewClient(serverIP string, serverPotr int) *Client {
	//创建客户端对象
	client := &Client{
		ServerIP:   serverIP,
		ServerPort: serverPort,
	}

	//链接server
	conn, 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 int

func 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 main

import (
	"flag"
	"fmt"
	"net"
)

type Client struct {
	ServerIP   string
	ServerPort int
	ClientName string
	conn       net.Conn
	flag       int // 当前客户端的模型
}

func NewClient(serverIP string, serverPotr int) *Client {
	//创建客户端对象
	client := &Client{
		ServerIP:   serverIP,
		ServerPort: serverPort,
		flag:       999,
	}

	//链接server
	conn, 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 int
	fmt.Println("*****************************")
	fmt.Println("**********1.公聊模式**********")
	fmt.Println("**********2.私聊模式**********")
	fmt.Println("**********3.更新用户名********")
	fmt.Println("**********0.退出**************")
	fmt.Println("******************************")

	fmt.Scanln(&flag) // 输入flag

	if flag >= 0 && flag <= 3 {
		clt.flag = flag
		return 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 int

func 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 main

import (
	"flag"
	"fmt"
	"io"
	"net"
	"os"
)

type Client struct {
	ServerIP   string
	ServerPort int
	ClientName string
	conn       net.Conn
	flag       int // 当前客户端的模型
}

func NewClient(serverIP string, serverPotr int) *Client {
	//创建客户端对象
	client := &Client{
		ServerIP:   serverIP,
		ServerPort: serverPort,
		flag:       999,
	}

	//链接server
	conn, 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 int
	fmt.Println("*****************************")
	fmt.Println("**********1.公聊模式**********")
	fmt.Println("**********2.私聊模式**********")
	fmt.Println("**********3.更新用户名********")
	fmt.Println("**********0.退出**************")
	fmt.Println("******************************")

	fmt.Scanln(&flag) // 输入flag

	if flag >= 0 && flag <= 3 {
		clt.flag = flag
		return 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 int

func 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 string
	fmt.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 string
	var chatMsg string
	fmt.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 main

import (
	"flag"
	"fmt"
	"io"
	"net"
	"os"
)

type Client struct {
	ServerIP   string
	ServerPort int
	ClientName string
	conn       net.Conn
	flag       int // 当前客户端的模型
}

func NewClient(serverIP string, serverPotr int) *Client {
	//创建客户端对象
	client := &Client{
		ServerIP:   serverIP,
		ServerPort: serverPort,
		flag:       999,
	}

	//链接server
	conn, 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 int
	fmt.Println("*****************************")
	fmt.Println("**********1.公聊模式**********")
	fmt.Println("**********2.私聊模式**********")
	fmt.Println("**********3.更新用户名********")
	fmt.Println("**********0.退出**************")
	fmt.Println("******************************")

	fmt.Scanln(&flag) // 输入flag

	if flag >= 0 && flag <= 3 {
		clt.flag = flag
		return 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 string
	fmt.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 string
	var chatMsg string
	fmt.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 int

func 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.coloradmin.cn/o/1407651.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

阿里云幻兽帕鲁服务器租用价格表,免费?

幻兽帕鲁异常火爆自建幻兽帕鲁服务器不卡又稳定&#xff0c;继腾讯云推出幻兽帕鲁自建服务器教程和4核16G幻兽帕鲁专用特价游戏服务器后&#xff0c;阿里云坐不住了&#xff0c;直接推出特价4核32G和4核16G的palworld专属游戏机&#xff0c;另外还可以申请免费3个月的4核8G无影…

C语言或C++通过IShellLinkA创建或解析lnk快捷方式(使用char字符数组)

本例程用到的COM接口有IShellLinkA和IPersistFile。 请注意因为函数参数的类型不为BSTR&#xff0c;所以这两个接口可直接传char *或wchar_t *字符串&#xff0c;不需要提前转化为BSTR类型。 C语言的写法&#xff1a; /* 这个程序只能在C编译器下编译成功, 请确保源文件的扩展…

智慧之光:ChatGPT 引领工作效率新纪元

随着科技的不断发展&#xff0c;人工智能&#xff08;AI&#xff09;已经逐渐融入我们的日常生活和工作中。其中&#xff0c;ChatGPT 作为一种先进的 AI 技术&#xff0c;正逐步改变我们的工作方式&#xff0c;提升我们的工作效率。本文灸哥将介绍如何利用ChatGPT提升工作效率&…

intellij idea怎么设置中文

CtrlAltS快捷键打开Settings界面选择Plugins在搜索部分搜索chinese&#xff0c;选择下方的Chinese&#xff08;simplified&#xff09;Language下载最后重启软件即可

数据结构:完全二叉树(递归实现)

如果完全二叉树的深度为h&#xff0c;那么除了第h层外&#xff0c;其他层的节点个数都是满的&#xff0c;第h层的节点都靠左排列。 完全二叉树的编号方法是从上到下&#xff0c;从左到右&#xff0c;根节点为1号节点&#xff0c;设完全二叉树的节点数为sum&#xff0c;某节点编…

leetcode-hot100双指针专题

第一题&#xff1a;移动零 题目链接 283. 移动零 - 力扣&#xff08;LeetCode&#xff09; 解题思路 我们创建两个指针i,j&#xff0c;第一次遍历的时候指针j用来记录当前面有多少非0元素。即遍历的时候每遇到一个非0元素就将其往数组左边挪&#xff0c;第一次遍历完后&…

解决国内 github.com 打不开的准确方法

** 下载watt toolkit&#xff0c; 选择‘github’&#xff0c;点击‘一键加速’&#xff0c;很简单方便 **

【阿里云服务器数据迁移】 同一个账号 不同区域服务器

前言 假如说一台云服务器要过期了,现在新买了一台,有的人会烦恼又要将重新在新的服务器上装环境,部署上线旧服务器上的网站项目, 但是不必烦恼,本文将介绍如何快速将就旧的服务器上的数据迁移到新的服务器上. 包括所有的环境和网站项目噢 ! 步骤 (1) 创建旧服务器自定义镜像…

Llama2中文大模型——牛刀小试

文章目录 Llama2中文大模型——牛刀小试前言更新库导包加载模型对话问答-1对话问答-2对话问答-3对话问答-4对话问答-5 Llama2中文大模型——牛刀小试 前言 Meta开源的Llama从第一版开始&#xff0c;效果就很不错&#xff0c;有很多开源LLM都是基于它训练的&#xff0c;例如Vic…

GPT-5不叫GPT-5?下一代模型会有哪些新功能?

OpenAI首席执行官奥特曼在上周三达沃斯论坛接受媒体采访时表示&#xff0c;他现在的首要任务就是推出下一代大模型&#xff0c;这款模型不一定会命名GPT-5。虽然GPT-5的商标早已经注册。 如果GPT-4目前解决了人类任务的10%&#xff0c;GPT-5应该是15%或者20%。 OpenAI从去年开…

登录kafka报错:Could notstart Jolokia agent: java.net.BindException: Address in use

在执行kafka命令增加或删除topic的时候&#xff0c;发现总是报错。 Address in use&#xff1f;端口还是ip被占用了。只能禁用了&#xff0c;再执行命令。 禁用代码&#xff1a; 然后就可以拉~ 总体步骤&#xff1a; 1.先进入k8s的kafka pod 2.进入kafka的bin目录 3.两个…

PPO学习

openai用tf实现的真的看不懂&#xff0c;大佬的世界… PPO的详细细节 1. 奖励模型和策略的价值头将 query 和 response 的连接作为输入 奖励模型和策略的价值头 不 仅仅查看响应。相反&#xff0c;它将 query 和 response 连接在一起&#xff0c;作为 query_response def ge…

openEuler操作系统的安装及免密远程连接(超详细版)

一、下载地址 注意&#xff1a;可以先注册华为账号&#xff0c;注册后可享1倍加速 mirrors.huaweicloud.com/openeuler/openEuler-22.03-LTS-SP3/ISO/x86_64/ 二、创建虚拟机步骤 ①选择自定义 ② 根据自己的VMware选择版本 ③选择稍后安装操作系统 ④没有openEuler可以选择…

如何在CentOS使用docker-compose部署Apache Superset并实现公网访问

文章目录 前言1. 使用Docker部署Apache Superset1.1 第一步安装docker 、docker compose1.2 克隆superset代码到本地并使用docker compose启动 2. 安装cpolar内网穿透&#xff0c;实现公网访问3. 设置固定连接公网地址 前言 Superset是一款由中国知名科技公司开源的“现代化的…

“接口”公共规范的遵守者

&#x1f468;‍&#x1f4bb;作者简介&#xff1a;&#x1f468;&#x1f3fb;‍&#x1f393;告别&#xff0c;今天 &#x1f4d4;高质量专栏 &#xff1a;☕java趣味之旅 欢迎&#x1f64f;点赞&#x1f5e3;️评论&#x1f4e5;收藏&#x1f493;关注 &#x1f496;衷心的希…

【C++】命名空间详解

目录 前言 命名空间的定义 命名空间的使用 前言 在C/C中&#xff0c;变量、函数和后面要学到的类都是大量存在的&#xff0c;这些变量、函数和类的名称将都存 在于全局作用域中&#xff0c;可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化&#xff0c; 以…

厂务设备设施中如何使用工具实现预测性维护(PdM)

在现代工业环境中&#xff0c;厂务设备设施的有效维护对于确保生产连续性和降低运营成本至关重要。随着技术的不断发展&#xff0c;预测性维护&#xff08;PdM&#xff09;成为提前发现潜在设备故障的关键。本文将深入研究厂务设备设施如何利用各种先进工具实现预测性维护的目标…

扩散模型公式推导

这篇文章将尝试推导扩散模型 DDPM 中涉及公式&#xff0c;主要参考两个 B 站视频&#xff1a; 大白话AI狗中赤兔 本文所用 PPT 元素均来自 UP 主&#xff0c;狗中赤兔和大白兔AI&#xff0c;特此感谢。 在证明开始&#xff0c;我们需要先对扩散模型有一个整体的认知。扩散模型…

第二证券:大金融板块逆势护盘 北向资金尾盘加速净流入

周一&#xff0c;A股商场低开低走&#xff0c;沪指收盘失守2800点。截至收盘&#xff0c;上证综指跌2.68%&#xff0c;报2756.34点&#xff1b;深证成指跌3.5%&#xff0c;报8479.55点&#xff1b;创业板指跌2.83%&#xff0c;报1666.88点。沪深两市合计成交额7941亿元&#xf…

学习笔记-李沐动手学深度学习(二)(08-09、线性回归、优化算法、Softmax回归、损失函数、图片分类)

总结 以_结尾的方法&#xff0c;好像是原位替换&#xff08;即 原地修改&#xff0c;就地修改变量&#xff09;如 fill_() 感恩的心&#xff1a;&#xff08;沐神的直播环境&#xff09; 08-线性回归基础优化算法 引言&#xff08;如何在美国买房&#xff09; 根据现在行…