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

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Vue电商项目--分页器制作

分页器静态组件 分页这个组件&#xff0c;不单单是一个页面用到了。多个页面同时用它,因此我们可以封装成一个全局组件 需要将这个分页结构拆分到components 通用的分页组件Pagination <template><div class"pagination"><button>1</butto…

公司只有1个测试,领导却让我测试10个项目,这不是为难我....

读者提问&#xff1a;公司只有 1个测试&#xff0c;领导让我同时测试 10个项目&#xff0c;我该怎么办&#xff1f;回答&#xff1a;如果我是那个测试&#xff0c;我会做这三件事 1、向上申请资源2、任务分配到人3、执行测试任务 一、向上申请资源 1个测试同时对接 10个项目&a…

python 自动化学习(三) 句柄获取、模拟按键、opencv安装

一、什么是句柄 句柄是在操作系统中的一种标识符&#xff0c;相当于我们每个人的身份证一样&#xff0c;句柄在电脑中也是有唯一性的&#xff0c;我们启动的每一个程序都有自己的句柄号&#xff0c;表示自己的身份 为什么要说句柄&#xff0c;我们如果想做自动化操作时&#xf…

分布式项目 11 在项目中使用jsonp发送请求并且处理

在项目中使用jsonp技术 01.相关子系统的搭建 第一步&#xff1a;创建一个新的子系统&#xff0c;叫做jt-sso 选中jt父级项目&#xff0c;然后鼠标右键进行new&#xff0c;然后选中maven Model&#xff0c;进行项目的创建&#xff0c;具体操 作如下图所示&#xff1a; 第二步…

空气中的声压级、声功率级、声强级的区别

空气中的声压级、声功率级、的区别 在学习声学理论时&#xff0c;经常听到&#xff0c;声压级、声强级、声功率级的名称&#xff0c;经常也听到它们的单位为dB.但是它们是怎样的区别呢&#xff1f;下面介绍这几个名词 一、定义和计算 1.声压级 声压级以 L p {L_p} Lp​表示&am…

解决weekofyear跨年问题

目录 前言跨年问题计算当年第几周前言 前段时间,做了一个日期维度表的需求,发现 计算当年第几周 有误,发现 Hive 中 weekofyear 函数存在跨年问题! 跨年问题 这一周算上一年还是下一年,取决于这一周的大多数日期(4天及以上)在哪一年。算在前一年,就是前一年的最后一…

算法修炼之练气篇——练气十二层

博主&#xff1a;命运之光 专栏&#xff1a;算法修炼之练气篇 前言&#xff1a;每天练习五道题&#xff0c;炼气篇大概会练习200道题左右&#xff0c;题目有C语言网上的题&#xff0c;也有洛谷上面的题&#xff0c;题目简单适合新手入门。&#xff08;代码都是命运之光自己写的…

机械师曙光16电脑开机自动蓝屏怎么解决?

机械师曙光16电脑开机自动蓝屏怎么解决&#xff1f;有的用户在使用机械师曙光16电脑的时候&#xff0c;遇到了一些系统问题&#xff0c;导致自己无法正常的开机使用电脑。因为电脑总会变成蓝屏&#xff0c;无法进行任何操作。那么这个情况怎么去进行问题的解决呢&#xff1f;来…

【2023年电工杯数学建模竞赛B题人工智能对大学生学习影响的评价】完整思路分析+完整代码

1.问题背景与描述 这道题整体还是相对简单的&#xff0c;比较适合新手&#xff0c;选的人多对应获奖数量也会多&#xff0c;所以不要纠结于选题&#xff0c;就选你看上去能做的就好 2.问题分析 2.1 问题一的分析 对附件2中所给数据进行分析和数值化处理&#xff0c;并给出处…

SpringMVC一站式学习,分分钟钟让你上手

文章目录 一、SpringMVC1.1 引言1.2 MVC架构1.2.1 概念1.2.2 好处 二、开发流程2.1 导入依赖2.2 配置核心(前端)控制器2.3 后端控制器2.4 配置文件2.5 访问 三、接收请求参数3.1 基本类型参数3.2 实体收参【重点】3.3 数组收参3.4 集合收参 【了解】3.5 路径参数3.6 中文乱码 四…

【Netty】字节缓冲区 ByteBuf(七)(下)

文章目录 前言一、实现原理二、ByteBuf 的使用案例三、ByteBuf 的3种使用模式3.1 堆缓冲模式3.2 直接缓冲区模式3.3 复合缓冲区模式 总结 前言 回顾Netty系列文章&#xff1a; Netty 概述&#xff08;一&#xff09;Netty 架构设计&#xff08;二&#xff09;Netty Channel 概…

springcloud-alibaba (03)sentinel实现规则持久化-流控规则为例

Sentinel和Nacos的整合可以实现规则动态配置&#xff0c;即在Nacos中修改规则后&#xff0c;Sentinel能够实时地读取并应用新的规则。而规则持久化则是指将规则保存在Nacos中&#xff0c;以避免意外故障或重启时规则被丢失。 实现规则持久化&#xff0c;可以按照以下步骤进行操…

达梦数据库索引的建立使用

达梦数据库支持聚集索引&#xff0c;复合索引&#xff0c;函数索引&#xff0c;唯一索引&#xff0c;位图索引等等。 一.建立索引的准则 1.1在表中插入数据后创建索引 一般情况下&#xff0c;在插入或装载了数据后&#xff0c;为表创建索引会更加有效率。如果在装载数据之前…

进阶篇丨链路追踪(Tracing)很简单:常见问题排查

作者&#xff1a;涯海 经过前面多篇内容的学习&#xff0c;想必大部分同学都已经熟练掌握分布式链路追踪的基础用法&#xff0c;比如回溯链路请求轨迹&#xff0c;定位耗时瓶颈点&#xff1b;配置核心接口黄金三指标告警&#xff0c;第一时间发现流量异常&#xff1b;大促前梳…

电赛E题声源定位跟踪系统制作全过程

声源定位 文章目录 声源定位前言一、题目二、设计步骤1.设计思路2.声源追踪定位的分析3.舵机转角的确定4.声源距离的计算 三、代码编写1.求均值2.卡尔曼滤波 复刻一下电赛的声源定位 前言 2023年的电子设计竞赛快要开始了&#xff0c;同时我也已经大三下了正在准备找工作&…

涨点神器:基于Yolov5/Yolov7的小目标性能提升

1.小目标介绍 目标检测近十年涌现了一大批如Faster R-CNN、RetinaNet、YOLO等可以在工业界实用的目标检测方法,但小目标检测性能差的问题至今也没有被完全解决。因为Swin Transformer的提出,COCO test-dev上的 AP 已经刷到64 ,但小目标检测性能(即APS )和大目标检测性能(…

分享以MM32SPIN0280单片机为主控洗衣机方案

洗衣机是利用电能产生机械作用来洗涤衣物的清洁电器&#xff0c;按驱动方法有3类&#xff0c;直接驱动&#xff0c;皮带驱动&#xff0c;波轮式驱动。 主变一体洗衣机方案以MM32SPIN0280为主控 MCU规格&#xff1a; -ArmCortex-M0内核&#xff0c;最高工作频率可达96MHz -128…

MOSN 基于延迟负载均衡算法——走得更快,期待走得更稳

文&#xff5c;纪卓志&#xff08;GitHub ID&#xff1a;jizhuozhi) 京东高级开发工程师 MOSN 项目 Committer 专注于云原生网关研发的相关工作&#xff0c;长期投入在负载均衡和流量控制领域 前言 这篇文章主要是介绍 MOSN 在 v1.5.0 中新引入的基于延迟的负载均衡算法#2…

【vimsolo】让vim看起来像VSCode:颜色主题和状态栏的配置

文章目录 1. 目的2. 理念&#xff1a; vimsolo3. vimrc: 配置颜色4. vimrc: 配置状态栏5. 拷贝颜色主题和.vimrc: python安装脚本 1. 目的 习惯了 VSCode 默认的配色&#xff1a;黑色主题&#xff0c;蓝色状态栏。偶尔使用 Vim 时想让 vim 伪装的像 VSCode&#xff0c;不考虑花…

float浮点/double双精度浮点和二进制的相互转换,小白也能看明白!

二进制文件包含了太多的数据&#xff0c;如何看懂二进制文件&#xff0c;决定于基础。 文章目录 前言1、重点知识1.1何为二进制文件1.2浮点和双精度的浮点如何生成二进制1.2.1 float和double的基础知识1.2.2 IEEE754约束的重点1.2.3 浮点是如何表示二进制 1.3 例子说明 双精度d…