[소켓 프로그래밍] 논블로킹 소켓

Jin Hur·2022년 6월 25일
0

Server Programming

목록 보기
7/14

블로킹 방식

  • accept: (서버) 접속한 클라가 있을 때 리턴
  • connect: (클라) 서버 접속에 성공했을 때 리턴
  • send: 요청한 데이터를 소켓 송신 버퍼에 복사했을 때 리턴
  • recv: 소켓 수신 버퍼에 도착한 데이터가 있고, 이를 유저레벨 버퍼에 복사했을 때 리턴

각 함수는 리턴되기 전까지 블로킹된다.


네트워크 프로그래밍에서의 고려사항

블로킹 소켓 방식으로는 클라이언트가 상대적으로 많을 때 문제가 발생한다. send/recv 함수를 호출할 때 마다 블로킹이 발생하면, 전반적인 서버 작동에 차질을 빚게 된다.

네트워킹 대상(클라이언트) 개수만큼 쓰레드를 만들어 블록된 쓰레드 대신 다른 쓰레드를 처리하여 CPU를 최대한 활용하는 방식을 고안할 수 있다.
하지만 각 네트워크가 차지하는 전체 호출 스택의 크기가 커질 수 있고, 쓰레드 간 컨텍스트 스위치가 대량 발생하여 오버헤드가 커질 수 있다.

대부분 OS에서는 소켓 함수가 블로킹되지 않게 하는 API를 추가로 제공한다. 이를 논블록 소켓이라 한다.

  1. 소켓을 논블록 소켓으로 모드 전환한다.
  2. 논블록 소켓에 대해 평소처럼 송신, 수신, 연결과 관련된 함수를 호출한다.
  3. 논블록 소켓은 무조건 이 함수 호출에 대해 즉시 리턴한다. 리턴 값은 (1)'성공' 혹은 (2)'would block' 오류 둘 중 하나이다.

'would bock': 블로킹이 걸렸어야 할 상황인데 블로킹이 걸리지 않았음을 의미

source: https://12bme.tistory.com/231

논블로킹 소켓의 장점

  • 쓰레드 블로킹이 없으므로 중도 취소 같은 제어가 가능
  • 쓰레드 개수가 1개이거나 적은 갯수만으로 여러 개의 소켓을 다룰 수있다.
    이에 따라 난무하는 블로킹으로 인한 다수의 컨텍스트 스위치 오버헤드를 줄일 수 있으며, 호출 스택 메모리도 낭비되지 않는다.
    => 예를 들어 하나의 쓰레드에서 여러 개의 소켓을 루프를 돌며 송수신할 수 있다. 이 경우 컨텍스트 스위치가 발생하지 않는다.

논블로킹 소켓의 단점

  • 소켓 I/O 함수가 리턴한 코드가 would block인 경우 재시도 호출 낭비가 발생
    => 시스템 콜이 반복적으로 호출됨.
    => 소켓 I/O 함수를 호출할 때 입력하는 데이터 블록에 대한 복사 연산이 발생
  • connect() 함수는 재시도 호출이 발생하지 않지만, send/receive() 함수는 재시도 호출을 해야 함. => API가 일관되지 않다는 문제

소켓 모델

논블로킹 방식의 소켓도 would block 코드가 발생함을 가정하고 매번 루프를 돌아 CPU 자원을 낭비하는 단점이 있다.
블로킹/논블로킹 방식의 단점을 개선한 다양한 소켓 모델들이 있다.

  • Select 모델
  • WSAEventSelect 모델
  • Overlapped 모델
  • Coompletion Port 모델
    (추후 포스팅)

논블록킹 방식의 송신

// 블로킹 방식
void BlockSocketOperation() {
    s = socket(TCP);
    ...;
    s.connect(...);
    ...;
    while(true) {
        s.send(data);
    }
}

// ------------------------------------

// 논-블로킹 방식
void NonBlockSocketOperation() {
    s = socket(TCP);
    ...;
    s.connect(...);
    // 논블록 소켓으로 변경
    s.SetNonBlocking(true);
    
    while(true) {
    	// 논블로킹 소켓 모드에서 send() 함수는 언제나 즉시 리턴
        r = s.send(dest, data);
        
        if(r == EWOULDBLOCK) {
            // 블로킹 걸릴 상황, 실제로 송신 x
            // ex) 소켓 송신 버퍼가 가득 찬 경우
            // 송신 함수를 다시 호출해야 하는 로직 필요
            continue;
        }
        
        if(r == OK) {
            // 보내기 성공에 대한 처리
        }
        else {
            // 보내기 실패에 대한 처리
        }
    }
}

would block이라는 오류 코드를 만나면 송신 함수 호출을 다시 해주어야 한다.


논블록 수신: 논블록 소켓으로 한 쓰레드 안 여러 소켓 다루기

논블록 소켓을 이용하면 한 쓰레드에서 여러 소켓을 한꺼번에 다룰 수 있다. 루프를 돌면서 소켓 100개에 대한 수신 함수 recv()나 recvfrom()을 호출한다 가정한다. 블록킹 방식이라면 루프를 도는 동안 블로킹, 블로킹, 블로킹..이 난무할 것이고 매우 비효율적인 프로그램이 될 것이다.

이를 논블록 소켓으로 처리하면 블로킹이 난무하는 문제가 사라진다.

블로킹이 난무하다면 문맥교환 오버헤드가 증가할 것이다.
논블로킹 방식의 수신으로 많은 수의 소켓 데이터를 지연 시간 없이 처리할 수 있다.

List<Socket> sockets;

void NonBlockSocketOperation() {
    foreach(s in sockets) {
        // 논블록 수신
        // (오류 코드, 수신된 데이터)
        (result, data) = s.receive();
        
        if(data.length > 0) {
        	// 정상적으로 수신
            print(data);
        }
       	else if(result == EWOULDBLOCK) {
        	// would block 코드를 리턴할 경우
            // 그저 다시 한번 논블로킹 수신 함수를 호출하면 된다.
            continue;
        }
        else if(result != EWOULDBLOCK) {
            // 필요한 에러 처리 진행
        }
    }
}

위와 같은 방식으로 많은 수의 소켓 데이터를 지연 시간 없이 처리할 수 있다. 논블록 수신 함수가 would block 코드를 리턴한 경우에는 잠시 후 다시 논블록 수신 함수를 호출하면 될 뿐이다.


논블로킹 방식의 서버 코드

#include "pch.h"

// 윈도우즈 버전의 소켓 라이브러리
#include <WinSock2.h>
#include <MSWSock.h>
#include <WS2tcpip.h>

// 정적 라이브러리 링크
#pragma comment(lib, "ws2_32.lib")


int main() {
	// WinSock 라이브러리(ws2_32.lib) 초기화
	WSAData wsData;
	if (::WSAStartup(MAKEWORD(2, 2), &wsData) != 0) {
		// 초기화 실패
		return 0;
	}

	// 1. 핸드폰 준비 == 소켓 생성
	//::socket();	// (주소체계, 소켓 타입, 프로토콜)
					// 프로토콜의 경우 0으로 전달하면 알아서 프로토콜을 정해줌
					// TCP/IP 프로토콜 군으로 셋팅될 것
	SOCKET listenSock = ::socket(AF_INET, SOCK_STREAM, 0);
	if (listenSock == INVALID_SOCKET) {
		// 소켓 생성 실패시 에러 메시지 출력
		int32 errCode = ::WSAGetLastError();
		cout << "Socket Err Code: " << errCode << endl;
		return 0;
	}

	// 소켓을 논블로킹 방식으로 전환 ///////////////////////////////
	u_long on = 1;
	if (::ioctlsocket(listenSock, FIONBIO, &on) == INVALID_SOCKET)
		return 0;
	////////////////////////////////////////////////////////////////


	// 2. 식당 번호를 바탕으로 통신 준비 == 서버 주소를 바탕으로 셋팅
	SOCKADDR_IN serverAddr;
	::memset(&serverAddr, 0, sizeof(serverAddr));
	serverAddr.sin_family = AF_INET;
	::inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);
	serverAddr.sin_port = ::htons(7777);

	// bind
	if (::bind(listenSock, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
		int32 errCode = ::WSAGetLastError();
		cout << "Bind Err Code: " << errCode << endl;
		return 0;
	}

	// 3. 영업 시작 == listen
	if (::listen(listenSock, 10) == SOCKET_ERROR) {
		// 백로깅(10): 대기열의 최대 한도
		int32 errCode = ::WSAGetLastError();
		cout << "Listen Err Code: " << errCode << endl;
		return 0;
	}

	// 4. 전화 연결 -> 안내 == accept
	while (true) {
		SOCKADDR_IN clientAddr;;
		::memset(&clientAddr, 0, sizeof(clientAddr));
		int32 clientAddrLen = sizeof(clientAddr);

		// 안내원 -> 대리인 전화기로 연결 토스
		// 클라이언트와 연결된 소켓이란 의미로 "clientSock"
		SOCKET clientSock = ::accept(listenSock, (SOCKADDR*)&clientAddr, &clientAddrLen);
		if (clientSock == INVALID_SOCKET) {
			/*
			int32 errCode = ::WSAGetLastError();
			cout << "Accept Err Code: " << errCode << endl;
			return 0;
			*/
			// => 논블로킹 방식에서는 바로 프로그램을 종료하는 것이 아니다.

			
			// 논블로킹 방식 처리
			int32 errCode = ::WSAGetLastError();
			if (errCode == WSAEWOULDBLOCK)
				continue;
			else
				// error
				return 0;
		}

		// 손님 입장
		char ipAddr[16];
		::inet_ntop(AF_INET, &clientAddr.sin_addr, ipAddr, sizeof(ipAddr));
		cout << "Client Connect IP: " << ipAddr << endl;

		/****************/
		/***** RECV *****/
		/****************/
		while (true) {
			char recvBuffer[1000];
			int32 retDataSize = ::recv(clientSock, recvBuffer, sizeof(recvBuffer), 0);

			if (retDataSize == 0) {
				// 연결 종료
				cout << "FINISH" << endl;
				break;
			}

			// 논블로킹 처리
			if (retDataSize == SOCKET_ERROR) {
				if (::WSAGetLastError() == WSAEWOULDBLOCK)
					continue;
				else
					// error
					return 0;
				
			}

			cout << "Recv Data: " << recvBuffer << endl;
			cout << "Recv Data Len: " << retDataSize << endl;


			/****************/
			/***** SEND *****/
			/****************/
			char ackBuffer[] = "ACK";
			int32 retCode = ::send(clientSock, ackBuffer, sizeof(ackBuffer), 0);
			if (retCode == SOCKET_ERROR) {
				/*
				int32 errCode = ::WSAGetLastError();
				cout << "Send Err Code: " << errCode << endl;
				return 0;
				*/

				// 논블로킹 처리
				if (::WSAGetLastError() == WSAEWOULDBLOCK)
					continue;
				else
					// error
					return 0;
			}
		}

	}


	// 소켓 리소스 반환
	::closesocket(listenSock);
	// 윈속 종료
	::WSACleanup();
}

0개의 댓글