지금까지 배운 것을 바탕으로 서버를 만들고, 차례로 성능을 높여보자.
우선 간단한 기본 서버이다.
@Slf4j
public class JavaIOServer {
@SneakyThrows
public static void main(String[] args) {
log.info("start");
try(ServerSocket serverSocket = new ServerSocket()){
serverSocket.bind(new InetSocketAddress("localhost", 8080));
while(true){
Socket clientSocket = serverSocket.accept();
byte[] requestBytes = new byte[1024];
InputStream inputStream = clientSocket.getInputStream();
inputStream.read(requestBytes);
log.info("request: {}", new String(requestBytes).trim());
OutputStream outputStream = clientSocket.getOutputStream();
String response = "I am Server";
outputStream.write(response.getBytes());
outputStream.flush();
}
}
}
}
간단한 서버이다.
while(true)를 돌며 계속 응답을 받고, 받은 후에 클라이언트에게 I am Server라는 값을 준다.
클라이언트이다.
요청을 보내고, 응답을 받은 후 종료된다.
@Slf4j
public class JavaIOClient {
public static void main(String[] args) {
try(Socket socket = new Socket()){
socket.connect(new InetSocketAddress("localhost", 8080));
var outputStream = socket.getOutputStream();
String requestBody = "I am Seungkyu client";
outputStream.write(requestBody.getBytes());
outputStream.flush();
InputStream inputStream = socket.getInputStream();
byte[] responseBytes = new byte[1024];
inputStream.read(responseBytes);
log.info("result: {}", new String(responseBytes).trim());
}catch (IOException e){
throw new RuntimeException(e);
}
}
}

이런 식으로 서버에 계속 요청이 오는 것을 볼 수 있다.
이제 서버를 NIO로 변경해보자.
@Slf4j
public class JavaNIOBlockingServer {
@SneakyThrows
public static void main(String[] args) {
log.info("start");
try(ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) {
serverSocketChannel.bind(new InetSocketAddress("localhost", 8080));
while(true){
SocketChannel socketChannel = serverSocketChannel.accept();
ByteBuffer requestByteBuffer = ByteBuffer.allocateDirect(1024);
socketChannel.read(requestByteBuffer);
requestByteBuffer.flip();
String requestBody = StandardCharsets.UTF_8.decode(requestByteBuffer).toString();
log.info("request: {}", requestBody);
ByteBuffer responseByteBuffer = ByteBuffer.wrap("I am Server".getBytes());
socketChannel.write(responseByteBuffer);
socketChannel.close();
}
}
}
}
Channel을 이용하여 데이터를 읽고 쓰며, 버퍼를 사용하는 것을 볼 수 있다.
이렇게 속도를 높이겠지만, 아직도 accept에서 Blocking이 발생하고 있다.
그렇기에 이거를 Non-Blocking으로 바꿔보자
@Slf4j
public class JavaNIONonBlockingServer {
@SneakyThrows
public static void main(String[] args) {
log.info("start");
try(ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) {
serverSocketChannel.bind(new InetSocketAddress("localhost", 8080));
serverSocketChannel.configureBlocking(false);
while(true){
SocketChannel socketChannel = serverSocketChannel.accept();
if(socketChannel == null){
Thread.sleep(100);
continue;
}
ByteBuffer requestByteBuffer = ByteBuffer.allocateDirect(1024);
while(socketChannel.read(requestByteBuffer) == 0){
log.info("reading");
}
socketChannel.read(requestByteBuffer);
requestByteBuffer.flip();
String requestBody = StandardCharsets.UTF_8.decode(requestByteBuffer).toString();
log.info("request: {}", requestBody);
ByteBuffer responseByteBuffer = ByteBuffer.wrap("I am Server".getBytes());
socketChannel.write(responseByteBuffer);
socketChannel.close();
}
}
}
}
configureBlocking에 false를 주어 Non-Blocking으로 동작하게 했다.
하지만 socketChannel이 null 일 때 Thread.sleep을 하기에 좋은 방법은 아닌 것 같다.
또한 requestBuffer에도 데이터가 있는지를 계속 체크 해주어야 하기 때문에, 이 부분도 수정이 필요해보인다.
이거를 1000개의 요청으로 얼마나 걸리는지 테스트를 해보도록 하자.
테스트 용도의 코드는 다음과 같다.
@Slf4j
public class JavaIOMultiClient {
private static ExecutorService executorService = Executors.newFixedThreadPool(50);
@SneakyThrows
public static void main(String[] args) {
log.info("start main");
List<CompletableFuture<Void>> futures = new ArrayList<>();
long start = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
var future = CompletableFuture.runAsync(() -> {
try (Socket socket = new Socket()) {
socket.connect(new InetSocketAddress("localhost", 8080));
OutputStream out = socket.getOutputStream();
String requestBody = "This is client";
out.write(requestBody.getBytes());
out.flush();
InputStream in = socket.getInputStream();
byte[] responseBytes = new byte[1024];
in.read(responseBytes);
log.info("result: {}", new String(responseBytes).trim());
} catch (Exception e) {}
}, executorService);
futures.add(future);
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
executorService.shutdown();
log.info("end main");
long end = System.currentTimeMillis();
log.info("duration: {}", (end - start) / 1000.0);
}
}
50개의 쓰레드로 1000개의 요청을 한다.
이렇게 실행해서 시간을 측정해보니, 약 12초가 나왔다.

여기서 코드를 수정해서 속도를 높일 수는 없을까?
서버에서 처리하는 코드를 CompletableFuture를 이용해 별개의 쓰레드에서 처리하도록 했다.
@Slf4j
public class JavaNIONonBlockingServer {
@SneakyThrows
public static void main(String[] args) {
log.info("start main");
try (ServerSocketChannel serverSocket = ServerSocketChannel.open()) {
serverSocket.bind(new InetSocketAddress("localhost", 8080));
serverSocket.configureBlocking(false);
while (true) {
SocketChannel clientSocket = serverSocket.accept();
if (clientSocket == null) {
Thread.sleep(100);
continue;
}
CompletableFuture.runAsync(() -> {
handleRequest(clientSocket);
});
}
}
}
@SneakyThrows
private static void handleRequest(SocketChannel clientSocket) {
ByteBuffer requestByteBuffer = ByteBuffer.allocateDirect(1024);
while (clientSocket.read(requestByteBuffer) == 0) {
log.info("Reading...");
}
requestByteBuffer.flip();
String requestBody = StandardCharsets.UTF_8.decode(requestByteBuffer).toString();
log.info("request: {}", requestBody);
Thread.sleep(10);
ByteBuffer responeByteBuffer = ByteBuffer.wrap("This is server".getBytes());
clientSocket.write(responeByteBuffer);
clientSocket.close();
}
}
이렇게 서버를 실행해보니 시간이 많이 준 것을 볼 수 있었다.

'백엔드 > 리액티브 프로그래밍' 카테고리의 다른 글
Java Selector (1) | 2024.03.20 |
---|---|
Java AIO (0) | 2024.03.19 |
Java NIO (0) | 2024.03.18 |
JAVA IO (0) | 2024.03.18 |
Mutiny (0) | 2024.03.14 |