The Spring Festival holiday looked at websocket, made a note, the original link: oolap.com/websocket

WebSocket has a long history and is often used in “server push” scenarios. Recently started learning about WebSocket (starting with Tomcat Examples), the main purpose of this article is to take study notes and document a development guide.

See github.com/hanyong/exe…

HTTP Protocol Overview

Let’s look at HTTP first.

An HTTP request is as follows:

The client The server
1. The client establishes a TCP connection to the server
2. The client sends the request
3. The client waits for the response
4. Server receives request
5. The server sends a response
6. The client receives a response. Procedure
7. End of the request

TCP is a full-duplex protocol that supports simultaneous bidirectional reads and writes. However, the traditional HTTP protocol has several problems:

  1. The request process is sequential, with the client and server waiting for each other.
  2. Requests are one-way and the client must always initiate the request first.

That is, the traditional HTTP 1.0/1.1 protocol does not take full advantage of the power of TCP connections.

  1. HTTP is stateless. Whether two requests are sent from the same TCP connection or from different TCP connections is equivalent to the server.

The HTTP protocol is designed primarily to simplify the programming model. Think of traditional CGI scripts, which can provide Web services as long as they accept input and produce output. The HTTP protocol lacks the session layer in the ISO Layer 7 network model, and dynamic Web applications use cookies to store session information. HTTP/1.1 enables long connections by default to optimize performance, but HTTP connections and requests remain stateless. For traditional Web applications that provide static content services or return relatively deterministic information, this design is fine or tolerable with some limitations. Stateless design also simplifies HTTP testing, and log playback has become an important means of testing HTTP services.

Websocket protocol description

Until the “server push” scenario. The server information can change at any time, and we want to notify the client of the latest information immediately. The traditional solution is that the client constantly polls the server, say once per second. This polling incurs many additional costs, including mobile traffic charges, and the programming model is relatively complex. Therefore, it is time to open up TCP’s bi-directional communication capabilities. We can rewrite a TCP server and use a new protocol to communicate. However, maybe in order to reuse HTTP port 80, attach to the existing HTTP ecosystem, so that the Smooth upgrade of Web applications, WebSocket is designed based on HTTP protocol, as the name implies, is based on web HTTP protocol, release the ability of the native TCP socket. So websockets communicate over HTTP at first and then convert to Websockets later.

A WebSocket connection is set up as follows:

The client The server
1. The client establishes a TCP connection to the server
2. The client requests that the current TCP connection be used as a Websocket
3. The server receives the request, agrees and confirms that the TCP connection will be used as a Websocket
4. The client receives an acknowledgement, and the HTTP communication ends
5. The two parties use websocket protocol for free bidirectional communication

Websocket can be established based on HTTP, namely WS protocol, or HTTPS, namely WSS protocol, sure enough, reuse HTTP infrastructure.

HTTP and Websocket clients

The HTTP client listens for the response only after it sends the request and receives it once. Common HTTP clients are:

  1. curl, such ascurl localhost:8080/http.
  2. Browser JS clients such as AngularJS’s $HTTP service.

    $http.get("/http").then(function(resp) {
        vm.msg = resp.data;
    });
    Copy the code
  3. Type the URL directly into the browser.

Looking back at the “server push” scenario, the biggest difference between Websocket and HTTP is that the server doesn’t have to wait for a request, and the terms “request” and “response” are no longer used. Instead, they are called messages, and both sides can send messages to each other at any time. HTTP clients don’t listen for messages all the time, so obviously they can’t be used as WebSocket clients (regardless of protocol compatibility). To use Websocket, both the client and the server need to be modified. Common Websocket clients are:

  1. Browser JS client. Thanks to browser vendors, webSocket is now supported by all major browsers. Reference: developer.mozilla.org/en-US/docs/…

    var uri = "ws://" + window.location.host + "/ws";
    vm.ws = new WebSocket(uri);
    vm.ws.onmessage = function(event) {
        vm.msg = event.data;
        $scope.$apply();
    }
    Copy the code

Websocket server development

As for server-side development, Java defines a set of Javax. servlet-API, an HttpServlet is an HTTP service. Java WebSocket is not based on a simple extension of servlet-API, but a new set of Javax. websocket-API is defined. One Websocket service corresponds to one Endpoint. Corresponding to ServletContext, websocket-API also defines WebSocketContainer, The interface to register webSockets programmatically is ServerContainer, which inherits from WebSocketContainer. A Websocket can accept and manage multiple connections and therefore be treated as a server. Mainstream servlet containers support WebSockets, such as Tomcat, Jetty, etc. Look at the ServerContainer API documentation and find the ServerContainer from the ServletContext Attribute.

    @Bean
    public ServerContainer serverContainer(ServletContext context) {
        return (ServerContainer) context.getAttribute(ServerContainer.class.getName());
    }
Copy the code

The key to registering an Endpoint is two things, the Endpoint class and the corresponding URL path. The code is as follows:

@org.springframework.context.annotation.Configuration public class WsConfig implements ApplicationRunner { @Autowired protected ServerContainer serverContainer; @Override public void run(ApplicationArguments args) throws Exception { ServerEndpointConfig sec = ServerEndpointConfig.Builder.create(Ws.class, "/ws").build(); serverContainer.addEndpoint(sec); }}Copy the code

An example of a simple servlet is as follows:

public class Http extends HttpServlet { private static final long serialVersionUID = 1L; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String msg = "Hello Http"; resp.setContentType(MimeTypeUtils.TEXT_PLAIN_VALUE); resp.getWriter().println(msg); }}Copy the code

An example of a simple Endpoint is as follows:

public class Ws extends Endpoint { @Override public void onOpen(Session session, EndpointConfig config) { String msg = "Hello WebSocket"; Basic remote = session.getBasicRemote(); try { remote.sendText(msg); } catch (IOException ex) { throw new RuntimeException(ex); }}}Copy the code

They look similar, but in fact they are very different.

  1. doGet()Yes process a request, input parametersreqrespThis request is valid only once.
  2. onOpen()Yes To open a WebSocket session, enter parameterssessionThe session is always valid and can send and receive multiple messages. The first message is sent when the session is opened in the example.
  3. The Endpoint is stateful, and the container creates an Endpoint object instance for each session to maintain the current session state information. So to register an Endpoint, you must use classes instead of objects, and the Endpoint class must build functions with no arguments. However, servlets are stateless and can be registered using Servlet instances. There is only one instance of a Servlet object for multiple connections and threads.
  4. Websocket connections are stateful and must use long connections. A connection is naturally a session. HTTP connections are stateless, and servlets manage sessions with cookies and are unaware of long connections. Note: Websocket long connections are very different from HTTP long connections. HTTP long connections are only used to reuse existing TCP connections when sending requests to the same server to optimize performance. Different COOKIES may be associated with different HTTP sessions when sending requests. A WebSocket long connection serves only one session and can send and receive only messages of the session. After HTTP long connections are converted to Websockets, they cannot be used to send HTTP requests.
  5. HTTP requests are serial, and an HTTP long connection must return after the previous request response before it can continue sending requests. Both sides of the Websocket can send and receive messages without waiting. The message just received may not correspond to the message just sent. The meaning of sending and receiving messages should be confirmed when the WebSocket connection is established.
  6. Since the meaning of webSocket sending and receiving messages is confirmed when the WebSocket connection is established, many headers and parameters, including cookie information identifying the session, can be omitted during the sending and receiving of messages, effectively saving bandwidth.

The server code, combined with the previous client code, tests the WebSocket.

Use curl to test the websocket

The websocket client mentioned earlier only has browser JS. Curl curl curl curl curl curl curl curl curl curl curl curl curl curl curl curl Curl does not receive responses or messages.

--no-buffer -H 'Connection: keep-alive, Upgrade' -H 'Upgrade: websocket' -v -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: websocket'
Copy the code

Curl does not output until the buffer is full or the response is received. Since websocket responses “never end “, –no-buffer disables the buffer inside curl to output the received information immediately. Other parameters are required information to be filtered out when the websocket is set up by the replication browser.

Using curl to test websocket:

$ curl --no-buffer -H 'Connection: keep-alive, Upgrade' -H 'Upgrade: websocket' -v -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: websocket' http://localhost:8080/ws | od -t c % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Trying ::1... * Connected to localhost (::1) port 8080 (#0) > GET /ws HTTP/1.1 > Host: localhost:8080 > user-agent: Curl /7.47.0 > Accept: */* > Connection: keep-alive, Upgrade > Upgrade: websocket > sec-websocket-version: 13 > sec-websocket-key: WebSocket > < HTTP/1.1 101 < Upgrade: WebSocket < Connection: Upgrade < sec-websocket-accept: qVby4apnn2tTYtB1nPPVYUn68gY= < Date: Tue, 31 Jan 2017 12:31:23 GMT 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0{ [17 bytes data] 0000000 201 017 H e l l o W e b S  o c k e 100 32 0 32 0 0 26 0 --:--:-- 0:00:01 --:--:-- 26^CCopy the code

The websocket message contains a binary header, which is escaped using OD-t C. The header is 201 017 bytes, where 017 = 15, indicating the length of the message. The message length is expressed as variable-length integers, with multibyte lengths exceeding 127. We can see that the body of the message is not fully output. This is because the OD command page is buffered and will be output only after one line is saved. Change to Od-t c-w1-v, i.e. one line per byte, to avoid this problem. -v displays duplicate lines. By default, duplicate lines are compressed.

The header information, the Sec – WebSocket – Key is used to test server support WebSocket, details refer to tools.ietf.org/html/rfc645… . The server returns the correct SEC-websocket-accept message indicating that it supports WebSocket, and the appropriate WebSocket client should validate this value.

When sendText() is used, websocket automatically adds the message header information to automatically frame and unframe the message. Websocket also supports sending binary messages or streaming data. Tests have found that a Websocket can support both binary and text messaging. However, other types of messages cannot be sent while streaming messages are being sent.

Can webSockets be used as raw TCP sockets when sending streaming binary data? That is, copy all the data without adding the message frame header. The test code is as follows:

public class Ws extends Endpoint { @Override public void onOpen(Session session, EndpointConfig config) { byte[] msg = { 'w', 's', }; Basic remote = session.getBasicRemote(); try { OutputStream out = remote.getSendStream(); for (int i = 0; i < 1; ++i) { for (int j = 0; j < 4; ++j) { out.write(msg[j % msg.length]); } out.flush(); } } catch (IOException ex) { throw new RuntimeException(ex); }}}Copy the code

The outer loop represents how many times data is written, and the inner loop represents the length of data written each time. If the write length is less than 4, the client does not receive any data.

The test found the following problems:

  1. The data length is small and does not callflush()“, the client never receives data. This also differs from servlets, which normally should not be calledflush()When the servlet method returns, the response is returned to the client, explicitly invokedflush()Cause toChunkedMethod returns part of the content immediately.
  2. Streaming data still adds headers. Od output is as follows:

    0000000 002
    0000001 004
    0000002   w
    0000003   s
    0000004   w
    0000005   s
    Copy the code

    And if you write data multiple times, you add a header each time. Write 3 times od output as follows:

    0000000 002
    0000001 004
    0000002   w
    0000003   s
    0000004   w
    0000005   s
    0000006  \0
    0000007 004
    0000010   w
    0000011   s
    0000012   w
    0000013   s
    0000014  \0
    0000015 004
    0000016   w
    0000017   s
    0000020   w
    0000021   s
    Copy the code

    The first header is different, and each subsequent header is \0 + data length.

  3. A long write sends data to the client without calling Flush (). , and send data into N segments, each with a header.

Question: How do I end streaming messaging?

Therefore, websocket will always add headers for framing and cannot be used as the original TCP socket. Think about it, websocket can’t distinguish between streaming data and frame messages without a message header, and ping/ Pong application layer heartbeat detection can be embedded between ordinary messages. Therefore, WebSocket should be a session layer protocol that supports message framing and application layer heartbeat detection.

Use WebSocket in Spring

Spring provides a good support for websocket, reference documents: docs. Spring. IO/spring/docs… .

As mentioned earlier, Java Websocket-API requires websockets to be registered using an Endpoint class, and then the Servlet container creates an Endpoint object instance for each connection. This makes it difficult to include the Endpoint instance in the Spring container. Spring’s handling of WebSockets is similar to that of HTTP requests using DispatcherServlet. Spring defines the WebSocketHandler interface to handle Websocket requests, similar to HttpRequestHandler for HTTP. Spring then intercepts all hosted WebSocket requests and distributes them to the WebSocketHandler. The only drawback is that WebSocketHandler, like HttpRequestHandler, is a stateless singleton and can’t store individual session state directly, but that doesn’t matter. Next we can forget about the Java Websocket-API and use the Spring WebSocket API for programming.

The websocket of the previous example is rewritten using Spring as follows:

public class SpringWs extends AbstractWebSocketHandler { @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { super.afterConnectionEstablished(session); session.sendMessage(new TextMessage("Hello WebSocket")); }}Copy the code

The code for registering websocket is as follows:

@Configuration @EnableWebSocket public class SpringWsConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new SpringWs(), "/ws"); }}Copy the code

Taking a look at the AbstractWebSocketHandler method definition, we find that streaming data sending and receiving is missing, but encapsulation simplifies the sending and receiving of messages commonly used in Web applications.

Websocket Heartbeat detection

Websocket provides heartbeat detection at the application layer, consisting of ping/ Pong messages. Ping means “Are you there?” Pong means “I’m here!” . Ping /pong corresponds to ping requests and ping responses to IP. Websockets do not use request response mode, so they are called messages. Websocket distinguishes between data messages and control messages, so only listening for data messages will not receive control messages. Ping /pong is a control message. Browsers usually do not support control of ping/pong messages, and reply to ping directly and ignore pong. Both Java Websocket-API and Spring provide direct support for sending ping/pong, ignoring pong by default.

Websocket Java client

Once the websocket connection is established, the client and server are peer, both called Endpoint, and both ends can reuse protocol parsing and message monitoring codes. So Java websocket – API defines the client API, also called WebSocketContainer. ConnectToServer () can establish a connection to the server client. Mentioned above is WebSocketContainer ServerContainer, clients can also call ContainerProvider. GetWebSocketContainer WebSocketContainer (). Example code is as follows:

public class Client { public static void main(String[] args) throws Exception { WebSocketContainer container = ContainerProvider.getWebSocketContainer(); URI path = new URI("ws://localhost:8080/ws"); Session session = container.connectToServer(new Ws(), path); session.addMessageHandler(new javax.websocket.MessageHandler.Whole<String>() { @Override public void onMessage(String message) { System.out.println("Client get message: " + message); }}); Thread.sleep(5000); }}Copy the code
  1. Java Websocket-API client code is annotation-oriented, and the Endpoint class must be added@ClientEndpointAnnotation.onOpen()Methods must also be added@OnOpenThe Endpoint class uses the normal POJO class.
  2. Since the client can control the creation of each connection, you can use the Endpoint object example as a parameter, which should make it easier to integrate with Spring.
  3. Best practices for the Endpoint class should only be used to save session state during the session.
  4. Websocket client does not block process (no foreground thread?) So the sample program adds sleep to prevent the program from exiting immediately.
  5. Websocket clients do not rely on servlet containers, and ordinary applications can easily use WebSockets.

The client did not receive a message when executing addMessageHandler(). The Endpoint class adds message handling:

    @javax.websocket.OnMessage
    public void onMessage(String message) {
        System.out.println("Endpoint onMessage: " + message);
    }
Copy the code

This time I finally received the message, and the following exception occurred:

Exception in thread "main" java.lang.IllegalStateException: A text message handler has already been configured
    at org.apache.tomcat.websocket.WsSession.doAddMessageHandler(WsSession.java:252)
    at org.apache.tomcat.websocket.WsSession.addMessageHandler(WsSession.java:213)
    at atest.Client.main(Client.java:16)
Copy the code

Unknown:

  1. To avoid missing messages, add message handling methods (using annotations) to the Endpoint class, andaddMessageHandler()For dynamically adding message handlers. Remove message processing on the Endpoint class, the server sends messages slowly, and the client receives them. Or the WebSocket session application layer maintains the ready state and tells the server ready after the client initialization.
  2. Only one message handler can be set for one message type, such as String text messages.

Spring also has carried on the simple to the websocket client encapsulation, and reuse the WebSocketHandler interface design, core classes is WebSocketConnectionManager connection is established, the example code is as follows:

public class SpringClient { public static void main(String[] args) { SpringApplication app = new SpringApplication(SpringClient.class, SpringWs.class); app.setApplicationContextClass(AnnotationConfigApplicationContext.class); WebSocketConnectionManager conn = app.run(args).getBean(WebSocketConnectionManager.class); conn.start(); } @Bean public WebSocketClient webSocketClient() { return new StandardWebSocketClient(); } @Bean public WebSocketConnectionManager conn(WebSocketClient client, WebSocketHandler handler) { return new WebSocketConnectionManager(client, handler, "ws://localhost:8080/ws"); }}Copy the code

The client and server use the same WebSocketHandler, and after running the sample program the client and server each send and receive the same message. Note that there is no sleep on the client, the message is received before the client exits gracefully.

Modify the WebSocketHandler to send ping messages and print pong messages. The test found that both the server and the client received a Pong message. It can be seen that Spring also processed the ping message and automatically replied to the Pong message.

The overall feel of the Spring WebSocket API is much simpler and more consistent than Javax. websocket-API. Except for the loss of streaming read-write interfaces, which is not important.