구현 | 요청 읽기

제어구조

파싱 및 유효성 검사 과정에 대해서는 따로 설명하지 않습니다. 한 가지만 꼽아 말씀을 드린다면, 요청 메시지의 각 줄 끝을 "\r\n"이 아니라 "\n"으로 다룬다면 파싱할 때 문제가 생길 수 있습니다. 그 외에는 리퀘스트를 읽는 과정에서 발생할 수 있는 에러의 수가 굉장히 많기 때문에, 다양한 상태 코드에 대해 학습하면서 어떤 에러가 발생할 수 있는지 고민해보시기를 바랍니다.

리퀘스트를 읽는 과정에서 문제가 있다면 에러 상태 코드(int)를 throw하는데, 이 경우 해당 상태 코드로 바로 응답을 생성합니다. 문제가 없는 경우에만 URL과 method를 참고하여 요청을 처리하고 거기에 맞는 응답을 생성하는 것이죠.

리퀘스트를 읽는 과정에서 recv든 read든 read operation을 하는 함수가 0 또는 -1을 리턴한다면 에러로 간주하고 Client를 제거해야 합니다. 에러 응답도 보내지 않습니다.

bool
Server::runRecvAndSolve(Connection& connection)
{
	try {
		recvRequest(connection, connection.get_m_request());
	} catch (int status_code) {
		createResponse(connection, status_code);
		return (true);
	} catch (Server::IOError& e) {
		throw (e);
	} catch (std::exception& e) {
		ft::log(ServerManager::log_fd, std::string("[Failed][Request] Failed to create request because ") + e.what());
		createResponse(connection, 50001);
		return (true);
	}
	const Request& request = connection.get_m_request();
	if (request.get_m_phase() == Request::COMPLETE)
	{
		writeCreateNewRequestLog(request);
		connection.set_m_status(Connection::ON_EXECUTE);
		solveRequest(connection, connection.get_m_request());
		return (true);
	}
	return (false);
}

Custom 상태 코드

에러 발생시 상태코드를 throw할 때, 아래와 같이 상태코드 뒤에 2자리 수를 더 붙여 해당 에러가 발생한 원인이나 위치를 로그로 남기면 유용합니다. 실제로 응답을 보낼 때는 앞의 3자리만 잘라서 보내면 되고, status_map에 3자리 상태코드에 대한 사유구절도 모두 저장되어 있기 때문에 문제가 없습니다.

status_map[40022] = "Bad Request: Credential Form unvalid";
status_map[40023] = "Bad Request: Not CGI-prgoram, POST method, Content-Length is not 0";
status_map[40101] = "Unauthorized";
status_map[40301] = "Forbidden: Credential Content unvalid";    
status_map[40401] = "Not Found: No suitable location";

Enum

Enum을 활용하면 효율적인 프로그래밍이 가능합니다.

	enum Method { DEFAULT, GET, HEAD, POST, PUT, DELETE, OPTIONS, TRACE };
	enum URIType { DIRECTORY, FILE, FILE_TO_CREATE, CGI_PROGRAM };
	enum TransferType { GENERAL, CHUNKED };
	enum Phase { READY, ON_HEADER, ON_BODY, COMPLETE };

Chunked Transfer-Encoding

바디가 있는 리퀘스트의 경우 클라이언트가 바디를 전송해오는 방식에는 2가지가 있습니다. 하나는 Content-Length가 설정된 경우로, 그 값만큼만 body를 읽으면 됩니다. 하지만 대용량 데이터의 경우 한 번에 그렇게 많은 양을 읽을 수 없으므로 나누어서 보내게 되는데, 이 때에는 Transfer-Encdoing 헤더의 value가 chuked로 오게 됩니다.

위와 같이 얼마만큼의 길이를 보낼 것인지 토큰을 한 번 보내고, 그 숫자만큼의 메시지를 보내고, 또 토큰을 보내고, 토큰의 숫자만큼 메시지를 보내는 과정을 반복합니다. 0 토큰이 종료 신호로 읽히며, raw data로 보면 "0\r\n\r\n"으로 종료됩니다.

토큰은 16진수로 옵니다. 변환을 위한 stoi 함수는 c++11 문법인데, 98을 지키라는 내용이 서브젝트에 없긴 하지만 저희는 98로 작성하기 위해 stoi, to_string과 같은 함수들도 모두 만들어서 사용했습니다.

"0\r\n\r\n"이 body에 존재한다고 무조건 끝인 것은 아닙니다. 앞 토큰의 숫자 범위에 이 데이터가 포함되는 경우, 이 데이터는 종료 토큰이 아니라 바디의 일부로 읽히기 때문입니다. 이미지 데이터의 경우 이런 패턴이 많이 나오기 때문에 조심해서 읽어야 합니다.

Last updated