Go语言TCP即时通讯聊天室:300行代码实现公聊与私聊
本文提供了一个基于Go语言实现的轻量级即时通讯聊天室方案。该示例程序结构精简,通过服务器端管理在线用户状态,结合并发处理客户端连接,完整实现了包括加入/退出广播、全员公聊以及指定用户私聊在内的核心IM功能。此架构为理解Golang中的网络I/O模型和协程协作提供了直观的参考。
系统设计与运行逻辑
整个聊天室系统由四个核心模块组成:用于启动服务的 main.go、负责监听连接与消息路由的 server.go、封装单用户业务逻辑的 user.go 以及提供命令行交互界面的 client.go。
各模块职责明确:
- Server (服务器):负责建立TCP监听,维护在线用户列表
OnlineMap,并通过Channel实现消息的统一分发与广播。 - User (用户对象):封装了每个独立连接的读写操作,处理具体的协议指令(如改名、私聊转发),并利用互斥锁保证数据结构的安全。
- Client (客户端):解析命令行输入,区分公聊、私聊和改名指令,并异步处理服务器下发的响应信息。
环境准备与部署
无需复杂的依赖安装,只需具备Go编译环境即可直接运行。
- 初始化项目:确保GOPATH配置正确,新建文件夹存放源代码文件。
- 参数配置:
- 默认服务器IP为127.0.0.1,端口为8888。
- 可通过修改server.go或client.go中的常量来调整通信地址。 - 构建要求:由于代码使用了相对路径引用包内类型,建议将四个文件放置于同一
package main目录下,或使用Go Modules管理依赖。
服务端实现详解
服务器是整个系统的中枢。它使用 net.Listen 进行监听,并为每一个新建立的连接开启一个协程进行处理。
第一步:定义 Server 结构与初始化
目的:建立在线用户容器和全局消息管道。
入口:server.go
动作:
1. 创建包含 OnlineMap 的结构体,该Map以用户名为Key。
2. 初始化 Message chan string,作为所有在线用户的消息中转站。
第二步:启动服务与监听连接
目的:接受外部TCP请求。
入口:Start() 方法
动作:
1. 调用 net.Listen("tcp", ...) 阻塞监听端口。
2. 启动一个后台协程专门读取全局 Message Channel 中的消息,并进行群发。
3. 循环调用 listener.Accept() 获取新连接,并为每个连接启动 Handler 协程。
第三步:处理长连接与保活
目的:检测活跃用户,超时自动断连。
入口:Handler(conn net.Conn)
动作:
1. 创建User对象并上线。
2. 开启协程阻塞读取客户端数据,若读取到内容则投递至用户队列。
3. 使用 select 监听心跳信号与定时器(300秒),若无活动则判定为掉线。
package main
import (
"fmt"
"io"
"net"
"sync"
"time"
)
type Server struct {
Ip string
Port int
//在线用户列表
OnlineMap map[string]*User
mapLock sync.RWMutex
//消息广播的Channel
Message chan string
}
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 (s *Server) Handler(conn net.Conn) {
//业务逻辑
//fmt.Println("链接建立成功")
user := NewUser(conn, s)
user.Online()
//监听用户是否活跃
isLive := make(chan bool)
go func() {
buf := make([]byte, 4096)
for {
n, error := conn.Read(buf)
if n == 0 {
user.Offline()
return
}
if error != nil && error != io.EOF {
fmt.Println("read error")
}
msg := string(buf[:n-1])
user.DoMessage(msg)
//表示用户活跃
isLive <- true
}
}()
for {
select {
case <-isLive:
//当前用户活跃,不做任何时,激活select,重置定时器
case <-time.After(time.Second * 300):
//超时,将user强制关闭
user.SendMsg("你被踢了")
close(user.C)
conn.Close()
return
}
}
}
func (s *Server) ListenMessager() {
for {
msg := <-s.Message
s.mapLock.Lock()
for _, user := range s.OnlineMap {
user.C <- msg
}
s.mapLock.Unlock()
}
}
func (s *Server) BroadCast(user *User, msg string) {
sendMsg := "[" + user.Addr + "]" + user.Name + ":" + msg
s.Message <- sendMsg
}
func (s *Server) Start() {
listener, error := net.Listen("tcp", fmt.Sprintf("%s:%d", s.Ip, s.Port))
if error != nil {
fmt.Println("listener error...")
return
}
defer listener.Close()
go s.ListenMessager()
for {
conn, error := listener.Accept()
if error != nil {
fmt.Println("accept error...")
continue
}
go s.Handler(conn)
}
}
用户协议处理 (user.go)
user.go 核心在于解析特定前缀的命令字。例如 rename|... 触发更名逻辑,to|...|... 触发私有寻址。
package main
import (
"net"
"strings"
)
type User struct {
Name string
Addr string
C chan string
conn net.Conn
server *Server
}
func NewUser(conn net.Conn, server *Server) *User {
userAddr := conn.RemoteAddr().String()
user := &User{
Name: userAddr,
Addr: userAddr,
C: make(chan string),
conn: conn,
server: server,
}
go user.ListenMessage()
return user
}
//用户上线
func (u *User) Online() {
u.server.mapLock.Lock()
u.server.OnlineMap[u.Name] = u
u.server.mapLock.Unlock()
u.server.BroadCast(u, "上线")
}
//用户下线
func (u *User) Offline() {
u.server.mapLock.Lock()
delete(u.server.OnlineMap, u.Name)
u.server.mapLock.Unlock()
u.server.BroadCast(u, "下线")
}
//给当前user的客户端发送消息
func (u *User) SendMsg(msg string) {
u.conn.Write([]byte(msg))
}
//处理消息
func (u *User) DoMessage(msg string) {
if msg == "who" {
//查询当前在线用户
u.server.mapLock.Lock()
for _, user := range u.server.OnlineMap {
onlineMsg := "[" + user.Addr + "]" + user.Name + ":在线...\n"
u.SendMsg(onlineMsg)
}
u.server.mapLock.Unlock()
} else if len(msg) > 7 && msg[:7] == "rename|" {
//修改用户名 rename|xxx
newName := strings.Split(msg, "|")[1]
//判断名字是否已经存在
_, ok := u.server.OnlineMap[newName]
if ok {
u.SendMsg("用户名已存在\n")
} else {
u.server.mapLock.Lock()
delete(u.server.OnlineMap, u.Name)
u.server.OnlineMap[newName] = u
u.server.mapLock.Unlock()
u.Name = newName
u.SendMsg("用户名成功修改为:" + newName + "\n")
}
} else if len(msg) > 4 && msg[:3] == "to|" {
//私聊 to|zhangsan|你好
//获取对方用户名
remoteName := strings.Split(msg, "|")[1]
if remoteName == "" {
u.SendMsg("用户名格式不对\n")
return
}
//获取对方user
remoteUser, ok := u.server.OnlineMap[remoteName]
if !ok {
u.SendMsg("用户不存在\n")
return
}
//获取消息
msg := strings.Split(msg, "|")[2]
if msg == "" {
u.SendMsg("无消息内容,重新发送\n")
}
//发送消息
remoteUser.SendMsg(u.Name + "对您说:" + msg)
} else {
u.server.BroadCast(u, msg)
}
}
func (u *User) ListenMessage() {
for {
msg := <-u.C
u.conn.Write([]byte(msg + "\n"))
}
}
客户端交互流程 (client.go)
客户端在连接成功后进入主循环,支持公聊(广播)、私聊(带目标名)和重命名三种操作。使用 flag` 库支持命令行启动时传入IP和端口,默认连接本地回环地址。
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 {
client := &Client{
ServerIp: serverIp,
ServerPort: serverPort,
flag: 9999,
}
conn, error := net.Dial("tcp", fmt.Sprintf("%s:%d", serverIp, serverPort))
if error != nil {
fmt.Println("net dial error...")
return nil
}
client.conn = conn
return client
}
func (c *Client) menu() bool {
var flag int
fmt.Println("1.公聊模式")
fmt.Println("2.私聊模式")
fmt.Println("3.修改用户名")
fmt.Println("0.退出")
fmt.Scanln(&flag)
if flag >= 0 && flag <= 3 {
c.flag = flag
return true
} else {
fmt.Println(">>>>请输入合法数字<<<<")
return false
}
}
//修改用户名
func (c *Client) UpdateName() bool {
fmt.Println(">>>>请输入用户名")
fmt.Scanln(&c.Name)
sendMsg := "rename|" + c.Name + "\n"
_, error := c.conn.Write([]byte(sendMsg))
if error != nil {
fmt.Println("conn.write error...")
return false
}
return true
}
//公聊
func (c *Client) PublicChat() {
var chatMsg string
fmt.Println(">>>>请输入聊天内容,输入exit退出")
fmt.Scanln(&chatMsg)
for chatMsg != "exit" {
if len(chatMsg) != 0 {
msg := chatMsg + "\n"
_, error := c.conn.Write([]byte(msg))
if error != nil {
fmt.Println("conn.Write error....")
break
}
}
chatMsg = ""
fmt.Println(">>>>请输入聊天内容,输入exit退出")
fmt.Scanln(&chatMsg)
}
}
//私聊
func (c *Client) PrivateChat() {
var remoteUser string
var chatMsg string
c.SelectUsers()
fmt.Println(">>>>请输入聊天对象的用户名,输入exit退出")
fmt.Scanln(&remoteUser)
for remoteUser != "exit" {
fmt.Println(">>>>请输入聊天内容,输入exit退出")
fmt.Scanln(&chatMsg)
for chatMsg != "exit" {
if len(chatMsg) != 0 {
msg := "to|" + remoteUser + "|" + chatMsg + "\n\n"
_, error := c.conn.Write([]byte(msg))
if error != nil {
fmt.Println("conn.Write error....")
break
}
}
chatMsg = ""
fmt.Println(">>>>请输入聊天内容,输入exit退出")
fmt.Scanln(&chatMsg)
}
c.SelectUsers()
remoteUser = ""
fmt.Println(">>>>请输入聊天对象的用户名,输入exit退出")
fmt.Scanln(&remoteUser)
}
}
//查询在线用户
func (c *Client) SelectUsers() {
sendMsg := "who\n"
_, error := c.conn.Write([]byte(sendMsg))
if error != nil {
fmt.Println("conn.write error....")
return
}
}
//处理server返回的消息
func (c *Client) DealResponse() {
io.Copy(os.Stdout, c.conn)
}
func (c *Client) Run() {
for c.flag != 0 {
for c.menu() != true {
}
switch c.flag {
case 1:
//公聊
c.PublicChat()
case 2:
//私聊
c.PrivateChat()
case 3:
//修改用户名
c.UpdateName()
}
}
}
var serverIp string
var serverPort int
func init() {
flag.StringVar(&serverIp, "ip", "127.0.0.1", "设置服务器IP地址(默认为127.0.0.1)")
flag.IntVar(&serverPort, "port", 8888, "设置服务器端口(默认为8888)")
}
func main() {
flag.Parse()
client := NewClient(serverIp, serverPort)
if client == nil {
fmt.Println(">>>>链接服务器失败")
return
}
go client.DealResponse()
fmt.Println(">>>>链接服务器成功")
client.Run()
}
服务启动与运行验证
main.go 极其简洁,实例化 Server 对象后立即启动。在实际部署时,需先启动服务端,再开启多个终端实例运行客户端进行测试。
package main
func main() {
server := NewServer("127.0.0.1", 8888)
server.Start()
}
简要总结
以上就是Go语言利用300行代码实现即时通讯聊天室的详细内容,更多关于Go语言并发编程的基础知识请继续关注本站其它相关文章!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









