当前位置:主页 > 查看内容

一起学Go吧!即时通信系统项目实战

发布时间:2021-05-05 00:00| 位朋友查看

简介:文章目录 前言 项目的功能需求 项目整体架构图 Server端代码 User类代码 Client端代码 项目运行步骤 项目运行展示 遇到的问题 总结 后记 前言 把Go语言学习基础部分学习完之后我想要结合所有的基础去做一个拿来练手的小练习Demo项目跟着B站视频一起写代码的……

在这里插入图片描述

前言

把Go语言学习基础部分学习完之后,我想要结合所有的基础,去做一个拿来练手的小练习Demo项目,跟着B站视频一起写代码的同时,我也思考了很多,也遇到了一些小小的问题,但是总算是结束了这个项目的练习,所以记录这篇博客,想和我一样找一份练手的项目的童鞋可以直接跟着这篇,做一个模拟的即时通信系统出来!所以,一起加油把!
在这里插入图片描述


项目的功能需求

是一个服务端和客户端的经典C/S聊天通信系统,可以有多个客户端连接服务端,然后每个客户端就是一个用户,所有的用户可以在服务端进行公聊和私聊,同时也可以更改用户名。


项目整体架构

在这里插入图片描述

让我们来一起理解一下这幅图,项目运行的整体的流程从一个Server服务端开始,Server端保存一个 OnlineMap 存储当前在线的用户,Server端中还有一个专门用来接收消息并广播的管道 channel , 所有系统消息通过这个 channel 来广播给用户,User类里面包含一个Conn接口和Name,和channel,Conn接口标识这个用户的客户端连接信息,Name代表用户名,User里面的channel是User与服务端通信的通道。系统有消息的话,会遍历一遍OnlineMap,将该消息发送给Map里面每个User的channel,每个User就可以从自己的channel 来读取消息。


Server端代码

server.go

package main

import (
	"fmt"
	"io"
	"net"
	"sync"
	"time"
)
//看完这篇博客点赞的都是靓仔靓女,都会早日进大厂!
type Server struct {
	Ip string
	Port int

	//onlineUserMap
	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
}

func (this *Server) BroadCast(user *User, msg string) {
	sendMsg := "[" + user.Addr + "]" + user.Name + ":" + msg
	this.Message<-sendMsg
}

//监听Message广播消息 channel 的 goroutine ,一旦有消息就发送给全部的在线User
func (this *Server) ListenMessage() {
	for {
		msg := <-this.Message
		//将 msg 发送给全部的在线User
		this.mapLock.Lock()
		for _, cli := range this.OnlineMap {
			cli.C <- msg
		}
		this.mapLock.Unlock()
	}
}
//当前链接的业务
func (this *Server) Handler(conn net.Conn) {
	//根据传递过来的 conn 连接新建用户
	user := NewUser(conn,this)
	//用户上线,将用户加入到onlineMap中
	user.Online()
	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])

			//将得到的消息进行广播
			user.DoMessage(msg)
			isLive<-true
		}
	}()
	//当前 handler 阻塞
	for {
		select {
		case <-isLive:

		case <-time.After(time.Second * 30):
			user.SendMsg("你被踢了!")
			close(user.C)
			conn.Close()
			return
		}
	}
	//fmt.Println("链接建立成功!")
}

//启动服务的接口
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的gorotine
	go this.ListenMessage()

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

}

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

User类代码

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|"{
		newName := strings.Split(msg,"|")[1]
		_,ok := this.server.OnlineMap[newName]
		if ok {
			this.SendMsg("当前用户名已经存在!\n")
			return
		}
		this.server.mapLock.Lock()
		delete(this.server.OnlineMap,this.Name)
		this.server.OnlineMap[newName] = this
		this.server.mapLock.Unlock()
		this.Name = newName
		this.SendMsg("您已经成功修改用户名"+newName+"\n")
	} else if len(msg) > 4 && msg[:3]=="to|" {
		remoteName := strings.Split(msg,"|")[1]
		if remoteName == "" {
			this.SendMsg("用户名不能为空!\n")
			return
		}
		remoteUser,ok := this.server.OnlineMap[remoteName]
		if(!ok) {
			this.SendMsg("该用户不存在!\n")
			return
		}
		content := strings.Split(msg,"|")[2]
		if content == "" {
			this.SendMsg("消息不能为空!")
			return
		}
		remoteUser.SendMsg(this.Name+"对您说:" + content +"\n")
	} else {
		this.server.BroadCast(this,msg)

	}
}

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

Client端代码

client.go

package main

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

type Client struct {
	ServerIp string
	ServerPort int
	Name string
	conn net.Conn
	flag int
}

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
}

//查询在线用户
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) DealResponse() {
	//一旦 client.conn 有数据,就直接copy 到 stdout标准输出上,永久阻塞监听
	io.Copy(os.Stdout,client.conn)
}

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) 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) 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
		case 0:
			fmt.Println(4)
		}
	}
}

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(serverIp,serverPort)
	if client == nil {
		fmt.Println(">>>>>> 连接服务器失败...")
		return
	}

	go client.DealResponse()

	fmt.Println(">>>>>链接服务器成功...")
	client.Run()
}

项目运行步骤

项目启动步骤:

如果是Windows下运行项目,则需要先运行 chcp 65001 临时将 cmd 的编码方式修改为 UTF-8 ,保证不会中文乱码

步骤:

  1. 先运行 go build server.go user.go 将server编译好
  2. 运行 go build client.go 将 client 编译好
  3. 运行 server 端,启动服务端进行监听
  4. 运行 3 个终端,都运行 client -ip 127.0.0.1 -port 8888
  5. 在客户端中测试各个命令是否都可以成功运行

项目运行展示

(这里我的gif压缩弄了好久😄)

在这里插入图片描述


遇到的问题

  1. Windows 下 nc 命令模拟客户端的时候报错
    解决方案:Windows下添加nc命令

  2. 命令行 go build 的时候报错
    解决方案:go 语言是多文件编译模式,所以用下面的命令,将三个文件一起编译即可

go build server.go user.go client.go

总结

通过这次小项目的学习,让我学习巩固了go语言基础的语法和 goroutine 的具体使用场景,学习到了 go 语言本身的并行思想,对锁的应用场景,管道的使用,也加强了对网络编程的熟悉程度。如果对童鞋你有帮助的话,可以给博主点个赞,评个论,收个藏啥的支持一下哈哈哈😄👍👍
在这里插入图片描述


后记

最近也陆陆续续有童鞋联系博主询问关于软件专业的一些岗位问题,因为我是大三,具体的岗位我也不好推荐,我印象中前端组应该小姐姐的数量是比后端组的更多把哈哈哈😄😄 ,希望更多的小伙伴可以一起交流学习,共同成长!💪💪
在这里插入图片描述


坚持分享,坚持原创,喜欢博主的靓仔靓女们可以看看博主的首页博客!
您的点赞与收藏是我分享博客的最大赞赏!
博主博客地址: https://blog.csdn.net/weixin_43967679

;原文链接:https://blog.csdn.net/weixin_43967679/article/details/115445552
本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!

推荐图文


随机推荐