Skip to content

Commit

Permalink
[톰캣 구현하기 - 3, 4단계] 민트(유재민) 미션 제출합니다. (#462)
Browse files Browse the repository at this point in the history
  • Loading branch information
yujamint authored Sep 12, 2023
1 parent dbfb437 commit d0b0986
Show file tree
Hide file tree
Showing 31 changed files with 764 additions and 256 deletions.
2 changes: 1 addition & 1 deletion study/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ handlebars:
server:
tomcat:
accept-count: 1
max-connections: 1
max-connections: 2
threads:
max: 2
2 changes: 1 addition & 1 deletion study/src/test/java/thread/stage0/SynchronizationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ private static final class SynchronizedMethods {

private int sum = 0;

public void calculate() {
public synchronized void calculate() {
setSum(getSum() + 1);
}

Expand Down
10 changes: 6 additions & 4 deletions study/src/test/java/thread/stage0/ThreadPoolsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,24 +29,26 @@ void testNewFixedThreadPool() {
executor.submit(logWithSleep("hello fixed thread pools"));
executor.submit(logWithSleep("hello fixed thread pools"));
executor.submit(logWithSleep("hello fixed thread pools"));
executor.submit(logWithSleep("hello fixed thread pools"));
executor.submit(logWithSleep("hello fixed thread pools"));

// 올바른 값으로 바꿔서 테스트를 통과시키자.
final int expectedPoolSize = 0;
final int expectedQueueSize = 0;
final int expectedPoolSize = 2;
final int expectedQueueSize = 3;

assertThat(expectedPoolSize).isEqualTo(executor.getPoolSize());
assertThat(expectedQueueSize).isEqualTo(executor.getQueue().size());
}

@Test
void testNewCachedThreadPool() {
void testNewCachedThreadPool() throws InterruptedException {
final var executor = (ThreadPoolExecutor) Executors.newCachedThreadPool();
executor.submit(logWithSleep("hello cached thread pools"));
executor.submit(logWithSleep("hello cached thread pools"));
executor.submit(logWithSleep("hello cached thread pools"));

// 올바른 값으로 바꿔서 테스트를 통과시키자.
final int expectedPoolSize = 0;
final int expectedPoolSize = 3;
final int expectedQueueSize = 0;

assertThat(expectedPoolSize).isEqualTo(executor.getPoolSize());
Expand Down
4 changes: 3 additions & 1 deletion study/src/test/java/thread/stage1/ConcurrencyTest.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package thread.stage1;

import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
Expand All @@ -17,7 +18,8 @@
*/
class ConcurrencyTest {

@Test
// @Test
@RepeatedTest(2000)
void test() throws InterruptedException {
final var userServlet = new UserServlet();

Expand Down
2 changes: 1 addition & 1 deletion study/src/test/java/thread/stage1/UserServlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public void service(final User user) {
join(user);
}

private void join(final User user) {
private synchronized void join(final User user) {
if (!users.contains(user)) {
users.add(user);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,5 @@ public static Optional<User> findByAccount(String account) {
return Optional.ofNullable(database.get(account));
}

public static boolean existsByAccount(String account) {
return database.containsKey(account);
}

private InMemoryUserRepository() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,27 @@
import java.io.UncheckedIOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

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_MAX_THREADS = 200;

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

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

public Connector(final int port, final int acceptCount) {
public Connector(final int port, final int acceptCount, final int maxThreads) {
executorService = Executors.newFixedThreadPool(maxThreads);
this.serverSocket = createServerSocket(port, acceptCount);
this.stopped = false;
}
Expand Down Expand Up @@ -67,7 +72,7 @@ private void process(final Socket connection) {
return;
}
var processor = new Http11Processor(connection);
new Thread(processor).start();
executorService.execute(processor);
}

public void stop() {
Expand Down
218 changes: 26 additions & 192 deletions tomcat/src/main/java/org/apache/coyote/http11/Http11Processor.java
Original file line number Diff line number Diff line change
@@ -1,37 +1,36 @@
package org.apache.coyote.http11;

import nextstep.jwp.db.InMemoryUserRepository;
import nextstep.jwp.exception.UncheckedServletException;
import nextstep.jwp.model.User;
import org.apache.coyote.Processor;
import org.apache.coyote.http11.common.FileReader;
import org.apache.coyote.http11.controller.AuthController;
import org.apache.coyote.http11.controller.Controller;
import org.apache.coyote.http11.controller.DefaultController;
import org.apache.coyote.http11.controller.RegisterController;
import org.apache.coyote.http11.controller.RequestMapping;
import org.apache.coyote.http11.request.HttpRequest;
import org.apache.coyote.http11.response.HttpResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UncheckedIOException;
import java.net.Socket;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.apache.coyote.http11.common.HttpHeaderType.CONTENT_LENGTH;
import static org.apache.coyote.http11.common.HttpHeaderType.CONTENT_TYPE;

public class Http11Processor implements Runnable, Processor {

private static final Logger log = LoggerFactory.getLogger(Http11Processor.class);
private static final String SAFARI_CHROME_ACCEPT_HEADER_DEFAULT_VALUE = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8";

private final Socket connection;
private final SessionManager sessionManager = new SessionManager();
private final HttpRequestReader requestReader = new HttpRequestReader();
private final RequestMapping requestMapping = new RequestMapping(
new AuthController(),
new RegisterController(),
new DefaultController()
);

public Http11Processor(final Socket connection) {
this.connection = connection;
Expand All @@ -51,186 +50,21 @@ public void process(final Socket connection) {
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);

List<String> headers = new ArrayList<>();

String header = "";
while (!(header = bufferedReader.readLine()).equals("")) {
headers.add(header);
}
String[] splitStatusLine = Objects.requireNonNull(headers.get(0)).split(" ");
String requestMethod = splitStatusLine[0];
String requestUri = splitStatusLine[1];

String requestAcceptHeader = findHeader("Accept", headers);
String contentTypeHeader = getContentTypeHeaderFrom(requestAcceptHeader);

RequestHandler requestHandler;
if (requestMethod.equalsIgnoreCase("POST")) {
int contentLength = getContentLength(headers);
String requestBody = readRequestBody(bufferedReader, contentLength);
requestHandler = handlePostRequest(requestUri, requestBody);
} else {
String cookieHeader = findHeader("Cookie", headers);
requestHandler = handleGetRequest(requestMethod, requestUri, cookieHeader);
}
HttpRequest httpRequest = requestReader.readHttpRequest(bufferedReader);
HttpResponse httpResponse = new HttpResponse(httpRequest.httpVersion());

String responseBody = readFile(requestHandler.getResponseFilePath());
Controller controller = requestMapping.getController(httpRequest);
controller.service(httpRequest, httpResponse);

List<String> responseHeaders = new ArrayList<>();
responseHeaders.add("HTTP/1.1 " + requestHandler.getHttpStatus() + " ");
responseHeaders.add(contentTypeHeader);
responseHeaders.add("Content-Length: " + responseBody.getBytes().length + " ");
for (Entry<String, String> headerEntry : requestHandler.getHeaders().entrySet()) {
responseHeaders.add(headerEntry.getKey() + ": " + headerEntry.getValue());
}
String responseHeader = String.join("\r\n", responseHeaders);
httpResponse.setBody(FileReader.readFile(httpResponse.getResponseFileName()));
httpResponse.addHeader(CONTENT_LENGTH.getValue(), String.valueOf(httpResponse.getBody().getBytes().length));
httpResponse.addHeader(CONTENT_TYPE.getValue(), httpRequest.getContentTypeByAcceptHeader());

BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream);

var response = String.join("\r\n", responseHeader, "", responseBody);

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

private static String findHeader(String key, List<String> headers) {
return headers.stream()
.filter(it -> it.startsWith(key + ": "))
.findFirst()
.orElse("Accept: " + SAFARI_CHROME_ACCEPT_HEADER_DEFAULT_VALUE);
}

private static String getContentTypeHeaderFrom(String requestAcceptHeader) {
String[] splitAcceptHeader = requestAcceptHeader.split(" ");
String headerValue = splitAcceptHeader[1];
String[] acceptTypes = headerValue.split(";");
String[] splitAcceptTypes = acceptTypes[0].split(",");

if (Arrays.asList(splitAcceptTypes).contains("text/css")) {
return "Content-Type: text/css;charset=utf-8 ";
}
return "Content-Type: text/html;charset=utf-8 ";
}

private int getContentLength(List<String> headers) {
Optional<String> contentLengthHeader = headers.stream()
.filter(it -> it.startsWith("Content-Length"))
.findFirst();

if (contentLengthHeader.isEmpty()) {
return -1;
}
int index = contentLengthHeader.get().indexOf(" ");
return Integer.parseInt(contentLengthHeader.get().substring(index + 1));
}

private static String readRequestBody(BufferedReader bufferedReader, int contentLength) throws IOException {
char[] buffer = new char[contentLength];
bufferedReader.read(buffer, 0, contentLength);
return new String(buffer);
}

private RequestHandler handlePostRequest(String requestUri, String requestBody) {
String[] splitRequestBody = requestBody.split("&");
if (requestUri.equals("/login")) {
return handleLoginRequest(splitRequestBody);
}
if (requestUri.equals("/register")) {
return handleRegisterRequest(splitRequestBody);
}
return RequestHandler.of("GET", "404 Not Found", "static/404.html");
}

private RequestHandler handleLoginRequest(String[] splitQueryString) {
Optional<String> account = getValueOf("account", splitQueryString);
Optional<String> password = getValueOf("password", splitQueryString);

if (account.isEmpty() || password.isEmpty()) {
return RequestHandler.of("GET", "400 Bad Request", "static/401.html");
}

Optional<User> findUser = InMemoryUserRepository.findByAccount(account.get());
if (findUser.isPresent() && findUser.get().checkPassword(password.get())) {
User user = findUser.get();
log.info(user.toString());
Session session = new Session(UUID.randomUUID().toString());
session.setAttribute("user", user);
sessionManager.add(session);
RequestHandler requestHandler = RequestHandler.of("GET", "302 Found", "static/index.html");
requestHandler.addHeader("Set-Cookie", "JSESSIONID=" + session.getId());
return requestHandler;
}
return RequestHandler.of("GET", "401 Unauthorized", "static/401.html");
}

private Optional<String> getValueOf(String key, String[] splitQueryString) {
return Arrays.stream(splitQueryString)
.filter(it -> equalsKey(key, it))
.map(it -> it.substring(it.indexOf("=") + 1))
.findFirst();
}

private boolean equalsKey(String expected, String actual) {
String[] splitActual = actual.split("=");
return splitActual[0].equals(expected);
}

private RequestHandler handleRegisterRequest(String[] splitQueryString) {
Optional<String> account = getValueOf("account", splitQueryString);
Optional<String> email = getValueOf("email", splitQueryString);
Optional<String> password = getValueOf("password", splitQueryString);

if (account.isEmpty() || email.isEmpty() || password.isEmpty()) {
return RequestHandler.of("GET", "400 Bad Request", "static/register.html");
}

InMemoryUserRepository.save(new User(account.get(), password.get(), email.get()));
return RequestHandler.of("GET", "302 Found", "static/index.html");
}

private RequestHandler handleGetRequest(String requestMethod, String requestUri, String cookie) throws IOException {
if (!requestMethod.equalsIgnoreCase("GET")) {
throw new IllegalArgumentException("GET 요청만 처리 가능합니다.");
}

if (requestUri.equals("/login.html") || requestUri.equals("/login")) {
return handleLoginPageRequest(cookie);
}

String fileName = "static" + requestUri;
return RequestHandler.of("GET", "200 OK", fileName);
}

private RequestHandler handleLoginPageRequest(String cookie) throws IOException {
Optional<String> sessionId = getSessionId(cookie);
if (sessionId.isEmpty()) {
return RequestHandler.of("GET", "200 OK", "static/login.html");
}
Session session = sessionManager.findSession(sessionId.get());
User user = getUser(session);
if (InMemoryUserRepository.existsByAccount(user.getAccount())) {
return RequestHandler.of("GET", "302 Found", "static/index.html");
}
return RequestHandler.of("GET", "200 OK", "static/login.html");
}

private Optional<String> getSessionId(String cookieHeader) {
String[] splitCookie = cookieHeader.split(" ");
return getValueOf("JSESSIONID", splitCookie);
}

private User getUser(Session session) {
return (User) session.getAttribute("user");
}

private String readFile(String filePath) {
try (Stream<String> lines = Files.lines(Paths.get(filePath))) {
return lines.collect(Collectors.joining("\n", "", "\n"));
} catch (IOException | UncheckedIOException e) {
return "Hello world!";
}
}
}
Loading

0 comments on commit d0b0986

Please sign in to comment.