[TCP/IP 소켓 프로그래밍] (2) 소켓 프로토콜과 데이터 전송 특성
출처 : 열혈 TCP/IP 소켓 프로그래밍 윤성우 저
윈도우 기반의 소켓 프로그래밍입니다.
각 소켓 생성 과정에 필요한 정보에 대해 정리합니다.
프로토콜 이란
네트워크 통신을 위한 상호간의 규약
1. 소켓 생성
SOCKET socket(int af, int type, int protocol);
- af: 프로토콜 체계 (Protocol Family)
- type: 소켓 데이터 전송 방식
- protocol: 프로토콜
- 첫번째 인자: 프로토콜 체계
- PF_INET: IPv4 인터넷 프로토콜
- PF_INET6: IPv6 인터넷 프로토콜
- PF_LOCAL: 로컬 통신을 위한 UNIX 프로토콜
- PF_PACAKET: Low Level 소켓을 위한 프로토콜
- PF_IPX: IPX 노벨 프로토콜
- 두번째 인자: 타입
TCP (SOCK_STREAM)와 UDP (SOCK_DGRAM)
TCP
- 연결지향형 - 연결 확립시 통신 시작
- 순차적인 데이터 패킷을 전송
- 오류에 의한 재전송으로 신뢰성 있는 연결
- 소켓 수신 버퍼에 대한 유연성으로 전송하는 데이터 양에 대한 경계가 없음 : 신뢰성 있는 전송(흐름 제어)으로 남아있는 수신 버퍼를 초과하는 데이터가 전송되지 않음.
UDP
- 비 연결지향형
- 전송 순서가 없는 가장 빠른 전송
- 데이터 손실이 가능함
- 한 번에 전송 가능한 데이터 크기가 제한됨. UDP에 그 데이터 길이가 기록됨.
- 세번째 인자: 프로토콜
위 프로토콜 체계와 타입을 만족하는 프로토콜 중 하나를 선택한다.
- TCP : PF_INET, SOCK_STREAM, IPPROTO_TCP
- UDP : PF_INET, SOCK_STREAM, IPPROTO_UDP
소켓 생성 예시)
SOCKET tcpSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
SOCKET udpSocket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
2. 소켓에 IP 주소와 PORT 번호를 할당
네트워크 주소의 결정 IPv4 vs IPv6
- IPv4: 4바이트 주소 체계를 가진 IP 주소
- IPv6: 16바이트 주소 체계를 가진 IP 주소
- IPv4 주소를 표현하기 위한 구조체 SOCKADDR_IN
typedef struct sockaddr_in {
#if(_WIN32_WINNT < 0x0600)
short sin_family;
#else //(_WIN32_WINNT < 0x0600)
ADDRESS_FAMILY sin_family;
#endif //(_WIN32_WINNT < 0x0600)
USHORT sin_port;
IN_ADDR sin_addr;
CHAR sin_zero[8];
} SOCKADDR_IN, *PSOCKADDR_IN;
sin_family: 16비트(2) 주소 체계
- AF_INET: IPv4 인터넷 프로토콜에 적용되는 주소체계
- AF_INET6: IPv6 인터넷 프로토콜에 적용되는 주소체계
- AF_LOCAL: 로컬 통신을 위한 유닉스 프로토콜의 주소체계
sin_port: 16비트(2) TCP/UDP 포트 번호
네트워크 바이트 순서로 지정된 포트 번호
sin_addr: 32비트(4) IP 주소
네트워크 바이트 순서로 지정된 32비트 IP 주소. 부호없는 정수로 정의되어 있다.
sin_zero[8]: 사용되지 않음
일반적인 주소를 표현하는 구조체 SOCKADDR과 호환하여 사용할 수 있도록 사이즈를 일치시키기 위해 0으로 패딩해준다. SOCKADDR은 16바이트를 사용하고 이와 일치시키기 위해 8바이트를 제로 패딩한다.
※ 네트워크 바이트 순서
CPU 마다 다른 데이터 저장 방식으로 송수신된 데이터가 다르게 해석될 수 있다. 이에 대해 데이터 정렬 기준을 빅엔디안으로 통일하여 송수신하도록 한다.
CPU가 데이터를 메모리에 저장하는 방식이 다를 수 있다. (인텔 CPU: 리틀 엔디안 방식)
빅 엔디안 : 상위 바이트의 값(MSB)부터 낮은 번지수에 표현한다.
0x12345678 --낮은 주소-- (0x12) (0x34) (0x56) (0x78) --높은 주소--
리틀 엔디안 : 하위 바이트 값(LSB)부터 낮은 번지수에 표현한다.
0x12345678 --낮은 주소-- (0x78) (0x56) (0x34) (0x12) --높은 주소--
- 바이트 순서 변환 관련 함수
unsigned short ntohs(unsigned short); // short 형 데이터를 network 표현에서 host 표현으로 변환
unsigned long ntohl(unsigned long); // long 형 데이터를 network 표현에서 host 표현으로 변환
unsigned long htonl(unsigned long); // long 형 데이터를 host 표현에서 network 표현으로 변환
short/long형 데이터를 host to newtork 혹은 그 반대로 변환한 값을 반환
SOCKADDR
대부분의 IP 주소 정보를 담을 수 있지만 sa_data에 IP 주소와 PORT 번호를 같이 담고 나머지는 제로 패딩을 해줘야하는 불편한 인터페이스를 지니고 있다.
typedef struct sockaddr {
#if (_WIN32_WINNT < 0x0600)
u_short sa_family;
#else
ADDRESS_FAMILY sa_family; // Address family.
#endif //(_WIN32_WINNT < 0x0600)
CHAR sa_data[14]; // Up to 14 bytes of direct address.
} SOCKADDR, *PSOCKADDR, FAR *LPSOCKADDR;
- 문자열을 네트워크 바이트로 변환
"1.2.3.4" - > 0x40302010
unsigned long inet_addr(const char* string);
- 네트워크 바이트를 문자열로 변환
char* inet_ntoa(struct in_addr adr);
- 반환된 포인터는 함수 내부 문자열을 가리키고 있으므로 반드시 복사해서 사용
문자열을 활용한 소켓에 IP주소 및 포트번호 할당하는 예제
SOCKADDR_IN servAddr = {};
const char* server_ip = "192.231.10.1";
const char* serv_port = "9190";
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = inet_addr(serv_port);
servAddr.sin_port = htons(atoi(serv_port));
bind(servSock, (SOCKADDR*)&servAddr, sizeof(servAddr));
INADDR_ANY
만약 컴퓨터가 단일 IP 주소(랜카드가 하나)를 사용한다면, IP주소에 상관없이(자동으로 설정) 포트번호만 일치하면 데이터를 수신하도록 할 수 있다. 랜카드가 하나 이상인 컴퓨터에서는 어떤 IP 주소로 수신할 것이지 주소를 초기화 할 때 명시해야한다.
SOCKADDR_IN servAddr = {};
// const char* server_ip = "192.231.10.1"; 생략
const char* serv_port = "9190";
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = inet_addr(INADDR_ANY);
servAddr.sin_port = htons(atoi(serv_port));
윈속에서 사용가능한 주소-문자열 변환 함수
IPv4, IPv6 모두 변환 가능하다.
WSAStringToAddress() / WSAAddressToString()
INT
WSAAPI
WSAStringToAddressW(
_In_ LPWSTR AddressString,
_In_ INT AddressFamily,
_In_opt_ LPWSAPROTOCOL_INFOW lpProtocolInfo,
_Out_writes_bytes_to_(*lpAddressLength,*lpAddressLength) LPSOCKADDR lpAddress,
_Inout_ LPINT lpAddressLength
);
참고: 윤성우 저 TCP/IP 소켓 프로그래밍