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)
	}
}
相关推荐
zzzzzz3101 天前
9K Star 炸裂开源!这个 C 语言写的代码知识图谱,把 Linux 内核索引压缩到了 3 分钟
linux·服务器·sql
XIAOHEZIcode1 天前
Linux系统鼠标偏移常见原因以及修复方案
linux·运维·游戏
用户0328472220702 天前
如何搭建本地yum源(上)
运维
大树885 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠5 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质5 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
小宇宙Zz5 天前
Maven依赖冲突
java·服务器·maven
Inhand陈工5 天前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信
酣大智5 天前
ARP代理--工作原理
运维·网络·arp·arp代理
shushangyun_5 天前
2026年快消品B2B系统推荐:支持终端门店订货、促销政策自动化的工具?
java·运维·网络·数据库·人工智能·spring·自动化