각 함수는 리턴되기 전까지 블로킹된다.
블로킹 소켓 방식으로는 클라이언트가 상대적으로 많을 때 문제가 발생한다. send/recv 함수를 호출할 때 마다 블로킹이 발생하면, 전반적인 서버 작동에 차질을 빚게 된다.
네트워킹 대상(클라이언트) 개수만큼 쓰레드를 만들어 블록된 쓰레드 대신 다른 쓰레드를 처리하여 CPU를 최대한 활용하는 방식을 고안할 수 있다.
하지만 각 네트워크가 차지하는 전체 호출 스택의 크기가 커질 수 있고, 쓰레드 간 컨텍스트 스위치가 대량 발생하여 오버헤드가 커질 수 있다.
대부분 OS에서는 소켓 함수가 블로킹되지 않게 하는 API를 추가로 제공한다. 이를 논블록 소켓이라 한다.
'would bock': 블로킹이 걸렸어야 할 상황인데 블로킹이 걸리지 않았음을 의미
source: https://12bme.tistory.com/231
논블로킹 방식의 소켓도 would block 코드가 발생함을 가정하고 매번 루프를 돌아 CPU 자원을 낭비하는 단점이 있다.
블로킹/논블로킹 방식의 단점을 개선한 다양한 소켓 모델들이 있다.
// 블로킹 방식
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();
}