websocket实现多房间聊天室


  Web 应用的交互过程通常是客户端通过浏览器发出一个请求,服务器端接收请求后进行处理并返回结果给客户端,客端浏览器将信息呈现。但是对于实时性要求较高、海量并发的应用,比如金融证券的实时信息,web导航应用中地理位置获取,社交网络的实时消息推送等。

解决方案

方案一:客户端用js代码每隔一定时间向服务器发送请求,这样会造成资源浪费,在高并发的情况下还可能造成服务器奔溃。

方案二:基于Flash、AdobeFlash,通过socket实现数据信息交互,再利用Flash暴露的接口供js调用,但是Flash在移动互联网上的支持不好,IOS和Android都不支持Flash了。

方案三:WebSocket,2014年开始,各大应用服务器和浏览器厂商逐步统一,J2EE7也实现了WebSocket协议,无论客户端还是服务器都提供了对其的支持。

WebSocket介绍与原理

WebSocket 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯,它建立在 TCP 之上,同 HTTP 一样通过 TCP 来传输数据,但是它和HTTP 最大不同是:

WebSocket 是一种双向通信协议,在基于http建立连接后,WebSocket 服务器和 browser都能主动向对方发送或接收数据,就像 Socket 一样;WebSocket 需要类似 TCP 的客户端和服务器端通过握手连接,连接成功后才能相互通信,实现长连接。

WebSocket 客户端连接报文
GET /webfin/websocket/ HTTP/1.1

Host: localhost
Upgrade: websocket

Connection: Upgrade
Sec-WebSocket-Key: xqBt3ImNzJbYqRINxEFlkg==

Origin: http://localhost:8080

Sec-WebSocket-Version: 13


可以看到,客户端发起的 WebSocket 连接报文类似传统 HTTP 报文,”Upgrade:websocket”参数值表明这是 WebSocket 类型请求,“Sec-WebSocket-Key”是 WebSocket 客户端发送的一个 base64 编码的密文,要求服务端必须返回一个对应加密的“Sec-WebSocket-Accept”应答,否则客户端会抛出“Error during WebSocket handshake”错误,并关闭连接。

WebSocket 服务端响应报文
HTTP/1.1 101 Switching Protocols

“Sec-WebSocket-Accept”的值是服务端采用与客户端一致的密钥计算出来后返回客户端的,“HTTP/1.1 101 Switching Protocols”表示服务端接受 WebSocket 协议的客户端连接,经过这样的请求-响应处理后,客户端服务端的 WebSocket 连接握手成功, 后续就可以进行 TCP 通讯了。

下载javax.websocket.jar,使用注解方式实现了一个简单的多房间聊天demo,demo只有一个服务端类和一个前端chat.html页面,打开多个chat.html页面,输入相同的房间名,进入房间后可以相互通信,不同房间不能互相通信,不同用户我用websocket的session自己分配的id来区分,因为一个用户连接到webSocket服务器就对应一个session,实际开发可以用http的session中登录的用户名来区分,连接到服务器的url中,roomName是一个路径参数,即在chat.html中获取到房间名。多房间的原理其实就是把多个用(session)放在roomName对应的set集合中,每次广播信息只在房间名对应的set集合中广播,实现房间聊天信息的隔离。

代码如下:

package cn.com.taiji.controller;

import cn.com.taiji.util.StringUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.net.URLDecoder;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;


/**
 * @ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个websocket服务器端,
 * 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端
 */
@ServerEndpoint(value = "/websocket/{userJson}")
@Component
public class MyWebSocket {

    static Log log= LogFactory.getLog(MyWebSocket.class);

    //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
    private static int onlineCount = 0;

    // 使用map来收集session,key为roomName,value为同一个房间的用户集合
    // concurrentMap的key不存在时报错,不是返回null
    //以案号为key作为一个房间
    private static Map<String, ConcurrentHashMap<String,MyWebSocket>> rooms = new ConcurrentHashMap();

    //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识
//    private static CopyOnWriteArraySet<MyWebSocket> webSocketSet = new CopyOnWriteArraySet<MyWebSocket>();
    //记录每个房间的
//    private static ConcurrentHashMap<String,MyWebSocket> webSocketMap = new ConcurrentHashMap<>();
    /**
     *  与某个客户端的连接对话,需要通过它来给客户端发送消息
     */
    private Session session;
    /**
     * 标识当前连接客户端的用户名
     */
    private String userName;
    /**
     * 标识当前连接客户端的房间名称
     */
    private String roomName;

    /**
     * 连接建立成功调用的方法
     * @param session  可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("userJson") String userJson){
        try{
            //解析发送的报文
            String[] split = userJson.split(",");
            this.roomName =  split[0].trim();
            if(split.length>=1){
                this.userName = split[1].trim();
            }
            this.session = session;
            // 将session按照房间名来存储,将各个房间的用户隔离
            if (!rooms.containsKey(roomName)) {
                // 对应房间不存在时,创建房间
                ConcurrentHashMap<String,MyWebSocket> room = new ConcurrentHashMap<>();
                // 添加用户
                if (StringUtil.isEmpty(this.userName)){
                    room.put(session.getId(),this);
                }else {
                    room.put(userName,this);
                }
                rooms.put(roomName, room);
            } else {
                // 房间已存在,直接添加用户到相应的房间
                rooms.get(roomName).put(userName,this);
            }
            addOnlineCount();//在线数加1
            log.info("有新连接加入!当前在线人数为" + getOnlineCount());
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    /**
     * 收到客户端消息后调用的方法
     * @param msg 客户端发送过来的消息
     * @param session 可选的参数
     */
    @OnMessage
    public void receiveMsg(String msg, Session session) throws Exception {
        // 此处应该有html过滤
        // 接收到信息后进行广播
        String roomName = this.roomName;
        broadcast(roomName, msg);
    }

    // 按照房间名进行广播
    public static void broadcast(String roomName, String msg) throws Exception {
        ConcurrentHashMap<String, MyWebSocket> map = rooms.get(roomName);
        for(String key:map.keySet()){//keySet获取map集合key的集合  然后在遍历key即可
            try{
                MyWebSocket myWebSocket = map.get(key);
                myWebSocket.sendMessage(msg);//
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose(@PathParam("roomName") String roomName, Session session){
        String userName = this.userName;
        if (StringUtil.isEmpty(userName)){
            rooms.get(roomName).remove(session.getId());
        }else {
            rooms.get(roomName).remove(userName);
        }
        subOnlineCount();
        log.info("用户退出:"+userName+",当前在线人数为:" + getOnlineCount());
    }


    /**
     * 发生错误时调用
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error){
        log.error("发生错误");
        error.printStackTrace();
    }

    /**
     * 这个方法与上面几个方法不一样。没有用注解,是根据自己需要添加的方法。
     * @param message
     * @throws IOException
     */
    public void sendMessage(String message) throws IOException{
        this.session.getBasicRemote().sendText(message);
        //this.session.getAsyncRemote().sendText(message);
    }

    public static synchronized int getOnlineCount() {
        return onlineCount;
    }

    public static synchronized void addOnlineCount() {
        MyWebSocket.onlineCount++;
    }

    public static synchronized void subOnlineCount() {
        MyWebSocket.onlineCount--;
    }

    private boolean isjson(String str) {
        try {
            JSONObject jsonStr = JSONObject.parseObject(str);
            return true;
        }catch (Exception e) {
            return false;
        }
    }
}

 

页面代码

<%@ page language="java" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>网络聊天室</title>
</head>
<style type="text/css">
    .msg_board {
        width: 322px;
        height: 100px;
        border: solid 1px darkcyan;
        padding: 5px;
        overflow-y: scroll;
    // 文字长度大于div宽度时换行显示
    word-break: break-all;
    }
    /*set srcoll start*/
    ::-webkit-scrollbar
    {
        width: 10px;
        height: 10px;
        background-color: #D6F2FD;
    }
    ::-webkit-scrollbar-track
    {
        -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
        /*border-radius: 5px;*/
        background-color: #D6F2FD;
    }
    ::-webkit-scrollbar-thumb
    {
        height: 20px;
        /*border-radius: 10px;*/
        -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
        background-color: #89D7F7;
    }
    /*set srcoll end*/
</style>
<body>
<label>房间名</label>
<input id="userName" type="hidden" name="userName" value="${userName}">
<input id="dsrdw" type="hidden" name="dsrdw" value="${dsrdw}">
<input id="input_roomName" type="text" value="${caseNum}">
<button onclick="initWebSocket()">进入聊天室</button>
<button onclick="closeWs()">退出聊天室</button>
<div class="msg_board"></div>
<input id="input_msg" size="43" maxlength="40">
<button onclick="send_msg()">发送</button>
</body>
<script type="text/javascript">
    var webSocket = null;

    function send_msg() {
        var t = "";
        if (webSocket != null) {
            var input_msg = document.getElementById("input_msg").value.trim();
            if (input_msg == "") {
                return;
            }
            webSocket.send(input_msg);
            // 清除input框里的信息
            document.getElementById("input_msg").value = "";
            // var msg_board = document.getElementsByClassName("msg_board")[0];
            // var received_msg = input_msg;
            // var old_msg = msg_board.innerHTML;
            // msg_board.innerHTML = old_msg + received_msg + "<br>";
            // // 让滚动块往下移动
            // msg_board.scrollTop = msg_board.scrollTop + 40;
        } else {
            alert("您已掉线,请重新进入聊天室...");
        }
    };

    function closeWs() {
        webSocket.close();
    };

    function initWebSocket() {
        var roomName = document.getElementById("input_roomName").value;
        var userName = document.getElementById("userName").value;
        // 房间名不能为空
        if (roomName == null || roomName == "") {
            alert("请输入房间名");
            return;
        }
        var userJson = roomName+","+userName;
        if ("WebSocket" in window) {
            if (webSocket == null) {
                var url = "ws://127.0.0.1:8080/websocket/" + userJson;
                // 打开一个 web socket
                webSocket = new WebSocket(url);
            } else {
                alert("您已进入聊天室...");
            }
            webSocket.onopen = function () {
                alert("已进入聊天室,畅聊吧...");
            };

            webSocket.onmessage = function (evt) {
                var msg_board = document.getElementsByClassName("msg_board")[0];
                var received_msg = evt.data;
                var old_msg = msg_board.innerHTML;
                msg_board.innerHTML = old_msg + received_msg + "<br>";
                // 让滚动块往下移动
                msg_board.scrollTop = msg_board.scrollTop + 40;
            };

            webSocket.onclose = function () {
                // 关闭 websocket,清空信息板
                alert("连接已关闭...");
                webSocket = null;
                document.getElementsByClassName("msg_board")[0].innerHTML = "";
            };
        }
        else {
            // 浏览器不支持 WebSocket
            alert("您的浏览器不支持 WebSocket!");
        }
    }
</script>
</html>

 


原文链接:https://blog.csdn.net/zhengholien/java/article/details/76696509


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM