+5

Setup project Spring boot 2.x + Socket.io Client 4.x

Hí mn, Hôm nay mình sẽ hướng dẫn mn setup 1 project Java Spring boot + Socket.io Client đơn giản nhất chỉ sử dụng 1 port duy nhất.

MÌnh đã research trên internet các tutorial về đề tài này, hầu hết chúng đều hướng dẫn sử dụng lib này https://github.com/mrniko/netty-socketio.

Nhược điểm của nó là nó được implement trên một server Netty có sẵn, thế nên nếu sử dụng Spring boot để tích hợp thì chúng ta sẽ phải sử dụng 2 port. Netty socket.io architecture

  • Khiến cho proj setup bị phức tạp khi ta phải config 2 domain trong quá trình dev và prod.
  • Khó khăn trong việc tích hợp security giữa Socket.io server và Spring security.

Vì thế chúng ta sẽ sử dụng HttpRequest và HttpResponse có sẵn trong embedded Tomcat của Spring boot 2 để handle Socket.io. Engine.io Java Server architecture

Không dài dòng nữa, chúng ta bắt tay vào làm thôi.

Results achieved.

  • Socket io client hoạt động trên giao thức polling và websocket.
  • req/resp json object.

Getting started.

Website quen thuộc của Spring dev. https://start.spring.io/

image.png

Dependencies:

  • JDK 17
  • Spring boot 2.x
  • Spring web
  • Websocket

Tạo proj dir theo cấu trúc ntn. image.png

Đây là github của Socket.io java server

Mn thêm dependency này vào nha.

<dependency>
    <groupId>io.socket</groupId>
    <artifactId>socket.io-server</artifactId>
    <version>4.0.1</version>
</dependency>

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.10-SNAPSHOT</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>io.huyvu</groupId>
    <artifactId>springboot-socketio</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-socketio</name>
    <description>Demo project for Spring Boot x Socketio</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>

        <!--Socket.io-->
        <dependency>
            <groupId>io.socket</groupId>
            <artifactId>socket.io-server</artifactId>
            <version>4.0.1</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <releases>
                <enabled>false</enabled>
            </releases>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </pluginRepository>
        <pluginRepository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <releases>
                <enabled>false</enabled>
            </releases>
        </pluginRepository>
    </pluginRepositories>

</project>

Classes.

/**
* @Author HuyVu
* @CreatedDate 2/24/2023 1:41 PM
*/
package io.huyvu.springbootsocketio.config.socketio;

import io.huyvu.springbootsocketio.util.JsonUtils;
import io.socket.engineio.server.EngineIoServer;
import io.socket.engineio.server.EngineIoServerOptions;
import io.socket.socketio.server.SocketIoNamespace;
import io.socket.socketio.server.SocketIoServer;
import io.socket.socketio.server.SocketIoSocket;
import org.json.JSONObject;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class BeanConfig {

   @Bean
   EngineIoServer engineIoServer() {
       var opt = EngineIoServerOptions.newFromDefault();
       opt.setCorsHandlingDisabled(true);
       var eioServer = new EngineIoServer(opt);
       return eioServer;
   }

   @Bean
   SocketIoServer socketIoServer(EngineIoServer eioServer) {
       var sioServer = new SocketIoServer(eioServer);

       var namespace = sioServer.namespace("/mynamespace");

       namespace.on("connection", args -> {
           var socket = (SocketIoSocket) args[0];
           System.out.println("Client " + socket.getId() + " (" + socket.getInitialHeaders().get("remote_addr") + ") has connected.");

           socket.on("message", args1 -> {

               JSONObject o = (JSONObject) args1[0];

               var messageVo = JsonUtils.toPojoObj(o, MessageVo.class);

               System.out.println("[Client " + socket.getId() + "] " + messageVo);
               socket.send("hello", JsonUtils.toJsonObj(new MessageVo("Server", "Heo khô đi những kỉ niệm xưa kia")));
           });
       });

       return sioServer;
   }

   record MessageVo(
           String author,
           String msg) {

   }

}

package io.huyvu.springbootsocketio.config.socketio;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebSocket
public class EngineIoConfigurator implements WebSocketConfigurer {

    private final EngineIoHandler mEngineIoHandler;

    public EngineIoConfigurator(EngineIoHandler engineIoHandler) {
        mEngineIoHandler = engineIoHandler;
    }

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(mEngineIoHandler, "/socket.io/")
                .addInterceptors(mEngineIoHandler)
                .setAllowedOrigins("*");
    }
}
package io.huyvu.springbootsocketio.config.socketio;

import io.socket.engineio.server.EngineIoServer;
import io.socket.engineio.server.EngineIoWebSocket;
import io.socket.engineio.server.utils.ParseQS;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.socket.*;
import org.springframework.web.socket.server.HandshakeInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Controller
public class EngineIoHandler implements HandshakeInterceptor, WebSocketHandler {

    private static final String ATTRIBUTE_ENGINE_IO_BRIDGE = "engine.io.bridge";
    private static final String ATTRIBUTE_ENGINE_IO_QUERY = "engine.io.query";
    private static final String ATTRIBUTE_ENGINE_IO_HEADERS = "engine.io.headers";

    private final EngineIoServer mEngineIoServer;

    public EngineIoHandler(EngineIoServer engineIoServer) {
        mEngineIoServer = engineIoServer;
    }

    @RequestMapping(
            value = "/socket.io/",
            method = {RequestMethod.GET, RequestMethod.POST, RequestMethod.OPTIONS},
            headers = "Connection!=Upgrade")
    public void httpHandler(HttpServletRequest request, HttpServletResponse response) throws IOException {
        mEngineIoServer.handleRequest(request, response);
    }

    /* HandshakeInterceptor */

    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) {
        attributes.put(ATTRIBUTE_ENGINE_IO_QUERY, request.getURI().getQuery());
        attributes.put(ATTRIBUTE_ENGINE_IO_HEADERS, request.getHeaders());
        return true;
    }

    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
    }

    /* WebSocketHandler */

    @Override
    public boolean supportsPartialMessages() {
        return false;
    }

    @Override
    public void afterConnectionEstablished(WebSocketSession webSocketSession) {
        final EngineIoSpringWebSocket webSocket = new EngineIoSpringWebSocket(webSocketSession);
        webSocketSession.getAttributes().put(ATTRIBUTE_ENGINE_IO_BRIDGE, webSocket);
        mEngineIoServer.handleWebSocket(webSocket);
    }

    @Override
    public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) {
        ((EngineIoSpringWebSocket)webSocketSession.getAttributes().get(ATTRIBUTE_ENGINE_IO_BRIDGE))
                .afterConnectionClosed(closeStatus);
    }

    @Override
    public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage<?> webSocketMessage) {
        ((EngineIoSpringWebSocket)webSocketSession.getAttributes().get(ATTRIBUTE_ENGINE_IO_BRIDGE))
                .handleMessage(webSocketMessage);
    }

    @Override
    public void handleTransportError(WebSocketSession webSocketSession, Throwable throwable) {
        ((EngineIoSpringWebSocket)webSocketSession.getAttributes().get(ATTRIBUTE_ENGINE_IO_BRIDGE))
                .handleTransportError(throwable);
    }

    private static final class EngineIoSpringWebSocket extends EngineIoWebSocket {

        private final WebSocketSession mSession;
        private final Map<String, String> mQuery;
        private final Map<String, List<String>> mHeaders;

        EngineIoSpringWebSocket(WebSocketSession session) {
            mSession = session;

            final String queryString = (String)mSession.getAttributes().get(ATTRIBUTE_ENGINE_IO_QUERY);
            if (queryString != null) {
                mQuery = ParseQS.decode(queryString);
            } else {
                mQuery = new HashMap<>();
            }
            this.mHeaders = (Map<String, List<String>>) mSession.getAttributes().get(ATTRIBUTE_ENGINE_IO_HEADERS);
        }

        /* EngineIoWebSocket */

        @Override
        public Map<String, String> getQuery() {
            return mQuery;
        }

        @Override
        public Map<String, List<String>> getConnectionHeaders() {
            return mHeaders;
        }

        @Override
        public void write(String message) throws IOException {
            mSession.sendMessage(new TextMessage(message));
        }

        @Override
        public void write(byte[] message) throws IOException {
            mSession.sendMessage(new BinaryMessage(message));
        }

        @Override
        public void close() {
            try {
                mSession.close();
            } catch (IOException ignore) {
            }
        }

        /* WebSocketHandler */

        void afterConnectionClosed(CloseStatus closeStatus) {
            emit("close");
        }

        void handleMessage(WebSocketMessage<?> message) {
            if (message.getPayload() instanceof String || message.getPayload() instanceof byte[]) {
                emit("message", (Object) message.getPayload());
            } else {
                throw new RuntimeException(String.format(
                        "Invalid message type received: %s. Expected String or byte[].",
                        message.getPayload().getClass().getName()));
            }
        }

        void handleTransportError(Throwable exception) {
            emit("error", "write error", exception.getMessage());
        }
    }
}
/**
 * @Author HuyVu
 * @CreatedDate 3/1/2023 10:43 AM
 */
package io.huyvu.springbootsocketio.util;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.json.JSONObject;

public class JsonUtils {
    private static final ObjectMapper om = new ObjectMapper();

    public static JSONObject toJsonObj(Object obj) {
        try {
            String s = om.writeValueAsString(obj);
            return new JSONObject(s);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static <T> T toPojoObj(JSONObject jo, Class<T> clazz) {
        try {
            return om.readValue(jo.toString(), clazz);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

}

Client

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Socket io client</title>
    <script src="https://cdn.socket.io/4.4.1/socket.io.min.js"></script>
</head>
<body>
<script>

    const socket = io('/mynamespace')

    socket.on('hello', (arg) => {
        console.log('connected', arg)
    })

    socket.on('disconnect', () => {
        console.log('disconnect', socket.id) // undefined
    })

    const submit = () => {
        const txt = document.getElementById('input').value
        socket.emit('message', {
            author: 'client',
            msg: txt
        })
    }

</script>

<input id="input" type="text"/>
<button onclick="submit()">Submit</button>

</body>
</html>

Results.

Client form. image.png

Server log client đã connected. image.png

Client log đã connect bằng polling, sau đó upgrade lên ws. image.png

image.png

Submit form sau đó server log message parsed sang Pojo. image.png

Client log req & resp của ws.

image.png

image.png Nhanh gọn lẹ đúng không nào, nếu có bất cứ câu hỏi nào plz leave comments.

Thank.

github src: https://github.com/huyvu8051/springboot-socketio


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.