본문 바로가기

Java/Spring

[Spring] netty-socketio 소켓 서버 만들기

배경

Web 환경에서 채팅 등 실시간 통신을 만들려면 클라이언트와 서버가 양방향 통신을 할 수 있어야 합니다. 이 글에선 클라이언트가 서버로 요청을 보내는 단방향 프로토콜인 HTTP 대신, WebSocket 프로토콜을 사용하는 서버를 만드는 방법을 소개합니다.


단방향 vs 양방향

HTTP 단방향 네트워크 프로토콜로 설계되었습니다. 클라이언트와 서버가 연결이 성립된 후, 클라이언트가 서버에게 요청을 보내고, 서버가 클라이언트에게 응답을 하면 연결이 종료됩니다. 여기서 주목할 점은 요청은 항상 클라이언트가 하고, 응답은 항상 서버가 한다는 점입니다. 이런 일방적인 프로세스를 단방향이라고 합니다. 라디오를 예로 들 수 있는데요, 라디오를 들으려면 우리는 라디오를 켭니다(요청). 라디오가 켜지면 해당 주파수의 방송을 들을 수 있습니다(응답). 하지만 우리는 그 방송과 직접적으로 또는 실시간으로 커뮤니케이션을 할 수는 없습니다.

 

WebSocket 양방향 네트워크 프로토콜로써, 특정 포트를 통해 클라이언트와 서버가 연결 유지한 채, 서로가 데이터를 주고받을 수 있도록 설계되었습니다. 여기서 주목할 점은 클라이언트와 서버가 서로에게 요청을 할 수 있다는 점입니다. 이러한 것을 양방향이라고 합니다. 전화가 연결된 후에 통화가 유지된 채로 서로 원하는 대화를 이어갈 수 있는 것을 예로 들 수 있습니다.


Netty?

  • 비동기 이벤트 기반 네트워크 애플리케이션 프레임워크
  • WebFlux는 WAS로 Netty를 사용
  • 1개의 이벤트에 대해 다수의 worker 스레드로 동작
  • 이벤트 당 개인의 이벤트 큐를 가지고 있어, 발생 순서와 실행 순서의 일치를 할 수 있음
  • 연결 클라이언트가 많고, IO 처리가 적은 경우

개발환경

  • IDE: STS 4(Maven)
  • Spring Boot: 2.2.2
  • netty-socketio: 2.0.3
    • netty-socketio가 지원하는 socket.io-client 버전 숙지 필수(2버전 기준 client ~4버전 지원)
  • lombok 필요

서버 구현

 

1. pom.xml에 dependency 추가

https://mvnrepository.com/artifact/com.corundumstudio.socketio/netty-socketio/2.0.3

...
<dependency>
	<groupId>com.corundumstudio.socketio</groupId>
	<artifactId>netty-socketio</artifactId> 
	<version>2.0.3</version> 
</dependency>
...

 

2. application.yml 수정

app-config:
  socket-server:
    port: <YOUR_SOCKET_SERVER_PORT>
    host: <YOUR_HOST_NAME_OR_IP>

...

 

3. 구현 클래스

 

Message: 소켓 메시지 구현 클래스

@Data
public class Message {
    private MessageType type;
    private String message;
    private String room;

    public Message() {
    }

    public Message(MessageType type, String message) {
        this.type = type;
        this.message = message;
    }
}

enum MessageType {
    SERVER, CLIENT
}

 

 

SocketService: 소켓 관련 비즈니스 로직 처리

/*
 * 소켓 관련 비즈니스 로직 수행
 */
@Service
@Slf4j
@AllArgsConstructor
public class SocketService {
	
    @Autowired
    private final SocketIOServer server;
	
    /*
     * 한 room에 속한 모든 클라이언트들에게 메시지 전송
     * room: 메시지를 보낼 room 이름
     * eventName: 소켓에게 트리거 할 event 이름
     * message: 소켓들에게 Broadcasting 할 메시지
     */
    public void sendMessageAllClients(String room, String eventName, String message) {
        for (SocketIOClient client : server.getRoomOperations("a").getClients()) {
            client.sendEvent(eventName, new Message(MessageType.SERVER, message));
        }
    }
    
    /*
     * 메시지 송신자를 제외한 모든 클라이언트들에게 메시지 전송
	 * room: 메시지를 보낼 room 이름
	 * eventName: 소켓에게 트리거 할 event 이름
	 * senderClient: 이벤트 송신자 정보
	 * message: 소켓들에게 Broadcasting 할 메시지
	 */
    public void sendMessage(String room, String eventName, SocketIOClient senderClient, String message) {
        for (SocketIOClient client : senderClient.getNamespace().getRoomOperations(room).getClients()) {
            if (!client.getSessionId().equals(senderClient.getSessionId())) {
                client.sendEvent(eventName, new Message(MessageType.SERVER, message));
            }
        }
    }
}

 

SocketIOConfig: 소켓 서버 관련 설정(첫 번째로 실행됨)

@Configuration
public class SocketIOConfig {
	
    @Autowired
    private AppConfig appConfig; // 앱 설정 파일에서 host, port 값 가져오기
	
    @Bean
    public SocketIOServer socketIOServer() {
    	String host = appConfig.getSocketServer().getHost();
    	int port = appConfig.getSocketServer().getPort();
        com.corundumstudio.socketio.Configuration config = new com.corundumstudio.socketio.Configuration();
        config.setHostname(host);
        config.setPort(port);
        return new SocketIOServer(config);
    }

}

 

 

SocketModule: 소켓 서버 이벤트 등록(두 번째로 실행됨)

@Slf4j
@Component
public class SocketModule {

    private final SocketIOServer server;
    private final SocketService socketService;

    public SocketModule(SocketIOServer server, SocketService socketService) {
        this.server = server;
        this.socketService = socketService;

        // 소켓 서버에 연결 시 콜백 지정
        server.addConnectListener(onConnected());
        
        // 소켓 서버에서 연결 해제 시 콜백 지정
        server.addDisconnectListener(onDisconnected());
        
        // 소켓 이벤트 등록, socket.io에서 socket.on(“send_message”) 부분
        server.addEventListener("send_message", Message.class, onChatReceived());

    }

    // 채팅을 받았을 때 실행
    private DataListener<Message> onChatReceived() {
        return (senderClient, data, ackSender) -> {
            log.info(data.toString());
            System.out.println("testtesttest");
            System.out.println(senderClient.getAllRooms().toString());
            System.out.println(senderClient.getNamespace().toString());
            socketService.sendMessage(data.getRoom(),"get_message", senderClient, data.getMessage());
        };
    }
    

    // 소켓 서버에 연결 시 실행
    private ConnectListener onConnected() {
        return (client) -> {
        	// 서버에 연결 시 특정 room에 소켓을 입장시킴
        	String room = client.getHandshakeData().getSingleUrlParam("room");
            client.joinRoom(room);
            log.info("Socket ID[{}]  Connected to socket", client.getSessionId().toString());
        };
    }

    // 소켓 서버에 연결 해제 시 실행
    private DisconnectListener onDisconnected() {
        return client -> {
            log.info("Client[{}] - Disconnected from socket", client.getSessionId().toString());
        };
    }

}

 

 

ServerCommandLineRunner: 소켓 서버 실행(마지막에 실행됨)

@Component
@Slf4j
@RequiredArgsConstructor
public class ServerCommandLineRunner implements CommandLineRunner {
    private final SocketIOServer server;
    @Override
    public void run(String... args) throws Exception {
    	System.out.println("Socket Server is running...");
        server.start();
    }
}

실행 인수 추가

그냥 실행하면 아래와 같은 에러가 콘솔에 표시됩니다. 원인은 Java 버전과 netty-socketio가 호환되지 않기 때문입니다.

 

관련 참고

java.lang.UnsupportedOperationException: Reflective setAccessible(true) disabled
        at io.netty.util.internal.ReflectionUtil.trySetAccessible(ReflectionUtil.java:31)
        at io.netty.util.internal.PlatformDependent0$4.run(PlatformDependent0.java:225)
        at java.base/java.security.AccessController.doPrivileged(Native Method)
        at io.netty.util.internal.PlatformDependent0.<clinit>(PlatformDependent0.java:219)
        at io.netty.util.internal.PlatformDependent.isAndroid(PlatformDependent.java:273)
        at io.netty.util.internal.PlatformDependent.<clinit>(PlatformDependent.java:92)
        at io.netty.channel.epoll.Native.loadNativeLibrary(Native.java:225)
        at io.netty.channel.epoll.Native.<clinit>(Native.java:57)
        at io.netty.channel.epoll.Epoll.<clinit>(Epoll.java:39)
        at io.ktor.server.netty.EventLoopGroupProxy$Companion.create(NettyApplicationEngine.kt:189)
        at io.ktor.server.netty.NettyApplicationEngine.<init>(NettyApplicationEngine.kt:74)
        at io.ktor.server.netty.EngineMain.main(EngineMain.kt:22)

 

따라서 아래 인수를 추가하고 실행해야 합니다.

--add-opens java.base/jdk.internal.misc=ALL-UNNAMED -Dio.netty.tryReflectionSetAccessible=true


서버에서 클라이언트에게 메시지 전송

...
@Autowired
SocketService socketService;

...
socketService.sendMessageAllClients(..., ..., ...);

Spring Netty SocketIO Client

https://github.com/sinrimin/netty-socketio-client


참고

 

'Java > Spring' 카테고리의 다른 글

[오류] A bean with that name has already been defined  (0) 2023.08.01
Java HTTP Request 클래스 구현  (0) 2023.07.21