Computer Science 기본 지식/소켓 프로그래밍

[TCP/IP 소켓 프로그래밍] (2) 소켓 프로토콜과 데이터 전송 특성

로파이 2021. 3. 26. 18:02

출처 : 열혈 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 소켓 프로그래밍