Go完整即时通讯项目及Go的生态介绍

news2024/11/27 16:15:23

Go完整即时通讯项目

项目架构:
在这里插入图片描述

1 编写基本服务端-Server

server.go

package main

import (
	"fmt"
	"net"
)

// 定义服务端
type Server struct {
	ip   string
	port int
}

// 创建一个Server
func NewServer(ip string, port int) *Server {
	return &Server{
		ip:   ip,
		port: port,
	}
}

// 定义Server方法
func (this *Server) Start() {
	//根据ip+port监听socket套接字 tcp表明类型【socket listen】
	listen, err := net.Listen("tcp", fmt.Sprintf("%s:%d", this.ip, this.port))
	if err != nil {
		fmt.Println("net.Listen err=", err)
		return
	}
	//关闭套接字,避免资源浪费
	defer listen.Close()
	for {
		conn, err := listen.Accept()
		if err != nil {
			fmt.Println("listen accept err=", err)
			continue
		}
		//处理连接请求【具体业务:处理客户端请求】
		//开启协程处理,避免占用主线程【server一直要监听ip+port】
		go this.Handler(conn)
	}
}

// 处理连接请求
func (this *Server) Handler(conn net.Conn) {
	//当前连接的业务
	fmt.Printf("连接建立成功...")
}

通过在main.go中启动一个server并配合telnet命令检测代码是否正确

  • telnet:可以模拟连接的建立
  • telnet 127.0.0.1 8082

main.go:

package main

func main() {
	//创建一个server
	server := NewServer("127.0.0.1", 8082)
	//启动server【监听】
	server.Start()
}
//打包代码为exe
go build -o intime.exe .\main.go .\server.go

在这里插入图片描述

2 实现用户上线广播机制【用户上线功能】

架构图:Server端存储一个OnlineMap,用于记录在线的用户

在这里插入图片描述

  1. 编写user.go,编写User结构体并实现对user.channel的监听
  2. 修改server.go,新增OnlineMap和Message属性,在处理的客户端上线的Handler中连接建立成功之后将用户添加到OnlineMap;并新增广播消息方法
  3. 在server.go中新增监听广播消息channel的方法,同时用一个goroutine单独监听message
//构建代码 生成intime.exe文件
go build -o intime.exe .\main.go .\server.go .\user.go 

3 用户消息广播机制

修改server.go,完善一个handle处理业务方法,启动一个专门针对当前用户的goroutine

server.go:

package main

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

// 定义服务端
type Server struct {
	ip   string
	port int

	//定义一个map,用于存储在线用户
	OnlineMap map[string]*User
	//定义map锁,保证存储map时候的数据正确,避免并发读取
	mapLock sync.RWMutex
	//消息广播的channel[当该chan中有数据时,直接广播给所有在线用户]
	Message chan string
}

// 创建一个Server
func NewServer(ip string, port int) *Server {
	return &Server{
		ip:        ip,
		port:      port,
		OnlineMap: make(map[string]*User),
		Message:   make(chan string),
	}
}

// 定义Server方法
func (this *Server) Start() {
	//根据ip+port监听socket套接字 tcp表明类型【socket listen】
	listen, err := net.Listen("tcp", fmt.Sprintf("%s:%d", this.ip, this.port))
	if err != nil {
		fmt.Println("net.Listen err=", err)
		return
	}
	//关闭套接字,避免资源浪费
	defer listen.Close()
	//启动监听Message的goroutine
	go this.ListenMessage()

	for {
		conn, err := listen.Accept()
		if err != nil {
			fmt.Println("listen accept err=", err)
			continue
		}
		//处理连接请求【具体业务:处理客户端请求】
		//开启协程处理,避免占用主线程【server一直要监听ip+port】
		go this.Handler(conn)
	}
}

// 处理连接请求
func (this *Server) Handler(conn net.Conn) {
	//当前连接的业务[用户上线之后,发送广播通知]
	//fmt.Printf("连接建立成功...")
	//1. 创建user,并将user添加到OnlineMap中,此处操作map(可能涉及并发,因此加锁保证安全性)
	user := NewUser(conn)
	this.mapLock.Lock()
	this.OnlineMap[user.Name] = user
	this.mapLock.Unlock()
	//2. 将该用户上线消息进行广播
	this.Broadcast(user, "已上线")

	//3. 接收用户发送的消息,并广播
	go func() {
		buf := make([]byte, 4096)
		//接收用户信息
		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])
		//广播消息
		this.Broadcast(user, msg)
	}()

	//4. 阻塞当前handler
	select {}
}

// 广播消息
func (this *Server) Broadcast(user *User, message string) {
	sendMsg := "[" + user.Addr + "] " + user.Name + ":" + message
	//将消息写入广播chan中
	this.Message <- sendMsg
}

// 监听Message广播消息channel的goroutine,一旦有消息就广播发送给所有在线用户
func (this *Server) ListenMessage() {
	for {
		msg := <-this.Message
		//遍历OnlineMap,将消息广播给所有用户[设置并发操作map,加锁]
		this.mapLock.Lock()
		for _, cli := range this.OnlineMap {
			//将广播消息写入用户的channel,等待用户监听读取
			cli.C <- msg
		}
		this.mapLock.Unlock()
	}
}

4 用户业务层封装

修改user.go,新增对应方法:

  • user中新增一个Server属性
  • Online
  • Offline
  • DoMessage等

替换之前server.go中涉及到user的代码

user.go:

package main

import (
	"net"
	"unicode/utf8"
)

type User struct {
	Name string
	Addr string
	//管道用于接收服务端的消息
	C chan string
	//与服务器端的连接
	conn net.Conn

	//对应连接的Server
	server *Server
}

func NewUser(conn net.Conn, server *Server) *User {
	userAddr := conn.RemoteAddr().String()
	user := &User{
		Name:   userAddr,
		Addr:   userAddr,
		C:      make(chan string),
		conn:   conn,
		server: server,
	}

	//创建一个user就应该监听自己chan管道中的消息,如果有就取出
	go user.ListenMessage()
	return user
}

// 监听当前user 的channel,一旦有消息,就直接发送给对应的客户端
func (this *User) ListenMessage() {
	for {
		msg := <-this.C
		//从user管道中读取消息,发送给user客户端
		对中文进行处理
		//msgByte := []byte(msg)
		//bytes, err := simplifiedchinese.GB18030.NewDecoder().Bytes(msgByte)
		//if err != nil {
		//	fmt.Println("simplifiedchinese decoder err=", err)
		//}
		runes := []rune(msg + "\n")
		bytes := make([]byte, len(runes)*4)
		for _, v := range runes {
			buf := make([]byte, 4)
			size := utf8.EncodeRune(buf, v)
			bytes = append(bytes, buf[:size]...)
		}

		//this.conn.Write([]byte(msg + "\n"))
		this.conn.Write(bytes)
	}
}

// Online:用户上线方法
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() {
	this.server.mapLock.Lock()
	//根据key删除对应值
	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)
}

server.go:

package main

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

// 定义服务端
type Server struct {
	ip   string
	port int

	//定义一个map,用于存储在线用户
	OnlineMap map[string]*User
	//定义map锁,保证存储map时候的数据正确,避免并发读取
	mapLock sync.RWMutex
	//消息广播的channel[当该chan中有数据时,直接广播给所有在线用户]
	Message chan string
}

// 创建一个Server
func NewServer(ip string, port int) *Server {
	return &Server{
		ip:        ip,
		port:      port,
		OnlineMap: make(map[string]*User),
		Message:   make(chan string),
	}
}

// 定义Server方法
func (this *Server) Start() {
	//根据ip+port监听socket套接字 tcp表明类型【socket listen】
	listen, err := net.Listen("tcp", fmt.Sprintf("%s:%d", this.ip, this.port))
	if err != nil {
		fmt.Println("net.Listen err=", err)
		return
	}
	//关闭套接字,避免资源浪费
	defer listen.Close()
	//启动监听Message的goroutine
	go this.ListenMessage()

	for {
		conn, err := listen.Accept()
		if err != nil {
			fmt.Println("listen accept err=", err)
			continue
		}
		//处理连接请求【具体业务:处理客户端请求】
		//开启协程处理,避免占用主线程【server一直要监听ip+port】
		go this.Handler(conn)
	}
}

// 处理连接请求
func (this *Server) Handler(conn net.Conn) {
	//当前连接的业务[用户上线之后,发送广播通知]
	//fmt.Printf("连接建立成功...")
	// 创建user,并将user添加到OnlineMap中,此处操作map(可能涉及并发,因此加锁保证安全性)
	user := NewUser(conn, this)
	user.Online()

	// 接收用户发送的消息,并广播
	go func() {
		buf := make([]byte, 4096)
		//接收用户信息
		n, err := conn.Read(buf)
		if n == 0 {
			//用户下线
			user.Offline()
			return
		}
		if err != nil && err != io.EOF {
			fmt.Println("Conn read err:", err)
			return
		}
		//提取用户消息(去除"\n")
		msg := string(buf[:n-1])
		//用户针对msg进行消息处理
		user.DoMessage(msg)
	}()

	//4. 阻塞当前handler
	select {}
}

// 广播消息
func (this *Server) Broadcast(user *User, message string) {
	sendMsg := "[" + user.Addr + "] " + user.Name + ":" + message
	//将消息写入广播chan中
	this.Message <- sendMsg
}

// 监听Message广播消息channel的goroutine,一旦有消息就广播发送给所有在线用户
func (this *Server) ListenMessage() {
	for {
		msg := <-this.Message
		//遍历OnlineMap,将消息广播给所有用户[设置并发操作map,加锁]
		this.mapLock.Lock()
		for _, cli := range this.OnlineMap {
			//将广播消息写入用户的channel,等待用户监听读取
			cli.C <- msg
		}
		this.mapLock.Unlock()
	}
}

5 用户查询功能

实现,用户在终端输入who,查看当前在线用户(修改user.go)

  • 添加SendMsg():给客户端发送消息
  • 新增判断“who”命令逻辑
package main

import (
	"net"
	"unicode/utf8"
)

type User struct {
	Name string
	Addr string
	//管道用于接收服务端的消息
	C chan string
	//与服务器端的连接
	conn net.Conn

	//对应连接的Server
	server *Server
}

func NewUser(conn net.Conn, server *Server) *User {
	userAddr := conn.RemoteAddr().String()
	user := &User{
		Name:   userAddr,
		Addr:   userAddr,
		C:      make(chan string),
		conn:   conn,
		server: server,
	}

	//创建一个user就应该监听自己chan管道中的消息,如果有就取出
	go user.ListenMessage()
	return user
}

// 监听当前user 的channel,一旦有消息,就直接发送给对应的客户端
func (this *User) ListenMessage() {
	for {
		msg := <-this.C
		//从user管道中读取消息,发送给user客户端
		对中文进行处理
		//msgByte := []byte(msg)
		//bytes, err := simplifiedchinese.GB18030.NewDecoder().Bytes(msgByte)
		//if err != nil {
		//	fmt.Println("simplifiedchinese decoder err=", err)
		//}
		runes := []rune(msg + "\n")
		bytes := make([]byte, len(runes)*4)
		for _, v := range runes {
			buf := make([]byte, 4)
			size := utf8.EncodeRune(buf, v)
			bytes = append(bytes, buf[:size]...)
		}

		//this.conn.Write([]byte(msg + "\n"))
		this.conn.Write(bytes)
	}
}

// Online:用户上线方法
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() {
	this.server.mapLock.Lock()
	//根据key删除对应值
	delete(this.server.OnlineMap, this.Name)
	this.server.mapLock.Unlock()
	this.server.Broadcast(this, "下线")
}

// 用户处理消息的业务
func (this *User) DoMessage(msg string) {
	//添加who命令逻辑:查询当前在线用户
	if msg == "who" {
		this.server.mapLock.Lock()
		for _, user := range this.server.OnlineMap {
			onlineMsg := "[" + user.Addr + "]" + user.Name + ":" + "online...\n"
			this.SendMsg(onlineMsg)
		}
	}
	this.server.Broadcast(this, msg)
}

// 给当前user的客户端发送消息
func (this *User) SendMsg(msg string) {
	this.conn.Write([]byte(msg))
}

6 修改用户名

定义命令rename|zhangsan:将当前用户名修改为张三

  • 修改user.go:在DoMessage()方法中判断命令是否为rename

user.go:

package main

import (
	"net"
	"strings"
	"unicode/utf8"
)

type User struct {
	Name string
	Addr string
	//管道用于接收服务端的消息
	C chan string
	//与服务器端的连接
	conn net.Conn

	//对应连接的Server
	server *Server
}

func NewUser(conn net.Conn, server *Server) *User {
	userAddr := conn.RemoteAddr().String()
	user := &User{
		Name:   userAddr,
		Addr:   userAddr,
		C:      make(chan string),
		conn:   conn,
		server: server,
	}

	//创建一个user就应该监听自己chan管道中的消息,如果有就取出
	go user.ListenMessage()
	return user
}

// 监听当前user 的channel,一旦有消息,就直接发送给对应的客户端
func (this *User) ListenMessage() {
	for {
		msg := <-this.C
		//从user管道中读取消息,发送给user客户端
		对中文进行处理
		//msgByte := []byte(msg)
		//bytes, err := simplifiedchinese.GB18030.NewDecoder().Bytes(msgByte)
		//if err != nil {
		//	fmt.Println("simplifiedchinese decoder err=", err)
		//}
		runes := []rune(msg + "\n")
		bytes := make([]byte, len(runes)*4)
		for _, v := range runes {
			buf := make([]byte, 4)
			size := utf8.EncodeRune(buf, v)
			bytes = append(bytes, buf[:size]...)
		}

		//this.conn.Write([]byte(msg + "\n"))
		this.conn.Write(bytes)
	}
}

// Online:用户上线方法
func (this *User) Online() {
	//用户上线,将用户添加到OnlineMap中
	this.server.mapLock.Lock()
	this.server.OnlineMap[this.Name] = this
	this.server.mapLock.Unlock()

	//广播用户上线信息
	this.server.Broadcast(this, "is online")
}

// 用户下线业务
func (this *User) Offline() {
	this.server.mapLock.Lock()
	//根据key删除对应值
	delete(this.server.OnlineMap, this.Name)
	this.server.mapLock.Unlock()
	this.server.Broadcast(this, "is offline")
}

// 用户处理消息的业务
func (this *User) DoMessage(msg string) {
	//添加who命令逻辑:查询当前在线用户
	if msg == "who" {
		this.server.mapLock.Lock()
		for _, user := range this.server.OnlineMap {
			onlineMsg := "[" + user.Addr + "]" + user.Name + ":" + "online...\n"
			this.SendMsg(onlineMsg)
		}
	} else if len(msg) > 7 && msg[:7] == "rename|" {
		newName := strings.Split(msg, "|")[1]
		//判断要修改的name是否已经被占用
		_, ok := this.server.OnlineMap[newName]
		if ok {
			this.SendMsg("the name is already exists...")
		} 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("update name success:" + this.Name + "\n")
		}
	} else {
		this.server.Broadcast(this, msg)
	}
}

// 给当前user的客户端发送消息
func (this *User) SendMsg(msg string) {
	this.conn.Write([]byte(msg))
}

7 超时强踢功能

如果某个用户长时间不发消息,不活跃,达到一定时间则断开连接,达到强踢效果

  • 修改server.go:
    ①在用户的Hander() goroutine中,添加用户活跃channel,一旦有消息就向该channel发送数据
    ②在用户的Hander()goroutine中,添加定时器功能,超时则强踢

server.go:

package main

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

// 定义服务端
type Server struct {
	ip   string
	port int

	//定义一个map,用于存储在线用户
	OnlineMap map[string]*User
	//定义map锁,保证存储map时候的数据正确,避免并发读取
	mapLock sync.RWMutex
	//消息广播的channel[当该chan中有数据时,直接广播给所有在线用户]
	Message chan string
}

// 创建一个Server
func NewServer(ip string, port int) *Server {
	return &Server{
		ip:        ip,
		port:      port,
		OnlineMap: make(map[string]*User),
		Message:   make(chan string),
	}
}

// 定义Server方法
func (this *Server) Start() {
	//根据ip+port监听socket套接字 tcp表明类型【socket listen】
	listen, err := net.Listen("tcp", fmt.Sprintf("%s:%d", this.ip, this.port))
	if err != nil {
		fmt.Println("net.Listen err=", err)
		return
	}
	//关闭套接字,避免资源浪费
	defer listen.Close()
	//启动监听Message的goroutine
	go this.ListenMessage()

	for {
		conn, err := listen.Accept()
		if err != nil {
			fmt.Println("listen accept err=", err)
			continue
		}
		//处理连接请求【具体业务:处理客户端请求】
		//开启协程处理,避免占用主线程【server一直要监听ip+port】
		go this.Handler(conn)
	}
}

// 处理连接请求
func (this *Server) Handler(conn net.Conn) {
	//当前连接的业务[用户上线之后,发送广播通知]
	//fmt.Printf("连接建立成功...")
	// 创建user,并将user添加到OnlineMap中,此处操作map(可能涉及并发,因此加锁保证安全性)
	user := NewUser(conn, this)
	user.Online()
	//监听用户是否活跃的channel
	isLive := make(chan bool)

	// 接收用户发送的消息,并广播
	go func() {
		buf := make([]byte, 4096)
		//接收用户信息
		n, err := conn.Read(buf)
		if n == 0 {
			//用户下线
			user.Offline()
			return
		}
		if err != nil && err != io.EOF {
			fmt.Println("Conn read err:", err)
			return
		}
		//提取用户消息(去除"\n")
		msg := string(buf[:n-1])
		//用户针对msg进行消息处理
		user.DoMessage(msg)
		//用户的任意消息代表用户当前是一个活跃的
		isLive <- true
	}()

	//4. 当前handler阻塞【超时强制踢出】
	for {
		select {
		case <-isLive:
			//当前用户是活跃的,应该重置定时器
			//不做任何事,为了激活select,更新下面的定时器
		case <-time.After(time.Second * 10):
			//已经超时,将当前的User强制关闭
			user.SendMsg("you have been offline")
			//销毁用的资源,关闭channel
			close(user.C)
			//关闭连接
			conn.Close()
			//退出当前Handler[runtime.Goexit()]
			return
		}
	}
}

// 广播消息
func (this *Server) Broadcast(user *User, message string) {
	sendMsg := "[" + user.Addr + "] " + user.Name + ":" + message
	//将消息写入广播chan中
	this.Message <- sendMsg
}

// 监听Message广播消息channel的goroutine,一旦有消息就广播发送给所有在线用户
func (this *Server) ListenMessage() {
	for {
		msg := <-this.Message
		//遍历OnlineMap,将消息广播给所有用户[设置并发操作map,加锁]
		this.mapLock.Lock()
		for _, cli := range this.OnlineMap {
			//将广播消息写入用户的channel,等待用户监听读取
			cli.C <- msg
		}
		this.mapLock.Unlock()
	}
}

8 私聊功能

消息格式:to|zhangsan|hello, how are you

  • 修改user.go的DoMessage()逻辑,新增私聊消息判断

user.go:

package main

import (
	"net"
	"strings"
	"unicode/utf8"
)

type User struct {
	Name string
	Addr string
	//管道用于接收服务端的消息
	C chan string
	//与服务器端的连接
	conn net.Conn

	//对应连接的Server
	server *Server
}

func NewUser(conn net.Conn, server *Server) *User {
	userAddr := conn.RemoteAddr().String()
	user := &User{
		Name:   userAddr,
		Addr:   userAddr,
		C:      make(chan string),
		conn:   conn,
		server: server,
	}

	//创建一个user就应该监听自己chan管道中的消息,如果有就取出
	go user.ListenMessage()
	return user
}

// 监听当前user 的channel,一旦有消息,就直接发送给对应的客户端
func (this *User) ListenMessage() {
	for {
		msg := <-this.C
		//从user管道中读取消息,发送给user客户端
		对中文进行处理
		//msgByte := []byte(msg)
		//bytes, err := simplifiedchinese.GB18030.NewDecoder().Bytes(msgByte)
		//if err != nil {
		//	fmt.Println("simplifiedchinese decoder err=", err)
		//}
		runes := []rune(msg + "\n")
		bytes := make([]byte, len(runes)*4)
		for _, v := range runes {
			buf := make([]byte, 4)
			size := utf8.EncodeRune(buf, v)
			bytes = append(bytes, buf[:size]...)
		}

		//this.conn.Write([]byte(msg + "\n"))
		this.conn.Write(bytes)
	}
}

// Online:用户上线方法
func (this *User) Online() {
	//用户上线,将用户添加到OnlineMap中
	this.server.mapLock.Lock()
	this.server.OnlineMap[this.Name] = this
	this.server.mapLock.Unlock()

	//广播用户上线信息
	this.server.Broadcast(this, "is online")
}

// 用户下线业务
func (this *User) Offline() {
	this.server.mapLock.Lock()
	//根据key删除对应值
	delete(this.server.OnlineMap, this.Name)
	this.server.mapLock.Unlock()
	this.server.Broadcast(this, "is offline")
}

// 用户处理消息的业务
func (this *User) DoMessage(msg string) {
	//添加who命令逻辑:查询当前在线用户
	if msg == "who" {
		this.server.mapLock.Lock()
		for _, user := range this.server.OnlineMap {
			onlineMsg := "[" + user.Addr + "]" + user.Name + ":" + "online...\n"
			this.SendMsg(onlineMsg)
		}
	} else if len(msg) > 7 && msg[:7] == "rename|" {
		newName := strings.Split(msg, "|")[1]
		//判断要修改的name是否已经被占用
		_, ok := this.server.OnlineMap[newName]
		if ok {
			this.SendMsg("the name is already exists...")
		} 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("update name success:" + this.Name + "\n")
		}
	} else if len(msg) > 4 && msg[:3] == "to|" {
		//如果是私聊命令 消息格式: to|zhangsan|msg content
		//1. 获取对方用户名
		remoteName := strings.Split(msg, "|")[1]
		if remoteName == "" {
			this.SendMsg("the msg format is incorrect, please use the 'to|zhangsan|msg content' to send a msg\n")
			return
		}
		//2. 根据用户名,得到对方的user对象
		remoteUser, ok := this.server.OnlineMap[remoteName]
		if !ok {
			this.SendMsg("the user is not exist")
			return
		}
		//3. 获取消息内容,通过对方的User对象将消息内容发送过去
		content := strings.Split(msg, "|")[2]
		if content == "" {
			this.SendMsg("please do not send a empty msg\n")
			return
		}
		remoteUser.SendMsg(this.Name + "is speak to you:" + content)
	} else {
		this.server.Broadcast(this, msg)
	}
}

// 给当前user的客户端发送消息
func (this *User) SendMsg(msg string) {
	this.conn.Write([]byte(msg))
}

9 客户端实现(过程省略)

9.1 客户端类型定义与链接

9.2 解析命令行

9.3 菜单显示

9.4 更新用户名客户端实现

9.5 公聊模式

9.6 私聊模式

10 最终代码

①main.go

package main

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

②server.go

package main

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

type Server struct {
	Ip   string
	Port int

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

	//消息广播的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 (this *Server) ListenMessager() {
	for {
		msg := <-this.Message

		//将msg发送给全部的在线User
		this.mapLock.Lock()
		for _, cli := range this.OnlineMap {
			cli.C <- msg
		}
		this.mapLock.Unlock()
	}
}

//广播消息的方法
func (this *Server) BroadCast(user *User, msg string) {
	sendMsg := "[" + user.Addr + "]" + user.Name + ":" + msg

	this.Message <- sendMsg
}

func (this *Server) Handler(conn net.Conn) {
	//...当前链接的业务
	//fmt.Println("链接建立成功")

	user := NewUser(conn, this)

	user.Online()

	//监听用户是否活跃的channel
	isLive := make(chan bool)

	//接受客户端发送的消息
	go func() {
		buf := make([]byte, 4096)
		for {
			n, err := conn.Read(buf)
			if n == 0 {
				user.Offline()
				return
			}

			if err != nil && err != io.EOF {
				fmt.Println("Conn Read err:", err)
				return
			}

			//提取用户的消息(去除'\n')
			msg := string(buf[:n-1])

			//用户针对msg进行消息处理
			user.DoMessage(msg)

			//用户的任意消息,代表当前用户是一个活跃的
			isLive <- true
		}
	}()

	//当前handler阻塞
	for {
		select {
		case <-isLive:
			//当前用户是活跃的,应该重置定时器
			//不做任何事情,为了激活select,更新下面的定时器

		case <-time.After(time.Second * 300):
			//已经超时
			//将当前的User强制的关闭

			user.SendMsg("你被踢了")

			//销毁用的资源
			close(user.C)

			//关闭连接
			conn.Close()

			//退出当前Handler
			return //runtime.Goexit()
		}
	}
}

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

	//启动监听Message的goroutine
	go this.ListenMessager()

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

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

③user.go

package main

import (
	"net"
	"strings"
)

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

	server *Server
}

//创建一个用户的API
func NewUser(conn net.Conn, server *Server) *User {
	userAddr := conn.RemoteAddr().String()

	user := &User{
		Name: userAddr,
		Addr: userAddr,
		C:    make(chan string),
		conn: conn,

		server: server,
	}

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

	return user
}

//用户的上线业务
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, "下线")

}

//给当前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 if len(msg) > 7 && msg[:7] == "rename|" {
		//消息格式: rename|张三
		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")
		}

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

	} else {
		this.server.BroadCast(this, msg)
	}
}

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

		this.conn.Write([]byte(msg + "\n"))
	}
}

④client.go

package main
 
import (
    "flag"
    "fmt"
    "io"
    "net"
    "os"
)
 
type Client struct {
    ServerIp   string
    ServerPort int
    Name       string
    conn       net.Conn
    flag       int //当前client的模式
}
 
func NewClient(serverIp string, serverPort int) *Client {
    //创建客户端对象
    client := &Client{
        ServerIp:   serverIp,
        ServerPort: serverPort,
        flag:       999,
    }
 
    //链接server
    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
}
 
//处理server回应的消息, 直接显示到标准输出即可
func (client *Client) DealResponse() {
    //一旦client.conn有数据,就直接copy到stdout标准输出上, 永久阻塞监听
    io.Copy(os.Stdout, client.conn)
}
 
func (client *Client) menu() bool {
    var flag int
 
    fmt.Println("1.公聊模式")
    fmt.Println("2.私聊模式")
    fmt.Println("3.更新用户名")
    fmt.Println("0.退出")
 
    fmt.Scanln(&flag)
 
    if flag >= 0 && flag <= 3 {
        client.flag = flag
        return true
    } else {
        fmt.Println(">>>>请输入合法范围内的数字<<<<")
        return false
    }
 
}
 
//查询在线用户
func (client *Client) SelectUsers() {
    sendMsg := "who\n"
    _, err := client.conn.Write([]byte(sendMsg))
    if err != nil {
        fmt.Println("conn Write err:", err)
        return
    }
}
 
//私聊模式
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)
        }
 
        client.SelectUsers()
        fmt.Println(">>>>请输入聊天对象[用户名], exit退出:")
        fmt.Scanln(&remoteName)
    }
}
 
func (client *Client) PublicChat() {
    //提示用户输入消息
    var chatMsg string
 
    fmt.Println(">>>>请输入聊天内容,exit退出.")
    fmt.Scanln(&chatMsg)
 
    for chatMsg != "exit" {
        //发给服务器
 
        //消息不为空则发送
        if len(chatMsg) != 0 {
            sendMsg := chatMsg + "\n"
            _, err := client.conn.Write([]byte(sendMsg))
            if err != nil {
                fmt.Println("conn Write err:", err)
                break
            }
        }
 
        chatMsg = ""
        fmt.Println(">>>>请输入聊天内容,exit退出.")
        fmt.Scanln(&chatMsg)
    }
 
}
 
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
}
 
func (client *Client) Run() {
    for client.flag != 0 {
        for client.menu() != true {
        }
 
        //根据不同的模式处理不同的业务
        switch client.flag {
        case 1:
            //公聊模式
            client.PublicChat()
            break
        case 2:
            //私聊模式
            client.PrivateChat()
            break
        case 3:
            //更新用户名
            client.UpdateName()
            break
        }
    }
}
 
var serverIp string
var serverPort int
 
//./client -ip 127.0.0.1 -port 8888
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(serverIp, serverPort)
    if client == nil {
        fmt.Println(">>>>> 链接服务器失败...")
        return
    }
 
    //单独开启一个goroutine去处理server的回执消息
    go client.DealResponse()
 
    fmt.Println(">>>>>链接服务器成功...")
 
    //启动客户端的业务
    client.Run()
}

11 go的全部生态

在这里插入图片描述

参考:

  • 资料地址:https://pan.baidu.com/s/1glckD7XGInHDFQQKCRE66g#list/path=%2F

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

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

相关文章

Jenkins + docker-compose 在 Centos 上搭建部署

一、前期准备 1. 检查 CentOS上 是否安装 docker 可以使用以下命令&#xff1a; sudo docker version 如果已经安装了Docker&#xff0c;它将显示有关Docker版本和构建信息的输出。如果未安装Docker&#xff0c;将收到有关命令未找到的错误消息。 2. 检查是否安装 docker-…

cookie-机制

目录 一、基础概念 二、cookie的处理方式 一、基础概念 1、cookie是存储在客户端的一组键值对 2、web中cookie的典型应用&#xff1a;免密登陆 3、cookie和爬虫之间的关联 有时&#xff0c;对一张页面进行请求的时候&#xff0c;如果请求的过程中不携带cookie的话&#xf…

Openai+Coursera: ChatGPT Prompt Engineering(四)

想和大家分享一下最近学习的Coursera和openai联合打造ChatGPT Prompt Engineering在线课程.以下是我写的关于该课程的前两篇博客&#xff1a; ChatGPT Prompt Engineering(一)ChatGPT Prompt Engineering(二)ChatGPT Prompt Engineering(三) 今天我们来学习第三部分内容&…

Java on Azure Tooling 4月更新|路线图更新及 Azure Toolkit for IntelliJ 增强

作者&#xff1a;Jialuo Gan - Program Manager, Developer Division at Microsoft 排版&#xff1a;Alan Wang 大家好&#xff0c;欢迎来到 Java on Azure 工具产品的4月更新。让我们首先来谈谈我们对未来几个月的 Java on Azure 开发工具的投资。在这次更新中&#xff0c;我们…

js - 闭包

1、闭包的概念 闭包&#xff1a;函数嵌套函数&#xff0c;内层函数访问了外层函数的局部变量。 // 闭包 function func1() {let a 9;let b 8;function func2() {console.log("a", a); // a 9}func2(); } func1(); 分析&#xff1a; 需要访问的变量会被放到闭包…

【云原生|Kubernetes】05-Pod的存储卷(Volume)

【云原生Kubernetes】05-Pod的存储卷&#xff08;Volume) 文章目录 【云原生Kubernetes】05-Pod的存储卷&#xff08;Volume)简介Volume类型解析emptyDirHostPathgcePersistentDiskNFSiscsiglusterfsceph其他volume 简介 Volume 是Pod 中能够被多个容器访问的共享目录。 Kubern…

ChatGPT可以帮助开发人员的8种方式...

“适应或灭亡”是科技界的口头禅&#xff0c;如果您是开发人员&#xff0c;则尤其如此。 由于技术的动态发展&#xff0c;开发人员面临着比大多数人更大的压力&#xff0c;他们要领先于适应和精通最好的工具。ChatGPT 是最新的此类工具。 虽然有人说 ChatGPT 是“工作杀手”&…

比Figma更丝滑的“Figma网页版“

随着互联网的全面普及和全球化&#xff0c;设计协作工具逐渐成为团队协作中不可或缺的一部分。设计师们常需要通过在线设计协作工具来完成设计任务&#xff0c;而 Figma 作为协作工具的佼佼者&#xff0c;成为了许多设计师心中的首选。但是&#xff0c;对于国内设计师来说&…

Leetcode406. 根据身高重建队列

Every day a Leetcode 题目来源&#xff1a;406. 根据身高重建队列 解法1&#xff1a;贪心 题解&#xff1a;根据身高重建队列 我们先按照身高从大到小排序&#xff08;身高相同的情况下K小的在前面&#xff09;&#xff0c;这样的话&#xff0c;无论哪个人的身高都小于等于…

kubeadm安装集群的时候kube-proxy是如何安装的

背景 最近升级k8s集群时遇到这个问题&#xff0c;集群是使用kuberadm自动化脚本安装的&#xff0c;之前一直认为kubeadm安装的集群这些组件除了kubelet都是静态pod跑起来的。 其实kube-proxy并不是. kube-proxy是如何安装的 在使用kubeadmin安装Kubernetes集群时&#xff0c…

Echarts通过Jquery添加下拉列表动态改变展示的数据和图表

前言 在项目中&#xff0c;有时候我们会一些需求&#xff0c;比如要用Echarts绘制一个饼状图&#xff0c;并且要设置一个下拉列表&#xff0c;当我点击某个选项的时候&#xff0c;饼状图里面的数据会改变&#xff0c;图表样式也会发生改变。我们可以配合Jquery来实现这个功能。…

数字电路基础

目录 一、不同进制之间的转换 二、逻辑代数基础 三、门电路 四、组合逻辑电路 五、半导体存储电路 六、时序电路 一、不同进制之间的转换 二-十转换&#xff1a; 十-二转换&#xff1a; 二-十六转换 十六-二转换 八-二转换 二-八转换 十六-十转换&#xff1a; 先转换成…

python绘制气泡图|随机生成数据

python绘图系列文章目录 往期python绘图合集: python绘制简单的折线图 python读取excel中数据并绘制多子图多组图在一张画布上 python绘制带误差棒的柱状图 python绘制多子图并单独显示 python读取excel数据并绘制多y轴图像 python绘制柱状图并美化|不同颜色填充柱子 python随机…

log4cpp的使用

log4cpp的使用逻辑构造基本模板布局的格式化目的地对象操作文件回卷文件 log4cpp的使用 逻辑构造 目的地Appender&#xff1a;用于表示日志系统最后输出到哪 布局Layout&#xff1a;表示你输出的格式&#xff0c;类似与printf 优先级Priority&#xff1a;常见的优先级有emerg&…

【WSN覆盖】基于麻雀搜索算法的三维无线传感器网络覆盖优化 三维WSN覆盖优化【Matlab代码#26】

文章目录 【可更换其他算法&#xff0c;获取资源请见文章第5节&#xff1a;资源获取】1. SSA算法2. 三维覆盖模型3. 部分代码展示4. 仿真结果展示5. 资源获取 【可更换其他算法&#xff0c;获取资源请见文章第5节&#xff1a;资源获取】 1. SSA算法 2. 三维覆盖模型 三维覆盖模…

搜狐发布Q1财报:读懂前瞻性布局背后的长期主义

5月15日&#xff0c;搜狐发布了2023年第一季度财报。财报显示&#xff0c;搜狐总收入为1.62亿美元&#xff0c;其中&#xff0c;品牌广告收入为2300万美元&#xff1b;在线游戏收入为1.29亿美元。 同时&#xff0c;归于搜狐公司的非美国通用会计准则净亏损为1300万美元。 搜狐…

ChatGPT+Mermaid Live Editor画流程图

1.粘贴代码通过gpt翻译成Mermaid代码&#xff0c;生成流程图 public int largestValsFromLabels(int[] values, int[] labels, int numWanted, int useLimit) {// 将元素按值从大到小排序PriorityQueue<int[]> pq new PriorityQueue<>((a, b) -> b[0] - a[0])…

MySQL运维篇

一.日志 1.1 错误日志 错误日志是 MySQL 中最重要的日志之一&#xff0c;它记录了当 mysqld 启动和停止时&#xff0c;以及服务器在运行过程中发生任何严重错误时的相关信息。当数据库出现任何故障导致无法正常使用时&#xff0c;建议首先查看此日志。 错误日志是默认开启的…

数学(四) -- LC[29][166] 两数相除与分数到小数

1 分数到小数 1.1 题目描述 题目链接&#xff1a;https://leetcode.cn/problems/fraction-to-recurring-decimal/description/ 1.2 思路分析 1. 长除法 题目要求根据给定的分子和分母&#xff0c;将分数转成整数或小数。由于给定的分子和分母的取值范围都是 [ − 2 31 , 2 …

Linux环境变量提权

linux提权信息收集 Exploit Database - Exploits for Penetration Testers, Researchers, and Ethical Hackers Vulnerability & Exploit Database - Rapid7 NVD - Home CVE -CVE SecWiki GitHub linux系统内核漏洞提权 脏牛提权漏洞&#xff1a; 脏牛提权&#xf…