Настройка клиента Stomp в android с Spring framework на стороне сервера
Я разрабатываю приложение для android, которое обменивается данными с сервером причала, настроенным весной. Чтобы получить более динамичное приложение для android, я пытаюсь использовать протокол WebSocket с сообщениями Stomp.
чтобы реализовать этот материал, я настраиваю брокера сообщений веб-сокета весной:
@Configuration
//@EnableScheduling
@ComponentScan(
basePackages="project.web",
excludeFilters = @ComponentScan.Filter(type= FilterType.ANNOTATION, value = Configuration.class)
)
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/message");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/client");
}
}
и a SimpMessageSendingOperations
весной контроллер для отправки сообщения с сервера на клиент:
@Controller
public class MessageAddController {
private final Log log = LogFactory.getLog(MessageAddController.class);
private SimpMessageSendingOperations messagingTemplate;
private UserManager userManager;
private MessageManager messageManager;
@Autowired
public MessageAddController(SimpMessageSendingOperations messagingTemplate,
UserManager userManager, MessageManager messageManager){
this.messagingTemplate = messagingTemplate;
this.userManager = userManager;
this.messageManager = messageManager;
}
@RequestMapping("/Message/Add")
@ResponseBody
public SimpleMessage addFriendship(
@RequestParam String content,
@RequestParam Long otherUser_id
){
if(log.isInfoEnabled())
log.info("Execute MessageAdd action");
SimpleMessage simpleMessage;
try{
User curentUser = userManager.getCurrentUser();
User otherUser = userManager.findUser(otherUser_id);
Message message = new Message();
message.setContent(content);
message.setUserSender(curentUser);
message.setUserReceiver(otherUser);
messageManager.createMessage(message);
Message newMessage = messageManager.findLastMessageCreated();
messagingTemplate.convertAndSend(
"/message/add", newMessage);//send message through websocket
simpleMessage = new SimpleMessage(null, newMessage);
} catch (Exception e) {
if(log.isErrorEnabled())
log.error("A problem of type : " + e.getClass()
+ " has occured, with message : " + e.getMessage());
simpleMessage = new SimpleMessage(
new SimpleException(e.getClass(), e.getMessage()), null);
}
return simpleMessage;
}
}
когда я тестирую эту конфигурацию в веб-браузере с помощью топать.js, у меня нет никаких проблем : сообщения отлично обмениваются между веб-браузером и сервером причала. Код JavaScript, используемый для теста веб-браузера:
var stompClient = null;
function setConnected(connected) {
document.getElementById('connect').disabled = connected;
document.getElementById('disconnect').disabled = !connected;
document.getElementById('conversationDiv').style.visibility = connected ? 'visible' : 'hidden';
document.getElementById('response').innerHTML = '';
}
function connect() {
stompClient = Stomp.client("ws://YOUR_IP/client");
stompClient.connect({}, function(frame) {
setConnected(true);
stompClient.subscribe('/message/add', function(message){
showMessage(JSON.parse(message.body).content);
});
});
}
function disconnect() {
stompClient.disconnect();
setConnected(false);
console.log("Disconnected");
}
function showMessage(message) {
var response = document.getElementById('response');
var p = document.createElement('p');
p.style.wordWrap = 'break-word';
p.appendChild(document.createTextNode(message));
response.appendChild(p);
}
проблемы возникают, когда я пытаюсь использовать stomp в Android с библиотеками, такими как gozirra, activemq-stomp или другие : в большинстве случаев соединение с сервером не работает. Мое приложение перестает работать, и через несколько минут у меня есть следующее сообщение в logcat:java.net.UnknownHostException: Unable to resolve host "ws://192.168.1.39/client": No address associated with hostname
и я не понимаю, почему. Код, использующий библиотеку Gozzira, которая управляет обращением stomp в моей деятельности android:
private void stomp_test() {
String ip = "ws://192.172.6.39/client";
int port = 8080;
String channel = "/message/add";
Client c;
try {
c = new Client( ip, port, "", "" );
Log.i("Stomp", "Connection established");
c.subscribe( channel, new Listener() {
public void message( Map header, String message ) {
Log.i("Stomp", "Message received!!!");
}
});
} catch (IOException ex) {
Log.e("Stomp", ex.getMessage());
ex.printStackTrace();
} catch (LoginException ex) {
Log.e("Stomp", ex.getMessage());
ex.printStackTrace();
} catch (Exception ex) {
Log.e("Stomp", ex.getMessage());
ex.printStackTrace();
}
}
после некоторых исследований я обнаружил, что большинство людей, которые хотят использовать stomp over websocket с клиентом Java, используют сервер ActiveMQ, как в этом сайт. Но spring tools очень просты в использовании, и было бы здорово, если бы я мог сохранить свой серверный слой, как сейчас. Кто-то знает, как использовать stomp java (Android) на стороне клиента с конфигурацией Spring на стороне сервера?
3 ответов
моя реализация протокола STOMP для android (или простой java) с RxJava https://github.com/NaikSoftware/StompProtocolAndroid. Протестировано на сервере STOMP с SpringBoot. Простой пример (с retrolambda):
private StompClient mStompClient;
// ...
mStompClient = Stomp.over(WebSocket.class, "ws://localhost:8080/app/hello/websocket");
mStompClient.connect();
mStompClient.topic("/topic/greetings").subscribe(topicMessage -> {
Log.d(TAG, topicMessage.getPayload());
});
mStompClient.send("/app/hello", "My first STOMP message!");
// ...
mStompClient.disconnect();
добавьте следующий путь к классам в project:
classpath 'me.tatarka:gradle-retrolambda:3.2.0'
добавьте следующую вещь в сборку приложения.Gradle в :
apply plugin: 'me.tatarka.retrolambda'
android {
.............
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
............................
compile 'org.java-websocket:Java-WebSocket:1.3.0'
compile 'com.github.NaikSoftware:StompProtocolAndroid:1.1.5'
}
все работает асинхронно! Вы можете позвонить connect()
после subscribe()
и send()
, сообщения будут помещены в очередь.
дополнительные характеристики:
- дополнительные заголовки HTTP для запроса рукопожатия (для передачи токена аутентификации или другого)
- вы можете реализовать собственный транспорт для библиотеки, просто реализовать интерфейс ConnectionProvider
- подписаться на события жизненного цикла соединения (подключено, закрыто, ошибка)
Например :
public class MainActivity extends AppCompatActivity {
private StompClient mStompClient;
public static final String TAG="StompClient";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button view = (Button) findViewById(R.id.button);
view.setOnClickListener(e-> new LongOperation().execute(""));
}
private class LongOperation extends AsyncTask<String, Void, String> {
private StompClient mStompClient;
String TAG="LongOperation";
@Override
protected String doInBackground(String... params) {
mStompClient = Stomp.over(WebSocket.class, "ws://localhost:8080/app/hello/websocket");
mStompClient.connect();
mStompClient.topic("/topic/greetings").subscribe(topicMessage -> {
Log.d(TAG, topicMessage.getPayload());
});
mStompClient.send("/app/hello", "My first STOMP message!").subscribe();
mStompClient.lifecycle().subscribe(lifecycleEvent -> {
switch (lifecycleEvent.getType()) {
case OPENED:
Log.d(TAG, "Stomp connection opened");
break;
case ERROR:
Log.e(TAG, "Error", lifecycleEvent.getException());
break;
case CLOSED:
Log.d(TAG, "Stomp connection closed");
break;
}
});
return "Executed";
}
@Override
protected void onPostExecute(String result) {
}
}
}
добавить разрешение в интернет декларация.в XML
<uses-permission android:name="android.permission.INTERNET" />
Я достигаю использования stomp через веб-сокет с Android и spring server.
для этого я использовал библиотеку веб-сокетов: werbench (следуйте этому ссылке чтобы скачать ее). Для установки я использовал команду maven mvn install
и я вернул банку в свой локальный репозиторий. Затем мне нужно добавить слой stomp на базовый веб-сокет, но я не мог найти библиотеку stomp на java, которая могла бы управлять stomp через веб-сокет (мне пришлось отказаться от gozzira). Поэтому я создаю свой собственный. один (с топотом.Яш, как модель). Не стесняйтесь спрашивать меня, хотите ли вы взглянуть на него, но я понял это очень быстро, поэтому он не может управлять столько, сколько топать.js. Затем мне нужно реализовать аутентификацию с моим сервером spring. Чтобы добиться этого, я следовал указаниям этот сайт. когда я получаю обратно файл cookie JSESSIONID, мне просто нужно было объявить заголовок с этим файлом cookie в экземпляре веб-сокета werbench в моей "библиотеке"stomp.
изменить : это основной класс в этой библиотеке, тот, который управляет подключением к веб-сокету:
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import android.util.Log;
import de.roderick.weberknecht.WebSocket;
import de.roderick.weberknecht.WebSocketEventHandler;
import de.roderick.weberknecht.WebSocketMessage;
public class Stomp {
private static final String TAG = Stomp.class.getSimpleName();
public static final int CONNECTED = 1;//Connection completely established
public static final int NOT_AGAIN_CONNECTED = 2;//Connection process is ongoing
public static final int DECONNECTED_FROM_OTHER = 3;//Error, no more internet connection, etc.
public static final int DECONNECTED_FROM_APP = 4;//application explicitely ask for shut down the connection
private static final String PREFIX_ID_SUBSCIPTION = "sub-";
private static final String ACCEPT_VERSION_NAME = "accept-version";
private static final String ACCEPT_VERSION = "1.1,1.0";
private static final String COMMAND_CONNECT = "CONNECT";
private static final String COMMAND_CONNECTED = "CONNECTED";
private static final String COMMAND_MESSAGE = "MESSAGE";
private static final String COMMAND_RECEIPT = "RECEIPT";
private static final String COMMAND_ERROR = "ERROR";
private static final String COMMAND_DISCONNECT = "DISCONNECT";
private static final String COMMAND_SEND = "SEND";
private static final String COMMAND_SUBSCRIBE = "SUBSCRIBE";
private static final String COMMAND_UNSUBSCRIBE = "UNSUBSCRIBE";
private static final String SUBSCRIPTION_ID = "id";
private static final String SUBSCRIPTION_DESTINATION = "destination";
private static final String SUBSCRIPTION_SUBSCRIPTION = "subscription";
private static final Set<String> VERSIONS = new HashSet<String>();
static {
VERSIONS.add("V1.0");
VERSIONS.add("V1.1");
VERSIONS.add("V1.2");
}
private WebSocket websocket;
private int counter;
private int connection;
private Map<String, String> headers;
private int maxWebSocketFrameSize;
private Map<String, Subscription> subscriptions;
private ListenerWSNetwork networkListener;
/**
* Constructor of a stomp object. Only url used to set up a connection with a server can be instantiate
*
* @param url
* the url of the server to connect with
*/
public Stomp(String url, Map<String,String> headersSetup, ListenerWSNetwork stompStates){
try {
this.websocket = new WebSocket(new URI(url), null, headersSetup);
this.counter = 0;
this.headers = new HashMap<String, String>();
this.maxWebSocketFrameSize = 16 * 1024;
this.connection = NOT_AGAIN_CONNECTED;
this.networkListener = stompStates;
this.networkListener.onState(NOT_AGAIN_CONNECTED);
this.subscriptions = new HashMap<String, Subscription>();
this.websocket.setEventHandler(new WebSocketEventHandler() {
@Override
public void onOpen(){
if(Stomp.this.headers != null){
Stomp.this.headers.put(ACCEPT_VERSION_NAME, ACCEPT_VERSION);
transmit(COMMAND_CONNECT, Stomp.this.headers, null);
Log.d(TAG, "...Web Socket Openned");
}
}
@Override
public void onMessage(WebSocketMessage message) {
Log.d(TAG, "<<< " + message.getText());
Frame frame = Frame.fromString(message.getText());
boolean isMessageConnected = false;
if(frame.getCommand().equals(COMMAND_CONNECTED)){
Stomp.this.connection = CONNECTED;
Stomp.this.networkListener.onState(CONNECTED);
Log.d(TAG, "connected to server : " + frame.getHeaders().get("server"));
isMessageConnected = true;
} else if(frame.getCommand().equals(COMMAND_MESSAGE)){
String subscription = frame.getHeaders().get(SUBSCRIPTION_SUBSCRIPTION);
ListenerSubscription onReceive = Stomp.this.subscriptions.get(subscription).getCallback();
if(onReceive != null){
onReceive.onMessage(frame.getHeaders(), frame.getBody());
} else{
Log.e(TAG, "Error : Subscription with id = " + subscription + " had not been subscribed");
//ACTION TO DETERMINE TO MANAGE SUBCRIPTION ERROR
}
} else if(frame.getCommand().equals(COMMAND_RECEIPT)){
//I DON'T KNOW WHAT A RECEIPT STOMP MESSAGE IS
} else if(frame.getCommand().equals(COMMAND_ERROR)){
Log.e(TAG, "Error : Headers = " + frame.getHeaders() + ", Body = " + frame.getBody());
//ACTION TO DETERMINE TO MANAGE ERROR MESSAGE
} else {
}
if(isMessageConnected)
Stomp.this.subscribe();
}
@Override
public void onClose(){
if(connection == DECONNECTED_FROM_APP){
Log.d(TAG, "Web Socket disconnected");
disconnectFromApp();
} else{
Log.w(TAG, "Problem : Web Socket disconnected whereas Stomp disconnect method has never "
+ "been called.");
disconnectFromServer();
}
}
@Override
public void onPing() {
}
@Override
public void onPong() {
}
@Override
public void onError(IOException e) {
Log.e(TAG, "Error : " + e.getMessage());
}
});
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
/**
* Send a message to server thanks to websocket
*
* @param command
* one of a frame property, see {@link Frame} for more details
* @param headers
* one of a frame property, see {@link Frame} for more details
* @param body
* one of a frame property, see {@link Frame} for more details
*/
private void transmit(String command, Map<String, String> headers, String body){
String out = Frame.marshall(command, headers, body);
Log.d(TAG, ">>> " + out);
while (true) {
if (out.length() > this.maxWebSocketFrameSize) {
this.websocket.send(out.substring(0, this.maxWebSocketFrameSize));
out = out.substring(this.maxWebSocketFrameSize);
} else {
this.websocket.send(out);
break;
}
}
}
/**
* Set up a web socket connection with a server
*/
public void connect(){
if(this.connection != CONNECTED){
Log.d(TAG, "Opening Web Socket...");
try{
this.websocket.connect();
} catch (Exception e){
Log.w(TAG, "Impossible to establish a connection : " + e.getClass() + ":" + e.getMessage());
}
}
}
/**
* disconnection come from the server, without any intervention of client side. Operations order is very important
*/
private void disconnectFromServer(){
if(this.connection == CONNECTED){
this.connection = DECONNECTED_FROM_OTHER;
this.websocket.close();
this.networkListener.onState(this.connection);
}
}
/**
* disconnection come from the app, because the public method disconnect was called
*/
private void disconnectFromApp(){
if(this.connection == DECONNECTED_FROM_APP){
this.websocket.close();
this.networkListener.onState(this.connection);
}
}
/**
* Close the web socket connection with the server. Operations order is very important
*/
public void disconnect(){
if(this.connection == CONNECTED){
this.connection = DECONNECTED_FROM_APP;
transmit(COMMAND_DISCONNECT, null, null);
}
}
/**
* Send a simple message to the server thanks to the body parameter
*
*
* @param destination
* The destination through a Stomp message will be send to the server
* @param headers
* headers of the message
* @param body
* body of a message
*/
public void send(String destination, Map<String,String> headers, String body){
if(this.connection == CONNECTED){
if(headers == null)
headers = new HashMap<String, String>();
if(body == null)
body = "";
headers.put(SUBSCRIPTION_DESTINATION, destination);
transmit(COMMAND_SEND, headers, body);
}
}
/**
* Allow a client to send a subscription message to the server independently of the initialization of the web socket.
* If connection have not been already done, just save the subscription
*
* @param subscription
* a subscription object
*/
public void subscribe(Subscription subscription){
subscription.setId(PREFIX_ID_SUBSCIPTION + this.counter++);
this.subscriptions.put(subscription.getId(), subscription);
if(this.connection == CONNECTED){
Map<String, String> headers = new HashMap<String, String>();
headers.put(SUBSCRIPTION_ID, subscription.getId());
headers.put(SUBSCRIPTION_DESTINATION, subscription.getDestination());
subscribe(headers);
}
}
/**
* Subscribe to a Stomp channel, through messages will be send and received. A message send from a determine channel
* can not be receive in an another.
*
*/
private void subscribe(){
if(this.connection == CONNECTED){
for(Subscription subscription : this.subscriptions.values()){
Map<String, String> headers = new HashMap<String, String>();
headers.put(SUBSCRIPTION_ID, subscription.getId());
headers.put(SUBSCRIPTION_DESTINATION, subscription.getDestination());
subscribe(headers);
}
}
}
/**
* Send the subscribe to the server with an header
* @param headers
* header of a subscribe STOMP message
*/
private void subscribe(Map<String, String> headers){
transmit(COMMAND_SUBSCRIBE, headers, null);
}
/**
* Destroy a subscription with its id
*
* @param id
* the id of the subscription. This id is automatically setting up in the subscribe method
*/
public void unsubscribe(String id){
if(this.connection == CONNECTED){
Map<String, String> headers = new HashMap<String, String>();
headers.put(SUBSCRIPTION_ID, id);
this.subscriptions.remove(id);
this.transmit(COMMAND_UNSUBSCRIBE, headers, null);
}
}
}
Это кадр сообщения Stomp:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Frame {
// private final static String CONTENT_LENGTH = "content-length";
private String command;
private Map<String, String> headers;
private String body;
/**
* Constructor of a Frame object. All parameters of a frame can be instantiate
*
* @param command
* @param headers
* @param body
*/
public Frame(String command, Map<String, String> headers, String body){
this.command = command;
this.headers = headers != null ? headers : new HashMap<String, String>();
this.body = body != null ? body : "";
}
public String getCommand(){
return command;
}
public Map<String, String> getHeaders(){
return headers;
}
public String getBody(){
return body;
}
/**
* Transform a frame object into a String. This method is copied on the objective C one, in the MMPReactiveStompClient
* library
* @return a frame object convert in a String
*/
private String toStringg(){
String strLines = this.command;
strLines += Byte.LF;
for(String key : this.headers.keySet()){
strLines += key + ":" + this.headers.get(key);
strLines += Byte.LF;
}
strLines += Byte.LF;
strLines += this.body;
strLines += Byte.NULL;
return strLines;
}
/**
* Create a frame from a received message. This method is copied on the objective C one, in the MMPReactiveStompClient
* library
*
* @param data
* a part of the message received from network, which represented a frame
* @return
* An object frame
*/
public static Frame fromString(String data){
List<String> contents = new ArrayList<String>(Arrays.asList(data.split(Byte.LF)));
while(contents.size() > 0 && contents.get(0).equals("")){
contents.remove(0);
}
String command = contents.get(0);
Map<String, String> headers = new HashMap<String, String>();
String body = "";
contents.remove(0);
boolean hasHeaders = false;
for(String line : contents){
if(hasHeaders){
for(int i=0; i < line.length(); i++){
Character c = line.charAt(i);
if(!c.equals(''))
body += c;
}
} else{
if(line.equals("")){
hasHeaders = true;
} else {
String[] header = line.split(":");
headers.put(header[0], header[1]);
}
}
}
return new Frame(command, headers, body);
}
// No need this method, a single frame will be always be send because body of the message will never be excessive
// /**
// * Transform a message received from server in a Set of objects, named frame, manageable by java
// *
// * @param datas
// * message received from network
// * @return
// * a Set of Frame
// */
// public static Set<Frame> unmarshall(String datas){
// String data;
// String[] ref = datas.split(Byte.NULL + Byte.LF + "*");//NEED TO VERIFY THIS PARAMETER
// Set<Frame> results = new HashSet<Frame>();
//
// for (int i = 0, len = ref.length; i < len; i++) {
// data = ref[i];
//
// if ((data != null ? data.length() : 0) > 0){
// results.add(unmarshallSingle(data));//"unmarshallSingle" is the old name method for "fromString"
// }
// }
// return results;
// }
/**
* Create a frame with based fame component and convert them into a string
*
* @param command
* @param headers
* @param body
* @return a frame object convert in a String, thanks to <code>toStringg()</code> method
*/
public static String marshall(String command, Map<String, String> headers, String body){
Frame frame = new Frame(command, headers, body);
return frame.toStringg();
}
private class Byte {
public static final String LF = "\n";
public static final String NULL = "";
}
}
этот объект используется для создания подписки по протоколу stomp:
public class Subscription {
private String id;
private String destination;
private ListenerSubscription callback;
public Subscription(String destination, ListenerSubscription callback){
this.destination = destination;
this.callback = callback;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getDestination() {
return destination;
}
public ListenerSubscription getCallback() {
return callback;
}
}
по крайней мере, есть два интерфейса, используемые в качестве класса java "Run", для прослушивания сети веб-сокетов и заданного канала подписки
public interface ListenerWSNetwork {
public void onState(int state);
}
import java.util.Map;
public interface ListenerSubscription {
public void onMessage(Map<String, String> headers, String body);
}
для Больше информации, не смутитесь спросить мне.
идеальное решение Eperrin спасибо. Я хотел бы заполнить полное решение, например, на этапе вашей деятельности/обслуживания, который вы называете connection
метод, конечно, не в MainThread.
private void connection() {
Map<String,String> headersSetup = new HashMap<String,String>();
Stomp stomp = new Stomp(hostUrl, headersSetup, new ListenerWSNetwork() {
@Override
public void onState(int state) {
}
});
stomp.connect();
stomp.subscribe(new Subscription(testUrl, new ListenerSubscription() {
@Override
public void onMessage(Map<String, String> headers, String body) {
}
}));
}
и будьте осторожны в websocket weberknecht библиотека ошибка в класс WebSocketHandshake методом verifyServerHandshakeHeaders находится на линии 124 только проверка если(!заголовки.вам("соединение")."равно" ("обновить")) и когда сервер отправить обновление вместо обновление вы получаете ошибку ошибка подключения: отсутствует поле заголовка в рукопожатии сервера: подключение вы должны отключить игнорировать случаи если(!заголовки.вам("соединение").equalsIgnoreCase("обновить"))