서버/클라이언트 중 하나가 소켓을 close하지 않으면 어떻게 될까?

jinwook han·2022년 6월 6일
0
post-thumbnail

소켓을 close하지 않았을 때, 소켓의 TCP 상태는 무엇으로 변하는지 확인한다.
서버, 클라이언트 코드는 모두를 위한 리눅스 책 코드를 사용했다.
아래 repository의 daytime.c, daytimed.c 파일에서 코드를 볼 수 있다.
https://github.com/Jpub/Linux_for_Everyone

클라이언트

daytime.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>

static int open_connection(char *host, char *service);

int main(int argc, char *argv[]) {
    int sock;
    FILE *f;
    char buf[1024];

    sock = open_connection((argc > 1 ? argv[1] : "localhost"), "daytime"); /* 1 */
    f = fdopen(sock, "r");
    if (!f) {
        perror("fdopen(3)");
	exit(1);
    }
    fgets(buf, sizeof buf, f); /* 5 */
    fclose(f); /* 6 */
    fputs(buf, stdout); /* 7 */
    exit(0);

}

static int open_connection(char *host, char *service) {
    int sock;
    struct addrinfo hints, *res, *ai;
    int err;

    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    if ((err = getaddrinfo(host, service, &hints, &res)) != 0) { /* 2 */
        fprintf(stderr, "getaddrinfo(3): %s\n", gai_strerror(err));
	exit(1);
    }

    for (ai = res; ai; ai = ai -> ai_next) {
        sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); /* 3 */
	if (sock < 0) {
	    continue;
	}
	if (connect(sock, ai -> ai_addr, ai -> ai_addrlen) < 0) { /* 4 */
	    close(sock);
	    continue;
	}
	freeaddrinfo(res);
	return sock;
    }
    fprintf(stderr, "socket(2)/connect(2) failed");
    freeaddrinfo(res);
    exit(1);
}
  1. 인자에 따라 localhost 또는 외부 ip를 넘긴다. 서비스명은 daytime으로 설정해서, port 13번에 대해서 연결할 수 있도록 한다.
  2. host name으로 ip를 조회한다.
  3. 2에서 얻은 정보로 socket 구조체를 생성한다.
  4. 서버와 연결한다.
  5. 서버에서 받은 입력(time 정보)를 버퍼로 옮긴다.
  6. 서버와 커넥션을 끊는다.
  7. 버퍼의 내용을 출력한다.

서버

daytimed.c

#if defined(__digital__) && defined(__unix__)
# ifdef _XOPEN_SOURCE
#  undef _XOPEN_SOURCE
# endif
# define _XOPEN_SOURCE 500
# define _OSF_SOURCE
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <signal.h>

#define DEFAULT_PORT 13

static int listen_socket(int port);

int main(int argc, char *argv[]) {
    struct sockaddr_storage addr;
    socklen_t addrlen = sizeof addr;
    int sock, server;
    time_t t;
    struct tm *tm;
    char *timestr;

    server = listen_socket(argc > 1? atoi(argv[1]) : DEFAULT_PORT); /* 1 */

    for(;;) {
        sock = accept(server, (struct sockaddr*)&addr, &addrlen); /* 6 */

        if (sock < 0) {
            perror("accept(2)");
	    exit(1);
        }

        time(&t);
        tm = localtime(&t);
        timestr = asctime(tm);
        write(sock, timestr, strlen(timestr)); /* 7 */
        close(sock); /* 8 */
    }
    close(server);
    exit(0);
}

static int listen_socket(int port) {
    struct addrinfo hints, *res, *ai;
    int err;
    char service[16];

    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags = AI_PASSIVE;
    snprintf(service, sizeof service, "%d", port);

    if ((err = getaddrinfo(NULL, service, &hints, &res))!= 0) { /* 2 */
	fprintf(stderr, "%s\n", gai_strerror(err));
	exit(1);
    }

    for (ai = res; ai; ai = ai->ai_next) {
	int sock;

	sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); /* 3 */
	if (sock < 0) continue;
	if (bind(sock, ai->ai_addr, ai->ai_addrlen) < 0) { /* 4 */
	    close(sock);
	    continue;
	}
	if (listen(sock, 5) < 0) { /* 5 */
	    close(sock);
	    continue;
	}
	freeaddrinfo(res);
	fprintf(stderr, "listening on port %d...\n", port);
	return sock;
    }
    fprintf(stderr, "cannot listen socket\n");
    exit(1);
}
  1. DEFAULT_PORT:13 또는 인자로 받은 포트로 함수를 호출한다.
  2. 도메인 이름으로 주소 구조체를 얻는다.
  3. 2에서 얻은 주소 구조체로 socket을 생성한다.
  4. bind 호출
  5. listen 호출
  6. 클라이언트와 접속한다.
  7. time 정보를 소켓에 write한다.
  8. write한 후 클라이언트와 연결을 끊는다.

테스트

1. 클라이언트에서 socket을 close 하지 않는다면?

1.. client 코드에서 socket close하는 부분을 제거한다.

fgets(buf, sizeof buf, f);
fclose(f); /* ----> 제거 */
fputs(buf, stdout);
exit(0);
...

2.. 서버를 실행한다.
daytime 서버를 실행한다.

3.. 클라이언트를 실행한다.

클라이언트 프로세스는 서버 응답을 출력하고 종료한다.


4.. 소켓 상태를 확인한다.

클라이언트 → 서버 소켓은 리스트에 나오지 않는다.
서버 → 클라이언트 소켓은 TIME_WAIT 상태가 되었다.
클라이언트 프로세스가 종료하여, close 호출이 없더라도 커넥션이 정상적으로 close된 것 같다.

2. 서버에서 socket을 close 하지 않는다면?

1.. 서버 코드에서 socket close하는 부분을 제거한다.

timestr = asctime(tm);
write(sock, timestr, strlen(timestr));
close(sock); /* -----> 제거 */
...

2.. 서버를 실행한다.

3.. 클라이언트를 실행한다.

4.. 소켓 상태를 확인한다.

client→ server 소켓은 FIN_WAIT_2 상태다.
FIN_WAIT_2: FIN 요청에 대한 ACK 응답은 받았지만, FIN 응답은 받지 못한 상태

server→client 소켓은 CLOSE_WAIT 상태다.
CLOSE_WAIT: active close한 쪽에서의 FIN 응답을 받았고, 그에 대한 ACK를 발송했지만 FIN을 아직 발송하지 못한 상태

3. 클라이언트가 close하지 않고 일정 시간 동안 대기한다면?

server는 close 함수를 호출했는데, client에서는 close를 호출하기 전에 일정 시간 동안 대기한다면 어떻게 되는지 확인한다.

1.. client 코드에 sleep(100)을 추가한다.

fgets(buf, sizeof buf, f);
sleep(100); /* 100초동안 대기 */
fclose(f);
...

2.. 앞선 테스트 케이스의 2와 동일하다.

3.. 클라이언트를 실행한다.

클라이언트는 서버 호출 후 대기중이다.


4.. 소켓 상태를 확인한다.

server→client 소켓은 FIN_WAIT_2 상태다.
client→server 소켓은 CLOSE_WAIT 상태다.
이번에는 server가 active close했고, client가 close에 대한 응답을 하지 못했다.

4. 서버가 close하지 않고 일정 시간 동안 대기한다면?

server에서 close하기 전에 일정 시간동안 대기한다면 어떻게 되는지 확인한다.

1.. server 코드에 sleep(100)을 추가한다.

timestr = asctime(tm);
write(sock, timestr, strlen(timestr));
sleep(100); /* -----> 추가 */
close(sock); 
...
  1. 이전 테스트 케이스 2와 동일하다.
    3.. 이전 테스트 케이스 3과 동일하다.
    4.. 소켓 상태를 확인한다.

client→server 소켓은 FIN_WAIT_2 상태다.
server→client 소켓은 CLOSE_WAIT 상태다.
client가 active close했고, server가 close에 대한 응답을 하지 못했다.

결론

  • passive close 쪽에서 close() 호출을 하지 않으면, active close한 쪽에서는 FIN_WAIT_2 상태가 되고 passive close 쪽에서는 CLOSE_WAIT 상태가 된다.
  • 프로세스가 종료된다면(exit을 호출한다면) close() 호출하지 않더라도 커넥션은 정상적으로 종료된다.

0개의 댓글