기본적으로 RFC1459, RFC2812와 같은 통신 규약을 기반으로 서버 - 클라이언트 간 통신 방식이 정해져있다.
그에 해당하는 방식을 참고하고 inspircd
와 같은 상용 서버를 기반으로 클라이언트의 메시지에 어떤 형식으로 답변하는지를 참고하였음.
이를 기반으로 ft_irc
를 구성하였고, 서브젝트에서 요구하는 사항들 이상의 기능은 없기때문에 다소 서버라기엔 부족한 사항들이 있었다.
등록 과정은 패스워드 확인 여부(user.auth), 닉네임등록여부(user.nickComplete), 유저정보(user.userComplete)의 bool 타입 변수들이 모두 true가 될 때까지 진행하도록 한다.
하나라도 false인 상태라면 서버의 그 어느 기능도 사용할 수 없다.
1) 닉네임
닉네임 룰은 RFC1459에 나와있듯, 9자 이내의 캐릭터 구성, 특수문자 불가 등 여러가지 규칙이 정해져있다. 하지만 irc 서버마다 이를 처리하는 방식은 다름.
실제로 inspircd
에서는 32자 길이까지 허용하며 대소문자 구분과 특수문자 허용여부도 서버별로 다르다.
ft_irc 과제에서는 닉네임에 대한 특별한 제약을 언급하지 않았기에 별도로 제약을 두고 만들지는 않았다.'
또한 닉네임은 서버 당 유일하게 존재해야한다.
중복된 닉네임이 서버로 요청되는경우, 클라이언트로 이미 존재하는 닉네임이라는 응답 코드를 보내면 irssi 클라이언트의 경우 기존에 보냈던 닉네임 + _ 로 다시 NICK 명령어를 수행한다.
2) 패스워드
서버가 생성되면서 설정한 비밀번호와 클라이언트가 전송한 비밀번호를 비교
3) 유저정보
"USER :"
유저의 식별에 필요한 정보. 주로 server-to-server 통신 시 사용됨.
server-to-server communication
IRC 서버들 간의 통신을 의미. IRC 네트워크를 구성하는 핵심적인 기능.
- 사용자 정보 공유: 사용자가 다른 서버에 접속했을 때 해당 사용자 정보를 공유하여 전체 IRC 네트워크에서 일관된 사용자 관리가 가능하도록 함.
- 채널 정보 공유: 사용자가 다른 서버에 있는 채널에 참여할 수 있도록 채널 정보 공유.
- 메시지 전달 : 사용자가 다른 서버의 사용자에게 메시지를 보낼 때 해당 메시지를 전달함.
- 서버 간 동기화 : 서버 간 사용자, 채널, 설정 등의 정보를 동기화하여 일관된 IRC 네트워크를 유지함.
초대가 되는지를 확인하려면 invite mode 설정이 잘 되는지를 먼저 확인.(mode +i)
서버는 클라이언트에 메시지를 받으면 응답해주는 일종의 자동응답기의 역할을 한다.
이는 서버와 클라이언트가 잘 연결되어있는 상황에선 문제가 되지 않으나, 갑자기 연결이 끊겨버리는 경우엔 기존 응답 경로로 메시지를 보내는 것이 문제가 되고 변경된 클라이언트의 상태를 확인하지 못해서 서버에 문제가 생길 수 있다.
실제 ft_irc를 진행하면서 발생했던 문제는 클라이언트가 예기치 못한 종료(SIGINT)로 소켓 연결이 해제되었을 때였다.
정상적으로 종료되는 클라이언트는 서버에 종료 커맨드(QUIT)를 전송하여 상호 소켓 해제 절차를 밟는 것으로 안다. 하지만 그렇지 못하다면, 클라이언트의 급 종료를 캐치하여 서버가 지속 연결되어있는 상태를 해제해주는 절차가 필요함.
다른 사람들의 irc 평가를 갔었을땐 signal 함수를 활용하여 SIGINT를 서버가 감지하면 해당 클라이언트를 종료하는 방식을 사용하였다. 이를 위해선 전역변수가 필요하고 가능한 전역변수를 사용하지 않는 코딩 스타일을 고수하고싶었다.
그렇게 찾은 다른 방식은 poll()
의 리턴값을 통해 시그널을 받아 종료되었음을 파악하는 것이었다.
ft_irc에서는 서버가 클라이언트와 연결된 fd가 이벤트가 발생했을 때 해당 이벤트에 대해 알려줄 수 있는 함수를 하나 사용해야한다. poll()
, epoll()
, kqueue()
등 여러 함수가 유사한 기능을 하지만 그 중에서도 우리 팀이 사용한 방식은 poll()
이었다.
poll() 은 이벤트가 발생할때까지 대기하다가 fd에서 이벤트가 발생했을 때 반환값을 준다. 그러면 서버에서는 연결된 각 fd를 순회하면서 읽기가능한 이벤트가 발생했는지를 찾는다.
SIGINT로 종료된 클라이언트는 pollfd 라는 구조체의 revents 필드를 POLLHUP
으로 변경한다.
이렇게 이벤트가 발생하면 poll 함수는 그걸 감지하고, 서버는 fd 순회에 돌입한다.
그리고 이벤트가 발생한 fd에 대해 read를 수행하는데, 단순히 revents 필드가 POLLHUP 이 되고 읽을 수 있는 데이터는 없으므로 read의 반환값은 0이다.
읽을 수 있는 데이터의 이벤트가 발생한 것 외의 이벤트는 모두 예외로 처리하여 해당 소켓의 연결을 해제해주었다.
POLLIN
: 읽기 가능한 데이터가 있음을 나타냅니다.POLLOUT
: 쓰기 가능한 상태임을 나타냅니다.POLLERR
: 오류가 발생했음을 나타냅니다.POLLHUP
: 피어가 연결을 종료했음을 나타냅니다.POLLNVAL
: 파일 디스크립터가 유효하지 않음을 나타냅니다.POLLOUT, POLLIN을 제외하면 모든 이벤트가 에러 케이스에 해당하므로, 종료하는 것이 타당하다.
따라서 IRCServer 내의 채널 및 유저 정보에서 해당 fd 의 데이터를 모두 제거한 후 소켓을 close해주는 식으로 문제를 해결하였음.
사실 POLLOUT 값은 경우 소켓이 쓰기가능한 상태임을 알리는 이벤트라 read시 0 바이트인데, 에러상황이 아니라 종료해서는 안된다.
하지만 대부분의 상용 클라이언트(irssi, nc 등) 에서 큰 문자열을 프로토콜에 의거하여 512 바이트 단위로 잘라서 버퍼가 가득 차는 상황을 피하므로 POLLOUT이 발생하지 않는다.
privmsg #a :01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789
^M
을 보내게 되었을 때, 서버는
REVENTS : 1
READ BYTES : 512
REVENTS : 1
READ BYTES : 121
이와 같이 최대 512 바이트만을 보내여 write가 차단되지 않으므로, revents값은 항상 1(POLLIN)을 유지한다.
그러므로 POLLIN 이외의 상황에 대해선 에러처리하여 종료하여도 문제가 없었다.
평가 자체에서 irssi를 베이스로 사용할것이니, 별도의 처리를 하지 않았다...
write()
함수를 호출한다.write()
함수가 차단된다.poll()
함수를 호출하면, 출력 버퍼에 여유 공간이 생겼을 때 POLLOUT 이벤트가 발생한다.write()
함수를 호출하여 더 많은 데이터를 보낼 수 있다.사실 irssi라는 상용 클라이언트를 사용해서, 클라이언트가 서버로 규격에 알맞은 메시지로 만들어 보내준다. irssi를 사용할때는 예상치 못한 입력값에 대해 따로 처리해줄 필요가 거의 없어서, 편하다고 생각했지만...nc로도 평가가 진행되어야하기 때문에 가능한 대부분의 에러 케이스에 대해 응답코드를 리턴해줄 수 있어야했다.
inspircd
라는 상용 서버를 활용해서 응답에 대한 리턴이 어떤지를 확인해가면서, RFC와 어떻게 다른지 체크했고, 그에 맞추어 우리 서버에서는 어떻게 대응할지를 판단했다.