基于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:

package mainimport ("fmt""net""strconv"
)type Server struct {Ip   stringPort 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和errlistener, 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 mainfunc 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 mainimport ("fmt""net""strconv""sync"
)type Server struct {Ip   stringPort int//在线用户的列表OnlineMap map[string]*UsermapLock   sync.RWMutex//消息广播的channelMessage 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发送给全部的在线的Userthis.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] = userthis.mapLock.Unlock()//调用广播函数进行广播当前用户上线信息this.BroadCast(user, "ok is onling")//阻塞handlerselect {}
}// 启动服务器的接口
func (this *Server) Start() {//socket listen//启动一个监听器。传入协议和IP加端口的字符串(127.0.0.1:8888),返回一个listener和errlistener, 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的goroutinego 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 mainimport "net"type User struct {Name stringAddr stringC    chan stringConn 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消息的goroutinego 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] = userthis.mapLock.Unlock()//调用广播函数进行广播当前用户上线信息this.BroadCast(user, "ok is onling")//接受客户端发送的消息go func() {//创建了一个字节切片,其长度为4096,容量也为4096buf := 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)}}()//阻塞handlerselect {}
}

测试

版本四:⽤户业务层封装

此版本只是对前面的业务除了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,容量也为4096buf := 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)}}()//阻塞handlerselect {}
}

User类

package mainimport "net"type User struct {Name stringAddr stringC    chan stringConn net.ConnServer *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消息的goroutinego user.ListenMessage()return user
}//用户的上线业务
func (this *User) Oneline()  {//用户上线,将用户加入到OnlineMap中this.Server.mapLock.Lock()this.Server.OnlineMap[this.Name] = thisthis.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 mainimport ("fmt""io""net""strconv""sync""time"
)type Server struct {Ip   stringPort int//在线用户的列表OnlineMap map[string]*UsermapLock   sync.RWMutex//消息广播的channelMessage 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发送给全部的在线的Userthis.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()//监听用户是否活跃的channelisLive := make(chan bool)//接受客户端发送的消息go func() {//创建了一个字节切片,其长度为4096,容量也为4096buf := 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}}()//阻塞handlerfor {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和errlistener, 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的goroutinego 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 mainimport ("net""strings"
)type User struct {Name   stringAddr   stringC      chan stringConn   net.ConnServer *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消息的goroutinego user.ListenMessage()return user
}// 用户的上线业务
func (this *User) Oneline() {//用户上线,将用户加入到OnlineMap中this.Server.mapLock.Lock()this.Server.OnlineMap[this.Name] = thisthis.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|zhangsannewName := 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中的信息再修改个人信息再重新添加到mapthis.Server.mapLock.Lock()delete(this.Server.OnlineMap, this.Name)this.Name = newNamethis.Server.OnlineMap[this.Name] = thisthis.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 mainfunc main() {//实例化对象server := NewServer("127.0.0.1", 8888)//启动套接字监听server.Start()
}

客户端

client.go

package mainimport ("flag""fmt""io""net""os"
)type Client struct {ServerIp   stringServerPort intName       stringConn       net.Connflag       int
}func NewClient(serverip string, serverport int) *Client {//创建客户端对象,name由rename来修改不需要传入client := &Client{ServerIp:   serverip,ServerPort: serverport,flag:       999,}//连接serverconn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", serverip, serverport))if err != nil {fmt.Println("net.Dial error:", err)return nil}client.Conn = connreturn 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 = flagreturn 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()breakcase 2://私聊模式Client.PrivateChat()breakcase 3://更新用户名Client.UpdateName()breakcase 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 stringfmt.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 stringvar 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.mzph.cn/diannao/55949.shtml

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

相关文章

LLM | Tokenization 从原理与代码了解GPT的分词器

声明&#xff1a;以上内容全是学习Andrej Karpathy油管教学视频的总结。 --------------------------------------------------------------------------------------------------------------------------------- 大家好。在今天我们学习llm中的Tokenization&#xff0c;即分…

使离医院最远的村庄到医院的路程最短

给定n个村庄之间的交通图&#xff0c;若村庄i和j之间有道路&#xff0c;则将顶点i和j用边连接&#xff0c;边上的Wij表示这条道路的长度&#xff0c;现在要从这n个村庄中选择一个村庄建一所医院&#xff0c;问这所医院应建在哪个村庄&#xff0c;才能使离医院最远的村庄到医院的…

HCIP--以太网交换安全(三)MAC地址漂移防止与检测

MAC地址漂移防止与检测 一、MAC地址漂移防止与检测知识点 1.1MAC地址漂移的概述 MAC地址漂移是指交换机上一个vlan内有两个端口学习到同一个MAC地址&#xff0c;后学习到的MAC地址表项覆盖原MAC地址表项的现象。 1.2.MAC地址漂移的防止方法 &#xff08;1&#xff09;配置…

Windows7 X64 成功安装 .NET Framework 4.8 的两种方法

Windows7 X64 成功安装 .NET Framework 4.8 的两种方法 windows7系统SP1安装完成后&#xff0c;在安装某软件时&#xff0c;提示需要先安装4.6以上的版本net-framework包&#xff0c;正好电脑里有个net-framework4.8软件包&#xff0c;于是打算用上&#xff0c;可是在安装时&a…

Github 2024-10-11 Java开源项目日报 Top9

根据Github Trendings的统计,今日(2024-10-11统计)共有9个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量Java项目9TypeScript项目1Vue项目1C++项目1JeecgBoot 企业级低代码开发平台 创建周期:2062 天开发语言:Java, Vue协议类型:Apache License 2.…

LabVIEW激光诱导击穿光谱识别与分析系统

LabVIEW激光诱导击穿光谱&#xff08;LIBS&#xff09;分析系统利用高能量脉冲激光产生高温等离子体&#xff0c;通过分析等离子体发出的光谱来定性分析样品中的元素种类。该系统的开发集成了软件与硬件的设计&#xff0c;实现了自动识别和定性分析功能&#xff0c;适用于环境监…

OpenCV 之 实现基于Lucas-Kanade算法的光流追踪

引言 在计算机视觉中&#xff0c;光流是指物体、场景或摄像机之间的相对运动造成的像素变化。光流估计是计算机视觉中的一个基础课题&#xff0c;广泛应用于许多领域&#xff0c;比如自动驾驶汽车、机器人导航、手势识别等。OpenCV是一个开源的计算机视觉库&#xff0c;提供了…

计算机网络:计算机网络概述 —— 描述计算机网络的参数

文章目录 数据量性能指标速率带宽数据传输速率 吞吐量时延分析时延问题 时延带宽积往返时间利用率丢包率丢包的情况 抖动可用性可靠性安全性 计算机网络是现代信息社会的基础设施&#xff0c;其性能和可靠性对各类应用至关重要。为了理解和优化计算机网络&#xff0c;我们需要深…

vue后台管理系统从0到1(1)

文章目录 vue后台管理系统从0到1&#xff08;1&#xff09;nvm 下载安装1.卸载nodejs环境2.安装nvm 安装nrm vue后台管理系统从0到1&#xff08;1&#xff09; 第一节主要是先安装我们的工具nvm nodejs版本管理工具&#xff0c;和nrm镜像管理工具 nvm 下载安装 nvm是一款管理…

重学SpringBoot3-集成Redis(一)之基础功能

更多SpringBoot3内容请关注我的专栏&#xff1a;《SpringBoot3》 期待您的点赞&#x1f44d;收藏⭐评论✍ 重学SpringBoot3-集成Redis&#xff08;一&#xff09;之基础功能 1. 项目初始化2. 配置 Redis3. 配置 Redis 序列化4. 操作 Redis 工具类5. 编写 REST 控制器6. 测试 AP…

论文翻译 | Language Models are Few-Shot Learners 语言模型是少样本学习者(上)

摘要 最近的工作表明&#xff0c;通过在大规模文本语料库上进行预训练&#xff0c;然后在特定任务上进行微调&#xff0c;许多自然语言处理&#xff08;NLP&#xff09;任务和基准测试都取得了显著的提升。尽管这种方法在架构上通常是任务无关的&#xff0c;但它仍然需要成千上…

JavaWeb三大组件之Servlet

1. Servlet 一、Servlet介绍 1、概念 Servlet&#xff08;Server Applet&#xff09;是Java Servlet的简称&#xff0c;称为小服务程序或服务连接器&#xff0c;用Java编写的服务器端程序&#xff0c;具有独立于平台和协议的特性&#xff0c;主要功能在于交互式地浏览和生成…

【Python】JSON操作中的高效小窍门

JSON&#xff08;JavaScript Object Notation&#xff09;作为一种轻量级的数据交换格式&#xff0c;在各种应用场景中扮演着重要角色。Python 中处理 JSON 数据非常便捷&#xff0c;主要通过内置的 json 模块来实现。 本文将详细介绍如何使用 Python 进行 JSON 数据的操作&am…

AI与物理学的交汇:Hinton与Hopfield获诺贝尔物理学奖

诺贝尔物理学奖颁给了AI&#xff01;机器学习先驱Hinton与Hopfield联手获奖&#xff0c;出乎所有人的意料。 今年的诺贝尔物理学奖颁给了机器学习领域的两位先驱&#xff0c;杰弗里辛顿&#xff08;Geoffrey Hinton&#xff09;和约翰霍普菲尔德&#xff08;John Hopfield&…

CSS计数器

CSS 中的计数器类似于变量&#xff0c;可以实现简单的计数功能&#xff0c;并将结果显示在页面上&#xff0c;在早期的网站上应用比较广泛。要实现计数器需要用到以下几个属性&#xff1a; counter-reset&#xff1a;创建或者重置计数器&#xff1b;counter-increment&#xf…

【软件部署安装】OpenOffice转换PDF字体乱码

现象与原因分析 执行fc-list查看系统字体 经分析发现&#xff0c;linux默认不带中文字体&#xff0c;因此打开我们本地的windows系统的TTF、TTC字体安装到centos机器上。 安装字体 将Windows的路径&#xff1a; C:\Windows\Fonts 的中文字体&#xff0c;如扩展名为 TTC 与TT…

力扣题31~40

题31&#xff08;中等&#xff09;&#xff1a; 分析&#xff1a; 其实这题题目比较难懂&#xff0c;题目还是挺简单的 我们可以从后面末尾开始&#xff0c;如果前一个大于后面的&#xff0c;说明后面不用动&#xff0c;如果小于&#xff0c;那就找仅仅大于它的数字放前面&…

Chromium 关闭 Google Chrome 后继续运行后台应用功能分析c++

此功能允许关闭 Google Chrome 后继续运行后台&#xff0c;控制此功能的开关是 // Set to true if background mode is enabled on this browser. //更改此值可以修改默认开启关闭 inline constexpr char kBackgroundModeEnabled[] "background_mode.enabled"; …

案例分享—国外优秀UI设计作品赏析

深色UI界面的优点众多&#xff0c;首先体现在视觉舒适度上。深色背景能减少屏幕高亮面积&#xff0c;降低眼部压力&#xff0c;尤其在夜间或光线不足的环境下&#xff0c;深色模式能显著缓解眼睛疲劳&#xff0c;提供更舒适的使用体验。 深色UI界面在设计上更具高端感和优雅氛围…

用Raspberry Pi Imager重装树莓派系统

今天删东西的时候&#xff0c;无意中把系统文件给remove了&#xff0c;结果树莓派无法正常启动&#xff0c;只能重新安装。 用DiskGenius工具将SD卡彻底清空&#xff0c;并将boot分区和文件分区合并为一&#xff0c;之后再对这个新分区进行了格式化。接下来就是烧录镜像了。以…