前言 名词解释 HTTP :HTTP是应用层协议(在传输层使用 TCP,在网络层使用 IP 协议),是一种无状态(即每个请求都是一个新的请求)、无连接(每次连接只处理一个请求)协议,但是HTTP仍然可以借助Cookie(客户端数据储存)和Session(服务端数据储存)实现长连接(HTTP的长连接需要在请求头中加入Connection:keep-alive
)。整个通信过程,客户端(浏览器)向服务端发送请求,服务端接收请求并返回响应信息。
Socket :与HTTP不一样,Socket不是一种协议,而是传输层的一种接口((应用程序与网络协议栈进行交互的接口),它是对 TCP/IP
协议的封装。
WebSocket :WebSocket和HTTP一样是应用层的协议。但是WebSocket是一种双向通信协议,是一种有状态协议。在WebSocket中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
长连接 :
短连接:
短连接是一次性连接,即浏览器和服务器每次进行操作时都需要重新建立TCP连接,操作结束后即中断连接。
过程:建立连接–>传输数据–>断开连接
HTTP和Socket均支持长连接和短连接;
使用长连接的 Http 协议,浏览器或者服务器在其头信息加入了这行代码:Connection:keep-alive
,TCP 连接在发送后将仍然保持打开状态,于是,浏览器可以继续通过相同的连接发送请求
WebSocket长连接与HTTP长连接区别 既然HTTP也能实现长连接,为什么还要用WebSocket呢?这就要说一说他们两者之间的区别了。
区别一
HTTP协议只能做单向传输即每次进行通信时,都需要客户端发送请求(request)然后服务端进行应答(response),服务端是无法主动向客户端发送请求的。
而WebSocket则是双向传输,服务端与客户端可以互相主动发起请求,并且即使客户端或者服务端其中一方断开连接,后续请求时也不需要再次连接请求。
区别二
HTTP是无状态协议,虽然HTTP可以使用长连接,在一次 TCP 连接中完成多HTTP请求,但是对于每个请求仍要单独发请求头,也就是说仍然是一问一答的模式。
与HTTP不同的是,Websocket是一种有状态的协议,在进行通讯前需要先创建连接(发送一个附加请求头信息(Upgrade:WebSocket
)的HTTP协议),连接建立完成后双方就可以自由(使用TCP)进行通信,并且此连接状态会一直持续到其中一方断开为止。
区别三
由于HTTP的单向请求,如果服务端有连续的状态变化时,客户端就需要频繁的通过请求实现长轮询来获取服务端的最新变化状态,轮询效率较低,且非常浪费资源(因为需要不停的连接,或者HTTP连接始终打开)
而WebSocket是实时的双向通信,就不需要客户端发送请求来获取服务端状态,服务端可以主动推送最新状态
用图表示的话,就是下面这个样子:
HTTP长连接:
WebSocket:
说明 客户端(浏览器) 创建websocket对象 Websocket协议定义了两种URL方案
ws: 非加密
wss :加密(使用HTTPS采用的安全机制保证HTTP连接的安全)。
1 var ws = new WebSocket(url);
url格式:
不加密:ws://ip地址:端口号/资源地址;
加密:wss://ip地址:端口号/资源地址;
websocket事件
事件
使用
描述
open
websocket对象.onopen
连接时触发
message
websocket对象.onmessage
客户端接收服务端数据时触发
error
websocket对象.onerror
通信错误时触发
close
websocket对象.onclose
连接关闭时触发
websocket方法
方法
描述
send()
使用连接发送数据
close()
关闭连接
服务端 服务端使用websocket需要定义Endpoint(就和实现HTTP需要Servlet一样)。
主要通过两种方式定义Endpoint:
编程式:通过继承javax.websocket.Endpoint
类,实现其方法;
注解式:通过定义一个POJO,并添加@ServicePoint
相关注解。
在Endpoint类中可以看到各生命周期相关方法:
方法
注解
描述
onClose
@OnClose
当会话关闭时调用
onOpen
@OnOpen
当开启一个新会话时调用
onError
@OnError
当连接过程中异常时调用
实例 非springboot项目可以引入tomcat中ws:
1 2 3 4 5 <dependency > <groupId > org.apache.tomcat.embed</groupId > <artifactId > tomcat-embed-websocket</artifactId > <version > 9.0.56</version > </dependency >
springboot项目直接引入下面依赖
1 2 3 4 5 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-websocket</artifactId > <version > 2.6.7</version > </dependency >
本示例是springboot项目,非springboot项目可做参考
示例中使用了json和对象间转换,需要引入fastjson依赖:
1 2 3 4 5 <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > <version > 1.2.49</version > </dependency >
客户端界面 wsPage.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="utf-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0, maximum-scale=1.0" > <title > ws</title > <script type ="text/javascript" > var ws = new WebSocket("ws://localhost:8080/wsTest" ); /** * 绑定事件 */ ws.onopen = function ( ) { alert("连接成功" ) } ws.onmessage = function (event ) { const data = event.data; const msg = JSON .parse(data); if (msg.isSystem) { const info = document .getElementById("info" ); const user = document .getElementById("user" ); info.innerText = msg.user + msg.message; if (undefined === user.innerText || '' ===user.innerText) { user.innerText = msg.user; } } else { const info = document .getElementById("message" ); info.innerText = msg.message; } } ws.onclose = function ( ) { alert("连接已断开" ) } function send ( ) { const input = document .getElementById("input" ); const user = document .getElementById("user" ); const sendTo = document .getElementById("sendTo" ); const msg = input.value; input.value = "" ; const sendMsg = {"msg" : user.innerText + ":" + msg, "sendTo" : sendTo.value}; ws.send(JSON .stringify(sendMsg)) } </script > </head > <body class ="login-body" > <h3 id ="user" > </h3 > 给<input id ="sendTo" placeholder ="要给谁发消息" value ="ahzoo" > 发送消息<br /> <input id ="input" placeholder ="需要发送的消息" > <button onclick ="send()" > 发送消息</button > <h3 id ="info" > </h3 > <p id ="message" > </p > </body > </html >
服务端 wobsocket服务类:
用于信息的传递
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 import com.alibaba.fastjson.JSONObject;import com.springboot.config.HttpSessionConfigurator;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Component;import javax.servlet.http.HttpSession;import javax.websocket.*;import javax.websocket.server.ServerEndpoint;import java.io.IOException;import java.util.HashMap;import java.util.Map;import java.util.Set;import java.util.concurrent.ConcurrentHashMap;@ServerEndpoint(value = "/wsTest", configurator = HttpSessionConfigurator.class) @Component public class WsEndpoint { private static final Logger logger = LoggerFactory.getLogger(WsEndpoint.class); private static Map<String, WsEndpoint> users = new ConcurrentHashMap<>(); private Session session; HttpSession httpSession; @OnOpen public void onOpen (Session session, EndpointConfig endpointConfig) throws IOException, EncodeException { this .session = session; HttpSession httpSession = (HttpSession) endpointConfig.getUserProperties().get(HttpSession.class.getName()); this .httpSession = httpSession; String user = (String) httpSession.getAttribute("username" ); users.put(user, this ); HashMap<String, String> message = new HashMap<>(); message.put("isSystem" , "true" ); message.put("user" , user); message.put("message" , "上线了!" ); this .sendToAll(message); } @OnMessage public void onMessage (String message, Session session) throws IOException { Object obj = JSONObject.parse(message); String sendTo = (String) ((JSONObject) obj).get("sendTo" ); String msg = (String) ((JSONObject) obj).get("msg" ); HashMap<String, String> sendMsg = new HashMap<>(); sendMsg.put("message" , msg); users.get(sendTo).session.getBasicRemote().sendText(JSONObject.toJSONString(sendMsg)); } @OnClose public void onClose (Session session) { } private void sendToAll (HashMap<String, String> message) throws IOException { Set<String> names = users.keySet(); for (String name : names) { WsEndpoint wsEndpoint = users.get(name); wsEndpoint.session.getBasicRemote().sendText(JSONObject.toJSONString(message)); } } }
websocket配置类:
用于注册websocket服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.web.socket.server.standard.ServerEndpointExporter;@Configuration public class WsConfig { @Bean public ServerEndpointExporter serverEndpointExporter () { return new ServerEndpointExporter(); } }
配置类:
用于获取session中的(用户)信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import javax.servlet.http.HttpSession;import javax.websocket.HandshakeResponse;import javax.websocket.server.HandshakeRequest;import javax.websocket.server.ServerEndpointConfig;public class HttpSessionConfigurator extends ServerEndpointConfig .Configurator { @Override public void modifyHandshake (ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) { HttpSession httpSession = (HttpSession) request.getHttpSession(); sec.getUserProperties().put(HttpSession.class.getName(),httpSession); } }
控制器类:
用于界面展示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RequestMapping;import javax.servlet.http.HttpSession;@Controller public class WsController { @RequestMapping("ws/{username}") public String toChat (HttpSession httpSession, @PathVariable("username") String username) { if (null ==httpSession.getAttribute("username" )) { httpSession.setAttribute("username" , username); } return "wsPage" ; } }
实例中只是简单实现功能,直接用session储存的用户数据,所以为了避免一个浏览器多个用户登录,防止数据错乱的情况,直接在控制器中限制了session只能储存新数据,不能覆盖储存。
储存功能 消息的储存,如果没有特殊需求的话,可以选择直接储存在浏览器的sessionStorage中
关闭连接 客户端调用websocket对象的close()
方法关闭;
服务端调用websocket对象的end()
方法关闭
心跳机制 由于WebSocket是实时通信,所以如果客户端和服务端长期为通信的话就需要确定双方是否都还“存活”,然后才能继续通信。
实现方式就是每隔一段时间向服务器发送一个数据包,告诉服务器自己还活着,然后服务端接收到数据后,也向客户端回复一个数据,告诉服务端也还活着,否则的话,就是连接断开了,这个时候就需要进行重连操作。
实现流程: