GO语言网络编程demo-聊天室
代码展示:
package main import ( "fmt" "net" "strings" "time" ) type Client struct { C chan string Name string Addr string } var onlinemap map[string]Client var meassage = make(chan string) func main() { listenr, err := net.Listen("tcp", "127.0.0.1:8080") if err != nil { fmt.Println("err:=", err) return } defer listenr.Close() go Manager() for true { conn, err := listenr.Accept() if err != nil { fmt.Println("err:=", err) return } defer conn.Close() //处理用户连接 go Handleconn(conn) } } // 处理用户连接 func Handleconn(conn net.Conn) { defer conn.Close() cliaddr := conn.RemoteAddr().String() cli := Client{make(chan string), cliaddr, cliaddr} onlinemap[cliaddr] = cli go WriteMagToClient(cli, conn) meassage <- MakeMsg(cli, "login") isQuit := make(chan bool) //对方是否主动退出 hasDate := make(chan bool) //对方是否有数据发送 //新建一个协程,接收用户发来的数据 go func() { buf := make([]byte, 2048) for { n, err := conn.Read(buf) if n == 0 { isQuit <- true fmt.Println("conn.read err=", err) return } msg := string(buf[:n-1]) if len(msg) == 3 && msg == "who" { conn.Write([]byte("user list:\n")) for _, tmp := range onlinemap { msg = tmp.Addr + ":" + tmp.Name + "\n" conn.Write([]byte(msg)) } } else if len(msg) >= 8 && msg[:6] == "rename" { name := strings.Split(msg, "|")[1] cli.Name = name onlinemap[cliaddr] = cli conn.Write([]byte("rename ok \n")) } else { meassage <- MakeMsg(cli, msg) } hasDate <- true } }() for { select { case <-isQuit: delete(onlinemap, cliaddr) meassage <- MakeMsg(cli, "login out") return case <-hasDate: case <-time.After(60 * time.Second): delete(onlinemap, cliaddr) meassage <- MakeMsg(cli, "time out") return } } } func MakeMsg(cli Client, login string) (buf string) { buf = "[" + cli.Addr + "]" + cli.Name + " " + login return buf } func WriteMagToClient(cli Client, conn net.Conn) { for msg := range cli.C { conn.Write([]byte(msg + "\n")) } } func Manager() { onlinemap = make(map[string]Client) for { msg := <-meassage for _, cli := range onlinemap { cli.C <- msg } } }
完成本demo需要的技术栈:
go语言基础(到多线程,通道即可)
网络编程
思路讲解部分:
本聊天室功能旨在实现多用户的聊天功能,需要下列功能:
1.一个用户发送消息所有用户都能接收
2.更改名字
3.输入:who,展示所有在线用户
4.长时间无操作自动退出
首先需要创建一个监听,使用TCP协议,使用本地地址,然后创建conn对象,用来向用户输出和读取信息。
对于功能实现:
1.一个用户发送消息所有用户都能接收
通过通道功能,不进入信息则阻塞,进入信息则向全体广播信息。
2.更改名字
对用户输入进行判断,符合设定的更改命名格式则允许更改结构体中的name,修改成功则给出提示。
3.输入:who,展示所有在线用户
遍历一遍结构体中的name,并输入给用户。
4.长时间无操作自动退出
使用time 包的函数,当时间到的时候若用户仍然无操作,则删除该用户信息,并返回
代码讲解部分:
我将仔细讲解每段代码的作用,作为网络编程和GO的复习
“不要使用共享内存来通信,而是要通过通信来共享内存”
本demo使用的所有包
第一个Client(用户端)结构体,他有三个参数,作用如上。
对于onlineMap,他是一个集合类型,索引就是用户的ID,索引对应的值就是Client类型
对于message,他是一个管道,用于通信
这些全局变量在后面的代码中用到的时候会进行详细说明
现在展示main()函数的所有内容:
现在逐层进行讲解
这段代码创建了一个监听器listener,通过调用net包中的Listen()函数,该函数有两个参数,
network,指的是网络协议类型,这里使用TCP协议进行通信。
address,指的是监听地址,这里的地址杀死本地ip地址的8080端口。
错误处理就不说了。
这里的Close()是关闭监听器,defer语句是在函数返回前被调用。
Managger()开启了一个协程,后续会展示代码
这段代码调用了一个listener.Accept()方法,这个方法会等待并接受一个传入的连接请求,如果成功接受到连接,就会
返回一个conn连接对象,这个对象的作用如下:
该对象可以与客户端实现双向的数据交互,可以使用Read()方法读取客户端发送的数据,并使用Write()方法向客户端发送响应数据。
conn的类型是 net.Conn类型。
总结:用于客户端与服务端进行交互等操作。
HanleConn开启了 一个协程,传入conn,该函数用于处理用户链接。
现讲解Manager()的代码:
首先你创建了一个之前说过的map,然后创建了一个msg,用来接收meassage通道的信息
使用cli来遍历map中的内容,并将msg传给cli.C
这两个使用通道的代码都会阻塞,所以这段代码没有接受信息之前是阻塞的,当有信息进入meassage通道的时候,才能将信息传到
map中的通道中。
现在这个代码是单功能HandleConn()函数,看懂这个之后我们会重新看完全版的HandleConn()
对于cliAddr := conn.RemoteAddr().String(),
conn.RemoteAddr()
返回一个 net.Addr
类型的对象,net.Addr
类型是一个接口类型,可以表示各种类型的网络地址。
他的String()方法,可以返回地址的字符串表示形式。
Network()方法,可以返回地址的网络类型,例如 “tcp”,“IP”
所以cliAddr是个字符串类型的网址地址。
然后创建一个cli结构体,把结构体放到map中。
之后创建一个协程,发送信息给客户端。
这个是把信息写给客户端,使用msg遍历cli.c通道
使用conn.Write()接收信息,并写入conn
现在展示完全版的HandleConn:
上面四行已经说过了,从第六行开始:
第六行的函数稍后讲解
第七八行创建了两个bool 类型的通道,用于之后的代码。
MakeMsg(),两个参数,第一个参数为Client结构体,,第二个参数为字符串
这个函数的作用是返回一个字符串,符合我所写的结构,即:
[地址]姓名login
login为变量,主要看输入的是什么。
启动了一个协程,用一个匿名函数来包装内部代码
函数中首先创建了一个缓冲区 buf
,用于接收读取的数据。
在循环中,通过 conn.Read(buf)
从连接 conn
中读取数据,并返回读取的字节数 n
和可能的错误 err
。如果读取的字节数为零,表示连接已关闭,此时触发 isQuit
通道的发送操作,并输出错误信息。然后函数返回。
如果读取到有效的数据,将其转换为字符串形式,存储在变量 msg
中。
接下来进行条件判断。如果 msg
的长度为 3 且等于 "who",则表示客户端请求用户列表。服务器将向客户端连接 conn
发送当前在线用户的列表。
如果 msg
的长度大于等于 8 且前六个字符为 "rename",则表示客户端请求修改自身的名称。服务器将从 msg
中提取新名称,并更新 cli.Name
以及 onlinemap
中对应客户端的信息。随后向客户端连接 conn
发送 "rename ok" 的响应。
如果以上条件均不满足,表示客户端发送的是聊天消息。使用 MakeMsg
函数生成包含客户端和消息的完整消息,并将其通过 meassage
通道发送出去。
这里就实现了修改姓名的功能与发送在线用户的功能。
在这段代码中,在处理客户端连接的循环中,使用了 select
语句来同时等待多个通道的操作。
select
语句用于监视通道的状态,当有多个通道都准备好进行操作时,select
会随机选择其中一个通道执行对应的操作。
在这个循环中,select
语句包含了三个 case
分支:
-
<-isQuit
:等待从isQuit
通道接收数据。如果接收到数据,表示客户端主动退出。在这种情况下,会从onlinemap
中删除对应的客户端信息,然后通过meassage
通道向其他客户端发送退出消息,并返回函数。这里是用户的主动退出。
-
<-hasDate
:等待从hasDate
通道接收数据。如果接收到数据,表示有新的数据可处理。这里的作用是当消息处理完后,继续等待下一个消息的到来。 -
<-time.After(60 * time.Second)
:等待一个 60 秒的定时器。如果 60 秒内没有其他操作发生,表示客户端超时断开连接。在这种情况下,会从onlinemap
中删除对应的客户端信息,然后通过meassage
通道向其他客户端发送超时退出消息,并返回函数。
这里实现了超时自动退出的功能。
通过使用 select
语句和不同的通道操作,可以实现对不同事件的处理,包括客户端主动退出、接收到新的数据、以及客户端超时等情况的处理。
代码讲解完成。