개발일기/Spring + Unix Domain Socket

[Spring + UDS] 4. Spring Boot Client 구현

ignuy 2024. 8. 8.

실행 환경

실행환경은 다음과 같다.

OS : Alpine Linux(3.20.0)
Lan. : Java 21
Framework : SpringBoot 3.3.2
dependencies : (아래 코드 뭉치 참조)

// dependencies
dependencies {
	implementation 'org.springframework.boot:spring-boot-starter'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'com.kohlschutter.junixsocket:junixsocket-core:2.10.0'
	implementation 'com.kohlschutter.junixsocket:junixsocket-common:2.10.0'
	implementation 'org.projectlombok:lombok'

	annotationProcessor 'org.projectlombok:lombok'

	developmentOnly 'org.springframework.boot:spring-boot-devtools'

	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

테스트 앱 프로세스

socket 통신에 대한 기본적인 방식은 이해하고 있다 생각하고 넘어가겠다. 일단 통신이 제대로 이루어지는지 확인하기 위해 Spring Controller에 간단하게 구현부를 작성했다. 테스트에 호출할 순서는 아래와 같다.

  1. POST /connect 요청으로 socket에 연결하고 java io의 output stream과 input stream을 열어준다.
  2. POST /write 요청으로 socket을 통해 message를 전송한다.
  3. Get /read 요청으로 C server가 echo 한 message를 읽어 반환한다.
  4. POST /disconnect 요청으로 연결을 종료하고 리소스를 반환한다.

전체 코드

package com.example.test;

import lombok.extern.slf4j.Slf4j;
import org.newsclub.net.unix.AFSocketAddress;
import org.newsclub.net.unix.AFUNIXSocket;
import org.newsclub.net.unix.AFUNIXSocketAddress;
import org.springframework.web.bind.annotation.*;

import java.io.*;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.URI;
import java.nio.charset.StandardCharsets;

@Slf4j
@RestController
@RequestMapping("/api")
public class MessageController {

    private static final String SOCKET_PATH = "/tmp/my_unix_socket.sock";
    private AFUNIXSocket sock;
    private OutputStream out;
    private InputStream in;

    @PostMapping("/connect")
    public String connect() throws IOException {
        SocketAddress endpoint = getSocketAddress(SOCKET_PATH);
        sock = AFUNIXSocket.connectTo(AFUNIXSocketAddress.of(endpoint));
        out = sock.getOutputStream();
        in = sock.getInputStream();

        log.info("UDS Connected Completely to {}", endpoint);
        return "UDS Connected Completely to " + endpoint + "\\n";
    }

    @PostMapping("/disconnect")
    public String disconnect() throws IOException {
        sock.close();
        log.info("UDS Disconnected Completely");
        return "UDS Disconnected Completely\\n";
    }

    @PostMapping("/write")
    public String write(@RequestBody MessageRequest request) throws IOException {
        if (sock == null || !sock.isConnected()) {
            log.info("UDS NOT CONNECTED!!!");
            return "UDS Not Connected";
        }

        try {
            log.info("Now writing string({}) to the server...", request.getMessage());

            out.write(request.getMessage().getBytes(StandardCharsets.UTF_8));
            out.flush();
        } catch (IOException e) {
            log.info("ERROR...{}", e.getMessage());
            throw e;
        }

        log.info("End of communication");
        return "End of communication\\n";
    }

    @GetMapping("/read")
    public String read() throws IOException {
        if (sock == null || !sock.isConnected()) {
            log.info("UDS NOT CONNECTED!!!");
            return "UDS Not Connected";
        }

        String response = "Initial String";

        try (DataInputStream dis = new DataInputStream(in)) {
            byte[] buf = new byte[2048];
            int read = dis.read(buf);

            if (read > 0) {
                response = new String(buf, 0, read, StandardCharsets.UTF_8);
                log.info("Server said: {}", response);
            } else {
                response = "No response from server";
            }
        } catch (IOException e) {
            log.error("Error reading from c server ... {}", e.getMessage());
        }

        return response;
    }

    private SocketAddress getSocketAddress(String socketName) throws IOException {
        if (socketName.startsWith("file:")) {
            // demo only: assume file: URLs are always handled by AFUNIXSocketAddress
            return AFUNIXSocketAddress.of(URI.create(socketName));
        } else if (socketName.contains(":/")) {
            // assume URI, e.g., unix:// or tipc://
            return AFSocketAddress.of(URI.create(socketName));
        }

        int colon = socketName.lastIndexOf(':');
        int slashOrBackslash = Math.max(socketName.lastIndexOf('/'), socketName.lastIndexOf('\\\\'));

        if (socketName.startsWith("@")) {
            // abstract namespace (Linux only!)
            return AFUNIXSocketAddress.inAbstractNamespace(socketName.substring(1));
        } else if (colon > 0 && slashOrBackslash < colon && !socketName.startsWith("/")) {
            // assume TCP socket
            String hostname = socketName.substring(0, colon);
            int port = Integer.parseInt(socketName.substring(colon + 1));
            return new InetSocketAddress(hostname, port);
        } else {
            // assume unix socket file name
            return AFUNIXSocketAddress.of(new File(socketName));
        }
    }
}

실행 로그 관찰

1. POST /connect 요청

2. POST /write 요청

3. GET /read 요청

4. POST /disconnect 요청

 

댓글