WebSocket 通信原理和详细使用(十六)

今天我们详细分析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 通信原理和详细使用分析完毕,小伙伴们一定要多测试、多理解 一定会很快掌握的。
  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

寅灯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值