把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.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.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.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 ,保证不会中文乱码
步骤:
(这里我的gif压缩弄了好久😄)
Windows 下 nc 命令模拟客户端的时候报错
解决方案:Windows下添加nc命令
命令行 go build 的时候报错
解决方案:go 语言是多文件编译模式,所以用下面的命令,将三个文件一起编译即可
go build server.go user.go client.go
通过这次小项目的学习,让我学习巩固了go语言基础的语法和 goroutine 的具体使用场景,学习到了 go 语言本身的并行思想,对锁的应用场景,管道的使用,也加强了对网络编程的熟悉程度。如果对童鞋你有帮助的话,可以给博主点个赞,评个论,收个藏啥的支持一下哈哈哈😄👍👍
最近也陆陆续续有童鞋联系博主询问关于软件专业的一些岗位问题,因为我是大三,具体的岗位我也不好推荐,我印象中前端组应该小姐姐的数量是比后端组的更多把哈哈哈😄😄 ,希望更多的小伙伴可以一起交流学习,共同成长!💪💪
坚持分享,坚持原创,喜欢博主的靓仔靓女们可以看看博主的首页博客!
您的点赞与收藏是我分享博客的最大赞赏!
博主博客地址: https://blog.csdn.net/weixin_43967679
1. 始终在 `v-for` 中使用 `:key` 在需要操纵数据时,将key属性与v-for指令一起...
PHP+Mysql简单实现了图书购物车 本文主要讲述如何通过PHP+HTML简单实现图书购物...
步骤: 1、新建一个空文件,文件名为hhhh 2、初始化 git init 3、自己要与origin...
mysql提供的模式匹配的其他类型是使用扩展正则表达式。 当你对这类模式进行匹配...
图片来自 Pexels 突然电话响了起来,一看是我们的一个开发同学,顿时紧张了起来...
Java HashMap removeNode 方法 分析 源码分析仓库 https://github.com/HANXU2018...
官网连接 https://docs.rt-thread.org/#/rt-thread-version/rt-thread-standard/...
有许多命令行选手在linux下开发的时候会经常遇到一个问题,无论是svn还是git,提...
vscode怎么浏览器打开html预览?这里大家可以通过安装open in browser插件解决。...
1.小程序端代码示例 my.getPhoneNumber({ success: (res) = { let encryptedData...