0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

Go预言实现的后段状态推送设计与实践

Linux爱好者 来源:segmentfault 作者:hammermax 2021-05-10 17:46 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

【导读】本文是一篇Go预言实现的后段状态推送设计与实践,写的非常详细,一起来学习吧!

状态推送

前言:扫码登录功能自微信提出后,越来越多的被应用于各个web与app。这两天公司要做一个扫码登录功能,在leader的技术支持帮助下(基本都靠leader排坑),终于将服务搭建起来,并且支持上万并发。

长连接选择

决定做扫码登录功能之后,在网上查看了很多的相关资料。对于扫码登录的实现方式有很多,淘宝用的是轮询,微信用长连接,QQ用轮询……。方式虽多,但目前看来大体分为两种,1:轮询,2:长连接。(两种方式各有利弊吧,我研究不深,优缺点就不赘述了)

在和leader讨论之后选择了用长连接的方式。所以对长连接的实现方式调研了很多:

1.微信长连接:通过动态加载script的方式实现。

这种方式好在没有跨域问题。

2.websocket长连接:在PC端与服务端搭起一条长连接后,服务端主动不断地向PC端推送状态。这应该是最完美的做法了。

3.我使用的长连接:PC端向服务端发送请求,服务端并不立即响应,而是hold住,等到用户扫码之后再响应这个请求,响应后连接断开。

为什么不采用websocket呢?因为当时比较急、而对于websocket的使用比较陌生,所以没有使用。不过我现在这种做法在资源使用上比websocket低很多。

接口设计

(本来想把leader画的一副架构图放上来,但涉及到公司,不敢)

自己画的一副流程图

96217642-b0bf-11eb-bf61-12bb97331649.png

稍微解释一下:

第一条连接:打开PC界面的时候向服务端发送请求并建立长连接(1)。当APP成功扫码后(2),响应这次请求(3)。

第二条连接类似。

分析得出我们的服务只需要两个接口即可

1.与PC建立长连接的接口

2.接收APP端数据并将数据发送给前端的接口

再细想可将这两个接口抽象为:

1.PC获取状态接口:get

2.APP设置状态接口:set

具体实现

用GO写的(不多哔哔)

长连接的根本原理:连接请求后,服务端利用channel阻塞住。等到channel中有value后,将value响应

Router

func Router(){

http.HandleFunc(“/status/get”, Get)

http.HandleFunc(“/status/set”, Set)

}

GET

每一条连接需要有一个KEY作标识,不然APP设置的状态不知道该发给那台PC。每一条连接即一个channel

var Status map[string](chan string) = make(map[string](chan string))

func Get(w http.ResponseWriter, r *http.Request){

//接收key的操作

key = //PC在请求接口时带着的key

Status[key] = make(chan string) //不需要缓冲区

value := 《-Status[key]

ResponseJson(w, 0, “success”, value) //自己封的响应JSON方法

}

SET

APP扫码后可以得到二维码中的KEY,同时将想给PC发送的VALUE一起发送给服务端

func Set(w http.ResponseWriter, r *http.Request){

key =

value = //向PC传递的值

Status[key] 《- value

}

这就是实现的最基本原理。

接下来我们一点点实现其他的功能。

1.超时

从网上找了很多资料,大部分都说这种方式

srv := &http.Server{

ReadTimeout: 5 * time.Second,

WriteTimeout: 10 * time.Second,

}

log.Println(srv.ListenAndServe())

这种方式确实是设置读超时与写超时。但(亲测)这种超时方式并不友善,假如现在WriteTimeout是10s,PC端请求过来之后,长连接建立。PC处于pending状态,并且服务端被channel阻塞住。10s之后,由于超时连接失效(并没有断,我也不了解其中原理)。PC并不知道连接断了,依然处于pending状态,服务端的这个goroutine依然被阻塞在这里。这个时候我调用set接口,第一次调用没用反应,但第二次调用PC端就能成功接收value。

99289618-b0bf-11eb-bf61-12bb97331649.png

从图可以看出,我设置的WriteTimeout为10s,但这条长连接即使15s依然能收到成功响应。(ps:我调用了两次set接口,第一次没有反应)

研究后决定不使用这种方式设置超时,采用接口内部定时的方式实现超时返回

select {

case 《-`Timer`:

utils.ResponseJson(w, -1, “timeout”, nil)

case value := 《-statusChan:

utils.ResponseJson(w, 0, “success”, value)

}

Timer即为定时器。刚开始Timer是这样定义的

Timer := time.After(60 * time.Second)

60s后Timer会自动返回一个值,这时上面的通道就开了,响应timeout

但这样做有一个弊端,这个定时器一旦创建就必须等待60s,并且我没想到办法提前将定时器关了。如果这个长连接刚建立后5s就被响应,那么这个定时器就要多存在55s。这样对资源是一种浪费,并不合理。

这里选用了context作为定时器

ctx, cancel := context.WithTimeout(context.Background(), time.Duration(Timeout)*time.Second)

defer cancel()

select {

case 《-ctx.Done():

utils.ResponseJson(w, -1, “timeout”, nil)

case result := 《-Status[key]:

utils.ResponseJson(w, 0, “success”, result)

}

ctx在初始化的时候就设置了超时时间time.Duration(Timeout)*time.Second

超时之后ctx.Done()返回完成,起到定时作用。如果没有cancel()则会有一样的问题。原因如下

993413e4-b0bf-11eb-bf61-12bb97331649.png

context对比time包。提供了手动关闭定时器的方法cancel()

只要get请求结束,都会去关闭定时器,这样可以避免资源浪费(一定程度避免内存泄漏)。

注即使golang官方文档中,也推荐defer cancel()这样写

993ef908-b0bf-11eb-bf61-12bb97331649.jpg

官方文档也写到:即使ctx会在到期时关闭,但在任何场景手动调用cancel都是很好的做法。

2.多机支持

服务如果只部署在一台机器上,万一机器跪了,那就全跪了。

所以我们的服务必须同时部署在多个机器上工作。即使其中一台挂了,也不影响服务使用。

这个图不会画,只能用leader的图了

99ae677a-b0bf-11eb-bf61-12bb97331649.jpg

在项目初期讨论的时候leader给出了两种方案。1.如图使用redis做多机调度。2.使用zookeeper将消息发送给多机

因为现在是用redis做的,只讲述下redis的实现。(但依赖redis并不是很好,多机的负载均衡还要依赖其他工具。zookeeper能够解决这个问题,之后会将redis换成zookeeper)

首先我们要明确多机的难点在哪?

我们有两个接口,get、set。get是给前端建立长连接用的。set是后端设置状态用的。

假设有两台机器A、B。若前端的请求发送到A机器上,即A机器与前端连接,此时后端调用set接口,如果调用的是A机器的set接口,那是最好,长连接就能成功响应。但如果调用了B机器的set接口,B机器上又没有这条连接,那么这条连接就无法响应。

所以难点在于如何将同一个key的get、set分配到一台机器。

有人给我提过一个意见:在做负载均衡的时候,就将连接分配到指定机器。刚开始我觉的很有道理,但细细想,如果这样做,在以后如果要加机器或减机器的时候会很麻烦。对横向的增减机器不友善。

最后我还是采用了leader给出的方案:用redis绑定key与机器的关系

即前端请求到一台机器上,以key做键,以机器IP做值放在redis里面。后端请求set接口时先用key去redis里面拿到机器IP,再将value发送到这台机器上。

此时就多了一个接口,用于机器内部相互调用

ChanSet

func Router(){

http.HandleFunc(“/status/get”, Get)

http.HandleFunc(“/status/set”, Set)

http.HandleFunc(“/channel/set”, ChanSet)

}

func ChanSet(w http.ResponseWriter, r *http.Request){

key =

value =

Status[key] 《- value

}

GET

func Get(w http.ResponseWriter, r *http.Request){

IP = getLocalIp() //得到本机IP

RedisSet(key, IP) //以key做键,IP做值放入redis

Status[key] 《- value

}

SET

func Set(w http.ResponseWriter, r *http.Request){

IP = RedisGet(key) //用key去取对应机器的IP

Post(IP, key, value) //将key与value都发送给这台机器

}

注这里相当于用redis sentinel做多台机器的通信。哨兵会帮我们将数据同步到所有机器上

这样即可实现多机支持

3.跨域

刚部署到线上的时候,第一次尝试就跪了。查看错误(Access-Control-Allow-Origin)

因为前端是通过AJAX请求的长连接服务,所以存在跨域问题。

在服务端设置允许跨域

func Get(w http.ResponseWriter, r *http.Request){

w.Header().Set(“Access-Control-Allow-Origin”, “*”)

w.Header().Add(“Access-Control-Allow-Headers”, “Content-Type”)

}

若是像微信的做法,动态的加载script方式,则没有跨域问题。

服务端直接允许跨域,可能会有安全问题,但我不是很了解,这里为了使用,就允许跨域了。

4.Map并发读写问题

跨域问题解决之后,线上可以正常使用了。紧接着请测试同学压测了一下。

预期单机并发10000以上,测试同学直接压了10000,服务挂了。

可能预期有点高,5000吧,于是压了5000,服务挂了。

1000呢,服务挂了。

100,服务挂了。

……

这下豁然开朗,不可能是机器问题,绝对是有BUG

看了下报错

9a396aaa-b0bf-11eb-bf61-12bb97331649.jpg

去看了下官方文档

9a43e4da-b0bf-11eb-bf61-12bb97331649.png

Map是不能并发的写操作,但可以并发的读。

原来对Map操作是这样写的

func Get(w http.ResponseWriter, r *http.Request){

`Status[key] = make(chan string)`

`defer close(Status[key])`

select {

case 《-ctx.Done():

utils.ResponseJson(w, -1, “timeout”, nil)

case `result := 《-Status[key]`:

utils.ResponseJson(w, 0, “success”, result)

}

}

func ChanSet(w http.ResponseWriter, r *http.Request){

`Status[key] 《- value`

}

Status[key] = make(chan string)在Status(map)里面初始化一个通道,是map的写操作

result := 《-Status[key]从Status[key]通道中读取一个值,由于是通道,这个值取出来后,通道内就没有了,所以这一步也是对map的写操作

Status[key] 《- value向Status[key]内放入一个值,map的写操作

由于这三处操作的是一个map,所以要加同一把锁

var Mutex sync.Mutex

func Get(w http.ResponseWriter, r *http.Request){

//这里是同组大佬教我的写法,通道之间的拷贝传递的是指针,即statusChan与Status[key]指向的是同一个通道

statusChan := make(chan string)

Mutex.Lock()

Status[key] = statusChan

Mutex.Unlock()

//在连接结束后将这些资源都释放

defer func(){

Mutex.Lock()

delete(Status, key)

Mutex.Unlock()

close(statusChan)

RedisDel(key)

}()

select {

case 《-ctx.Done():

utils.ResponseJson(w, -1, “timeout”, nil)

case result := 《-statusChan:

utils.ResponseJson(w, 0, “success”, result)

}

}

func ChanSet(w http.ResponseWriter, r *http.Request){

Mutex.Lock()

Status[key] 《- value

Mutex.Unlock()

}

到现在,服务就可以正常使用了,并且支持上万并发。

5.Redis过期时间

服务正常使用之后,leader review代码,提出redis的数据为什么不设置过期时间,反而要自己手动删除。我一想,对啊。

于是设置了过期时间并且将RedisDel(key)删了。

设置完之后不出意外的服务跪了。

究其原因

我用一个key=1请求get,会在redis内存储一条数据记录(1 =》 Ip)。如果我set了这条连接,按之前的逻辑会将redis里的这条数据删掉,而现在是等待它过期。若是在过期时间内,再次以这个key=1,调用set接口。set接口依然会从redis中拿到IP,Post数据到ChanSet接口。而ChanSet中Status[key] 《- value由于Status[key]是关闭的,会阻塞在这里,阻塞不要紧,但之前这里加了锁,导致整个程序都阻塞在这里。

这里和leader讨论过,仍使用redis过期时间但需要修复这个Bug

func ChanSet(w http.ResponseWriter, r *http.Request){

Mutex.Lock()

ch := Status[key]

Mutex.Unlock()

if ch != nil {

ch 《- value

}

}

不过这样有一个问题,就是同一个key,在过期时间内是无法多次使用的。不过这与业务要求并不冲突。

6.Linux文件最大句柄数

在给测试同学测试之前,自己也压测了一下。不过刚上来就疯狂报错,“%¥#@¥……%……%%..too many fail open.。.”

搜索结果是linux默认最大句柄数1024.

开了下自己的机器 ulimit -a 果然1024。修改(修改方法不多BB)

7.同时监听两个端口

服务有两个API,get是给前端使用的,对外开放。set是给后端使用的,内部接口。所以这两个接口需要放在两个端口上。

由于http.ListenAndServe()本身有阻塞,故第一个监听需要一个goroutine

go http.ListenAndServe(“:11000”, FrontendMux) //对外开放的端口

http.ListenAndServe(“:11001”, BackendMux) //内部使用的端口

原文标题:Golang-长连接-状态推送

文章出处:【微信公众号:Linux爱好者】欢迎添加关注!文章转载请注明出处。

责任编辑:haq

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • PC
    PC
    +关注

    关注

    9

    文章

    2168

    浏览量

    159769
  • 连接
    +关注

    关注

    2

    文章

    102

    浏览量

    21891

原文标题:Golang-长连接-状态推送

文章出处:【微信号:LinuxHub,微信公众号:Linux爱好者】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    论马斯克的预言:AI使人类边缘化

    当地时间3月11日,在“Abundance Summit”科技峰会上,马斯克谈及AI进展时表示,AI已经进入自我改进阶段,在超高量级AI面前,人类终将走向边缘化。以下是对这一预言的相关分析: 预言
    发表于 03-14 05:27

    蔚来世界模型NWM全新版本正式推送

    2026年1月28日,「蔚来世界模型 NWM」全新版本正式开启推送,首批将为超过46万辆「Banyan 榕」车型推送。「Cedar 雪松」车型及「Cedar S 雪松」车型,也将于近期开启推送
    的头像 发表于 01-28 15:38 580次阅读

    低成本TLI4971/TLE4971电流传感器评估套件——MS2Go与S2Go

    低成本TLI4971/TLE4971电流传感器评估套件——MS2Go与S2Go 在电子工程师的日常工作中,电流传感器的评估和应用是一个重要的环节。今天我们要介绍的是英飞凌(Infineon
    的头像 发表于 12-19 16:50 1170次阅读

    探索TLE493D-P3XX-MS2GO 3D 2Go套件:开启3D磁传感器评估之旅

    探索TLE493D-P3XX-MS2GO 3D 2Go套件:开启3D磁传感器评估之旅 在电子工程师的日常工作中,评估和开发磁传感器是一项常见且重要的任务。英飞凌(Infineon
    的头像 发表于 12-18 17:15 1323次阅读

    深度解析 | 低抖动高精度EtherCAT多轴控制的实现实践案例

    深度解析 | 低抖动高精度EtherCAT多轴控制的实现实践案例 在工业自动化领域,运动控制的精度和稳定性直接决定了生产效率和产品质量。其中EtherCAT多轴控制技术尤为引人注目。今天,我们
    发表于 12-09 17:17

    电能质量在线监测装置支持的数据推送频率是多少?

    1 秒~24 小时 的自定义周期,部分高端设备可实现 毫秒级实时推送 。 一、按数据类型划分的推送频率 数据类型 典型推送频率 应用场景 标准 / 行业建议 实时基础参数 (电压 /
    的头像 发表于 12-05 15:07 555次阅读
    电能质量在线监测装置支持的数据<b class='flag-5'>推送</b>频率是多少?

    房产数据平台安家go获取地区列表数据的API接口

    如何使用安家go提供的API接口来获取地区列表数据,包括API端点、请求参数、响应格式以及代码实现。我们将逐步引导您完成整个过程,确保您能轻松集成到自己的项目中。 1. API概述 安家go的“获取地区列表”API是一个基于RE
    的头像 发表于 11-21 14:38 495次阅读
    房产数据平台安家<b class='flag-5'>go</b>获取地区列表数据的API接口

    电能质量在线监测装置的数据推送频率调整会影响数据的准确性吗?

    电能质量在线监测装置的数据推送频率调整对数据准确性的影响需从 采样、处理、传输 三个核心环节综合评估,其关键取决于 调整策略与装置设计的匹配度 。以下是基于技术原理与工程实践的详细分析: 一、核心
    的头像 发表于 11-07 11:08 872次阅读

    设备的状态监测可以通过哪些方式实现

    设备状态监测的核心是 通过 “硬件传感 + 软件自检 + 通信反馈 + 远程联动”,实现对设备 “健康状态、运行状态、安全状态” 的全维度感
    的头像 发表于 11-07 09:44 1119次阅读

    资源状态感知是如何实现对网络链路状态的实时感知的?

    资源状态感知对网络链路状态的实时监测是通过硬件底层检测、协议层交互、算法模型分析的多层协同实现的,具体技术路径如下: 一、硬件层:物理信号的实时捕获 PHY 芯片的直接感知以太网 PHY 芯片(如
    的头像 发表于 11-06 14:49 875次阅读

    订单实时状态查询接口技术实现

    ​  在电子商务系统中,订单实时状态查询是核心功能之一。用户需要即时获取订单的最新状态(如“已支付”、“发货中”或“已完成”),这对用户体验和业务运营至关重要。本文将一步步介绍如何设计并实现一个高效
    的头像 发表于 10-21 17:58 877次阅读
    订单实时<b class='flag-5'>状态</b>查询接口技术<b class='flag-5'>实现</b>

    淘宝/天猫:使用订单查询API实时追踪包裹状态,自动推送物流通知至用户

    实现包裹状态的实时监控,并自动推送物流更新通知给用户,从而优化服务流程。本文将逐步介绍如何利用淘宝/天猫的开放平台API实现这一功能,确保高效、可靠。 1. 理解订单查询API的基本原
    的头像 发表于 09-10 16:55 1504次阅读
    淘宝/天猫:使用订单查询API实时追踪包裹<b class='flag-5'>状态</b>,自动<b class='flag-5'>推送</b>物流通知至用户

    锂电池制造:电芯后段处理中的除气工艺

    在锂离子电池的规模化制造中,电芯后段处理是将电极组件转化为合格成品的关键环节,直接决定电池的能量密度、循环寿命与安全性能。其中,除气工艺作为后段处理的核心工序,专门针对电芯在化成过程中产生的反应气体
    的头像 发表于 08-11 14:52 2042次阅读
    锂电池制造:电芯<b class='flag-5'>后段</b>处理中的除气工艺

    蔚来世界模型NWM首个版本正式推送

    近日,「蔚来世界模型 NWM」首个版本正式开启推送。首批推送车型为超过40万台的「Banyan 榕」车型。「Cedar 雪松」车型,包括ET9、新ES6、新EC6、新ET5、新ET5T在内,将会于6月底开启推送
    的头像 发表于 06-04 15:13 1017次阅读

    单片机C语言实例(350+例)

    350+单片机C语言实例! 纯分享帖,需要者可点击附件免费获取完整资料~~~【免责声明】本文系网络转载,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请第一时间告知,删除内容!
    发表于 05-22 21:47