技术基础
想要实现这个聊天窗口需要掌握以下技术:
-
Socket编程: Socket是用于在计算机网络之间进行通信的一种机制。在Go语言中,你可以使用net包来处理Socket编程。通过net.Listen创建服务器Socket,使用net.Dial创建客户端Socket,然后使用Socket连接来进行数据传输。
-
并发和goroutines: Go语言天生支持轻量级线程,称为goroutines。在聊天窗口中,你需要使用goroutines来处理多个客户端的同时连接。每个客户端都在自己的goroutine中处理连接,这样可以避免阻塞并提高并发性能。
-
TCP或UDP协议: 你可以选择使用TCP或UDP协议来建立Socket连接。TCP提供可靠的连接和顺序传输,适用于聊天窗口的通信,而UDP则更适合实时性要求较高的场景。
-
消息协议和数据格式: 在聊天窗口中,你需要定义一种消息协议和数据格式,以便客户端和服务器能够理解彼此发送的消息。通常可以使用JSON或自定义的二进制格式来表示消息。
-
事件处理和消息分发: 为了构建一个实时的聊天窗口,你需要设计事件处理和消息分发机制。当一个客户端发送消息时,服务器需要将消息分发给其他所有连接的客户端。
-
错误处理和异常情况: 在网络编程中,各种异常情况可能会发生,如连接中断、数据错误等。你需要编写健壮的错误处理代码来确保应用的稳定性。
代码实现
实现很简单,基于tcp socket,做这个小项目的目的是检测对channel的理解,channel在并发场景中真的好用,但是有时候也是真挺难理解的
准备
User
每个连接的用户需要对应一个账号,因此需要创建一个全局的struct,用户有名字,id,以及接受消息的管道
type User struct {
// 名字
name string
// id
id string
// msg管道
msg chan string
}
map&message
还需要一个全局的map保存所有的用户信息,以便服务器端向所有的用户转发消息
// 需要一个全局的map存储所有user信息
var allUsers = make(map[string] User)
转发消息时又需要一个管道接收用户发送的消息后转发给所有用户
// 需要一个全局的管道message 向所有用户发送消息
var message = make(chan string, 10)
功能实现
需要一个广播函数,开启该goroutine后可以一直监听message管道中的消息,然后向用户转发消息
需要一个业务处理函数,当用户发送消息后,通过该函数处理
需要一个消息反馈函数,将User.msg中的消息返回到客户端
broadcast
func broadcast(){
fmt.Println("[+]:广播go协程启动成功...")
for{
// 从message中读取数据
info := <- message
// 将消息发送给所有用户
if info != ""{
for _, user := range allUsers{
user.msg <- info
}
}
}
}
handler
func handler(conn net.Conn) {
for true {
fmt.Println("[+]:启动业务...")
// 每次建立新连接需要创建一个user
clientAddr := conn.RemoteAddr().String()
newUser := User{
name : clientAddr,
id : clientAddr,
msg: make(chan string), // 一定要使用make,否则没有空间写人
}
fmt.Println(clientAddr)
_, ok := allUsers[clientAddr]
if !ok{
//将新创建的用户添加到map中
allUsers[newUser.id] = newUser
// 向广播中写入消息 通知其他人你已经上线
loginInfo := fmt.Sprintf("[%s]:[%s] ====> online now!", newUser.name, newUser.id)
message <- loginInfo
time.Sleep(time.Second)
go writeToClient(newUser, conn)
}
buf:= make([]byte, 1024)
cnt, err := conn.Read(buf)
if err!=nil{
fmt.Println("conn.Read error: " , err)
}
//fmt.Println("服务器端接受的数据为:", string(buf[:cnt]), ", cnt:", cnt)
userInfo:= fmt.Sprintf("[%s] say:", clientAddr)
message <- userInfo + string(buf[:cnt])
}
}
writeToClient
func writeToClient(user User, conn net.Conn){
for msg := range user.msg{
conn.Write([]byte(msg + "\n"))
}
}
完整程序
package main
import (
"fmt"
"net"
"time"
)
type User struct {
// 名字
name string
// id
id string
// msg管道
msg chan string
}
// 需要一个全局的map存储所有user信息
var allUsers = make(map[string] User)
// 需要一个全局的管道message 向所有用户发送消息
var message = make(chan string, 10)
func main() {
listen, err := net.Listen("tcp", ":8080")
if err != nil {
fmt.Println("net.listen err: ", err)
}
fmt.Println("[+]:服务器监听成功")
// 启动全局唯一的 广播协程
go broadcast()
for{
accept, err := listen.Accept()
if err != nil {
fmt.Println("[+]:listen.accept err: ", err)
}
fmt.Println("[+]:建立连接成功...")
go handler(accept)
}
}
func handler(conn net.Conn) {
for true {
fmt.Println("[+]:启动业务...")
// 每次建立新连接需要创建一个user
clientAddr := conn.RemoteAddr().String()
newUser := User{
name : clientAddr,
id : clientAddr,
msg: make(chan string), // 一定要使用make,否则没有空间写人
}
fmt.Println(clientAddr)
_, ok := allUsers[clientAddr]
if !ok{
//将新创建的用户添加到map中
allUsers[newUser.id] = newUser
// 向广播中写入消息 通知其他人你已经上线
loginInfo := fmt.Sprintf("[%s]:[%s] ====> online now!", newUser.name, newUser.id)
message <- loginInfo
time.Sleep(time.Second)
go writeToClient(newUser, conn)
}
buf:= make([]byte, 1024)
cnt, err := conn.Read(buf)
if err!=nil{
fmt.Println("conn.Read error: " , err)
}
//fmt.Println("服务器端接受的数据为:", string(buf[:cnt]), ", cnt:", cnt)
userInfo:= fmt.Sprintf("[%s] say:", clientAddr)
message <- userInfo + string(buf[:cnt])
}
}
// 向所有用户广播消息, 全局唯一
func broadcast(){
fmt.Println("[+]:广播go协程启动成功...")
for{
// 从message中读取数据
info := <- message
// 将消息发送给所有用户
if info != ""{
for _, user := range allUsers{
user.msg <- info
}
}
}
}
func writeToClient(user User, conn net.Conn){
for msg := range user.msg{
conn.Write([]byte(msg + "\n"))
}
}
测试
用户一
用户二
用户三
控制台消息
后续再对聊天室进一步优化