클라이언트로부터 받은 메시지를 HTTP 형식에 맞는 Request , Response 객체에 담아 파싱하기
우선 HTTP 메시지 형식에 대해 간단히 알아보면,
아래와 같이 Start Line / Header / Body(선택) 로 되어있다.
클라이언트로 부터 TCP 소켓을 사용하여 받아온 메시지를 읽어와 HTTP 형식으로 파싱하기위해
실제 HTTP 메시지를 살펴보자.
- Start Line은 공백(" ")을 기준으로 구분되며 HttpMethod / URL / HTTP protocol 로 나뉘어져있다.
- header는 key : value 형식으로 있으며 start line 다음 줄부터 빈줄(blank line)까지 다양한 header들이 있다.
- body는 headers의 빈줄(blank line)이후 나오는 값으로 선택에 따라 없을 수도있으며, header의 content-length를 통해 body값의 길이를 알수있다.
위의 구분을 토대로 Http 메시지를 구분하여 파싱할수있는 Request 객체를 만들어보자.
public class Request {
private static final Logger logger = LoggerUtil.getLogger(Request.class);
private HttpMethod method;
private String url;
private String httpVersion;
private HashMap<String, String> header;
private String body;
private HashMap<String,String> params;
public String getParameter(String name) {
return params.get(name);
}
public String toString() {
return this.method+","+this.url+","+this.httpVersion+","+this.body;
}
//Getter,Setter 생략
}
public enum HttpMethod {
GET,POST,PUT,DELETE;
}
Http 메시지의 기본적인 요소들을 멤버로 가지고있는 Request 클래스이다.
Header값들은 해시맵형태로 가진다.
우리가 일반적으로 tomcat을 통해 서블릿으로 사용하는 request객체는 HttpServletRequest객체로, 아래와 같은 상속관계를 가진다.
클라이언트단에서 <form> 태그를 통해 <input name="param"> 값을 가지고
url을 타고 넘어오는 쿼리스트링을 읽어 parameter 값을 파싱하여 request.getParemeter("param") 값을 추출해내는 과정을 바로 HttpServletRequest가 제공해주는 것이다.
이러한 기능들을 위 Request에서 모두 해주기위해 Hashmap<String,String> 타입의 params를 넣어주었고, 쿼리스트링을 읽어 파라미터값을 파싱하는 메소드도 직접 구현해볼것이다.
HTTP 메시지 parse 메소드
// ClientSocket으로 부터 받은 HTTP 메시지 파싱
public void parse(Socket clientSocket) {
BufferedReader in;
try {
in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
String message = in.readLine(); // 클라이언트로부터 메시지 읽기
logger.info("Client로 부터 받은 message : {}",message);
String[] firstHeader = message.split(" ");
this.method = HttpMethod.valueOf(firstHeader[0]);
String[] mappingUrl = firstHeader[1].split("\\?");
this.url = mappingUrl[0];
parseQueryString(firstHeader[1]); // param parse
this.httpVersion = firstHeader[2];
this.header = new HashMap<>();
String headerLine;
int contentLength = 0;
while ((headerLine = in.readLine()) != null && !headerLine.isEmpty()) {
logger.info("Header: {}", headerLine);
String[] part = headerLine.split(":");
header.put(part[0].trim(), part[1].trim());
}
if(header.containsKey("content-length")) {
contentLength = Integer.parseInt(header.get("content-length"));
char[] body = new char[contentLength];
in.read(body, 0, contentLength);
String bodyStr = new String(body);
header.put("body", bodyStr);
this.body = bodyStr;
logger.info("Body Message : {}",header.get("body"));
}
logger.info("요청 Request : {}",toString());
logger.info("Thread 이름 : {}",Thread.currentThread());
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
parse()는 clientSocket을 매개변수로 받아 InputStream을 통해 값을 읽어들인다.
여기서 주의할점은 HTTP Protocol의 공백(" ")과 빈줄에 따라 나뉘어지는 HTTP 요소들을 구분해주는 것이다.
readline() 메소드는 한 줄을 읽는다. 따라서 맨 처음 inputStream이 읽는 첫줄은 HTTP 메시지의 StartLine이므로,
이에따라 HttpMethod / url / Http version을 파싱한다.
그리고 다음 줄부터 공백줄까지는 Headers 영역이므로 while구문을 사용해 HashMap<String,String> header에 파싱하여 저장해준다.
이때, 만약 Body가 존재한다면 header값에 content-length key가 있을것이므로 조건문을 통해 Body값이 존재할경우
body값또한 파싱해준다.
Body영역은 byte값을 String으로 인코딩하여 얻어낸다.
request.getParameter("name") 구현하기
public void parseQueryString(String url) {
params = new HashMap<>();
// 쿼리 스트링 추출 정규식
String regex = "\\?([^#]*)";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(url);
if (matcher.find()) {
String queryString = matcher.group(1);
String[] pairs = queryString.split("&");
// 각 쿼리 파라미터를 HashMap에 추가
for (String pair : pairs) {
String[] keyValue = pair.split("=");
String key = keyValue[0];
String value = keyValue.length > 1 ? keyValue[1] : ""; // 값이 없을 경우 빈 문자열
params.put(key, value);
}
}
}
예를들어, 쿼리스트링을 통해 넘어오는 url값이 localhost:8080/home?name=tom&age=21 이라면
url중 파라미터값은 ?부터 시작하여 &로 구분되는 key=value값일것이다.
이러한 구분을 정규식 regex를 통해 구분하여 얻어낸 쿼리스트링의 파라미터 key=value값을 Hashmap<String,String> params에 파싱해주면, getParameter(param.get("key")) 를 통해 얻어낼수있다.
Response 객체 만들기
HTTP 응답메시지는 위와같이 생겼다.
Response 객체는 간단하게 필요한부분들을 클래스로 만들어준다.
public class Response {
private int status;
private String statusMsg;
private HashMap<String, String> header;
private String body;
// getter,setter 생략
}
RequestHandler 리팩토링
public class RequestHandler implements Runnable {
private static final Logger logger = LoggerUtil.getLogger(RequestHandler.class);
private final Socket clientSocket;
private static MappingServlet servletHandler;
public RequestHandler(Socket socket) {
this.clientSocket = socket;
}
public void run() {
Request request = new Request();
request.parse(clientSocket);
}
}
이전편에 만들어두었던 RequestHandler 클래스는 Connector이 소켓을 생성하고 클라이언트로 부터 요청이 들어오면,
ThreadPool에서 새로운 스레드를 생성하여 RequestHandler 객체에 ClientSocket을 담아 넘겨주었다.
이 RequestHandler의 실행부인 run() 메소드에서 Request객체를 생성해준뒤, parse() 메소드를 통해 해당 요청을 파싱해주면 된다.
이제, Request와 Response 객체에 HTTP 메시지를 담아 파싱했으니 해당 요청을 읽고 처리해줄 Servlet을 구현해보자!
'Category > Project' 카테고리의 다른 글
JAVA TCP 소켓을 사용하여 HTTP통신이 가능한 WAS 직접 구현하기 -4 (1) | 2024.09.26 |
---|---|
JAVA TCP 소켓을 사용하여 HTTP통신이 가능한 WAS 직접 구현하기 -2 (0) | 2024.09.19 |
JAVA TCP 소켓을 사용하여 HTTP통신이 가능한 WAS 직접 구현하기 -1 (0) | 2024.09.12 |
[개인프로젝트] 15일차 - 배열 탐색 (0) | 2024.03.02 |
[개인프로젝트] 13일차 - 제이쿼리,자바스크립트 (0) | 2024.02.28 |