Go语言凭借原生支持的goroutine和channel特性,非常适合开发高并发的网络通信类应用,实现简单聊天室就是验证这些特性的经典实践场景。整个聊天室项目主要分为服务端和客户端两部分,服务端负责监听连接、管理在线用户、转发消息,客户端负责连接服务端并收发消息。

项目核心设计思路
聊天室的核心需求是支持多个客户端同时连接,每个客户端发送的消息要同步给所有其他在线客户端,这就需要解决并发连接管理和消息广播两个问题。我们可以设计三个核心组件:
- 服务端主协程:负责监听TCP端口,接收新的客户端连接,为每个连接启动独立的协程处理
- 用户管理模块:维护在线用户列表,支持用户加入、退出、获取所有在线用户等操作
- 消息广播模块:接收单个客户端发送的消息,将其转发给所有其他在线客户端
goroutine用来处理每个客户端的独立逻辑,channel用来在不同协程之间传递消息和用户状态变更信号,避免共享变量带来的并发安全问题。
服务端实现代码
首先实现服务端的核心逻辑,包含连接监听、用户管理、消息处理三个部分:
package main
import (
"fmt"
"net"
"sync"
)
// 定义客户端结构体,存储连接信息和用户名
type Client struct {
conn net.Conn
name string
}
// 聊天室结构体,管理所有在线客户端
type ChatRoom struct {
clients map[net.Conn]Client
mutex sync.RWMutex
// 消息广播通道
broadcast chan string
// 用户加入通道
join chan Client
// 用户退出通道
leave chan net.Conn
}
// 初始化聊天室
func NewChatRoom() *ChatRoom {
return &ChatRoom{
clients: make(map[net.Conn]Client),
broadcast: make(chan string),
join: make(chan Client),
leave: make(chan net.Conn),
}
}
// 启动聊天室核心协程,处理用户加入、退出、消息广播
func (cr *ChatRoom) Start() {
go func() {
for {
select {
// 处理新用户加入
case client := <-cr.join:
cr.mutex.Lock()
cr.clients[client.conn] = client
cr.mutex.Unlock()
// 广播用户加入通知
cr.broadcast <- fmt.Sprintf("用户 %s 加入聊天室", client.name)
// 处理用户退出
case conn := <-cr.leave:
cr.mutex.Lock()
if client, ok := cr.clients[conn]; ok {
delete(cr.clients, conn)
// 广播用户退出通知
cr.broadcast <- fmt.Sprintf("用户 %s 离开聊天室", client.name)
}
cr.mutex.Unlock()
conn.Close()
// 处理消息广播
case msg := <-cr.broadcast:
cr.mutex.RLock()
for conn := range cr.clients {
_, err := fmt.Fprintf(conn, "%sn", msg)
if err != nil {
// 发送失败说明连接已断开,触发退出逻辑
go func(c net.Conn) {
cr.leave <- c
}(conn)
}
}
cr.mutex.RUnlock()
}
}
}()
}
// 处理单个客户端连接
func (cr *ChatRoom) HandleClient(conn net.Conn) {
defer func() {
cr.leave <- conn
}()
// 读取客户端输入的用户名
buffer := make([]byte, 1024)
n, err := conn.Read(buffer)
if err != nil {
return
}
name := string(buffer[:n-1]) // 去掉换行符
client := Client{conn: conn, name: name}
// 将用户加入聊天室
cr.join <- client
// 循环读取客户端发送的消息
for {
n, err := conn.Read(buffer)
if err != nil {
return
}
msg := string(buffer[:n-1])
// 将消息广播给所有用户
cr.broadcast <- fmt.Sprintf("%s: %s", name, msg)
}
}
func main() {
// 监听8080端口
listener, err := net.Listen("tcp", ":8080")
if err != nil {
fmt.Println("监听端口失败:", err)
return
}
defer listener.Close()
fmt.Println("聊天室服务端启动,监听端口8080")
chatRoom := NewChatRoom()
chatRoom.Start()
// 循环接收新的客户端连接
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("接收连接失败:", err)
continue
}
// 为每个连接启动独立协程处理
go chatRoom.HandleClient(conn)
}
}
客户端实现代码
客户端需要同时处理两个任务:读取用户输入并发送给服务端,接收服务端的消息并打印。因此也需要启动两个goroutine分别处理这两个逻辑:
package main
import (
"fmt"
"net"
"os"
)
func main() {
// 连接服务端
conn, err := net.Dial("tcp", "127.0.0.1:8080")
if err != nil {
fmt.Println("连接服务端失败:", err)
return
}
defer conn.Close()
// 输入用户名
fmt.Print("请输入用户名: ")
var name string
fmt.Scanln(&name)
// 发送用户名给服务端
_, err = fmt.Fprintf(conn, "%sn", name)
if err != nil {
fmt.Println("发送用户名失败:", err)
return
}
// 启动协程接收服务端消息
go func() {
buffer := make([]byte, 1024)
for {
n, err := conn.Read(buffer)
if err != nil {
fmt.Println("连接已断开")
os.Exit(0)
}
fmt.Println(string(buffer[:n]))
}
}()
// 主协程读取用户输入并发送
buffer := make([]byte, 1024)
for {
n, err := os.Stdin.Read(buffer)
if err != nil {
return
}
_, err = fmt.Fprintf(conn, "%s", string(buffer[:n]))
if err != nil {
fmt.Println("发送消息失败:", err)
return
}
}
}
项目运行与测试
运行项目时,先启动服务端程序,然后启动多个客户端程序,分别输入不同的用户名完成连接。任意一个客户端输入消息后,其他所有客户端都能收到对应的消息,同时用户加入和退出时所有在线用户也会收到通知。
这个示例用到的核心并发知识点包括:
- goroutine的使用:每个客户端连接、消息接收、广播处理都运行在独立的协程中,实现并发处理
- channel的通信:通过join、leave、broadcast三个通道在不同协程之间传递数据,避免直接操作共享变量
- 读写锁的使用:用户列表是共享资源,读多写少的场景下使用RWMutex提升性能
如果需要扩展功能,还可以添加私聊、历史消息存储、用户列表查询等特性,进一步巩固Go并发编程的相关知识。