자바 NIO는 세가지 주요 클래스를 사용합니다.
간단하게 알아보고 채팅 서버와 클라이언트 애플리케이션을 만들어보죠.
1. Buffer, channel
Buffer
특정 원시(primitive) 타입 데이터의 컨테이너입니다. 원시 타입의 데이터들을 많이 담을 수 있는 통이라고 생각하면 편합니다. 한 곳에서 다른 장소로 데이터를 보내기 위해 사용되는 일시적인 공간이며, 채널을 위한 읽거나 기록된 정보를 보유합니다. Buffer에 대해 자세히 알려면 capacity, limit, postion에 대해 알고 있어야 합니다.
- capacity : 버퍼의 크기. 버퍼의 크기 만큼 데이터 요소를 담을 수 있다.
- position : 다음 읽거나 써야할 인덱스 위치. 그림1에 의하면 min까지는 데이터를 읽었으니 position은 그 다음 데이터를 읽을 위치인 s를 가리키고 있다.
- limit : 데이터가 읽히거나 쓰여지면 안될 첫 인덱스. 그림1을 예시로 들면, k까지 쓰여졌으니 그 다음 인덱스인 6을 대상으로는 데이터가 읽히면 안된다.
Channel
애플리케이션 간의 데이터 흐름을 나타냅니다. 개념적으로 버퍼와 채널은 함께 동작해 데이터를 처리합니다. 다음 그림처럼 데이터는 버퍼 및 채널 간에 어느 방향으로도 이동할 수 있습니다.
2. 채팅 서버 & 클라이언트
아래는 간단한 서버, 클라이언트 코드입니다. 서버는 ServerSocketChannel을 이용하여 소켓 요청을 수신하며 생성된 소켓을 SocketChannel 객체로 반환합니다. 사용자가 문자열을 입력하면 이를 SocketChannel을 이용하여 클라이언트에 데이터를 보냅니다(HelperMethods.sendMessage). 이후에는 클라이언트에서 응답 요청이 오기 전까지 block 됩니다(HelperMethods.receive에서 처리합니다). 채널과 버퍼의 구체적인 동작 방식(HelperMethods의 코드)은 그 다음 장에서에 설명할 예정입니다. 이번 장에서는 전체적인 코드가 어떤 흐름으로 진행되는지 이해하면 좋을 것 같습니다. 특히 주석을 보며 이해하면 훨씬 이해하기 쉬우실겁니다.
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Scanner;
public class ChatServer {
public ChatServer() {
System.out.println("Chat server started");
try {
// ServerSocketChannel : 소켓 요청을 수신합니다.
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 5000번 포트에서 소켓 요청을 수신합니다.
serverSocketChannel.bind(new InetSocketAddress(5000));
boolean running = true;
while (running) {
System.out.println("Waiting for request ...");
// SocketChannel : 스트리밍 소켓을 지원합니다.
// accept에서 blocking됩니다. 클라이언트의 연결 요청이 있을 경우에 스트리밍 소켓을 반환합니다.
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("Connected to Client");
String message;
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.print("> ");
message = scanner.nextLine();
if (message.equalsIgnoreCase("quit")) {
// 여기서 버퍼를 이용하여 데이터를 보냅니다.
HelperMethods.sendMessage(socketChannel, "Server terminating");
running = false;
break;
} else {
// 여기서 버퍼를 이용하여 데이터를 읽어옵니다.
HelperMethods.sendMessage(socketChannel, message);
System.out.println("Waiting for message from client ...");
System.out.println("Message: " + HelperMethods.receive(socketChannel));
}
}
}
} catch (IOException e) {
}
}
public static void main(String[] args) {
new ChatServer();
}
}
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.util.Scanner;
public class ChatClient {
public ChatClient() {
// 127.0.0.1의 5000번 포트 서버를 대상으로 스트리밍 소켓 연결을 시도합니다.
try (SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 5000))) {
System.out.println("Connected to Chat server");
String message;
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("Waiting for message from the server ...");
// 여기서 버퍼를 이용하여 응답을 받아옵니다.
System.out.println("Message: " + HelperMethods.receive(socketChannel));
System.out.print("> ");
message = scanner.nextLine();
if (message.equalsIgnoreCase("quit")) {
// 여기서 버퍼를 이용하여 데이터를 전송합니다.
HelperMethods.sendMessage(socketChannel, "Client terminating");
break;
}
HelperMethods.sendMessage(socketChannel, message);
}
} catch (IOException e) {
}
}
public static void main(String[] args) {
new ChatClient();
}
}
3. 채널과 버퍼를 이용하여 Data send & receive
아래 코드를 바탕하여 각 코드 라인에서 버퍼가 어떤 방식으로 동작하는지 알아보겠습니다.
public static void sendMessage(SocketChannel socketChannel, String message) {
ByteBuffer byteBuffer = ByteBuffer.allocate(message.length() + 1);
byteBuffer.put(message.getBytes());
byteBuffer.put((byte) 0x00);
byteBuffer.flip();
try {
while (byteBuffer.hasRemaining()) {
socketChannel.write(byteBuffer);
}
System.out.println("Sent: " + message);
} catch (IOException e) {
e.printStackTrace();
}
}
message에 "minsuk"을 넣었다고 가정해보자
ByteBuffer byteBuffer = ByteBuffer.allocate(message.length() + 1);
byteBuffer.put(message.getBytes());
byteBuffer.put((byte) 0x00);
byteBuffer.flip();
byteBuffer.hasRemaining()
hasRemaining 메소드는 읽을 데이터가 더 있는지 물어보는 역할을 합니다. limit은 데이터가 읽히거나 쓰여지면 안될 첫 인덱스이므로 그 전까지 데이터를 읽었는지 확인하는 거죠. true를 반환하면 더 읽을 데이터가 있다는 뜻입니다. 그림5를 보면 아직 읽을 데이터가 많죠?
socketChannel.write(byteBuffer);
write 함수를 실행하면 버퍼에 있는 내용이 채널을 통해 클라이언트로 전달될겁니다. 이때 클라이언트의 버퍼 크기, 네트워크 상황 등 여러 요인으로 인해 한번에 데이터가 모두 안보내질 수 있습니다. 그림6은 "min"만 보내졌을 경우에 대한 예시입니다.
아래는 receive 코드입니다. socketChannel.read(byteBuffer) 코드는 채널을 통해 읽어온 데이터를 byteBuffer에 채우는 역할을 합니다. 데이터를 채널을 통해 버퍼로 읽어왔으니 position == limit == capacity가 돼있을 겁니다. 이에 그 이후 byteBuffer.flip()를 호출하여 postion을 0으로 리셋합니다. 이후 각 코드가 실행될 때 마다 어떻게 Buffer가 채워지는지 생각해보는 것은 독자들에게 맡기도록 하겠습니다:)
public static String receive(SocketChannel socketChannel) {
try {
String message = "";
ByteBuffer byteBuffer = ByteBuffer.allocate(16);
while (socketChannel.read(byteBuffer) > 0) {
char b = 0x00;
byteBuffer.flip();
while (byteBuffer.hasRemaining()) {
b = (char) byteBuffer.get();
if (b == 0x00) {
break;
}
message += (char) b;
}
if (b == 0x00) {
break;
}
byteBuffer.clear();
}
return message;
} catch (IOException e) {
e.printStackTrace();
}
return "";
}
'Computer launguage > Java' 카테고리의 다른 글
Java Semaphore 동작 원리 deep dive (2) | 2024.12.31 |
---|---|
제한된 메모리 크기 환경에서의 LinkedList와 HashMap 튜닝 방법 (0) | 2022.07.03 |
[Deep dive] 부동 소수점, 고정 소수점 표현 방법? 연산 속도? 오차? 돈 계산? (0) | 2022.03.26 |
[Deep Dive] Garbage Collector(GC) 구조? 동작 과정? SE7, 8 차이? (0) | 2022.03.11 |
[Deep dive] JVM 구조? 자바 애플리케이션 실행 과정? 컴파일 과정? (2) | 2022.03.05 |