基于socket的多人聊天窗口 | 青训营

36 阅读3分钟

技术基础

想要实现这个聊天窗口需要掌握以下技术:

  • 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"))
   }
 }
 ​
 ​

测试

用户一

image-20221207103317397

用户二

image-20221207103251004

用户三

image-20221207103427557

控制台消息

后续再对聊天室进一步优化