SockJS Fallback

Dev.Hammy·2024년 4월 8일
0

public 인터넷을 통해 통제할 수 없는 제한적인 프록시는 UPgrade 헤더를 전달하도록 구성되지 않았거나 유휴 상태로 보이는 장기 연결을 닫기 때문에 WebSocket 상호 작용을 방해할 수 있습니다.

이 문제에 대한 해결책은 WebSocket 에뮬레이션입니다. 즉, WebSocket을 먼저 사용하려고 시도한 다음 WebSocket 상호 작용을 에뮬레이트하고 동일한 애플리케이션 수준 API를 노출하는 HTTP 기반 기술을 사용하는 것입니다.

Servlet 스택에서 Spring Framework는 SockJS 프로토콜에 대한 서버(및 클라이언트) 지원을 모두 제공합니다.

Overview

SockJS의 목표는 애플리케이션이 WebSocket API를 사용하도록 허용하지만 애플리케이션 코드를 변경할 필요 없이 런타임에 필요할 때 WebSocket이 아닌 대안으로 대체하는 것입니다.

SockJS는 다음으로 구성됩니다.

  • 실행 가능한 설명 테스트 형식으로 정의된 SockJS 프로토콜입니다.

  • SockJS JavaScript 클라이언트 — 브라우저에서 사용하기 위한 클라이언트 라이브러리입니다.

  • Spring Framework spring-websocket 모듈의 구현을 포함한 SockJS 서버 구현.

  • spring-websocket 모듈의 SockJS Java 클라이언트(버전 4.1부터).

SockJS는 브라우저에서 사용하도록 설계되었습니다. 다양한 기술을 사용하여 다양한 브라우저 버전을 지원합니다. SockJS 전송 유형 및 브라우저의 전체 목록은 SockJS 클라이언트 페이지를 참조하세요. 전송은 WebSocket, HTTP 스트리밍 및 HTTP 긴 폴링의 세 가지 일반 범주로 분류됩니다. 이러한 카테고리에 대한 개요는 이 블로그 게시물을 참조하세요.

SockJS 클라이언트는 서버에서 기본 정보를 얻기 위해 GET /info를 보내는 것으로 시작합니다. 그 후에는 어떤 전송 수단을 사용할지 결정해야 합니다. 가능하다면 WebSocket이 사용됩니다. 그렇지 않은 경우 대부분의 브라우저에는 하나 이상의 HTTP 스트리밍 옵션이 있습니다. 그렇지 않은 경우 HTTP(긴) 폴링이 사용됩니다.

모든 전송 요청에는 다음과 같은 URL 구조가 있습니다.

https://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}

어디:

  • {server-id}는 클러스터에서 요청을 라우팅하는 데 유용하지만 그 외에는 사용되지 않습니다.

  • {session-id}는 SockJS 세션에 속하는 HTTP 요청을 연관시킵니다.

  • {transport}는 전송 유형(예: websocket, xhr-streaming 등)을 나타냅니다.

WebSocket 전송에서는 WebSocket 핸드셰이크를 수행하기 위해 단일 HTTP 요청만 필요합니다. 이후의 모든 메시지는 해당 소켓에서 교환됩니다.

HTTP 전송에는 더 많은 요청이 필요합니다. 예를 들어 Ajax/XHR 스트리밍은 서버-클라이언트 메시지에 대한 하나의 장기 실행 요청과 클라이언트-서버 메시지에 대한 추가 HTTP POST 요청에 의존합니다. 긴 폴링은 각 서버에서 클라이언트로 전송한 후 현재 요청을 종료한다는 점을 제외하면 비슷합니다.

SockJS는 최소한의 메시지 프레이밍을 추가합니다. 예를 들어, 서버는 처음에 문자 o(“open” 프레임)를 보내고, 메시지는 a["message1","message2"](JSON 인코딩 배열)로 전송되며, 메시지가 없으면 문자 h("heartbeat" 프레임)로 전송됩니다. 25초(기본값) 동안 흐르고 문자 c("닫기" 프레임)는 세션을 닫습니다.

자세히 알아보려면 브라우저에서 예제를 실행하고 HTTP 요청을 살펴보세요. SockJS 클라이언트를 사용하면 전송 목록을 수정할 수 있으므로 각 전송을 한 번에 하나씩 볼 수 있습니다. SockJS 클라이언트는 브라우저 콘솔에서 유용한 메시지를 활성화하는 디버그 플래그도 제공합니다. 서버 측에서는 org.springframework.web.socket에 대한 TRACE 로깅을 활성화할 수 있습니다. 더 자세한 내용은 SockJS 프로토콜 설명 테스트를 참조하세요.

Enabling Sock JS

다음 예제와 같이 Java 구성을 통해 SockJS를 활성화할 수 있습니다.

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

	@Override
	public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
		registry.addHandler(myHandler(), "/myHandler").withSockJS();
	}

	@Bean
	public WebSocketHandler myHandler() {
		return new MyHandler();
	}

}

다음 예에서는 앞의 예와 동일한 XML 구성을 보여줍니다.

<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:websocket="http://www.springframework.org/schema/websocket"
	xsi:schemaLocation="
		http://www.springframework.org/schema/beans
		https://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/websocket
		https://www.springframework.org/schema/websocket/spring-websocket.xsd">

	<websocket:handlers>
		<websocket:mapping path="/myHandler" handler="myHandler"/>
		<websocket:sockjs/>
	</websocket:handlers>

	<bean id="myHandler" class="org.springframework.samples.MyHandler"/>

</beans>

앞의 예제는 Spring MVC 애플리케이션에서 사용하기 위한 것이며 DispatcherServlet 구성에 포함되어야 합니다. 그러나 Spring의 WebSocket 및 SockJS 지원은 Spring MVC에 의존하지 않습니다. SockJsHttpRequestHandler의 도움으로 다른 HTTP 서비스 환경에 통합하는 것은 비교적 간단합니다.

브라우저 측에서 애플리케이션은 sockjs-client(버전 1.0.x)를 사용할 수 있습니다. W3C WebSocket API를 에뮬레이션하고 서버와 통신하여 실행되는 브라우저에 따라 최상의 전송 옵션을 선택합니다. sockjs-client 페이지와 브라우저에서 지원하는 전송 유형 목록을 참조하세요. 클라이언트는 또한 포함할 전송을 지정하는 등 여러 구성 옵션을 제공합니다.

IE 8 and 9

Internet Explorer 8과 9는 계속 사용됩니다. 이것이 SockJS를 사용하는 주요 이유입니다. 이 섹션에서는 해당 브라우저에서 실행하는 데 대한 중요한 고려 사항을 다룹니다.

SockJS 클라이언트는 Microsoft의 XDomainRequest를 사용하여 IE 8 및 9에서 Ajax/XHR 스트리밍을 지원합니다. 이는 도메인 전체에서 작동하지만 쿠키 전송을 지원하지 않습니다. 쿠키는 Java 애플리케이션에 필수적인 경우가 많습니다. 그러나 SockJS 클라이언트는 Java 서버뿐만 아니라 다양한 서버 유형과 함께 사용할 수 있으므로 쿠키가 중요한지 여부를 알아야 합니다. 그렇다면 SockJS 클라이언트는 스트리밍을 위해 Ajax/XHR을 선호합니다. 그렇지 않으면 iframe 기반 기술을 사용합니다.

SockJS 클라이언트의 첫 번째 /info 요청은 클라이언트의 전송 선택에 영향을 미칠 수 있는 정보에 대한 요청입니다. 이러한 세부 정보 중 하나는 서버 애플리케이션이 쿠키에 의존하는지 여부입니다(예: 인증 목적 또는 고정 세션과의 클러스터링). Spring의 SockJS 지원에는 sessionCookieNeeded라는 속성이 포함되어 있습니다. 대부분의 Java 애플리케이션이 JSESSIONID 쿠키에 의존하기 때문에 기본적으로 활성화되어 있습니다. 애플리케이션에 필요하지 않은 경우 이 옵션을 끌 수 있으며 SockJS 클라이언트는 IE 8 및 9에서 xdr-streaming을 선택해야 합니다.

iframe 기반 전송을 사용하는 경우 HTTP 응답 헤더 X-Frame-OptionsDENY, SAMEORIGIN 또는 ALLOW-FROM <origin>으로 설정하여 특정 페이지에서 IFrame 사용을 차단하도록 브라우저에 지시할 수 있다는 점에 유의하세요. 이는 클릭재킹을 방지하는 데 사용됩니다.

[Note]
Spring Security 3.2+는 모든 응답에서 X-Frame-Options 설정을 지원합니다. 기본적으로 Spring Security Java 구성은 이를 DENY로 설정합니다. 3.2에서는 Spring Security XML 네임스페이스가 기본적으로 해당 헤더를 설정하지 않지만 그렇게 하도록 구성할 수 있습니다. 앞으로는 기본적으로 설정될 수도 있습니다.

X-Frame-Options 헤더 설정을 구성하는 방법에 대한 자세한 내용은 Spring Security 설명서의 기본 보안 헤더를 참조하세요. 추가 배경 정보는 gh-2718을 참조하세요.

애플리케이션이 X-Frame-Options 응답 헤더를 추가하고(필요한 대로!) iframe 기반 전송을 사용하는 경우 헤더 값을 SAMEORIGIN 또는 ALLOW-FROM <origin>으로 설정해야 합니다. Spring SockJS 지원은 iframe에서 로드되기 때문에 SockJS 클라이언트의 위치도 알아야 합니다. 기본적으로 iframe은 CDN 위치에서 SockJS 클라이언트를 다운로드하도록 설정되어 있습니다. 애플리케이션과 동일한 출처의 URL을 사용하도록 이 옵션을 구성하는 것이 좋습니다.

다음 예에서는 Java 구성에서 이를 수행하는 방법을 보여줍니다.

  @Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

	@Override
	public void registerStompEndpoints(StompEndpointRegistry registry) {
		registry.addEndpoint("/portfolio").withSockJS()
				.setClientLibraryUrl("http://localhost:8080/myapp/js/sockjs-client.js");
	}

	// ...

}

XML 네임스페이스는 <websocket:sockjs> 요소를 통해 유사한 옵션을 제공합니다.

[Note]
초기 개발 중에 브라우저가 캐시될 SockJS 요청(예: iframe)을 캐시하지 못하도록 방지하는 SockJS 클라이언트 devel 모드를 활성화하세요. 활성화 방법에 대한 자세한 내용은 SockJS 클라이언트 페이지를 참조하세요.

Heartbeats

SockJS 프로토콜에서는 프록시가 연결이 중단되었다는 결론을 내리지 못하도록 서버가 하트비트 메시지를 보내도록 요구합니다. Spring SockJS 구성에는 빈도를 사용자 정의하는 데 사용할 수 있는 heartbeatTime이라는 속성이 있습니다. 기본적으로 해당 연결에서 다른 메시지가 전송되지 않았다고 가정하면 하트비트는 25초 후에 전송됩니다. 이 25초 값은 공용 인터넷 애플리케이션에 대한 다음 IETF 권장 사항을 따릅니다.

[Note]
WebSocket 및 SockJS를 통해 STOMP를 사용할 때 STOMP 클라이언트와 서버가 교환할 하트비트를 협상하면 SockJS 하트비트가 비활성화됩니다.

Spring SockJS 지원을 통해 TaskScheduler를 구성하여 하트비트 작업을 예약할 수도 있습니다. 작업 스케줄러는 사용 가능한 프로세서 수에 따른 기본 설정을 사용하여 스레드 풀로 지원됩니다. 특정 요구 사항에 따라 설정을 사용자 정의하는 것을 고려해야 합니다.

Client Disconnects

HTTP 스트리밍 및 HTTP 긴 폴링 SockJS 전송에는 평소보다 오랫동안 열려 있는 연결이 필요합니다. 이러한 기술에 대한 개요는 이 블로그 게시물을 참조하세요.

서블릿 컨테이너에서 이는 서블릿 컨테이너 스레드 종료, 요청 처리 및 다른 스레드의 응답에 계속 쓰기를 허용하는 Servlet 3 비동기 지원을 통해 수행됩니다.

특정 문제는 Servlet API가 사라진 클라이언트에 대한 알림을 제공하지 않는다는 것입니다. eclipse-ee4j/servlet-api#44를 참조하세요. 그러나 서블릿 컨테이너는 응답에 쓰려는 후속 시도에서 예외를 발생시킵니다. Spring의 SockJS 서비스는 서버에서 보내는 하트비트(기본적으로 25초마다)를 지원하므로 클라이언트 연결 끊김이 일반적으로 해당 기간(또는 메시지가 더 자주 전송되는 경우 더 일찍) 내에 감지된다는 의미입니다.

[Note]
결과적으로 클라이언트 연결이 끊어져 네트워크 I/O 오류가 발생할 수 있으며 이로 인해 로그가 불필요한 스택 추적으로 채워질 수 있습니다. Spring은 클라이언트 연결 끊김(각 서버에 특정)을 나타내는 네트워크 오류를 식별하고 전용 로그 카테고리 DISCONNECTED_CLIENT_LOG_CATEGORY(AbstractSockJsSession에 정의됨)를 사용하여 최소한의 메시지를 기록하기 위해 최선의 노력을 다합니다. 스택 추적을 확인해야 하는 경우 해당 로그 범주를 TRACE로 설정할 수 있습니다.

SockJS and CORS

원본 간 요청을 허용하는 경우(허용된 origin 참조) SockJS 프로토콜은 XHR 스트리밍 및 폴링 전송에서 도메인 간 지원을 위해 CORS를 사용합니다. 따라서 응답에서 CORS 헤더가 감지되지 않는 한 CORS 헤더가 자동으로 추가됩니다. 따라서 애플리케이션이 이미 CORS 지원(예: 서블릿 필터를 통해)을 제공하도록 구성된 경우 Spring의 SockJsService는 이 부분을 건너뜁니다.

Spring의 SockJsService에서 supressCors 속성을 설정하여 이러한 CORS 헤더 추가를 비활성화하는 것도 가능합니다.

SockJS에는 다음 헤더와 값이 필요합니다.

  • Access-Control-Allow-Origin: Origin 요청 헤더 값에서 초기화됩니다.

  • Access-Control-Allow-Credentials: 항상 true로 설정됩니다.

  • Access-Control-Request-Headers: 동등한 요청 헤더의 값에서 초기화됩니다.

  • Access-Control-Allow-Methods: 전송이 지원하는 HTTP 메소드입니다(TransportType 열거형 참조).

  • Access-Control-Max-Age: 31536000(1년)으로 설정합니다.

정확한 구현을 보려면 AbstractSockJsServiceaddCorsHeaders와 소스 코드의 TransportType 열거형을 참조하세요.

또는 CORS 구성이 허용하는 경우 SockJS 끝점 접두사가 있는 URL을 제외하여 Spring의 SockJsService가 이를 처리하도록 하는 것을 고려하세요.

SockJsClient

Spring은 브라우저를 사용하지 않고 원격 SockJS 끝점에 연결할 수 있는 SockJS Java 클라이언트를 제공합니다. 이는 공용 네트워크를 통해 두 서버 간에 양방향 통신이 필요한 경우(즉, 네트워크 프록시가 WebSocket 프로토콜 사용을 배제할 수 있는 경우) 특히 유용할 수 있습니다. SockJS Java 클라이언트는 테스트 목적(예: 다수의 동시 사용자 시뮬레이션)에도 매우 유용합니다.

SockJS Java 클라이언트는 websocket, xhr-streamingxhr-polling 전송을 지원합니다. 나머지 것들은 브라우저에서만 사용할 수 있습니다.

다음을 사용하여 WebSocketTransport를 구성할 수 있습니다.

  • JSR-356 런타임의 StandardWebSocketClient.

  • Jetty 9+ 기본 WebSocket API를 사용하는 JettyWebSocketClient.

  • Spring의 WebSocketClient 구현.

XhrTransport는 정의상 xhr-streamingxhr-polling을 모두 지원합니다. 클라이언트 관점에서 볼 때 서버에 연결하는 데 사용되는 URL 외에는 차이가 없기 때문입니다. 현재 두 가지 구현이 있습니다.

  • RestTemplateXhrTransport는 HTTP 요청에 Spring의 RestTemplate을 사용합니다.

  • JettyXhrTransport는 HTTP 요청에 Jetty의 HttpClient를 사용합니다.

다음 예제에서는 SockJS 클라이언트를 생성하고 SockJS 엔드포인트에 연결하는 방법을 보여줍니다.

List<Transport> transports = new ArrayList<>(2);
transports.add(new WebSocketTransport(new StandardWebSocketClient()));
transports.add(new RestTemplateXhrTransport());

SockJsClient sockJsClient = new SockJsClient(transports);
sockJsClient.doHandshake(new MyWebSocketHandler(), "ws://example.com:8080/sockjs");

[Note]
SockJS는 메시지에 JSON 형식의 배열을 사용합니다. 기본적으로 Jackson 2가 사용되며 클래스 경로에 있어야 합니다. 또는 SockJsMessageCodec의 사용자 정의 구현을 구성하고 이를 SockJsClient에서 구성할 수 있습니다.

SockJsClient를 사용하여 다수의 동시 사용자를 시뮬레이션하려면 충분한 수의 연결 및 스레드를 허용하도록 기본 HTTP 클라이언트(XHR 전송용)를 구성해야 합니다. 다음 예에서는 Jetty를 사용하여 이를 수행하는 방법을 보여줍니다.

HttpClient jettyHttpClient = new HttpClient();
jettyHttpClient.setMaxConnectionsPerDestination(1000);
jettyHttpClient.setExecutor(new QueuedThreadPool(1000));

다음 예에서는 사용자 정의도 고려해야 하는 서버측 SockJS 관련 속성(자세한 내용은 javadoc 참조)을 보여줍니다.

@Configuration
public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport {

	@Override
	public void registerStompEndpoints(StompEndpointRegistry registry) {
		registry.addEndpoint("/sockjs").withSockJS()
			.setStreamBytesLimit(512 * 1024) // (1) 
			.setHttpMessageCacheSize(1000) // (2)
			.setDisconnectDelay(30 * 1000);  // (3)
	}

	// ...
}

(1) streamBytesLimit 속성을 512KB로 설정합니다(기본값은 128KB — 128 1024).
(2) httpMessageCacheSize 속성을 1,000(기본값은 100)으로 설정합니다.
(3) DisconnectDelay 속성을 30 속성 초로 설정합니다(기본값은 5초 — 5
1000).

0개의 댓글