BioHttpProtocolHandler.java

package sprout.server.builtins;

import sprout.mvc.dispatcher.RequestDispatcher;
import sprout.mvc.http.HttpRequest;
import sprout.mvc.http.HttpResponse;
import sprout.mvc.http.ResponseEntity;
import sprout.mvc.http.parser.HttpRequestParser;
import sprout.server.AcceptableProtocolHandler;
import sprout.server.RequestExecutorService;

import java.io.*;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Map;

import static sprout.server.HttpUtils.readRawRequest;


public class BioHttpProtocolHandler implements AcceptableProtocolHandler {
    private final RequestDispatcher dispatcher;
    private final HttpRequestParser parser;
    private final RequestExecutorService requestExecutorService;

    public BioHttpProtocolHandler(RequestDispatcher dispatcher, HttpRequestParser parser, RequestExecutorService requestExecutorService) {
        this.dispatcher = dispatcher;
        this.parser = parser;
        this.requestExecutorService = requestExecutorService;
    }

    @Override
    public void accept(SocketChannel channel, Selector selector,  ByteBuffer initialBuffer) throws Exception {
        detachFromSelector(channel, selector);
        channel.configureBlocking(true);
        Socket socket = channel.socket();

        requestExecutorService.execute(() -> {
            try (InputStream in = socket.getInputStream();
                 OutputStream out = socket.getOutputStream()) {

                ByteBuffer currentBuffer = initialBuffer;

                // HTTP/1.1 keep-alive 처리: 같은 연결에서 여러 요청을 순차 처리
                while (!socket.isClosed()) {
                    String raw = readRawRequest(currentBuffer, in);

                    // 요청이 없거나 연결이 끊긴 경우
                    if (raw.isBlank()) break;

                    HttpRequest<?> req = parser.parse(raw);
                    HttpResponse res = new HttpResponse();

                    dispatcher.dispatch(req, res);

                    // Connection 헤더 확인
                    String connectionHeader = req.getHeaders().getOrDefault("Connection", "keep-alive");
                    boolean shouldClose = "close".equalsIgnoreCase(connectionHeader);

                    writeResponse(out, res.getResponseEntity(), shouldClose);

                    // Content-Length가 없거나 Connection: close 요청이면 종료
                    if (shouldClose) {
                        break;
                    }

                    // 다음 요청을 위해 버퍼 초기화
                    currentBuffer = null;
                }

            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    socket.close();
                } catch (IOException ignored) {}
            }
        });
    }

    @Override
    public boolean supports(String protocol) {
        return "HTTP/1.1".equals(protocol);
    }

    private void detachFromSelector(SocketChannel ch, Selector sel) {
        SelectionKey k = ch.keyFor(sel);
        if (k != null) {
            k.cancel();
            k.attach(null);
        }
    }

    private void writeResponse(OutputStream out, ResponseEntity<?> res, boolean shouldClose) throws IOException {
        if (res == null) return;

        // Body를 바이트로 변환 (UTF-8)
        byte[] bodyBytes = res.getBody() != null
            ? res.getBody().toString().getBytes(StandardCharsets.UTF_8)
            : new byte[0];

        // HTTP 헤더 작성
        StringBuilder header = new StringBuilder();
        header.append("HTTP/1.1 ")
              .append(res.getStatusCode().getCode())
              .append(" ")
              .append(res.getStatusCode().getMessage())
              .append("\r\n");

        // Content-Type
        header.append("Content-Type: ")
              .append(res.getContentType())
              .append("\r\n");

        // Content-Length (바이트 단위로 정확히)
        header.append("Content-Length: ")
              .append(bodyBytes.length)
              .append("\r\n");

        // Connection 헤더: keep-alive 활성화 (HTTP/1.1 기본)
        if (shouldClose) {
            header.append("Connection: close\r\n");
        } else {
            header.append("Connection: keep-alive\r\n");
            header.append("Keep-Alive: timeout=5, max=1000\r\n");
        }

        // Custom headers
        if (res.getHeaders() != null) {
            for (Map.Entry<String, String> entry : res.getHeaders().entrySet()) {
                header.append(entry.getKey())
                      .append(": ")
                      .append(entry.getValue())
                      .append("\r\n");
            }
        }

        // 헤더 끝
        header.append("\r\n");

        // 헤더 + 바디를 바이트 단위로 전송
        out.write(header.toString().getBytes(StandardCharsets.UTF_8));
        out.write(bodyBytes);
        out.flush();
    }


}