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/
Dependencies:
- JDK 17
- Spring boot 2.x
- Spring web
- Websocket
Tạo proj dir theo cấu trúc ntn.
Đây là github của Socket.io java server
- https://github.com/trinopoty/socket.io-server-java
- https://trinopoty.github.io/socket.io-server-java/
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.
Server log client đã connected.
Client log đã connect bằng polling, sau đó upgrade lên ws.
Submit form sau đó server log message parsed sang Pojo.
Client log req & resp của ws.
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