1. 개요

WebSocket은 양방향, 전이중, 실시간 클라이언트/서버 통신을 제공하여 서버와 웹 브라우저 간의 효율적인 통신 제한에 대한 대안을 제공합니다. 서버는 언제든지 클라이언트에 데이터를 보낼 수 있습니다. TCP를 통해 실행되기 때문에 대기 시간이 짧은 저수준 통신도 제공하고 각 메시지의 오버헤드를 줄 입니다.

이 기사에서는 채팅과 유사한 애플리케이션을 작성하여 WebSockets용 Java API를 살펴보겠습니다.

2. JSR 356

JSR 356 또는 WebSocket용 Java API는 Java 개발자가 서버 측과 Java 클라이언트 측 모두에서 WebSocket을 애플리케이션과 통합하는 데 사용할 수 있는 API를 지정합니다.

이 Java API는 서버 및 클라이언트 측 구성 요소를 모두 제공합니다.

  • Server : javax.websocket.server 패키지의 모든 것.
  • 클라이언트 : 클라이언트 측 API와 서버 및 클라이언트 모두에 대한 공통 라이브러리로 구성된 javax.websocket 패키지의 내용입니다.

3. WebSocket을 사용하여 채팅 만들기

우리는 매우 간단한 채팅과 같은 애플리케이션을 만들 것입니다. 모든 사용자는 모든 브라우저에서 채팅을 열고 이름을 입력하고 채팅에 로그인하고 채팅에 연결된 모든 사람과 통신을 시작할 수 있습니다.

pom.xml 파일 에 최신 의존성을 추가하여 시작하겠습니다 .

<dependency>
    <groupId>javax.websocket</groupId>
    <artifactId>javax.websocket-api</artifactId>
    <version>1.1</version>
</dependency>

최신 버전은 여기 에서 찾을 수 있습니다 .

Java 개체 를 JSON 표현으로 또는 그 반대로 변환하기 위해 Gson을 사용합니다.

<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.0</version>
</dependency>

최신 버전은 Maven Central 리포지토리에서 사용할 수 있습니다.

3.1. Endpoints 구성

Endpoints을 구성하는 방법에는 어노테이션 기반과 확장 기반 의 두 가지가 있습니다 . javax.websocket.Endpoint 클래스 를 확장 하거나 전용 메서드 수준 어노테이션을 사용할 수 있습니다. 어노테이션 모델은 프로그램 모델에 비해 더 깨끗한 코드로 이어지기 때문에 어노테이션은 일반적인 코딩 선택이 되었습니다. 이 경우 WebSocket Endpoints 수명 주기 이벤트는 다음 어노테이션에 의해 처리됩니다.

  • @ServerEndpoint: @ServerEndpoint 장식된 경우 컨테이너는 특정 URI 공간을 수신 하는 WebSocket 서버 로 클래스의 가용성을 보장합니다.
  • @ClientEndpoint : 이 어노테이션으로 장식된 클래스는 WebSocket 클라이언트 로 취급됩니다.
  • @OnOpen : 새로운 WebSocket 연결이 시작될 때 @OnOpen 이 포함 된 Java 메서드가 컨테이너에 의해 호출됩니다.
  • @OnMessage : @OnMessage 로 어노테이션이 달린 Java 메소드 는 메시지가 엔드포인트로 전송될 때 WebSocket 컨테이너 에서 정보를 수신합니다.
  • @OnError : @OnError 가 있는 메서드는 통신에 문제가 있을 때 호출됩니다.
  • @OnClose : WebSocket 연결이 닫힐 때 컨테이너가 호출하는 Java 메서드를 데코레이션하는 데 사용됩니다.

3.2. 서버 엔드포인트 작성

@ServerEndpoint 어노테이션을 달아 Java 클래스 WebSocket 서버 Endpoints을 선언합니다 . 엔드포인트가 배포되는 URI도 지정합니다. URI는 서버 컨테이너의 루트에 상대적으로 정의되며 슬래시로 시작해야 합니다.

@ServerEndpoint(value = "/chat/{username}")
public class ChatEndpoint {

    @OnOpen
    public void onOpen(Session session) throws IOException {
        // Get session and WebSocket connection
    }

    @OnMessage
    public void onMessage(Session session, Message message) throws IOException {
        // Handle new messages
    }

    @OnClose
    public void onClose(Session session) throws IOException {
        // WebSocket connection closes
    }

    @OnError
    public void onError(Session session, Throwable throwable) {
        // Do error handling here
    }
}

위의 코드는 채팅과 유사한 애플리케이션의 서버 Endpoints 골격입니다. 보시다시피 각각의 메서드에 매핑된 4개의 어노테이션이 있습니다. 아래에서 이러한 방법의 구현을 볼 수 있습니다.

@ServerEndpoint(value="/chat/{username}")
public class ChatEndpoint {
 
    private Session session;
    private static Set<ChatEndpoint> chatEndpoints 
      = new CopyOnWriteArraySet<>();
    private static HashMap<String, String> users = new HashMap<>();

    @OnOpen
    public void onOpen(
      Session session, 
      @PathParam("username") String username) throws IOException {
 
        this.session = session;
        chatEndpoints.add(this);
        users.put(session.getId(), username);

        Message message = new Message();
        message.setFrom(username);
        message.setContent("Connected!");
        broadcast(message);
    }

    @OnMessage
    public void onMessage(Session session, Message message) 
      throws IOException {
 
        message.setFrom(users.get(session.getId()));
        broadcast(message);
    }

    @OnClose
    public void onClose(Session session) throws IOException {
 
        chatEndpoints.remove(this);
        Message message = new Message();
        message.setFrom(users.get(session.getId()));
        message.setContent("Disconnected!");
        broadcast(message);
    }

    @OnError
    public void onError(Session session, Throwable throwable) {
        // Do error handling here
    }

    private static void broadcast(Message message) 
      throws IOException, EncodeException {
 
        chatEndpoints.forEach(endpoint -> {
            synchronized (endpoint) {
                try {
                    endpoint.session.getBasicRemote().
                      sendObject(message);
                } catch (IOException | EncodeException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

새 사용자가 로그인하면( @OnOpen ) 즉시 활성 사용자의 데이터 구조에 매핑됩니다. 그런 다음 브로드캐스트 방법 을 사용하여 메시지가 생성되고 모든 Endpoints으로 전송됩니다 .

이 방법은 연결된 사용자 가 새 메시지( @OnMessage )를 보낼 때마다 사용됩니다. 이것이 채팅의 주요 목적입니다.

어느 시점에서 오류가 발생하면 @OnError 어노테이션이 있는 메서드가 오류를 처리합니다. 이 방법을 사용하여 오류에 대한 정보를 기록하고 엔드포인트를 지울 수 있습니다.

마지막으로 사용자가 더 이상 채팅에 연결되어 있지 않으면 @OnClose 메서드 가 엔드포인트를 지우고 모든 사용자에게 사용자가 연결 해제되었음을 알립니다.

4. 메시지 유형

WebSocket 사양은 두 가지 온-와이어 데이터 형식(텍스트 및 바이너리)을 지원합니다. API는 이러한 형식을 모두 지원하고 사양에 정의된 대로 Java 개체 및 상태 확인 메시지(ping-pong)와 함께 작동하는 기능을 추가합니다.

  • Text : 모든 텍스트 데이터( java.lang.String , 프리미티브 또는 이와 동등한 래퍼 클래스)
  • Binary : java.nio.ByteBuffer 또는 byte[] (바이트 배열) 로 표현되는 이진 데이터(예: 오디오, 이미지 등 )
  • Java 개체 : API를 사용하면 코드에서 기본(Java 개체) 표현으로 작업하고 사용자 정의 변환기(인코더/디코더)를 사용하여 WebSocket 프로토콜에서 허용하는 호환 가능한 온-와이어 형식(텍스트, 바이너리)으로 변환할 수 있습니다.
  • Ping-Pong : javax.websocket.PongMessage 는 상태 확인(ping) 요청에 대한 응답으로 WebSocket 피어에서 보낸 승인입니다.

우리 애플리케이션의 경우 Java 개체를 사용합니다. 메시지 인코딩 및 디코딩을 위한 클래스를 만듭니다.

4.1. 인코더

인코더는 Java 개체를 가져와 JSON, XML 또는 이진 표현과 같은 메시지로 전송하기에 적합한 일반적인 표현을 생성합니다. 인코더는 Encoder.Text<T> 또는 Encoder.Binary<T> 인터페이스를 구현하여 사용할 수 있습니다.

아래 코드 에서 인코딩할 Message 클래스를 정의하고 encode 메서드 에서 Java 개체를 JSON으로 인코딩 하기 위해 Gson을 사용합니다.

public class Message {
    private String from;
    private String to;
    private String content;
    
    //standard constructors, getters, setters
}
public class MessageEncoder implements Encoder.Text<Message> {

    private static Gson gson = new Gson();

    @Override
    public String encode(Message message) throws EncodeException {
        return gson.toJson(message);
    }

    @Override
    public void init(EndpointConfig endpointConfig) {
        // Custom initialization logic
    }

    @Override
    public void destroy() {
        // Close resources
    }
}

4.2. 디코더

디코더는 인코더의 반대이며 데이터를 다시 Java 객체로 변환하는 데 사용됩니다. 디코더는 Decoder.Text<T> 또는 Decoder.Binary<T> 인터페이스를 사용하여 구현할 수 있습니다.

인코더에서 보았듯이 디코딩 방법은 Endpoints으로 전송된 메시지에서 검색된 JSON을 가져오고 Gson을 사용하여 Message 라는 Java 클래스로 변환하는 곳입니다 .

public class MessageDecoder implements Decoder.Text<Message> {

    private static Gson gson = new Gson();

    @Override
    public Message decode(String s) throws DecodeException {
        return gson.fromJson(s, Message.class);
    }

    @Override
    public boolean willDecode(String s) {
        return (s != null);
    }

    @Override
    public void init(EndpointConfig endpointConfig) {
        // Custom initialization logic
    }

    @Override
    public void destroy() {
        // Close resources
    }
}

4.3. 서버 Endpoints에서 인코더 및 디코더 설정

클래스 수준 어노테이션 @ServerEndpoint 에서 데이터 인코딩 및 디코딩을 위해 생성된 클래스를 추가하여 모든 것을 함께 넣어 보겠습니다 .

@ServerEndpoint( 
  value="/chat/{username}", 
  decoders = MessageDecoder.class, 
  encoders = MessageEncoder.class )

메시지가 Endpoints으로 전송될 때마다 자동으로 JSON 또는 Java 개체로 변환됩니다.

5. 결론

이 기사에서는 WebSockets용 Java API가 무엇인지, 실시간 채팅과 같은 애플리케이션을 구축하는 데 어떻게 도움이 되는지 살펴보았습니다.

엔드포인트 생성을 위한 두 가지 프로그래밍 모델인 어노테이션 및 프로그래밍 방식을 살펴보았습니다. 수명 주기 메서드와 함께 애플리케이션의 어노테이션 모델을 사용하여 Endpoints을 정의했습니다.

또한 서버와 클라이언트 간에 통신할 수 있으려면 Java 개체를 JSON으로 또는 그 반대로 변환하기 위해 인코더와 디코더가 필요하다는 것을 알았습니다.

JSR 356 API는 매우 간단하며 WebSocket 애플리케이션을 매우 쉽게 구축할 수 있게 해주는 어노테이션 기반 프로그래밍 모델입니다.

예제에서 빌드한 애플리케이션을 실행하려면 웹 서버에 war 파일을 배포하고 http://localhost:8080/java-websocket/ URL로 이동하기만 하면 됩니다. 여기 에서 저장소에 대한 링크를 찾을 수 있습니다 .

Generic footer banner