본문으로 건너뛰기
홈
기술
기술 전체
프로그래밍68
컴퓨터 과학63
AI48
웹 개발36
인프라33
데이터31
소프트웨어 공학18
소개
← 목록으로프로그래밍 › Java › 입출력과 네트워크

NIO TCP blocking channel 소켓서버 & 클라이언트

목차

NIO TCP blocking channel 소켓 서버와 클라이언트

ServerSocketChannel은 연결을 기다리고, 연결을 수락하면 SocketChannel을 얻는다. 클라이언트는 SocketChannel로 서버 주소에 연결한다. 원래 예제의 read()와 write()에는 ByteBuffer 인자가 필요하며, 한 번의 호출이 메시지 전체를 읽거나 쓴다고 가정하면 안 된다.

sequenceDiagram
    participant C as 클라이언트
    participant S as 서버
    S->>S: bind(127.0.0.1:9190), accept 대기
    C->>S: connect
    C->>S: 길이 4바이트 + UTF-8 본문
    S->>S: 정해진 길이만큼 반복 read
    S-->>C: 길이 4바이트 + 응답 본문

TCP는 바이트 스트림이다. read() 한 번에 문자열 하나가 정확히 도착한다는 경계가 없다. 아래 예제는 본문 앞에 4바이트 길이를 붙여 한 메시지의 범위를 정한다. 설명을 단순하게 하기 위해 서버는 클라이언트 한 명의 요청 한 건만 처리하고 종료한다.

1. 공통 메시지 읽기·쓰기 (FrameIO.java)

import java.io.EOFException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;

public final class FrameIO {
    private static final int MAX_BYTES = 64 * 1024;

    private FrameIO() { }

    public static void writeFrame(SocketChannel channel, String message) throws IOException {
        byte[] body = message.getBytes(StandardCharsets.UTF_8);
        if (body.length > MAX_BYTES) throw new IOException("메시지가 너무 깁니다");
        ByteBuffer frame = ByteBuffer.allocate(4 + body.length);
        frame.putInt(body.length);
        frame.put(body);
        frame.flip(); // 쓰기 모드에서 읽기 모드로 전환
        while (frame.hasRemaining()) channel.write(frame);
    }

    public static String readFrame(SocketChannel channel) throws IOException {
        ByteBuffer header = ByteBuffer.allocate(4);
        readFully(channel, header);
        header.flip();
        int length = header.getInt();
        if (length < 0 || length > MAX_BYTES) {
            throw new IOException("허용되지 않은 메시지 길이: " + length);
        }
        ByteBuffer body = ByteBuffer.allocate(length);
        readFully(channel, body);
        body.flip();
        return StandardCharsets.UTF_8.decode(body).toString();
    }

    private static void readFully(SocketChannel channel, ByteBuffer buffer)
            throws IOException {
        while (buffer.hasRemaining()) {
            if (channel.read(buffer) == -1) {
                throw new EOFException("메시지 도중 연결이 닫혔습니다");
            }
        }
    }
}

flip() 전에는 버퍼에 데이터를 채우고, 이후에는 버퍼에서 읽는다. write()는 남은 바이트가 없어질 때까지 호출하고, read()는 정해진 길이를 모두 받거나 연결 종료(-1)를 확인할 때까지 호출한다. 길이를 검사하지 않고 그대로 메모리를 할당하면 큰 숫자 하나로 서버 메모리를 소진시킬 수 있다. Java ByteBuffer 문서와 SocketChannel 문서에 버퍼와 입출력 동작이 설명돼 있다.

2. 서버 (EchoServer.java)

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

public class EchoServer {
    public static void main(String[] args) throws IOException {
        try (ServerSocketChannel server = ServerSocketChannel.open()) {
            server.configureBlocking(true);
            server.bind(new InetSocketAddress("127.0.0.1", 9190));
            System.out.println("연결 기다리기");
            try (SocketChannel client = server.accept()) {
                String request = FrameIO.readFrame(client);
                System.out.println("받은 메시지: " + request);
                FrameIO.writeFrame(client, "서버 응답: " + request);
            }
        }
    }
}

configureBlocking(true)에서는 연결이 올 때까지 accept()가 기다린다. try-with-resources가 정상 종료와 예외 발생 시 채널을 닫는다. 원래 코드에서는 newCachedThreadPool()을 만들고 사용하지 않았으며, sock.isOpen()이 참이면 첫 연결 뒤 서버를 닫았다. 위 코드는 한 번만 처리한다는 동작을 명시한다. 여러 연결을 처리하려면 반복 수락과 각 연결의 작업 관리가 필요하다. Java ServerSocketChannel 문서를 참고한다.

3. 클라이언트 (EchoClient.java)

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;

public class EchoClient {
    public static void main(String[] args) throws IOException {
        try (SocketChannel channel = SocketChannel.open()) {
            channel.configureBlocking(true);
            channel.connect(new InetSocketAddress("127.0.0.1", 9190));
            FrameIO.writeFrame(channel, "hello world");
            System.out.println(FrameIO.readFrame(channel));
        }
    }
}

원래 클라이언트의 new InetSocketAddress(9190)은 로컬 와일드카드 주소 형태다. 원격 서버에 연결하려면 위처럼 호스트와 포트를 함께 지정한다. 시험할 때는 같은 디렉터리에 세 파일을 저장하고 javac FrameIO.java EchoServer.java EchoClient.java로 컴파일한다. 터미널 하나에서 java EchoServer를 실행한 뒤 다른 터미널에서 java EchoClient를 실행한다. 실제 서비스에는 연결 제한 시간, 오류 처리, 여러 클라이언트 관리가 추가로 필요하다.

같은 카테고리의 글