目前,前端与服务器端的交互有两种常用方式:主动式(HTTP)和非主动式(Web-Socket)。
1. HTTP
HTTP是一种网络传输的应用层协议,是一种建立在TCP上的无状态连接。其工作流程如下。
客户端发送一个HTTP请求,服务器端收到请求之后,根据请求的参数进行解析读取,处理完毕后把相应的内容通过HTTP响应返回到客户端,交互过程如图2-4所示。
图2-4 HTTP请求交互图
HTTP结构分为以下部分。
(1)HTTP请求
HTTP请求由状态行、请求头、请求正文三部分组成。其中,状态行包括请求方式Method、资源路径URL、协议版本Version;请求头包括访问域名、Cookie、代理信息等;请求正文包括请求数据。
(2)HTTP响应
在收到客户端发送的HTTP请求后,服务器端会根据请求方式做出具体动作,将结果返回客户端。请求响应包括状态行、响应头、响应正文三部分组成。其中,状态行包括状态码、协议版本Version;响应头包括发送响应时间、回应数据格式;响应文正包括响应数据。
HTTP版本分为1.0、1.1、2.0,各版本说明如下。
1)HTTP/1.*:一次请求-响应,建立一个连接,用完关闭。
2)HTTP/1.1:串行化单线程处理,可以同时在同一个TCP链接上发送多个请求,但是只有响应是有顺序的,只有上一个请求完成后,下一个才能响应。一旦有任务处理超时等,后续任务只能被阻塞(线头阻塞)。
3)HTTP/2:并行执行。某任务耗时严重,不会影响到任务正常执行。
HTTP适用场景: HTTP适用于客户端主动请求服务器端获取数据的情况,适用于大部分业务场景。但客户端和服务器端交互时,有些场景需要服务器端主动发送内容至客户端,HTTP不能高效实现。
2. WebSocket
传统HTTP协议不能满足客户端和服务器端的双向通信,WebSocket的诞生解决了这个问题。WebSocket扩展了客户端与服务器端的通信功能,使服务器端也能主动向客户端发送数据。
传统的HTTP协议是无状态的,每次请求(request)都要由客户端主动发起,再由服务器端处理后返回响应结果,而服务器端很难主动向客户端发送数据。这种客户端是主动方、服务器端是被动方的传统Web模式,对于信息变化不频繁的Web应用来说造成的麻烦较小,而对于涉及实时信息的Web应用却带来了很大不便,如带有即时通信、实时数据、订阅推送等功能的应用。传统的处理方式有轮询(polling)和Comet技术,而WebSocket本质上也是一种轮询,是优化改进后的交互方式。
轮询是最原始的实现实时Web应用的解决方案。轮询是指客户端以设定的时间间隔周期性地主动向服务器端发送请求,频繁地查询是否有新的数据改动。这种方法会导致过多不必要的请求,浪费流量和服务器端资源。
Comet技术又可以分为长轮询和流技术。长轮询改进了上述轮询技术,减少了无用的请求。它会为某些数据设定过期时间,当数据过期后才会向服务端发送请求。这种机制适合数据改动不是特别频繁的情况。流技术通常是指客户端使用一个隐藏的窗口与服务器端建立一个HTTP长连接,服务器端会不断更新连接状态以保持HTTP长连接存活,然后通过这条长连接主动将数据发送给客户端。在大并发环境下,流技术可能会考验服务端的性能。
WebSocket是一种网络通信协议,是H5提出的一种协议规范,主要用于客户端和服务器端实时通信,本质是基于TCP进行网络传输。
WebSocket工作流程如下。浏览器通过脚本向服务器端发出建立WebSocket类型连接的请求,WebSocket连接建立成功后,客户端和服务器端就可以通过TCP连接传输数据,不需要每次传输都带上重复的头部数据,所以它的数据传输量比轮询要少很多。交互过程如图2-5所示。
图2-5 前后端用WebSocket交互效果
注意
WebSocket可以实现由服务器主动向客户端推送消息的功能,而不需要依赖来自客户端的请求,增加了服务器的主动性和灵活性。
HTTP的通信只能由客户端发起,而WebSocket的通信在客户端与服务器握手建立连接后,双方即可相互传送信息,直至任意一方主动断开连接结束。
WebSocket依赖的是TCP协议,因此在连接建立后,传输数据量比依赖HTTP的传输数据量要小,提高了传输的效率。同时,WebSocket是长连接,受网络限制较大,使用时需着重考虑重连等问题,另外WebSocket做实时推送,特别是多页面复杂逻辑的实时推送,成本较高。
前端通过WebSocket调用后端代码的具体实现如代码清单2-6所示。
<%@ page language="java" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <title>WebSocket</title> </head> <body> Welcome WebSocket <input id="text" type="text" /> <button onclick="send()">发送消息</button> <button onclick="closeWebSocket()">关闭WebSocket连接</button> <div id="message"></div> </body> <script type="text/javascript"> var websocket = null; // 判断当前浏览器是否支持WebSocket if ('WebSocket' in window) { websocket = new WebSocket("ws:// localhost:8080/websocket"); } else { alert('当前浏览器Not support websocket') } // 连接发生错误的回调方法 websocket.onerror = function() { setMessageInnerHTML("WebSocket连接发生错误"); }; // 连接成功建立的回调方法 websocket.onopen = function() { setMessageInnerHTML("WebSocket连接成功"); } // 接收到消息的回调方法 websocket.onmessage = function(event) { setMessageInnerHTML(event.data); } // 连接关闭的回调方法 websocket.onclose = function() { setMessageInnerHTML("WebSocket连接关闭"); } // 监听窗口关闭事件,当窗口关闭时,主动去关闭WebSocket连接,防止连接还没断开就关闭窗口,server端会抛异常 window.onbeforeunload = function() { closeWebSocket(); } // 将消息显示在网页上 function setMessageInnerHTML(innerHTML) { document.getElementById('message').innerHTML += innerHTML; } // 关闭WebSocket连接 function closeWebSocket() { websocket.close(); } // 发送消息 function send() { var message = document.getElementById('text').value; websocket.send(message); } </script> </html>
服务器端WebSocket处理方式如代码清单2-7所示。
package com.zachary.springboot.blog.pushlian.bean; import java.io.IOException; import java.util.concurrent.CopyOnWriteArraySet; import javax.websocket.OnClose; import javax.websocket.OnError; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.ServerEndpoint; /** * author zachary * desc @ServerEndpoint主要是将目前的类定义成一个WebSocket服务器端, 注解的值将被用于监听用户连接的终端访问URL地址, 客户端可以通过这个URL来连接到WebSocket服务器端 */ @ServerEndpoint("/websocket") public class WebSocketTest { // 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的 private static int onlineCurrCount = 0; // concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以作为用户标识 private static CopyOnWriteArraySet<WebSocketTest> webSocketSet = new Co-pyOnWriteArraySet<WebSocketTest>(); // 与某个客户端的连接会话,需要通过它来给客户端发送数据 private Session session; /** * 连接建立成功调用的方法 * * @param session可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据 */ @OnOpen public void onOpen(Session session) { this.session = session; webSocketSet.add(this); // 加入set中 addOnlineCurrCount(); // 在线数加1 System.out.println("有新连接加入!当前在线人数为" + getOnlineCurrCount()); } /** * 连接关闭调用的方法 */ @OnClose public void onClose() { webSocketSet.remove(this); // 从set中删除 subOnlineCurrCount(); // 在线数减1 System.out.println("有一连接关闭!当前在线人数为" + getOnlineCurrCount()); } /** * 收到客户端消息后调用的方法 * * @param message 客户端发送过来的消息 * @param session 可选的参数 */ @OnMessage public void onMessage(String message, Session session) { System.out.println("来自客户端的消息:" + message); // 群发消息 for (WebSocketTest item : webSocketSet) { try { item.sendMessage(message); } catch (IOException e) { e.printStackTrace(); continue; } } } /** * 发生错误时调用 * * @param session * @param error */ @OnError public void onError(Session session, Throwable error) { System.out.println("发生错误"); error.printStackTrace(); } /** * 这个方法与上面几个方法不一样。没有用注解,是根据自己需要添加的方法。* @param message * * @throws IOException */ public void sendMessage(String message) throws IOException { this.session.getBasicRemote().sendText(message); // this.session.getAsyncRemote().sendText(message); } public static synchronized int getOnlineCurrCount() { return onlineCurrCount; } public static synchronized void addOnlineCurrCount() { WebSocketTest.onlineCurrCount++; } public static synchronized void subOnlineCurrCount() { WebSocketTest.onlineCurrCount--; } }
WebSocket适用于服务器主动通知客户端等交互情况,如在线聊天、通信。
交互常见状态码列举如下。
客户端和服务器端交互常见状态码分析如下。
(1)当出现403状态码时
在客户端请求服务器端过程中,如有负载均衡服务器,可能代理后未开放权限,检查代理服务器的策略是否正确。如无代理服务器,服务器端查看是否有权限认证,是否未识别或未正常授权地址。
(2)当出现404状态码时
由客户端请求服务器端的地址和服务器端地址不一致导致失败,此时可以检查地址是否正确。
(3)当出现405状态码时
在客户端请求服务器端过程中,由客户端请求数据格式和服务器端接口的数据格式不一致造成。更改数据格式和类型,与接口保持一致即可。
(4)当出现500状态码时
服务器接收到请求后内部处理失败,但未捕捉异常处理,导致错误下发到服务器端。可以全局异常处理,统一返回信息至客户端。
(5)当出现503状态码时
如提示“‘No Access-Control-Allow-Orign’header is present on the requested resource.”跨域问题,表明客户端和服务器端部署不在同一个域名下。服务器端设置响应支持跨域即可。