멀티플렉싱 서버의 구현에 있어서 가장 대표적인 방법 : select()


select함수를 사용하면 한곳에 여러 개의 file descriptor를 모아놓고 동시에 이들을 관찰할 수 있다.

*파일 디스크립터의 관찰은 소켓의 관찰로 해석할 수 있다.

파일 디스크립터에 대한 관찰을 하는 데 사용되는 것이 fd_set형 변수이다. 이는 0과 1로 표현되는, 비트단위로 이뤄진 배열이다.

이러한 자료형으로 각 file descriptor에 대해 세 가지에 대해서 관찰을 한다.

- read buffer 관련

- write buffer 관련

- exception 관련


이러한 관찰범위를 select 함수에 fd_set 변수를 선언하여 변수의 주소값을 넘겨준다.

관심없는 관찰에 대해선 NULL을 넘겨주면된다.


select의 parameter를 살펴보면 다음과 같다.



readfds 는 read buffer 즉, '수신된 데이터의 존재여부'에 관심 있는 file descriptor 정보를 모두 등록해서 그 변수의 주소 값을 전달한다.

writefds 는 write buffer 즉, '블로킹 없는 데이터 전송의 가능여부'에 관심 있는 file descriptor 정보를 모두 등록해서 그 변수의 주소 값을 전달한다.

exceptfds는 exception 즉, ' 예외상황의 발생여부'에 관심있는 파일 디스크립터 정보를 모두 등록해서 그 변수의 주소 값을 전달한다.


※readfds, writefds, exceptfds는 모두 서로 다른 fd_set 형 변수이다.



관찰 대상이되는 파일 디스크립터의 수를 나타내는 nfds에 대해서 알아보자.

여기서 주의해야할 것은 관찰 대상이 되는 file descriptor의 정확한 개수를 넘기는 게 아니라

'가장 큰 file descriptor의 값 + 1' 을 select의 가장 첫번째 인수로 넘겨준다는 것이다.

이 의미는 fd_set형 배열을 nfds만큼 순회하면서 값이 1인 file descriptor를 캐치한다는 것이다.


Q. 관찰을 원하지 않는 file descriptor의 변화를 감지할 수 있지 않은가?

우리는 fd_set형 변수에서 관심있는 file descriptor를 1로 set한다. 그리고 이 fd_set 변수는 초기화하는 데 사용되고

이 변수를 또다른 fd_set형 변수를 선언하여 복사해준다. 

그래서 매번 select 하기 전에 원래의 fd_set(1)으로 초기화한다. 그 다음, select의 fd_set 자리에는 복사한 fd_set(2)형 변수의 주소값을 넘겨준다.

select는 fd_set의 1로 셋팅된 것으로 관찰 대상인 file descriptor를 판별하는데 매번 초기화를 안 해주면 들어갈 때마다 값이 바뀌기 때문에 관찰대상이 계속 바뀌어 의미가 없어진다.



Select함수는 변화를 감지하기 위한 목적이 있다. 그래서 세 가지 관찰범위에 대해서 변화가 일어나지 않으면 반환을 하지 않는다. 이러한 infinite blocking 상태에 빠지지 않게 하기 위해서 설정한 timeout 시간을 초과하면 0을 반환하도록 한다.


실패하면 -1을 반환하고 성공 시, 변화가 일어난 file descriptor의 수를 반환한다.









일방적인 연결 종료-close() 의 문제점

close  호출 후

- read buffer는 close와 동시에 모두 없어짐

- write buffer는 안의 내용을 모두 내보낸 다음에 파일의 마지막을 의미하는 'EOF'를 전송 후 스트림이 종료된다. 

※ EOF가 스트림 종료가 아닌 정확히는 파일의 마지막을 의미한다. 데이터를 전송하고, close하게 되면 언젠가 출력스트림의 내용이 모두 빠져나가고 파일의 마지막을 의미하는 'EOF'가 보내지게 된다.

BUF_SIZE만큼 (지속적으로) 전송하고 마지막 write은 BUF_SIZE을 못 채울 수도 있지만 마지막 BUF_SIZE에 대한 read의 block은 해제된다.

또한 EOF가 온 경우에도 read는 해제된다. 따라서 이 둘을 구분하기 위해 read의 return 값이 0일때와 0이 아닐 때를 구분하여 EOF(이때 read의 return값은 0, error일 경우 -1 반환)가 올 때, 모든 write(전송)을 마쳤음을 인식하도록 한다.


- close를 호출한 측에서 buffer를 닫게되고, EOF를 통해 상대에게 출력 스트림을 닫았음을 알리게 된다. 

- close는 소켓의 완전 소멸을 의미한다.

- close를 통해 read / write buffer를 모두 닫게 되면  외부로부터의 데이터는 차단되게 된다. <-- 문제점

메시지를 지속적으로 받거나 두 개의 패킷으로 나눠서 오는 경우는 다른 문제 --> read를 while로 구현


cf) 일반 file descriptor 와 socket file descriptor 는 구분하지 않는다.







half-close

- 이러한 일방적인 연결 종료의 문제점으로 인해 하나의 스트림을 유지시켜놓을 필요성이 있다.

- 종료를 원하다는 것은, 더이상 전송할 데이터가 존재하지 않는 상황

- 다만 송신측에서 상대방도 종료를 원하는 지 확인되지 않은 상황이므로, 입력 스트림은 종료시키지 않을 필요가 있다.

- 때문에 일반적으로 Half-close라 하면, 입력 스트림을 유지한 채, 출력 스트림만 종료하는 것을 의미한다.

- 이 때 shutdown()을 사용한다.

- 데이터를 모두 보내고, 출력스트림(shutdown())을 종료할 경우, 파일 출력 마지막에 EOF를 전송하여 출력스트림을 닫았음을 의도적으로 알릴 수 있다. 

※EOF가 반드시 스트림 종료를 의미하는 것이 아니다. 송신측이 close()나 shutdown()을 호출하지 않으면 어느 스트림도 닫히지 않는다.

- 수신측에선 어차피 상대가 출력스트림을 닫았으므로 출력스트림과 입력스트림 모두 닫기위해 close를 호출한다.

- 송신측에서 EOF를 받게되면 나머지 입력스트림에 대한 half-close를 해준다. * close 또는 shutdown(sd,SHUT_RDWR)해도 무방




shutdown(sock fd, int howto)

성공 시 0, 실패 시 -1반환




close는 아예 연결을 끊는 것

close 호출되면 상대방에게 EOF가 날라감.

EOF가 데이터 전송이 끝났다는 의미.

shutdown에서도 똑같이 EOF를 날려서 한쪽 스트림을 닫을 수 있다.


EOF를 보내고 잘 받았는지 확인하기 위해 상대는 Thank you 메시지를 보내준다.


이번 실습은 서버와 클라이언트 다른 디렉토리 상황에서 실시해보기 175 176

성공하면 receive.dat가 클라이언트 디렉토리에 생성됨

-thank you 확인

-close 로 바꾸고 에러확인


'CS & IT실무' 카테고리의 다른 글

IO 멀티플렉싱  (0) 2018.11.13
네트워크 바이트 순서와 인터넷 주소 변환  (0) 2018.10.04
소켓에 할당되는 IP주소와 PORT번호  (0) 2018.10.04

+ Recent posts