История WebSocket (5) — В Springboot реализовать собственный агент сообщений для веб-чатов.

задняя часть JavaScript WebSocket продукт

Обзор

Серию рассказов о WebSocket планируется разделить на пять глав и шесть глав, чтобы представить WebSocket и узнать, как быстро создавать и использовать возможности, предоставляемые WebSocket в Springboot. В эту серию планов входят следующие статьи:

Часть 1, что такое WebSocket и для чего он используется
Вторая статья, как использовать STOMP для быстрой сборки режима широковещательных сообщений WebSocket в Spring
Третья статья в Springboot о том, как использовать WebSocket и STOMP для быстрого создания режима двухточечных сообщений (1)
Четвертая часть, в Springboot, как использовать WebSocket и STOMP для быстрого создания режима сообщений точка-точка (2)
Пятая статья в Springboot реализует собственный агент сообщений WebSocket для веб-чатов.
Шестая статья в Springboot реализует более гибкий WebSocket.

Основная линия этой статьи

В этой статье подробно описывается, как использовать WebSocket для достижения некоторых конкретных функций продукта с помощью почти реальной демонстрации в веб-чате. В этой статье будет использоваться только сам WebSocket, STOMP и другие пакеты больше не используются. Практическая реализация приема, обработки, отправки сообщений и управления сеансом WebSocket.Это также самая важная статья в этой серии. Будьте ли вы взволнованы или нет, я все равно взволнован..下面我们就开始。

Студенты и начинающие молодые люди, которые хотят знать, как настраивать и реализовывать более сложную логику продукта WebSocket в Springboot.

Потребности небольших веб-чатов

Чтобы четко выразить технические моменты, которые будут описаны в этой статье, я разработал небольшой продукт для чата и сначала перечислил требования, чтобы каждый мог понять, почему, когда позже будет смотреть на реализацию.

Вышесказанное — это то, чего мы хотим достичь в этой статье. Проще говоря, это:

Пользователи могут присоединяться и выходить из комнаты, а после присоединения они могут отправить сообщение всем в комнате или отправить личное сообщение определенному человеку..

Анализ спроса и проектирование

Дизайн пользовательского хранилища

Легко подумать, что основной частью нашего дизайна являются пользователи, сеансы и комнаты, тогда в управлении пользователями мы можем использовать следующую диаграмму для представления отношений между ними:

Таким образом, мы можем использовать простую карту для хранения房间<->用户组Такие отношения сопоставления в группе пользователей мы используем для хранения Map用户名<->会话SessionЭти сопоставления (при условии отсутствия повторяющихся имен). Таким образом, мы можем решать комнаты и группы пользователей, пользователей и сеансы, хранить и поддерживать эти отношения.

Проектировать отношения между поведением пользователя и пользователями

Братья видят, что говорите: «Вы так долго говорите, с несколькими договор о том, что топом, какое информационное агентство, отношения шерсти?» Большой брат вы, чтобы высказать свой гнев, мы изучаем топку, научно-информационное агентство, школу Спиерские сообщения, важно, чтобы школа мысли, не говоришь? Здесь мы использовали.

Когда пользователь присоединяется к комнате, если в комнате возникают какие-либо проблемы, то есть кто-то присоединяется, выходит или отправляет общедоступное экранное сообщение, пользователь будет «уведомлен». На данный момент мы можем понимать создание комнаты как "Создание брокера сообщений", добавить пользователя в комнату, как если бы это была комната для этого"брокер сообщений"один из"подписка", рассматривать пользователя, выходящего из комнаты, как если бы комната была "брокер сообщений"один из"отписаться".

Затем первого человека, присоединившегося к комнате, мы определяем как "Создать комнату", который создает брокера сообщений. Для лучшего понимания приведенное выше изображение:

Красный человечек представляет первого пользователя, присоединившегося к комнате, то есть человека, создавшего комнату. Когда пользователь отправляет сообщение, если он решит отправить сообщение всем в комнате чата, это эквивалентно отправке трансляции в комнате, и все пользователи, подписавшиеся на эту комнату, получат широковещательное сообщение; если он решит отправить личное сообщение, затем отправлять сообщения только пользователям с определенным именем пользователя, т. е. одноранговый обмен сообщениями.

Резюмируя основные моменты, которых мы хотим достичь:

  • Хранилище пользователей, то есть отношения между пользователями, комнатами, сеансами и доступом к объектам.
  • Динамически создавать брокер сообщений (комнату) и реализовывать привязку пользователя (подписку) к комнате.
  • Возможность индивидуальной отправки сообщений пользователю.

На этом общий дизайн закончился, остались еще некоторые детали, давайте сначала посмотрим на демонстрационный эффект, а потом посмотрим на реализацию через код.

Чат Показать результаты

После открытия клиентской страницы в браузере отображается поле ввода и кнопка присоединения. входитьномер номер 1и имя пользователяСяомин,Нажмите, чтобы войти в комнату.

После успешного входа в комнату покажитеТекущая занятость номера и приветствие.

Показывать уведомления, когда кто-то еще присоединяется к комнате или покидает ее. Вы можете отправлять общедоступные экранные сообщения и сообщения в приватном чате.

Давайте посмотрим, как реализованы эти основные функции.

Код

В соответствии с нашим вышеприведенным дизайном я сосредоточусь на дизайне кода и технических моментах ключевых частей.

Реализация сервера

1. Настройте веб-сокет

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new MyHandler(), "/webSocket/{INFO}").setAllowedOrigins("*")
                .addInterceptors(new WebSocketInterceptor());
    }
}

Анализ ключевых моментов:

  • регистрWebSocketHandler(MyHandler), это класс, используемый для управления установлением WebSocket и обработкой сообщений, которые будут подробно описаны позже.
  • регистрWebSocketInterceptorInterceptor, перехватчик, запускаемый в первый раз при подключении к серверу на клиенте, клиент перехватывает записываемую информацию.
  • Зарегистрируйте адрес WebSocket и приходите с{INFO}Параметр, используемый для переноса информации о пользователе при регистрации.

Вышеизложенное будет подробно описано в последующем коде.

2. Реализуйте перехватчик рукопожатий

public class WebSocketInterceptor implements HandshakeInterceptor {
    @Override
    public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map<String, Object> map) throws Exception {
        if (serverHttpRequest instanceof ServletServerHttpRequest) {
            String INFO = serverHttpRequest.getURI().getPath().split("INFO=")[1];
            if (INFO != null && INFO.length() > 0) {
                JSONObject jsonObject = new JSONObject(INFO);
                String command = jsonObject.getString("command");
                if (command != null && MessageKey.ENTER_COMMAND.equals(command)) {
                    System.out.println("当前session的ID="+ jsonObject.getString("name"));
                    ServletServerHttpRequest request = (ServletServerHttpRequest) serverHttpRequest;
                    HttpSession session = request.getServletRequest().getSession();
                    map.put(MessageKey.KEY_WEBSOCKET_USERNAME, jsonObject.getString("name"));
                    map.put(MessageKey.KEY_ROOM_ID, jsonObject.getString("roomId"));
                }
            }
        }
        return true;
    }
}

Анализ ключевых моментов:

  • HandshakeInterceptorИспользуется для перехвата клиента запроса сначала подключает сервер, то есть. Клиент подключен/webSocket/{INFO}мы можем получить соответствующийINFOИнформация.
  • выполнитьbeforeHandshakeспособ сохранения информации о пользователе, здесь мы сохраняем имя пользователя и номер комнаты вSessionначальство.

3. Реализовать обработчик сообщений WebSocketHandler

public class MyHandler implements WebSocketHandler {

    //用来保存用户、房间、会话三者。使用双层Map实现对应关系。
    private static final Map<String, Map<String, WebSocketSession>> sUserMap = new HashMap<>(3);

    //用户加入房间后,会调用此方法,我们在这个节点,向其他用户发送有用户加入的通知消息。
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        System.out.println("成功建立连接");
        String INFO = session.getUri().getPath().split("INFO=")[1];
        System.out.println(INFO);
        if (INFO != null && INFO.length() > 0) {
            JSONObject jsonObject = new JSONObject(INFO);
            String command = jsonObject.getString("command");
            String roomId = jsonObject.getString("roomId");
            if (command != null && MessageKey.ENTER_COMMAND.equals(command)) {
                Map<String, WebSocketSession> mapSession = sUserMap.get(roomId);
                if (mapSession == null) {
                    mapSession = new HashMap<>(3);
                    sUserMap.put(roomId, mapSession);
                }
                mapSession.put(jsonObject.getString("name"), session);
                session.sendMessage(new TextMessage("当前房间在线人数" + mapSession.size() + "人"));
                System.out.println(session);
            }
        }
        System.out.println("当前在线人数:" + sUserMap.size());
    }

    //消息处理方法
    @Override
    public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage<?> webSocketMessage) {
        try {
            JSONObject jsonobject = new JSONObject(webSocketMessage.getPayload().toString());
            Message message = new Message(jsonobject.toString());
            System.out.println(jsonobject.toString());
            System.out.println(message + ":来自" + webSocketSession.getAttributes().get(MessageKey.KEY_WEBSOCKET_USERNAME) + "的消息");
            if (message.getName() != null && message.getCommand() != null) {
                switch (message.getCommand()) {
                        //有新人加入房间信息
                    case MessageKey.ENTER_COMMAND:
                        sendMessageToRoomUsers(message.getRoomId(), new TextMessage("【" + getNameFromSession(webSocketSession) + "】加入了房间,欢迎!"));
                        break;
                        //聊天信息
                    case MessageKey.MESSAGE_COMMAND:
                        if (message.getName().equals("all")) {
                            sendMessageToRoomUsers(message.getRoomId(), new TextMessage(getNameFromSession(webSocketSession) +
                                    "说:" + message.getInfo()
                            ));
                        } else {
                            sendMessageToUser(message.getRoomId(), message.getName(), new TextMessage(getNameFromSession(webSocketSession) +
                                    "悄悄对你说:" + message.getInfo()));
                        }
                        break;
                        //有人离开房间信息
                    case MessageKey.LEAVE_COMMAND:
                        sendMessageToRoomUsers(message.getRoomId(), new TextMessage("【" + getNameFromSession(webSocketSession) + "】离开了房间,欢迎下次再来"));
                        break;
                        default:
                            break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 发送信息给指定用户
     */
    public boolean sendMessageToUser(String roomId, String name, TextMessage message) {
        if (roomId == null || name == null) return false;
        if (sUserMap.get(roomId) == null) return false;
        WebSocketSession session = sUserMap.get(roomId).get(name);
        if (!session.isOpen()) return false;
        try {
            session.sendMessage(message);
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 广播信息给某房间内的所有用户
     */
    public boolean sendMessageToRoomUsers(String roomId, TextMessage message) {
        if (roomId == null) return false;
        if (sUserMap.get(roomId) == null) return false;
        boolean allSendSuccess = true;
        Collection<WebSocketSession> sessions = sUserMap.get(roomId).values();
        for (WebSocketSession session : sessions) {
            try {
                if (session.isOpen()) {
                    session.sendMessage(message);
                }
            } catch (IOException e) {
                e.printStackTrace();
                allSendSuccess = false;
            }
        }

        return allSendSuccess;
    }

    //退出房间时的处理
    @Override
    public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) {
        System.out.println("连接已关闭:" + closeStatus);
        Map<String, WebSocketSession> map = sUserMap.get(getRoomIdFromSession(webSocketSession));
        if (map != null) {
            map.remove(getNameFromSession(webSocketSession));
        }
    }
}

Анализ ключевых моментов:

  • использоватьsUserMapЭта статическая переменная для хранения информации о пользователе. Мы соответствуем приведенной выше схеме.
  • выполнитьafterConnectionEstablishedметод, когда пользователь успешно входит в комнату, сохраните информацию о пользователе вMap, и позвонитеsendMessageToRoomUsersВещание новичков, чтобы присоединиться к информации.
  • выполнитьhandleMessageДобавлен метод обработки пользователя, оставив три типа сообщений и передачи информации.
  • выполнитьafterConnectionClosedметод обработки уничтожения информации, когда пользователь покидает комнату. отMapчтобы очистить пользователя.
  • выполнитьsendMessageToUser,sendMessageToRoomUsersДва метода отправки сообщений клиентам. непосредственно черезSessionСтруктурированные данные могут быть отправлены клиенту.sendMessageToUserОтправьте сообщение, чтобы достичь точки,sendMessageToRoomUsersВещательное сообщение реализовано.

Реализация клиента

На стороне клиента мы можем просто использовать интерфейс WebSocket JS, предоставленный нам HTML5.

<html>
    <script type="text/javascript">
        function ToggleConnectionClicked() {
            if (SocketCreated && (ws.readyState == 0 || ws.readyState == 1)) {
                lockOn("离开聊天室...");
                SocketCreated = false;
                isUserloggedout = true;
                var msg = JSON.stringify({'command':'leave', 'roomId':groom , 'name': gname,
                    'info':'离开房间'});
                ws.send(msg);
                ws.close();
            } else if(document.getElementById("roomId").value == "请输入房间号!") {
                Log("请输入房间号!");
            } else {
                lockOn("进入聊天室...");
                Log("准备连接到聊天服务器 ...");
                groom = document.getElementById("roomId").value;
                gname = document.getElementById("txtName").value;
                try {
                    if ("WebSocket" in window) {
                        ws = new WebSocket(
                            'ws://localhost:8080/webSocket/INFO={"command":"enter","name":"'+ gname + '","roomId":"' + groom + '"}');
                    }
                    else if("MozWebSocket" in window) {
                        ws = new MozWebSocket(
                            'ws://localhost:8080/webSocket/INFO={"command":"enter","name":"'+ gname + '","roomId":"' + groom + '"}');
                    }
                    SocketCreated = true;
                    isUserloggedout = false;
                } catch (ex) {
                    Log(ex, "ERROR");
                    return;
                }
                document.getElementById("ToggleConnection").innerHTML = "断开";
                ws.onopen = WSonOpen;
                ws.onmessage = WSonMessage;
                ws.onclose = WSonClose;
                ws.onerror = WSonError;
            }
        };


        function WSonOpen() {
            lockOff();
            Log("连接已经建立。", "OK");
            $("#SendDataContainer").show();
            var msg = JSON.stringify({'command':'enter', 'roomId':groom , 'name': "all",
                'info': gname + "加入聊天室"})
            ws.send(msg);
        };
</html>

Анализ ключевых моментов:

  • При инициации подключения к серверу обратите внимание на адресную информацию:'ws://localhost:8080/webSocket/INFO={"command":"enter","name":"'+ gname + '","roomId":"' + groom + '"}', мы здесьINFOПосле добавления личной информации пользователя сервер может пометить эту сессию в соответствии с этой информацией после ее получения.
  • Как только соединение будет установлено, отправьте сообщение о присоединении всем остальным в комнате. пройти черезws.send()реализация метода.

На этом этапе была введена часть кода, и слишком много кода больше не будет складываться.Для получения более подробного кода обратитесь к адресу Github ниже.

Резюме этой статьи

На относительно полном примере веб-чата мы представляем некоторые детали нашего собственного использования WebSocket:

  • Сервер хочет сделать что-то во время установления соединения, то есть на этапе рукопожатия, чтобы достичьHandshakeInterceptor.
  • Сервер хочет обработать сообщение, отправленное клиентом после установления соединения, для достиженияWebSocketHandler.
  • сервер черезWebSocketSessionможет отправить сообщение клиенту через пользователя иSessionПривязка для достижения соответствующих отношений.

Студенты, которые хотят углубить свое понимание, должны углубиться в код и тщательно изучить его. Место ограничено, и добавление большого количества кода в саму статью не легко читать. Так что нам все еще нужно лучше понимать код.

Код, используемый в этой статье

Достигнуть полный код - маленький веб-чат

Добро пожаловать, чтобы продолжать обращать внимание на оригинал, если он вам нравится, не забудьте собрать и подписаться, кодовое слово слишком утомительно, ваша поддержка - движущая сила для меня, чтобы продолжать!

Произведено Сяомином, это должен быть бутик

Добро пожаловать на технический форум xNPE, ежедневно появляется все больше оригинальных галантерейных товаров.