基于go开发的终端版即时通信系统(c-s架构)

news2024/11/26 12:28:52

项目架构图

类似一个聊天室一样

整体是一个客户端和服务端之间的并发多线程网络通信,效果可以翻到最后面看。

为了巩固基础的项目练手所以分为9个阶段进行迭代开发

版本⼀:构建基础Server

新建一个文件夹就叫golang-IM_system

第一阶段先将server的大致写好

新建一个server.go文件

server.go中写package main作为服务端的主文件,里面主要是server的结构体

再创建一个mian.go也写上package main 并且初始化main函数作为当前进程的主入口

server.go要做的事情一共需要有4个步骤

//socket listen

使用net.Listen创建一个listener进行监听,需要传入协议类型和ip地址和端口号

//accept

死循环中使用listener.Accept()进行接受客户端的连接

//do handler

对于接受到的客户端(建立的链接)使用goroutine开辟协程进行处理业务逻辑,不阻塞accept的继续接受

//close listen socket

使用defer定义listener.Close()进行关闭

完整代码:

server.go:

package main

import (
    "fmt"
    "net"
    "strconv"
)

type Server struct {
    Ip   string
    Port int
}

// 创建一个server对象的接口(返回server对象)
func NewServer(ip string, port int) *Server { //返回的是指针类型也就是地址
    server := &Server{ //取的是对象的地址
        Ip:   ip,
        Port: port,
    }
    return server
}

// 当有客户端链接就用协程来进行走下去防止主函数阻塞,传入的就是建立的链接的实例conn
func (this *Server) Handler(conn net.Conn) {
           //当前链接的业务

    fmt.Println("链接建立成功!!!")
}

// 启动服务器的接口
func (this *Server) Start() {
    //socket listen
    //启动一个监听器。传入协议和IP加端口的字符串(127.0.0.1:8888),返回一个listener和err
    listener, err := net.Listen("tcp", this.Ip+":"+strconv.Itoa(this.Port))
    //判断是否启动监听器成功
    if err != nil {
        fmt.Println("net.Listen err:", err)
        return
    }

    //close listen socket
    //使用defer关闭套接字
    defer listener.Close()

    //死循环接受链接
    for {
        //accept
        //返回一个链接的实例和err,这个实例有内置的读写操作
        conn, err := listener.Accept()
        //不停的进行接受链接的对象
        if err != nil {
            fmt.Println("conn.Accept err:", err)
            continue
        }

        //do handler
        //一旦有了链接就开个携程去执行业务逻辑,同时不阻塞这里的服务端继续接受新的链接
        go this.Handler(conn)
    }

}

main.go:

package main

func main() {
    //实例化对象
    server := NewServer("127.0.0.1", 8888)
    //启动套接字监听
    server.Start()
}

测试

代码编辑完后使用build进行生成可执行文件

go build -o server.exe main.go server.go

使用nc暂时作为客户端进行连接,程序成功的执行

版本⼆: ⽤户上线功能

结构图

相比版本一需要加入用户上线后的一些处理功能,需要新定义一个user的结构体用来表示每个client对应的user。为了server进行区分client需要添加新的属性即一个map用来存储加入的所有的user对象(对于server来说他的眼里只有user),同时server需要有个新的"广播"的功能,比如有个新client上线了可以广播给所有的用户都可以收到有个新的client的上线了的信息,这就是message发送功能,同时每个user也需要有相应的channel进行接受信息和conn属性用来将chan接受的信息实际的发送给真实的client。所以本版本任然是在server上做升级不涉及client

新建user.go文件

创建User的结构体,四个属性

channel是用来接受server信息的,conn是连接的实例。

创建一个函数用来new一个user对象

创建一个用来监听channel的函数

此时可以在NewUser()函数中直接增加goroutine启动的步骤,因为一旦一个新的对象被创建就可以顺便将监听的步骤启动

回到server.go

修改结构体,添加新的属性

mapLock sync.RWMutex 定义了一个读写锁,用于保护一个映射(map)的操作,确保在并发环境下对映射的读写是线程安全的。

相应的NewServer()函数也要修改

广播功能的发生在用户上线之后,可以写在业务处理的函数Handler()中

先将建立的客户端信息写入map中(这里用了互斥锁,在进行map操作时先上锁防止其他协程在操作锁导致出问题,写完map后再解锁)

创建一个广播函数

在handler函数中调用广播函数

创建一个监听广播消息channel的函数

用一个for range来遍历所有的user

在Start()函数中用goroutine启动ListenMessager()

最后在handler中用个select{}进行阻塞,效果就只是为了阻塞当前线程否则函数执行完了连接就直接掉了

完整代码

server.go

package main

import (
    "fmt"
    "net"
    "strconv"
    "sync"
)

type Server struct {
    Ip   string
    Port int

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

    //消息广播的channel
    Message chan string
}

// 创建一个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
}

// 广播消息的方法
func (this *Server) BroadCast(user *User, msg string) {
    //这是要发送的消息的内容
    sendmmsg := "[" + user.Addr + "]" + user.Name + ":" + msg
    //将内容写入管道
    this.Message <- sendmmsg
}

// 监听this.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()
    }
}

// 当有客户端链接就用协程来进行走下去防止主函数阻塞,传入的就是建立的链接的实例conn
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, "ok is onling")

    //阻塞handler
    select {}
}

// 启动服务器的接口
func (this *Server) Start() {
    //socket listen
    //启动一个监听器。传入协议和IP加端口的字符串(127.0.0.1:8888),返回一个listener和err
    listener, err := net.Listen("tcp", this.Ip+":"+strconv.Itoa(this.Port))
    //判断是否启动监听器成功
    if err != nil {
        fmt.Println("net.Listen err:", err)
        return
    }

    //close listen socket
    //使用defer关闭套接字
    defer listener.Close()

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

    //死循环接受链接
    for {
        //accept
        //返回一个链接的实例和err,这个实例有内置的读写操作
        conn, err := listener.Accept()
        //不停的进行接受链接的对象
        if err != nil {
            fmt.Println("conn.Accept err:", err)
            continue
        }

        //do handler
        //一旦有了链接就开个携程去执行业务逻辑,同时不阻塞这里的服务端继续接受新的链接
        go this.Handler(conn)
    }

}

user.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 {
    //获取client端的地址作为下面的Name的值
    userAddr := conn.RemoteAddr().String()
    //创建对象
    user := &User{
        Name: userAddr,
        Addr: userAddr,
        C:    make(chan string),
        Conn: conn,
    }
    //启动当前监听的user channel消息的goroutine
    go user.ListenMessage()

    return user
}

// 监听当前的User channel的内容,一但有消息就直接发送发给client端
func (this *User) ListenMessage() {
    //死循环一直监听
    for {
        msg := <-this.C
        //以字节的形式发送,同时加换行
        this.Conn.Write([]byte(msg + "\r\n"))
    }
}

main.go不变

测试

将代码编译一下进行测试,这里编码有点问题我直接换成了英文,可以看到没上线一个client后server就会向已存在的client发一次消息。

版本三: ⽤户消息⼴播机制

完善handle处理业务⽅法,启动 ⼀个针对当前客户端的接受信息的操作,实现将客户端的消息接受然后广播到每一个在线用户

增加代码片段

增加了一个匿名函数用来接受客户端的消息并且进行广播

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, "ok is onling")

    //接受客户端发送的消息
    go func() {
        //创建了一个字节切片,其长度为4096,容量也为4096
        buf := make([]byte, 4096)
        for {
            //从套接字中读取数据放入buf中,返回实际读取到的字节数n和错误
            n, err := conn.Read(buf)
            //在tcp挥手最后会发送0字节的数据所以这里可以用0来判断是否断开链接
            if n == 0 {
                this.BroadCast(user, "quit")
                return
            }
            //检查是否有非io的EOF错误发生
            if err != nil && err != io.EOF {
                fmt.Println("conn read err:", err)
                return
            }
            
            //提取用户的消息(去掉'\n').不过这样写肯会出现切片越界的错误,正常应该要对buf中的n做限制
            msg := string(buf[:n-1])

            //将得到的消息进行广播
            this.BroadCast(user, msg)
        }

    }()

         //阻塞handler
     select {}
}

测试

版本四:⽤户业务层封装

此版本只是对前面的业务除了handler中的一些函数将其封装为user类的函数,使得代码整体更加的简洁美观

首先在user类中增加一个用户上线功能的业务函数

用户上线功能

也就是将handler中的这一部分进行封装

不过此方式需要使用到当前的server对象,所以在user类中应该要把server对象作为属性传进来

同时修改NewUser函数,并且需要传入形参

最后在server中调用的NewUser函数需要传入server对象,直接使用this指针

于是User类中的用户上线业务功能就可以集成为

server中直接调用就可以了

用户下线功能

之前的handler中只是简单的进行了下线的广播,实际上应该将下线的用户从OnlienMap中删除后再进行广播的

原本处理方式:

现在在User类中新增加一个Offline的功能,使用delete方法将其从map中删除后再进行广播

修改后直接调用函数

用户处理消息功能

在User中新增DoMessage⽅法用来处理客户端发送广播消息的功能

handler中

User类中,只是进行封装没啥大变化

修改后

完整代码

server中的handler

// 当有客户端链接就用协程来进行走下去防止主函数阻塞,传入的就是建立的链接的实例conn
func (this *Server) Handler(conn net.Conn) {
    //当前链接的业务

    //fmt.Println("链接建立成功!!!")

    user := NewUser(conn,this)

    //用户的上线功能
    user.Oneline()

    //接受客户端发送的消息
    go func() {
        //创建了一个字节切片,其长度为4096,容量也为4096
        buf := make([]byte, 4096)
        for {
            //从套接字中读取数据放入buf中,返回实际读取到的字节数n和错误
            n, err := conn.Read(buf)
            //在tcp挥手最后会发送0字节的数据所以这里可以用0来判断是否断开链接
            if n == 0 {
                user.Offline()
                return
            }
            //检查是否有非io的EOF错误发生
            if err != nil && err != io.EOF {
                fmt.Println("conn read err:", err)
                return
            }
            
            //提取用户的消息(去掉'\n').不过这样写肯会出现切片越界的错误,正常应该要对buf中的n做限制
            msg := string(buf[:n-1])

            //将得到的消息进行广播
            user.DoMessage(msg)
        }

    }()

    //阻塞handler
    select {}
}

User类

package main

import "net"

type User struct {
    Name string
    Addr string
    C    chan string
    Conn net.Conn
    Server *Server
}

// 创建一个用户的API
func NewUser(conn net.Conn,server *Server) *User {
    //获取client端的地址作为下面的Name的值
    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) Oneline()  {
    //用户上线,将用户加入到OnlineMap中
    this.Server.mapLock.Lock()
    this.Server.OnlineMap[this.Name] = this
    this.Server.mapLock.Unlock()

    //调用广播函数进行广播当前用户上线信息
    this.Server.BroadCast(this, "ok is onling")
}

//用户的下线业务
func (this *User) Offline()  {
    //用户下线后将用户信息从OnlineMap中删除
    this.Server.mapLock.Lock()
    delete(this.Server.OnlineMap, this.Name)
    this.Server.mapLock.Unlock()
    //调用广播函数进行广播当前用户下线信息
    this.Server.BroadCast(this,"is Offline")
} 

//用户处理消息的业务
func (this *User) DoMessage(msg string) {
    this.Server.BroadCast(this,msg)
}

// 监听当前的User channel的内容,一但有消息就直接发送发给client端
func (this *User) ListenMessage() {
    //死循环一直监听
    for {
        msg := <-this.C
        //以字节的形式发送,同时加换行
        this.Conn.Write([]byte(msg + "\r\n"))
    }
}

测试

版本五:在线⽤户查询

此版本增加了客户输入who后server会将所有的当前在线的用户单独的发送给当前的客户进行显示

因为是先接受客户端发送来的消息进行判断是不是"who"然后在执行显示在线客户的业务所以代码就写在User中的DoMessage()中

新建了一个SendMsg函数用来对当前用户的客户端发送消息,也就是单独的发送,这里没有使用channel因为channel写死了广播的功能,如果要指定单个客户端就需要去修改前面的代码不如直接使用Conn.Write方便

使用for循环将OnlineMaph中的用户遍历出来让后作为msg发送给客户端

新增代码

测试

版本六:修改⽤户名

此版本新增功能修改用户名,逻辑实现在DoMessage()中,先对新名字做判断是否已经使用过,判断完后从map中删除存在的当前此用户的信息,然后修改当前用户的信息即修改名字,然后再将这个修改后的user的信息写入map中。

消息格式定义为“rename|张三”

判断是否有rename然后提取张三,rename的判断使用长度和字符比较,然后通过|进行切割得到第二个也就是名字的数组内容

新增代码

测试

版本七:超时强踢功能

此版本新增一个如果客户端长时间不发消息就将客户端踢出去的功能,实现逻辑通过select进行监视一个ilive的管道同时使用time.After(time.Second * 10)来做超时的定时器,time.After(time.Second * 10)返回一个管道内容是当前的时间,也就是隔10秒就发一个当前的时间,每次 select 语句执行时,都会创建一个新的定时器也就是刷新计时器这是他自带的功能。

首先新增islive的channel来监视客户的存活情况

在server中的handler进行监视

增加isLive管道

发出了消息就代表活跃

增加select监视功能

在踢人时需要注意释放资源,这里isLivehui伴随当前线程一起消亡但是user.C不是在当前创建的所以需要手动的释放资源

测试

可以看到实际中在踢了人后map中也会把被踢着的信息删除,这是前面写的Offline函数的触发条件由conn.close()也会触发也就是n==0。

版本⼋:私聊功能

此版本增加私聊功能,可以指定对某一个用户发消息,格式 to|dreamer292|hello,代码逻辑和前面的在线用户查询和修改名字差不多。

在user中的DoMessage()函数中增加

继续增加一个else if{}

先判断是不是to|

然后三个判断格式、用户是否存在、发的消息是否正常,最后才将消息转发,直接使用对象中的SendMsg

测试

版本九:客户端实现

1、客户端类型定义与conn连接

写一个client不再使用nc当做客户端

结构体和NewClient,需要注意name不需要赋值,等待rename的操作即可,

main函数启动客户端,这里select也是用来阻塞的,写个计时器是编译后select没有case会立刻的结束写个case卡在那里。

测试

2、解析命令⾏

实现从终端输入指令进行接收,需要使用到go中的flag库来实现

在go中init()函数在main之前执行

main()中

先使用flag解析,然后传入全局变量即可

测试

使用-h 参数查看提示,这是flag库封装好的,只能说太强了简直专门为写脚本而生的库

3、菜单显示

新增加一个flag属性并且初始化为999,这个flag用来判断选择的功能

首先定义一个menu()菜单函数来展示菜单的内容

写这里的时候突然想到这个函数没有传入对象怎么修改了对象的属性的,我之前一直以为(Client *Client)是定义的这个函数的类型,说明他是一个类指针函数,然后去查了一下这个的意思是定义的接受者的意思,就是这个函数要由一个*Client类型的对象来调用那么函数里面的Client就是这个调用者。所以就会修改这个调用者的信息。

菜单定义完成后我们需要定义一个处理业务的函数run(),先是两个判断然后再使用switch进行业务选择

最后在main函数中调用run

测试

4、实现更新⽤户名

将更新用户名的操作进行封装函数然后在run里面直接调就行了

这里直接发送消息正好就是server处理消息的方式,刚好对上了

这个时候server也会回消息,所以需要定义一个函数专门用来接受server的消息,接受到了就直接输出即可

在main中开个goruotine

测试

5、实现公聊模式

也是封装一个函数处理一下消息然后发送给server即可

一个for循环来持续发送消息,如果输入exit就退出

业务主函数调用

测试

6、实现查询当前在线用户和私聊模式

查询当前在线用户很简答server端写死了只需要输入who即可查询,相应的menu中也加上一段提示,case也加个4

私聊模式实现

两个for循环,外面一个用来接收指定的用户名,里面一个用来接收要发送的内容

最后在case那里调用一下函数

测试

完美的实现了本项目哈哈哈

总结

        通过本次小项目算是对go的理解更加的深入了,本身我不是做开发的所以一些设计模式什么的也不是很懂,底层的东西也不是很清楚,对我而言能用来写一写脚本做一些简单的poc\exp的漏洞利用工具就可以了哈哈哈。这次算是比较完整的跟进了一次网络通信的模型,对于客户端和服务端之间有了更深的理解,以及在开发方面的一些思想也有了更深的理解,对于后续使用go做工具开发打下了很好的基础,另外对此项目在逻辑方面我可以看到有很多不足的地方,我想这就是是安全存在的意义,感谢刘丹冰老师,老师讲的真的非常的好,对于掌握了其他语言有其他语言基础的同学如果想要快速入门go他的视频真的值得一看,主打就是高效。

        刘丹冰老师的教程:8小时转职Golang工程师(如果你想低成本学习Go语言)_哔哩哔哩_bilibili

最后附上项目源码

server端

server.go

package main

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

type Server struct {
    Ip   string
    Port int

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

    //消息广播的channel
    Message chan string
}

// 创建一个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
}

// 广播消息的方法
func (this *Server) BroadCast(user *User, msg string) {
    //这是要发送的消息的内容
    sendmmsg := "[" + user.Addr + "]" + user.Name + ":" + msg
    //将内容写入管道
    this.Message <- sendmmsg
}

// 监听this.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()
    }
}

// 当有客户端链接就用协程来进行走下去防止主函数阻塞,传入的就是建立的链接的实例conn
func (this *Server) Handler(conn net.Conn) {
    //当前链接的业务

    //fmt.Println("链接建立成功!!!")

    user := NewUser(conn, this)

    //用户的上线功能
    user.Oneline()

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

    //接受客户端发送的消息
    go func() {
        //创建了一个字节切片,其长度为4096,容量也为4096
        buf := make([]byte, 4096)
        for {
            //从套接字中读取数据放入buf中,返回实际读取到的字节数n和错误
            n, err := conn.Read(buf)
            //在tcp挥手最后会发送0字节的数据所以这里可以用0来判断是否断开链接
            if n == 0 {
                user.Offline()
                return
            }
            //检查是否有非io的EOF错误发生
            if err != nil && err != io.EOF {
                fmt.Println("conn read err:", err)
                return
            }

            //提取用户的消息(去掉'\n').不过这样写肯会出现切片越界的错误,正常应该要对buf中的n做限制
            msg := string(buf[:n-1])

            //将得到的消息进行广播
            user.DoMessage(msg)

            //用户发出任何的消息代表他是存活的状态
            isLive <- true
        }

    }()

    //阻塞handler
    for {
        select {
        case <-isLive:
            //当前用户是活跃的,不需要做任何事情就激活select然后下面的定时器会刷新重置
        case <-time.After(time.Second * 1000):
            //如果这个case接受到值就代表超时了
            //将当前的user强制关闭
            user.SendMsg("You have been forcibly taken offline!!!")

            //销毁占用的资源
            close(user.C)
            //关闭链接
            conn.Close()

            return

        }
    }

}

// 启动服务器的接口
func (this *Server) Start() {
    //socket listen
    //启动一个监听器。传入协议和IP加端口的字符串(127.0.0.1:8888),返回一个listener和err
    listener, err := net.Listen("tcp", this.Ip+":"+strconv.Itoa(this.Port))
    //判断是否启动监听器成功
    if err != nil {
        fmt.Println("net.Listen err:", err)
        return
    }

    //close listen socket
    //使用defer关闭套接字
    defer listener.Close()

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

    //死循环接受链接
    for {
        //accept
        //返回一个链接的实例和err,这个实例有内置的读写操作
        conn, err := listener.Accept()
        //不停的进行接受链接的对象
        if err != nil {
            fmt.Println("conn.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 {
    //获取client端的地址作为下面的Name的值
    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) Oneline() {
    //用户上线,将用户加入到OnlineMap中
    this.Server.mapLock.Lock()
    this.Server.OnlineMap[this.Name] = this
    this.Server.mapLock.Unlock()

    //调用广播函数进行广播当前用户上线信息
    this.Server.BroadCast(this, "ok is onling")
}

// 用户的下线业务
func (this *User) Offline() {
    //用户下线后将用户信息从OnlineMap中删除
    this.Server.mapLock.Lock()
    delete(this.Server.OnlineMap, this.Name)
    this.Server.mapLock.Unlock()
    //调用广播函数进行广播当前用户下线信息
    this.Server.BroadCast(this, "is Offline")
}

// 用来给当前用户对应的客户端发送消息
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 {
            onlineuser_msg := "[" + user.Addr + "]" + user.Name + ": is onling......\n"
            this.SendMsg(onlineuser_msg)
        }
        this.Server.mapLock.Unlock()
    } else if len(msg) > 7 && msg[:7] == "rename|" {
        //消息格式 rename|zhangsan
        newName := strings.Split(msg, "|")[1]

        //this.SendMsg(msg)
        //判断新名字是否被使用了
        _, ok := this.Server.OnlineMap[newName]
        if ok {
            //已经被使用就了通知一下
            this.SendMsg("newName already in use please new one!")
        } else {
            //先删除map中的信息再修改个人信息再重新添加到map
            this.Server.mapLock.Lock()
            delete(this.Server.OnlineMap, this.Name)
            this.Name = newName
            this.Server.OnlineMap[this.Name] = this
            this.Server.mapLock.Unlock()

            this.SendMsg("already use newName you name is " + this.Name + "\n")
        }
    } else if len(msg) > 4 && msg[:3] == "to|" {
        //消息格式 to|user|content

        //1、获取对方的用户名
        remoteName := strings.Split(msg, "|")[1]
        if remoteName == "" {
            //判断一下格式问题
            this.SendMsg("The message format is incorrect, please use \"to|user|content\"\n")
            return
        }
        //2、根据用户名获取user对象
        remoteUser, ok := this.Server.OnlineMap[remoteName]
        if !ok {
            this.SendMsg("The remote user does not exist\n")
            return
        }
        //3、获取消息内容,通过对方的user对象的将消息发送出去
        content := strings.Split(msg, "|")[2]
        if content == "" {
            this.SendMsg("The content is empty, please enter the content\n")
            return
        }
        //发送消息
        remoteUser.SendMsg(this.Name + " tell you: " + content)

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

// 监听当前的User channel的内容,一但有消息就直接发送发给client端
func (this *User) ListenMessage() {
    //死循环一直监听
    for {
        msg := <-this.C
        //以字节的形式发送,同时加换行
        this.Conn.Write([]byte(msg + "\r\n"))
    }
}

main.go

package main

func main() {
    //实例化对象
    server := NewServer("127.0.0.1", 8888)
    //启动套接字监听
    server.Start()
}

客户端

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 {
    //创建客户端对象,name由rename来修改不需要传入
    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
}

var serverIp string
var serverPort int

// client.exe -ip 127.0.0.1 -port 8888
func init() {
    //第2个参数就是终端-ip 第3个参数是默认值,第4个参数是提示词
    flag.StringVar(&serverIp, "ip", "127.0.0.1", "set server ip address(default: 127.0.0.1)")
    flag.IntVar(&serverPort, "port", 8888, "set server port number(default: 8888)")
}

// 菜单展示功能
func (Client *Client) menu() bool {
    //定义一个flag参数确定使用的是哪一个功能
    var flag int
    //提示选择
    fmt.Println("1、Public chat mode")
    fmt.Println("2、Private Chat Mode")
    fmt.Println("3、Update name")
    fmt.Println("4、Show onlining users")
    fmt.Println("0、Return system")

    //此时从终端接收选择
    fmt.Scanln(&flag)
    //判断输入的数字
    if flag >= 0 && flag <= 4 {
        //修改flag的值为输入的值
        Client.flag = flag
        return true
    } else {
        fmt.Println(">>>>Please input legal num<<<<")
        return false
    }
}

// 业务主函数
func (Client *Client) run() {
    //检查flag的值,只要不是0就说明可能选择了业务
    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 4:
            //查询当前在线用户
            Client.SelectUsers()
            break

        }
    }
}

// 处理server回应的消息,直接显示到标准输入即可
func (Client *Client) DealRespone() {
    //一但client.conn有数据,就是copy到stdout的标准输出上,永久的阻塞监听
    //和for { client.conn.Read()}的效果一直
    io.Copy(os.Stdout, Client.Conn)
}

// 更新用户名
func (Client *Client) UpdateName() bool {
    fmt.Println("Please input new name:")
    //这里是传入的是name变量的地址
    fmt.Scanln(&Client.Name)
    //和server中处理rename刚好对上了,妙啊
    sendMsg := "rename|" + Client.Name + "\n"

    _, err := Client.Conn.Write([]byte(sendMsg))
    if err != nil {
        fmt.Println("write error:", err)
        return false
    }
    return true
}

// 公聊模式
func (Client *Client) PublicChat() {
    //提示用户输入消息
    var chatMsg string

    fmt.Println(">>>>Please input chat message,use \"exit\" off chat")
    fmt.Scanln(&chatMsg)
    //死循环来持续发送,一旦输入exit就推出聊天
    for chatMsg != "exit" {
        //发送给server
        //消息不为空发送
        if len(chatMsg) != 0 {
            sendMsf := chatMsg + "\n"
            _, err := Client.Conn.Write([]byte(sendMsf))
            if err != nil {
                fmt.Println("write error:", err)
                break
            }
        }
        chatMsg = ""
        fmt.Println(">>>>Please input chat message,use \"exit\" off chat")
        fmt.Scanln(&chatMsg)
    }
}

// 查询当前在线用户
func (Client *Client) SelectUsers() {
    sendMsg := "who\n"
    _, err := Client.Conn.Write([]byte(sendMsg))
    if err != nil {
        fmt.Println("write error:", err)
        return
    }
}

// 私聊模式
func (Client *Client) PrivateChat() {
    var remoteName string
    var chatMsg string
    //显示当前在线的用户
    Client.SelectUsers()
    fmt.Println(">>>>Please choose user to caht[username],\"exit\" off")
    fmt.Scanln(&remoteName)

    for remoteName != "exit" {
        fmt.Println(">>>>Please input content,\"exit\" off")
        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("write error:", err)
                    break
                }
            }
            chatMsg = ""
            fmt.Println(">>>>Please input content,\"exit\" off")
            fmt.Scanln(&chatMsg)
        }
        Client.SelectUsers()
        fmt.Println(">>>>Please choose user to caht[username],\"exit\" off")
        fmt.Scanln(&remoteName)
    }
}
func main() {
    //命令行解析
    flag.Parse()

    client := NewClient(serverIp, serverPort)
    if client == nil {
        fmt.Println(">>>>>>>>Failed to connect to server...")
        return
    }
    //开个goroutine来处理server的消息
    go client.DealRespone()
    fmt.Println(">>>>>>>>Connecting to server...")

    //启动客户端的业务
    client.run()

}

编译命令

go build -o server.exe server.go main.go user.go

go build -o client.exe client.go

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

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

相关文章

沈阳化工大学第十一届程序设计沈阳区竞赛:凿冰 Action(博弈论,思维)

链接&#xff1a;登录—专业IT笔试面试备考平台_牛客网 来源&#xff1a;牛客网 题目描述 北极探险队有新收获了&#xff01;&#xff01;&#xff01; 北极探险队发现了NNN条长度不一的冰柱&#xff0c;由于冰柱里封存有价值的生物&#xff0c;现在需要两名生物学家小A和小…

TON生态小游戏开发:推广、经济模型与UI设计的建设指南

随着区块链技术的快速发展&#xff0c;基于区块链的Web3游戏正引领行业变革。而TON生态小游戏&#xff0c;借助Telegram庞大的用户基础和TON&#xff08;The Open Network&#xff09;链上技术&#xff0c;已成为这一领域的明星之一。国内外开发者正迅速涌入&#xff0c;开发和…

如何在算家云搭建Kolors(图像生成)

一、模型介绍 Kolors 是快手 Kolors 团队基于潜在扩散的大规模文本转图片生成模型。经过数十亿个文本-图片对的训练&#xff0c;Kolors 在视觉质量、复杂语义准确性和中英文文本渲染方面均比开源和闭源模型具有显著优势。此外&#xff0c;Kolors 支持中英文输入&#xff0c;在…

C语言基础语法——类型转换

数据有不同的类型&#xff0c;不同类型数据之间进行混合运算时涉及到类型的转换问题。 转换的方法有两种&#xff1a; 自动类型转换&#xff08;隐式转换&#xff09;&#xff1a;遵循一定的规则&#xff0c;由编译系统自动完成强制类型转换&#xff08;显示转换&#xff09;…

http协议概述与状态码

目录 1.HTTP概述 1.1请求报文起始行与开头 ​1.2响应报文起始行 ​ 1.3响应报文开头 ​ 2.http状态协议码 1.HTTP概述 默认端口 80 HTTP超文本传输与协议: 数据请求和响应 传输:将网站的数据传递给用户 超文本:图片 视频等 请求request:打开网站 访问网站 响应r…

Python数据分析-垃圾邮件分类

一、研究背景 随着电子通信技术的飞速发展&#xff0c;电子邮件已经成为人们日常工作和生活中不可或缺的一部分。然而&#xff0c;伴随着这一趋势&#xff0c;垃圾邮件&#xff08;Spam&#xff09;的数量也在急剧增加。垃圾邮件不仅会占用用户的邮箱空间&#xff0c;还可能含…

设置dl服务解决github pushTimed out问题

提交代码到GitHub&#xff0c;一直提示提交失败 我们一般是fq挂的dl服务器进行的&#xff0c;而git需要配置下dl&#xff0c;此时我们要将dl服务器对应的IP地址和端口为我们所调用。 查找dl服务器&#xff08;windows直接搜索dl服务器设置&#xff0c;mac参考官网&#xff09…

【北京迅为】《STM32MP157开发板嵌入式开发指南》-第二十二章 安装VMware Tool 工具

iTOP-STM32MP157开发板采用ST推出的双核cortex-A7单核cortex-M4异构处理器&#xff0c;既可用Linux、又可以用于STM32单片机开发。开发板采用核心板底板结构&#xff0c;主频650M、1G内存、8G存储&#xff0c;核心板采用工业级板对板连接器&#xff0c;高可靠&#xff0c;牢固耐…

学习python自动化——pytest单元测试框架

一、什么是pytest 单元测试框架&#xff0c;unittest&#xff08;python自带的&#xff09;&#xff0c;pytest&#xff08;第三方库&#xff09;。 用于编写测试用例、收集用例、执行用例、生成测试结果文件&#xff08;html、xml&#xff09; 1.1、安装pytest pip instal…

【ARM Linux驱动开发】嵌入式ARM Linux驱动开发基本步骤

【ARM Linux驱动开发】嵌入式ARM Linux驱动开发基本步骤 文章目录 开发环境驱动开发&#xff08;以字符设备为例&#xff09;安装驱动应用程序开发附录&#xff1a;压缩字符串、大小端格式转换压缩字符串浮点数压缩Packed-ASCII字符串 开发环境 首先需要交叉编译器和Linux环境…

豆包PixelDance指南:字节跳动推出的AI视频生成大模型,突破多主体互动难关

豆包PixelDance是由字节跳动旗下火山引擎发布的AI视频生成大模型。它是业界首个突破多主体互动难关的视频生成模型&#xff0c;支持多风格多比例的一致性多镜头生成。PixelDance基于DiT架构&#xff0c;具备高效的DiT融合计算单元&#xff0c;能够实现复杂的多主体运动交互和多…

【高等数学】 一元函数积分学

1. 不定积分的计算 1.1. 基本积分表 知识点 例题 1.2. 凑微分&#xff08;第一类换元法&#xff09; 知识点 本质&#xff1a;利用复合函数求导法则的逆运算 第一步&#xff0c;识别或者凑出来复合函数的导函数 如果被积函数具备以下特点&#xff1a; 1.它由两项相乘来表…

《案例》—— OpenCV 实现2B铅笔填涂的答题卡答案识别

文章目录 一、案例介绍二、代码解析 一、案例介绍 下面是一张使用2B铅笔填涂选项后的答题卡 使用OpenCV 中的各种方法进行真确答案识别&#xff0c;最终将正确填涂的答案用绿色圈出&#xff0c;错误的答案不圈出&#xff0c;用红色圈出错误题目的正确答案最终统计正确的题目数…

PCL用KDtree,给搜索到的邻近点上色

用KDtree&#xff0c;给搜索到的邻近点上色。 #include <pcl/io/pcd_io.h> #include <pcl/point_types.h>#include <pcl/search/kdtree.h> // 包含kdtree头文件 #include <pcl/visualization/pcl_visualizer.h> #include <boost/thread/thread.hpp&…

【MATLAB】基于RSSI的蓝牙定位程序,4个锚点、二维平面

目录 ​编辑 商品描述 主要功能 技术细节 适用场景 下载链接 商品描述 这款基于接收信号强度指示&#xff08;RSSI&#xff09;原理的蓝牙定位程序&#xff0c;专为需要高效、可靠定位解决方案的开发者和研究人员设计。它能够在二维平面内&#xff0c;通过4个锚点实现对未…

ESNP静态路由进阶

0x01 前言 前一篇文章说了一个简单的ESNP静态路由实验&#xff0c;那么后面给了一个进阶拓扑&#xff1a;https://blog.csdn.net/Moxin1044/article/details/142850946 现在我们来解决一下 0x02 拓扑图 在上一个拓扑图基础上&#xff0c;规划了一个新的7.0网段&#xff0c;那…

DGX平台

NVIDIA DGX 平台 文章目录 前言一、优势经过验证的企业 AI 标准体验 DGX 平台的优势1. 先进的 AI 开发平台2. 融入了 NVIDIA AI 专业技术和服务3. 性能卓越、成本可预测二、用例DGX 助力 AI 走向各行各业1. 壳牌推动能源领域不断取得突破2. 宝马借助 AI 重新定义工厂物流3. 索尼…

【Linux】段错误(核心已转储)

原因:linux在安装docker 安装完之后再添加用户就报错了。。。。 各种查原因: 内存问题:系统可能存在内存损坏或不足的问题。磁盘空间不足:系统分区可能没有足够的空间来创建新用户。文件系统错误:文件系统可能存在错误。SELinux或AppArmor:安全模块可能阻止了 useradd 命…

el-table表头加红色星标

代码&#xff1a; <el-table-column prop"name" label"姓名" width"auto"><template #header><span style"color: red; margin-right: 4px">*</span><span>姓名</span></template></el…

Java idea中项目在maven执行install命令后出现Java heap space错误

Java idea中项目在maven执行install命令后出现Java heap space错误 这代表着你的maven需要足够的内存来编译和打包你的项目&#xff0c;而你分配的堆内存却不够用。 解决办法&#xff1a; idea左上角File —> Settings Build —> Build Tools —> Maven —> Run…