初学Go,初步了解了管道、IO等用法之后,尝试实现了一个简单的聊天功能,在此记录一下思路(主要是参考的是Go语言圣经)
总体框架
- 客户端之间负责接收消息和发送消息,服务端只是一个中转消息的中转站。
- 因为实现较为简单,因此当服务端收到消息后,采用广播的方式向所有客户端进行广播
客户端实现
客户端需要实现的主要是一个向服务器发送消息并且接收消息的功能,因此实现较为简单:
- Dial连接服务器
- 创建一个协程对conn进行监听,将消息打印到标准输出
- 主协程则负责消息的输入
实现较为简单,因此略,主要讲服务端的实现
func main() {
conn, err := net.Dial("tcp", "localhost:8888")
if err != nil {
log.Fatal(err)
return
}
// 接收conn的消息
go func() {
mustCopy(os.Stdout, conn)
}()
defer conn.Close()
mustCopy(conn, os.Stdin)
}
func mustCopy(dst io.Writer, src io.Reader) {
if _, err := io.Copy(dst, src); err != nil {
log.Fatal(err)
}
}
服务端实现
首先要思考清楚服务端的设计:
- 监听线程,也就是主线程,该线程主要负责监听服务器的端口并且把客户端的连接交给工作线程
- 工作线程(handleConn)主要负责处理客户端的登录、退出(实际上在本场景中就是链接的断开以及将登录退出的消息广播给其他客户端),以及客户端消息的接收、发送。
当然,客户端消息的接收和发送也很难在一个线程中完成,因此工作线程中还需要再创建一个线程专门负责将客户端收到的广播信息发送给对应的客户端 - 中转站(broadcaster),当某个工作线程收到来自客户端的消息之后,需要将消息发给中转站,由中转站进行广播;此外还需要负责保存当前所有连接的客户端
数据结构
因此数据结构的设计如下:
// client被定义为一个只能发送的管道,主要目的就是广播到各个工作线程
type client chan<- string
// leaving和entering处理客户端的注册、注销
// message负责接受客户端消息并且广播
// 都是全局变量
var (
message = make(chan string)
leaving = make(chan client)
entering = make(chan client)
)
此外中转站用map结构来保存客户端的连接
clients := make(map[client]bool)
监听线程
func main() {
listener, err := net.Listen("tcp", "localhost:8888")
if err != nil {
log.Fatal(err)
return
}
go broadcaster()
for {
conn, err := listener.Accept()
if err != nil {
log.Fatal(err)
continue
}
go handlerConn(conn)
}
}
中转站
主要使用select来实现注册、注销、广播功能,思路简单
func broadcaster() {
clients := make(map[client]bool)
for {
select {
case msg := <-message:
for cli := range clients {
cli <- msg
}
case cli := <-leaving:
delete(clients, cli)
case cli := <-entering:
clients[cli] = true
}
}
}
工作线程
// 客户端处理
func handlerConn(conn net.Conn) {
ch := make(chan string)
// 将广播的消息写回客户端
go clientWriter(conn, ch)
name := conn.RemoteAddr().String()
// 登录
ch <- "You are " + name + "!"
message <- name + " has arrived!"
entering <- ch
// 从客户端接收消息,发送到中转站
input := bufio.NewScanner(conn)
for input.Scan() {
msg := input.Text()
message <- name + ": " + msg
}
// 注销
message <- name + " has exited!"
leaving <- ch
conn.Close()
}
func clientWriter(conn net.Conn, ch <-chan string) {
for msg := range ch {
fmt.Fprintln(conn, msg)
}
}