네트워크

논블로킹 I/O

쥐4 2025. 6. 1. 01:27

1. 논블로킹 I/O 개요

논블로킹 I/O는 기본적으로 클라이언트에서 I/O 시 블로킹을 피하기 위한 기술이다.

즉, I/O 호출 후 바로 return을 통해, 쓰레드의 제어권을 I/O의 범위까지 넘어가지 않게 해준다.

때문에,

**클라이언트-서버 통신에서 논블로킹 I/O는 온전히 클라이언트의 책임이다.**

**서버1-서버2에서의 논블로킹 I/O는 온전히 서버1의 책임이다.**

즉, 위의 블로킹 방식의 네트워크 통신에서 Client 쪽의 로직 변경이 필요하다.

int main(int argc, char *argv[]){
	int fd, rc_gai;
    struct addrinfo ai, *ai_ret;
    if(argc != 3){
    	printf("%s <hostname> <port>\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    
    //1. 서버 측 주소, 소켓 연결 타입 등 설정
   	memset(&ai,0,sizeof(ai));
    ai.ai_family = AF_UNSPEC;
    ai.ai_socktype = SOCK_STREAM;
    ai.ai_flags = AI_ADDRCONFIG;
    if((rc_gai = getaddrinfo(argv[1], argv[2], &ai, &ai_ret)) != 0)){
    	pr_err("Fail: getaddrinfo():%s", gai_strerror(rc_gai));
    }
    
    //2. socket() : 파일 디스크럽터에 소켓이라는 자원에 접근하겠다고 선언
    if((fd = socket(ai_ret->ai_family, ai_ret->ai_socktype, ai_ret->ai_protocol)) == -1){
    	pr_err("[Client] : Fail: socket()");
    }
    
    //3. 소켓을 논블로킹 모드로 전환(파일 디스크럽터 조작)
    if(fcntl(fd, F_SETFL, O_NONBLOCK | fcntl(fd, F_GETFL)) == -1){
    	//err 처리
    }
    
    //4. 연결(3-way-handshake) : (void)형변환으로, 바로 return 받음을 알 수 있음
    (void)connect(fd, ai_ret->ai_addr, ai_ret->ai_addrlen);
    if(errno != EINPROGRESS){
    	//err 처리
    }
    
    //5. fd_set이라는 flag를 나타내는 자료구조에 서버로부터 SYN+ACK을 받았는지 확인
    //select()로 모든 fd에 flag가 변환값이 있는지 확인 변환값있다면, SYN+ACK을 받았다는 뜻
    fd_set fdset_w;
    FD_ZERO(&fdset_w);
    FD_SET(fd, &fdset_w);
    if(select(fd+1, NULL, &fdset_w, NULL, NULL) == -1){
    	//err 처리
    }
    
    //6. 연결 실패, 성공 분기
    int sockopt;
    socklen_t len_sockopt = sizeof(sockopt);
    if(getsockopt(fd, SOL_SOCKET, SO_ERROR, &sockopt, &len_sockopt) == -1){
    	//error 처리
    }
    if(sockopt){
    	pr_err("SO_ERROR: %s(%d)", strerror(sockopt), sockopt);
    }
   //이하 생략(send...등)

로직은 이런 식이다.

 

2. 논블로킹 I/O connect()

    //4. 연결(3-way-handshake) : (void)형변환으로, 바로 return 받음을 알 수 있음
    (void)connect(fd, ai_ret->ai_addr, ai_ret->ai_addrlen);
    if(errno != EINPROGRESS){
    	//err 처리
    }
    
    //5. fd_set이라는 flag를 나타내는 자료구조에 서버로부터 SYN+ACK을 받았는지 확인
    //select()로 모든 fd에 flag가 변환값이 있는지 확인 변환값있다면, SYN+ACK을 받았다는 뜻
    fd_set fdset_w;
    FD_ZERO(&fdset_w);
    FD_SET(fd, &fdset_w);
    if(select(fd+1, NULL, &fdset_w, NULL, NULL) == -1){
    	//err 처리
    }

중요한 부분은 이 부분이다.

connect() 호출 시 void로 return값이 없다.

즉, 3-way-handshake를 완료하고, SYN+ACK을 받을 때까지 기다리지(블록하지)않겠다는 뜻이다.

아래로 내려가 select()를 호출해주면 된다.

2-1. 잘 이해가 가지 않는 select()

블록 I/O에서는 클라이언트가 connect()를 호출하면, 블락을 시켜, 서버로부터의 SYN+ACK를 받음으로 connect()를 정상적으로 마칠 수 있었다.

하지만, 논블록 I/O에서는 connect()를 블록하지 않는데 어떻게 connect()를 정상적으로 마칠까?

 

위에서 보는 코드처럼 Non blocking I/O에서는 connect()는 select()와 쌍을 이루어 진행된다.

그럼 select()가 하는 일이 뭘까?

잘 이해가 가지 않는다. 자세하게 알아보자.

2-2. connect() 이후 select()의 동작

select() 호출 전, fdset_w를 생성/초기화 해준다.

select()를 호출하면, select() 전용 쓰레드가 블록된 채로 기다린다.

즉, 블록의 시점만 다른 것이다.

2-3. 뭔소리야?

즉, connect() 시점에서 보면, non-blocking이라는 뜻이다.

결국, 전체적으로 보면 똑같다는 소리다.

때문에, 이벤트 루프 non-blocking I/O를 봐보자.

(사실, 아직 정확하게 이해하지는 못했다.)

 

3. I/O Multi-plexing