ft_irc
Subject
IRC는 텍스트 기반 커뮤니케이션 프로토콜이다 인터넷에서
리얼타임 메시징, 퍼블릭 오어 프라이빗으로 제공해야한다. 유저는 dm이나 그룹채널 참여가 가능합니다.
제너럴 룰
니 프로그램은 어떤 상황이든 크래시 나면 안되고 , 예기치 않게 종료되선 안된다.
만약 그러면 0점이다.
메이크파일을 만들어야한다. 리링크 안되어야한다.
$NAME, all clean, fclean, re 포함해야한다.
c++ -Wall -Wextra -Werror 플래그 넣어서 컴파일해라 + -std=c++98
항상 c++ 스타일로 해라 c 함수는 허용되어있지만 C++ 버전을 선호한다.
외부 라이브러리나 부스트 라이브러리는 금지된다.
멘데토리
./ircserv
Makefile ... h, hpp, cpp, tpp, ipp 선택적인 컨피그 파일
포트, 패스워드를 인자로 전달한다.
c++98 을 사용하고 socket, setsockopt, getsockname, getprotobyname, gethostbyname, getaddrinfo, freeaddrinfo, bind, connect, listen, accept, htons, htonl, ntohs, ntohl, inet_addr, inet_ntoa, send, recv, signal, lseek, fstat, fcntl, poll (or equivalent)
poll 과 비슷한 어떤것이라도 사용가능. select(), kqueue(), or epoll().
헤더
#include <sys/socket.h>
socket, listen, accept, connect, bind, setsockopt,
getsockname, getaddrinfo(netdb.h), freeaddrifo(netdb.h),
send, recv,
#include <netdb.h>
getprotobyname, gethostbyname,
#include <arpa/inet.h>
htons, htonl, ntohs, ntohl, inet_addr, inet_ntoa,
#include <signal.h>
signal
#include <unistd.h>
lseek
#include <sys/stat.h>
fstat
#include <fcntl.h>
fcntl
etc
poll(poll.h), select(sys/select.h), kqueue(sys/types.h...)
클라이언트 개발금지
서버투 서버 통신 처리 x
ircserv <port> <password> : 리스닝포트, 비밀번호(서버에 연결하려는 클라이언트가 무조거ㅏㄴ 입력)
요구사항
서버는 동시에 여러 클라이언트를 처리할 수 있어야한다.
fork는 허용되지 않는다. 입출력시 블럭되면 안된다.
=> 클라이언트 요청 시 프로세스를 복사(fork)해서 자식프로세스와 클라이언트의 통신을 하게하여 서버데몬의 블러킹이 발생하지 않게 만들 수 있는데, fork 방식은 사용하지 말라는 뜻이다.
읽기 쓰기 리스닝 등의 작업을 할때 하나의 poll 만 사용한다. => __먼소리임?
* non blocking 파일 디스크립터를 쓰면 poll 없이도 되긴함 근데 느리다 그래서 써라
하나의 irc 클라이언트를 정해라 평가때 쓸것이다
서버와 클라이언트는 tcp/ip로 통신해야한다
일반적인 irc서버를 따라야한다.
1. (인증기능있어야됨 닉네임, 유저네임 세팅, 채널조인, 프라이빗메시지 보내기받기)를 레퍼런스클라이언트로 할수있어야한다
2. 채널로 보낸 메시지는 채널에있는 모든 클라이언트에게 보내져야한다
3. 운영자랑 사용자가 있어야한다
4. 운영자 명령도 구현해야한다
예쁘게 코드를 작성해라
맥은 write가 다른 유닉스랑 다르다 fcntl을 허용해줬으니 확인하라
fcntl(fd, F_SETFL, O_NONBLOCK); 플래그만 써라
테스트 => 나눠서 보내도 명령어가 잘동작해야한다
개념
소켓통신
코드흐름
- socket : 소켓을 생성하는 함수. type에 따라 블러킹 모드를 선택할 수 있다. 서버소켓 리턴
- 서버주소(sockaddr_in) 구조체설정 (서버의 아이피 세팅=INADDR_ANY, listen port 세팅)
* INADDR_ANY : 랜카드주소 암거나를 의미. 이미 클라이언트는 라우팅과 공유기의 포트포워딩을 타고 서버까지 데이터를 보내왔다. 랜카드는 데이터를 수신했고, 그 데이터중 어떤 아이피로 왔는지에 따라 수용여부를 선택하는것이다. INADDR_ANY는 모두 수용이다.
- bind : 위에서 생성한 소켓과, 세팅한 서버소켓 구조체를 엮는다. 이제 그 정보에 맞는 데이터는 1에서 생성한 소켓으로 전달된다.
- listen : 소켓을 리스닝 상태로 변경하는 함수. 데이터를 받아들이기 시작하는 시점
+ socket : 서버와 연결할 소켓을 만든다.
+ 서버주소 구조체 설정. 클라이언트라서 공인아이피 주소와 서버에서 공개한 포트포워딩된 포트로 세팅해야한다.
+ connect : 서버로 syn 메시지를 전송한다.
- accept : 소켓의 백로그에 있는 클라이언트의 요청을 수락하는 함수 syn ack. 클라이언트 요청이 없다면 여기서 블록이 된다. 리턴값으로 클라이언트 소켓을 반환받고 앞으로 데이터 입출력은 이 소켓으로 하면된다.
클라이언트가 ack 보내고 연결완료
=> 즉 servsock에 값이 입력된것은 클라이언트의 연결요청이 있다는 뜻이고, accept은 인자로 전달된 servsock을 검사하여 값이 입력됐다면 그 클라이언트 요청을 연결해서 clntaddr에 정보를 세팅하고, 클라이언트와 데이터를 주고받을 새로운 소켓인 clntsock을 리턴한다. 이후 clntsock에서 데이터 송수신을 한다.
입출력 작업
하드웨어 작업이라 커널영역에서 이뤄져야 한다.
* 입출력 작업이라는게 사실 모든 입출력을 통틀어서 진행되는건데, 이 작업이 커널에선 어떻게 수행될까? 커널입장에서 쓰레드 돌리면서 하나씩 작업하는건가?
동기 vs 비동기
동기 방식 : 하나의 입출력이 끝날때까지 기다려서 다음 입출력을 시작한다. 시간은 오래걸리지만 작업 순서가 보장된다.
비동기 방식 : 그냥 순서에 상관없이 계속 요청한다. 동시에 진행되는 만큼 빠를 수 있지만, 작업 순서는 보장되지 않는다.
블로킹 vs 넌블로킹
블로킹 방식 : 입출력이 끝날때까지 대기하는 방식
넌블로킹 방식 : 입출력을 요청해두고 알아서 하라고 냅둔다. 끝났는지 중간중간 확인할수도 있고, 끝나면 콜백이 실행되게할수도 있을것이다.
IBM Developer 에서의 입출력 모델 분류
blocking i/o (sync + block)
얘는 user process 가 kernel에게 요청을 보내고 커널이 작업 결과를 반환할때까지 대기한다. 이때 CPU작업은 필요 없으므로 kernel의 응답을 기다린다. => system call 에서 블러킹이 된다.
signal에 의해 system call이 중단될 수 있지만, 그렇지 않다면 프로세스는 대기상태에서 kernel의 응답을 받으면 user space의 buffer로 돌아오게되고 unblocking 되어 반환받은 데이터를 처리할 수 있게된다.
non-blocking i/o (sync + non-block)
소켓 생성시 O_NONBLOCK 옵션으로 구성한다. 해당 socket으로 io system call을 하면 즉시 결과를 반환받는데, 읽을 데이터가 없다면 -1을 반환하여 없다는것을 확인한다. errno로 상세내역 확인가능
블로킹이 되지 않는것은 다른일을 하면서 확인할 수 있다는 장점이 있지만, 항상 userprocess에서 확인해봐야 한다는점이 문제이다.
여기서 동기식이기 때문에 어차피 기다려야한다.
만약 loop 안에서 확인하는 코드 주기가 길다면 io 가 완료되어도 놀고있는 상황이 발생할 수 있고, 짧다면 cpu를 계속 활용하기도 하고, kernel입장에선 -1 return 을 계속 해줘야하기 때문에 io 자체가 지연될수도 있다.
대신 루프안의 내용을 io 입출력이 끝나기 전에 계속해서 처리할 수 있다는게 장점이다.
이 모델들에서는 다중처리를 하기위해선 multi thread를 사용해야한다.
i/o multiplexing (async + block)
다중화(멀티플렉싱)라는것은 한 process가 여러 file을 관리하는 방법이다.
네트워크에선 server의 한 프로세스(스레드)가 여러 socket(어차피 그냥 주소를 가진 file임)을 관리하여 여러 client를 수용할 수 있게 구성하는것이다.
여러 fd를 두고 어떻게 감시하냐에 따라서 함수들이 나뉜다. (select poll kqueue epoll iocp)
io system call은 넌블러킹으로 호출되고 select나 poll에서 설정한 타임아웃시간동안 system call에서 커널의 응답이 블러킹된다.
=> select 나 poll에서 만약 데이터 입출력이나 클라이언트 연결이 발생되면 리턴되어 소켓 확인 작업을 실행한다.
네트워크에서 async라는 것은 어떤 클라이언트가 먼저 연결하더라도 할일이 없다면 나중에 연결한 클라이언트가 우선권을 잡을 수 있다.
함수
- select
int select(int nfds , fd_set *restrict readfds ,
fd_set *restrict writefds , fd_set *restrict exceptfds ,
struct timeval *restrict timeout );
대상 fd를 배열에 집어넣고 하나하나 순차검색하여 O(n)의 시간복잡도를 갖게된다.
1024 bit 크기의 bit table을 사용하기 때문에 최대 1024개의 fd만 관리할 수 있고, 파일 변경이 감지되면 fd에 맞는 bit를 1로 변경하게 된다.
수신대기 select에서는 fd_set에 어떤 fd에 대한 변경을 확인할건지 bit table의 인덱스를 1로 세팅해두고, readfds로 전달한다.
(반대로 쓰기 변화를 확인할땐 writefds이다.)
select는 타임아웃동안 블럭되는 함수이고, 전달된 bit table의 1인 소켓을 확인하여 변경된 파일의 개수를 반환하며 readfds로 전달한 bit table에서 입출력이 된 소켓인덱스만 && 해서 1로 변경된다. (기존 1인소켓 && 입출력됐다면 1)
* select는 bit table을 변경하게 되는데, 다음 select를 호출할때 read할 소켓을 지정해줘야하기 때문에 read할 소켓정보가 저장된 bit table을 따로만들고 루프 시작부분에서 select가 수정해줄 bit table을 복사해둬야 한다. (
리턴값이 -1이면 에러, 0이면 변경되지 않은것(타임아웃), 1 이상이라면 FT_ISSET을 전체 bit table에 대해 확인해서 변경된 fd를 찾고,
만약 발견된 fd가 serv_sock이라면
클라이언트의 connet이기 때문에 accept해준다. 이후 해당 소켓을 검사대상에 포함(IS_SET)시키고, 반복문 카운트를 해당소켓까지로 증가시킨다.
* 소켓은 낮은수부터 증가하기 때문에 처음엔 for문을 가장먼저 생성된 servsock 까지만 검사하면되고, 이후에 accept가 되어 clntsock이 추가되면 추가된 clntsock까지만 검사하면 servsock, 추가된clntsock을 검사하게된다.
만약 servsock이 아니라면 데이터를 읽는다.
클라이언트에서 close를 보내면 read는 0을 리턴한다.
- poll
select 는 1024개 밖에 관리할수 없다는 단점이 있는데, poll은 무한개를 검사할 수 있다.
fd에 이벤트가 발생했을때 blocking이 해제되며 실제 fd 수 만큼 검사하여 마찬가지로 O(n)이다.
slect는 한 이벤트 전달에 3bit만 사용되고, poll은 fd가 무한대인 대신 64bit라서 효율이 떨어질 수 있다.
타임아웃은 -1을 지정했을때 무한대가된다.
int poll(struct pollfd * fds , nfds_t nfds , int timeout );
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
리턴값 : select와 동일하게 event가 발생한 fd 갯수이며, 0이면 timeout, -1이면 에러가 발생한 것이다.
select에선 fd를 인덱스로 알 수 있지만, poll에선 pollfd 구조체로 관리된다.
fd가 음수인 경우 해당 fd를 검사하지 않겠다는것이고, events필드는 무시되며 revents는 0으로 세팅된다.
events에 관심있는 이벤트를 세팅하고, revents에는 events에 등록된 이벤트중 하나가 발생하면 poll이 리턴되며 revents에 세팅된다.
0번엔 서버 fd를 설정하고 0번에 revents가 POLLIN이라면 connect 요청이 들어온거고 만약 1번부터 POLLIN이라면 데이터가 들어온것이다.
이슈
poll revents 에서 클라이언트 연결 종료 시 최근 bsd에서는 POLLHUP | POLLIN 이다.