tcp聊天室的实现

模型

第一个模型即用户模型,我们简单的以用户的名字作为用户的主键,并为其创建两个channel:

1.receive channel:即获取消息的channel
2.done channel:发送/获取断开连接的channel

// user is a single connection to the tcp chat server
type user struct {
	name    string
	receive chan message
	done    chan interface{}
}

message 是聊天室消息模型:

// message is the model for every message flows through the chatroom
type message struct {
	typ     msgType 	// 利用msgType来区分其应该是系统消息还是用户消息
	from    string
	content string
	when    time.Time
}

const (
	msgTypeUser msgType = iota
	msgTypeSystem
)

接下来建立聊天室模型,可想而知聊天室由多个用户组成,所以我们需要一个存储用户指针的slice,同时利用聊天室名去作为主键区分不同的聊天室,为了聊天室强化聊天室的功能在添加一个历史消息组件,存储一定数量的历史消息,在新的用户进入聊天室后将历史消息一并发送给用户:

// chatroom is the collection of users which they can receive every message from each other
type chatroom struct {
	name  string
	users []*user
	mu    sync.Mutex	// 在对用户slice进行操作进行加锁时使用
	his   *history
}

最后时server模型的创建,一个tcp聊天室中应包含多个群聊聊天室和多个群聊用户,我们利用map结构的方式去存储这些数据,同时聊天室需要有一个网络地址:

// chatServer is the listening and dispatching server for tcp chatroom,
// it stores information about all the rooms and all the users ever created
type chatServer struct {
	addr   string
	rooms  map[string]*chatroom
	users  map[string]*user
	muRoom sync.Mutex
	muUser sync.Mutex
}

交互过程

在开始讲解交互过程之前,我们首先需要了解net包中的一个interface net.Conn:

type Conn interface {
	
	Read(b []byte) (n int, err error)

	Write(b []byte) (n int, err error)

	Close() error

	LocalAddr() Addr

	RemoteAddr() Addr

	SetDeadline(t time.Time) error

	SetReadDeadline(t time.Time) error

	SetWriteDeadline(t time.Time) error
}

net.Conn实现了io.Reader和io.Writer接口,也就是说我们可以从Conn中读取字节,也可以往Conn中写入字节,更好的是我们可以利用bufio包中的工具去对他进行操作。

user goroutine

互过程的第一步,我们要在user结构体下编写编写一个listen的函数,其主要作用就是读取receive channel中的内容并编写到net.Conn中,在这我利用到了go经典的select case语句模型,也就是说当我从done channel中读取到任何一种内容时我就要停止读取:

// listen starts a loop to receive from the receive channel and writes to the net.Conn
func (u *user) listen(conn net.Conn) {
	for {
		select {
		case msg := <-u.receive:
			_, err = conn.Write(msg.bytes()) 
			// msg.bytes()是我在message结构体下定义的将message打包成字符串
			// 并转化成字节数组返回的函数
                        if err != nil{
                           log.Fatalln(err)
                        }
		case <-u.done:
			break
		}
	}
}

chatroom 广播

我们在定义user结构体下的listen函数时从receive channel中读取了message,那么我们需要一个goroutine往该channel中写入内容。chatroom下的broadcast(广播)函数,接受一个message并将其广播发送给聊天室中的每一个用户:

// broadcast sends the message to every user in the chatroom except the sender
func (room *chatroom) broadcast(msg message) {
	for _, u := range room.users {
		if u.name == msg.from {
			continue
		}
		// 这里启动的goroutine是为了定义写入的超时时间(因为写入可能会block)
		// 如果不需要也可以抛弃这里的goroutine,直接进行写入
		go func(u *user) {
			select {
			case u.receive <- msg:
				break
			case <-time.After(3 * time.Second):
				break
			}
		}(u)
	}
}

该函数将在下一部分chatroom goroutine内容中定义的函数中利用

chatroom goroutine

每一个聊天室对每一个用户连接都需要保持一个tcp连接,即tcp连接的数量 = 聊天室1 * 聊天室1用户数量 + 聊天室2 * 聊天室2用户数量 + ···
每一个tcp连接利用下面定义的chatroom结构体下的newUser函数来维持:

// newUser adds the user to the chatroom and starts a loop for reading from the net.Conn
func (room *chatroom) newUser(user *user, conn net.Conn) {
	room.mu.Lock()
	room.users = append(room.users, user)
	room.mu.Unlock()
	room.broadcast(newSystemMsg(contentHello(user.name)))
	room.writeHistory(conn)
	for {
		reader := bufio.NewReader(conn)
		bytes, err := reader.ReadBytes('\n')
		if err != nil {
			continue
		}
		content := strings.Trim(string(bytes), "\n")
		log.Printf("%s -> %s: %s\n", user.name, room.name, content)
		// if content equals to "exit" then close the connection
		if content == contentExit {
			user.done <- struct{}{}
			room.broadcast(newSystemMsg(contentGoodbye(user.name)))
			_ = conn.Close()
			break
		}
		msg := newUserMsg(user.name, content)
		room.his.push(msg)
		room.broadcast(msg)
	}
}

1.加锁添加新用户
2.广播新用户加入消息
3.为该用户写入历史消息
4.从net.Conn中读取一行内容(该聊天室只能一行一行地发送消息)
5.若内容等于exit,在该用户的done channel中发送消息,并广播退出聊天室的消息并关闭连接,结束,否则跳到6
6.处理内容并打包成message结构
7.将消息推入历史消息
8.在聊天室中广播该消息
9.回到4

server的建立

有c++、java或python tcp编程的经验的同学都知道,tcp连接是由socket实现的,socket分为两种(监听socket和通信socket),每个socket又是四元组(两组ip和端口号)。golang中并没有socket的概念,golang中的两种socket就是net包中的net.Listener和net.Conn接口,net.Listner对应监听socket,net.Conn对应通信socket。net.Listener的建立我们只需要利用net包中的net.Listen函数并传入协议名称(这里是tcp)和服务器地址即可。在利用net.Listenet的Listen方法连接新的用户创建通信socket(net.Conn),而这里的通信socket正式贯通了整个tcp聊天室的tcp连接体。

// Spin starts the chatServer at given address
func (server *chatServer) Spin() {
	listener, err := net.Listen("tcp", server.addr)
	if err != nil {
		log.Fatalf("failed to start the server at %s, err: %s\n", server.addr, err.Error())
	}
	log.Printf("server started at address %s...\n", server.addr)
	for {
		// 开启循环接受新的连接
		conn, err := listener.Accept()
		log.Printf("server accepted a new connection from %s\n", conn.RemoteAddr())
		if err != nil {
			continue
		}
		// 将获取的conn对象传给spin方法(注意不是Spin方法),开启新的goroutine
		go server.spin(conn)
	}
}

连接建立以后,我们需要提前定一个简单的协议让用户决定自己的用户名和想要加入的聊天室名称。这里我们就定一行字符串username;chatroomName利用分号分割用户名和聊天室名称。在服务器接收到连接以后的第一步就是要读取用户发来的协议请求,解析创建并将用户分配到对应的聊天室中,其次在启动新的chatroom goroutine和user goroutine即可:

// spin do the protocol procedure and starts the connection goroutines
func (server *chatServer) spin(conn net.Conn) {
	reader := bufio.NewReader(conn)
	bytes, err := reader.ReadBytes('\n')
	if err != nil {
		log.Printf("connection failed with client %s with err: %s\n",
			conn.RemoteAddr(), err.Error())
		return
	}
	username, roomname, err := parseProtocol(bytes)
	if err != nil {
		_, _ = conn.Write(comm.BytesProtocolErr)
		return
	}
	if _, ok := server.users[username]; ok {
		_, _ = conn.Write(comm.BytesUsernameExists)
		_ = conn.Close()
		log.Printf("connection from %s closed by server\n", conn.RemoteAddr())
		return
	}
	log.Printf("connecting user %s to chatroom %s...\n", username, roomname)
	u := server.newUser(username)
	room, ok := server.rooms[roomname]

	if !ok {
		room = server.newRoom(roomname)
	}
	go room.newUser(u, conn)
	go u.listen(conn)
	log.Printf("user %s is connected to chatroom %s\n", username, roomname)
}

以上函数的内容和步骤可以总结为以下:

1.从net.Conn中读取一行内容(即协议内容)
2.解析协议,若协议错误,发送错误内容并返回
3.在server的usersmap中寻找用户名,若用户名已存在,发送用户存在错误,并返回
4.创建新的用户
5.若聊天室不存在,创建新的聊天室
6.启动chatroom goroutine
7.启动user goroutine
到此服务器内容全部结束,现在我们就开始测试我们的服务器,首先建立main函数:

func main() {
	// 利用flag获取命令行输入参数
	host := flag.String("h", "127.0.0.1", "the host name of the server")
	port := flag.Int("p", 8888, "the port number of the server")
	flag.Parse()
	chatServer := server.New(*host, *port)
	chatServer.Spin()
}

原文连接:https://blog.csdn.net/ElzatAhmed/article/details/125589615
不知道咋启动
换一个博客
聊天室的组成
聊天室分为两个部分,分别是:

服务端
客户端
然后,一般情况下我们互相聊天使用的都只是客户端而已,服务端只是起到调度的作用。

信息发送与接收的流程
假设我们有 服务端(S) 客户端(C1) 客户端(C2) 客户端(C3)并且 S 已经 与 C1 C2 C3 建立了连接。

理论上的流程是这样的:

C1 向 S 发出信息
S 接收到信息
S 将接收到的信息广播给 C2 C3
C2 C3 接收信息
服务端代码

package main
 
import (
	"fmt"
	"net"
	"time"
)
 
// 客户端 map
var clientMap = make(map[string]*net.TCPConn) // 存储当前群聊中所有用户连接信息:key: ip+port, val: 用户连接信息
 
// 监听请求
func listenClient(ipAndPort string) {
	tcpAddr, _ := net.ResolveTCPAddr("tcp", ipAndPort)
	tcpListener, _ := net.ListenTCP("tcp", tcpAddr)
	for { // 循环接收
		clientConn, _ := tcpListener.AcceptTCP()                 // 监听请求连接
		clientMap[clientConn.RemoteAddr().String()] = clientConn // 将连接添加到 map
		go addReceiver(clientConn)
		fmt.Println("用户 : ", clientConn.RemoteAddr().String(), " 已连接.")
	}
}
 
// 向连接添加接收器
func addReceiver(newConnect *net.TCPConn) {
	for {
		byteMsg := make([]byte, 2048)
		len, err := newConnect.Read(byteMsg) // 从newConnect中读取信息到缓存中
		if err != nil {
			newConnect.Close()
		}
		fmt.Println(string(byteMsg[:len]))
		msgBroadcast(byteMsg[:len], newConnect.RemoteAddr().String())
	}
}
 
// 广播给所有 client
func msgBroadcast(byteMsg []byte, key string) {
	for k, con := range clientMap {
		if k != key { // 转发消息给当前群聊中,除自身以外的其他用户
			con.Write(byteMsg)
		}
	}
}
 
// 初始化
func initGroupChatServer() {
	fmt.Println("服务已启动...")
	time.Sleep(1 * time.Second)
	fmt.Println("等待客户端请求连接...")
	go listenClient("127.0.0.1:1801")
	select {}
}
 
func main() {
	initGroupChatServer()
}

客户端代码

package main
 
import (
	"bufio"
	"fmt"
	"net"
	"os"
)
 
// 用户名
var loginName string
 
// 本机连接
var selfConnect *net.TCPConn
 
// 读取行文本
var reader = bufio.NewReader(os.Stdin)
 
// 建立连接
func connect(addr string) {
	tcpAddr, _ := net.ResolveTCPAddr("tcp", addr) // 使用tcp
	con, err := net.DialTCP("tcp", nil, tcpAddr)  // 拨号:主动向server建立连接
	selfConnect = con
	if err != nil {
		fmt.Println("连接服务器失败")
		os.Exit(1)
	}
	go msgSender()
	go msgReceiver()
}
 
// 消息接收器
func msgReceiver() {
	buff := make([]byte, 2048)
	for {
		len, _ := selfConnect.Read(buff) // 从建立连接的缓冲区读消息
		fmt.Println(string(buff[:len]))
	}
}
 
// 消息发送器
func msgSender() {
	for {
		bMsg, _, _ := reader.ReadLine()
		bMsg = []byte(loginName + " : " + string(bMsg))
		selfConnect.Write(bMsg) // 发消息
	}
}
 
// 初始化
func initGroupChatClient() {
	fmt.Println("请问您怎么称呼?")
	bName, _, _ := reader.ReadLine()
	loginName = string(bName)
	connect("127.0.0.1:1801")
	select {}
}
 
func main() {
	initGroupChatClient()
}

运行结果展示:
server端:

client端:

————————————————
版权声明:本文为CSDN博主「dreamer'~」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_37102984/article/details/121701789

posted @   蹇爱黄  阅读(129)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· 什么是nginx的强缓存和协商缓存
· 一文读懂知识蒸馏
· Manus爆火,是硬核还是营销?
点击右上角即可分享
微信分享提示