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) {}
}
}
}
}
}
|
