关于长连接:轮询和长轮询的区别

1.轮询由客户端发送申请,服务器接管申请的过程,通过客户端一直申请,使得客户端可能模仿达到相似实时收到服务器的成果。客户端定时向服务器发送 Ajax 申请,服务器接到申请后马上返回响应信息,并敞开连贯。不论服务端数据有无更新,客户端每隔定长时间申请拉取一次数据,可能有更新数据返回,也可能什么都没有。实用用户量比拟小,不太重视性能的我的项目,如小型利用、WEB 利用、例如零碎音讯、天气展现等。长处:逻辑简略,易于了解,开发疾速。毛病:(1)须要反复建设 HTTP 连贯,占用大量客户端和服务端的连贯资源。 1.客户端越多, 服务端压力越大,很多时候并没有新的数据更新,因而绝大部分申请都是有效申请。2.数据不肯定是实时更新,要看设定的申请距离,根本会有提早。 2.长轮询长轮询是长连贯的一种,当服务器收到客户端发来的申请后,服务器端不会间接进行响应,而是先将这个申请挂起,而后判断服务器端数据是否有更新。如果有更新,则进行响应,如果始终没有数据,则会 hold 住申请,直到服务端的数据发生变化,或者期待肯定工夫超时才会返回。客户端 JavaScript 响应处理函数会在解决完服务器返回的信息后,再次发出请求,从新建设连贯。像 WebQQ/FaceBook 早起都是应用长轮询实现的。长处:音讯即时达到,和短轮询比起来,显著缩小了很多不必要的 HTTP 申请次数,在无音讯的状况下不会频繁的申请,相比之下节约了资源,在无音讯的状况下不会频繁的申请。毛病:连贯挂起会导致资源的节约,长轮询会造出十分多的申请,一直的申请可能会造成的影响是数据程序无奈失去保障。 3.倡议轮训可能实现的性能长轮训都能满足,从技术角度思考倡议应用长轮训替换轮训实现,节俭服务器性能和带宽,相比下来开发成本也不高。  源码附件曾经打包好上传到百度云了,大家自行下载即可~ 链接: https://pan.baidu.com/s/14G-b...提取码: yu27百度云链接不稳固,随时可能会生效,大家放松保留哈。如果百度云链接生效了的话,请留言通知我,我看到后会及时更新~开源地址码云地址:http://github.crmeb.net/u/defuGithub 地址:http://github.crmeb.net/u/defu

August 1, 2022 · 1 min · jiezi

验证Nginx的长连接keepalive配置

写在前面最近都在折腾 Nginx 服务器的学习和测试,前几天稍微温习了一下计算机网络方面的知识(一方面是兴趣,一方面是这次学习过程中因为这些计算机基础遗忘,有很多细节问题让人很懵逼),也在 Linux 上试了一下 tcpdump 命令,通过抓包来验证自己之前的各种猜想(因为不懂所以瞎猜),分析数据包依旧是用的 Wireshark 之前一直以为只要使用 Http1.1 协议就可以复用连接,节省反复握手挥手的时间消耗,但抓包后才发现,"简简单单"的长连接使用,还真是涉及了 Nginx 、 JMeter 、 Tomcat 里的很多配置啊 JMeter 虽然在 Http 请求的配置中默认勾选了使用 KeepAlive,但是实际使用中并没有生效Nginx 涉及到与客户端的配置 keepalive_timeout 和 keepalive_requests,与后端服务器的配置 keepalive(1.15.3之后,upstream 模块也新增了 keepalive_timeout 和 keepalive_requests,本篇暂不涉及)Tomcat 默认也有一个 keepAliveTimeout 配置知道了这些相关配置后,一方面想实战下 tcpdump 和 Wireshark 的使用,一方面也想用数据验证下 Nginx 的这三个配置,所以就有了接下来的内容 环境说明Nginx 1.14.0Linux 2.6.32JMeter 5.0Wireshark 2.6.4JMeter 所在主机 IP 172.16.40.199Tomcat 所在主机 IP 172.16.40.201Nginx 所在主机 IP 172.16.40.2240. 和客户端的长连接超时时间指令说明语法: keepalive_timeout timeout [header_timeout];默认值: keepalive_timeout 75s; 上下文: http, server, location 第一个参数设置客户端的长连接在服务器端保持的最长时间(在此时间客户端未发起新请求,则长连接关闭)。 第二个参数为可选项,设置“Keep-Alive: timeout=time”响应头的值。 可以为这两个参数设置不同的值。 ...

October 14, 2019 · 2 min · jiezi

长连接

本篇是工程开发的网络一个分篇,整体见:https://segmentfault.com/a/11...。概述长连接原本核心只是消息推送和部分数据上报,一般公司里面当成IM用的会比较多后来演变成一个通道,不只是IM,消息push,就连短连接也走这个通道。降低连接成本,尤其是位置上报push等场景,还可以利用高速通道做多点接入 一. 外部连接native APP使用TCP直连长连接域名,通过LVS/TGW的转发(gz机房为FULLNAT模式,QQ机房貌似是TUNNEL模式),连接到某个CONNSVR的对外服务端口上(支持加密和非加密两种), 二. 连接网络过程此时APP与CONNSVR完成了TCP的三次握手,然后开始业务层面的握手,握手过程如下: 三.架构和功能 下行,push● 业务向push推送数据,可能提供uid,也可能提供role+phone进行索引 ● 如果是role+phone的形式,则push需要向AAASVR发起请求,使用role+phone获取uid,注意这里会导致AAASVR的流量增大很多 ● PUSHSVR使用uid向CONNMASTER请求,获取用户所在的CONNSVR ● PUSHSVR将数据推给CONNSVR,后者推给app ● 如果用户不在线(step 3查询失败),或者CONNSVR发送失败, 则PUSHSVR根据消息推送的不同策略(由业务方指定),决定是否将消息写入REPUSHSVR,REPUSHSVR则会不停的将消息重新推回PUSHSVR进行重试 上行统一接入 统一接入后,依赖端上请求中Header携带的city_id,配合Apollo实现流量切换(可统一在LVS做)过程1.App->Connsvr->Trans, 携带端上从HTTP请求转换来的内容,例如Header、Body等2.Trans->Connsvr->App, 这个用于快速回复App刚才发送的Req是否通过了拆包、路由检查等结果,因为App连接变动,这个包可能回不到App上3.Trans->Push->Connsvr->App, 这个和一般下行包类似链路,用于携带真正的HTTP RSPtrans工作:1.接受Connsvr转发过来的REQ@PB2.配置路由,根据REQ@PB中的Scheme+Host+Uri,找到对应的内网Router VIP:VPORT3.翻译REQ@PB到REQ@HTTP,发往Router的内网VIP:VPORT(Router实质上变为了InRoute,域名变为了路由key)4.在Timeout内,维护REQ的信息(此时如果Trans down了,则状态丢失,引入缓存来维护state)5.收到服务端的RSP@HTTP后,翻译成RSP@PB,通过pushsvr发往App(Pushsvr会完成路由寻找和发送功能)多点接入引入长连接后建立连接的代价少,可以维持高速通道,就近设置接入点,接入点到真正的业务接入点之间可以走专线,更快

May 14, 2019 · 1 min · jiezi

Golang-长连接-状态推送

状态推送前言:扫码登录功能自微信提出后,越来越多的被应用于各个web与app。这两天公司要做一个扫码登录功能,在leader的技术支持帮助下(基本都靠leader排坑),终于将服务搭建起来,并且支持上万并发。长连接选择决定做扫码登录功能之后,在网上查看了很多的相关资料。对于扫码登录的实现方式有很多,淘宝用的是轮询,微信用长连接,QQ用轮询……。方式虽多,但目前看来大体分为两种,1:轮询,2:长连接。(两种方式各有利弊吧,我研究不深,优缺点就不赘述了)在和leader讨论之后选择了用长连接的方式。所以对长连接的实现方式调研了很多:1.微信长连接:通过动态加载script的方式实现。这种方式好在没有跨域问题。2.websocket长连接:在PC端与服务端搭起一条长连接后,服务端主动不断地向PC端推送状态。这应该是最完美的做法了。3.我使用的长连接:PC端向服务端发送请求,服务端并不立即响应,而是hold住,等到用户扫码之后再响应这个请求,响应后连接断开。为什么不采用websocket呢?因为当时比较急、而对于websocket的使用比较陌生,所以没有使用。不过我现在这种做法在资源使用上比websocket低很多。接口设计(本来想把leader画的一副架构图放上来,但涉及到公司,不敢)自己画的一副流程图稍微解释一下:第一条连接:打开PC界面的时候向服务端发送请求并建立长连接(1)。当APP成功扫码后(2),响应这次请求(3)。第二条连接类似。分析得出我们的服务只需要两个接口即可1.与PC建立长连接的接口2.接收APP端数据并将数据发送给前端的接口再细想可将这两个接口抽象为:1.PC获取状态接口:get2.APP设置状态接口:set具体实现用GO写的(不多哔哔)长连接的根本原理:连接请求后,服务端利用channel阻塞住。等到channel中有value后,将value响应Routerfunc Router(){ http.HandleFunc("/status/get", Get) http.HandleFunc("/status/set", Set)}GET每一条连接需要有一个KEY作标识,不然APP设置的状态不知道该发给那台PC。每一条连接即一个channelvar 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方法}SETAPP扫码后可以得到二维码中的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。从图可以看出,我设置的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()则会有一样的问题。原因如下具体参考如下:https://blog.csdn.net/liangzh…context对比time包。提供了手动关闭定时器的方法cancel()只要get请求结束,都会去关闭定时器,这样可以避免资源浪费(一定程度避免内存泄漏)。注即使golang官方文档中,也推荐defer cancel()这样写官方文档也写到:即使ctx会在到期时关闭,但在任何场景手动调用cancel都是很好的做法。这样超时功能就实现了2.多机支持服务如果只部署在一台机器上,万一机器跪了,那就全跪了。所以我们的服务必须同时部署在多个机器上工作。即使其中一台挂了,也不影响服务使用。这个图不会画,只能用leader的图了在项目初期讨论的时候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发送到这台机器上。此时就多了一个接口,用于机器内部相互调用ChanSetfunc 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}GETfunc Get(w http.ResponseWriter, r *http.Request){ … IP = getLocalIp() //得到本机IP RedisSet(key, IP) //以key做键,IP做值放入redis Status[key] <- value …}SETfunc 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看了下报错去看了下官方文档Map是不能并发的写操作,但可以并发的读。原来对Map操作是这样写的func Get(w http.ResponseWriter, r *http.Request){ … Status[key] = make(chan string) … select { case <-ctx.Done(): utils.ResponseJson(w, -1, “timeout”, nil) case result := &lt;-Status[key]: utils.ResponseJson(w, 0, “success”, result) } …}func ChanSet(w http.ResponseWriter, r *http.Request){ … Status[key] &lt;- 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.Mutexfunc 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过期时间但需要修复这个Bugfunc 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) ...

January 12, 2019 · 2 min · jiezi