原文出处:《Go 语言编程之旅》第四章4.1节

基于TCP的聊天室

1、服务端

  • 新用户到来,生成一个User的实例,代表该用户。
type User struct{
ID int // 用户的唯一标识,通过GenUserID 函数生成
Addr string // 用户的IP地址和端口
EnterAt time.Time // 用户进入的时间
MessageChannel chan string // 当前用户发送消息的通道
}
  • 新开一个goroutine用于给用户发送消息
func sendMessage(conn net.Conn, ch <- chan string){
for msg := range ch{
fmt.Fprintln(conn, msg)
}
}

结合User结构体的MessageChannel,很容易知道,需要给某个用户发送消息,只需要往该用户的MessageChannel中写入消息即可。这里需要特别提醒下,因为sendMessage在一个新的goroutine中,如果函数的ch不关闭,该goroutine是不会退出的,因此需要注意不关闭ch导致goroutine泄露问题。

  • 给当前用户发送欢迎信息,同时给聊天室所有的用户发送有新用户到来的提醒
user.MessageChannel <- "Welcome" + user.String()
msg := Message{
OwnerID: user.ID,
Content: "user:`" + strconv.Itoa(user.ID) + "` has enter",
}
messageChannel <- msg
  • 将该新用户写入全局用户列表,也就是聊天室用户列表。同时控制用户超时退出,超过5分钟没有任何响应,则提出
enteringChannel <- user

// 控制超时用户踢出
var userActive = make(chan struct{})
go func() {
d := 5 * time.Minute
timer := time.NewTimer(d)
for{
select {
case <- timer.C:
conn.Close()
case <- userActive:
timer.Reset(d)
}
}
}()
  • 读取用户的输入,并将用户信息发送给其他用户。

    在bufio包中有多重方式获取文本输入,ReadBytes、ReadString和独特的ReadLine,对于简单的目的这些都有些复杂。在Go1,1中添加了一个新类型,Scabber,以便更容易的处理如按行读取输入序列或空格分隔单词等这类简单任务。它终结了如输入一个很长的有问题的行这样的输入错误,并且提供了简单的默认行为:基于行的输入,每行都提出了分隔标识。

//  循环读取用户的输入
input := bufio.NewScanner(conn)
for input.Scan(){
msg.Content = strconv.Itoa(user.ID) + ";" + input.Text()
messageChannel <- msg // 用户活跃
userActive <- struct{}{}
} if err := input.Err();err != nil {
log.Println("读取错误:", err)
}
  • 用户离开,需要做登记,并给连天使其他用户发通知
leavingChannel <- user
msg.Content = "user: `" + strconv.Itoa(user.ID) + "` has left"
messageChannel <- msg
完整代码
package main

import (
"bufio"
"fmt"
"log"
"net"
"strconv"
"sync"
"time"
) type User struct{
ID int // 用户的唯一标识,通过GenUserID 函数生成
Addr string // 用户的IP地址和端口
EnterAt time.Time // 用户进入的时间
MessageChannel chan string // 当前用户发送消息的通道
} // 给用户发送信息
type Message struct{
OwnerID int
Content string
} var (
// 新用户到来,通过该channel进行登记
enteringChannel = make(chan *User)
// 用户离开,通过该channel进行登记
leavingChannel = make(chan *User)
// 广播专用的用户普通消息channel, 缓冲是尽可能避免出现异常情况阻塞
messageChannel = make(chan Message, 9)
) func (u *User) String() string{
return u.Addr + ",UID:" + strconv.Itoa(u.ID) + ", Enter At:" + u.EnterAt.Format("2006-01-02 15:04:05+8000")
} func main() {
listener, err := net.Listen("tcp",":2020")
if err != nil {
panic(err)
} go broadcaster() for {
conn, err := listener.Accept()
if err != nil {
log.Println(err)
continue
} go handleConn(conn)
}
} // broadcaster 用于记录聊天室用户,并进行消息广播
// 1. 新用户进来; 2.用户普通消息; 3.用户离开
func broadcaster(){
users := make(map[*User]struct{}) for {
select{
case user := <- enteringChannel:
// 新用户进入
users[user] = struct{}{}
case user := <- leavingChannel:
// 用户离开
delete(users, user)
// 避免goroutine泄露
close(user.MessageChannel)
case msg := <-messageChannel:
// 给所有在线用户发送消息
for user := range users {
if user.ID == msg.OwnerID{
continue
}
user.MessageChannel <- msg.Content
}
}
}
} func handleConn(conn net.Conn){
defer conn.Close() // 1. 新用户进来,构建该用户实例
user := &User{
ID: GenUserID(),
Addr: conn.RemoteAddr().String(),
EnterAt: time.Now(),
MessageChannel: make(chan string,8),
} // 2. 当前在一个新的goroutine 中,用来进行读写操作,因此需要开一个goroutine用于读写操作
// 读写goroutine 之间通过channel 进行通信
go sendMessage(conn, user.MessageChannel) // 3. 给当前用户发送欢迎信息;给所有用户告知新用户列表
user.MessageChannel <- "Welcome" + user.String()
msg := Message{
OwnerID: user.ID,
Content: "user:`" + strconv.Itoa(user.ID) + "` has enter",
}
messageChannel <- msg // 4. 将该记录到全局的用户列表中,避免用锁
enteringChannel <- user // 控制超时用户踢出
var userActive = make(chan struct{})
go func() {
d := 5 * time.Minute
timer := time.NewTimer(d)
for{
select {
case <- timer.C:
conn.Close()
case <- userActive:
timer.Reset(d)
}
}
}() // 5. 循环读取用户的输入
input := bufio.NewScanner(conn)
for input.Scan(){
msg.Content = strconv.Itoa(user.ID) + ";" + input.Text()
messageChannel <- msg // 用户活跃
userActive <- struct{}{}
} if err := input.Err();err != nil {
log.Println("读取错误:", err)
} // 6. 用户离开
leavingChannel <- user
msg.Content = "user: `" + strconv.Itoa(user.ID) + "` has left"
messageChannel <- msg
} func sendMessage(conn net.Conn, ch <- chan string){
for msg := range ch{
fmt.Fprintln(conn, msg)
}
} // 生成用户id
var (
globalID int
idocker sync.Mutex
) func GenUserID() int {
idocker.Lock()
defer idocker.Unlock() globalID ++
return globalID
}

2、客户端

客户端的实现直接采用 《The Go Programming Language》一书对应的示例源码:ch8/netcat3/netcat.go 。

func main() {
conn, err := net.Dial("tcp", ":2020")
if err != nil {
panic(err)
} done := make(chan struct{})
go func() {
io.Copy(os.Stdout, conn) // NOTE: ignoring errors
log.Println("done")
done <- struct{}{} // signal the main goroutine
}() mustCopy(conn, os.Stdin)
conn.Close()
<-done
} func mustCopy(dst io.Writer, src io.Reader) {
if _, err := io.Copy(dst, src); err != nil {
log.Fatal(err)
}
}
  • 新开了一个 goroutine 用于接收消息;
  • 通过 io.Copy 来操作 IO,包括从标准输入读取数据写入 TCP 连接中,以及从 TCP 连接中读取数据写入标准输出;
  • 新开的 goroutine 通过一个 channel 来和 main goroutine 通讯;

基于TCP实现简单的聊天室的更多相关文章

  1. 基于TCP/IP的局域网聊天室---C语言

    具备注册账号,群聊,查看在线人员信息,私发文件和接收文件功能,因为每个客户端只有一个属于自己的socket,所以无论客户端是发聊天消息还是文件都是通过这一个socket发送, 这也意味着服务器收发任何 ...

  2. 基于websocket实现的一个简单的聊天室

    本文是基于websocket写的一个简单的聊天室的例子,可以实现简单的群聊和私聊.是基于websocket的注解方式编写的.(有一个小的缺陷,如果用户名是中文,会乱码,不知如何处理,如有人知道,请告知 ...

  3. 基于LINUX的多功能聊天室

    原文:基于LINUX的多功能聊天室 基于LINUX的多功能聊天室 其实这个项目在我电脑已经躺了多时,最初写完项目规划后,我就认认真真地去实现了它,后来拿着这个项目区参加了面试,同样面试官也拿这个项目来 ...

  4. Android简单的聊天室开发(client与server沟通)

    请尊重他人的劳动成果.转载请注明出处:Android开发之简单的聊天室(client与server进行通信) 1. 预备知识:Tcp/IP协议与Socket TCP/IP 是Transmission ...

  5. Netty学习笔记(四) 简单的聊天室功能之服务端开发

    前面三个章节,我们使用了Netty实现了DISCARD丢弃服务和回复以及自定义编码解码,这篇博客,我们要用Netty实现简单的聊天室功能. Ps: 突然想起来大学里面有个课程实训,给予UDP还是TCP ...

  6. 基于EPOLL模型的局域网聊天室和Echo服务器

    一.EPOLL的优点 在Linux中,select/poll/epoll是I/O多路复用的三种方式,epoll是Linux系统上独有的高效率I/O多路复用方式,区别于select/poll.先说sel ...

  7. 基于 OpenResty 实现一个 WS 聊天室

    基于 OpenResty 实现一个 WS 聊天室 WebSocket WebSocket 协议分析 WebSocket 协议解决了浏览器和服务器之间的全双工通信问题.在WebSocket出现之前,浏览 ...

  8. 玩转Node.js(四)-搭建简单的聊天室

    玩转Node.js(四)-搭建简单的聊天室 Nodejs好久没有跟进了,最近想用它搞一个聊天室,然后便偶遇了socket.io这个东东,说是可以用它来简单的实现实时双向的基于事件的通讯机制.我便看了一 ...

  9. TCP/IP以及Socket聊天室带类库源码分享

    TCP/IP以及Socket聊天室带类库源码分享 最近遇到个设备,需要去和客户的软件做一个网络通信交互,一般的我们的上位机都是作为客户端来和设备通信的,这次要作为服务端来监听客户端,在这个背景下,我查 ...

  10. 简单的聊天室代码php+swoole

    php swoole+websocket 客户端代码 <!DOCTYPE html> <html> <head> <title></title&g ...

随机推荐

  1. pytorch报错 No module named 'nn'

    问题描述 pytorch 报错 No module named 'nn' 如果你 import torch 没问题,而 import torch.nn时出现问题,你可能命名使用了一个名字为 torch ...

  2. 用Docker Swarm实现容器服务高可用

    背景与技术选择 根据我之前的几篇「Django 系列」文章,后端架构中我使用了 Django + Celery + RabbitMQ 三个框架/服务.现在有几个问题: 如何用容器快速部署这三个应用? ...

  3. CompletableFuture原理及应用场景详解

    1.应用场景 现在我们打开各个APP上的一个页面,可能就需要涉及后端几十个服务的API调用,比如某宝.某个外卖APP上,下面是某个外卖APP的首页.首页上的页面展示会关联很多服务的API调用,如果使用 ...

  4. json中用到的token

    JSON Web Token (JWT)是一个开放标准(RFC 7519). 用于JSON对象在各个层之间安全地传输信息.该信息可以被验证和信任,通过数字签名. 应用场景:    Authorizat ...

  5. vue3第二次传递数据方法无法获取到最新的值

    使用reactive父组件第二次传递给子组件的数据:方法中可以获取到最新数据 <template> <div> <div> <h1>子组件</h1 ...

  6. 比df更好用的命令!

    大家好,我是良许. 对于分析磁盘使用情况,有两个非常好用的命令:du 和 df .简单来说,这两个命令的作用是这样的: du 命令:它是英文单词 disk usage 的简写,主要用于查看文件与目录占 ...

  7. Python 潮流周刊#96:MCP 到底是什么?(摘要)

    本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章.教程.开源项目.软件工具.播客和视频.热门话题等内容.愿景:帮助所有读者精进 Python 技术,并增长职 ...

  8. RAGflow搭建text-to-sql的AI研发助手

    一.概述 text-to-sql 技术允许用户通过自然语言提问,系统自动将其转换为 SQL 语句并执行,大大降低了数据查询的门槛,提高了工作效率. text-to-sql 技术在数据分析.智能客服.数 ...

  9. Google发布A2A开源协议:“MCP+A2A”成未来标配?

    就在刚刚Google Cloud Next 25大会上,谷歌重磅开源Agent2Agent(A2A)协议,这项被类比为"AI界的HTTP协议"的技术标准,彻底打破了智能体间的信息孤 ...

  10. 如何使用Nacos作为配置中心统一管理配置

    如何使用Nacos作为配置中心统一管理配置 1).引入依赖, <dependency> <groupId>com.alibaba.cloud</groupId> & ...