Search
🌐

HTTP Web Server

1. index.html에 대한 응답

가. 준비단계

1) 요구사항 정리

요청
→ 아래와 같은 URI로 클라이언트의 요청 발생
응답
→ 로컬 저장소의 index.html 파일 읽기
→ index.html 파일을 응답으로 전송

나. 클라이언트의 Request URI에서 파일 정보 추출

1) Request Header 읽기

HTTP Request Header 전체 읽기
try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()) { // InputStream을 line by line으로 변환 BufferedReader br = new BufferedReader(new InputStreamReader(in, "UTF-8")); String line = br.readLine(); // HEADER 부분에 내용이 더이상 없는 경우 if (line == null) { return; } // HEADER의 내용을 한줄씩 읽어서 출력 while (!"".equals(line)) { log.debug("header: {}", line); line = br.readLine(); } } catch (IOException e) { log.error(e.getMessage()); }
Java
복사

2) HEADER에서 원하는 정보 추출하기

HEADER의 Request Line 중 URI에서 index.html 부분 추출하기
BufferedReader br = new BufferedReader(new InputStreamReader(in, "UTF-8")); String line = br.readLine(); if (line == null) { return; } String[] splited = line.split(" "); String path = splited[1];
Java
복사

다. 서버 로컬 저장소의 특정 파일 전송

1) 로컬 저장소의 파일을 byte[]로 읽기

DataOutputStream dos = new DataOutputStream(out); byte[] body = Files.readAllBytes(new File("./webapp" + path).toPath());
Java
복사

2) 파일을 응답으로 전송

response200Header(dos, body.length); responseBody(dos, body);
Java
복사

라. 리팩토링

1) URL 추출 부분에 대해 메소드 분할

// HttpRequestUtils.java public static String getUrl(String line) { String[] splited = line.split(" "); log.debug("request path: {}", splited[1]); return splited[1]; }
Java
복사

2. GET 요청 처리

가. GET 방식의 요청에서 queryString 추출

1) Header의 Request Line에서 URI 추출

if (uri.startsWith("/user/create")) { int index = uri.indexOf("?"); String queryString = uri.substring(index + 1); log.debug("queryString {}", queryString); }
Java
복사

2) 회원가입 필드 정보로 User 객체 생성

Map<String, String> params = HttpRequestUtils.parseQueryString(queryString); User user = new User(params.get("userId"), params.get("password"), params.get("name"), params.get("email")); log.debug("User: {}", user);
Java
복사

3) GET 요청 처리 완료 후 index.html로 이동

if (uri.startsWith("/user/create")) { // 생략 uri = "/index.html"; }
Java
복사

3. POST 요청 처리

가. POST 요청의 Body에서 데이터 추출

1) Request Header 읽고 Content-Length 추출

헤더를 line by line으로 읽기
헤더를 ": " 기준으로 분할해서 String[]에 저장
String[]에서 Content-Length에 해당하는 부분을 Map 자료구조에 추가
String uri = HttpRequestUtils.getUri(line); Map<String, String> headers = new HashMap<>(); while (!line.equals("")) { log.debug("header: {}", line); line = br.readLine(); String[] headerTokens = line.split(": "); if (headerTokens.length == 2) { // contents length 부분 headers.put(headerTokens[0], headerTokens[1]); } }
Java
복사

2) Request Body 읽기 & 파싱

Request Body에 해당하는 부분(Content-Length만큼만) BufferedReader에서 읽어오기
Request Body에서 User의 속성에 해당하는 부분을 파싱
파싱한 값으로 User 객체 생성
if (uri.startsWith("/user/create")) { String requestBody = IOUtils.readData(br, Integer.parseInt(headers.get("Content-Length"))); log.debug("requestBody {}", requestBody); Map<String, String> params = HttpRequestUtils.parseQueryString(requestBody); User user = new User(params.get("userId"), params.get("password"), params.get("name"), params.get("email")); log.debug("User: {}", user); uri = "/index.html"; }
Java
복사

4. 302 status code 적용

가. 개념 정리

1) Redirect 원리

클라이언트의 Request URI에 매칭되어 있는 새로운 URI를 서버에서 response로 전달함. 클라이언트는 reponse에 포함된 새로운 URI로 재요청을 실시함
→ Spring MVC의 'redirect:'도 위와 같은 원리로 동작함

2) Redirect 사용하는 경우

시스템 변화에 대한 Request의 경우 Redirect를 사용
Redirect 적용 X, 회원가입 요청
→ 클라이언트에서 폼 태그에 회원가입 정보를 담아서 회원 가입 요청(POST, user/create) 시도
→ 서버에서 클라이언트 요청 처리 후, 200 ok Response 전달
→ 클라이언트에서 새로고침 클릭
→ 똑같은 회원가입 요청 발생, 클라이언트의 회원가입 요청 Request 객체 그대로 존재
Redirect 적용 O, 회원가입 요청
→ 클라이언트에서 폼 태그에 회원가입 정보를 담아서 회원 가입 요청(POST, user/create) 시도
→ 서버에서 클라이언트 요청 처리 후, 303 SEE OTHER와 함께 새로운 URI를 포함하여 Response 전달
→ 클라이언트에서 Response에 포함된 새로운 URI로 요청 실행
→ 새로운 URI 요청에 대해 서버에서 200 ok 응답을 보냄
→ 클라이언트에서 새로고침을 눌러도, 회원가입 요청에 대한 객체가 사라졌기 때문에 회원가입 요청 발생 X

3) 3xx Status Code 정리

나. Redirect 구현

1) 303 상태를 포함한 Header 작성

303 status code는 Redirect로 GET 방식을 요구하는 것이기 때문에 response에 body 부분이 필요 없음
private void response303Header(DataOutputStream dos) { try { dos.writeBytes("HTTP/1.1 303 SEE OTHER \r\n"); dos.writeBytes("Location: /index.html\r\n"); dos.writeBytes("\r\n"); } catch (IOException e) { log.error(e.getMessage()); } }
Java
복사

2) Header 추가해서 response 생성

if (uri.startsWith("/user/create")) { // 생략 DataOutputStream dos = new DataOutputStream(out); response303Header(dos); } else { DataOutputStream dos = new DataOutputStream(out); byte[] body = Files.readAllBytes(new File("./webapp" + uri).toPath()); response200Header(dos, body.length); responseBody(dos, body); }
Java
복사

5. 로그인 요청 처리

가. 개념 정리

1) Set-Cookie 헤더 필드 활용

최초 Request에 대한 Response에 Set-Cookie 헤더에 logined=true 값을 할당하여 전송
두번째 Request의 Cookie 헤더에 logined=true 값 추출하여 확인

나. 구현

1) POST, '/user/login' 요청의 Message Body 확인

path.startsWith을 path.equals로 변경
→ path.startsWith의 경우 부분적으로 일치하면 true가 되기 때문에 /user/login.html 요청도 true로 인식하게 됨
user/login.html에서 요구하는 필드에 대한 로그를 찍어서 정상적으로 request에 포함되는지 확인
else if (path.equals("/user/login")) { // 생략 Map<String, String> params = HttpRequestUtils.parseQueryString(body); log.debug("userId: {}, password: {}", params.get("userId"), params.get("password")); // 생략 }
Java
복사

2) DB에서 저장된 User 객체의 존재여부 확인

else if (path.equals("/user/login")) { // 생략 User loginUser = DataBase.findUserById(params.get("userId")); log.debug("loginUser: {}", loginUser); if (loginUser == null) { log.debug("User Not Found"); } else if (!loginUser.getPassword().equals(params.get("password"))) { log.debug("Password Mismatch!"); } else { log.debug("Login Success"); } // 생략 }
Java
복사

3) Response의 Cookie 필드에 값 할당

if (loginUser == null) { log.debug("User Not Found"); DataOutputStream dos = new DataOutputStream(out); response302Header(dos, "/index.html"); } else if (!loginUser.getPassword().equals(params.get("password"))) { log.debug("Password Mismatch!"); DataOutputStream dos = new DataOutputStream(out); response302Header(dos, "/index.html"); dos.flush(); } else { log.debug("Login Success"); DataOutputStream dos = new DataOutputStream(out); response302HeaderWithCookie(dos, "/index.html", "logined=true"); dos.flush(); } private void response302HeaderWithCookie(DataOutputStream dos, String location, String cookie) { try { dos.writeBytes("HTTP/1.1 302 FOUND \r\n"); dos.writeBytes("Location: " + location + "\r\n"); dos.writeBytes("Set-Cookie: " + cookie + "\r\n"); dos.writeBytes("\r\n"); } catch (IOException e) { log.error(e.getMessage()); } }
Java
복사

4) browser의 network에서 쿠키 상태 확인

login request에 대한 response의 Headers 탭에서 Set-Cookie Header 안에서 logined=true 확인
index.html 외 나머지 파일의 경우, Cookies 탭 안에서 'logined' Cookie 확인 가능
→ 안보일 경우, 'show filtered out request cookies' 클릭

6. CSS 지원

가. 구현

1) Header 내 content-type에 css 반영

private void response200HeaderWithCss(DataOutputStream dos, int lengthOfBodyContent) { try { dos.writeBytes("HTTP/1.1 200 OK \r\n"); dos.writeBytes("Content-Type: text/css;charset=utf-8\r\n"); dos.writeBytes("Content-Length: " + lengthOfBodyContent + "\r\n"); dos.writeBytes("\r\n"); } catch (IOException e) { log.error(e.getMessage()); } }
Java
복사

2) request 경로 상의 파일이 css인 경우 처리

else if (path.endsWith(".css")) { DataOutputStream dos = new DataOutputStream(out); byte[] body = Files.readAllBytes(new File("./webapp" + path).toPath()); response200HeaderWithCss(dos, body.length); responseBody(dos, body); }
Java
복사

7. 기타

가. 프로젝트 세팅

IntelliJ의 Welcome 화면에서 'Open' X but 'Get from VCS' 사용
→ local에 remote repository를 clone하고 IntelliJ에서 open하는 방식 X.
→ 프로젝트 본 파일(java 코드 및 build 파일) 불러오는데 실패

나. 개념 정리

1) URI vs URL

2) 3xx Status Code

Reference