WebSocket
关于websocket的⼀个⼩demo,是聊天室,源代码地址:
websocket的背景
现在,很多⽹站为了实现推送技术,所⽤的技术都是 Ajax 轮询或者long poll 。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然⽽HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很⼩的⼀部分,显然这样会浪费很多的带宽等资源。
websocket的特点
WebSocket 是 HTML5 开始提供的⼀种在单个 TCP 连接上进⾏全双⼯通讯的协议。能更好的节省服务器资源和带宽,并且能够更实时地进⾏通讯。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。
在 WebSocket API 中,浏览器和服务器只需要完成⼀次握⼿,两者之间就直接可以创建持久性的连接,并进⾏双向数据传输。
浏览器通过 JavaScript 向服务器发出建⽴ WebSocket 连接的请求,连接建⽴以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。
Ajax轮询
ajax轮询 的原理⾮常简单,让浏览器隔个⼏秒就发送⼀次请求,询问服务器是否有新信息。场景再现:
客户端:啦啦啦,有没有新信息(Request)服务端:没有(Response)
客户端:啦啦啦,有没有新消息(Request)服务端:好啦好啦,有啦给你。(Response)客户端:啦啦啦,有没有新消息(Request)
服务端:。。。。。没。。。。没。。。没有(Response) ---- loop
long poll
long poll 其实原理跟 ajax轮询 差不多,都是采⽤轮询的⽅式,不过采取的是阻塞模型(⼀直打电话,没收到就不挂电话),也就是说,客户端发起连接后,如果没消息,就⼀直不返回Response给客户端。直到有消息才返回,返回完之后,客户端再次建⽴连接,周⽽复始。场景再现:
客户端:啦啦啦,有没有新信息,没有的话就等有了才返回给我吧(Request)服务端:额。。 等待到有消息的时候。。来 给你(Response)
客户端:啦啦啦,有没有新信息,没有的话就等有了才返回给我吧(Request) -loop
从上⾯可以看出其实这两种⽅式,都是在不断地建⽴HTTP连接,然后等待服务端处理,可以体现HTTP协议的另外⼀个特点,被动性。何为被动性呢,其实就是,服务端不能主动联系客户端,只能有客户端发起。从上⾯很容易看出来,不管怎么样,上⾯这两种都是⾮常消耗资源的。ajax轮询 需要服务器有很快的处理速度和资源。(速度)
long poll 需要有很⾼的并发,也就是说同时接待客户的能⼒。(场地⼤⼩)所以ajax轮询 和long poll 缺点⾮常明显。
websocket 与Http的关系
WebSocket是HTML5出的东西(协议),也就是说HTTP协议没有变化,或者说没关系,但HTTP是不⽀持持久连接的(长连接,循环连接的不算)
⾸先HTTP有1.1和1.0之说,也就是所谓的keep-alive,把多个HTTP请求合并为⼀个,但是Websocket其实是⼀个新协议,跟HTTP协议基本没有关系,只是为了兼容现有浏览器的握⼿规范⽽已,也就是说它是HTTP协议上的⼀种补充,可以通过这样⼀张图理解⾸先,相对于HTTP这种⾮持久的协议来说,Websocket是⼀个持久化的协议。
HTTP还是⼀个⽆状态协议。通俗的说就是,服务器因为每天要接待太多客户了,是个健忘⿁,你⼀挂电话,他就把你的东西全忘光了,把你的东西全丢掉了。你第⼆次还得再告诉服务器⼀遍。
HTTP的⽣命周期通过Request来界定,也就是⼀个Request ⼀个Response,那么在HTTP1.0中,这次HTTP请求就结束了。
在HTTP1.1中进⾏了改进,使得有⼀个keep-alive,也就是说,在⼀个HTTP连接中,可以发送多个Request,接收多个Response。但是 Request = Response , 在HTTP中永远是这样,也就是说⼀个request只能有⼀个response。⽽且这个response也是被动的,不能主动发起。
所以在这种情况下出现了,Websocket出现了。他解决了HTTP的难题。
websocket协议建⽴
WebSocket并不是全新的协议,⽽是利⽤了HTTP协议来建⽴连接。我们来看看WebSocket连接是如何创建的。⾸先,WebSocket连接必须由浏览器发起,因为请求协议是⼀个标准的HTTP请求,格式如下:
GET ws://localhost:3000/ws/chat HTTP/1.1Host: localhost
Upgrade: websocketConnection: Upgrade
Origin: http://localhost:3000
Sec-WebSocket-Key: client-random-stringSec-WebSocket-Version: 13
该请求和普通的HTTP请求有⼏点不同:
1. GET请求的地址不是类似/path/,⽽是以ws://开头的地址;
2. 请求头Upgrade: websocket和Connection: Upgrade表⽰这个连接将要被转换为WebSocket连接;3. Sec-WebSocket-Key是⽤于标识这个连接,并⾮⽤于加密数据;4. Sec-WebSocket-Version指定了WebSocket的协议版本。服务器如果接受该请求,就会返回如下响应:
HTTP/1.1 101 Switching ProtocolsUpgrade: websocketConnection: Upgrade
Sec-WebSocket-Accept: server-random-string
该响应代码101表⽰本次连接的HTTP协议即将被更改,更改后的协议就是Upgrade: websocket指定的WebSocket协议。这⾥开始就是HTTP最后负责的区域了,告诉客户端,已经成功切换协议啦~
websocket的客户端简单实例
var ws = new WebSocket(\"wss://localhost:8080/ws/asset\");//连接建⽴成功调⽤的⽅法ws.onopen = function(evt) {
console.log(\"Connection open ...\"); //向服务端发送消息
ws.send(\"Hello WebSockets!\");};
//收到服务端消息后调⽤的⽅法ws.onmessage = function(evt) {
console.log( \"Received Message: \" + evt.data); ws.close();};
//连接关闭调⽤的⽅法
ws.onclose = function(evt) {
console.log(\"Connection closed.\");};
webSocket的服务端简单实例
@ServerEndpoint(value = \"/ws/asset\")@Component@Slf4j
public class WebSocketServer {
private static AtomicInteger OnlineCount = new AtomicInteger(0);
// concurrent包的线程安全Set,⽤来存放每个客户端对应的Session对象。
private static CopyOnWriteArraySet /** * 连接建⽴成功调⽤的⽅法 */ @OnOpen public void onOpen(Session session) { SessionSet.add(session); int cnt = OnlineCount.incrementAndGet(); // 在线数加1 log.info(\"有连接加⼊,当前连接数为:{}\ SendMessage(session, \"连接成功\"); } /** * 连接关闭调⽤的⽅法 */ @OnClose public void onClose(Session session) { SessionSet.remove(session); int cnt = OnlineCount.decrementAndGet(); log.info(\"有连接关闭,当前连接数为:{}\ } /** * 收到客户端消息后调⽤的⽅法 * * @param message * 客户端发送过来的消息 */ @OnMessage public void onMessage(String message, Session session) { log.info(\"来⾃客户端的消息:{}\ System.out.println(session.toString()); SendMessage(session, \"收到消息,消息内容:\"+message+session.getId()); } /** * 出现错误 * @param session * @param error */ @OnError public void onError(Session session, Throwable error) { log.error(\"发⽣错误:{},Session ID: {}\ error.printStackTrace(); } /** * 发送消息,实践表明,每次浏览器刷新,session会发⽣变化。 * @param session * @param message */ public static void SendMessage(Session session, String message) { try { session.getBasicRemote().sendText(message); } catch (IOException e) { log.error(\"发送消息出错:{}\ e.printStackTrace(); } } /** * 群发消息 * @param message * @throws IOException */ public static void BroadCastInfo(String message) throws IOException { for (Session session : SessionSet) { if(session.isOpen()){ SendMessage(session, message); } } } /** * 指定Session发送消息 * @param sessionId * @param message * @throws IOException */ public static void SendMessage(String message,String sessionId) throws IOException { Session session = null; for (Session s : SessionSet) { if(s.getId().equals(sessionId)){ session = s; break; } } if(session!=null){ SendMessage(session, message); } else{ log.warn(\"没有找到你指定ID的会话:{}\ } }} websocket的⼼跳机制 websockt⼼跳机制,不得不说很形象;那何为⼼跳机制,就是表明client与server的连接是否还在的检测机制; 如果不存在检测,那么⽹络突然断开,造成的后果就是client、server可能还在傻乎乎的发送⽆⽤的消息,浪费了资源;怎样检测呢?原理就是定时向server发送消息,如果接收到server的响应就表明连接依旧存在;这个⼼跳机制在分布式中也很常见, demo 聊天室demo (1)Client:客户端说明 客户端的代码主要是使⽤H5的WebSocket进⾏实现,在前端⽹页中使⽤WebSocket进⾏连接服务端,然后建⽴Socket连接进⾏通讯。(2)Server:服务端说明 服务端主要是建⽴多个客户端的关系,进⾏消息的中转等。客户端成功连接到服务端之后,就可以通过建⽴的通道进⾏发送消息到服务端,服务端接收到消息之后在群发给所有的客户端。(3)客户端和服务端连接 var websocket = new WebSocket(\"ws://localhost:8080/myWs\"); (4)客户端和服务端怎么发送消息? 客户端可以使⽤webSocket提供的send()⽅法,如下代码: var message = document.getElementById('text').value; websocket.send(message); 服务端怎么发送消息呢?主要是使⽤在成功建⽴连接的时候,创建的Session对象进⾏发送,如下代码: session.getAsyncRemote().sendText(\"恭喜您成功连接上WebSocket\"); (5)客户端和服务端怎么接受消息? 客户端接收消息消息使⽤的是websocket的onmessage回调⽅法,如下代码: websocket.onmessage = function(event) { //⽂本信息直接显⽰,如果是json信息,需要转换下在显⽰. var data = event.data; document.getElementById('message').innerHTML += data; } 服务端: @OnMessage public void onMessage(String message, Session session) { System.out.println(\"来⾃客户端的消息:\" + message); } (6)群聊原理(群发消息) 服务端在和客户端建⽴连接的时候,会创建⼀个webSocket对象,我们会将每个连接创建的对象进⾏报错到⼀个列表中,⽐如:CopyOnWriteArraySet(这是线程安全的);在要进⾏群发的时候,编写我们的列表对象进⾏群发消息。(7)单聊原理(⼀对⼀消息) 聊的时候,就⽆需遍历列表,⽽是需要知道发送者和接受者各⾃的Session对象,这个Session对象怎么获取呢?Session可以获取到 sessionId,发送者在发送消息的时候,携带接收消息的sessionId,那么问题就演变成了:发送者怎么知道接受者的sessionId,那就是加⼊⼀个在线⽤户列表即可,在线⽤户列表中有⽤户的基本信息,包括sessionId。 websocket的实时推送 对⽐聊天室的demo,不同之处在于,客户端连⼊服务器时候,会开启⼀个线程,在线程中对客户端进⾏推送数据。关键代码: /** * 接收到消息 * * @param text */ @OnMessage public void onMsg(Session session,@PathParam(\"param\") String param) throws IOException { //记录客户端 webSocketMaps.put(session, param); //实例化⼯作任务 Operater operater =new Operater(session,param); //开启线程 Thread thread = new Thread(operater); thread.start(); logger.info(\"发送线程启动成功\"); } ⽬前业务还不是很复杂,后期功能添加时候,再进⾏扩展,关于这个实时推送,⼤概开了50个窗⼝就连接失败了。关于websocket的⾼并发,可以考虑。 因篇幅问题不能全部显示,请点此查看更多更全内容