410013796724260
• Webmoney
R335386147728
Z369087728698
WebSocket, описание и примерWebSocket — это независимый протокол, основанный на протоколе TCP и предназначенный для обмена сообщениями между браузером и веб-сервером в режиме реального времени. Таким образом, изначально синхронный протокол HTTP, построенный на модели «запрос — ответ», становится полностью асинхронным и симметричным. При использовании WebSocket'a нет клиента и сервера с фиксированными ролями, а есть два равноправных участника обмена данными. Каждый участник функционирует самостоятельно : отправил сообщение и продолжил выполнять свои функции. Участник, получивший сообщение, может вообще не отвечать : протокол дает полную свободу в обмене данными. Достоинство WebSocketWebSocket - это двунаправленный, полнодуплексный протокол, который используется в сценарии взаимодействия «Клиент-сервер». Протокол WebSocket предполагает, что соединение устанавливается отправкой запроса, начинающегося с префикса 'ws://'. В случае безопасного соединения (https, SSL-протокол) необходимо отправить запрос на соединение с префиксом 'wss://'. Плюсы WebSocket'a :
Описание протокола WebSocketПрежде чем переходить к описанию примера необходимо, в первую очередь, рассмотреть протокол WebSocket, определенный стандартом RFC 6455. Протокол WebSocket определяет две URI (Uniform Resource Identifier - унифицированный идентификатор ресурса) схемы :
// Синтаксис конструктора объекта let ws = new WebSocket(uri); let ws = new WebSocket(uri, protocols); // Пример создания объекта let ws = new WebSocket("ws://demo-websocket.org/params"); uri – URL адрес, к которому нужно подключиться (сервер WebSocket). protocols – строка, либо массив строк протокола, которые используются для указания вложенных протоколов, чтобы один сервер мог реализовать несколько вложенных протоколов WebSocket. Необязательный параметр. Примечание : включенные в URI params можно использовать как параметры, передаваемые серверному объекту. В примере таким образом передается наименование чата и имя пользователя. Созданный объект WebSocket имеет четыре callback'a : один при получении данных и три – при изменениях состояния соединения : ws.onmessage = function(event) { alert("Получены данные " + event.data); }; ws.onopen = function() { alert("Соединение установлено."); }; ws.onclose = function(event) { if (event.wasClean) { alert('Соединение закрыто чисто'); } else { alert('Обрыв соединения'); // при остановке сервера } alert('Код: '+ event.code +' причина: '+event.reason); }; ws.onerror = function(error) { alert("Ошибка " + error.message); }; Ваш браузер не поддерживает Javascript. Интегрированный в страницу пример c Websocket'ом не будет функционировать без Javascript. Если Вы хотите увидеть пример в действии необходимо разрешить использование Javascript и перезагрузить страницу. Пример WebSocket
Примечание : Как работает этот пример?
С точки зрения изучения WebSocket'a Вы можете использовать данный пример для обучения. Ниже приводится описание примера. Описание примераРазделим описание примера на две части. В первой части будет представлено описание интерфейсной части, реализованной в данной статье. Во второй части — описание серверной части. Интерфейсная часть примеравключает представленный ниже html-код, интегрированный в интерфейс страницы, и javascript-код в виде файла websocket.js, реализующий описанную ниже бизнес-логику. Листинг html-кодаОсобый комментарий к листингу не требуется; здесь всё тривиально просто, выше представлено его отображение в браузере. Обратите внимание, что к кнопкам не подключены обработчики событий нажатия click : это реализовано в javascript-коде. листинг <p>Введите наименование чата и имя :</p> <table> <tr><td>Наименование чата</td> <td><input type="text" id="chat" /></td></tr> <tr><td>Имя пользователя</td> <td><input type="text" id="user" /></td></tr> <tr><td><input type="button" id="btnConnect" value="Подключение" /></td> <td><input type="button" id="btnDisconnect" value="Отключение" /></td></tr> </table> <p style="margin-top:10px" /> <div style="margin-bottom:5px"> <div style="display:inline-block;font-weight:bold"> Участники чата</div> <div style="display:inline-block;font-weight:bold"> Сообщения</div> </div> <div id="users" style="float:left;border:1px solid #bbb; width:210px;height:240px"> </div> <div id="messages" style="margin-left:225px;border:1px solid #bbb; width:450px;height:240px;padding:5px"> </div> <p style="margin-bottom:10px" /> <table> <tr><td colspan="2"> <b>Отправка сообщений</b></td></tr> <tr><td><input id="message" type="text" size="92" placeholder="Введите текст..." /></td></tr> <tr><td><input type="button" id="btnSend" value="Отправить" /></td></tr> </table> Листинг javascript-кодаБизнес-логика интерфейсной части примера определена в js-файле websocket.js. Ниже представлена только структура : переменные и наименования методов. Наименования переменных отражают элементы доменной структуры страницы. Исходные коды и описание методов будет представлено ниже. С целью избежания конфликтов глобальных переменных скриптовый блок включен в анонимную функцию. В последней строке функции подключается слушатель загрузки страницы с последующим вызовом метода init, который выполняет инициализацию переменных. листинг (function() { var btnConnect = null; var btnDisconnect = null; var btnSend = null; var txtMessage = null; var txtMessages = null; var txtUsers = null; var txtChat = null; var txtUser = null; var ws = null; //----------------------------------------------- function init() { ... } function lockFields(mode) { ... } function connect() { ... } function onConnect(msg) { ... } function userList(users) { ... } function addUser2List(userName) { ... } function disconnect() { ... } function clearComponents() { ... } function sendMessage() { ... } function addMessage2List(msg) { ... } //----------------------------------------------- window.addEventListener('load', init, false); })(); Методы init, lockFieldsМетод init инициализирует переменные с использованием функции document.getElementById(), т.е. переменные «связываются» с элементами DOM страницы. Кроме этого, к кнопкам управления подключаются слушатели, которые определяют вызываемые при нажатии на кнопки методы. Метод lockFields, в зависимости от значения параметра mode, блокирует и разблокирует компоненты формы (элементы DOM-структуры). Данный метод вызывается при создании WebSocket, т.е. при установлении соединения с сервером, и при отключении (disconnect) от сервера. листинг function init() { txtChat = document.getElementById('chat' ); txtUser = document.getElementById('user' ); btnConnect = document.getElementById('btnConnect' ); btnDisconnect= document.getElementById('btnDisconnect'); btnSend = document.getElementById('btnSend' ); txtMessage = document.getElementById('message' ); txtMessages = document.getElementById('messages' ); txtUsers = document.getElementById('users' ); btnConnect .addEventListener('click',connect ,false); btnDisconnect.addEventListener('click',disconnect ,false); btnSend .addEventListener('click',sendMessage,false); lockFields(true); } //---------------------------------------------------------- function lockFields(mode) { txtChat .disabled = mode; txtUser .disabled = mode; btnConnect .disabled = mode; btnDisconnect.disabled = !mode; btnSend .disabled = !mode; txtMessage .disabled = !mode; } Методы connect, onConnect, userList, addUser2ListНиже представлен метод connect для создания WebSocket'a, а также методы onConnect, userList, addUser2List, вызываемые после установления соединения с сервером. Метод connect вызывается при нажатии на кнопку «Подключение». В методе определяется URI WebSocket'a, включающего host, наименование чата и имя пользователя. Бизнес-логика данного примера предполагает, что участники чата должны знать его наименование; посторонние не должны здесь появиться. Кроме этого, не допускается дублирование имени пользователя. Поэтому, после создания объекта ws, т.е. после установления соединения с сервером, он вышлет сразу же сообщение с типом msg.type="connection" с результатом соединения msg.result. Если результат положительный, то в методе onConnect(msg) пользователь будет добавлен в список участников чата, в противном случае откроется окно с сообщением 'Ошибка подключения к чату'; причина связана с дублированием имени пользователя. Если к чату подключаются новые пользователи, то сервер всем участникам чата рассылает список в виде сообщения msg.type="userlist", которое отправляется в процедуру userList(users), в которой обновляется список участников чата. листинг function connect() { let chat = $("#chat").val(); let user = $("#user").val(); // определение URI let host = document.location.host + "/"; let pathname = document.location.pathname; let data = pathname.split('/'); if (data.length > 2) pathname = "/" + data[1] + "/websocket"; else pathname = "/websocket"; let uri = null; if (window.location.protocol == 'http:') uri = 'ws://' + host + pathname + "/" + chat + "_" + user; else uri = 'wss://' + host + ":9090" + pathname + "/" + chat + "_" + user; // создание объекта WebSocket ws = new WebSocket(uri); // подключение обработчика поступивших сообщений ws.onmessage = function(event) { let msg = JSON.parse(event.data); switch(msg.type) { case "connection" : onConnect(msg); break; case "userlist" : userList(msg.users); break; case "message" : addMessage2List(msg); break; default : console.log("Unknown message received : " + msg.type); } }; } //--------------------------------------------------------- function onConnect(msg) { if (msg.result) { lockFields(false); addUser2List(msg.username); } else alert ('Ошибка подключения к чату'); } //--------------------------------------------------------- function userList(users) { while (txtUsers.firstChild) txtUsers.removeChild(txtUsers.firstChild); users.forEach(function(username) { addUser2List(username); }); } //--------------------------------------------------------- function addUser2List(userName) { let p = document.createElement("p"); p.setAttribute('style', 'margin:0px'); if (userName === $("#user").val()) p.innerHTML = "<b>" + userName + "</b><br />"; else p.innerHTML = userName + "<br />"; txtUsers.appendChild(p); }
ПРИМЕЧАНИЕ :
При определении URI используются параметры host и pathname, чтобы совместить разработку в IDE Eclipse (DEVELOP) и выкладывание данной страницы на сервер (PRODUCTION). ----- Среда DEVELOP ----- URL страницы : http://localhost:8080/java-online/websocket.xhtml URI WebSocket'a : ws://localhost:8080/java-online/test_alex • host=localhost:8080 • pathname=/java-online/websocket.xhtml -------------------------------------------------------------------------------------------- ----- Среда PRODUCTION ----- URL страницы : https://java-online.ru/websocket.xhtml URI WebSocket'a : wss://java-online.ru:9090/test_alex • host=java-online.ru • pathname=/websocket.xhtml Методы disconnect, clearComponentsНиже представлен листинг метода отключения от сервера disconnect и метода очистки компонентов clearComponents. При отключении от сервера очищаются компоненты и блокируются компоненты формы, разблокируется кнопка подключения, после чего закрывается соединение WebSocket'a. При очистке компонентов формы удаляются дочерние элементы списка участников чата и списка поступивших сообщений, а также поле отправки сообщения. листинг function disconnect() { // очистка компонентов формы clearComponents(); // блокирование полей lockFields(true); // закрытие соединения ws.close(); } //--------------------------------------------------------- function clearComponents() { while (txtUsers.firstChild) txtUsers.removeChild(txtUsers.firstChild); while (txtMessages.firstChild) txtMessages.removeChild(txtMessages.firstChild); $("#message").val(""); } Методы sendMessage, addMessage2ListНиже представлен листинг метода отправки сообщения sendMessage на сервер и метод представления поступившего сообщения от сервера (от других участников чата). Перед отправкой сообщения на сервер оно конвертируется в объект JSON. Сервер, получивший сообщение, рассылает его всем участникам чата. Поступившее пользователю сообщение методом addMessage2List отображается в списке : личные сообщения выравниваются по правому краю компонента, остальные – по левому. листинг function sendMessage() { let msg = { from : $("#user").val(), chat : $("#chat").val(), type : "message", content : $("#message").val() }; // конвертация JavaScript-объекта в JSON let msgJSON = JSON.stringify(msg); // отправка объекта на сервер ws.send(msgJSON); } //--------------------------------------------------------- function addMessage2List(msg) { let from = null; let time = new Date(); // определение стиля выравнивания сообщения let align = ""; let margin = ""; if (msg.from === $("#user").val()) { align = "text-align:right"; margin = 'margin:0px 0px 0px 130px;'; from = time.toLocaleTimeString() + "<br />"; } else { align = "text-align:left"; margin = 'margin:0px;'; from = time.toLocaleTimeString() + ' ' + msg.from + "<br />"; } let style = 'width:320px;' + margin + align; // создание элемента DOM let span = document.createElement("p"); span.setAttribute('style', style); // определение текста сообщения span.innerHTML = from + msg.content + "<br />"; // добавление сообщения к списку txtMessages.appendChild(span); } Серверная часть WebSocket'aСервер WebSocket работает аналогично клиентам WebSocket : он также реагирует на определенные события. Независимо от используемого языка программирования каждый сервер WebSocket выполняет определенные действия. Как правило, примеры описания серверной части WebSocket'a связаны с использованием Node.JS. Здесь же будет представлено описание серверной части WebSocket'a на Java, реализованное на этой странице и включающее следующие модули :
Листинг методов WebSocketMessage, MessageEncoder, MessageDecoderНиже представлены листинги следующих классов :
WebSocketMessage включает поля, определяющие отправителя from, наименование чата chat, тип сообщения type и текст сообщения content. Классы кодировки MessageEncoder и декодировки MessageDecoder сообщений используют com.google.gson.Gson для форматирования объектов сообщений. листинг public class WebSocketMessage { private String from; private String chat; private String type; private String content; public String getFrom() { return from; } public void setFrom(String from) { this.from = from; } public String getChat() { return chat; } public void setChat(String chat) { this.chat = chat; } public String getType() { return type; } public void setType(String type) { this.type = type; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } } //--------------------------------------------------------- import com.google.gson.Gson; import javax.websocket.Encoder; import javax.websocket.EndpointConfig; import javax.websocket.EncodeException; public class MessageEncoder implements Encoder.Text<WebSocketMessage> { private static Gson gson = new Gson(); @Override public String encode(WebSocketMessage message) throws EncodeException { String json = gson.toJson(message); return json; } @Override public void init(EndpointConfig endpointConfig) { // Custom initialization logic } @Override public void destroy() { // Close resources } } //--------------------------------------------------------- import com.google.gson.Gson; import javax.websocket.Decoder; import javax.websocket.EndpointConfig; import javax.websocket.DecodeException; public class MessageDecoder implements Decoder.Text<WebSocketMessage> { private static Gson gson = new Gson(); @Override public WebSocketMessage decode(String s) throws DecodeException { WebSocketMessage message; message = gson.fromJson(s, WebSocketMessage.class); return message; } @Override public boolean willDecode(String s) { return (s != null); } @Override public void init(EndpointConfig endpointConfig) { // Custom initialization logic } @Override public void destroy() { // Close resources } } Листинг метода WebSocketUserКласс описания пользователя, включающего наименования чата chat с пользователем user и объект сессии session. Экземпляр объекта WebSocketUser используется сервером для учета чатов и связанных с ним пользователей : дублирование пользователей в чате не допускается. листинг import javax.websocket.Session; public class WebSocketUser { private String chat; private String user; private Session session; public WebSocketUser(final Session session, final String chat, final String user) { this.session = session; this.chat = chat; this.user = user; } public Session getSession() { return session; } public void setSession(Session session) { this.session = session; } public String getChat() { return chat; } public void setChat(String chat) { this.chat = chat; } public String getUser() { return user; } public void setUser(String user) { this.user = user; } } Листинг метода WebSocketEndpointНиже представлен листинг основного класса WebSocketEndpoint, формирующего серверную часть WebSocket'a и организующего взаимодействие с клиентскими частями web-сокетов. Имеется 2 способа формирования конечной точки WebSocket'a : либо использование анотации @ServerEndpoint, либо расширение (extends) класса javax.websocket.server.ServerEndpoint. При описании WebSocketEndpoint используется аннотация @ServerEndpoint, которая включает одно обязательное поле/значение и два опциональных (возможно использование 4-х полей). Модуль, наследующий свойства javax.websocket.server.ServerEndpoint (аннотируемый @ServerEndpoint), имеет следующие методы :
Кроме обязательных методов WebSocketEndpoint включает следующие дополнительные методы :
В листинге каждый метод включает комментарии. листинг import java.io.IOException; import java.util.ArrayList; import java.util.List; import javax.websocket.EncodeException; import javax.websocket.OnClose; import javax.websocket.OnError; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; @ServerEndpoint(value = "/websocket/{username}", decoders = MessageDecoder.class, encoders = MessageEncoder.class) public class WebSocketEndpoint { // список зарегистрированных пользователей чатов private List<WebSocketUser> chat_list = null; // шаблон сообщения создания соединения String MSG_CONNECTION = "{'type':'connection', 'username':'%s', 'result':%s}"; // шаблон списка пользователей String MSG_USER_LIST = "{'type':'userlist', 'users':[%s]}"; //----------------------------------------------------- /** * Метод onOpen вызывается контейнером при создании * соединения с клиентским WebSocket'ом */ @OnOpen public void onOpen(Session session, @PathParam("username") String username) throws IOException, EncodeException { // определение чата и пользователя String chat = null; String user = null; String[] data = username.split("_"); if (data.length > 1) { chat = data[0]; user = data[1]; } else user = username; String content = null; try { // добавление пользователя boolean bool = addUser(session, chat, user); // результат добавления String result = (bool) ? "true" : "false"; // формирование ответного сообщения content = String.format(MSG_CONNECTION, chat, user, result); synchronized (session) { // отправка сообщения session.getAsyncRemote().sendText(content); } } catch (Exception e) {} // набор чатов/пользователей if (chat_list == null) chat_list = new ArrayList<WebSocketUser>(); if (chat_list.size() > 1) { try { Thread.sleep(500); } catch (InterruptedException e) {} // рассылка списка пользователей чата userListDistribution(chat); } } //----------------------------------------------------- /** * Метод onOpen вызывается контейнером при поступлении * сообщения от клиентских WebSocket'ом */ @OnMessage public void onMessage(final Session session, WebSocketMessage msg) throws IOException, EncodeException { // рассылка сообщения пользователя чата if (msg.getType().equalsIgnoreCase("message")) broadcast(msg); } //----------------------------------------------------- /** * Метод onClose вызывается контейнером при закрытии * соединения с клиентским WebSocket'ом */ @OnClose public void onClose(Session session) throws IOException, EncodeException { // получение пользователя WebSocketUser wuser = getWebSocketUser(session); // удаление пользователя из списка chat_list.remove(wuser); // рассылка обновленного списка пользователей чата userListDistribution(); } //----------------------------------------------------- /** * Метод обработки ошибки */ @OnError public void onError(Session session, Throwable t) { // Do error handling here } //----------------------------------------------------- /** * Метод рассылки списка пользователей чата */ private void userListDistribution(final String chat) throws IOException, EncodeException { if (chat_list.size() == 0) return; // список пользователей String users = ""; int cnt = 0; for (int i = 0; i < chat_list.size(); i++) { WebSocketUser wuser = chat_list.get(i); if (cnt > 0) users += ","; if (wuser.getChat().equalsIgnoreCase(chat)) { users += "\"" + wuser.getUser() + "\""; cnt++; } } // сообщение со списком пользователей String message=String.format(MSG_USER_LIST, users); if (users.length() > 0) { // рассылка списка пользователей в цикле for(int i = 0; i < chat_list.size(); i++){ WebSocketUser wuser = chat_list.get(i); // проверка принадлежности пользователя // к чату и состояние сессии if (wuser.getChat().equalsIgnoreCase(chat) && wuser.getSession().isOpen()){ synchronized (wuser.getSession()) { try { // отправка сообщения wuser.getSession() .getAsyncRemote() .sendText(message); } catch (Exception e) {} } } } } } } //----------------------------------------------------- /** * Метод добавления пользователя в набор */ private boolean addUser(final Session session, final String chat_name, final String user_name) { boolean result = false; for (int i = 0; i < chat_list.size(); i++) { if (chat_list.get(i) .getChat() .equalsIgnoreCase(chat_name) && chat_list.get(i) .getUser() .equalsIgnoreCase(user_name)) { break; } } if (!result) { chat_list.add(new WebSocketUser(session, chat_name, user_name)); result = true; } return result; } //----------------------------------------------------- /** * Метод чтения пользователя */ private WebSocketUser getWebSocketUser(Session session) { WebSocketUser wuser = null; // Цикл перебора for (int i = 0; i < chat_list.size(); i++) { // Контроль сессии if (chat_list.get(i) .getSession() .getId() .equals(session.getId())){ wuser = chat_list.get(i); break; } } return wuser; } //----------------------------------------------------- /** * Метод рассылки сообщений */ private void broadcast(WebSocketMessage wsmsg) throws IOException, EncodeException { // Цикл перебора списка пользователй for(WebSocketUser wuser : chat_list){ // Контроль принадлежности к чату if (wuser.getChat() .equalsIgnoreCase(wsmsg.getChat())) { synchronized (wuser) { try { // Отправка сообщения wuser.getSession() .getAsyncRemote() .sendObject(wsmsg); } catch (Exception e) {} } } } } } |