Java의 New I/O (NIO) 패키지는 고성능 비동기 I/O 작업을 위한 강력한 기능을 제공합니다. 기존의 블로킹 I/O 모델과 달리, NIO는 하나의 스레드가 여러 채널의 I/O 이벤트를 동시에 처리할 수 있게 하여, 높은 확장성과 효율성을 요구하는 네트워크 애플리케이션 개발에 적합합니다. 주요 구성 요소로는 채널(Channel), 버퍼(Buffer), 그리고 셀렉터(Selector)가 있습니다.
- 채널 (Channel): 데이터가 흐르는 통로입니다. 파일, 소켓 등을 표현하며, 읽기/쓰기 작업을 수행할 수 있습니다.
- 버퍼 (Buffer): 데이터를 저장하는 메모리 블록입니다. 채널과의 상호작용을 통해 데이터를 읽거나 씁니다.
- 셀렉터 (Selector): 하나 이상의 채널에서 발생한 I/O 이벤트를 모니터링하고 처리합니다. 비동기 I/O의 핵심 메커니즘을 제공합니다.
이 문서에서는 Java NIO를 사용하여 비동기 이미지 전송 서버와 클라이언트를 구현하는 방법을 예제를 통해 설명합니다.
NIO 이미지 수신 서버 구현
서버는 특정 포트에서 클라이언트 연결을 수락하고, 연결된 클라이언트로부터 전송된 이미지를 수신하여 로컬 파일로 저장합니다. 이미지 수신이 완료되면 클라이언트에게 확인 메시지를 보냅니다.
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Iterator;
import java.util.Set;
public class NioImageReceiver {
private static final int SERVER_PORT = 7777;
private static final String OUTPUT_IMAGE_PATH = "received_image.png";
public static void main(String[] args) throws IOException {
// 1. 서버 소켓 채널 생성 및 비블로킹 모드 설정
ServerSocketChannel serverListener = ServerSocketChannel.open();
serverListener.configureBlocking(false); // 비블로킹 모드
// 2. 포트 바인딩
serverListener.bind(new InetSocketAddress(SERVER_PORT));
System.out.println("서버가 포트 " + SERVER_PORT + "에서 시작되었습니다.");
// 3. 셀렉터 생성
Selector eventSelector = Selector.open();
// 4. 서버 채널을 셀렉터에 등록, 연결 요청(OP_ACCEPT) 이벤트 감지
serverListener.register(eventSelector, SelectionKey.OP_ACCEPT);
// 5. 이벤트 대기 및 처리 루프
while (eventSelector.select() > 0) { // 준비된 이벤트가 있는지 확인
Set<SelectionKey> selectedKeys = eventSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
// 클라이언트 연결 요청 처리
SocketChannel clientSocket = serverListener.accept();
clientSocket.configureBlocking(false); // 클라이언트 소켓도 비블로킹 모드
clientSocket.register(eventSelector, SelectionKey.OP_READ); // 읽기 이벤트 감지 등록
System.out.println("새로운 클라이언트 연결 수락: " + clientSocket.getRemoteAddress());
} else if (key.isReadable()) {
// 클라이언트로부터 데이터 읽기 처리
SocketChannel connectedClient = (SocketChannel) key.channel();
ByteBuffer dataBuffer = ByteBuffer.allocate(1024);
// 이미지를 저장할 파일 채널
FileChannel outputFileChannel = FileChannel.open(
Paths.get(OUTPUT_IMAGE_PATH),
StandardOpenOption.WRITE,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING // 기존 파일이 있으면 덮어쓰기
);
int bytesRead;
try {
while ((bytesRead = connectedClient.read(dataBuffer)) > 0) {
dataBuffer.flip(); // 쓰기 모드에서 읽기 모드로 전환
outputFileChannel.write(dataBuffer);
dataBuffer.clear(); // 읽기 모드에서 쓰기 모드로 전환 준비
}
if (bytesRead == -1) { // 클라이언트가 채널을 닫았을 때
System.out.println("클라이언트 연결 종료: " + connectedClient.getRemoteAddress());
} else {
System.out.println("이미지 수신 완료 및 저장: " + OUTPUT_IMAGE_PATH);
}
// 이미지 수신 완료 후 클라이언트에게 확인 메시지 전송
String confirmationMsg = "서버가 이미지를 성공적으로 수신했습니다.";
ByteBuffer responseBuffer = ByteBuffer.wrap(confirmationMsg.getBytes("UTF-8"));
while (responseBuffer.hasRemaining()) { // 버퍼의 모든 데이터를 쓸 때까지 반복
connectedClient.write(responseBuffer);
}
System.out.println("클라이언트에게 확인 메시지 전송 완료.");
} catch (IOException e) {
System.err.println("클라이언트 통신 중 오류 발생 (" + connectedClient.getRemoteAddress() + "): " + e.getMessage());
} finally {
outputFileChannel.close();
connectedClient.close(); // 작업 완료 후 클라이언트 채널 닫기
System.out.println("클라이언트 채널 닫힘: " + connectedClient.getRemoteAddress());
}
}
keyIterator.remove(); // 처리된 SelectionKey 제거
}
}
serverListener.close();
eventSelector.close();
}
}
NIO 이미지 전송 클라이언트 구현
클라이언트는 서버에 연결하고, 지정된 로컬 이미지를 읽어 서버로 전송합니다. 이미지 전송 후에는 서버로부터의 응답 메시지를 기다려 수신하고 출력합니다.
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Iterator;
import java.util.Set;
public class NioImageSender {
private static final String SERVER_HOST = "127.0.0.1";
private static final int SERVER_PORT = 7777;
// 전송할 이미지 파일 경로를 지정하세요. 이 파일은 반드시 존재해야 합니다.
private static final String SOURCE_IMAGE_PATH = "local_image.png";
public static void main(String[] args) throws IOException {
// 1. 소켓 채널 생성 및 서버 연결 시도
SocketChannel clientSocketChannel = SocketChannel.open();
clientSocketChannel.configureBlocking(false); // 비블로킹 모드 설정
// 2. 셀렉터 생성 및 채널 등록 (연결 이벤트 감지)
Selector eventSelector = Selector.open();
clientSocketChannel.register(eventSelector, SelectionKey.OP_CONNECT);
// 3. 서버 연결 시작
clientSocketChannel.connect(new InetSocketAddress(SERVER_HOST, SERVER_PORT));
System.out.println("서버 " + SERVER_HOST + ":" + SERVER_PORT + "에 연결 시도 중...");
boolean connected = false;
while (!connected) { // 연결 완료 대기
eventSelector.select(); // 연결 이벤트 대기 (블록킹)
Iterator<SelectionKey> keyIterator = eventSelector.selectedKeys().iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isConnectable()) {
if (clientSocketChannel.isConnectionPending()) {
clientSocketChannel.finishConnect(); // 연결 완료 처리
}
System.out.println("서버에 성공적으로 연결되었습니다.");
connected = true;
}
keyIterator.remove(); // 처리된 키는 제거
}
}
// 4. 이미지 파일을 읽을 파일 채널 생성
FileChannel sourceFileChannel = FileChannel.open(Paths.get(SOURCE_IMAGE_PATH), StandardOpenOption.READ);
ByteBuffer transferBuffer = ByteBuffer.allocate(1024);
System.out.println("이미지 파일 '" + SOURCE_IMAGE_PATH + "' 전송 시작.");
// 5. 이미지 데이터를 읽어 서버로 전송
// NIO 채널이 비블로킹 모드라도, 여기서는 FileChannel에서 읽어 SocketChannel로 바로 쓰는 직접적인 루프를 사용합니다.
// 이는 작은 파일 전송 시 간편하며, SocketChannel.write()는 현재 버퍼에 있는 데이터를 가능한 한 많이 쓰고 즉시 반환합니다.
while (sourceFileChannel.read(transferBuffer) != -1) {
transferBuffer.flip(); // 쓰기 모드에서 읽기 모드로 전환
while (transferBuffer.hasRemaining()) { // 버퍼의 모든 데이터를 쓸 때까지 반복
clientSocketChannel.write(transferBuffer); // 채널에 데이터 쓰기
}
transferBuffer.clear(); // 읽기 모드에서 쓰기 모드로 전환 준비
}
System.out.println("이미지 파일 전송 완료.");
sourceFileChannel.close(); // 파일 채널 닫기
// 이미지 전송 후, 서버 응답을 받기 위해 읽기 이벤트만 감지하도록 셀렉터에 재등록
// 이전의 OP_CONNECT(이제 필요 없음) 및 불필요한 OP_WRITE 이벤트를 제거하고 OP_READ만 남깁니다.
clientSocketChannel.register(eventSelector, SelectionKey.OP_READ);
// 6. 서버 응답 대기 및 수신
boolean responseReceived = false;
while (!responseReceived && clientSocketChannel.isOpen()) {
if (eventSelector.select() > 0) { // 응답 이벤트 대기 (블록킹)
Set<SelectionKey> selectedKeys = eventSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isReadable()) {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer incomingBuffer = ByteBuffer.allocate(1024);
int bytesRead = channel.read(incomingBuffer);
if (bytesRead > 0) {
incomingBuffer.flip();
System.out.println("서버 응답: " + new String(incomingBuffer.array(), 0, bytesRead, "UTF-8"));
responseReceived = true; // 응답 수신 완료
} else if (bytesRead == -1) { // 서버가 채널을 닫았을 때
System.out.println("서버가 연결을 닫았습니다.");
responseReceived = true; // 응답은 없지만 연결 종료로 간주
}
}
keyIterator.remove(); // 처리된 키는 제거
}
}
}
// 7. 모든 작업 완료 후 자원 해제
clientSocketChannel.close();
eventSelector.close();
System.out.println("클라이언트 작업 완료 및 자원 해제.");
}
}