HTTP 에 대해서 간단히 알아보고 직접 구현해보자.
웹의 핵심 요소인 HTTP는 웹 개발자에게 있어서 필수적으로 알아야 하는 지식이다. 그러나 상당수의 개발자들이 HTTP가 어떠한 원리로 동작하는지 제대로 알지 못하고 있다. 이번 과정을 통해 HTTP 통신을 처리하는 웹 서버가 의외로 간단한 원리로 구현되어 있다는 걸 알게 될 것이다. 직접 간단한 웹 서버를 구현해보면서, HTTP를 제대로 이해하고 동시에 Spring Web MVC가 어떤 편의를 제공하는지 절실하게 느껴보자.
HTTP Server 를 가볍게 구현해보자.
세팅은 HTTP Client 에서 했던 것과 동일하게 해준다.
- gradle init
- AppTest.java 내용을 날린다.
3.설정 > tools
에서 on Save 맨 위에 2개를 체크 표시로 바꿔준다.구성 편집
에서 app:run 을 Run Server 로 잡는다.이는 프로젝트마다 초기화 되기 때문에 매번 해줘야 한다.
HTTP 서버는 1, 2, 4, 5 단계를 처리하면 된다.
1단계: 서버는 접속 요청을 받기 위한 소켓을 연다. → Listen
3단계: 서버는 접속 요청을 받아서 클라이언트와 통신할 소켓을 따로 만든다. → Accept
4단계: 소켓을 통해 서로 데이터를 주고 받는다. → Send & Receive ⇒ 반복!
5단계: 통신을 마치면 소켓을 닫는다. → Close ⇒ 상대방은 Receive로 인지할 수 있다.
이는 다른 곳에 접속하는 게 아니기 때문에 포트 번호만 정하면 된다. ('이 포트 번호로 요청을 줘') 만약 80 포트를 사용 중이라면 8080 등 다른 포트 번호를 쓰면 된다.
int port = 8080;
그리고 Java에서는 Socket 을 여는데 ServerSocket이라는 별도의 클래스를 사용한다.
ServerSocket 은 그냥 Socket 과 다르게 Send, Recive 를 안 할 것이기 때문에 별로의 Server Socket 을 만들어준 것이다. 그래서 Socket을 상속한 게 아니라, 완전히 구별된다는 점을 주의해야 한다.
(내부를 보면 Socket 을 상속받지 않고 바로 java.io.Closeable 을 상속받는다.)
backlog 는 요청이 여러 번 들어올 때 얼마나 대기를 세울 지이다.
한 줄로 Listen 처리가 된다.
ServerSocket listener = new ServerSocket(port, backlog); // backlog 의 기본은 50이다.
ServerSocket listener = new ServerSocket(port, 0);
서버는 클라이언트와 다르게 Listen 되고 있는 상태에서 Accept 를 여러 번 할 수 있다.
Accpt 는 클라이언트의 접속을 기다린다. 클라이언트가 접속하면 통신용 소켓을 따로 만들어서 돌려준다.
Socket socket = listener.accept();
연결을 하면 한 번 받아주고 바로 끊기게 된다. 그리고 기다리게 된다.
I/O에서 이렇게 기다리는 걸 Blocking이라고 한다. 파일 읽기, 쓰기 등도 모두 Blocking 동작이지만, TCP 통신에서는 네트워크 상태 같은 요인에 의해 크게 지연될 수 있고, accept처럼 상대방의 요청이 없으면 영원히 기다리는 (Blocking) 일이 벌어질 수 있다. 그래서 멀티스레드나 비동기, 이벤트 기반 처리 등이 필요하다. (이에 대해 지금은 다루지 않겠다.)
서버 입장에서 Request 는 요청을 하는 게 아니라, 요청을 받는 것이다. 따라서 먼저 읽어야 한다.
코드는 클라이언트에서 다룬 것과 100% 동일하다.
InputStream inputStream = socket.getInputStream();
Reader reader = new InputStreamReader(inputStream);
CharBuffer charBuffer = CharBuffer.allocate(1_000_000);
reader.read(charBuffer);
charBuffer.flip();
String text = charBuffer.toString();
System.out.println(text);
클라이언트의 요청과 마찬가지로, 응답 메시지를 만들어서 전송하면 된다.
HTTP/1.1 200 OK
(빈 줄)
Hello, world!
Java 코드
String message = """
HTTP/1.1 200 OK
Hello, world!
""";
또는
String message = "" +
"HTTP/1.1 200 OK\n" +
"\n" +
"Hello, world!\n";
⚠️ 마지막 줄에 Newline(\n)을 넣는 걸 잊지 말자.
제대로 하려면 Content-Type과 Content-Length를 더해주는 게 좋다.
String body = "Hello, world!";
byte[] bytes = body.getBytes();
String message = "" +
"HTTP/1.1 200 OK\n" +
"Content-Type: text/html; charset=UTF-8\n" +
"Content-Length: " + bytes.length + "\n" +
"\n" +
body;
⚠️ Content-Length로 정확한 크기를 알 수 있기 때문에 마지막 줄에 Newline(\n)을 넣지 않아도 된다.
전송 코드는 클라이언트와 100% 동일하다.
마찬가지로 클라이언트와 100% 동일하다.
package server;
import java.io.*;
import java.net.*;
import java.nio.*;
public class App {
public static void main(String[] args) throws IOException {
App app = new App();
app.run();
}
private void run() throws IOException {
// 1. Listen
int port = 8080;
ServerSocket listener = new ServerSocket(port, 0);
System.out.println("Listen!");
while (true) {
// 2. Accept
Socket socket = listener.accept();
System.out.println("Accept!");
// 3. Request
InputStream inputStream = socket.getInputStream();
Reader reader = new InputStreamReader(inputStream);
CharBuffer charBuffer = CharBuffer.allocate(1_000_000);
reader.read(charBuffer);
charBuffer.flip();
String text = charBuffer.toString();
System.out.println(text);
// 4. Response
String body = "Hello, world!";
byte[] bytes = body.getBytes();
String message = "" +
"HTTP/1.1 200 OK\n" +
"Content-Type: text/html; charset=UTF-8\n" +
"Content-Length: " + bytes.length + "\n" +
"\n" +
body;
Writer writer = new OutputStreamWriter(socket.getOutputStream());
writer.write(message);
writer.flush();
// 5. Close
socket.close();
}
}
}
HTTP 서버가 1, 2, 4, 5 단계를 어떻게 처리하는지 좀 감을 잡은 것 같다.
Blocking 같은 부분도 편하게 처리할 수 있는 준비된 것들에 대해 알아보자.