아래와 같은 non-blocking socket 서버를 간단하게 나마 제작(말이 non-blocking이지 어차피 read에서 block되기 때문에 non-blocking이라고 하기에 상당히 민망함)
#include <arpa/inet.h>
#include <fcntl.h>
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <string>
int main(int argc, char** argv) {
if(argc != 2) {
std::cout << "USAGE : ./a.out <port>" << std::endl;
exit(1);
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(struct sockaddr_in));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(atoi(argv[1]));
int sock_fd = ::socket(PF_INET, SOCK_STREAM, 0);
int option = 1;
/* setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &option, sizeof(option));*/
::bind(sock_fd, (sockaddr*)&addr, sizeof(sockaddr_in));
::listen(sock_fd, 5);
int val = fcntl(sock_fd, F_GETFL, 0);
if(val < 0) {
std::cout << "fcntl error(F_GETFL)" << std::endl;
exit(1);
}
val |= O_NONBLOCK;
if(fcntl(sock_fd, F_SETFL, val) < 0) {
std::cout << "fcntl error(F_SETFL)" << std::endl;
exit(1);
}
while(1) {
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_len = sizeof(sockaddr_in);
int clnt_fd = ::accept(sock_fd, (sockaddr*)&clnt_addr, &clnt_addr_len);
if(clnt_fd > 0) {
std::cout << "accept ok : " << clnt_fd << std::endl;
char buf[8192];
::read(clnt_fd, buf, 8191);
::write(clnt_fd, buf, strlen(buf));
::close(clnt_fd);
}
}
::close(sock_fd);
return 0;
}
#include <fcntl.h>
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <string>
int main(int argc, char** argv) {
if(argc != 2) {
std::cout << "USAGE : ./a.out <port>" << std::endl;
exit(1);
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(struct sockaddr_in));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(atoi(argv[1]));
int sock_fd = ::socket(PF_INET, SOCK_STREAM, 0);
int option = 1;
/* setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &option, sizeof(option));*/
::bind(sock_fd, (sockaddr*)&addr, sizeof(sockaddr_in));
::listen(sock_fd, 5);
int val = fcntl(sock_fd, F_GETFL, 0);
if(val < 0) {
std::cout << "fcntl error(F_GETFL)" << std::endl;
exit(1);
}
val |= O_NONBLOCK;
if(fcntl(sock_fd, F_SETFL, val) < 0) {
std::cout << "fcntl error(F_SETFL)" << std::endl;
exit(1);
}
while(1) {
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_len = sizeof(sockaddr_in);
int clnt_fd = ::accept(sock_fd, (sockaddr*)&clnt_addr, &clnt_addr_len);
if(clnt_fd > 0) {
std::cout << "accept ok : " << clnt_fd << std::endl;
char buf[8192];
::read(clnt_fd, buf, 8191);
::write(clnt_fd, buf, strlen(buf));
::close(clnt_fd);
}
}
::close(sock_fd);
return 0;
}
위의 서버를 작성하여 띄워 놓고, telnet command를 통해 접근. 처음에는 정상적인 에코서버 처럼 동작하였으나 서버를 한번 내리고 다시 올리면 TIME_WAIT 상태에 빠져서 모든 연결 요청들을 refuse 시킴.
성질이 나서 미친듯이 connection 요청을 하는 클라이언트를 만들어 서버를 두들겨 보았으나 오히려 TIME_WAIT 시간만 늘리는 역효과를 냄.
왜 그럴까?
원인 :
TCP에서 연결을 close하기 위해서는 four-way handshaking이라는 짓을 한다(참고로 연결을 만들기 위해서는 three way handshanking을 한다).
이게 어떤 짓이냐 하면, 두개의 컴퓨터가 연결을 끊으려 한다고 생각하자. A가 B에게 "이제 그만 하자!!"라고 외친다. 이것이 FIN이다. 그럼 B는 A에게 "Ok, 그런데 잠시만 기다려 봐봐"라고 하며 그냥 FIN을 잘 받았다는 ACK를 날린다. FIN을 날리지 않았다는 것을 유의하자. 그리고 잠시 후 이제야 B는 다시 A에게 FIN을 날리게 된다. 이렇게 A는 FIN을 받고 나서야 최종적인 ACK를 날리게 되고. 연결을 종료된다.
하지만 여기서 바로 사용되었던 주소와 포트가 반환 되느냐..하면 그것은 또 아니다. 결국 위의 예제에서 최초 서버를 실행시키고 난후 바로 다시 서버를 실행 시키고, 기존의 클라이언트(아마도 file destriptor가 같은)에서 패킷을 수신하게 되면 클라이언트가 아직 종료 요청을 받지 못한것으로 간주하고 TIME_WAIT 시간을 다시 연장하고 종료 메시지를 보낸다. 결코 연결 될 수 없는 수렁에 빠지게 된다.
해결 :
socket option에 SO_REUSEADDR의 상태를 변경해 주도록 한다. SO_REUSEADDR의 상태를 TRUE(1)로 변경하게 되면 TIME_WAIT 상태에 있는 소켓에 할당된 IP주소와 포트를 새로 시작하는 소켓에 할당 해 줄 수 있게 된다.
위의 예제에서 굵게 주석 처리가 된 부분을 살펴 보도록 하자.
참고 :
2007/01/31 - [진리는어디에] - 소켓 종료와 TIME_WAIT(Socket termination and TIME_WAIT)
열혈강의 TCP/IP 소켓 프로그래밍 : 윤성우
Unix Network Programming : Stevens