Статьи

Java WebSockets (JSR-356) на Jetty 9.1

Наконец-то выпущен Jetty 9.1 , в результате чего Java WebSockets (JSR-356) используется в средах без EE. Это потрясающая новость, и сегодня мы расскажем об использовании этого великолепного нового API вместе со Spring Framework .

JSR-356 определяет краткую модель на основе аннотаций, позволяющую современным веб-приложениям Java легко создавать двунаправленные каналы связи с помощью API WebSockets . Он охватывает не только серверную, но и клиентскую сторону, что делает этот API действительно простым для использования везде.

Давайте начнем! Нашей целью было бы создать сервер WebSockets, который будет принимать сообщения от клиентов и транслировать их всем остальным подключенным клиентам. Для начала давайте определим формат сообщения, которым будет обмениваться сервер и клиент, как этот простой класс Message . Мы можем ограничиться чем-то вроде String , но я хотел бы представить вам мощь другого нового API — Java API для обработки JSON (JSR-353) .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.example.services;
 
public class Message {
    private String username;
    private String message;
 
    public Message() {
    }
 
    public Message( final String username, final String message ) {
        this.username = username;
        this.message = message;
    }
 
    public String getMessage() {
        return message;
    }
 
    public String getUsername() {
        return username;
    }
 
    public void setMessage( final String message ) {
        this.message = message;
    }
 
    public void setUsername( final String username ) {
        this.username = username;
    }
}

Чтобы разделить объявления, относящиеся к серверу и клиенту, JSR-356 определяет две основные аннотации: @ServerEndpoint и @ClientEndpoit соответственно. Наша клиентская конечная точка, назовем ее BroadcastClientEndpoint , просто прослушивает сообщения, отправленные сервером:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.example.services;
 
import java.io.IOException;
import java.util.logging.Logger;
 
import javax.websocket.ClientEndpoint;
import javax.websocket.EncodeException;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
 
@ClientEndpoint
public class BroadcastClientEndpoint {
    private static final Logger log = Logger.getLogger(
        BroadcastClientEndpoint.class.getName() );
 
    @OnOpen
    public void onOpen( final Session session ) throws IOException, EncodeException  {
        session.getBasicRemote().sendObject( new Message( "Client", "Hello!" ) );
    }
 
    @OnMessage
    public void onMessage( final Message message ) {
        log.info( String.format( "Received message '%s' from '%s'",
            message.getMessage(), message.getUsername() ) );
    }
}

Это буквально это! Очень чистый, не требующий пояснений фрагмент кода: @OnOpen вызывается, когда клиент подключается к серверу, и @ OnMessage вызывается каждый раз, когда сервер отправляет сообщение клиенту. Да, это очень просто, но есть предостережение: реализация JSR-356 может обрабатывать любые простые объекты, но не такие сложные, как Message . Чтобы справиться с этим, JSR-356 представляет концепцию кодеров и декодеров .

Мы все любим JSON , так почему бы не определить наш собственный кодер и декодер JSON ? Это простая задача, которую Java API для JSON Processing (JSR-353) может решить для нас. Чтобы создать кодировщик, вам нужно всего лишь реализовать Encoder.Text <Message> и в основном сериализовать ваш объект в некоторую строку, в нашем случае в строку JSON , используя JsonObjectBuilder .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.example.services;
 
import javax.json.Json;
import javax.json.JsonReaderFactory;
import javax.websocket.EncodeException;
import javax.websocket.Encoder;
import javax.websocket.EndpointConfig;
 
public class Message {
    public static class MessageEncoder implements Encoder.Text< Message > {
        @Override
        public void init( final EndpointConfig config ) {
        }
 
        @Override
        public String encode( final Message message ) throws EncodeException {
            return Json.createObjectBuilder()
                .add( "username", message.getUsername() )
                .add( "message", message.getMessage() )
                .build()
                .toString();
        }
 
        @Override
        public void destroy() {
        }
    }
}

Что касается декодера, все выглядит очень похоже, мы должны реализовать Decoder.Text <Message> и десериализовать наш объект из строки, на этот раз используя JsonReader .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.example.services;
 
import javax.json.JsonObject;
import javax.json.JsonReader;
import javax.json.JsonReaderFactory;
import javax.websocket.DecodeException;
import javax.websocket.Decoder;
 
public class Message {
    public static class MessageDecoder implements Decoder.Text< Message > {
        private JsonReaderFactory factory = Json.createReaderFactory( Collections.< String, Object >emptyMap() );
 
        @Override
        public void init( final EndpointConfig config ) {
        }
 
        @Override
        public Message decode( final String str ) throws DecodeException {
            final Message message = new Message();
 
            try( final JsonReader reader = factory.createReader( new StringReader( str ) ) ) {
                final JsonObject json = reader.readObject();
                message.setUsername( json.getString( "username" ) );
                message.setMessage( json.getString( "message" ) );
            }
 
            return message;
        }
 
        @Override
        public boolean willDecode( final String str ) {
            return true;
        }
 
        @Override
        public void destroy() {
        }
    }
}

И в качестве последнего шага, мы должны сообщить клиенту (и серверу, они используют одни и те же декодеры и кодеры), что у нас есть кодер и декодер для наших сообщений. Проще всего это сделать, просто объявив их как часть аннотаций @ServerEndpoint и @ClientEndpoit .

1
2
3
4
5
6
import com.example.services.Message.MessageDecoder;
import com.example.services.Message.MessageEncoder;
 
@ClientEndpoint( encoders = { MessageEncoder.class }, decoders = { MessageDecoder.class } )
public class BroadcastClientEndpoint {
}

Чтобы завершить пример клиента, нам нужен способ подключения к серверу с помощью BroadcastClientEndpoint и обмена сообщениями. Класс ClientStarter завершает картину:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.example.ws;
 
import java.net.URI;
import java.util.UUID;
 
import javax.websocket.ContainerProvider;
import javax.websocket.Session;
import javax.websocket.WebSocketContainer;
 
import org.eclipse.jetty.websocket.jsr356.ClientContainer;
 
import com.example.services.BroadcastClientEndpoint;
import com.example.services.Message;
 
public class ClientStarter {
    public static void main( final String[] args ) throws Exception {
        final String client = UUID.randomUUID().toString().substring( 0, 8 );
 
        final WebSocketContainer container = ContainerProvider.getWebSocketContainer();   
        final String uri = "ws://localhost:8080/broadcast"
 
        try( Session session = container.connectToServer( BroadcastClientEndpoint.class, URI.create( uri ) ) ) {
            for( int i = 1; i <= 10; ++i ) {
                session.getBasicRemote().sendObject( new Message( client, "Message #" + i ) );
                Thread.sleep( 1000 );
            }
        }
 
        // Application doesn't exit if container's threads are still running
        ( ( ClientContainer )container ).stop();
    }
}

Просто пара комментариев о том, что делает этот код: мы подключаемся к конечной точке WebSockets по адресу ws: // localhost: 8080 / broadcast , выбираем случайное имя клиента (из UUID) и генерируем 10 сообщений, каждое с задержкой в ​​1 секунду (просто чтобы убедиться, у нас есть время, чтобы получить их все обратно).

Серверная часть не выглядит по-другому, и в этот момент ее можно понять без каких-либо дополнительных комментариев (за исключением того, что сервер просто передает каждое сообщение, которое он получает, всем подключенным клиентам). Здесь важно упомянуть: новый экземпляр конечной точки сервера создается каждый раз, когда к нему подключается новый клиент (именно поэтому коллекция пиров является статической), это поведение по умолчанию и его можно легко изменить.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.example.services;
 
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
 
import javax.websocket.EncodeException;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
 
import com.example.services.Message.MessageDecoder;
import com.example.services.Message.MessageEncoder;
 
@ServerEndpoint(
    value = "/broadcast",
    encoders = { MessageEncoder.class },
    decoders = { MessageDecoder.class }
)
public class BroadcastServerEndpoint {
    private static final Set< Session > sessions =
        Collections.synchronizedSet( new HashSet< Session >() );
 
    @OnOpen
    public void onOpen( final Session session ) {
        sessions.add( session );
    }
 
    @OnClose
    public void onClose( final Session session ) {
        sessions.remove( session );
    }
 
    @OnMessage
    public void onMessage( final Message message, final Session client )
            throws IOException, EncodeException {
        for( final Session session: sessions ) {
            session.getBasicRemote().sendObject( message );
        }
    }
}

Чтобы эта конечная точка была доступна для подключения, мы должны запустить контейнер WebSockets и зарегистрировать эту конечную точку внутри него. Как всегда, Jetty 9.1 легко запускается во встроенном режиме:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.example.ws;
 
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
 
import com.example.config.AppConfig;
 
public class ServerStarter  {
    public static void main( String[] args ) throws Exception {
        Server server = new Server( 8080 );
 
        // Create the 'root' Spring application context
        final ServletHolder servletHolder = new ServletHolder( new DefaultServlet() );
        final ServletContextHandler context = new ServletContextHandler();
 
        context.setContextPath( "/" );
        context.addServlet( servletHolder, "/*" );
        context.addEventListener( new ContextLoaderListener() );  
        context.setInitParameter( "contextClass", AnnotationConfigWebApplicationContext.class.getName() );
        context.setInitParameter( "contextConfigLocation", AppConfig.class.getName() );
 
        server.setHandler( context );
        WebSocketServerContainerInitializer.configureContext( context );       
 
        server.start();
        server.join();
    }
}

Наиболее важной частью приведенного выше фрагмента является WebSocketServerContainerInitializer.configureContext : он фактически создает экземпляр контейнера WebSockets . Поскольку мы еще не добавили никаких конечных точек, контейнер в основном сидит здесь и ничего не делает. Spring Framework и класс конфигурации AppConfig сделают эту последнюю проводку за нас.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.example.config;
 
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.websocket.DeploymentException;
import javax.websocket.server.ServerContainer;
import javax.websocket.server.ServerEndpoint;
import javax.websocket.server.ServerEndpointConfig;
 
import org.eclipse.jetty.websocket.jsr356.server.AnnotatedServerEndpointConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.WebApplicationContext;
 
import com.example.services.BroadcastServerEndpoint;
 
@Configuration
public class AppConfig  {
    @Inject private WebApplicationContext context;
    private ServerContainer container;
 
    public class SpringServerEndpointConfigurator extends ServerEndpointConfig.Configurator {
        @Override
        public < T > T getEndpointInstance( Class< T > endpointClass )
                throws InstantiationException {
            return context.getAutowireCapableBeanFactory().createBean( endpointClass );  
        }
    }
 
    @Bean
    public ServerEndpointConfig.Configurator configurator() {
        return new SpringServerEndpointConfigurator();
    }
 
    @PostConstruct
    public void init() throws DeploymentException {
        container = ( ServerContainer )context.getServletContext().
            getAttribute( javax.websocket.server.ServerContainer.class.getName() );
 
        container.addEndpoint(
            new AnnotatedServerEndpointConfig(
                BroadcastServerEndpoint.class,
                BroadcastServerEndpoint.class.getAnnotation( ServerEndpoint.class
            ) {
                @Override
                public Configurator getConfigurator() {
                    return configurator();
                }
            }
        );
    
}

Как мы упоминали ранее, контейнер по умолчанию будет создавать новый экземпляр конечной точки сервера каждый раз, когда подключается новый клиент, и делает это, вызывая конструктор, в нашем случае BroadcastServerEndpoint.class.newInstance () . Это может быть желательным поведением, но поскольку мы используем Spring Framework и внедрение зависимостей, такие новые объекты в основном являются неуправляемыми компонентами. Благодаря очень продуманному (на мой взгляд) дизайну JSR-356 , на самом деле довольно легко предоставить свой собственный способ создания экземпляров конечных точек с помощью реализации ServerEndpointConfig.Configurator . SpringServerEndpointConfigurator является примером такой реализации: он создает новый управляемый компонент каждый раз, когда запрашивается новый экземпляр конечной точки (если вам нужен один экземпляр, вы можете создать синглтон конечной точки как компонент в AppConfig и возвращать его все время).

Способ извлечения контейнера WebSockets зависит от Jetty : из атрибута контекста с именем «javax.websocket.server.ServerContainer» (он, вероятно, может измениться в будущем). Как только контейнер будет добавлен, мы просто добавляем новую (управляемую!) Конечную точку, предоставляя наш собственный ServerEndpointConfig (на основе AnnotatedServerEndpointConfig, который любезно предоставляет Jetty ).

Чтобы построить и запустить наш сервер и клиенты, нам нужно просто сделать это:

1
2
3
mvn clean package
java -jar target\jetty-web-sockets-jsr356-0.0.1-SNAPSHOT-server.jar // run server
java -jar target/jetty-web-sockets-jsr356-0.0.1-SNAPSHOT-client.jar // run yet another client

Например, запустив сервер и пару клиентов (я запускаю 4 из них, ‘ 392f68ef ‘, ‘ 8e3a869d ‘, ‘ ca3a06d0 ‘, ‘ 6cb82119 ‘), вы можете увидеть по выводу в консоли, что каждый клиент получает все сообщения от всех других клиентов (включая собственные сообщения):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
Nov 29, 2013 9:21:29 PM com.example.services.BroadcastClientEndpoint onMessage
INFO: Received message 'Hello!' from 'Client'
Nov 29, 2013 9:21:29 PM com.example.services.BroadcastClientEndpoint onMessage
INFO: Received message 'Message #1' from '392f68ef'
Nov 29, 2013 9:21:29 PM com.example.services.BroadcastClientEndpoint onMessage
INFO: Received message 'Message #2' from '8e3a869d'
Nov 29, 2013 9:21:29 PM com.example.services.BroadcastClientEndpoint onMessage
INFO: Received message 'Message #7' from 'ca3a06d0'
Nov 29, 2013 9:21:30 PM com.example.services.BroadcastClientEndpoint onMessage
INFO: Received message 'Message #4' from '6cb82119'
Nov 29, 2013 9:21:30 PM com.example.services.BroadcastClientEndpoint onMessage
INFO: Received message 'Message #2' from '392f68ef'
Nov 29, 2013 9:21:30 PM com.example.services.BroadcastClientEndpoint onMessage
INFO: Received message 'Message #3' from '8e3a869d'
Nov 29, 2013 9:21:30 PM com.example.services.BroadcastClientEndpoint onMessage
INFO: Received message 'Message #8' from 'ca3a06d0'
Nov 29, 2013 9:21:31 PM com.example.services.BroadcastClientEndpoint onMessage
INFO: Received message 'Message #5' from '6cb82119'
Nov 29, 2013 9:21:31 PM com.example.services.BroadcastClientEndpoint onMessage
INFO: Received message 'Message #3' from '392f68ef'
Nov 29, 2013 9:21:31 PM com.example.services.BroadcastClientEndpoint onMessage
INFO: Received message 'Message #4' from '8e3a869d'
Nov 29, 2013 9:21:31 PM com.example.services.BroadcastClientEndpoint onMessage
INFO: Received message 'Message #9' from 'ca3a06d0'
Nov 29, 2013 9:21:32 PM com.example.services.BroadcastClientEndpoint onMessage
INFO: Received message 'Message #6' from '6cb82119'
Nov 29, 2013 9:21:32 PM com.example.services.BroadcastClientEndpoint onMessage
INFO: Received message 'Message #4' from '392f68ef'
Nov 29, 2013 9:21:32 PM com.example.services.BroadcastClientEndpoint onMessage
INFO: Received message 'Message #5' from '8e3a869d'
Nov 29, 2013 9:21:32 PM com.example.services.BroadcastClientEndpoint onMessage
INFO: Received message 'Message #10' from 'ca3a06d0'
Nov 29, 2013 9:21:33 PM com.example.services.BroadcastClientEndpoint onMessage
INFO: Received message 'Message #7' from '6cb82119'
Nov 29, 2013 9:21:33 PM com.example.services.BroadcastClientEndpoint onMessage
INFO: Received message 'Message #5' from '392f68ef'
Nov 29, 2013 9:21:33 PM com.example.services.BroadcastClientEndpoint onMessage
INFO: Received message 'Message #6' from '8e3a869d'
Nov 29, 2013 9:21:34 PM com.example.services.BroadcastClientEndpoint onMessage
INFO: Received message 'Message #8' from '6cb82119'
Nov 29, 2013 9:21:34 PM com.example.services.BroadcastClientEndpoint onMessage
INFO: Received message 'Message #6' from '392f68ef'
Nov 29, 2013 9:21:34 PM com.example.services.BroadcastClientEndpoint onMessage
INFO: Received message 'Message #7' from '8e3a869d'
Nov 29, 2013 9:21:35 PM com.example.services.BroadcastClientEndpoint onMessage
INFO: Received message 'Message #9' from '6cb82119'
Nov 29, 2013 9:21:35 PM com.example.services.BroadcastClientEndpoint onMessage
INFO: Received message 'Message #7' from '392f68ef'
Nov 29, 2013 9:21:35 PM com.example.services.BroadcastClientEndpoint onMessage
INFO: Received message 'Message #8' from '8e3a869d'
Nov 29, 2013 9:21:36 PM com.example.services.BroadcastClientEndpoint onMessage
INFO: Received message 'Message #10' from '6cb82119'
Nov 29, 2013 9:21:36 PM com.example.services.BroadcastClientEndpoint onMessage
INFO: Received message 'Message #8' from '392f68ef'
Nov 29, 2013 9:21:36 PM com.example.services.BroadcastClientEndpoint onMessage
INFO: Received message 'Message #9' from '8e3a869d'
Nov 29, 2013 9:21:37 PM com.example.services.BroadcastClientEndpoint onMessage
INFO: Received message 'Message #9' from '392f68ef'
Nov 29, 2013 9:21:37 PM com.example.services.BroadcastClientEndpoint onMessage
INFO: Received message 'Message #10' from '8e3a869d'
Nov 29, 2013 9:21:38 PM com.example.services.BroadcastClientEndpoint onMessage
INFO: Received message 'Message #10' from '392f68ef'
2013-11-29 21:21:39.260:INFO:oejwc.WebSocketClient:main: Stopped org.eclipse.jetty.websocket.client.WebSocketClient@3af5f6dc

Потрясающие! Я надеюсь, что это вступительное сообщение блога покажет, как легко стало использовать современные протоколы веб-коммуникации в Java, благодаря Java WebSockets (JSR-356) , Java API для обработки JSON (JSR-353) и замечательным проектам, таким как Jetty 9.1 !

  • Как всегда, полный проект доступен на GitHub .

Ссылка: Java WebSockets (JSR-356) на Jetty 9.1 от нашего партнера по JCG Андрея Редько в блоге Андрея Редько {devmind} .