구현 | 구현 테크닉 16

1. Avoid Memory Leak

저희는 Memory Leak에 대해 큰 걱정이 없었습니다. malloc을 안 쓰면 되니까요. 컨테이너를 최대한 활용하면, 인스턴스의 소멸에 대해서는 컨테이너가 책임지기 때문에 이슈가 없습니다. 저희는 malloc을 프로그램 전체에 걸쳐 딱 한 번 사용하는데, CGI 실행 후 환경변수(char **)를 해제하는 것입니다. minishell을 진행하면서 사용했다 메모리 해제 함수를 끌어다 그대로 썼습니다.

2. Response Connection Type

enum ConnectionType { CLOSE, KEEP_ALIVE, };

응답에서 커넥션 타입의 경우 Close와 Keep-Alive를 enum으로 사용하면 편리합니다. 이런 작업들을 문자열로 관리하게 되면 오타 때문에 에러가 나기 쉽고, 개발을 하다가 후보 값들을 확인하려면 enum 의 정의를 보는 것보다 접근성 및 가독성이 무척 떨어집니다.

3. Transfer Type

enum TransferType { GENERAL, CHUNKED };

전송 타입의 경우 Request와 Response 객체 모두 GENERAL(Content-Length)과 CHUNKED로 구분하여 관리하면 편리합니다.

4. Method

enum Method { DEFAULT, GET, HEAD, POST, PUT, DELETE, OPTIONS, TRACE };

메소드를 체크해야 할 경우가 많은데 이것도 Enum으로 관리하면 편리합니다.

5. URI Type

enum URIType { DIRECTORY, FILE, FILE_TO_CREATE, CGI_PROGRAM };

URI 타입의 경우 매번 문자열을 파싱해서 처리하는 것이 아니라, 파싱할 때 분석하여 저장해두면 사용하기가 편리합니다.

6. Request & Response Phase

enum Phase { READY, ON_HEADER, ON_BODY, COMPLETE };

Request, Response, Connection 모두 한 번에 처리할 수 없는 작업이 많습니다. 특히 저희는 아래와 같이 data가 들어오는 대로 처리를 진행했기 때문에 요청과 응답 모두 phase가 필요했습니다. 만약 요청 메시지를 다 읽은 후에 리퀘스트를 생성하는 방식으로 구현하면 요청이나 응답의 처리 페이즈를 enum으로 구현하지 않아도 괜찮을 거에요. 하지만 그 경우 처리 시간이 길어진다는 이슈가 있습니다.

...
if (phase == Request::READY && hasRequest(connection) && (count = recvWithoutBody(connection, buf, sizeof(buf))) > 0)
    connection.addRbufFromClient(buf, count);
if (phase == Request::READY && parseStartLine(connection, request))
    phase = Request::ON_HEADER;
if (phase == Request::ON_HEADER && parseHeader(connection, request))
{
    request.set_m_phase(phase = Request::ON_BODY);
    if (isRequestHasBody(request))
        return ;
}
if (phase == Request::ON_BODY && (count = recvBody(connection, buf, sizeof(buf))) > 0)
    connection.addRbufFromClient(buf, count);
...

7. Connection Status

enum Status { ON_WAIT, TO_SEND, ON_SEND, TO_EXECUTE, ON_EXECUTE, ON_RECV };

커넥션은 리퀘스트 대기, 리퀘스트 읽기, CGI 프로그램과 입출력 시작 전, 입출력 시작 중, 응답 전송 전, 응답 전송 중이라는 선형적인 작업 흐름을 가집니다. 현재 커넥션이 어떤 상태에 있는가를 명확하게 status로 관리하면, 커넥션을 처리하는 흐름을 제어하기가 매우 편해집니다.

8. HTML Writer

...
void set_m_body(std::string body);

void add_title(std::string head_content);
void add_bgcolor(std::string bg_color);
void add_tag(std::string front_token, std::string tag, std::string content = "", bool newline = false);
void add_text(std::string front_toekn, std::string content, bool newline = false);

std::string makeLink(std::string address, std::string content = "");
void add_line(int line_idx, std::string line);
...

Autoindex를 처리하기 위해서는 HTML 페이지를 만들 필요가 있습니다. 저희는 HTML Writer 클래스를 생성하여 필요한 html 페이지를 만들었습니다. 이외에도 이런 클래스를 만들어서 처리하면 에러 페이지 생성 등 필요한 HTML 페이지를 편리하게 만들 수 있습니다.

9. stringVectorToMap/Set

std::map<std::string, std::string> map_block = ft::stringVectorToMap(ft::split(location_block, '\n'), ' ');
this->m_root_path = map_block.find("root")->second;

configuration 파일을 block 단위로 분할하여 Config, Location, Server 등을 생성할 때 효율적인 방법이 있습니다. 긴 스트링으로 들어오는 블록(문단)을 split하여 vector로 만든 다음 vector를 map으로 전환하는 것입니다.

index나 cgi의 경우 값이 복수이기 때문에 아래와 같이 vector를 set으로 전환하여 저장하면 처리가 편리합니다.

if (ft::hasKey(map_block, "index"))
    this->m_index = ft::stringVectorToSet(ft::split(map_block.find("index")->second, ' '));
if (ft::hasKey(map_block, "cgi"))
    this->m_cgi = ft::stringVectorToSet(ft::split(map_block.find("cgi")->second, ' '));

10. FD 함수와 ALL_SET

void
ServerManager::fdCopy(SetType fdset)
{
	if (fdset == WRITE_SET || fdset == ALL_SET) {
		ft::fdZero(&this->m_write_copy_set);
		this->m_write_copy_set = this->m_write_set;
	}
	if (fdset == READ_SET || fdset == ALL_SET) {
		ft::fdZero(&this->m_read_copy_set);
		this->m_read_copy_set = this->m_read_set;
	}
}

fd 함수들의 인자로 ALL_SET을 넘기면 호출하는 쪽에서 아래와 같이 한 줄로 같은 작업을 심플하게 처리할 수 있습니다.

fdCopy(ALL_SET);

if ((cnt = select(this->m_max_fd + 1, &this->m_read_copy_set, &this->m_write_copy_set, \
NULL, &timeout)) == -1)
...

11. resetMaxFd

void
ServerManager::resetMaxFd(int new_max_fd)
{
	if (new_max_fd != -1)
		set_m_max_fd(new_max_fd);
	else
	{
		for (int i = 512; i >= 0; --i)
		{
			if (fdIsset(i, READ_SET) || fdIsset(i, WRITE_SET))
			{
				m_max_fd = i;
				break ;
			}
		}
	}
}

select 함수의 인자로 감시할 fd의 범위를 넘겨주려면 max fd 값이 필요한데, fd를 닫거나 새로운 fd를 만들 때마다 재설정하면 코드는 빠르겠지만 관리해야 할 포인트들이 많아서 유지보수가 어려워집니다. 저는 read_set이나 write_set 중 둘 중 하나에는 client_fd와 server_fd가 늘 들어있기 때문에 isset 함수를 활용하여 설정된 최대 fd를 확인하는 방식으로 select 함수 실행 전 max_fd를 재설정했습니다.

read_set과 write_set 이외에도 connect_set이나 used_set을 만들어서 현재 사용중인 fd를 따로 관리하는 방식이 더 깔끔할 수 있습니다.

12. all_of와 유효성 검사

	if (ft::hasKey(map_block, "cgi")) {
		std::set<std::string> cgi_set = ft::stringVectorToSet(ft::split(map_block["cgi"], ' '));
		if (cgi_set.empty() || !std::all_of(cgi_set.begin(), cgi_set.end(), isValidCgi))
			return (false);
	}

알고리즘의 all_of 함수를 이용하면 컨테이너의 모든 데이터가 조건을 만족시키는지를 편리하게 검사할 수 있습니다. 컨테이너의 시작/끝 이터레이터, 검사할 함수(bool 자료형을 리턴하고, 컨테이너의 객체 인스턴스를 인자로 받는)를 넘기는 방식으로 사용할 수 있습니다.

13. MIME types

MIME type을 서버의 static 변수로 보관해두면 편리하게 사용할 수 있습니다.

std::map<std::string, std::string> makeMimeType ()
{
	std::map<std::string, std::string> type_map;

	type_map["avi"] = "video/x-msvivdeo";
	type_map["bin"] = "application/octet-stream";
	type_map["bmp"] = "image/bmp";
	...
}
std::map<std::string, std::string> Server::mime_types = makeMimeType();

14. IO Error와 try/catch

여러 번 강조하지만, read/write operation이 0이나 1을 리턴할 경우 connection(client)은 제거되어야 합니다. 그러나 이것을 변수나 리턴 값으로 조회하게 되면 로직이 매우 복잡해집니다. 입출력 에러가 발생했을 때에는 IOError를 정의하여 throw하고, 밖에서 catch로 받아서 클라이언트를 제거하는 것이 심플하고 효율적입니다.

try {
    if (hasSendWork(it2->second))
    {
        runSend(it2->second);
        continue ;
    }
    if (hasExecuteWork(it2->second))
    {
        runExecute(it2->second);
        continue ;
    }
    if (hasRequest(it2->second)) {                
        runRecvAndSolve(it2->second);
    }
} catch (Server::IOError& e) {
    ft::log(ServerManager::log_fd, ft::getTimestamp() + e.location() + std::string("\n"));
    closeConnection(fd);
}

15. getSetFdString

특정 fdSet에 설정되어 있는 fd들을 한 줄의 문자열로 확인하는 함수입니다. 현재 read_set이나 write_set의 상태가 어떤지를 디버깅할 때 매우 편리합니다.

std::string
	getSetFdString(int max_fd, fd_set* fset)
	{
		std::string ret;
		bool first = true;
		for (int i = 0; i <= max_fd; ++i) {
			if (ft::fdIsset(i, fset)) {
				if (!first) {
					ret.append(",");
				}
				first = false;
				ret.append(ft::to_string(i));
			}
		}
		return (ret);
	}

16. overtime 체크

hanging connection이 없어야 합니다. 일반적으로 클라이언트 측에서 커넥션을 닫으면 서버의 select에서 감지가 되지만, 계속해서 열어놓는 경우에는 문제가 될 수 있습니다. 오래된 커넥션을 주기적으로 닫아주는 로직을 만들어놓으면 커넥션들을 보다 깔끔하게 관리할 수 있습니다.

if (connection.isOverTime())
	closeClientConnection(fd);

Last updated