public class HttpServerV1 {
private final int port;
public HttpServerV1(int port) {
this.port = port;
}
public void start() throws IOException {
ServerSocket serverSocket = new ServerSocket(port); // localhost를 주지 않는다면 기본적으로 0.0.0.0으로 바인딩 된다.
log("서버 시작 port: " + port);
while (true) {
Socket socket = serverSocket.accept();
process(socket);
}
}
private void process(Socket socket) throws IOException {
try (
socket;
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), UTF_8));
PrintWriter writer = new PrintWriter(socket.getOutputStream(), false, UTF_8)) {
String requestString = requestToString(reader);
if (requestString.contains("/favicon.ico")) {
log("favicon 요청");
return;
}
log("HTTP 요청 정보 출력");
System.out.println(requestString);
log("HTTP 응답 생성중...");
responseToClient(writer);
}
}
private void responseToClient(PrintWriter writer) {
// 웹 브라우저에 전달하는 내용
String body = "<h1>Hello World</h1>";
int length = body.getBytes(UTF_8).length;
StringBuilder sb = new StringBuilder();
sb.append("HTTP/1.1 200 OK\r\n");
sb.append("Content-Type: text/html\r\n");
sb.append("Content-Length: ").append(length).append("\r\n");
sb.append("\r\n");
sb.append(body);
log("HTTP 응답 정보 출력");
sleep();
writer.println(sb);
writer.flush();
}
private static void sleep() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
private static String requestToString(BufferedReader reader) throws IOException {
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
if (line.isEmpty()) {
break;
}
sb.append(line).append("\n");
}
return sb.toString();
}
}
public HttpServerV1(int port) {
this.port = port;
}
public void start() throws IOException {
ServerSocket serverSocket = new ServerSocket(port); // localhost를 주지 않는다면 기본적으로 0.0.0.0으로 바인딩 된다.
log("서버 시작 port: " + port);
while (true) {
Socket socket = serverSocket.accept();
process(socket);
}
}
서버 객체는 port 인자를 받아 생성되며 start() 메서드로 하여금 시작된다.
시작시 요청에 대한 서버 소켓을 만들어줄 ServerSocket객체를 생성한다.
이에 port만 생성자 인자로 부여하게 된다면 기본적으로 모든 네트워크 인터페이스 0.0.0.0에 바인딩된다.
.accept()로 하여금 요청을 기다리고 요청이 도착하면 서버 소켓이 ServerSocket으로 부터 생성된다.
그리고 process로직을 이어나간다.
이 코드를 보면 바로 예측되는 문제점이 나타난다.
ServerSocket이 제공한 Socket의 process작업이 끝날 때 까지 다음 요청은 대기하게 된다.
서버는 현재 한 번에 하나의 요청만 처리할 수 있다.(후에 이를 개선해보자.)
private void process(Socket socket) throws IOException {
try (
socket;
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), UTF_8));
PrintWriter writer = new PrintWriter(socket.getOutputStream(), false, UTF_8)) {
String requestString = requestToString(reader);
if (requestString.contains("/favicon.ico")) {
log("favicon 요청");
return;
}
log("HTTP 요청 정보 출력");
System.out.println(requestString);
log("HTTP 응답 생성중...");
responseToClient(writer);
}
}
streamReader로는 BufferedReader, streamWriter로는 PrintWriter를 선택했다 그 이유는 아래와 같다.
BufferedReader 사용 이유
BufferedReader는 InputStreamReader와 함께 사용되어 네트워크 소켓의 입력 스트림에서 텍스트 데이터를 효율적으로 읽기 위한 것이다.
PrintWriter 사용 이유
PrintWriter는 소켓의 출력 스트림에 텍스트 데이터를 출력할 때 편리하게 사용할 수 있는 클래스이다.
PrintWriter의 autoFlush 인자 false 사용 이유
PrintWriter의 생성자에서 두 번째 인자로 전달하는 autoFlush 옵션은, true로 설정하면 println() 호출 시 자동으로 flush()가 호출된다.
요청을 requestToString으로 하여금 String으로 파싱한다.
private static String requestToString(BufferedReader reader) throws IOException {
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
if (line.isEmpty()) {
break;
}
sb.append(line).append("\n");
}
return sb.toString();
}
HTTP 스펙은 \r\n으로 하여금 줄바꿈을 나타내기에 이를 한줄한줄 읽기 위해 위와 같은 코드를 구성했다.
줄바꿈에 대해 IntelliJ console에 예쁘게 나타내기 위해 \n을 다시 주어 스트링 빌더 객체를 완성해서 리턴한다.
private void responseToClient(PrintWriter writer) {
// 웹 브라우저에 전달하는 내용
String body = "<h1>Hello World</h1>";
int length = body.getBytes(UTF_8).length;
StringBuilder sb = new StringBuilder();
sb.append("HTTP/1.1 200 OK\r\n");
sb.append("Content-Type: text/html\r\n");
sb.append("Content-Length: ").append(length).append("\r\n");
sb.append("\r\n");
sb.append(body);
log("HTTP 응답 정보 출력");
sleep();
writer.println(sb);
writer.flush();
}
버전1 구현에서 가장 중요한 부분이다. 기존에 스프링부트를 사용하며 Tomcat이 해주던 일을 직접느껴볼 수 있다.

HTTP 스펙에 맞게 Response Line을 구성하고 Response Headers를 구현하고 한줄 공백을 주고 Body를 작성해야 한다.
이 스펙에 맞게 브라우저는 응답라인과 응답헤더를 읽고 헤더에 나타난 Content-Length만큼의 body를 읽어낸다.