이전 포스팅에서 소켓의 블로킹(Blocking)/논블로킹(Non-blocking) 여부를 설정하기 위해 ioctlsocket 함수를 사용하였다.
이번 포스팅에선 간략하게 해당 함수에 대해서 다루고자 한다.
1) ioctlsocket 함수란?
ioctlsocket 함수는 소켓을 제어하기 위해 호출하는 함수이다.
ioctlsocket 함수의 인자는 다음과 같다.
int ioctlsocket(
SOCKET s,
long cmd,
u_long *argp
);
- 1번째 인자(SOCKET s): 제어할 소켓
- 2번째 인자(long cmd): 명령어
- 3번째 인자(u_long *argp): 명령어(2번째 인자)에 대한 매개 변수에 대한 포인터
2) ioctlsocket 명령어
ioctlsocket함수에서 2번째 인자로 오는 명령어에는 여러 종류가 있다.
해당 기능들은 BSD 소켓 기반에서 지원하는 명령 중 몇몇 옵션을 지원한다.
cmd명 | 기능 |
FIONBIO | 논블로킹 여부를 활성화/비활성화 한다. - argp가 0이 아님: 소켓이 논블로킹 모드가 된다. - argp가 0임: 소켓이 블로킹 모드가 된다. |
FIONREAD | 현재 수신 대기중이며 바로 읽을 수 있는 byte수를 가져온다. 보통 recv()함수로 데이터를 읽기 전, 얼마만큼 받아올 수 있는지 체크하는데 쓴다. - argp에 결과값을 저장한다. |
SIOCATMARK | 긴급 데이터(Out-of-band data,OOB)가 포함된 위치를 찾는다. ※ Out-of-band data(OOB): TCP프로토콜에서만 사용. 인터럽트 신호, 중요 공지 등 긴급 신호를 보내기 위해 사용된다. - argp에 결과값을 저장한다. |
3) ioctlsocket을 통해 논블로킹 모드로 변경하기
소켓은 생성하면 기본적으로 블로킹 모드로 설정되어있다. 따라서 ioctlsocket함수를 사용하여 논블로킹 모드를 사용해야한다.
아래는 논블로킹 모드로 변경하는 예시이다.
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
// 논블로킹 모드 설정
u_long nb_mode = 1;
ioctlsocket(sock, FIONBIO, &nb_mode);
여기에서, 논블로킹 모드로 변경하였다면 한가지 주의할 점이 있다.
논블로킹 소켓은 각종 소켓 처리함수(accept, recv등)에서 현재 처리할 작업이 없거나, 즉시 완료할 수 없는 상태면WSAEWOULDBLOCK 오류를 반환한다.
해당 오류는 소켓의 문제되는 오류가 아니므로 예외처리를 진행하며, 실제로 소켓에서 처리할 작업이 올때까지 대기하도록 해야한다.
아래는 간단하게 논블로킹 모드로 설정된 소켓통신을 하는 예시이다.
#include <winsock2.h>
#include <iostream>
#pragma comment(lib, "ws2_32.lib")
int main() {
WSADATA wsa;
WSAStartup(MAKEWORD(2, 2), &wsa);
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
char buf[1024];
// 논블로킹 모드 설정
u_long mode = 1;
ioctlsocket(sock, FIONBIO, &mode);
std::cout << "논블로킹 모드 설정됨" << std::endl;
while(1) {
int recvBytes = recv(sock, buf, sizeof(buf), 0);
if (recvBytes > 0) {
// 소켓을 통해 데이터가 정상적으로 수신되었을때
std::cout << "[수신 데이터]: " << buf << std::endl;
}
else if (recvBytes == 0) {
std::cout << "[소켓 연결 종료됨]" << std::endl;
closesocket(sock);
break;
}
else if (WSAGetLastError() != WSAEWOULDBLOCK) { // 논블로킹의 경우 소켓에 읽은 데이터가 큐에 없으면 반환된다.
if (WSAGetLastError() != WSAECONNRESET) { // 상대방이 먼저 연결을 종료한 경우는 오류 제외
std::cerr << "recv() 오류: " << WSAGetLastError() << std::endl;
break;
}
}
}
closesocket(sock);
WSACleanup();
return 0;
}
논블로킹 모드로 설정된 소켓을 다시 블로킹 모드로 돌려놓고 싶다면 아래와 같이 설정하면 된다.
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
// 논블로킹 모드 설정
u_long nb_mode = 1;
ioctlsocket(sock, FIONBIO, &nb_mode);
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
// ★추가 - 블로킹 모드로 돌아오기
u_long b_mode = 0;
ioctlsocket(sock, FIONBIO, &b_mode);
※ 비교
블로킹 모드(arg: 0) | 논블로킹 모드(arg: 1) | |
accept() | 클라이언트 연결이 있을 때까지 대기 | 연결이 없으면 즉시 반환 |
recv() | 수신데이터가 있을때까지 대기 | 데이터가 없으면 즉시 반환 (WSAEWOULDBLOCK 발생) |
send() | 버퍼가 가득 차면 대기 | 버퍼가 가득 차면 즉시 반환 (WSAEWOULDBLOCK 발생) |
블로킹 상태일 경우 무한대기를 하지만, 논블로킹의 경우 현재 작업을 처리할 수 없다면
곧바로 반환을 하여 복수의 클라이언트에게 네트워크 관련 처리를 하거나, 비동기 처리를 할 때 유용하게 사용할 수 있다.
4) ioctlsocket을 통해 수신 대기중인 데이터 양 확인
FIONREAD 명령을 통하여 수신 대기중인 데이터의 크기를 확인할 수 있다.
아래는 FIONREAD 커맨드를 사용하는 예시이다.
만일 수신 대기중인 데이터에 따라 버퍼의 동적할당이 필요하다면 유용하다.
u_long r_size = 0;
ioctlsocket(clntSocket, FIONREAD, &r_size);
printf("수신대기중인 byte : %ld\n", r_size);
해당 함수를 통해 동적으로 데이터 버퍼를 할당할 수 있다.
아래는 예시 코드이다
// (소켓 정의 및 연결 부분은 생략)
// 1. FIONREAD로 수신 대기 중인 데이터 크기 확인
u_long dataSize = 0;
if (ioctlsocket(clientSocket, FIONREAD, &dataSize) != 0) {
std::cerr << "ioctlsocket(FIONREAD) 실패: " << WSAGetLastError() << std::endl;
// 실패에 따른 작업 추가
return 1;
}
if (dataSize > 0) {
// 2. 수신 대기 중인 데이터만큼 버퍼 동적 할당
char* buffer = new char[dataSize + 1];
memset(buffer, 0, dataSize + 1); // 버퍼 초기화
// 3. 수신
int bytesReceived = recv(clientSocket, buffer, dataSize, 0);
if (bytesReceived > 0) {
buffer[bytesReceived] = '\0'; // 널 문자 추가
std::cout << "수신된 데이터: " << buffer << std::endl;
} else if (bytesReceived == 0) {
std::cout << "클라이언트 연결 종료!" << std::endl;
} else {
std::cerr << "recv() 실패: " << WSAGetLastError() << std::endl;
}
// 4. 동적 할당 해제
delete[] buffer;
} else {
std::cout << "수신 대기 중인 데이터가 없습니다." << std::endl;
}
5) 기타
ioctlSocket 함수는 BSD소켓의 일부 기능만 제공한다.
예를들면 FIONREAD는 제공하지만, 송신가능한 데이터를 확인하는 FIONSEND는 제공하지 않는다.
더 많은 기능들이 들어간 소켓제어 함수를 Windows에서 사용한다면 WSAIoctl함수를 사용해보자.
6) Non-blocking EchoServer 만들기
이전 포스팅에서 기록하였던 블로킹 모드의 echoServer 코드를 개선하여, 논블로킹 모드로 echoServer를 작성하였다.
EchoServer.cpp
#pragma comment(lib, "ws2_32")
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <iostream>
#include <stdio.h>
#include <winsock2.h>
#include <memory>
#include <vector>
#define BUF_SIZE 1024
using namespace std;
int main()
{
printf("Server Start!\n");
// 1) WinSock 라이브러리를 초기화한다
WSAData wsaData; //WinSock 초기화 구조체
int initWinSockRes = WSAStartup(MAKEWORD(2, 0), &wsaData);
if (initWinSockRes != 0) {
printf("윈도우소켓 초기화 실패");
return 0;
}
// 2) 서버와 클라이언트 소켓을 생성하고 서버소켓을 bind, listen한다.
SOCKET servSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in servAddr, clntAddr;
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
servAddr.sin_port = htons(11000);
if (bind(servSocket, (sockaddr*)&servAddr, sizeof(servAddr)) != 0) {
printf("bind() 오류 [오류코드: %d]\n", WSAGetLastError());
return 0;
}
// 크기 3의 대기열을 만든다.
// 클라이언트의 연결요청이 왔을때 서버에서 준비된 소켓이 없다면 대기열에서 연결을 기다리게된다.
if (listen(servSocket, 3) != 0) {
printf("listen() 오류 [오류코드: %d]\n", WSAGetLastError());
return 0;
}
char buf[BUF_SIZE] = {};
int clntAddrLen = sizeof(clntAddr);
int socketCnt = 0;
std::vector<SOCKET> clntSockets = vector<SOCKET>(0);
// ★ 추가: 서버 소켓을 논블로킹으로 한다.
u_long mode = 1;
ioctlsocket(servSocket, FIONBIO, &mode);
while (1) {
// 3) 클라이언트가 연결을 요청하면 수락한다.
SOCKET clntSocket = accept(servSocket, (sockaddr*)&clntAddr, &clntAddrLen);
if (clntSocket == INVALID_SOCKET) {
if (WSAGetLastError() != WSAEWOULDBLOCK) {
printf("accept() 오류 [오류 코드: %d]\n", WSAGetLastError());
return 0;
}
}
else {
// ★ 추가: 클라이언트 소켓을 논블로킹으로 한다.
ioctlsocket(clntSocket, FIONBIO, &mode);
clntSockets.push_back(clntSocket);
printf("accept client\n");
}
// 연결된 소켓들을 순차적으로 처리한다.
for (auto it = clntSockets.begin(); it != clntSockets.end();) {
SOCKET clntSocket = *it;
memset(buf, 0, sizeof(buf));
// ★ 추가: 수신 대기중인 데이터를 확인해본다.
u_long r_size = 0;
ioctlsocket(clntSocket, FIONREAD, &r_size);
if (r_size > 0) { // 읽어들일 데이터가 없을땐 0으로 반환한다. (출력이 많아지는것을 방지하기위해 0 이상일때만 출력하도록 함)
printf("readablie size : %ld\n", r_size);
}
int recvBytes = recv(clntSocket, buf, r_size, 0);
if (recvBytes > 0) {
// 4) 클라이언트로부터 온 메세지를 받는다.
std::cout << "[ 클라이언트 " << clntSocket << "] 수신: " << buf << std::endl;
// 5) 클라이언트로부터 온 메세지를 그대로 클라이언트에게 전송한다.
send(clntSocket, buf, recvBytes, 0);
}
else if (recvBytes == 0) {
std::cout << "[ 클라이언트 (소켓: " << clntSocket << ") 연결 종료 ]" << std::endl;
closesocket(clntSocket);
it = clntSockets.erase(it);
continue;
}
else if (WSAGetLastError() != WSAEWOULDBLOCK) { // 논블로킹의 경우 소켓에 읽은 데이터가 큐에 없으면 반환된다.
if (WSAGetLastError() != WSAECONNRESET) { // 클라이언트가 먼저 연결을 종료한 경우는 오류 제외
std::cerr << "recv() 오류: " << WSAGetLastError() << std::endl;
}
}
++it;
}
// TODO: 서버에서 수행할 다른 작업들을 실행할 수 있다.
}
WSACleanup();
return 0;
}
EchoClient.cpp
#pragma comment(lib, "ws2_32")
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <string>
#include <iostream>
#include <winsock2.h>
#include <memory>
#define BUF_SIZE 1024
using namespace std;
int main() {
WSAData wsaData;
SOCKET clnt_socket;
std::cout << "Client Start!\n";
// WINSOCK 초기화
int initWinSockRes = WSAStartup(MAKEWORD(2, 0), &wsaData);
if (initWinSockRes == SOCKET_ERROR) {
return 0;
}
clnt_socket = socket(AF_INET, SOCK_STREAM, 0);
if (clnt_socket == INVALID_SOCKET) {
printf("윈도우소켓 초기화 실패");
return 0;
}
sockaddr_in servAddr;
// 로컬 주소로 연결한다. 포트는 서버에서 bind한 포트와 동일해야, 서버로 연결이 가능하다
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
servAddr.sin_port = htons(11000);
if (connect(clnt_socket, (sockaddr*)& servAddr, sizeof(servAddr)) != 0) {
printf("connect() 오류 [오류 코드: %d\]n", WSAGetLastError());
return 0;
}
char received[BUF_SIZE];
srand((int)time(nullptr));
int randVal = rand()%1000;
int seq = 1;
while (1) {
memset(received, 0, BUF_SIZE);
string input;
cin >> input;
if (send(clnt_socket, input.c_str(), input.length(), 0) == SOCKET_ERROR) {
printf("send() 오류 [오류 코드: %d]\n", WSAGetLastError());
break;
}
printf("send작업 성공\n");
if (recv(clnt_socket, received, BUF_SIZE, 0) == SOCKET_ERROR) {
if (WSAGetLastError() != WSAEWOULDBLOCK) {
printf("recv() 오류 [오류 코드: %d]\n", WSAGetLastError());
return 0;
}
}
}
return 0;
}
[실행결과]
서버 실행결과 | 클라이언트 결과 |
![]() |
![]() |
만일 서버 소켓의 논블로킹 모드를 설정하는 코드를 지운다면, accept에서 연결이 올때까지 대기하는 것을 확인할 수 있을 것이다.
또한 클라이언트에서 보내주는 데이터만큼 FIONREAD명령어를 통해 byte수를 불러오는것을 확인할 수 있다.
'Windows Socket' 카테고리의 다른 글
블로킹vs논블로킹 모델 / 동기vs비동기형 모델 (2) (0) | 2022.11.04 |
---|---|
블로킹vs논블로킹 모델 / 동기vs비동기형 모델 (1) (0) | 2022.07.01 |
Windows TCP서버 소켓 통신- Select서버 (0) | 2022.02.17 |
다중 접속 서버 모델 (0) | 2021.11.16 |
Windows TCP서버 소켓 통신 - echoServer 구현하기 (0) | 2021.04.13 |