II : 윈도우 소켓 시작하기
TCP/IP 윈도우 소켓 프로그래밍 을 읽고 정리한 문서입니다 ;)
오류 처리
네트워크 프로그램에서는 여러 이유로 오류가 발생할 수 있기에, 함수 호출 시 오류를 체크하여 사용자나 개발자가 이에 따른 적합한 행동을 취할 수 있도록 해야 한다. 이에 따른 세 가지 오류 처리 방식이 존재한다.
- 오류를 처리할 필요가 없는 경우
- 함수의 리턴 값이 없거나, 항상 성공하는 일부 소켓 함수
- 리턴 값만으로 오류를 처리하는 경우
WSAStartup()
함수가 이에 해당한다.
- 리턴 값으로 오류 발생 여부만 확인하고, 구체적인 내용은 오류 코드를 받아오는 방식
- 대부분의 소켓 함수가 이 방식을 사용한다.
따라서 소켓에서 발생하는 대부분의 오류는 WSAGetLastError()
함수를 사용하여 오류 코드를 얻을 수 있다.
이 때 FormatMessage()
함수를 사용하면 오류 코드에 대응하는 오류 메시지를 쉽게 얻을 수 있고, 원형은 다음과 같다.
DWORD FormatMessage(
DWORD dwFlags,
// _ALLOCATE_BUFFER 는 오류 메시지를 함수가 알아서 할당한다.
// _FROM_SYSTEM 은 운영체제로부터 오류메시지를 가져온다는 의미이다.
LPCVOID lpSource,
DWORD dwMessageId,
// 오류 코드를 의미하며, WSAGetLastError()를 통해 얻은 값을 넣게 된다.
DWORD dwLanguageId,
// 오류 메시지를 표시할 언어를 의미한다.
// MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT)를 사용하면 제어판 기본 언어를 가져온다.
LPTSTR lpBuffer,
// 이곳으로 오류 메시지의 시작 주소가 저장된다.
// _ALLOCATE_BUFFER를 사용했다면 사용 후 LocalFree()를 호출해야 한다.
DWORD nSize,
va_list *Arguments
);
따라서 이 함수의 실제 소켓 프로그래밍에서의 사용은 다음과 같다.
LPVOID lpMessageBuffer;
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
NULL, WSAGetLastError(),
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR)&lpMessageBuffer, 0, NULL);
_tprintf(_T("%s"), lpMessageBuffer);
LocalFree(lpMessageBuffer);
윈속 초기화와 종료
모든 윈속 프로그램의 공통 구조는, 윈속 초기화 → 소켓 생성 → 네트워크 작업 → 소켓 닫기 → 윈속 종료를 따른다.
윈속 초기화 함수인 WSAStartup()
이 반드시 먼저 호출되어야 하는 이유는, 프로그램에서 사용할 윈속 버전을 요청함으로서 윈속 라이브러리 (WS2_32.DLL)을 초기화하여 메모리에 올리는 역할을 하기 때문이다.
만일 이 때 오류가 발생한다면 메모리에 윈속이 제대로 로드되지 않았으므로 WSAGetLastError
의 오류 메시지는 부정확하기에 직접 WSAStartup
함수가 오류 코드를 리턴하도록 설계되었다.
int WSAStartup(
WORD wVersionRequested,
// 프로그램이 요구하는 최상위 윈속 버전을 넣는다.
LPWSADATA lpWSAData
// WSADATA 구조체를 전달하면 이를 통해 운영체제가 제공하는 윈속 구현 정보를 얻어낸다.
);
프로그램을 종료할 때는 윈속 종료 함수인 WSACleanup()
을 호출해야 한다. 이는 윈속 사용을 중지함을 알리고, 관련 리소스들을 반환하는 역할을 한다.
소켓 생성과 닫기
소켓을 사용해 통신하기 위한 기본 조건은 통신 양단이 같은 프로토콜을 사용하는 것이다.
socket()
함수는 사용자가 요청한 프로토콜을 사용해 통신할 수 있도록 내부적으로 리소스를 할당하고, 이에 접근할 수 있는 핸들 값인 소켓 디스크립터를 리턴한다.
SOCKET socket(
int af, // 주소 체계를 지정한다
int type, // 소켓 타입을 지정한다
int protocol // 사용할 프로토콜을 지정한다
);
통신을 하려면 통신 상대를 유일하게 지정할 수 있는 주소가 필요하기에, 어떠한 주소 지정 방식을 사용할 것인지를 알려야 한다.
IPv4기반 TCP나 UDP 프로토콜을 사용하려면 AF_INET
값을 선택하고, IPv6 기반이라면 AF_INET6
을 선택하는 식이다.
소켓 타입은 사용할 프로토콜의 특성을 나타내는 값으로서, SOCK_STREAM
은 신뢰성 있는 데이터 전송 기능을 제공하는, 연결형 프로토콜이며, SOCK_DGRAM
은 신뢰성 없는 데이터 전송 기능으로서, 비연결형 프로토콜이다.
주소 체계와 소켓 타입의 지정만으로도 프로토콜이 결정되는 경우도, 아닌 경우도 있기에 존재하는 필드로서, IPPROTO_TCP
혹은 IPPROTO_UDP
등의 프로토콜을 설정할 수 있지만, 앞전 두 값으로 이미 TCP 혹은 UDP 가 결정된다면 이 필드에 0을 사용한다.
사용할 프로토콜 | 주소 체계 | 소켓 타입 | 프로토콜 |
---|---|---|---|
TCP | AF_INET or AF_INET6 | SOCK_STREAM | 0 |
UDP | AF_INET or AF_INET6 | SOCK_DGRAM | 0 |
소켓을 사용한 통신을 마치면 관련 리소스를 반환해야 한다.
closesocket()
함수는 해당 소켓을 닫고 관련 리소스를 반환하는 함수이다.
소켓의 초기화부터 생성, 닫기, 반환까지
#pragma comment(lib, "ws2_32")
#include <winsock2.h>
void err_print(LPWSTR message) {
LPVOID messageBuffer;
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
NULL, WSAGetLastError(),
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPWSTR)&messageBuffer, 0, NULL);
);
wprintf(L"[%s] : %s", message, messageBuffer);
LocalFree(messageBuffer);
}
int wmain(int argc, LPWSTR argv[]) {
WSADATA winSockData;
if (WSAStartup(MAKEWORD(2,2), &winSockData) != 0) return 1;
SOCKET tcpSocket = socket(AF_INET, SOCK_STREAM, 0);
if (tcpSocket == INVALID_SOCKET) {
err_print(L"socket()");
return;
}
closesocket(tcpSocket);
WSACleanup();
return 0;
}
요약
- 오류 처리
- 대부분의 소켓 함수는 오류가 발생하면
WSAGetLastError()
함수를 사용해 구체적인 오류 코드를 얻을 수 있다.
- 대부분의 소켓 함수는 오류가 발생하면
- 윈속 초기화와 종료
WSAStartup()
을 사용하여 윈속의 버전 초기화를 거쳐야 메모리에 윈속 DLL이 정상적으로 로드되어 사용 준비가 된다.WSACleanup()
이 초기화된 횟수와 동일하게 불려야 그만큼 시스템에 윈속 리소스를 반환할 수 있다.
- 소켓 생성과 닫기
socket()
함수는 사용자가 요청한 프로토콜을 사용해 통신할 수 있도록 내부적으로 리소스를 만들어 핸들인 소켓 디스크립터를 리턴한다. 인자로는 주소 체계, 소켓 타입, 프로토콜을 전달한다.closesocket()
함수는 해당 소켓을 닫고 관련 리소스를 반환한다.
'공부한 이야기 > 윈도우 소켓 프로그래밍' 카테고리의 다른 글
VI : 멀티스레드 (0) | 2023.04.29 |
---|---|
V : 데이터 전송하기 (0) | 2023.04.29 |
IV : TCP 서버-클라이언트 (0) | 2023.04.29 |
III : 소켓 주소 구조체 다루기 (0) | 2023.04.29 |
I : 네트워크 소켓 프로그래밍 (0) | 2023.04.29 |