精华内容
下载资源
问答
  • 1.背景1.1.WebSocket与普通Socket的差异Socket简单理解:Socket = IP地址 + 端口 + 协议。...WebSocketWebSocket是一个持久化的协议,它是伴随HTTP5而出的协议,用来解决http不支持持久化连接的问题。总之,W...

    1.背景

    1.1.WebSocket与普通Socket的差异

    Socket

    简单理解:Socket = IP地址 + 端口 + 协议。

    具体来说,Socket是一套标准,它完成了对TCP/IP的高度封装,屏蔽网络细节以方便开发者更好地进行网络编程。Socket有自己的原语。

    WebSocket

    WebSocket是一个持久化的协议,它是伴随HTTP5而出的协议,用来解决http不支持持久化连接的问题。

    总之,WebSocket和Socket其实是两个东西,Socket一个是网编编程的标准接口,而WebSocket是应用层通信协议。

    但是,说WebSocket与Socket的差异更多说的是:WebSocket与HTTP无状态被动协议的差异,或者说是WebSocket与Ajax轮询和long poll等实现实时信息传输方式的差异,这也是本文需要讨论的问题。

    1.2.http的非持久性

    http的生命周期通过Request来界定,在HTTP1.0中一个Request一个Response,这次HTTP请求就结束了。

    HTTP1.1进行了改进,通过使用一个keep-alive,在一个HTTP连接中,可以发送多个Request,接收多个Response,但其实本质上还是一个Request只能有一个Response,并且Reponse是被动的,不能主动发起。

    2.WebSocket原理

    2.1.WebSocket握手

    首先,虽然WebSocket是随着http5出的协议,但是WebSocket和http协议没什么关系,两者关系如下所示:

    e228263202042c2b81961adc2cfdea0b.png

    两者有交集,但不存在包含关系。

    WebSocket借用了HTTP协议来完成一部分握手。

    首先,我们来看个典型的Websocket握手

    GET /chat HTTP/1.1
    Host: server.example.com
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
    Sec-WebSocket-Protocol: chat, superchat
    Sec-WebSocket-Version: 13
    Origin: http://example.com

    熟悉HTTP的童鞋可能发现了,这段类似HTTP协议的握手请求中,多了几个东西。

    我会顺便讲解下作用。

    新增1:

    Upgrade: websocket
    Connection: Upgrade

    这个就是Websocket的核心了,告诉Apache、Nginx等服务器,这是Websocket协议,而不是http,快点帮我找到对应的助理处理一下。

    新增2:

    Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
    Sec-WebSocket-Protocol: chat, superchat
    Sec-WebSocket-Version: 13
    • Sec-WebSocket-Key 是一个Base64 encode的值,这个是浏览器随机生成的,用来验证服务器是不是真的是Websocket助理。
    • Sec_WebSocket-Protocol 是一个用户定义的字符串,用来区分同URL下,不同的服务所需要的协议。简单理解:今晚我要服务A,别搞错啦~
    • Sec-WebSocket-Version 是告诉服务器所使用的Websocket Draft(协议版本),在最初的时候,Websocket协议还在 Draft 阶段,各种奇奇怪怪的协议都有,而且还有很多期奇奇怪怪不同的东西,什么Firefox和Chrome用的不是一个版本之类的,当初Websocket协议太多可是一个大难题。。不过现在还好,已经定下来啦~大家都使用的一个东西~ 服务员,我要的是13岁的噢→_→

    然后服务器会返回下列东西,表示已经接受到请求, 成功建立Websocket啦!

    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
    Sec-WebSocket-Protocol: chat

    这里开始就是HTTP最后负责的区域了,告诉客户,我已经成功切换协议啦~

    Upgrade: websocket
    Connection: Upgrade

    首先Upgrade何Connection字段依然是固定的,告诉客户端即将升级的是Websocket协议,而不是mozillasocketlurnarsocket或者shitsocket。

    然后,Sec-WebSocket-Accept 这个则是经过服务器确认,并且加密过后的 Sec-WebSocket-Key。服务器:好啦好啦,知道啦,给你看我的ID CARD来证明行了吧。。

    后面的,Sec-WebSocket-Protocol 则是表示最终使用的协议。

    至此,HTTP已经完成它所有工作了,接下来就是完全按照Websocket协议进行了。

    2.2.ajax轮询与long poll的被动性

    在讲Websocket之前,我就顺带着讲下 long poll 和 ajax轮询 的原理。虽然这两种方式也可以实现实时信息传递,但他们都存在一个共同的问题,基于http协议。

    ajax轮询

    ajax轮询的原理非常简单,让浏览器隔个几秒就发送一次请求,询问服务器是否有新信息。

    场景再现:

    客户端:啦啦啦,有没有新信息(Request)

    服务端:没有(Response)

    客户端:啦啦啦,有没有新信息(Request)

    服务端:没有。。(Response)

    客户端:啦啦啦,有没有新信息(Request)

    服务端:你好烦啊,没有啊。。(Response)

    客户端:啦啦啦,有没有新消息(Request)

    服务端:好啦好啦,有啦给你。(Response)

    客户端:啦啦啦,有没有新消息(Request)

    服务端:。。。。。没。。。。没。。。没有(Response)

    long poll

    long poll 其实原理跟 ajax轮询 差不多,都是采用轮询的方式,不过采取的是阻塞模型(一直打电话,没收到就不挂电话),也就是说,客户端发起连接后,如果没消息,就一直不返回Response给客户端。直到有消息才返回,返回完之后,客户端再次建立连接,周而复始。

    场景再现

    客户端:啦啦啦,有没有新信息,没有的话就等有了才返回给我吧(Request)

    服务端:额。。 等待到有消息的时候。。来 给你(Response)

    客户端:啦啦啦,有没有新信息,没有的话就等有了才返回给我吧(Request) -

    loop

    从上面可以看出其实这两种方式,都是在不断地建立HTTP连接,然后等待服务端处理,可以体现HTTP协议的另外一个特点,被动性

    何为被动性呢,其实就是,服务端不能主动联系客户端,只能有客户端发起。

    简单地说就是,服务器是一个很懒的冰箱,不会、不能主动发起连接,但是上司有命令,如果有客户来,不管多么累都要好好接待。

    说完这个,我们再来说一说上面的缺陷

    从上面很容易看出来,不管怎么样,上面这两种都是非常消耗资源的。

    ajax轮询 需要服务器有很快的处理速度和资源。(速度)

    long poll 需要有很高的并发,也就是说同时接待客户的能力。(场地大小)

    所以ajax轮询 和long poll 都有可能发生这种情况。

    客户端:啦啦啦啦,有新信息么?

    服务端:月线正忙,请稍后再试(503 Server Unavailable)

    客户端:。。。。好吧,啦啦啦,有新信息么?

    服务端:月线正忙,请稍后再试(503 Server Unavailable)

    客户端:

    然后服务端在一旁忙的要死:冰箱,我要更多的冰箱!更多。。更多。。

    2.3.WebSocket协议

    通过上面这个例子,我们可以看出,这两种方式都不是最好的方式,需要很多资源。

    HTTP还是一个无状态协议。Websocket出现了。他解决了HTTP的这几个难题。简单来说就是WebSocket只需一次HTTP请求、服务端主动推送消息。

    当服务器完成协议升级后(HTTP->Websocket),服务端就可以主动推送信息给客户端啦。

    所以上面的情景可以做如下修改。

    客户端:啦啦啦,我要建立Websocket协议,需要的服务:chat,Websocket协议版本:17(HTTP Request)

    服务端:ok,确认,已升级为Websocket协议(HTTP Protocols Switched)

    客户端:麻烦你有信息的时候推送给我噢。。

    服务端:ok,有的时候会告诉你的。

    服务端:balabalabalabala

    服务端:balabalabalabala

    服务端:哈哈哈哈哈啊哈哈哈哈

    服务端:笑死我了哈哈哈哈哈哈哈

    就变成了这样,只需要经过一次HTTP请求,就可以做到源源不断的信息传送了。

    在程序设计中,这种设计叫做回调,即:你有信息了再来通知我,而不是我傻乎乎的每次跑来问你。

    这样的协议解决了上面同步有延迟,而且还非常消耗资源的这种情况。

    那么为什么他会解决服务器上消耗资源的问题呢?

    答案还是从WebSocket机制来回答:一次请求,持久连接,无需反复鉴别用户,服务端主动PUSH

    首先,我们得了解WebSocket请求过程,我们所用的程序是要经过两层代理的,即HTTP协议在Nginx等服务器的解析下,然后再传送给相应的Handler(PHP等)来处理。

    简单地说,我们有一个非常快速的接线员(Nginx),他负责把问题转交给相应的客服(Handler)

    本身接线员基本上速度是足够的,但是每次都卡在客服(Handler)了,老有客服处理速度太慢。,导致客服不够。

    Websocket就解决了这样一个难题,建立后,可以直接跟接线员建立持久连接,有信息的时候客服想办法通知接线员,然后接线员再统一转交给客户。

    这样就可以解决客服处理速度过慢的问题了。

    其次,在传统的方式上,要不断的建立,关闭HTTP协议,由于HTTP是无状态性的,每次都要重新传输identity info(鉴别信息),来告诉服务端你是谁。

    虽然接线员很快速,但是每次都要听这么一堆,效率也会有所下降的,同时还得不断把这些信息转交给客服,不但浪费客服的处理时间,而且还会在网路传输中消耗过多的流量/时间。

    但是Websocket只需要一次HTTP握手,所以说整个通讯过程是建立在一次连接/状态中,也就避免了HTTP的无状态性,服务端会一直知道你的信息,直到你关闭请求,这样就解决了接线员要反复解析HTTP协议,还要查看identity info的信息。

    同时由客户主动询问,转换为服务器(推送)有信息的时候就发送(当然客户端还是等主动发送信息过来的),没有信息的时候就交给接线员(Nginx),不需要占用本身速度就慢的客服(Handler)了。

    总结

    HTTP协议是被动的无状态协议,一个Request对应一个Response,每次Request,客户端都需要重复带上用于鉴别客户端身份的信息到服务端,且服务端无法主动推送消息给客户端。针对HTTP这种效率低、响应不及时、浪费通信带宽和服务端资源的情况,WebSocket应运而生,WebSocket采用一次请求,持久连接方式解决了反复校验客户端身份问题,服务端建立连接后可以主动推送消息给客户端,因此通信效率大大增加,同时也节约了客户端、服务端和网络资源。

    以上。

    展开全文
  • 验证猜想解决问题如何发现问题的呢? 简介 这次分享是在混合云场景下,基于websocket长连接,实现Server-Client(多个)架构模式中,云服务需要维护客户端的状态,但是云端维护的状态可能和实际的客户端的状态不一致...

    简介

    这次分享是在混合云场景下,基于websocket长连接,实现Server-Client(多个)架构模式中,云服务需要维护客户端的状态,但是云端维护的状态可能和实际的客户端的状态不一致,可能就会导致一些奇怪的事情发生,比较有意思的一个问题吧,非常不容易发现的一个问题,必须需要一个合适的契机才可以去发现。前面直接描述问题和解决方案,后面用一定的篇幅详细讲述一下怎么发现的这个问题。

    问题的现象、场景和解决方案

    基本的部署架构

    如下图:
    在这里插入图片描述
    比较简单,但也比较重要,这是看懂本问题以及解决的基本知识,但是仅仅描述和这个问题相关的内容,其它内容不进行扩展:

    • 云端部署一个服务,我们称之为server,或者服务端。
    • 我们的客户,把我们的客户端部署在自己的企业局域网内部,启动的时候,客户端使用websocket和服务端建立起长连接。
    • 连接成功以后,服务端会记录这个客户端为:在线。离线也是如此。后续在云端通过维持的长连接去和客户端进行沟通。
    • 为了保持长连接一直畅通,端之间会一直保持心跳(比如5秒),就是发送一个简单的报文,告诉服务端我还活着,同时服务端那边也会维护一个客户端的socket实例,当每个客户端心跳一次的话,服务器会更新这个客户端最后一次心跳时间,而且服务端也会有一个心跳的最大超时时间(以下简称:心跳超时),服务端会有定时任务,每隔心跳超时的时间,去判断上一次心跳时间距离现在是否已经超过了心跳超时,如果超过了,说明客户端已经出异常,强制主动关闭socket,网关记为离线。
    • 心跳超时的大小肯定要大于客户端的心跳间隔,但也不能太大,也不能太小;太大会导致可能维护的是一个早都离线的长连接,影响业务;太小会导致比较大数据量的报文传输堵塞通道,短时间内不心跳的话,会被强制关闭。
    • 企业內部的网络环境是不太稳定的,客户端与服务端的连接,随时可能断开,所以客户端那边也会有断线自动重连的机制,比如断线后,每隔6秒去尝试重连。

    问题是什么呢?

    客户端的在线状态特别重要,云端所有的业务操作都将可能依赖于数据库中存储的客户端状态。直接来看,有两种异常:

    1. 假在线:库里面是在线,实际上客户端是离线的。
    2. 假离线:库里面是离线,实际上客户端是在线的。

    假在线的问题,影响倒不是特别大。
    假离线的问题,影响还是蛮严重的,我们现在发现的也是这个问题,同时前人估计也是发现了这个假离线的问题,想了一些办法,在底层的调用逻辑里面,关于这个假离线做了一定程度的容错处理。比如发现是离线的,就实时的通过长连接去判断一次,到底是不是真的离线的话,不是的话,业务照常走,同时自动把数据库状态修复一下。

    知道有这么个事情,但是不知道怎么发生的,也更不知道怎么去解决了,提供容错率是最好的解决方案了。这也是解决问题里面非常有效的办法了,从侧面去补偿,要得就是很快解决问题,但不一定是最合理的方案。

    假离线到底是怎么来的?

    上面说过几个点,再来简述一下:

    1. 客户端所处环境网络极度不稳定,因为中间的各种网闸代理商会特别多。
    2. 不稳定,就会断线。
    3. 只是简单的网络波动的话,6秒就会重连成功。
    4. 客户端心跳间隔是5秒,服务端心跳超时是30秒。

    看着好像没有什么问题?

    本来想举个形象的例子,但是网络这块有些事情可能不完全清楚,所以担心举的例子表达偏了,直接结论吧,重点展示3种场景。

    1. 网络正常情况下。
    2. 客户端主动关闭应用而断开长连接。
    3. 客户端因为网络波动而短暂断开长连接;这种的会有问题。

    如下图,关于图片就不详细解释了,仔细看一下即可:
    在这里插入图片描述
    接下来再按照时间的趋势,从客户端、服务端、数据库状态以及客户端状态是否异常4个角度去分析一下:

    在这里插入图片描述
    这下问题就比较清晰了。但是问题来了,这种分析对不对呢?

    验证猜想

    在分析阶段,其实已经得到了这个结论,更准确地说,其实是猜想。基于这种猜想,也许有解决方案,但是似乎并没有验证方案呀,作为一个程序猿,没法测试的bug,那肯定是不行的。
    上面最难搞的肯定就是那个网络波动,本地该去怎么模拟呢?很快就有了答案,模拟步骤如下:

    1. 客户端启动。
    2. 电脑直接断网,用来模拟网络波动。
    3. 盯着客户端日志看,一旦发现长连接中断的日志后,立即恢复网络。
    4. 此时日志里面会显示连接成功,然后去云端数据库(或者界面)一直刷新,查看客户端的实时状态。大约30多秒后,客户端状态被置为了离线。

    不错,问题复现了。
    当时也是这样来证明上述猜想的。不过还有一个点,显示离线,万一是真离线呢,最好能够手段证明真的假离线,我们之前做了状态修正的功能,能够把假离线置为在线,所以也可以验证。

    解决问题

    想要解决一个问题,就要彻底地明确问题,已知的问题以及解决方案如下:
    在这里插入图片描述

    如何发现问题的呢?

    从某种程度上来说,发现问题的过程,比上面的更重要,也更复杂,需要从已知的仅有的各种数据上去分析,再加上自己过硬的专业知识,可能发现特别不可思议的事情,甚至可能需要突破固有的思维。

    想要发现问题,就必须有一个契机,一个可能和问题本身没有直接关系的现象,本次的就是。

    客户端离线预警

    客户端所处的网络环境是比较复杂的,所以会经常离线,因此我们做了监听到离线后,就会给相关人发送离线通知邮件,主要逻辑如下:

    1. 服务端监听到客户端离线的消息了,先把网关的数据库状态修改为离线,同时启动一个5分钟的延时任务。
    2. 5分钟后,判断当前客户端的数据库状态是否是在线,如果是在线,说明已经重连成功了,则什么都不做,结束。
    3. 如果不在线,说明5分钟了,还没有连接上,就发送邮件,并且记录下来。不用考虑一些特殊情况。比如连上了又断了等等,意义不大。

    上线这个功能以后,我们发现每天都会有400次左右的离线记录。客户端大约1500个。说实话,离线的次数还是非常多的。而且注意一点,基本只有超过离线5分钟的话,才会记录一次。而且专门找了一些离线次数特别多的客户,一天有几十次,甚至打电话咨询了一下,最近有没有什么异常,答案是:没有。

    奇怪的现象来了

    我们所统计的记录和我们所认知的那样,以及和实际情况发现对不上了。

    1. 客户端只要离线,业务一定会受到影响。
    2. 目前发现的离线统计,一定是离线超过了5分钟才会记录。
    3. 客户反馈说,业务没有受到影响。

    1+2可以推断出来的和实际的现象不符合,这个时候基本上已经意识到,哪里出了什么问题。客户的反馈应该没有问题,因为一旦有问题,肯定会联系我们的,但实际上没有找。

    该怎么去发现呢

    不知道是哪里的问题,但肯定是离线相关的,所以一定要从这个地方出发,关于离线,有这么几个事实:

    1. 客户端一旦离线,会打印一条错误日志。
    2. 客户端一旦连上,也会打印一条日志。
    3. 服务端发现客户端离线了,会打印一条日志。
    4. 服务端发现客户端连上了,会打印一条日志。

    基于上述,我联系了一家离线次数比较多的客户,收集了某一天的日志,同时把我们服务端的日志,从中找出这个客户的客户端的在线和离线日志。
    从日志里面,我发现这么几个现象:

    1. 客户端每次离线,下一次的重试就直接连上了,不会持续很长时间,确实只是网络波动。
    2. 客户端的每一次离线,每一次在线,和服务端的日志都是完全对上的。说明日志没有啥问题。
    3. 专门找了一个记录发送了邮件的离线记录,发现当它先离线后恢复以后的一个小时内,再也没有任何的离线记录了。但是邮件发出来了。

    第三个现象,其实已经和已有的知识,相悖了。前面提过,发送邮件的机制是:离线了,并且过了5分钟,还是离线的,才会发送。这个发送邮件了,意味当时库里面的状态一定是离线的!但是事实就是,在也没有离线了。
    那么问题来了:数据库状态改成了离线,而且是改错了。

    到底是谁改的库呢?

    当时的想法就是,当他重新连上的时候,一定会改成在线的,但是5分钟后,为什么又会变成了离线呢?所以一定有什么地方改了,思前想后,找到产品所有可能修改地方,但是那些都是由触发机制的,感觉从目前的现象来看,只有一网络波动,就会导致这个事情的发生,所以,当时这个问题就卡在这里了,想不明白了。

    一般想不明白的时候,说明已经进入歪道了,得换个脑子,或者找其他人一起过来讨论。我选择了后者,先说了一下我的所有发现以及问题。这个时候,同事注意到了一个关键的现象:

    每次离线和重新连上,服务端的日志,都是先在线,后离线。
    

    怎么会是这样呢?肯定是先离线,后在线呀。先在线,后离线,时间差了一丢丢而已。这个非常重要的发现!

    我刚开始再分析的时候,我也发现了,因为时间相差不大,不到1分钟,所以我很草率地认为是服务器时间不一致导致的,因为服务端是集群,日志都是随机打在节点上面的。这一点误导了自己!

    接下来找了好几个先在线,后离线的情况。把它们全放在一起,做了对比:

    2020-08-17 10:35:54,518 INFO  com.test.Handler - client: xxx is OFFLINE now
    2020-08-17 10:36:28,772 INFO  com.test.Handler - client: xxx is ONLINE now
    
    2020-08-17 11:12:04,747 INFO  com.test.Handler - client: xxx is ONLINE now
    2020-08-17 11:12:34,569 INFO  com.test.Handler - client: xxx is OFFLINE now
    
    2020-08-17 11:44:04,227 INFO  com.test.Handler - client: xxx is ONLINE now
    2020-08-17 11:44:32,707 INFO  com.test.Handler - client: xxx is OFFLINE now
    

    时间差的范围刚好是30秒左右!当发现这个的时候,30秒,我非常熟悉呀,是服务端定义的客户端心跳的最大间隔。这一下子就立马把所有的事情串了起来,得出了前文提到的所有的猜想,然后进行了模拟和验证。

    问题反思

    解决问题的思路很重要,多做总结和反思,会提高自己解决问题的能力,不断地去优化自己的思考方式。
    这个问题困扰了大半年了,这次才因为邮件的问题,和这件事情本身没有啥问题,为契机,才发现了这个问题。

    这次的问题是,客户端离线了,客户端发现了,服务端没有发现。
    那么有没有另外一种情况,服务端发现了,但是客户端没有发现呢。
    还真的有,这种情况非常极端吧,从网络层面上,我这边还没有特别好的解释,线上一千多家客户,目前我们只发现了一家出现了,这个现象比假离线的问题更加严重,客户端没有发现,意味着,不会重连,不重连的话,那一直就是断开,这个就需要另外一种心跳机制去做了。也可以解决。
    有空的话,也可以分享一下这个过程,也算是比较复杂,和今天的现象是另外一回事。

    展开全文
  • websocket相关

    2019-02-14 16:23:00
    之前使用过websocket,一直没彻底搞明白,趁着今天没事,总结下websocket相关。 ... ...在传输层使用TCP协议,在网络层使用IP协议。IP协议主要解决网络路由和寻址问题,TCP协议主要解决如何在IP层之上可靠的传递数据包...

      之前使用过websocket,一直没彻底搞明白,趁着今天没事,总结下websocket相关。

    HTTP(HyperText Transfer Protocol 超文本传输协议)

      http被设计用于web客户端和服务端通信,也可以用于其他目的,是应用层协议。在传输层使用TCP协议,在网络层使用IP协议。IP协议主要解决网络路由和寻址问题,TCP协议主要解决如何在IP层之上可靠的传递数据包,使在网络上的另一端收到发端发出的所有包,并且顺序与发出顺序一致。TCP有可靠,面向连接的特点。

      http协议是无状态的,即对事务的处理没有记忆能力,无法记录客户端状态,通俗点就是http请求下次会完全忘了上次干了什么(谁发起的请求,请求内容是什么)。

      HTTP的生命周期通过Request来界定,也就是一个Request 一个Response,那么HTTP1.0,这次HTTP请求就结束了。
    在HTTP1.1中进行了改进,使得有一个keep-alive,也就是说,在一个HTTP连接中,可以发送多个Request,接收多个Response。
    但是请记住 Request = Response , 在HTTP中永远是这样,也就是说一个request只能有一个response。而且这个response也是被动的,不能主动发起。

    http的长连接和短连接

      上文有提到http1.1的keep-alive,在HTTP/1.0中,默认使用的是短连接。也就是说,浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接。如果客户端浏览器访问的某个HTML或其他类型的 Web页中包含有其他的Web资源,如JavaScript文件、图像文件、CSS文件等;当浏览器每遇到这样一个Web资源,就会建立一个HTTP会话。

    但从 HTTP/1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头有加入这行代码:

    Connection:keep-alive

    在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的 TCP连接不会关闭,如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。实现长连接要客户端和服务端都支持长连接。

    HTTP协议的长连接和短连接,实质上是TCP协议的长连接和短连接。

    轮询和长轮询

      在h5推出websocket之前,为了实现状态更新或者服务端推送,主要采用轮询或者长轮询。轮询顾名思义,客户端定时循环去询问服务端,来获取最新的消息。长轮询主要实现方式有comet,基本原理就是基于http长连接,在客户端访问之后,保持http通信状态,不做回复,当有新的消息的时候再做回复,客户端收到回复之后再次发起访问。

    Webscoket

      Websocket是一个持久化的协议,相对于HTTP这种非持久的协议来说。websocket需要http请求来实现与服务端的握手。通过webscoket协议可以建立起持久的通信通道。

    websocket有以下特点

    (1)建立在 TCP 协议之上,服务器端的实现比较容易。

    (2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。

    (3)数据格式比较轻量,性能开销小,通信高效。

    (4)可以发送文本,也可以发送二进制数据。

    (5)没有同源限制,客户端可以与任意服务器通信。

    (6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

      之前的http短连接轮询方式,需要服务器有很快的处理速度和资源,由于http是无状态协议,长轮询每次访问都要重新解析请求头信息,验证请求头信息,同样很消耗资源。websocket实现了真正的服务端推送,建议与服务端直接交流的通道。

      

    展开全文
  • 简单的说,WebSocket协议之前,双工通信是通过多个http链接来实现,这导致了效率低下,而WebSocket解决了这个问题。1.1 思考: 传统web的请求和响应模式中, 我们如何实现实时信息传输, 如何实现服

    百度百科中这样定义WebSocket:WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。简单的说,WebSocket协议之前,双工通信是通过多个http链接来实现,这导致了效率低下,而WebSocket解决了这个问题。

    1.1 思考: 传统web的请求和响应模式中, 我们如何实现实时信息传输, 如何实现服务器反推数据?
    
        在浏览器中通过http仅能实现单向的通信,comet可以一定程度上模拟双向通信,但效率较低,并需要服务器有较好的支持; flash中的socket和xmlsocket可以实现真正的双向通信,通过 flex ajax bridge,可以在javascript中使用这两项功能.
    
        可以预见,如果websocket一旦在浏览器中得到实现,将会替代上面两项技术,得到广泛的使用.面对这种状况,HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽并达到实时通讯。在JavaEE7中也实现了WebSocket协议。
    

    1.2 WebSocket的目标:**打破传统的web请求响应模型, 实现管道式的实时通信。打开一个浏览器和服务器的通信通道,持续连接! 服务器给浏览器推送数据 非常方便!web的实时消息通信: 聊天,股票,游戏,监控等等。**
    
    1.3  软件需求
            (1). 安装jdk7 或更高版本
            (2). 下载tomcat7   两者保持一致(32、64位)
    
    
    2.1 实现一个webSocket应用程序,我们要学会几个基本操作。
            (1).  开启连接
            (2).  客户端给服务器端发送数据
            (3).  服务器端接收数据
            (4).  服务器端给客户端发送数据
            (5).  客户端接收数据
            (6).  监听三类基本事件: onopen,onmessage,onclose
    
            提示: onmessage 是发送数据时的响应事件。onopen是打开连接时的响应事件。onclose是关闭连接时的响应事件。
    

    3.1 接下来正式实现聊天室功能,首先写一个登录页面

    <%@ page language="java" contentType="text/html; charset=utf-8"
        pageEncoding="utf-8"%>
    <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
    <html>
    <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>Insert title here</title>
    <script type="text/javascript"  src="jquery-1.4.4.min.js"></script>
    </head>
    <body>
        <form  name="ff"  action="LoginServlet" method="post" >
            用户名:<input name="username"  /><br/>
            <input type="submit"  value="登录"/>
        </form>
    </body>
    </html>

    3.2 在web.xml配置LoginServlet

    <servlet>
        <description></description>
        <display-name>LoginServlet</display-name>
        <servlet-name>LoginServlet</servlet-name>
        <servlet-class>com.bjsxt.servlet.LoginServlet</servlet-class>
      </servlet>
      <servlet-mapping>
        <servlet-name>LoginServlet</servlet-name>
        <url-pattern>/LoginServlet</url-pattern>
      </servlet-mapping>

    3.3 添加LoginServlet,用于处理登录请求以及跳转到聊天室界面

    package com.bjsxt.servlet;
    
    import java.io.IOException;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    public class LoginServlet extends HttpServlet {
    
        public void doGet(HttpServletRequest request,
                  HttpServletResponse response)throws IOException,ServletException{
    
        }
    
    
        public void doPost(HttpServletRequest request,
                  HttpServletResponse response)throws IOException,ServletException{
    
            String username = request.getParameter("username");
            System.out.println("doPost当前登录用户为"+username);
            request.getSession().setAttribute("username",username);
            //这里只是简单地模拟登录,登陆之后直接跳转到聊天页面
            response.sendRedirect("chat.jsp");
        }
    }
    

    3.4 添加聊天界面

    <%@ page language="java" contentType="text/html; charset=utf-8"
        pageEncoding="utf-8"%>
    <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
    <html>
    <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>Insert title here</title>
    <script type="text/javascript" src="jquery-1.4.4.min.js"></script>
    <script type="text/javascript">
        var ws;
        var userName = ${sessionScope.username};
        //通过URL请求服务端(chat为项目名称)
        var url = "ws://localhost:8080/chat/chatSocket?username=${sessionScope.username}";
    
        //进入聊天页面就是一个通信管道
        window.onload = function() {
            if ('WebSocket' in window) {
                ws = new WebSocket(url);
            } else if ('MozWebSocket' in window) {
                ws = new MozWebSocket(url);
            } else {
                alert('WebSocket is not supported by this browser.');
                return;
            }
    
            //监听服务器发送过来的所有信息
            ws.onmessage = function(event) {
                eval("var result=" + event.data);
    
                //如果后台发过来的alert不为空就显示出来
                if (result.alert != undefined) {
                    $("#content").append(result.alert + "<br/>");
                }
    
                //如果用户列表不为空就显示
                if (result.names != undefined) {
                    //刷新用户列表之前清空一下列表,免得会重复,因为后台只是单纯的添加
                    $("#userList").html("");
                    $(result.names).each(
                            function() {
                                $("#userList").append(
                                        "<input  type=checkbox  value='"+this+"'/>"
                                                + this + "<br/>");
                            });
                }
    
                //将用户名字和当前时间以及发送的信息显示在页面上
                if (result.from != undefined) {
                    $("#content").append(
                            result.from + " " + result.date + " 说:<br/>"
                                    + result.sendMsg + "<br/>");
                }
    
            };
        };
    
        //将消息发送给后台服务器
        function send() {
            //拿到需要单聊的用户名
            //alert("当前登录用户为"+userName);
            var ss = $("#userList :checked");
            //alert("群聊还是私聊"+ss.size());
            var to = $('#userList :checked').val();
            if (to == userName) {
                alert("你不能给自己发送消息啊");
                return;
            }
            //根据勾选的人数确定是群聊还是单聊
            var value = $("#msg").val();
            //alert("消息内容为"+value);
            var object = null;
            if (ss.size() == 0) {
                object = {
                    msg : value,
                    type : 1, //1 广播 2单聊    
                };
            } else {
                object = {
                    to : to,
                    msg : value,
                    type : 2, //1 广播 2单聊    
                };
            }
            //将object转成json字符串发送给服务端
            var json = JSON.stringify(object);
            //alert("str="+json);
            ws.send(json);
            //消息发送后将消息栏清空
            $("#msg").val("");
        }
    </script>
    </head>
    <body>
    
        <h3>欢迎 ${sessionScope.username }使用本聊天系统!!</h3>
    
        <div id="content"
            style="border: 1px solid black; width: 400px; height: 300px; float: left; color: #7f3f00;"></div>
        <div id="userList"
            style="border: 1px solid black; width: 120px; height: 300px; float: left; color: #00ff00;"></div>
    
        <div style="clear: both;" style="color:#00ff00">
            <input id="msg" />
            <button onclick="send();">发送消息</button>
        </div>
    </body>
    </html>

    3.5 添加启动类

    package com.bjsxt.init;
    
    import java.util.Set;
    
    import javax.websocket.Endpoint;
    import javax.websocket.server.ServerApplicationConfig;
    import javax.websocket.server.ServerEndpointConfig;
    /**
     * 项目启动时会自动启动,类似与ContextListener.
     * 是webSocket的核心配置类。
     * @author xiongzichao
     *
     */
    public class ServerConfig implements ServerApplicationConfig {
    
        //扫描src下所有类@ServerEndPoint注解的类。
        @Override
        public Set<Class<?>> getAnnotatedEndpointClasses(Set<Class<?>> scan) {
            System.out.println("扫描到"+scan.size()+"个服务端程序");
            return scan;
        }
    
        //获取所有以接口方式配置的webSocket类。
        @Override
        public Set<ServerEndpointConfig> getEndpointConfigs(
                Set<Class<? extends Endpoint>> point) {
            System.out.println("实现EndPoint接口的类数量:"+point.size());
            return null;
        }
    
    }
    

    3.6 添加客户端发送给服务端消息实体

    package com.bjsxt.vo;
    /**
     * 客户端发送给服务端消息实体
     * @author xiongzichao
     *
     */
    public class ContentVo {
    
        private String to;
        private String msg;
        private Integer type;
        public String getTo() {
            return to;
        }
        public void setTo(String to) {
            this.to = to;
        }
        public String getMsg() {
            return msg;
        }
        public void setMsg(String msg) {
            this.msg = msg;
        }
        public Integer getType() {
            return type;
        }
        public void setType(Integer type) {
            this.type = type;
        }
    
    }
    

    3.7 添加服务端发送给客户端消息实体

    package com.bjsxt.vo;
    
    import java.util.Date;
    import java.util.List;
    
    /**
     * 服务端发送给客户端消息实体
     * @author xiongzichao
     *
     */
    public class Message {
    
        private  String  alert;   //
    
        private  List<String>  names;
    
        private  String  sendMsg;
    
        private String  from;
    
        private String  date;
    
    
        public String getDate() {
            return date;
        }
    
        public void setDate(String date) {
            this.date = date;
        }
    
        public String getSendMsg() {
            return sendMsg;
        }
    
        public void setSendMsg(String sendMsg) {
            this.sendMsg = sendMsg;
        }
    
        public String getFrom() {
            return from;
        }
    
        public void setFrom(String from) {
            this.from = from;
        }
    
        public String getAlert() {
            return alert;
        }
    
        public void setAlert(String alert) {
            this.alert = alert;
        }
    
        public List<String> getNames() {
            return names;
        }
    
        public void setNames(List<String> names) {
            this.names = names;
        }
    
        public Message() {
            super();
        }
    }
    

    3.8 添加服务端程序

    package com.bjsxt.server;
    
    import java.io.IOException;
    import java.util.ArrayList;
    import java.util.Date;
    import java.util.HashMap;
    import java.util.HashSet;
    import java.util.List;
    import java.util.Map;
    import java.util.Set;
    
    import javax.websocket.OnClose;
    import javax.websocket.OnMessage;
    import javax.websocket.OnOpen;
    import javax.websocket.Session;
    import javax.websocket.server.ServerEndpoint;
    
    import com.bjsxt.vo.ContentVo;
    import com.bjsxt.vo.Message;
    import com.google.gson.Gson;
    
    /**
     * 总通信管道
     * @author xiongzichao
     *
     */
    @ServerEndpoint("/chatSocket")
    public class ChatSocket {
    
        //定义一个全局变量集合sockets,用户存放每个登录用户的通信管道
        private  static  Set<ChatSocket>  sockets=new HashSet<ChatSocket>();
        //定义一个全局变量Session,用于存放登录用户的用户名
        private  Session  session;
        //定义一个全局变量map,key为用户名,该用户对应的session为value
        private  static  Map<String, Session>  map=new HashMap<String, Session>();
        //定义一个数组,用于存放所有的登录用户,显示在聊天页面的用户列表栏中
        private  static  List<String>names=new ArrayList<String>();
        private String username;
        private Gson  gson=new Gson();
    
        /*
         * 监听用户登录
         */
        @OnOpen
        public void open(Session session){
            this.session = session;
            //将当前连接上的用户session信息全部存到scokets中
            sockets.add(this);
            //拿到URL路径后面所有的参数信息
            String queryString = session.getQueryString();
            System.out.println();
            //截取=后面的参数信息(用户名),将参数信息赋值给全局的用户名
            this.username = queryString.substring(queryString.indexOf("=")+1);
            //每登录一个用户,就将该用户名存入到names数组中,用于刷新好友列表
            names.add(this.username);
            //将当前登录用户以及对应的session存入到map中
            this.map.put(this.username, this.session);
            System.out.println("用户"+this.username+"进入聊天室");
            Message message = new Message();
            message.setAlert("用户"+this.username+"进入聊天室");
            //将当前所有登录用户存入到message中,用于广播发送到聊天页面
            message.setNames(names);
            //将聊天信息广播给所有通信管道(sockets)
            broadcast(sockets, gson.toJson(message) );
        }
    
        /*
         * 退出登录
         */
        @OnClose
        public void close(Session session){
            //移除退出登录用户的通信管道
            sockets.remove(this);
            //将用户名从names中剔除,用于刷新好友列表
            names.remove(this.username);
            Message message = new Message();
            System.out.println("用户"+this.username+"退出聊天室");
            message.setAlert(this.username+"退出当前聊天室!!!");
            //刷新好友列表
            message.setNames(names);
            broadcast(sockets, gson.toJson(message));
        }
    
        /*
         * 接收客户端发送过来的消息,然后判断是广播还是单聊
         */
        @OnMessage
        public void receive(Session  session,String msg) throws IOException{
            //将客户端消息转成json对象
            ContentVo vo = gson.fromJson(msg, ContentVo.class);
            //如果是群聊,就像消息广播给所有人
            if(vo.getType()==1){
                Message message = new Message();
                message.setDate(new Date().toLocaleString());
                message.setFrom(this.username);
                message.setSendMsg(vo.getMsg());
                broadcast(sockets, gson.toJson(message));
            }else{
                Message message = new Message();
                message.setDate(new Date().toLocaleString());
                message.setFrom(this.username);
                message.setAlert(vo.getMsg());
                message.setSendMsg("<font color=red>正在私聊你:</font>"+vo.getMsg());
                String to  = vo.getTo();
                //根据单聊对象的名称拿到要单聊对象的Session
                Session to_session = this.map.get(to);
                //如果是单聊,就将消息发送给对方
                to_session.getBasicRemote().sendText(gson.toJson(message));
            }
        }
    
        /*
         * 广播消息
         */
        public void broadcast(Set<ChatSocket>sockets ,String msg){
            //遍历当前所有的连接管道,将通知信息发送给每一个管道
            for(ChatSocket socket : sockets){
                try {
                    //通过session发送信息
                    socket.session.getBasicRemote().sendText(msg);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

    4.0 实现界面
    具体实现界面如图

    注意

        这里一共需要三个jar包,分别是WebSocket-api.jar这个定义webSocket应用程序开发接口!tomcat7-webSocket.jar tomcat服务器对于webSocket接口的实现!!还有一个gson.jar用于序列化实体类信息。前面两个jar包在tomcat7的lib目录里面可以找到。
    

    总结

        这样就实现了一个简单的聊天室以及单聊功能,用到的API不是很多,主要是几个事件的监听与广播的使用。由于本人是第一次写博客,如有错误的地方,还请各位大牛指点一二,定感激不尽!我是話不哆先森,我爱编程,编程使我快乐!
    
    展开全文
  • IP协议主要解决网络路由和寻址问题,TCP协议主要解决如何在IP层之上可靠的传递数据包,使在网络上的另一端收到发端发出的所有包,并且顺序与发出顺序一致。TCP有可靠,面向连接的特点。 长连接短连接操作过程 短...
  • Web实时通信技术即消息推送机制- 简要介绍短连接 长连接 轮询 长轮询 SSE WebSocket?...其中IP协议主要解决网络路由和寻址的问题,TCP协议主要解决如何在IP层之上传输可靠的数据包。 1.2 如何理解HTTP协议
  • 最近在修改网狐框架时,对底层socket改成websocket适合...然而这些信息中间会夹杂乱码,一开始无论如何都找不到乱码原因,从分析自己对网络发包进行处理的那段代码开始查,可问题依旧,最后的最后,发现当只启动登...
  • IP协议主要解决网络路由和寻址问题,TCP协议主要解决如何在IP层之上可靠的传递数据包,使在网络上的另一端收到发端发出的所有包,并且顺序与发出顺序一致。TCP有可靠,面向连接的特点。 长连接短连接操作过程 短...
  • 该项目展示了针对常见用户问题解决方案,例如如何使用Amazon Cognito对IoT设备进行身份验证。 目录 产品特点 注册用户 登录/注销 创建房间 加盟室 实时聊天 未读消息指示器 展示的AWS服务 在Web客户端之间交换...
  • 在网络层使用IP协议,主要解决网络路由和寻址问题; HTTP把TCP分割好的各种数据包封装到IP包里面传送给接收方。 二.短连接、长连接、websocket、postmessage作用 1.短连接:(占用较多内存和带宽)。 在HTTP/1.0...
  • IP协议主要解决网络路由和寻址问题,TCP协议主要解决如何在IP层之上可靠地传递数据包,使得网络上接收端收到发送端所发出的所有包,并且顺序与发送顺序一致。TCP协议是可靠的、面向连接的。...
  • HTTP属于应用层协议,主要解决如何包装数据; 在传输层使用TCP协议,主要...在网络层使用IP协议,主要解决网络路由和寻址问题; HTTP把TCP分割好的各种数据包封装到IP包里面传送给接收方。 二.短连接、长连接、we...
  • 使用无监督学习可以解决网络问题。 然后,我想训练一个网络,使用监督学习对自动编码器输入和输出的比较结果进行分类。 我的个人目标是建立一个整体网络,可以将一些历史数据收集到其中,并观察它如何与其他...
  • 我们看看遇到自动刷新问题应该如何排除 <h2>livereload server启动问题 <ol><li>需要确认fis release时使用了 <code>--watch --live</code> 参数</li></ol> 在开启watch与live后,命令行或Shell会被Hold住&#...
  • 平常开发游戏逻辑也得开启一大堆进程,不仅启动慢,而且查找问题及其不方便,要在一堆堆日志里面查问题,这感觉非常糟糕,这么多年也没人解决这个问题。ET框架使用了类似守望先锋的组件设计,所有服务端内容都拆成了...
  • 解决solr更新索引报错问题 200-webflux 【web】WebFlux实例 React 201-web 【web】basic http实例 springmvc 202-web-params 【web】请求参数解析的各种姿势 get参数解析post参数解析自定义参数解析...
  • 该库的独特价值在于其用于解决不规则并行问题的加速基元。 NCCL:用于集体多GPU通信的优化基元。 OpenCL :并行编程的异构系统的开放标准。 OpenMP:OpenMP API。 SObjectizer:实现Actor、Publish-Subscribe和...
  • 已经支持用户会话上下文(解决服务器无法使用 Session 处理用户信息的问题)。 已经全面支持微信公众号、小程序、企业号(企业微信)、微信支付、开放平台的最新 API。 已经支持分布式缓存及缓存策略扩展(默认支持...
  • FAQ(持续更新)

    2021-01-08 12:27:51
    <div><h3>项目背景以及解决问题 <p>C++ Workflow项目起源于搜狗公司的分布式存储项目的通讯引擎,并且发展成为搜狗公司级C++标准,应用于搜狗大多数C++后端服务。项目将通讯...
  •  Java生成密钥、保存密钥的实例源码,通过本源码可以了解到Java如何产生单钥加密的密钥(myKey)、产生双钥的密钥对(keyPair)、如何保存公钥的字节数组、保存私钥到文件privateKey.dat、如何用Java对象序列化保存私钥...
  • JAVA上百实例源码以及开源项目

    千次下载 热门讨论 2016-01-03 17:37:40
     Java生成密钥、保存密钥的实例源码,通过本源码可以了解到Java如何产生单钥加密的密钥(myKey)、产生双钥的密钥对(keyPair)、如何保存公钥的字节数组、保存私钥到文件privateKey.dat、如何用Java对象序列化保存私钥...
  • ** 如果使用过程中有遇到问题,可以先去FAQ查找是否有解决方法 ** 对于VIVO设备,如果在开发者选项中包含“USB安全操作”,需要手动进行开启,否则录制回放与一机多控功能可能会无法正常操作 对于小米设备,需要开启...
  • 【JavaWeb基础】图书管理系统【部署开发环境、解决分类、图书、前台页面模块】 【JavaWeb基础】图书管理系统【用户、购买、订单模块、添加权限】 【JavaWeb基础】图书管理系统总结 :hamburger:Hibernate ...
  • 本文不是一篇关于如何学习微信小程序的入门指南,也非参考手册,只是一些资料的整理。 本仓库中的资料整理自网络,也有一些来自网友的推荐。在这里可以看到项目贡献者的完整名单。 如果这个仓库对你有帮助,欢迎 ...

空空如也

空空如也

1 2
收藏数 34
精华内容 13
关键字:

websocket如何解决网络问题