Golang:实时消息交互系统

一、项目框架

1.1 Server端:

1.1.1Server:服务器

---OnlineMap:记录都有哪些用户在线。(key:用户名,value:用户对象),当给一个用户发消息时,在OnlineMap中查询是否在线并找到用户对象找到对应连接。

---Message channel:server用来广播的channel,如果收到一个消息,先将消息写道该channel中,该channel再将消息转发给特定user的channel或者广播给所有user的channel,每个user就会将自己channel中收到的消息发给客户端。
1.1.2User:在线的用户

user这部分有两个goroutine,读写分离模型。

---一个goroutine负责从user channel中读消息,一旦有消息,就会立刻将消息发送给客户端。

---handler go:永久阻塞等待客户端发消息,read。

1.2 Client端:

二、基础server构建

2.1 server.go

<1.创建server结构体(Ip、Port)

<2.提供接口创建、初始化server结构体对象

<3.写一个start启动服务器的server的类方法,listen、accept、go handler

<4.Handler方法,用于收到客户端的业务处理

Go 复制代码
package main
import "net"
import "fmt"

type Server struct {
	Ip string
	Port int
}

func (this *Server) Handler(conn net.Conn) {//创建成功与客户端建立好连接的套接字
	//...当前连接的业务
	fmt.Println("链接建立成功!")
}

//创建一个server的接口-用于创建server对象-仅仅是对外提供的接口
func NewServer(ip string,port int) *Server {
	server := &Server{ //把当前创建对象的地址传给返回值
		Ip : ip,
		Port : port,
	}
	return server
}

//启动服务器的方法-server的成员方法
func (this *Server) Start() {

	//socket listen;Listen接口返回listener对象和error
	listener,err := net.Listen("tcp",fmt.Sprintf("%s:%d",this.Ip,this.Port))//"127.0.0.1:8888"
	if err != nil {
		fmt.Println("Listener accept err:",err)
		return
	}

	//close listen socket
	defer listener.Close()

	//
	for {
		//accept; 返回一个连接对象conn和err,这个conn可以中有读写等操作--跟客户端建立成功的套接字
		conn,err := listener.Accept()
		if err != nil {
			fmt.Println("Listener accept err:",err)
			continue
		}
		//do handler业务回调
		go this.Handler(conn)//让一个go去执行业务,主go成继续循环接收下一个连接
	}
}

2.2 main.go

Go 复制代码
package main //server.go和main.go都属于main包,不需要再Import

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

二、用户上线及广播功能

client客户端在server中如何表示呢?封装一个user类。这个user类中包含一个conn与客户端简历链接,在线的user存到OnlineMap中,每个user对象还会绑定一个channel,用来接收Message channel中的消息,被user的goroutine阻塞监听,收到就会通过conn发送给client客户端。Message channel是接收客户端的消息,Message goroutine阻塞监听channel,一旦有消息,就遍历OnlineMap去通过user channel广播给在线用户。

2.1封装一个User类

<1.User类中成员变量:name、addr、c(server)、conn(与客户端的连接)

<2.提供创建并初始化User对象的接口,并在初始化时启动goroutine对user channel监听。

<3.监听方法。从user channel中读数据,读出后将数据通过conn写入client。

Go 复制代码
package main
import "net"

type User struct {
	Name string
	Addr string
	C chan string
	Conn net.Conn
}

//创建一个用户的API
func NewUser(conn net.Conn) *User {
	userAddr := conn.RemoteAddr().String() //从conn获取addr
	user := &User{
		Name : userAddr,
		Addr : userAddr,
		C : make(chan string),
		Conn : conn,
	}

	//启动监听当前user channel消息的goroutine
	go user.ListenMessage()

	return user
}

//监听当前User channel的方法,一旦有消息,就直接发送给对端客户端
func (this *User) ListenMessage() {
	for {
		//一直从管道中读数据
		msg := <-this.C
		//向客户端写消息
		this.Conn.Write([]byte(msg + "\n"))
	}
}

2.2对Server类的更改

<1.新增成员变量OnlineMap、mapLock(保护onlinemap)、Message(转发的channel)

<2.server在listen时每accept一个conn就表明一个用户上线,每一个客户端连接都分配一个gorountine,将每一个客户端conn封装成user对象,加入到OnlineMap中,再向Message channel中写数据。

<3.在start服务器时新增gorountine去监听Message有没有收到用户建立连接的消息,收到后就将该消息广播给所有user的user channel。

Go 复制代码
package main
import "net"
import "fmt"
import "sync"

type Server struct {
	Ip string
	Port int

	//在线用户的列表
	OnlineMap map[string]*User
	mapLock sync.RWMutex

	//消息广播的channel
	Message chan string
}

//监听message广播消息channel的gorountine,一旦有消息就发送给全部在线的user
func (this *Server) ListenMessage() {
	for {
		msg := <-this.Message//server中start时仅有一个gorountine专门来监听message channel,而每一个用来处理监听user连接业务的gorountine像这一个message channel中写数据
		//将msg发送给全部在线的User
		this.mapLock.Lock()
		for _,cli := range this.OnlineMap {
			cli.C <- msg //广播给每一个user的channel
		}
		this.mapLock.Unlock()
	}
}

func(this *Server) BroadCast(user *User,msg string){
	sendMsg := "["+user.Addr+"]"+user.Name+":"+msg
	//给server类的channel写数据
	this.Message <- sendMsg//将打包好的消息给massage channel,由它进行广播。server端listen监听客户端,连接上后启用goroutine去处理这个conn,将这个conn封装成user,将这个user给到hash
}

//user的goroutine
func (this *Server) Handler(conn net.Conn) {//创建成功与客户端建立好连接的套接字
	//处理上线的用户
	// fmt.Println("链接建立成功!")
	user := NewUser(conn)
	//用户上线,将用户加入到onlineMap中
	this.mapLock.Lock()
	this.OnlineMap[user.Name]=user
	this.mapLock.Unlock()

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

	//当前的handler不能结束-该goroutine就会死亡,子goroutine就会死亡
}

//创建一个server的接口-用于创建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
}

//启动服务器的方法-server的成员方法
func (this *Server) Start() {

	//socket listen;Listen接口返回listener对象和error
	listener,err := net.Listen("tcp",fmt.Sprintf("%s:%d",this.Ip,this.Port))//"127.0.0.1:8888"
	if err != nil {
		fmt.Println("Listener accept err:",err)
		return
	}

	//close listen socket
	defer listener.Close()

	//启动监听Message的gorountine
	go this.ListenMessage()

	//
	for {
		//accept; 返回一个连接对象conn和err,这个conn可以中有读写等操作--跟客户端建立成功的套接字
		conn,err := listener.Accept()//此时就有用户上线了
		if err != nil {
			fmt.Println("Listener accept err:",err)
			continue
		}
		//do handler业务回调
		go this.Handler(conn)//让一个go去执行业务,主go成继续循环接收下一个连接
	}
}

三、用户消息广播功能

在接收每个client的conn的go程中又创建子go程去阻塞从conn中读数据,读不到就认为对方下线。

Go 复制代码
//user的goroutine,从client->onlinemap->message channel的线
func (this *Server) Handler(conn net.Conn) {//创建成功与客户端建立好连接的套接字
	//处理上线的用户
	// fmt.Println("链接建立成功!")
	user := NewUser(conn)
	//用户上线,将用户加入到onlineMap中
	this.mapLock.Lock()
	this.OnlineMap[user.Name]=user
	this.mapLock.Unlock()

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

	//接收客户端发送的消息,一直监听,从conn中读;
	go func() {
		buf := make([]byte,4096)
		for {
			//Read(b []byte) (n int,err error) 读成功返回n为读到的字节数,读失败err
			n,err := conn.Read(buf)
			if n==0 {
				this.BroadCast(user,"下线") //读不到消息就是下线了吗?依旧可...
				return
			}
			if err != nil && err != io.EOF {//不为空且不是读到文件末尾
				fmt.Println("Conn Read err:",err)
				return 
			}
			//提取用户的消息(去除'\n')
			msg := string(buf[:n-1])//从0到n-1

			//将得到的消息进行广播
			this.BroadCast(user,msg)
		}
	}()

	//当前的handler不能结束-该goroutine就会死亡,子goroutine就会死亡
	select {}//让当前handler阻塞????
}

四、用户业务封装

<2.在User类中新增Server关联(组合),通过关联可以对server中的属性和方法操作。

<1.将用户上线、用户下线、用户消息处理在User类中封装。

Go 复制代码
//用户上线的业务
func (this *User) Online() {
	//用户上线,将用户加入到onlineMap中
	this.server.mapLock.Lock()
	this.server.OnlineMap[this.Name]=this
	this.server.mapLock.Unlock()

	//广播当前用户上线消息
	this.server.BroadCast(this,"已上线")
}

//用户下线的业务
func (this *User) Offline() {
	//用户下线,将用户从onlineMap中删除
	this.server.mapLock.Lock()
	delete(this.server.OnlineMap,this.Name)
	this.server.mapLock.Unlock()

	//广播当前用户上线消息
	this.server.BroadCast(this,"下线")
}

//用户处理消息的业务,将该用户(客户端的)消息广播
func (this *User) DoMessage(msg string) {
	this.server.BroadCast(this,msg)
}

五、在线用户查询

遍历OnlineMap表通过conn发送给客户端所有在线用户

Go 复制代码
//给当前User对应的客户端发送消息
func (this *User) SendMsg(msg string) {
	this.Conn.Write([]byte(msg))
}

//用户处理消息的业务,将该用户(客户端的)消息广播
func (this *User) DoMessage(msg string) {
	if msg == "who" {
		//查询当前在线用户都有哪些

		this.server.mapLock.Lock()
		for _,user := range this.server.OnlineMap {
			OnlineMsg := "["+user.Addr+"]"+user.Name+":"+"在线...\n"
			this.SendMsg(OnlineMsg)
		}
		this.server.mapLock.Unlock()
	} else {
		this.server.BroadCast(this,msg)
	}
}

六、修改用户名

新用户名不能存在,如果存在提示。不存在就开始在OnlineMap删除旧用户名,添加新用户名。

Go 复制代码
else if len(msg) > 7 && msg[:7]=="rename|"{
		//消息格式:renanme|张三
		newName := strings.Split(msg,"|")[1]
		//判断name是否存在
		_, ok := this.server.OnlineMap[newName]
		if ok {
			this.SendMsg("当前用户名被使用\n")
		}else{
			this.server.mapLock.Lock()
			delete(this.server.OnlineMap,this.Name)
			this.server.OnlineMap[newName]=this
			this.server.mapLock.Unlock()

			this.Name = newName
			this.SendMsg("您已经更新用户名:"+this.Name +"\n")
		}
	} 

七、超时强踢功能

如果用户长时间不保持活跃,就断开它的连接。如果Read到用户数据就重置定时器,如果超过十秒没有收到用户消息,就将连接断开。

Go 复制代码
//当前的handler不能结束-该goroutine就会死亡,子goroutine就会死亡
	for {
		select { //select会阻塞监控管道channel的消息,如果超时会满足case,触发,select不会阻塞
			case <-isLive:
				//当前用户活跃应重置定时器--不做处理,只为了激活select顺序执行time.After重置定时器
			case <-time.After(time.Second*10): //time.After 返回一个单向通道(<-chan time.Time),该通道在指定的时间间隔后发送当前时间值‌
			//如果超时,将当前的user强制关闭
			user.SendMsg("你被超时踢出了")
			close(user.C)//关闭channel
			conn.Close()//关闭连接
			return//退出当前hanler
		}
	}

八、用户私聊功能

<1.目前只实现了将用户的消息进行广播的功能。客户端先通过who查询在线用户,再发送给服务器具有格式的消息指明私聊消息发送给哪位在线用户;

<2.从msg中拿到对方userName,在OnlineMap找到它的user对象,用它的user对象调用sendMsg函数通过conn给其发送消息。

<3.私聊消息的本质是在OnlineMap中保存的多个用户的连接中找到目标用户的长连接conn向它write。从client A的conn读到的消息进行处理后在Map找到它要发送的client B的conn去write。

Go 复制代码
else if len(msg) > 4 && msg[:3] == "to" {
		//消息格式:to|张三|消息内容
		//<1.获取对方的用户名
		remoteName := strings.Split(msg,"|")[1]
		if remoteName == "" {
			this.SendMsg("消息格式不正确,请使用\"to|张三|你好啊\"格式,\n")
			return
		}
		//<2.根据用户名得到对方的user对象
		remoteUser,ok := this.server.OnlineMap[remoteName]
		if !ok {
			this.SendMsg("该用户名不存在\n")
			return
		}
		//<3.获取消息内容,通过对方的User对象将消息发送过去
		content := strings.Split(msg,"|")[2]
		if content == "" {
			this.SendMsg("无消息内容,请重发\n")
			return 
		}
		remoteUser.SendMsg(this.Name + "对您说:"+content) //从msg中拿到对方userName,在OnlineMap找到它的user对象,用它的user对象调用sendMsg函数通过conn给其发送消息。
	}

九、客户端基本构建

Go 复制代码
package main

import "net"
import "fmt"

type Client struct {
	ServerIp string //服务器Ip
	ServerPort int
	Name string //客户端名称
	conn net.Conn //连接句柄
}

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

	//连接server,客户端去连接服务器func Dial(network,address string) (Conn,error)
	conn, err := net.Dial("tcp",fmt.Sprintf("%s:%d",serverIp,serverPort))
	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 {}
}

十、解析命令行

Go 复制代码
var serverIp string
var serverPort int
func init() { //将两个形参绑定到flag包中
	flag.StringVar(&serverIp,"ip","127.0.0.1","设置服务器IP地址(默认是127.0.0.1)")
	flag.IntVar(&serverPort,"port",8888,"设置服务器Port地址(默认是8888)")
}

func main() {
	//命令行解析
	flag.Parse()
}

十一、添加客户端菜单

Go 复制代码
func (client *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)

	if flag >= 0 && flag <= 3{
		client.flag = flag
		return true
	}else {
		fmt.Println(">>>>请输入合法范围内的数字<<<<")
		return false
	}
}

func (client *Client) Run() {
	for client.flag != 0{
		for client.menu() != true {//阻塞式等待用户输入正确的flag或者退出
		}
		//根据不同的模式处理不同的业务
		switch client.flag {
		case 1:
			//公聊模式
			fmt.Println("公聊模式")
			break
		case 2:
			//私聊模式
			fmt.Println("私聊模式")
			break
		case 3:
			//更新用户名
			fmt.Println("更新用户名")
			break
		}
	}
}

十二、公聊、私聊、更新用户名接口

12.1更新用户名接口实现

Go 复制代码
func (client *Client) UpdateName() bool {
	fmt.Println(">>>>>请输入用户名<<<<<")
	fmt.Scanln(&client.Name)

	sendMsg := "rename|" + client.Name +"\n"
	_,err := client.conn.Write([]byte(sendMsg)) //从命令行中读消息并发送给服务器
	if err != nil {
		fmt.Println("conn.Write err:",err)
		return false
	}
	return true
}

12.2公聊模式接口实现

Go 复制代码
func (client *Client) PublicChat() {
	//提示用户输入消息
	var chatMsg string
	fmt.Println(">>>>>请输入聊天内容,exit退出")
	fmt.Scanln(&chatMsg)
	
	for chatMsg != "exit"{
		//发给服务器
		if len(chatMsg) != 0{
			sendMsg := chatMsg + "\n"//协议中有\n
			_,err := client.conn.Write([]byte(sendMsg)) //发送给服务器
			if err != nil {
				fmt.Println("conn Write err:",err)
				break
			}
		}
		//只要用户不退出,就一直在公聊模式中
		chatMsg = ""
		fmt.Println(">>>>>请输入聊天内容,exit退出")
		fmt.Scanln(&chatMsg)
	}
}

12.3私聊模式接口实现

Go 复制代码
func (client *Client) PrivateChat() {
	var remoteName string
	var chatMsg string
	client.SelectUsers()
	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 := client.conn.Write([]byte(sendMsg))
				if err != nil {
					fmt.Println("conn Write err:",err)
					break
				}
			}
			chatMsg = ""
			fmt.Println(">>>>>请输入消息内容,exit退出")
			fmt.Scanln(&chatMsg)
		}
		remoteName = ""
		client.SelectUsers()
		fmt.Println(">>>>>请输入聊天对象[用户名],exit退出:")
		fmt.Scanln(&remoteName)
	}
}
相关推荐
jiarg3 小时前
linux 内网下载 yum 依赖问题
linux·运维·服务器
yi个名字4 小时前
Linux第一课
linux·运维·服务器
菜鸟xy..4 小时前
linux 基本命令教程,巡查脚本,kali镜像
linux·运维·服务器·kali镜像·巡查脚本·nmtui
暴躁的小胡!!!4 小时前
Linux权限维持之协议后门(七)
linux·运维·服务器·网络·安全
安 当 加 密4 小时前
中小企业Radius认证服务器的低成本高安全解决方案
运维·服务器·安全
dxaiofcu5 小时前
双网卡电脑,IP地址漂移
linux·服务器·网络
Macle_Chen5 小时前
Mac服务器上创建Docker并安装宝塔环境
服务器·macos·docker
ChinaRainbowSea5 小时前
Linux: Centos7 Cannot find a valid baseurl for repo: base/7/x86_64 解决方案
java·linux·运维·服务器·docker·架构
snpgroupcn6 小时前
ECC升级到S/4 HANA的功能差异 物料、采购、库存管理对比指南
运维·安全·数据库架构
晨曦启明7117 小时前
Linux云计算SRE-第十八周
linux·运维·云计算