이 게시글은 'TCP/IP 윈도우 소켓 프로그래밍 (김선우 저, 한빛아카데미)'를 공부한 내용을 기반으로 작성됨
#define _WINSOCK_DEPRECATED_NO_WARNINGS // winsock c4996 처리
#include <iostream>
#include <winsock2.h>
#include <thread>
using namespace std;
#pragma comment(lib,"ws2_32.lib") // ws2_32.lib 라이브러리를 링크
void err_quit(LPCTSTR msg)
{
LPVOID lpMsgBuf;
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
NULL, WSAGetLastError(),
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR)&lpMsgBuf, 0, NULL);
MessageBox(NULL, (LPCTSTR)lpMsgBuf, msg, MB_ICONERROR);
LocalFree(lpMsgBuf);
exit(1);
}
int main(int argc, char *argv[])
{
// 윈속 초기화
WSADATA wsa;
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
return 1;
MessageBox(NULL, TEXT("윈속 초기화 성공"), TEXT("알림"), MB_OK);
// socket()
SOCKET tcp_sock = socket(AF_INET, SOCK_STREAM, 0);
if (tcp_sock == INVALID_SOCKET) err_quit(TEXT("socket()"));
MessageBox(NULL, TEXT("TCP 소켓 생성 성공"), TEXT("알림"), MB_OK);
// closesocket()
closesocket(tcp_sock);
WSACleanup();
return 0;
}
위 코드를 봐도봐도 무슨 말인지 모르겠다. 하나하나 해부해보도록 하자.
에러 메시지 출력
가장 무섭게 생긴 부분이다.
void err_quit(LPCTSTR msg)
{
LPVOID lpMsgBuf;
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
NULL, WSAGetLastError(),
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR)&lpMsgBuf, 0, NULL);
MessageBox(NULL, (LPCTSTR)lpMsgBuf, msg, MB_ICONERROR);
LocalFree(lpMsgBuf);
exit(1);
}
사실 이 부분은 '에러 메시지 출력' 소켓을 열고 닫는데 영향을 주지는 않는다.
그치만 나같은 초보자는 한 번쯤 알아보고 넘어가는 게 좋겠다. (나.. 씨언어를 배운지 너무 오래돼서 아무런 기억도 나지 않는 노베다.)
void err_quit(LPCTSTR msg)
➜ 'err_quit' 함수의 선언부
➜ void : 이 함수는 값을 반환하지 않는다. 함수 내부에서 실행만 있다.
➜ LPCTSTR msg : 여기서 LPCTSTR은 'const TCHAR*'의 약자로, 유니코드와 멀티바이트 문자 집합을 모두 지원하는 포인터 타입이라고 한다. (무슨 말인지 모르겠다. 나중에 이해하게 된다면 돌아와서 첨언해야지)
LPVOID lpMsgBuf;
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
NULL, WSAGetLastError(),
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR)&lpMsgBuf, 0, NULL);
MessageBox(NULL, (LPCTSTR)lpMsgBuf, msg, MB_ICONERROR);
LocalFree(lpMsgBuf);
exit(1);
➜ LPVOID : void*, void 포인터를 의미한다. 'LP'는 Windows API에서 사용되는 포인터 타입을 나타내는 접두사로, "Long Pointer"를 의미한다. 또한, void 포인터는 어떠한 타입의 데이터도 가리킬 수 있는 포인터이다.
➜ LPVOID lpMsgBuf; : 'lpMsgBuf'라는 포인터 변수를 선언. 이 변수는 오류 메시지를 저장하기 위한 메모리 버퍼이다.
➜ FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM: 'FormatMessage'라는 함수가 호출되면 '시스템 메시지 가져오기', '메모리 버퍼 자동할당'을 수행하도록 지시하는 플래그
➜ NULL : 메시지 소스는 지정되지 않음. 이 경우 시스템 메시지를 사용
➜ WSAGetLastError() : 가장 최근의 소켓 오류 코드 반환. 이 오류 코드를 이용해서 오류 메시지를 생성
➜ MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT) : 기본 언어 ID 생성. 오류 메시지가 표시될 언어를 결정함.
➜ (LPTSTR)&lpMsgBuf: FormatMessage : 생성된 오류 메시를 'lpMsgBuf'가 가리키는 메모리 버퍼에 저장
➜ 0 : 메시지의 최대 크기를 지정하지 않음. 이 경우에는 FormatMessage가 자동으로 크기를 조정
➜ NULL : 함수에 전달되는 인수 목록이 없음
MessageBox(NULL, (LPCTSTR)lpMsgBuf, msg, MB_ICONERROR);
➜ 팝업 메시지 박스를 표시하는 함수
➜ NULL : 부모 창 핸들을 지정하지 않고, 팝업 창이 독립적으로 표시되도록 함
➜ (LPCTSTR)lpMsgBuf: FormatMessage로 생성된 오류 메시지를 팝업창에 표시
➜ msg : 함수에 전달된 메시지를 팝업 창의 제목으로 사용
➜ MB_ICONERROR : 팝업 창에 오류 아이콘 표시
LocalFree(lpMsgBuf);
➜ 'lpMsgBuf'가 가리키는 메모리 버퍼를 해제함. FormatMessage 함수가 할당한 메모리를 해제하여 메모리 낭비 방지.
exit(1);
➜ 1은 비정상 종료를 나타낸다. 에러가 발생한 상황이므로 1을 반환함으로써 OS에게 오류가 발생했음을 알림
소켓 열고 닫기
int main(int argc, char *argv[])
{
// 윈속 초기화
WSADATA wsa;
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
return 1;
MessageBox(NULL, TEXT("윈속 초기화 성공"), TEXT("알림"), MB_OK);
// socket()
SOCKET tcp_sock = socket(AF_INET, SOCK_STREAM, 0);
if (tcp_sock == INVALID_SOCKET) err_quit(TEXT("socket()"));
MessageBox(NULL, TEXT("TCP 소켓 생성 성공"), TEXT("알림"), MB_OK);
// closesocket()
closesocket(tcp_sock);
WSACleanup();
return 0;
}
➜ WSADATA wsa; : 윈속 API를 사용하기 위해 필요한 데이터를 담는 구조
➜ WSAStartup(MAKEWORD(2, 2), &wsa) : 윈속 초기화. MAKEWORD(2, 2)는 Winsock ver 2.2를 사용하겠다는 의미이다. 윈속 초기화를 성공하면 0을, 실패하면 0이 아닌 값을 반환한다.
➜ if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) : 윈속 초기화가 실패했다면? 밑 줄의 return 1을 이용하여 1을 반환하고 프로그램 종료 (일반적으로 에러가 발생했을 시 1을 반환한다.)
➜ MessageBox(NULL, TEXT("윈속 초기화 성공"), TEXT("알림"), MB_OK); : 윈속 초기화가 성공했음을 사용자에게 알리기 위해 MessageBox를 띄운다. 맨 앞 NULL은 부모창을 지정하지 않고 독립적인 메시지 박스를 표시한다는 의미. 마지막 MB_OK는 확인 버튼만 있는 메시지 박스를 표시한다는 의미이다.
➜ SOCKET tcp_sock = socket(AF_INET, SOCK_STREAM, 0); : TCP 소켓을 생성. AF_INET는 IPv4 주소 체계를 사용한다는 의미이며, SOCK_STREAM은 TCP 프로토콜을 의미. 마지막 0은 기본 프로토콜을 사용 (TCP의 경우 기본 프로토콜은 IPPROTO_TCP)
➜ if (tcp_sock == INVALID_SOCKET) err_quit(TEXT("socket()")); : 소켓 생성이 실패했는지 확인. 실패했다면 'INVALID_SOCKET'을 반환하며 'err_quit' 함수가 호출되며 프로그램 종료
➜ MessageBox(NULL, TEXT("TCP 소켓 생성 성공"), TEXT("알림"), MB_OK); : TCP 소켓이 성공적으로 생성되었음을 MessageBox를 통해 알림
➜ closesocket(tcp_sock); : 생성한 TCP 소켓을 닫는다.
➜ WSACleanup(); : Winsock을 종료한다.
바이트 정렬
바이트 정렬(Byte ordering)이란 메모리에 데이터를 저장할 때 바이트 순서를 나타내는 용어이다.
- 빅 엔디안(big-endian) : 최상위 바이트(MSB, Most Significant Byte)부터 차례로 저장
- 리틀 엔디안(little-endian) : 최하위 바이트(LSB, Least Significant Byte)부터 차례로 저장
위 그림은 MSB와 LSB를 이해하기 위한 예시이다. Decimal number 157을 binary로 표현하면 10011101이 되는데, bit가 왼쪽에 있을수록 significance가 올라가고, 오른쪽으로 갈수록 significance가 낮아진다고 생각하면 된다.
예를 들어 빅 엔디안 방식을 사용하여 0x12345678을 2bytes씩 정렬하여 저장한다면 0x12 / 0x34 / 0x56 / 0x78... 순으로 저장할 것이다.
반면 리틀 엔디안을 사용하여 저장한다면 빅 엔디안과 정확히 반대 순서로 데이터를 저장할 것이므로, 빅 엔디안으로 저장한 파일을 리틀 엔디안 방식으로 읽는 등의 오류가 발생하지 않도록 주의해야 한다.
int main(int argc, char* argv[])
{
WSADATA wsa;
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
return 1;
u_short x1 = 0x1234;
u_long y1 = 0x12345678;
u_short x2;
u_long y2;
// 호스트 바이트 -> 네트워크 바이트
printf("[호스트 바이트 -> 네트워크 바이트]\n");
printf("0x%x -> 0x%x\n", x1, x2 = htons(x1));
printf("0x%x -> 0x%x\n", y1, y2 = htonl(y1));
//네트워크 바이트 -> 호스트 바이트
printf("\n[네트워크 바이트 -> 호스트 바이트]\n");
printf("0x%x -> 0x%x\n", x2, ntohs(x2));
printf("0x%x -> 0x%x\n", y2, ntohl(y2));
// 잘못된 사용 예
printf("\n[잘못된 사용 예]\n");
printf("0x%x -> 0x%x\n", x1, htonl(x1));
WSACleanup();
return 0;
}
u_short x1 = 0x1234; ➜ u_short는 unsigned_short를 의미. 따라서 x1은 2Bytes 부호없는 정수 '0x1234'로 초기화
u_long y1 = 0x12345678; ➜ y1은 4Bytes 부호없는 정수 '0x12345678'
u_short x2; u_long y2; ➜ x2와 y2는 변환된 값을 저장할 변수를 선언합니다.
printf("0x%x -> 0x%x\n", x1, x2 = htons(x1)); ➜ 2Bytes 값을 호스트 바이트 순서에서 네트워크 바이트 순서로 변환. x1의 값을 변환하여 x2에 저장.
printf("0x%x -> 0x%x\n", y1, y2 = htonl(y1)); ➜ 4Bytes 값을 호스트 바이트 순서에서 네트워크 바이트 순서로 변환. y1의 값을 변환하여 y2에 저장.
바이트 순서(Byte Order)
- 호스트 바이트 순서: 컴퓨터의 CPU 아키텍처에 따라 다를 수 있는 바이트 순서. 예를 들어, x86 아키텍처는 리틀 엔디안(Little Endian) 방식을 사용.
- 네트워크 바이트 순서: 인터넷 프로토콜(IP)에서 사용하는 표준 바이트 순서로, 빅 엔디안(Big Endian) 방식을 사용.
htons()란?
- htons: host to network short
- h: Host (호스트, 즉 로컬 머신의 바이트 순서)
- n: Network (네트워크 표준 바이트 순서, 빅 엔디안)
- s: Short (16비트 정수)
- Host → Network이므로 Little Endian → Big Endian
ntohs()란?
- ntohs : network to host short
- Network → Host이므로 Big Endian → Little Endian
x2 = htons(x1) ➜ x1의 값을 htons()로 변환하여 x2에 저장.
x1 = 0x1234;로 저장되어 있으므로 이에 htons를 적용하면 Little Endian → Big Endian으로 데이터 저장 순서를 변환하여 x2 = 0x3412로 저장됨.
'윈도우 소켓 프로그래밍' 카테고리의 다른 글
[소켓프로그래밍] 6. 스레드 동기화 (임계 영역, 이벤트) (0) | 2024.08.26 |
---|---|
[소켓프로그래밍] 5. 스레드, 멀티스레드, 스레드 제어 (0) | 2024.08.25 |
[소켓프로그래밍] 4. 고정길이, 가변길이 데이터 TCP 통신 / 데이터 전송 후 종료 (0) | 2024.08.24 |
[소켓프로그래밍] 3. TCP 서버 클라이언트 통신 (0) | 2024.08.24 |