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

项目架构图

类似一个聊天室一样

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

为了巩固基础的项目练手所以分为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:

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:

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

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

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处理业务⽅法,启动 ⼀个针对当前客户端的接受信息的操作,实现将客户端的消息接受然后广播到每一个在线用户

增加代码片段

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

Go 复制代码
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

Go 复制代码
// 当有客户端链接就用协程来进行走下去防止主函数阻塞,传入的就是建立的链接的实例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类

Go 复制代码
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

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

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

Go 复制代码
package main

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

客户端

client.go

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

相关推荐
傻啦嘿哟12 分钟前
代理IP在后端开发中的应用与后端工程师的角色
网络·网络协议·tcp/ip
Red Red16 分钟前
网安基础知识|IDS入侵检测系统|IPS入侵防御系统|堡垒机|VPN|EDR|CC防御|云安全-VDC/VPC|安全服务
网络·笔记·学习·安全·web安全
2401_857610031 小时前
SpringBoot社团管理:安全与维护
spring boot·后端·安全
亚远景aspice2 小时前
ISO 21434标准:汽车网络安全管理的利与弊
网络·web安全·汽车
Estar.Lee2 小时前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
友友马3 小时前
『 Linux 』网络层 - IP协议(一)
linux·网络·tcp/ip
javaDocker3 小时前
业务架构、数据架构、应用架构和技术架构
架构
弗锐土豆3 小时前
工业生产安全-安全帽第二篇-用java语言看看opencv实现的目标检测使用过程
java·opencv·安全·检测·面部
码老白4 小时前
【老白学 Java】Warshipv2.0(二)
java·网络