今天我们详细分析WebSocket 通信原理和使用
一、什么是
WebSocket
?
WebSocket
——一种在
2011
年被互联网工程任务组(
IETF
)标准化的协议。WebSocket 解决了一个长期存在的问题:既然底层的协议(
HTTP
)是一个请求
/
响应模式的交互序列,那么如何实时地发布信息呢?AJAX
提供了一定程度上的改善,但是数据流仍然是由客户端所发送的请求驱动的。还有其他的一些或多或少的取巧方式(Comet)
WebSocket
规范以及它的实现代表了对一种更加有效的解决方案的尝试。简单地说,WebSocket 提供了“在一个单个的
TCP
连接上提供双向的通信……结合
WebSocket API
…… 它为网页和远程服务器之间的双向通信提供了一种替代 HTTP
轮询的方案。” 但是最终它们仍然属于扩展性受限的变通之法。也就是说,WebSocket
在客户端和
服务器之间提供了真正的双向数据交换。
WebSocket
连接允许客户端和服务器之间进行全双工通信,以便任一方都可以通过建立的连接将数据推送到另一端。WebSocket
只需要建立一次连接,就可以一直保持连接状态。这相比于轮询方式的不停建立连接显然效率要大大提高。
Web
浏览器和服务器都必须实现
WebSockets
协议来建立和维护连接。
特点
1、HTML5
中的协议,实现与客户端与服务器双向,基于消息的文本或二进制数据通信
2、适合于对数据的实时性要求比较强的场景,如通信、直播、共享桌面,特别适合于客户与服务频繁交互的情况下,如实时共享、多人协作等平台。
3、采用新的协议,后端需要单独实现
4、客户端并不是所有浏览器都支持
二、WebSocket
通信握手
Websocket
借用了
HTTP
的协议来完成一部分握手
1、客户端的请求:
Connection
必须设置
Upgrade
,表示客户端希望连接升级。
Upgrade
字段必须设置
Websocket
,表示希望升级到
Websocket
协议。
Sec-WebSocket-Key
是随机的字符串,服务器端会用这些数据来构造出一个
SHA-1
的信息摘要。把 “Sec-WebSocket-Key
” 加上一个特殊字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11
”,然后计算
SHA-1
摘要,之后进行
BASE-64
编码,将结果做为 “Sec-WebSocket-Accept
” 头的值,返回给客户端。如此操作,可以尽量避免普通 HTTP
请求被误认为
Websocket
协议。Sec-WebSocket-Version 表示支持的
Websocket
版本。
RFC6455
要求使用的版本是
13
, 之前草案的版本均应当弃用。
2、服务器端:
Upgrade: websocket
Connection: Upgrade
依然是固定的,告诉客户端即将升级的是
Websocket
协议,而不是
mozillasocket
,lurnarsocket 或者
shitsocket
。然后, Sec-WebSocket-Accept
这个则是经过服务器确认,并且加密过后的 Sec-WebSocket-Key
。后面的, Sec-WebSocket-Protocol
则是表示最终使用的协议。至此,HTTP
已经完成它所有工作了,接下来就是完全按照
Websocket
协议进行
WebSocket
通信
-STOMP
WebSocket
是个规范,在实际的实现中有
HTML5
规范中的
WebSocket API
、
WebSocket的子协议 STOMP
。 STOMP(Simple Text Oriented Messaging Protocol)
1、
简单(
流
)
文本定向消息协议
2、STOMP
协议的前身是
TTMP
协议(一个简单的基于文本的协议),专为消息中间件设计。是属于消息队列的一种协议,
和
AMQP, JMS
平级
.
它的简单性恰巧可以用于定义websocket 的消息体格式
. STOMP
协议很多
MQ
都已支持
,
比如
RabbitMq, ActiveMq
。
3、
生产者(发送消息)、消息代理、消费者(订阅然后收到消息)
4、
STOMP
是基于帧的协议
三、WebSocket
通信实现
1、SpringBoot
基于 Stomp
的聊天室
/IM
的实现
stomp具体实现案例:
首先:引入核心功能 jar 包:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
1)、页面web代码:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta name="aplus-terminal" content="1">
<meta name="apple-mobile-web-app-title" content="">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<meta name="format-detection" content="telephone=no, address=no">
<title>聊天</title>
<style type="text/css">
/*公共样式*/
body,h1,h2,h3,h4,p,ul,ol,li,form,button,input,textarea,th,td {
margin:0;
padding:0
}
body,button,input,select,textarea {
font:12px/1.5 Microsoft YaHei UI Light,tahoma,arial,"\5b8b\4f53";
*line-height:1.5;
-ms-overflow-style:scrollbar
}
h1,h2,h3,h4{
font-size:100%
}
ul,ol {
list-style:none
}
a {
text-decoration:none
}
a:hover {
text-decoration:underline
}
img {
border:0
}
button,input,select,textarea {
font-size:100%
}
table {
border-collapse:collapse;
border-spacing:0
}
/*rem*/
html {
font-size:62.5%;
}
body {
font:16px/1.5 "microsoft yahei", 'tahoma';
}
body .mobile-page {
font-size: 1.6rem;
}
/*浮动*/
.fl{
float: left;
}
.fr{
float: right;
}
.clearfix:after{
content: '';
display: block;
height: 0;
clear: both;
visibility: hidden;
}
body{
background-color: #F5F5F5;
}
.mobile-page{
max-width: 600px;
}
.mobile-page .admin-img, .mobile-page .user-img{
width: 45px;
height: 45px;
}
i.triangle-admin,i.triangle-user{
width: 0;
height: 0;
position: absolute;
top: 10px;
display: inline-block;
border-top: 10px solid transparent;
border-bottom: 10px solid transparent;
}
.mobile-page i.triangle-admin{
left: 4px;
border-right: 12px solid #fff;
}
.mobile-page i.triangle-user{
right: 4px;
border-left: 12px solid #9EEA6A;
}
.mobile-page .admin-group, .mobile-page .user-group{
padding: 6px;
display: flex;
display: -webkit-flex;
}
.mobile-page .admin-group{
justify-content: flex-start;
-webkit-justify-content: flex-start;
}
.mobile-page .user-group{
justify-content: flex-end;
-webkit-justify-content: flex-end;
}
.mobile-page .admin-reply, .mobile-page .user-reply{
display: inline-block;
padding: 8px;
border-radius: 4px;
background-color: #fff;
margin:0 15px 12px;
}
.mobile-page .admin-reply{
box-shadow: 0px 0px 2px #ddd;
}
.mobile-page .user-reply{
text-align: left;
background-color: #9EEA6A;
box-shadow: 0px 0px 2px #bbb;
}
.mobile-page .user-msg, .mobile-page .admin-msg{
width: 75%;
position: relative;
}
.mobile-page .user-msg{
text-align: right;
}
.chatRecord{
width: 100%;
height: 400px;
border-bottom: 1px solid blue;
line-height:20px;
overflow:auto;
overflow-x:hidden;
}
</style>
</head>
<body>
<div>
<div style="float:left;width:47%">
<p>请选择你是谁:
<select id="selectName" onchange="stompQueue();">
<option value="1">请选择</option>
<option value="Nandao">Nandao</option>
<option value="Lisi">Lisi</option>
<option value="WangWu">WangWu</option>
<option value="Piliu">Piliu</option>
<option value="Nini">Nini</option>
</select>
</p>
<div class="chatWindow">
<p style="color:darkgrey">群聊:</p>
<section id="chatRecord1" class="chatRecord">
<div id="mass_div" class="mobile-page">
</div>
</section>
<section class="sendWindow">
<textarea name="sendChatValue" id="sendChatValue" class="sendChatValue"></textarea>
<input type="button" name="sendMessage" id="sendMassMessage" class="sendMessage" onclick="sendMassMessage()" value="发送">
</section>
</div>
</div>
<div style="float:right; width:47%">
<p>请选择你要发给谁:
<select id="selectName2">
<option value="1">请选择</option>
<option value="Nandao">Nandao</option>
<option value="Lisi">Lisi</option>
<option value="WangWu">WangWu</option>
<option value="Piliu">Piliu</option>
<option value="Nini">Nini</option>
</select>
</p>
<div class="chatWindow">
<p style="color:darkgrey">单聊:</p>
<section id="chatRecord2" class="chatRecord">
<div id="alone_div" class="mobile-page">
</div>
</section>
<section class="sendWindow">
<textarea name="sendChatValue2" id="sendChatValue2" class="sendChatValue"></textarea>
<input type="button" name="sendMessage" id="sendAloneMessage" class="sendMessage" onclick="sendAloneMessage()" value="发送">
</section>
</div>
</div>
</div>
<!-- 独立JS -->
<script th:src="@{sockjs.min.js}"></script>
<script th:src="@{stomp.min.js}"></script>
<script th:src="@{jquery.js}"></script>
<script th:src="@{wechat_room.js}"></script>
</body>
</html>
2)、页面js 代码;
var stompClient = null;
//加载完浏览器后调用connect(),打开通道
$(function(){
//打开双通道
connect()
})
//强制关闭浏览器时调用websocket.close(),进行正常关闭
window.onunload = function() {
disconnect()
}
//打开通道
function connect(){
var socket = new SockJS('/endpointMark'); //连接SockJS的endpoint名称为"endpointMark"
stompClient = Stomp.over(socket);//使用STMOP子协议的WebSocket客户端
stompClient.connect({},function(frame){//连接WebSocket服务端
console.log('Connected:' + frame);
//接收广播信息
stompTopic();
});
}
//关闭通道
function disconnect(){
if(stompClient != null) {
stompClient.disconnect();
}
console.log("Disconnected");
}
//一对多,发起订阅
function stompTopic(){
//通过stompClient.subscribe订阅目标(destination)发送的消息(广播接收信息)
stompClient.subscribe('/mass/getResponse',function(response){
var message=JSON.parse(response.body);
//展示广播的接收的内容接收
var response = $("#mass_div");
var userName=$("#selectName").val();
if(userName==message.name){
response.append("<div class='user-group'>" +
" <div class='user-msg'>" +
" <span class='user-reply'>"+message.chatValue+"</span>" +
" <i class='triangle-user'></i>" +
" </div>" +userName+
" </div>");
}else{
response.append(" <div class='admin-group'>"+
message.name+
"<div class='admin-msg'>"+
" <i class='triangle-admin'></i>"+
" <span class='admin-reply'>"+message.chatValue+"</span>"+
"</div>"+
"</div>");
}
});
}
//群发消息
function sendMassMessage(){
var postValue={};
var chatValue=$("#sendChatValue");
var userName=$("#selectName").val();
postValue.name=userName;
postValue.chatValue=chatValue.val();
//postValue.userId="0";
if(userName==1||userName==null){
alert("请选择你是谁!");
return;
}
if(chatValue==""||userName==null){
alert("不能发送空消息!");
return;
}
stompClient.send("/massRequest",{},JSON.stringify(postValue));
chatValue.val("");
}
//单独发消息
function sendAloneMessage(){
var postValue={};
var chatValue=$("#sendChatValue2");
var userName=$("#selectName").val();
var sendToId=$("#selectName2").val();
var response = $("#alone_div");
postValue.name=userName;//发送者姓名
postValue.chatValue=chatValue.val();//聊天内容
postValue.userId=sendToId;//发送给谁
if(userName==1||userName==null){
alert("请选择你是谁!");
return;
}
if(sendToId==1||sendToId==null){
alert("请选择你要发给谁!");
return;
}
if(chatValue==""||userName==null){
alert("不能发送空消息!");
return;
}
stompClient.send("/aloneRequest",{},JSON.stringify(postValue));
response.append("<div class='user-group'>" +
" <div class='user-msg'>" +
" <span class='user-reply'>"+chatValue.val()+"</span>" +
" <i class='triangle-user'></i>" +
" </div>" +userName+
" </div>");
chatValue.val("");
}
//一对一,发起订阅
function stompQueue(){
var userId=$("#selectName").val();
//通过stompClient.subscribe订阅目标(destination)发送的消息(队列接收信息)
stompClient.subscribe('/user/' + userId + '/alone',
function(response){
var message=JSON.parse(response.body);
//展示一对一的接收的内容接收
var response = $("#alone_div");
response.append(" <div class='admin-group'>"+
message.name+
"<div class='admin-msg'>"+
" <i class='triangle-admin'></i>"+
" <span class='admin-reply'>"+message.chatValue+"</span>"+
"</div>"+
"</div>");
});
}
3)、后端stopm 设置代码:
@Configuration
/*开启使用Stomp协议来传输基于消息broker的消息
这时控制器支持使用@MessageMapping,就像使用@RequestMapping一样*/
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
/*注册STOMP协议的节点(endpoint),并映射指定的url,
* 添加一个访问端点“/endpointMark”,客户端打开双通道时需要的url,
* 允许所有的域名跨域访问,指定使用SockJS协议。*/
registry.addEndpoint("/endpointMark")
.setAllowedOrigins("*")
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/mass","/user");
registry.setUserDestinationPrefix("/user/");
}
}
4)、请求服务设置代码:
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/chatroom").setViewName("/wechat_room");
}
}
5)、控制层核心代码:
@Controller
public class StompController {
@Autowired
private SimpMessagingTemplate template;/*Spring实现的一个发送模板类*/
/*消息群发,接受发送至自massRequest的请求*/
@MessageMapping("/massRequest")
@SendTo("/mass/getResponse")
public ChatRoomResponse mass(ChatRoomRequest chatRoomRequest){
System.out.println("name = " + chatRoomRequest.getName());
System.out.println("chatValue = " + chatRoomRequest.getChatValue());
ChatRoomResponse response=new ChatRoomResponse();
response.setName(chatRoomRequest.getName());
response.setChatValue(chatRoomRequest.getChatValue());
return response;
}
/*单独聊天,接受发送至自aloneRequest的请求*/
@MessageMapping("/aloneRequest")
public ChatRoomResponse alone(ChatRoomRequest chatRoomRequest){
System.out.println("SendToUser = " + chatRoomRequest.getUserId()
+" FromName = " + chatRoomRequest.getName()
+" ChatValue = " + chatRoomRequest.getChatValue());
ChatRoomResponse response=new ChatRoomResponse();
response.setName(chatRoomRequest.getName());
response.setChatValue(chatRoomRequest.getChatValue());
this.template.convertAndSendToUser(chatRoomRequest.getUserId()+"",
"/alone",response);
return response;
}
}
6)、VO类数据实体:
请求的
返回的:
7)、启动类:
8)、浏览器请求:可以打开多个浏览器聊天,效果很成功!
2、原生
WebSocket
的集成
具体实现:参考
ws
模块下的代码
1)、前端代码:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta fromName="aplus-terminal" content="1">
<meta fromName="apple-mobile-web-app-title" content="">
<meta fromName="apple-mobile-web-app-capable" content="yes">
<meta fromName="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta fromName="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<meta fromName="format-detection" content="telephone=no, address=no">
<title>WebSocket原生API实现微信聊天</title>
<style type="text/css">
/*公共样式*/
body,h1,h2,h3,h4,p,ul,ol,li,form,button,input,textarea,th,td {
margin:0;
padding:0
}
body,button,input,select,textarea {
font:12px/1.5 Microsoft YaHei UI Light,tahoma,arial,"\5b8b\4f53";
*line-height:1.5;
-ms-overflow-style:scrollbar
}
h1,h2,h3,h4{
font-size:100%
}
ul,ol {
list-style:none
}
a {
text-decoration:none
}
a:hover {
text-decoration:underline
}
img {
border:0
}
button,input,select,textarea {
font-size:100%
}
table {
border-collapse:collapse;
border-spacing:0
}
/*rem*/
html {
font-size:62.5%;
}
body {
font:16px/1.5 "microsoft yahei", 'tahoma';
}
body .mobile-page {
font-size: 1.6rem;
}
/*浮动*/
.fl{
float: left;
}
.fr{
float: right;
}
.clearfix:after{
content: '';
display: block;
height: 0;
clear: both;
visibility: hidden;
}
body{
background-color: #F5F5F5;
}
.mobile-page{
max-width: 600px;
}
.mobile-page .admin-img, .mobile-page .user-img{
width: 45px;
height: 45px;
}
i.triangle-admin,i.triangle-user{
width: 0;
height: 0;
position: absolute;
top: 10px;
display: inline-block;
border-top: 10px solid transparent;
border-bottom: 10px solid transparent;
}
.mobile-page i.triangle-admin{
left: 4px;
border-right: 12px solid #fff;
}
.mobile-page i.triangle-user{
right: 4px;
border-left: 12px solid #9EEA6A;
}
.mobile-page .admin-group, .mobile-page .user-group{
padding: 6px;
display: flex;
display: -webkit-flex;
}
.mobile-page .admin-group{
justify-content: flex-start;
-webkit-justify-content: flex-start;
}
.mobile-page .user-group{
justify-content: flex-end;
-webkit-justify-content: flex-end;
}
.mobile-page .admin-reply, .mobile-page .user-reply{
display: inline-block;
padding: 8px;
border-radius: 4px;
background-color: #fff;
margin:0 15px 12px;
}
.mobile-page .admin-reply{
box-shadow: 0px 0px 2px #ddd;
}
.mobile-page .user-reply{
text-align: left;
background-color: #9EEA6A;
box-shadow: 0px 0px 2px #bbb;
}
.mobile-page .user-msg, .mobile-page .admin-msg{
width: 75%;
position: relative;
}
.mobile-page .user-msg{
text-align: right;
}
.chatRecord{
width: 100%;
height: 400px;
border-bottom: 1px solid blue;
line-height:20px;
overflow:auto;
overflow-x:hidden;
}
</style>
</head>
<body>
<div>
<div style="float:left;width:99%">
<p>请选择你是谁:
<select id="selectName" onchange="changeUser();">
<option value="1">请选择</option>
<option value="Nandao">Nandao</option>
<option value="Lisi">Lisi</option>
<option value="WangWu">WangWu</option>
<option value="Piliu">Piliu</option>
<option value="Nini">Nini</option>
</select>
</p>
<div class="chatWindow">
<p style="color:darkgrey">群聊:</p>
<section id="chatRecord1" class="chatRecord">
<div id="mass_div" class="mobile-page">
</div>
</section>
<section class="sendWindow">
<textarea fromName="sendChatValue" id="sendChatValue"
class="sendChatValue"></textarea>
<input type="button" fromName="sendMessage"
id="sendMassMessage" class="sendMessage"
onclick="sendMassMessage()" value="发送">
</section>
</div>
</div>
</div>
<script th:src="@{jquery.js}"></script>
<script type="text/javascript">
var socket;
if (typeof (WebSocket) == "undefined") {
console.log("遗憾:您的浏览器不支持WebSocket");
} else {
console.log("恭喜:您的浏览器支持WebSocket");
}
//群发消息
function sendMassMessage(){
var userName=$("#selectName").val();
if(userName==1||userName==null){
alert("请选择你是谁!");
return;
}
var chatValue=$("#sendChatValue");
if(chatValue==""||userName==null){
alert("不能发送空消息!");
return;
}
socket.send(chatValue.val());
chatValue.val("");
}
function changeUser(){
if (socket!=null){
socket.close();
}
var toUserId=$("#selectName").val();
if(toUserId==1||toUserId==null){
return;
}
//指定要连接的服务器地址与端口建立连接
//ws对应http、wss对应https。
socket = new WebSocket("ws://localhost:8080/ws/asset?toUserId="+toUserId);
//连接打开事件
socket.onopen = function() {
console.log("Socket 已打开");
};
//收到消息事件
socket.onmessage = function(msg) {
//展示广播的接收的内容接收
var response = $("#mass_div");
var dataObj=msg.data;
var arr = dataObj.split('@^');
var sendUser;
var acceptMsg;
$.each(arr, function (i, item) {
if(i==0){
sendUser = item;
}else{
acceptMsg = item;
}
});
if(toUserId==sendUser){
response.append("<div class='user-group'>" +
" <div class='user-msg'>" +
" <span class='user-reply'>"+acceptMsg+"</span>" +
" <i class='triangle-user'></i>" +
" </div>" +toUserId+
" </div>");
}else{
response.append(" <div class='admin-group'>"+
sendUser+
"<div class='admin-msg'>"+
" <i class='triangle-admin'></i>"+
" <span class='admin-reply'>"+acceptMsg+"</span>"+
"</div>"+
"</div>");
}
};
//连接关闭事件
socket.onclose = function() {
console.log("Socket已关闭");
};
//发生了错误事件
socket.onerror = function() {
alert("Socket发生了错误");
}
//窗口关闭时,关闭连接
window.unload=function() {
socket.close();
};
}
</script>
</body>
</html>
2)、后端服务设置:
@ServerEndpoint(value = "/ws/asset")/*将类定义成一个WebSocket服务器端*/
@Component
public class WebSocketServer {
private static Logger log = LoggerFactory.getLogger(WebSocketServer.class);
private static final AtomicInteger onlineCount
= new AtomicInteger(0);
/*线程安全Set,用来存放每个客户端对应的Session对象。*/
private static CopyOnWriteArraySet<Session> sessionSet
= new CopyOnWriteArraySet<Session>();
/*线程安全Map,用来存放每个客户端sessionid和用户名的对应关系。*/
private static Map<String,String> sessionMap
= new ConcurrentHashMap<>();
/*** 连接建立成功调用的方法*/
@OnOpen
public void onOpen(Session session) {
//将用户session,session和用户名对应关系放入本地缓存
sessionSet.add(session);
Map<String, List<String>> pathParameters
= session.getRequestParameterMap();
String userId = pathParameters.get("toUserId").get(0);
sessionMap.put(session.getId(),userId);
log.info("有连接加入,当前连接数为:{}", onlineCount.incrementAndGet());
try {
//通知所有用户有新用户上线
broadCastInfo("系统消息@^用户["+userId+"]加入群聊。");
} catch (IOException e) {
e.printStackTrace();
}
}
/*** 连接关闭调用的方法*/
@OnClose
public void onClose(Session session) {
//将用户session,session和用户名对应关系从本地缓存移除
sessionSet.remove(session);
Map<String, List<String>> pathParameters
= session.getRequestParameterMap();
String userId = sessionMap.get(session.getId());
sessionMap.remove(session.getId());
int cnt = onlineCount.decrementAndGet();
log.info("有连接关闭,当前连接数为:{}", cnt);
try {
//通知所有用户有用户下线
broadCastInfo("系统消息@^用户["+userId+"]退出群聊。");
} catch (IOException e) {
e.printStackTrace();
}
}
/*** 收到客户端消息后调用的方法*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("来自客户端{}的消息:{}",
sessionMap.get(session.getId()),message);
if(message.startsWith("ToUser:")){
//这里可以实现一对一聊天sendMessageAlone();
}else{
//实现群聊
String msger = sessionMap.get(session.getId());
try {
broadCastInfo(msger+"@^"+message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
/***出现错误时的处理*/
@OnError
public void onError(Session session, Throwable error) {
log.error("发生错误:{},Session ID: {}",error.getMessage(),session.getId());
error.printStackTrace();
}
/*** 发送消息的基础方法*/
public static void basicSendMessage(Session session, String message) {
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
log.error("发送消息出错:{}", e.getMessage());
e.printStackTrace();
}
}
/*** 群发消息*/
public static void broadCastInfo(String message) throws IOException {
for (Session session : sessionSet) {
if(session.isOpen()){
basicSendMessage(session, message);
}
}
}
/*** 指定Session发送消息*/
public static void sendMessageAlone(String sessionId, String message) throws IOException {
Session session = null;
for (Session s : sessionSet) {
if(s.getId().equals(sessionId)){
session = s;
break;
}
}
if(session!=null){
basicSendMessage(session, message);
}
else{
log.warn("没有找到你指定ID的会话:{}",sessionId);
}
}
}
3)、开启websocket 服务设置:
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
4)、请求服务设置:
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/ws")
.setViewName("/ws_chatroom");
}
}
5)、启动类
6)、启动后请求:
4、Netty
对
WebSocket
的支持
由
IETF
发布的
WebSocket RFC
,定义了
6
种帧,
Netty
为它们每种都提供了一个
POJO
实现。同时 Netty
也为我们提供很多的
handler
专门用来处理数据压缩,
ws
的通信握手等等。
具体实现:参考
netty-ws
模块下的代码
1)、前端页面代码:
public final class MakeIndexPage {
private static final String NEWLINE = "\r\n";
public static ByteBuf getContent(String webSocketLocation) {
return Unpooled.copiedBuffer(
"<html><head><title>Web Socket Test</title></head>"
+ NEWLINE +
"<body>" + NEWLINE +
"<script type=\"text/javascript\">" + NEWLINE +
"var socket;" + NEWLINE +
"if (!window.WebSocket) {" + NEWLINE +
" window.WebSocket = window.MozWebSocket;" + NEWLINE +
'}' + NEWLINE +
"if (window.WebSocket) {" + NEWLINE +
" socket = new WebSocket(\"" + webSocketLocation + "\");"
+ NEWLINE +
" socket.onmessage = function(event) {" + NEWLINE +
" var ta = document.getElementById('responseText');"
+ NEWLINE +
" ta.value = ta.value + '\\n' + event.data" + NEWLINE +
" };" + NEWLINE +
" socket.onopen = function(event) {" + NEWLINE +
" var ta = document.getElementById('responseText');"
+ NEWLINE +
" ta.value = \"Web Socket opened!\";" + NEWLINE +
" };" + NEWLINE +
" socket.onclose = function(event) {" + NEWLINE +
" var ta = document.getElementById('responseText');"
+ NEWLINE +
" ta.value = ta.value + \"Web Socket closed\"; "
+ NEWLINE +
" };" + NEWLINE +
"} else {" + NEWLINE +
" alert(\"Your browser does not support Web Socket.\");"
+ NEWLINE +
'}' + NEWLINE +
NEWLINE +
"function send(message) {" + NEWLINE +
" if (!window.WebSocket) { return; }" + NEWLINE +
" if (socket.readyState == WebSocket.OPEN) {" + NEWLINE +
" socket.send(message);" + NEWLINE +
" } else {" + NEWLINE +
" alert(\"The socket is not open.\");" + NEWLINE +
" }" + NEWLINE +
'}' + NEWLINE +
"</script>" + NEWLINE +
"<form onsubmit=\"return false;\">" + NEWLINE +
"<input type=\"text\" name=\"message\" " +
"value=\"Hello, World!\"/>" +
"<input type=\"button\" value=\"Send Web Socket Data\""
+ NEWLINE +
" onclick=\"send(this.form.message.value)\" />"
+ NEWLINE +
"<h3>Output</h3>" + NEWLINE +
"<textarea id=\"responseText\" " +
"style=\"width:500px;height:300px;\"></textarea>"
+ NEWLINE +
"</form>" + NEWLINE +
"</body>" + NEWLINE +
"</html>" + NEWLINE, CharsetUtil.US_ASCII);
}
}
2)、服务端启动代码:
public final class WebSocketServer {
/*创建 DefaultChannelGroup,用来保存所
有已经连接的 WebSocket Channel,群发和一对一功能可以用上*/
private final static ChannelGroup channelGroup =
new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
static final boolean SSL = false;//是否启用ssl
/*通过ssl访问端口为8443,否则为8080*/
static final int PORT
= Integer.parseInt(
System.getProperty("port", SSL? "8443" : "8080"));
public static void main(String[] args) throws Exception {
/*SSL配置*/
final SslContext sslCtx;
if (SSL) {
SelfSignedCertificate ssc = new SelfSignedCertificate();
sslCtx = SslContextBuilder.forServer(ssc.certificate(),
ssc.privateKey()).build();
} else {
sslCtx = null;
}
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new WebSocketServerInitializer(sslCtx,channelGroup));
Channel ch = b.bind(PORT).sync().channel();
System.out.println("打开浏览器访问: " +
(SSL? "https" : "http") + "://127.0.0.1:" + PORT + '/');
ch.closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
3)、服务端业务操作代码:
public class WebSocketServerInitializer
extends ChannelInitializer<SocketChannel> {
/*websocket访问路径*/
private static final String WEBSOCKET_PATH = "/websocket";
private final ChannelGroup group;
private final SslContext sslCtx;
public WebSocketServerInitializer(SslContext sslCtx,ChannelGroup group) {
this.sslCtx = sslCtx;
this.group = group;
}
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
if (sslCtx != null) {
pipeline.addLast(sslCtx.newHandler(ch.alloc()));
}
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpObjectAggregator(65536));
/*支持ws数据的压缩传输*/
pipeline.addLast(new WebSocketServerCompressionHandler());
pipeline.addLast(new WebSocketServerProtocolHandler(WEBSOCKET_PATH,
null,true));
/*浏览器访问的时候展示index页面*/
pipeline.addLast(new ProcessWsIndexPageHandler(WEBSOCKET_PATH));
/*对WS的数据进行处理*/
pipeline.addLast(new ProcesssWsFrameHandler(group));
}
}
4)、web页面返回给前端的设置:
public class ProcessWsIndexPageHandler
extends SimpleChannelInboundHandler<FullHttpRequest> {
private final String websocketPath;
public ProcessWsIndexPageHandler(String websocketPath) {
this.websocketPath = websocketPath;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx,
FullHttpRequest req) throws Exception {
// 处理错误或者无法解析的http请求
if (!req.decoderResult().isSuccess()) {
sendHttpResponse(ctx, req,
new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST));
return;
}
//只允许Get请求
if (req.method() != GET) {
sendHttpResponse(ctx, req,
new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN));
return;
}
// 发送index页面的内容
if ("/".equals(req.uri()) || "/index.html".equals(req.uri())) {
//生成WebSocket的访问地址,写入index页面中
String webSocketLocation
= getWebSocketLocation(ctx.pipeline(), req,
websocketPath);
System.out.println("WebSocketLocation:["+webSocketLocation+"]");
//生成index页面的具体内容,并送往浏览器
ByteBuf content
= MakeIndexPage.getContent(
webSocketLocation);
FullHttpResponse res = new DefaultFullHttpResponse(
HTTP_1_1, OK, content);
res.headers().set(HttpHeaderNames.CONTENT_TYPE,
"text/html; charset=UTF-8");
HttpUtil.setContentLength(res, content.readableBytes());
sendHttpResponse(ctx, req, res);
} else {
sendHttpResponse(ctx, req,
new DefaultFullHttpResponse(HTTP_1_1, NOT_FOUND));
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
/*发送应答*/
private static void sendHttpResponse(ChannelHandlerContext ctx,
FullHttpRequest req,
FullHttpResponse res) {
// 错误的请求进行处理 (code<>200).
if (res.status().code() != 200) {
ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(),
CharsetUtil.UTF_8);
res.content().writeBytes(buf);
buf.release();
HttpUtil.setContentLength(res, res.content().readableBytes());
}
// 发送应答.
ChannelFuture f = ctx.channel().writeAndFlush(res);
//对于不是长连接或者错误的请求直接关闭连接
if (!HttpUtil.isKeepAlive(req) || res.status().code() != 200) {
f.addListener(ChannelFutureListener.CLOSE);
}
}
/*根据用户的访问,告诉用户的浏览器,WebSocket的访问地址*/
private static String getWebSocketLocation(ChannelPipeline cp,
HttpRequest req,
String path) {
String protocol = "ws";
if (cp.get(SslHandler.class) != null) {
protocol = "wss";
}
return protocol + "://" + req.headers().get(HttpHeaderNames.HOST)
+ path;
}
}
5)、对数据进行处理的代码:
public class ProcesssWsFrameHandler
extends SimpleChannelInboundHandler<WebSocketFrame> {
private final ChannelGroup group;
public ProcesssWsFrameHandler(ChannelGroup group) {
this.group = group;
}
private static final Logger logger
= LoggerFactory.getLogger(ProcesssWsFrameHandler.class);
@Override
protected void channelRead0(ChannelHandlerContext ctx,
WebSocketFrame frame) throws Exception {
if(frame instanceof TextWebSocketFrame){
String request = ((TextWebSocketFrame)frame).text();
ctx.channel().writeAndFlush(
new TextWebSocketFrame(request.toUpperCase(Locale.CHINA)));
/*群发*/
group.writeAndFlush(
new TextWebSocketFrame(
"Client"+ctx.channel()+"say:"+request.toUpperCase(Locale.CHINA)
));
}else{
throw new UnsupportedOperationException("unsupport data frame");
}
}
/*重写 userEventTriggered()方法以处理自定义事件*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx,
Object evt) throws Exception {
/*检查事件类型,如果是ws握手成功事件,群发通知*/
if(evt == WebSocketServerProtocolHandler.
ServerHandshakeStateEvent.HANDSHAKE_COMPLETE){
group.writeAndFlush(
new TextWebSocketFrame("Client"+ctx.channel()+" joined"));
group.add(ctx.channel());
}
}
}
6)、客户端启动代码:
public final class WebSocketClient {
static final String URL
= System.getProperty("url",
"ws://127.0.0.1:8080/websocket");
static final String SURL
= System.getProperty("url",
"wss://127.0.0.1:8443/websocket");
public static void main(String[] args) throws Exception {
URI uri = new URI(URL);
String scheme = uri.getScheme() == null? "ws" : uri.getScheme();
final String host =
uri.getHost() == null? "127.0.0.1" : uri.getHost();
final int port = uri.getPort();
if (!"ws".equalsIgnoreCase(scheme)
&& !"wss".equalsIgnoreCase(scheme)) {
System.err.println("Only WS(S) is supported.");
return;
}
final boolean ssl = "wss".equalsIgnoreCase(scheme);
final SslContext sslCtx;
if (ssl) {
sslCtx = SslContextBuilder.forClient()
.trustManager(InsecureTrustManagerFactory.INSTANCE).build();
} else {
sslCtx = null;
}
EventLoopGroup group = new NioEventLoopGroup();
try {
// Connect with V13 (RFC 6455 aka HyBi-17). You can change it to V08 or V00.
// If you change it to V00, ping is not supported and remember to change
// HttpResponseDecoder to WebSocketHttpResponseDecoder in the pipeline.
final WebSocketClientHandler handler =
new WebSocketClientHandler(
WebSocketClientHandshakerFactory
.newHandshaker(
uri, WebSocketVersion.V13,
null,
true,
new DefaultHttpHeaders()));
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline p = ch.pipeline();
if (sslCtx != null) {
p.addLast(sslCtx.newHandler(ch.alloc(),
host, port));
}
p.addLast(
//http协议为握手必须
new HttpClientCodec(),
new HttpObjectAggregator(8192),
//支持WebSocket数据压缩
WebSocketClientCompressionHandler.INSTANCE,
handler);
}
});
//连接服务器
Channel ch = b.connect(uri.getHost(), port).sync().channel();
//等待握手完成
handler.handshakeFuture().sync();
BufferedReader console = new BufferedReader(
new InputStreamReader(System.in));
while (true) {
String msg = console.readLine();
if (msg == null) {
break;
} else if ("bye".equals(msg.toLowerCase())) {
ch.writeAndFlush(new CloseWebSocketFrame());
ch.closeFuture().sync();
break;
} else if ("ping".equals(msg.toLowerCase())) {
WebSocketFrame frame = new PingWebSocketFrame(
Unpooled.wrappedBuffer(new byte[] { 8, 1, 8, 1 }));
ch.writeAndFlush(frame);
} else {
WebSocketFrame frame = new TextWebSocketFrame(msg);
ch.writeAndFlush(frame);
}
}
} finally {
group.shutdownGracefully();
}
}
}
7)、客户端业务代码:
public class WebSocketClientHandler extends SimpleChannelInboundHandler<Object> {
//负责和服务器进行握手
private final WebSocketClientHandshaker handshaker;
//握手的结果
private ChannelPromise handshakeFuture;
public WebSocketClientHandler(WebSocketClientHandshaker handshaker) {
this.handshaker = handshaker;
}
public ChannelFuture handshakeFuture() {
return handshakeFuture;
}
//当前Handler被添加到ChannelPipeline时,
// new出握手的结果的实例,以备将来使用
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
handshakeFuture = ctx.newPromise();
}
//通道建立,进行握手
@Override
public void channelActive(ChannelHandlerContext ctx) {
handshaker.handshake(ctx.channel());
}
//通道关闭
@Override
public void channelInactive(ChannelHandlerContext ctx) {
System.out.println("WebSocket Client disconnected!");
}
//读取数据
@Override
public void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
Channel ch = ctx.channel();
//握手未完成,完成握手
if (!handshaker.isHandshakeComplete()) {
try {
handshaker.finishHandshake(ch, (FullHttpResponse) msg);
System.out.println("WebSocket Client connected!");
handshakeFuture.setSuccess();
} catch (WebSocketHandshakeException e) {
System.out.println("WebSocket Client failed to connect");
handshakeFuture.setFailure(e);
}
return;
}
//握手已经完成,升级为了websocket,不应该再收到http报文
if (msg instanceof FullHttpResponse) {
FullHttpResponse response = (FullHttpResponse) msg;
throw new IllegalStateException(
"Unexpected FullHttpResponse (getStatus=" + response.status() +
", content=" + response.content().toString(CharsetUtil.UTF_8) + ')');
}
//处理websocket报文
WebSocketFrame frame = (WebSocketFrame) msg;
if (frame instanceof TextWebSocketFrame) {
TextWebSocketFrame textFrame = (TextWebSocketFrame) frame;
System.out.println("WebSocket Client received message: " + textFrame.text());
} else if (frame instanceof PongWebSocketFrame) {
System.out.println("WebSocket Client received pong");
} else if (frame instanceof CloseWebSocketFrame) {
System.out.println("WebSocket Client received closing");
ch.close();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
if (!handshakeFuture.isDone()) {
handshakeFuture.setFailure(cause);
}
ctx.close();
}
}
8)、启动服务端,访问浏览器:
浏览器访问:
9)、启动客户端,并发送数据:
到此,WebSocket 通信原理和详细使用分析完毕,小伙伴们一定要多测试、多理解 一定会很快掌握的。