Tomcat13——webSocket

1. 简介

        webSocket是HTML5新增的协议,是一个持久化的协议。它的目的是在浏览器和服务器之间建立一个不受限的双向通信通道。例如:服务器可以在任意时刻向浏览器发送消息。

        webSocket的出现,让浏览器和服务器之间可以建立无限制的全双工通信,任何一方都可以主动发消息给对方。wesocket并不是全新的协议,而是利用HTTP协议来建立连接的。

2. webSocket与Http

1. webSocket是一个持久化的协议,HTTP是不支持持久连接的(长连接,循环连接的不算)

2. HTTP有 1.1 和 1.0 之说,也就是所谓的 keep-alive ,把多个HTTP请求合并为一个,但是 Websocket 其实是一个新协议,跟HTTP协议基本没有关系,只是为了兼容现有浏览器的握手规范而已,也就是说它是HTTP协议上的一种补充

3. 在HTTP中,一个请求request只能有一个响应response,而且这个response也是被动的,不能主动发起。

4. Websocket只需要一次HTTP握手,所以说整个通讯过程是建立在一次连接/状态中,也就避免了HTTP的非状态性,服务端会一直知道你的信息,直到你关闭请求

5. webSocket连接必须由浏览器发起,因为请求协议是一个标准的HTTP请求。

浏览器向服务器发起请求,请求头带了一个Upgrade,Upgrade映射的是webSocket,服务器接收到该请求头之后,将请求转换成weSocket请求,状态码改成101,然后再给客户端响应数据。

wesocket请求和普通HTTP请求有几点不同:

1)Get请求的地址不是类似 http://  而是以 ws:// 开头的地址

2)请求头 Connection:Upgrade和请求头Upgrade:wesocket 表示这个链接将要被转换成websocket链接

3)Sec-WebSocket-Key是用于标识这个链接,是一个BASE64编码的秘文,要求服务端响应一个对应加密的Sec-WebSocket-Accept头信息作为应答

4)Sec-WebSocket-Version指定了WebSocket的协议版本

5)HTTP 101 状态码表明服务器已经识别并切换为webSocket协议,Sec-WebSocket-Accept是服务端与客户端一致的秘钥计算出来的信息。

3. Tomcat的webSocket

        tomcat7.0.5版本开始支持WebSocket,并且实现了Java WebSocket规范(JSR356),而在7.0.5版本之前(7.0.2之后)则采用自定义API,即WebSocketService来实现。

        Java WebSocket应用由一系列的WebSocketEndpoint组成。Endpoint是一个java对象,代表webSocket链接的一端,对于服务端,我们可以视为处理具体WebSocket消息的接口,就像Servlet与HTTP请求一样。

3.1 定义Endpoint的方式

1)编程式:继承类javax.websocket.Endpoint并实现其方法

2)注解式:定义一个类,在类上添加@ServerEndpoint注解,表明该类是一个webSocket类。

@ServerEndpoint(value="/websocket")
public class ChatSocket {

}

Endpoint实例在webSocket握手时创建,并在客户端与服务端链接过程中有效,最后在连接关闭时结束。在endpoint接口中明确定义了与其生命周期相关的方法,规范实现者确保生命周期的各阶段调用实例的相关方法。

tomcat中EndPoint类的定义:

package javax.websocket;

public abstract class Endpoint {

    public abstract void onOpen(Session session, EndpointConfig config);

    public void onClose(Session session, CloseReason closeReason) {
        // NO-OP by default
    }

    public void onError(Session session, Throwable throwable) {
        // NO-OP by default
    }
}

生命周期方法如下:

方法含义描述注解
onOpen当开启一个新的会话的时候调用,该方法是客户端与服务端握手成功后调用的方法@OnOpen
onClose当会话关闭时调用@OnClose
onError当连接过程中异常时调用@OnError

        我们发现,endpoint中没有接受消息的方法,因为webSocket接收消息采用的是消息处理器MessageHandler来接收消息。

        在编程式方式中,我们可以为每一个会话(webSocket 中的session)添加MessageHandler消息处理器来接收消息;

        当采用注解方式定义Endpoint时,通过@OnMessage注解指定接收消息的方式,当客户端有消息发送过来的时候,就会自动触发该方法,我们可以在onMessage这个方法中处理消息。

        发送消息则由RemoteEndpoint完成,其实例由session维护,根据使用情况,我们可以通过Session.getBasicRemote获取同步消息发送的实例,然后调用RemoteEndpoint的sendXxx()方法就可以发送消息,可以通过Session.getAsyncRemote获取异步消息发送实例。

4. webSocket的demo案例

4.1 业务需求

        实现一个简易的聊天室功能

4.2 实现流程时序图

4.3 传递消息的格式

4.3.1 客户端——>服务端

        {"fromName":"张三",“toName”:"李四","content":"干嘛了?"}

4.3.2 服务端——>客户端

1)如果type为User,则说明返回的是用户列表

        {"data":"张三,李四,王五","toName":"","fromName":"","type":"user"}

2)如果type为message,则返回的是消息内容

        {"data":"你好","toName":"张三","fromName":"李四","type":"message"}

4.4 准备工作

1)创建项目,导入依赖

2)登录页面login.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
    <script src="js/jquery-1.8.2.js"></script>
</head>
<body>
    <form name="loginForm">
        用户名:<input type="text" name="username" id="username"/> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
        密码:<input type="password" name="password" id="password" /> <br/>
        <input type="button" value="登录" onclick="login()"/>
    </form>
</body>
<script type="text/javascript">
    function login() {
        $.ajax({
            type:'post',
            url:'/chartRoom/login',
            dataType:'json',
            data:{
                username:$("#username").val(),
                password:$("#password").val()
            },
            success:function(data){
                if(data.success){
                    window.location.href="chat.jsp";
                }else{
                    alert(data.message);
                }
            }
        });
    }
</script>
</html>

3)定义登录Servlet

简单实现,获取页面传递的用户名和密码,只要传递的聊天室密码是123456,则认为正确,允许登录。

package com.bjc.servlet;

import com.alibaba.fastjson.JSON;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@WebServlet(name="loginServlet",urlPatterns = "/login")
public class LoginServlet extends HttpServlet {

    private static final String PASSWORD = "123456";
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 设置响应字符集
        resp.setCharacterEncoding("UTF-8");

        // 1. 接收页面传递的参数
        String username = req.getParameter("username");
        String password = req.getParameter("password");

        Map<String,Object> restltMap = new HashMap<>();
        // 2. 判定用户名密码是否正确
        if(PASSWORD.equals(password)){ // 3. 如果正确,给客户端响应登录成功的信息
            restltMap.put("success",true);
            restltMap.put("message","登录成功!");

            // 保存用户信息到session
            req.getSession().setAttribute("username",username);

        } else { // 4. 如果不正确,响应登录失败的信息
            restltMap.put("success",false);
            restltMap.put("message","登录失败,用户名或密码错误!");
        }
        // 将数据以json格式响应给前端
        resp.getWriter().write(JSON.toJSONString(restltMap));
    }
}

4.5 OnOpen测试

4.5.1 页面准备

chat.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
    <link rel="stylesheet" type="text/css" href="css/main.css"/>
    <script src="js/jquery-1.8.2.js"></script>
</head>
<body onload="startWebSocket(self);" style="TEXT-ALIGN: center;">

    <h2 style="background-color: greenyellow">用户 ${username}</h2>
    <div id="contentBody" class="contentBody">
        <div class="leftDiv">
            <div class="msgShow">
                nihao s
            </div>
            <div clas="bottomDiv">
                <div class="textArea">
                    <textarea name="content" id="content" ></textarea>
                    <input type="button" value="发送" id="btnSend" />
                </div>
            </div>
        </div>
        <div class="rightDiv">
            <div class="up">
                <div style="border-bottom: 1px solid #000;">好友列表</div>
                <div id="userList"></div>
            </div>
            <div class="down">
                <div style="border-bottom: 1px solid #000;">系统广播</div>
                <div id="broadcastList"></div>
            </div>
        </div>
    </div>
</body>
<script type="text/javascript">
    <%
        String name = session.getAttribute("username") + "";
    %>
    var self = "<%=name%>";

    var ws;
    function startWebSocket(self) {
        var url = "ws://localhost:8080/chartRoom/chatWeSocket"
        if ("WebSocket" in window) {
            ws = new WebSocket(url);
        } else if ("MozWebSocket" in window) {
            ws = new MozWebSocket(url);
        } else {
            alert("浏览器版本过低,请升级您的浏览器");
        }

        // 监听消息,有消息传递,会触发此方法
        ws.onmessage = function(evt){
            var _data = evt.data;
            console.log("data >> " + _data);

            var o = JSON.parse(_data);
            if(o.type == 'message'){ // 如果后端响应的是消息,在页面展示
                setMessageInnerHTML(o,self);
            } else if(o.type == 'user'){ // 如果客户端相应的是用户列表,在界面显示用户列表
                var userArray = o.data.split(",");
                $("#userList").empty();
                $("#userList").append('<li><input type="radio" name="toUser" value="All">广播</li>');
                $.each(userArray,function(n,value){
                    if(value != self && value != 'admin'){
                        $("#userList").append('<li><input type="radio" name="toUser" value="'+value+'" />' + value + '</li>');

                        $("#broadcastList").append('<li>您的好友 '+value+' 上线啦!</li>');
                    }

                });
            }

            // 关闭连接的时候触发
            ws.onclose = function(evt){
                $("#username").html("用户" + self + "离线了");
            }

            ws.onopen = function(evt){
                $("#username").html("用户" + self + "上线了");
            }

        }
    }

    function setMessageInnerHTML(o,self){
        console.log("o = " + o);
    }

    $("#btnSend").click(function () {
        if (ws.readyState == WebSocket.OPEN) {
            var message = "{\"name\":\"" + $("#txtName").val() + "\",\"message\":\"" + $("#txtInput").val() + "\"}";
            ws.send(message);
        }
        else {
            $("#msg").text("Connection is Closed!");
        }
    });
</script>
</html>

css文件main.css

.contentBody {
    display: flex;
    justify-content: space-between;
    border:1px solid #000;
    margin: auto;
    padding: 10px;
    width: 800px;
    height: 500px;
}

.leftDiv {
    border:1px solid #000;
    margin: auto;
    padding: 10px;
    width: 500px;
    height: 450px;
}

.rightDiv {
    border:1px solid #000;
    margin: auto;
    padding: 10px;
    width: 250px;
    height: 450px;
}

.up {
    border:1px solid #000;
    margin: auto;
    padding: 10px;
    height: 250px;
}

.down {
    border:1px solid #000;
    margin: auto;
    padding: 10px;
    height: 150px;
}

.msgShow {
    width: 500px;
    height: 350px;
}

.bottomDiv {
    width: 500px;
    height: 180px;
}
.textArea {
    margin-bottom: 50px;
    padding: 0;
    width: 100%;
    height: 200px;
}
#content {
    width: 100%;
    height: 100px;
}

4.5.2 后端代码实现

1)MessageUtil工具类

该工具类用于处理后台返回前台的数据

package com.bjc.util;

import com.alibaba.fastjson.JSON;

import java.util.HashMap;
import java.util.Map;

public class MessageUtil {
    public final static String TYPE = "type";
    public final static String DATA = "data";
    public final static String FROM_NAME = "fromName";
    public final static String TO_NAME = "toName";

    public final static String TYPE_MESSAGE = "message";
    public final static String TYPE_USER = "user";

    // 组装消息,然后返回一个json格式的消息数据
    public static String getContent(String type,String fromName,String toName,String content) {
        Map<String,Object> userMap = new HashMap<>();
        userMap.put(MessageUtil.TYPE,type);
        userMap.put(MessageUtil.DATA,content);
        userMap.put(MessageUtil.FROM_NAME,fromName);
        userMap.put(MessageUtil.TO_NAME,toName);

        String jsonMsg = JSON.toJSONString(userMap);
        return jsonMsg;
    }

}

2)HTTPSessionConfigurator

该类是一个配置类,继承自ServerEndpointConfig.Configurator,在@ServerEndpoint注解上需要使用到

package com.bjc.webSocket;

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 {

    /**
      在客户端与webSocket在进行第一次握手的过程,获取HttpSession
     */
    @Override
    public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response) {
        // 1. 拿到HTTPSession
        HttpSession httpSession = (HttpSession)request.getHttpSession();
        // 2. 将HTTPSession存储在ServerEndpointConfig对象中
        config.getUserProperties().put(HttpSession.class.getName(),httpSession);
    }
}

3)webSocket逻辑处理类

ChatSocket.java

package com.bjc.webSocket;

import com.bjc.util.MessageUtil;

import javax.servlet.http.HttpSession;
import javax.websocket.EndpointConfig;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@ServerEndpoint(value="/chatWeSocket",configurator = HTTPSessionConfigurator.class)
public class ChatSocket {

    // 记录websocket的会话对象session
    private Session session;

    // 记录当前用户的Servlet的HttpSession
    private HttpSession httpSession;

    /**
     * map中存放的是key为HTTPSession,值为Endpoint的实例
     * 因为Endpoint的实例是在第一次握手的时候创建的,且每一个客户端都拥有一个Endpoint的实例对象
     * 而我们的ChatSocket实际上是实现了ChatSocket的类,所以,ChatSocket就是一个Endpoint
     * 该map可以用于记录当前登录用户的HttpSession信息,及对应的Endpoint实例信息。
     */
    private static Map<HttpSession,ChatSocket> onLineUsers = new HashMap<>();

    // 记录当前登录用户数
    private static int onLineCount = 0;

    /**
     * 表示只要请求的是 chatWeSocket ,且是第一次请求就会触发该方法
     * 这里的config就是HTTPSessionConfigurator类中modifyHandshake方法的形参ServerEndpointConfig
     */
    @OnOpen
    public void onOpen(Session session, EndpointConfig config){
        // 1. 记录webSocket的会话信息对象session
        this.session = session;

        // 2. 获取当前登录用户的HttpSession信息
        HttpSession httpSession = (HttpSession)config.getUserProperties().get(HttpSession.class.getName());
        this.httpSession = httpSession;

        System.out.println("当前登录用户:" + httpSession.getAttribute("username") + ", Endpoint : " +hashCode() + " , 在线用户数:" + onLineUsers.size());

        // 3. 记录当前登录用户信息,及对应的Endpoint实例
        if(httpSession.getAttribute("username") != null){
            onLineUsers.put(httpSession,this);
        }

        // 4. 获取当前所有登录用户
        String names = getNames();

        // 5.组装消息
        String messages = MessageUtil.getContent(MessageUtil.TYPE_USER, "", "", names);

        // 6. 通过广播的形式发送消息
        /*
            发送单挑消息
            发送消息则由RemoteEndpoint完成,其实例由session维护,根据使用情况,
            我们可以通过Session.getBasicRemote获取同步消息发送的实例,
            然后调用RemoteEndpoint的sendXxx()方法就可以发送消息
        * */
        // session.getBasicRemote().sendText(message);
        // 广播就是给所有用户发送信息,我们在onLineUsers中有存储所有在线用户的endpoint的实例信息
        broadcastAllUsers(messages);

        // 7. 记录当前用户登录数
        incrCount();

    }

    // 广播的形式发送消息
    private void broadcastAllUsers(String messages) {
        for(HttpSession s : onLineUsers.keySet()){
            ChatSocket chatSocket = onLineUsers.get(s);
            try {
                chatSocket.session.getBasicRemote().sendText(messages);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    // 获取在线用户
    private String getNames() {
        StringBuilder names = new StringBuilder();
        if(onLineUsers.size() > 0){
            for(HttpSession h : onLineUsers.keySet()){
                String username = (String)h.getAttribute("username");
                if(names.length() == 0){
                    names.append(username);
                } else {
                    names.append("," + username);
                }
            }
        }
        return names.toString();
    }

    public int getOnLineCount(){
        return onLineCount;
    }

    public synchronized void incrCount(){
        onLineCount ++;
    }

    public synchronized void decrCount(){
        onLineCount --;
    }
}

4.5.3 测试结果

在谷歌浏览器和360浏览器分别输入:http://localhost:8080/chartRoom/,输入用户名和密码,之后进入聊天界面,如图:

4.6 OnMessage

4.6.1 定义onMessage事件方法

/**
     * @param message  客户端传递过来的消息内容
     * @param session  当前会话对象
     */
    @OnMessage
    public void onMessage(String message,Session session){
        System.out.println("onMessage: name = " + httpSession.getAttribute("username") + " message内容:" + message );
        // 1. 获取客户端消息内容并解析
        Map<String,String> messageMap = JSON.parseObject(message, Map.class);
        String fromName = messageMap.get("fromName");   // 发送人
        String toName = messageMap.get("toName");       // 接收人
        String content = messageMap.get("content");     // 发送内容

        // 2. 判定是否存在接收人
        if(null == toName || toName.isEmpty()){
            return;
        }
        // 组装消息内容
        String msgContent = MessageUtil.getContent(MessageUtil.TYPE_MESSAGE, fromName, toName, message);
        System.out.println("服务端给客户端发送消息,消息内容:" + msgContent);
        if("All".equals(toName)){  // 3. 如果接收人是广播(All),如果是,则说明发送广播消息
            broadcastAllUsers(msgContent);
        } else {  // 4. 如果不是广播,则给指定的用户推送消息
            singlePushMsg(msgContent,fromName,toName);
        }
    }

    // 给指定用户发送消息
    private void singlePushMsg(String msgContent,String fromName,String toName) {
        // 定义标记字段,标记toName是否在线
        boolean isOnline = false;
        // 定义toName对应的EndPoint实例对象
        ChatSocket chatSocket2 = null;
        // 定义fromName对应的EndPoint实例对象
        ChatSocket chatSocket4 = null;

        // 1. 判断当前接收人是否在线
        for(HttpSession h : onLineUsers.keySet()){
            if(toName.equals(h.getAttribute("username"))){
                isOnline = true;
                chatSocket2 = onLineUsers.get(h);
            }
            if(fromName.equals(h.getAttribute("username"))){
                chatSocket4 = onLineUsers.get(h);
            }
        }

        // 2. 如果存在,发送消息
        if(isOnline){
            try {
                // 分别给发送人和接收人发送消息
                chatSocket2.session.getBasicRemote().sendText(msgContent);
                chatSocket4.session.getBasicRemote().sendText(msgContent);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

4.6.2 前台代码

1)发送消息的js

$("#btnSend").click(function () {
        var content = $("#content").val();
        if(!content){
            alert("请输入内容!");
            return;
        }
        var message = {};
        message.fromName = self;
        message.toName = $("input:radio:checked").val() ? $("input:radio:checked").val() : "All";
        message.content = content;
        var msg = JSON.stringify(message);
        console.log(msg);
        ws.send(msg);

    });

2)显示消失的js

function setMessageInnerHTML(o,self){
        var data = JSON.parse(o.data);
        if(self == data.fromName){
            $("#msgShow").append("<div style='margin-left: 10px;text-align: left'> "+ data.fromName +" 说: "+data.content+"</div>");
        } else {
            $("#msgShow").append("<div style='margin-right: 10px;text-align: right'> "+ data.fromName +" 说: "+data.content+"</div>");
        }
    }

4.6.3 onMessage测试

分别打开三个浏览器,输入网址http://localhost:8080/chartRoom

用户:张三、李四、王五 如图:

登录进去之后,首先张三广播一条消息,如图:

发现,都可以收到该消息,广播成功,然后张三给李四发一条消息,如图:

发现,李四成功的收到了张三放松的私信,王五并没有收到。

4.7 onClose与onError

 /**
     * wesocket关闭连接的处理函数
     * @param session
     * @param closeReason
     */
    @OnClose
    public void onClose(Session session, CloseReason closeReason) {
        // 关闭之后,在线用户数减1
        decrCount();
        System.out.println("客户端关闭了一个链接,当前在线人数:" + getOnLineCount());
    }

    /**
     * websocket出现异常的处理函数
     * @param session
     * @param throwable
     */
    @OnError
    public void onError(Session session, Throwable throwable) {
        throwable.printStackTrace();
        System.out.println("服务异常");
    }

完整的demo工程的GitHub上,链接:https://github.com/zoudmbean/demoRepo.git

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值