-
websocket如何区分用户_WebSocket与普通Socket的差异
2021-01-02 08:57:381.背景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协议没什么关系,两者关系如下所示:
两者有交集,但不存在包含关系。
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协议,而不是mozillasocket,lurnarsocket或者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长连接因为网络波动而导致客户端的“假离线”---问题发现、分析到解决
2020-08-24 21:16:54验证猜想解决问题如何发现问题的呢? 简介 这次分享是在混合云场景下,基于websocket长连接,实现Server-Client(多个)架构模式中,云服务需要维护客户端的状态,但是云端维护的状态可能和实际的客户端的状态不一致...简介
这次分享是在混合云场景下,基于websocket长连接,实现Server-Client(多个)架构模式中,云服务需要维护客户端的状态,但是云端维护的状态可能和实际的客户端的状态不一致,可能就会导致一些奇怪的事情发生,比较有意思的一个问题吧,非常不容易发现的一个问题,必须需要一个合适的契机才可以去发现。前面直接描述问题和解决方案,后面用一定的篇幅详细讲述一下怎么发现的这个问题。
问题的现象、场景和解决方案
基本的部署架构
如下图:
比较简单,但也比较重要,这是看懂本问题以及解决的基本知识,但是仅仅描述和这个问题相关的内容,其它内容不进行扩展:- 云端部署一个服务,我们称之为server,或者服务端。
- 我们的客户,把我们的客户端部署在自己的企业局域网内部,启动的时候,客户端使用websocket和服务端建立起长连接。
- 连接成功以后,服务端会记录这个客户端为:在线。离线也是如此。后续在云端通过维持的长连接去和客户端进行沟通。
- 为了保持长连接一直畅通,端之间会一直保持心跳(比如5秒),就是发送一个简单的报文,告诉服务端我还活着,同时服务端那边也会维护一个客户端的socket实例,当每个客户端心跳一次的话,服务器会更新这个客户端最后一次心跳时间,而且服务端也会有一个心跳的最大超时时间(以下简称:心跳超时),服务端会有定时任务,每隔心跳超时的时间,去判断上一次心跳时间距离现在是否已经超过了心跳超时,如果超过了,说明客户端已经出异常,强制主动关闭socket,网关记为离线。
- 心跳超时的大小肯定要大于客户端的心跳间隔,但也不能太大,也不能太小;太大会导致可能维护的是一个早都离线的长连接,影响业务;太小会导致比较大数据量的报文传输堵塞通道,短时间内不心跳的话,会被强制关闭。
- 企业內部的网络环境是不太稳定的,客户端与服务端的连接,随时可能断开,所以客户端那边也会有断线自动重连的机制,比如断线后,每隔6秒去尝试重连。
问题是什么呢?
客户端的在线状态特别重要,云端所有的业务操作都将可能依赖于数据库中存储的客户端状态。直接来看,有两种异常:
- 假在线:库里面是在线,实际上客户端是离线的。
- 假离线:库里面是离线,实际上客户端是在线的。
假在线的问题,影响倒不是特别大。
假离线的问题,影响还是蛮严重的,我们现在发现的也是这个问题,同时前人估计也是发现了这个假离线的问题,想了一些办法,在底层的调用逻辑里面,关于这个假离线做了一定程度的容错处理。比如发现是离线的,就实时的通过长连接去判断一次,到底是不是真的离线的话,不是的话,业务照常走,同时自动把数据库状态修复一下。知道有这么个事情,但是不知道怎么发生的,也更不知道怎么去解决了,提供容错率是最好的解决方案了。这也是解决问题里面非常有效的办法了,从侧面去补偿,要得就是很快解决问题,但不一定是最合理的方案。
假离线到底是怎么来的?
上面说过几个点,再来简述一下:
- 客户端所处环境网络极度不稳定,因为中间的各种网闸代理商会特别多。
- 不稳定,就会断线。
- 只是简单的网络波动的话,6秒就会重连成功。
- 客户端心跳间隔是5秒,服务端心跳超时是30秒。
看着好像没有什么问题?
本来想举个形象的例子,但是网络这块有些事情可能不完全清楚,所以担心举的例子表达偏了,直接结论吧,重点展示3种场景。
- 网络正常情况下。
- 客户端主动关闭应用而断开长连接。
- 客户端因为网络波动而短暂断开长连接;这种的会有问题。
如下图,关于图片就不详细解释了,仔细看一下即可:
接下来再按照时间的趋势,从客户端、服务端、数据库状态以及客户端状态是否异常4个角度去分析一下:
这下问题就比较清晰了。但是问题来了,这种分析对不对呢?验证猜想
在分析阶段,其实已经得到了这个结论,更准确地说,其实是猜想。基于这种猜想,也许有解决方案,但是似乎并没有验证方案呀,作为一个程序猿,没法测试的bug,那肯定是不行的。
上面最难搞的肯定就是那个网络波动,本地该去怎么模拟呢?很快就有了答案,模拟步骤如下:- 客户端启动。
- 电脑直接断网,用来模拟网络波动。
- 盯着客户端日志看,一旦发现长连接中断的日志后,立即恢复网络。
- 此时日志里面会显示连接成功,然后去云端数据库(或者界面)一直刷新,查看客户端的实时状态。大约30多秒后,客户端状态被置为了离线。
不错,问题复现了。
当时也是这样来证明上述猜想的。不过还有一个点,显示离线,万一是真离线呢,最好能够手段证明真的假离线,我们之前做了状态修正的功能,能够把假离线置为在线,所以也可以验证。解决问题
想要解决一个问题,就要彻底地明确问题,已知的问题以及解决方案如下:
如何发现问题的呢?
从某种程度上来说,发现问题的过程,比上面的更重要,也更复杂,需要从已知的仅有的各种数据上去分析,再加上自己过硬的专业知识,可能发现特别不可思议的事情,甚至可能需要突破固有的思维。
想要发现问题,就必须有一个契机,一个可能和问题本身没有直接关系的现象,本次的就是。
客户端离线预警
客户端所处的网络环境是比较复杂的,所以会经常离线,因此我们做了监听到离线后,就会给相关人发送离线通知邮件,主要逻辑如下:
- 服务端监听到客户端离线的消息了,先把网关的数据库状态修改为离线,同时启动一个5分钟的延时任务。
- 5分钟后,判断当前客户端的数据库状态是否是在线,如果是在线,说明已经重连成功了,则什么都不做,结束。
- 如果不在线,说明5分钟了,还没有连接上,就发送邮件,并且记录下来。不用考虑一些特殊情况。比如连上了又断了等等,意义不大。
上线这个功能以后,我们发现每天都会有400次左右的离线记录。客户端大约1500个。说实话,离线的次数还是非常多的。而且注意一点,基本只有超过离线5分钟的话,才会记录一次。而且专门找了一些离线次数特别多的客户,一天有几十次,甚至打电话咨询了一下,最近有没有什么异常,答案是:没有。
奇怪的现象来了
我们所统计的记录和我们所认知的那样,以及和实际情况发现对不上了。
- 客户端只要离线,业务一定会受到影响。
- 目前发现的离线统计,一定是离线超过了5分钟才会记录。
- 客户反馈说,业务没有受到影响。
1+2可以推断出来的和实际的现象不符合,这个时候基本上已经意识到,哪里出了什么问题。客户的反馈应该没有问题,因为一旦有问题,肯定会联系我们的,但实际上没有找。
该怎么去发现呢
不知道是哪里的问题,但肯定是离线相关的,所以一定要从这个地方出发,关于离线,有这么几个事实:
- 客户端一旦离线,会打印一条错误日志。
- 客户端一旦连上,也会打印一条日志。
- 服务端发现客户端离线了,会打印一条日志。
- 服务端发现客户端连上了,会打印一条日志。
基于上述,我联系了一家离线次数比较多的客户,收集了某一天的日志,同时把我们服务端的日志,从中找出这个客户的客户端的在线和离线日志。
从日志里面,我发现这么几个现象:- 客户端每次离线,下一次的重试就直接连上了,不会持续很长时间,确实只是网络波动。
- 客户端的每一次离线,每一次在线,和服务端的日志都是完全对上的。说明日志没有啥问题。
- 专门找了一个记录发送了邮件的离线记录,发现当它先离线后恢复以后的一个小时内,再也没有任何的离线记录了。但是邮件发出来了。
第三个现象,其实已经和已有的知识,相悖了。前面提过,发送邮件的机制是:离线了,并且过了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实现一个简单的聊天室以及单聊功能
2017-11-08 12:18:28简单的说,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不是很多,主要是几个事件的监听与广播的使用。由于本人是第一次写博客,如有错误的地方,还请各位大牛指点一二,定感激不尽!我是話不哆先森,我爱编程,编程使我快乐!
-
网络基础06 HTTP长连接和短连接 + Websocket
2017-06-23 12:34:43IP协议主要解决网络路由和寻址问题,TCP协议主要解决如何在IP层之上可靠的传递数据包,使在网络上的另一端收到发端发出的所有包,并且顺序与发出顺序一致。TCP有可靠,面向连接的特点。 长连接短连接操作过程 短... -
【Java网络编程与IO流】Web实时通信技术即消息推送机制- 简要介绍短连接 长连接 轮询 长轮询 SSE WebSocket...
2020-11-24 21:41:35Web实时通信技术即消息推送机制- 简要介绍短连接 长连接 轮询 长轮询 SSE WebSocket?...其中IP协议主要解决网络路由和寻址的问题,TCP协议主要解决如何在IP层之上传输可靠的数据包。 1.2 如何理解HTTP协议 -
网狐 socket修改成websocket注意
2017-09-09 10:07:00最近在修改网狐框架时,对底层socket改成websocket适合...然而这些信息中间会夹杂乱码,一开始无论如何都找不到乱码原因,从分析自己对网络发包进行处理的那段代码开始查,可问题依旧,最后的最后,发现当只启动登... -
HTTP长连接和短连接 + Websocket
2018-03-14 18:31:00IP协议主要解决网络路由和寻址问题,TCP协议主要解决如何在IP层之上可靠的传递数据包,使在网络上的另一端收到发端发出的所有包,并且顺序与发出顺序一致。TCP有可靠,面向连接的特点。 长连接短连接操作过程 短... -
aws-iot-chat-example::speech_balloon:通过WebSocket协议通过MQTT使用AWS IoT平台的聊天应用程序-源码
2021-02-05 17:08:38该项目展示了针对常见用户问题的解决方案,例如如何使用Amazon Cognito对IoT设备进行身份验证。 目录 产品特点 注册用户 登录/注销 创建房间 加盟室 实时聊天 未读消息指示器 展示的AWS服务 在Web客户端之间交换... -
前端面试题必考(四)- HTTP短连接,长连接(keep-alive),websocket,postmessage
2019-03-09 15:36:32在网络层使用IP协议,主要解决网络路由和寻址问题; HTTP把TCP分割好的各种数据包封装到IP包里面传送给接收方。 二.短连接、长连接、websocket、postmessage作用 1.短连接:(占用较多内存和带宽)。 在HTTP/1.0... -
http、websocket、长连接、短连接(一)
2018-07-06 19:28:14IP协议主要解决网络路由和寻址问题,TCP协议主要解决如何在IP层之上可靠地传递数据包,使得网络上接收端收到发送端所发出的所有包,并且顺序与发送顺序一致。TCP协议是可靠的、面向连接的。... -
HTTP短连接,长连接(keep-alive),websocket,postmessage
2020-03-27 23:32:05HTTP属于应用层协议,主要解决如何包装数据; 在传输层使用TCP协议,主要...在网络层使用IP协议,主要解决网络路由和寻址问题; HTTP把TCP分割好的各种数据包封装到IP包里面传送给接收方。 二.短连接、长连接、we... -
pomalu-ale-jiste:...我想用AI玩股市赌徒游戏;-)-源码
2021-02-27 19:31:59使用无监督学习可以解决此网络问题。 然后,我想训练一个网络,使用监督学习对自动编码器输入和输出的比较结果进行分类。 我的个人目标是建立一个整体网络,可以将一些历史数据收集到其中,并观察它如何与其他... -
[FAQ] 自动刷新功能失效的排查步骤
2021-01-08 01:25:03我们看看遇到自动刷新问题应该如何排除 <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上百实例源码以及开源项目源代码
2016-09-17 21:58:33Java生成密钥、保存密钥的实例源码,通过本源码可以了解到Java如何产生单钥加密的密钥(myKey)、产生双钥的密钥对(keyPair)、如何保存公钥的字节数组、保存私钥到文件privateKey.dat、如何用Java对象序列化保存私钥... -
JAVA上百实例源码以及开源项目
2016-01-03 17:37:40Java生成密钥、保存密钥的实例源码,通过本源码可以了解到Java如何产生单钥加密的密钥(myKey)、产生双钥的密钥对(keyPair)、如何保存公钥的字节数组、保存私钥到文件privateKey.dat、如何用Java对象序列化保存私钥... -
** 如果使用过程中有遇到问题,可以先去FAQ查找是否有解决方法 ** 对于VIVO设备,如果在开发者选项中包含“USB安全操作”,需要手动进行开启,否则录制回放与一机多控功能可能会无法正常操作 对于小米设备,需要开启...
-
【JavaWeb基础】图书管理系统【部署开发环境、解决分类、图书、前台页面模块】 【JavaWeb基础】图书管理系统【用户、购买、订单模块、添加权限】 【JavaWeb基础】图书管理系统总结 :hamburger:Hibernate ...
-
本文不是一篇关于如何学习微信小程序的入门指南,也非参考手册,只是一些资料的整理。 本仓库中的资料整理自网络,也有一些来自网友的推荐。在这里可以看到项目贡献者的完整名单。 如果这个仓库对你有帮助,欢迎 ...
-
客户端向日葵SunloginEnterprise_3.0.0.27372.exe
-
FTP 文件传输服务
-
gdal2.x生成terrain地形数据-程序及说明.7z
-
2016通信中级互联网真题.pdf
-
基于python的dango框架购物商城毕业设计毕设源代码使用教程
-
从Docker到Kubernetes之技术实战.pdf
-
vue3从0到1-超详细
-
搭建etcd集群时,报错etcd: request cluster ID mismatch错误解决,只适用于新建etcd集群或无数据集群
-
FFmpeg4.3系列之16:WebRTC之小白入门与视频聊天的实战
-
NFS 网络文件系统
-
MyBatisSelf.rar
-
2005-2020信息系统项目管理师历年真题(含上午题、案例分析、论文)试题和答案.rar
-
Kubernetes概述.pdf
-
GC如何判断对象可以被回收
-
机器学习可视化软件机器学习可视化软件
-
基于Qt的LibVLC开发教程
-
BGLightChangeDLL.zip
-
东南大学历年c++复试题.zip
-
Java进阶--编译时注解处理器(APT)详解
-
大数据开发之Hadoop学习7--HDFS客户端操作