티스토리 뷰
HTTP 요청 메시지와 응답 메시지를 객체로 만들어 보자
HTTP 요청 메시지
public class HttpRequest {
private String method;
private String path;
private final Map<String, String> queryParameters = new HashMap<>();
private final Map<String, String> headers = new HashMap<>();
public HttpRequest(BufferedReader reader) throws IOException {
parseRequestLine(reader);
parseHeaders(reader);
// 메시지 바디는 이후에 처리
}
//GET /search?q=hello HTTP/1.1
//HOST: localhost:12345
private void parseRequestLine(BufferedReader reader) throws IOException {
String requestLine = reader.readLine();
if (requestLine == null) {
throw new IOException("EOF : No request line received");
}
String[] parts = requestLine.split(" ");
if (parts.length != 3 ) {
throw new IOException("Invalid request line : " + requestLine);
}
method = parts[0];
String[] pathParts = parts[1].split("\\?");
path = pathParts[0];
if (pathParts.length >1) {
parseQueryParameters(pathParts[1]);
}
}
private void parseQueryParameters(String queryString) {
for (String param : queryString.split("&")) {
String[] keyValue = param.split("=");
String key = URLDecoder.decode(keyValue[0], StandardCharsets.UTF_8);
String value = keyValue.length > 1 ? URLDecoder.decode(keyValue[1], StandardCharsets.UTF_8) : "";
queryParameters.put(key, value);
}
}
private void parseHeaders(BufferedReader reader) throws IOException {
String line;
while (!(line = reader.readLine()).isEmpty()) {
String[] headerParts = line.split(":");
headers.put(headerParts[0].trim(), headerParts[1].trim());
}
}
public String getMethod() {
return method;
}
public String getPath() {
return path;
}
public String getParameter(String name) {
return queryParameters.get(name);
}
public String getHeader(String name) {
return headers.get(name);
}
@Override
public String toString() {
return "HttpRequest{" +
"method='" + method + '\'' +
", path='" + path + '\'' +
", queryParameters=" + queryParameters +
", headers=" + headers +
'}';
}
}
HTTP 요청 메시지는 다음과 같다.
시작 라인을 통해 method, path, queryParameters를 구할 수 있다.
- method : GET
- path : /search
- queryParameters : [q=hello]
query, header의 경우 key=value 형식이기 때문에 Map을 사용하면 이후에 편리하게 데이터를 조회할 수 있다.
만약 다음과 같은 내용이 있다면 queryParameters의 Map에 저장되는 내용은 다음과 같다.
- /search?q=hello&type=text
- queryParameters : [q=hello, type=text]
퍼센트 디코딩도 URLDecoder.decode()를 사용해서 처리한 다음에 Map에 보관한다. 따라서 HttpRequest 객체를 사용하는 쪽에서는 퍼센트 디코딩을 고민하지 않아도 된다.
HTTP 명세에서 헤더가 끝나는 부분을 빈 라인으로 구분한다.
- while (!(line = reader.readLine()).isEmpty()
이렇게 하면 시작 라인의 다양한 정보와 헤더를 객체로 구조화 할 수 있다.
HTTP 응답 메시지
public class HttpResponse {
private final PrintWriter writer;
private int statusCode = 200;
private final StringBuilder bodyBuilder = new StringBuilder();
private String contentType = "text/html; charset=UTF-8";
public HttpResponse(PrintWriter writer) {
this.writer = writer;
}
public void setStatusCode(int statusCode) {
this.statusCode = statusCode;
}
public void setContentType(String contentType) {
this.contentType = contentType;
}
public void writeBody(String body) {
bodyBuilder.append(body);
}
public void flush() {
int contentLength = bodyBuilder.toString().getBytes(StandardCharsets.UTF_8).length;
writer.println("HTTP/1.1 " + statusCode + " " + "OK");
writer.println("Content-Type: " + contentType);
writer.println("Content-Length: " + contentLength);
writer.println();
writer.println(bodyBuilder);
writer.flush();
}
private String getReasonPhrase(int statusCode) {
switch (statusCode) {
case 200:
return "OK";
case 404:
return "Not Found";
case 500:
return "Internal Server Error";
default:
return "Unknown status";
}
}
}
HTTP 응답 메시지는 다음과 같다.
시작 라인
- HTTP 버전 : HTTP/1.1
- 응답 코드 : 200
- 응답 코드의 간단한 설명 : OK
응답 헤더
- Content-Type : HTTP 메시지 바디에 들어있는 내용의 종류
- Content-Length : HTTP 메시지 바디의 길이
public class HttpRequestHandlerV4 implements Runnable{
private final Socket socket;
public HttpRequestHandlerV4(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
process();
} catch (Exception e) {
log(e);
}
}
private void process() throws IOException {
try (socket; BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
PrintWriter writer = new PrintWriter(socket.getOutputStream(),false,StandardCharsets.UTF_8)) {
HttpRequest request = new HttpRequest(reader);
HttpResponse response = new HttpResponse(writer);
if (request.getPath().equals("/favicon.ico")) {
log("favicon 요청");
return;
}
log("HTTP 요청 정보 출력");
System.out.println(request);
log("HTTP 응답 생성중 ...");
if (request.getPath().equals("/site1")) {
site1(response);
} else if (request.getPath().equals("/site2")) {
site2(response);
} else if (request.getPath().equals("/search")) {
search(request,response);
} else if (request.getPath().equals("/")) {
home(response);
} else {
notFound(response);
}
response.flush();
log("HTTP 응답 전달 완료");
} catch (Exception e) {
log(e);
}
}
private void notFound(HttpResponse response) {
response.setStatusCode(404);
response.writeBody("<h1>404 페이지를 찾을 수 없습니다. </h1>");
}
private void home(HttpResponse response) {
// 원칙적으로 Content-Length를 계산해서 전달해야 하지만, 단순하게 하기위해 제외
response.writeBody("<h1>home</h1>");
response.writeBody("<ul>");
response.writeBody("<li><a href='/site1'>site1</a></li>");
response.writeBody("<li><a href='/site2'>site2</a></li>");
response.writeBody("<li><a href='/search?q=hello'>검색</a></li>");
response.writeBody("</ul>");
}
// "/search?q = hello
private void search(HttpRequest request, HttpResponse response) {
String query = request.getParameter("q");
response.writeBody("<h1>Search</h1>");
response.writeBody("<ul>");
response.writeBody("<li>query: " + query + "</li>");
response.writeBody("</ul>");
}
private void site2(HttpResponse response) {
response.writeBody("<h1>Site2</h1>");
}
private void site1(HttpResponse response) {
response.writeBody("<h1>Site1</h1>");
}
private void responseToClient(PrintWriter writer) {
// 웹 브라우저에 전달하는 내용
String body = "<h1>Hello World </h1>";
int length = body.getBytes(StandardCharsets.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 응답 정보 출력");
System.out.println(sb);
writer.println(sb);
writer.flush();
}
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();
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
- 클라이언트의 요청이 오면 요청 정보를 기반으로 HttpRequest 객체를 와 HttpResponse 객체를 만든다.
- HttpRequest를 통해서 필요한 정보를 편리하게 찾을 수 있다.
- /search의 경우 퍼센트 디코딩을 고민하지 않아도 된다. 이미 HttpRequest에서 다 처리해두었다.
- 응답의 경우 HttpResponse를 사용하고, HTTP 메시지 바디에 출력할 부분만 적어주면 된다. 나머지는 HttpResponse 객체가 대신 처리해준다.
- response.flush()는 꼭 호출해주어야 한다. 그래야 실제 응답이 클라이언트에 전달된다.
public class HttpServerV4 {
private final ExecutorService es = Executors.newFixedThreadPool(10);
private final int port;
public HttpServerV4(int port) {
this.port = port;
}
public void start() throws IOException {
ServerSocket serverSocket = new ServerSocket(port);
log("서버 시작 port: " + port);
while (true) {
Socket socket = serverSocket.accept();
es.submit(new HttpRequestHandlerV4(socket));
}
}
}
public class ServerMainV4 {
private static final int PORT = 12345;
public static void main(String[] args) throws IOException {
HttpServerV4 server = new HttpServerV4(PORT);
server.start();
}
}
HttpRequest, HttpResponse 객체가 HTTP 요청과 응답을 구조화한 덕분에 많은 중복을 제거하고, 또 코드도 매우 효과적으로 리팩토링 할 수 있었다.
지금 까지 학습한 내용을 생각해보면 전체적인 코드가 크게 2가지로 분류 되는 것을 확인할 수 있다.
- HTTP 서버와 관련된 부분 ( HttpServer, HttpRequestHandler, HttpRequest, HttpResponse)
- 서비스 개발을 위한 로직 ( home(), site1(), site2(), search(), notFound()
만약 웹을 통한 회원 관리 프로그램 같은 서비스를 새로 만들어야 한다면, 기존 코드에서 HTTP 서버와 관련된 부분은 거의 재사용하고, 서비스 개발을 위한 로직만 추가하면 된다.
'Java' 카테고리의 다른 글
Java - 애노테이션 (0) | 2024.11.26 |
---|---|
Java - 리플렉션 (0) | 2024.11.25 |
Java - URL 인코딩 (0) | 2024.11.25 |
Java - HTTP 서버 만들기 1 (1) | 2024.11.18 |
Java - 클라이언트,서버 프로그램 (0) | 2024.11.14 |