IRC서버 메시지 처리

시캉·2024년 7월 2일
0

ft_irc

목록 보기
8/13

1. 개요

기본적으로 RFC1459, RFC2812와 같은 통신 규약을 기반으로 서버 - 클라이언트 간 통신 방식이 정해져있다.
그에 해당하는 방식을 참고하고 inspircd와 같은 상용 서버를 기반으로 클라이언트의 메시지에 어떤 형식으로 답변하는지를 참고하였음.

이를 기반으로 ft_irc 를 구성하였고, 서브젝트에서 요구하는 사항들 이상의 기능은 없기때문에 다소 서버라기엔 부족한 사항들이 있었다.


2. 동작 프로세스

2-1 유저 등록 및 패스워드 확인

  • 패스워드 확인은 클라이언트가 서버에 연결 시 최우선적으로 보내야하는 사항.
  • 패스워드 -> 닉네임 -> 유저정보 순으로 유저등록을 진행함.
  • 패스워드 실패 시, 서버 접속 및 기능 이용 불가.

등록 과정은 패스워드 확인 여부(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 네트워크를 유지함.

2-2 TOPIC 기능

  • 토픽 세팅이 잘 되는가
  • 초기화(no set) 되는가
  • 띄어쓰기 구분되어도 받아들여서 토픽으로 설정되는가
  • operator만 설정할 수 있는가 (+t mode)
  • 채널 내 모두가 설정할 수 있는가 (-t mode)
  • 권한을 부여받은 후 토픽 설정이 가능한가 (+o mode -> topic)

2-3 KICK 기능

  • 채널 내 없는 사람 킥 시 에러
  • 서버에 없는 사람 킥 시 에러
  • 권한 없는 사람이 킥 시 에러
  • 제대로 킥이 되는가
  • 킥 이후 입력 안되는가
  • 강퇴 사유 입력 잘 되는가
  • 강퇴 후 인원 변경 반영되는지
  • 권한 부여 후 킥 동작( +o mode -> kick)

2-4 invite 기능

초대가 되는지를 확인하려면 invite mode 설정이 잘 되는지를 먼저 확인.(mode +i)

  • 초대 잘 되는지
  • 초대 후 킥 시 다시 초대가 필요한지.
  • 재초대 하면 잘 들어와지는지

2-5 mode 기능

[1] i mode
  • 초대 모드 기능의 on / off
[2] t mode
  • 토픽 설정 가능여부 on / off
[3] o mode
  • 권한이 필요한 명령어 사용이 +o 를 통해 권한 부여로 사용되는지
  • 올바르지않은 인자에 대해 에러처리 했는지
[4] l mode
  • 인원 제한이 잘 되는지 (+l)
  • 해제 시 인원 제한 없어지는지 (-l)
  • 꽉 찬 상태에서 초대 시 접속안되는지
[5] k mode
  • 비밀번호 잘 설정되는지 (+k)
  • 비밀번호 설정 해제 시 비번 없이 접속되는지
[BONUS] DCC
  • /dcc send <receive_user> 을 통해 파일 전송요청이 잘 되는가
  • /dcc get으로 전송한 파일을 받도록 전송요청 승인이 잘 되는가.
  • 기본적으로 privmsg 를 이용하기 때문에 dcc 문법이 privmsg의 기존 기능을 침해하지는 않는가

3. 문제 해결

1) 클라이언트의 예기치 못한 종료 시

서버는 클라이언트에 메시지를 받으면 응답해주는 일종의 자동응답기의 역할을 한다.
이는 서버와 클라이언트가 잘 연결되어있는 상황에선 문제가 되지 않으나, 갑자기 연결이 끊겨버리는 경우엔 기존 응답 경로로 메시지를 보내는 것이 문제가 되고 변경된 클라이언트의 상태를 확인하지 못해서 서버에 문제가 생길 수 있다.

실제 ft_irc를 진행하면서 발생했던 문제는 클라이언트가 예기치 못한 종료(SIGINT)로 소켓 연결이 해제되었을 때였다.

  • 채널 내에 사용자A 가 존재
  • 사용자A : SIGINT로 종료
  • 채널 내의 다른 사용자 B가 PRIVMSG 전송
  • 사용자 A도 동일하게 서버에 의해 PRIVMSG를 전달받음.
  • 하지만 이미 클라이언트는 없어져버린 상태.
  • 서버에는 그 클라이언트가 여전히 남아있다.

정상적으로 종료되는 클라이언트는 서버에 종료 커맨드(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이다.

읽을 수 있는 데이터의 이벤트가 발생한 것 외의 이벤트는 모두 예외로 처리하여 해당 소켓의 연결을 해제해주었다.

POLL 함수의 이벤트 종류
  • 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를 베이스로 사용할것이니, 별도의 처리를 하지 않았다...

POLLOUT 발생조건
  1. 클라이언트가 서버에 데이터를 보내려고 write() 함수를 호출한다.
  2. 서버의 출력 버퍼가 가득 차 있어서 write() 함수가 차단된다.
  3. 서버가 poll() 함수를 호출하면, 출력 버퍼에 여유 공간이 생겼을 때 POLLOUT 이벤트가 발생한다.
  4. 서버는 POLLOUT 이벤트를 감지하고, 이 시점에 다시 write() 함수를 호출하여 더 많은 데이터를 보낼 수 있다.

마치며

사실 irssi라는 상용 클라이언트를 사용해서, 클라이언트가 서버로 규격에 알맞은 메시지로 만들어 보내준다. irssi를 사용할때는 예상치 못한 입력값에 대해 따로 처리해줄 필요가 거의 없어서, 편하다고 생각했지만...nc로도 평가가 진행되어야하기 때문에 가능한 대부분의 에러 케이스에 대해 응답코드를 리턴해줄 수 있어야했다.

inspircd라는 상용 서버를 활용해서 응답에 대한 리턴이 어떤지를 확인해가면서, RFC와 어떻게 다른지 체크했고, 그에 맞추어 우리 서버에서는 어떻게 대응할지를 판단했다.

0개의 댓글

관련 채용 정보