Skip to content

Commit

Permalink
[톰캣 구현하기 - 3, 4단계] 땡칠(박성철) 미션 제출합니다. (#471)
Browse files Browse the repository at this point in the history
* refactor(Http11Processor): HTTP Version 관리를 Response로 위임

* refactor(HttpRequest): Request Line을 객체로 분리

* feat: GET, POST를 담당하는 핸들러

* feat(Connector): Thread Pool 적용

* feat(SessionManager): 발생할 수 있는 동시성 문제를 해결한다

* refactor: Handler 인터페이스를 request, response 로 수정한다

* refactor: 쿠키, 세션 조회에 Optional 사용

* refactor(Headers): 메서드 변수명 수정

* refactor(HttpResponse): 바디 길이 0일때, Content-Length 헤더 제외

* refactor: HttpResponse가 응답을 생성한다

* refactor: Handler를 Controller로 재명명

* refactor: 컨트롤러 매핑 RequestMapping으로 분리, TargetPath 객체 분리

* fix: 실패하는 테스트 보완

* refactor: RequestMapping 상수화

* refactor: 핸들러 목록의 자료형을 Set으로 변경

* typo(HttpResponse): 변수명 headers2 -> headers
  • Loading branch information
0chil authored Sep 14, 2023
1 parent 180b899 commit 00422ae
Show file tree
Hide file tree
Showing 25 changed files with 577 additions and 309 deletions.
23 changes: 23 additions & 0 deletions tomcat/src/main/java/nextstep/jwp/handler/HelloController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package nextstep.jwp.handler;

import org.apache.coyote.http11.HttpRequest;
import org.apache.coyote.http11.HttpResponse;
import org.apache.coyote.http11.TargetPath;
import org.apache.coyote.http11.controller.Controller;
import org.apache.coyote.http11.header.ContentType;

public class HelloController implements Controller {

private static final TargetPath SUPPORTED_PATH = new TargetPath("/");

@Override
public boolean supports(HttpRequest httpRequest) {
return httpRequest.getTarget().getPath().equals(SUPPORTED_PATH) && httpRequest.getMethod().isGet();
}

@Override
public void handle(HttpRequest httpRequest, HttpResponse httpResponse) {
httpResponse.setContentType(new ContentType("text/html"));
httpResponse.setBody("Hello world!");
}
}
78 changes: 78 additions & 0 deletions tomcat/src/main/java/nextstep/jwp/handler/LoginController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package nextstep.jwp.handler;

import nextstep.jwp.db.InMemoryUserRepository;
import nextstep.jwp.model.User;
import org.apache.coyote.http11.HttpRequest;
import org.apache.coyote.http11.HttpResponse;
import org.apache.coyote.http11.TargetPath;
import org.apache.coyote.http11.body.FormData;
import org.apache.coyote.http11.controller.FileController;
import org.apache.coyote.http11.controller.GetAndPostController;
import org.apache.coyote.http11.header.Cookies;
import org.apache.coyote.http11.session.Session;
import org.apache.coyote.http11.session.SessionManager;

import java.util.Optional;

public class LoginController extends GetAndPostController {

private static final String SESSION_KEY = "JSESSIONID";
private static final String SESSION_USER_KEY = "user";

private static final String UNAUTHORIZED_LOCATION = "/401";
private static final String MAIN_LOCATION = "/index";

private static final TargetPath SUPPORTED_PATH = new TargetPath("/login").autoComplete();

private final FileController fileHandler = new FileController();

@Override
public boolean supports(HttpRequest httpRequest) {
if (httpRequest.getMethod().isGet() || httpRequest.getMethod().isPost()) {
return httpRequest.getTarget().getPath().autoComplete().equals(SUPPORTED_PATH);
}
return false;
}

@Override
protected void doGet(final HttpRequest httpRequest, final HttpResponse httpResponse) {
boolean isSignedIn = httpRequest.getSession(SESSION_KEY)
.map(session -> session.hasAttribute(SESSION_USER_KEY))
.isPresent();
if (isSignedIn) {
httpResponse.redirectTo(MAIN_LOCATION);
}
fileHandler.handle(httpRequest, httpResponse);
}

@Override
protected void doPost(final HttpRequest httpRequest, final HttpResponse httpResponse) {
FormData formData = FormData.of(httpRequest.getBody());
String account = formData.get("account");
String password = formData.get("password");

Optional<User> found = InMemoryUserRepository.findByAccount(account);
if (found.isPresent()) {
User user = found.get();
signIn(httpResponse, user, password);
return;
}
httpResponse.redirectTo(UNAUTHORIZED_LOCATION);
}

private void signIn(final HttpResponse httpResponse, final User user, final String password) {
if (user.checkPassword(password)) {
Session session = new Session();
session.addAttribute(SESSION_USER_KEY, user);
SessionManager.add(session);

Cookies cookies = new Cookies();
cookies.add(SESSION_KEY, session.getId());

httpResponse.redirectTo(MAIN_LOCATION);
httpResponse.setCookies(cookies);
return;
}
httpResponse.redirectTo(UNAUTHORIZED_LOCATION);
}
}
69 changes: 0 additions & 69 deletions tomcat/src/main/java/nextstep/jwp/handler/LoginHandler.java

This file was deleted.

43 changes: 43 additions & 0 deletions tomcat/src/main/java/nextstep/jwp/handler/RegisterController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package nextstep.jwp.handler;

import nextstep.jwp.db.InMemoryUserRepository;
import nextstep.jwp.model.User;
import org.apache.coyote.http11.HttpRequest;
import org.apache.coyote.http11.HttpResponse;
import org.apache.coyote.http11.TargetPath;
import org.apache.coyote.http11.body.FormData;
import org.apache.coyote.http11.controller.FileController;
import org.apache.coyote.http11.controller.GetAndPostController;

public class RegisterController extends GetAndPostController {

private static final String MAIN_LOCATION = "/index";
private static final TargetPath SUPPORTED_PATH = new TargetPath("/register").autoComplete();

private final FileController fileHandler = new FileController();

@Override
public boolean supports(HttpRequest httpRequest) {
if (httpRequest.getMethod().isGet() || httpRequest.getMethod().isPost()) {
return httpRequest.getTarget().getPath().autoComplete().equals(SUPPORTED_PATH);
}
return false;
}

@Override
protected void doGet(final HttpRequest httpRequest, final HttpResponse httpResponse) {
fileHandler.handle(httpRequest, httpResponse);
}

@Override
protected void doPost(final HttpRequest httpRequest, final HttpResponse httpResponse) {
FormData formData = FormData.of(httpRequest.getBody());

String account = formData.get("account");
String password = formData.get("password");
String email = formData.get("email");

InMemoryUserRepository.save(new User(account, password, email));
httpResponse.redirectTo(MAIN_LOCATION);
}
}
29 changes: 0 additions & 29 deletions tomcat/src/main/java/nextstep/jwp/handler/RegisterHandler.java

This file was deleted.

26 changes: 17 additions & 9 deletions tomcat/src/main/java/org/apache/catalina/connector/Connector.java
Original file line number Diff line number Diff line change
@@ -1,31 +1,38 @@
package org.apache.catalina.connector;

import java.net.SocketException;
import org.apache.coyote.http11.Http11Processor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.apache.coyote.http11.Http11Processor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Connector implements Runnable {

private static final Logger log = LoggerFactory.getLogger(Connector.class);

private static final int DEFAULT_PORT = 8080;
private static final int DEFAULT_ACCEPT_COUNT = 100;
private static final int DEFAULT_ACCEPT_COUNT = 100; // 모든 쓰레드가 사용중일 때 들어오는 연결에 대한 대기열의 길이
private static final int MAX_THREADS = 250; // 동시에 연결 후 처리 가능한 요청의 최대 개수
// 최대 ThradPool의 크기는 250, 모든 Thread가 사용 중인(Busy) 상태이면 100명까지 대기 상태로 만들려면 어떻게 할까?
// acceptCount를 100으로 한다
// 처리중인 연결이 250개 이므로 나머지는 OS 큐에서 대기하게 된다.
private static final int SOCKET_TIMEOUT_SECONDS = 10;

private final ServerSocket serverSocket;
private boolean stopped;
private final ExecutorService pool;

public Connector() {
this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT);
this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT, MAX_THREADS);
}

public Connector(final int port, final int acceptCount) {
public Connector(final int port, final int acceptCount, final int maxThreads) {
this.pool = Executors.newFixedThreadPool(maxThreads);
this.serverSocket = createServerSocket(port, acceptCount);
this.stopped = false;
}
Expand Down Expand Up @@ -60,6 +67,7 @@ private void connect() {
try {
process(serverSocket.accept());
} catch (IOException e) {
pool.shutdown();
log.error(e.getMessage(), e);
}
}
Expand All @@ -70,7 +78,7 @@ private void process(final Socket connection) throws SocketException {
}
connection.setSoTimeout(SOCKET_TIMEOUT_SECONDS * 1000);
var processor = new Http11Processor(connection);
new Thread(processor).start();
pool.execute(processor);
}

public void stop() {
Expand Down
63 changes: 20 additions & 43 deletions tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java
Original file line number Diff line number Diff line change
@@ -1,38 +1,31 @@
package org.apache.coyote.http11;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
import java.util.Map;
import java.util.stream.Collectors;
import nextstep.jwp.exception.UncheckedServletException;
import nextstep.jwp.handler.LoginHandler;
import nextstep.jwp.handler.RegisterHandler;
import nextstep.jwp.handler.HelloController;
import nextstep.jwp.handler.LoginController;
import nextstep.jwp.handler.RegisterController;
import org.apache.coyote.Processor;
import org.apache.coyote.http11.handler.FileHandler;
import org.apache.coyote.http11.handler.Handler;
import org.apache.coyote.http11.header.HttpHeader;
import org.apache.coyote.http11.controller.FileController;
import org.apache.coyote.http11.controller.RequestMapping;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
import java.util.Set;

public class Http11Processor implements Runnable, Processor {

private static final Logger log = LoggerFactory.getLogger(Http11Processor.class);

private static final String HTTP_VERSION = "HTTP/1.1 ";
private static final String CRLF = "\r\n";

private static final Handler DEFAULT_HANDLER = new FileHandler();
private static final LoginHandler LOGIN_HANDLER = new LoginHandler();
private static final RegisterHandler REGISTER_HANDLER = new RegisterHandler();
private static final Map<String, Handler> PREDEFINED_HANDLERS = Map.of(
"/", httpRequest -> new HttpResponse("Hello world!", "text/html"),
"/login", LOGIN_HANDLER,
"/login.html", LOGIN_HANDLER,
"/register", REGISTER_HANDLER,
"/register.html", REGISTER_HANDLER
);
private static final RequestMapping REQUEST_MAPPING = new RequestMapping(Set.of(
new HelloController(),
new LoginController(),
new FileController(),
new RegisterController()
));

private final Socket connection;

Expand All @@ -52,29 +45,13 @@ public void process(final Socket connection) {
final var outputStream = connection.getOutputStream()) {

HttpRequest httpRequest = HttpRequest.from(bufferedReader);
HttpResponse httpResponse = handle(httpRequest);

String headerLines = httpResponse.getHeaders().stream()
.map(HttpHeader::toLine)
.collect(Collectors.joining(CRLF));
final var response = String.join(CRLF,
HTTP_VERSION + httpResponse.getStatus().toLine(),
headerLines,
"",
httpResponse.getBody()
);
HttpResponse httpResponse = new HttpResponse();
REQUEST_MAPPING.handle(httpRequest, httpResponse);

outputStream.write(response.getBytes());
outputStream.write(httpResponse.toLine().getBytes());
outputStream.flush();
} catch (IOException | UncheckedServletException e) {
log.error(e.getMessage(), e);
}
}

private HttpResponse handle(final HttpRequest httpRequest) {
if (PREDEFINED_HANDLERS.containsKey(httpRequest.getTarget().getPath())) {
return PREDEFINED_HANDLERS.get(httpRequest.getTarget().getPath()).handle(httpRequest);
}
return DEFAULT_HANDLER.handle(httpRequest);
}
}
Loading

0 comments on commit 00422ae

Please sign in to comment.