WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。
在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
HTML5 定义的 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。当获取 Web Socket 连接后,可以通过 send() 方法来向服务器发送数据,并通过 onmessage 事件来接收服务器返回的数据。
(1)ajax轮询
ajax轮询的原理非常简单,让浏览器隔个几秒就发送一次请求,询问服务器是否有新信息。
(2)long poll(长轮询)
long poll 其实原理跟 ajax轮询 差不多,都是采用轮询的方式,不过采取的是阻塞模型(一直打电话,没收到就不挂电话),也就是说,客户端发起连接后,如果没消息,就一直不返回Response给客户端。直到有消息才返回,返回完之后,客户端再次建立连接,周而复始。
从上面可以看出其实这两种方式,都是在不断地建立HTTP连接,关闭HTTP协议,由于HTTP是非状态性的,每次都要重新传输 identity info (鉴别信息),来告诉服务端你是谁。然后等待服务端处理,可以体现HTTP协议的另外一个特点,被动性。
被动性:其实就是,服务端不能主动联系客户端,只能有客户端发起。从上面很容易看出来,不管怎么样,上面这两种都是非常消耗资源的。
ajax轮询 需要服务器有很快的处理速度和资源。(速度)
long poll 需要有很高的并发,也就是说同时接待客户的能力。(场地大小)
(3)WebSocket
Websocket解决了HTTP的这几个难题。首先,被动性,当服务器完成协议升级后(HTTP-->Websocket),服务端就可以主动推送信息给客户端。解决了上面同步有延迟的问题。
解决服务器上消耗资源的问题:其实所用的程序是要经过两层代理的,即HTTP协议在Nginx等服务器的解析下,然后再传送给相应的Handler来处理。
简单地说,有一个非常快速的 接线员(Nginx) ,他负责把问题转交给相应的 客服(Handler) 。Websocket就解决了这样一个难题,建立后,可以直接跟接线员建立持久连接,有信息的时候客服想办法通知接线员,然后接线员在统一转交给客户。
由于Websocket只需要一次HTTP握手,所以说整个通讯过程是建立在一次连接/状态中,也就避免了HTTP的非状态性,服务端会一直知道你的信息,直到你关闭请求,这样就解决了接线员要反复解析HTTP协议,还要查看identity info的信息。
此外,WebSocket不兼容低版本的IE.
优点:
1、采用全双工通信,摆脱传统HTTP轮询的窘境。
2、采用W3C国际标准,完美支持HTML5。
3、实时性,推送消息及时高效;
考虑到服务器压力,使用轮询方式不可能很短的时间间隔,否则服务器压力太多,所以轮询时间间隔都比较长,好几秒,设置十几秒。 而WebSocket是由服务器主动推送过来,实时性是最高的。
4、支持服务器端向客户端推送功能。服务器可以直接发送数据而不用等待客户端的请求。
5、减少通信量
只要建立起websocket连接,就一直保持连接,在此期间可以源源不断的传送消息,直到关闭请求。
6、无浪费,减少服务器上资源消耗
轮询方式有可能轮询10次,才碰到服务端数据更新,那么前9次都白轮询了,因为没有拿到变化的数据。 而WebSocket是由服务器主动回发,来的都是新数据。
7、节约带宽
不停地轮询服务端数据这种方式,使用的是http协议,head信息很大,有效数据占比低, 而使用WebSocket方式,头信息很小,有效数据占比高。
缺点:
http:
http链接分为短链接,长链接。
短链接是每次请求都要三次握手才能发送自己的信息。即每一个request对应一个response。
长链接是在一定的期限内保持链接。保持TCP连接不断开。客户端与服务器通信,必须要有客户端发起然后服务器返回结果。客户端是主动的,服务器是被动的。
WebSocket:
WebSocket是为了解决客户端发起多个http请求到服务器资源浏览器必须要经过长时间的轮训问题而生的,它实现了多路复用,是全双工通信。在webSocket协议下客服端和浏览器可以同时发送信息。
建立了WebSocket之后服务器不必在浏览器发送request请求之后才能发送信息到浏览器。这时的服务器已有主动权想什么时候发就可以发送信息到客户端。而且信息当中不必在带有head的部分信息了与http的长链接通信来说,这种方式,不仅能降低服务器的压力。而且信息当中也减少了部分多余的信息。
社交聊天
弹幕
多玩家游戏
协同编辑,在线文档
视频会议
电商的实时报价
社交订阅信息实时更新与推送
..............
代码demo: 下载
得益于W3C国际标准的实现,在浏览器JS就能直接创建WebSocket对象,再通过简单的回调函数就能完成WebSocket客户端的编写。
使用:
1、获取WebSocket客户端对象。
var webSocket = new WebSocket(url);
2、获取WebSocket回调函数。
webSocket.onmessage = function (event) {console.log('WebSocket收到消息:' + event.data);
事件类型 | WebSocket回调函数 | 事件描述 |
---|---|---|
open | webSocket.onopen | 当打开连接后触发 |
message | webSocket.onmessage | 当客户端接收服务端数据时触发 |
error | webSocket.onerror | 当通信异常时触发 |
close | webSocket.onclose | 当连接关闭时触发 |
3、发送消息给服务端
webSokcet.send(jsonStr)
<script>
/**
* WebSocket客户端
*
* 使用说明:
* 1、WebSocket客户端通过回调函数来接收服务端消息。例如:webSocket.onmessage
* 2、WebSocket客户端通过send方法来发送消息给服务端。例如:webSocket.send();
*/
function getWebSocket() {
/**
* WebSocket客户端 PS:URL开头ws表示WebSocket协议 中间是域名端口 结尾是服务端映射地址
*/
var webSocket = new WebSocket('ws://localhost:8080/chat');
/**
* 当服务端打开连接
*/
webSocket.onopen = function (event) {
console.log('WebSocket打开连接');
};
/**
* 当服务端发来消息:1.广播消息 2.更新在线人数
*/
webSocket.onmessage = function (event) {
console.log('WebSocket收到消息:%c' + event.data, 'color:green');
//获取服务端消息
var message = JSON.parse(event.data) || {};
var $messageContainer = $('.message-container');
if (message.type === 'SPEAK') {
$messageContainer.append(
'<div class="mdui-card" style="margin: 10px 0;">' +
'<div class="mdui-card-primary">' +
'<div class="mdui-card-content message-content">' + message.username + ":" + message.msg + '</div>' +
'</div></div>');
}
$('.chat-num').text(message.onlineCount);
//防止刷屏
var $cards = $messageContainer.children('.mdui-card:visible').toArray();
if ($cards.length > 5) {
$cards.forEach(function (item, index) {
index < $cards.length - 5 && $(item).slideUp('fast');
});
}
};
/**
* 关闭连接
*/
webSocket.onclose = function (event) {
console.log('WebSocket关闭连接');
};
/**
* 通信失败
*/
webSocket.onerror = function (event) {
console.log('WebSocket发生异常');
};
return webSocket;
}
var webSocket = getWebSocket();
/**
* 通过WebSocket对象发送消息给服务端
*/
function sendMsgToServer() {
var $message = $('#msg');
if ($message.val()) {
webSocket.send(JSON.stringify({username: $('#username').text(), msg: $message.val()}));
$message.val(null);
}
}
/**
* 清屏
*/
function clearMsg(){
$(".message-container").empty();
}
/**
* 使用ENTER发送消息
*/
document.onkeydown = function (event) {
var e = event || window.event || arguments.callee.caller.arguments[0];
e.keyCode === 13 && sendMsgToServer();
};
</script>
得益于SpringBoot提供的自动配置,只需要通过简单注解
@ServerEndpoint
就能创建WebSocket服务端,再通过简单的回调函数就能完成WebSocket服务端的编写。
1、首先在pom.xml文件引入spring-boot-starter-websocket
、thymeleaf 、FastJson等依赖。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency><!--Webjars版本定位工具-->
<groupId>org.webjars</groupId>
<artifactId>webjars-locator-core</artifactId>
</dependency>
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>mdui</artifactId>
<version>0.4.0</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.49</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
2、开启WebSocket服务端的自动注册。
这里需要特别提醒:ServerEndpointExporter 是由Spring官方提供的标准实现,用于扫描ServerEndpointConfig配置类和@ServerEndpoint注解实例。
使用规则也很简单:1.如果使用默认的嵌入式容器 比如Tomcat 则必须手工在上下文提供ServerEndpointExporter。2. 如果使用外部容器部署war包,则不要提供提供ServerEndpointExporter,因为此时SpringBoot默认将扫描服务端的行为交给外部容器处理。
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
3、创建WebSocket服务端
@ServerEndpoint
来声明实例化WebSocket服务端。@OnOpen、@OnMessage、@OnClose、@OnError
来声明回调函数。事件类型 | WebSocket服务端注解 | 事件描述 |
---|---|---|
open | @OnOpen | 当打开连接后触发 |
message | @OnMessage | 当接收客户端信息时触发 |
error | @OnError | 当通信异常时触发 |
close | @OnClose | 当连接关闭时触发 |
ConcurrentHashMap
保存全部在线会话对象。@Component
@ServerEndpoint("/chat")//标记此类为服务端
public class WebSocketChatServer {
/**
* 全部在线会话
* 基于场景考虑 这里使用线程安全的ConcurrentHashMap存储会话对象。
*/
private static Map<String, Session> onlineSessions = new ConcurrentHashMap<>();
/**
* 当客户端打开连接:1.添加会话对象 2.更新在线人数
*/
@OnOpen
public void onOpen(Session session) {
onlineSessions.put(session.getId(), session);
sendMessageToAll(Message.jsonStr(Message.ENTER, "", "", onlineSessions.size()));
}
/**
* 当客户端发送消息:1.获取它的用户名和消息 2.发送消息给所有人
* <p>
* 这里约定传递的消息为JSON字符串 方便传递更多参数!
*/
@OnMessage
public void onMessage(Session session, String jsonStr) {
Message message = JSON.parseObject(jsonStr, Message.class);
sendMessageToAll(Message.jsonStr(Message.SPEAK, message.getUsername(), message.getMsg(), onlineSessions.size()));
}
/**
* 当关闭连接:1.移除会话对象 2.更新在线人数
*/
@OnClose
public void onClose(Session session) {
onlineSessions.remove(session.getId());
sendMessageToAll(Message.jsonStr(Message.QUIT, "", "下线了!", onlineSessions.size()));
}
/**
* 当通信发生异常:打印错误日志
*/
@OnError
public void onError(Session session, Throwable error) {
error.printStackTrace();
}
/**
* 公共方法:发送信息给所有人
*/
private static void sendMessageToAll(String msg) {
onlineSessions.forEach((id, session) -> {
try {
session.getBasicRemote().sendText(msg);
} catch (IOException e) {
e.printStackTrace();
}
});
}
}
javax.websocket.Session
来发消息给客户端。/**
* WebSocket 聊天消息类
*/
package com.hehe.chat;
import com.alibaba.fastjson.JSON;
/**
* WebSocket 聊天消息类
*/
public class Message {
public static final String ENTER = "ENTER";
public static final String SPEAK = "SPEAK";
public static final String QUIT = "QUIT";
private String type;//消息类型
private String username; //发送人
private String msg; //发送消息
private int onlineCount; //在线用户数
public static String jsonStr(String type, String username, String msg, int onlineTotal) {
return JSON.toJSONString(new Message(type, username, msg, onlineTotal));
}
public Message(String type, String username, String msg, int onlineCount) {
this.type = type;
this.username = username;
this.msg = msg;
this.onlineCount = onlineCount;
}
//这里省略了get/set方法
}
启动类:
@SpringBootApplication
@RestController
public class WebSocketChatApplication {
/**
* 登陆界面
*/
@GetMapping("/")
public ModelAndView login() {
return new ModelAndView("/login");
}
/**
* 聊天界面
*/
@GetMapping("/index")
public ModelAndView index(String username, String password, HttpServletRequest request) throws UnknownHostException {
if (StringUtils.isEmpty(username)) {
username = "匿名用户";
}
ModelAndView mav = new ModelAndView("/chat");
mav.addObject("username", username);
mav.addObject("webSocketUrl", "ws://"+InetAddress.getLocalHost().getHostAddress()+":"+request.getServerPort()+request.getContextPath()+"/chat");
return mav;
}
public static void main(String[] args) {
SpringApplication.run(WebSocketChatApplication.class, args);
}
}
运行启动类,浏览器访问http://localhost:8080/
使用 用户1登录,当前在线人数显示为1
新开一个浏览器窗口访问http://localhost:8080/,并使用 用户2登录,当前在线人数显示为2
分别在用户1和用户2窗口发送消息,消息实时推送
评论