모든 내용은 [윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어] 를 기반으로 제 나름대로 이해하여 정리한 것입니다. 다소 부정확한 내용이 있을수도 있으니 이를 유념하고 봐주세요!
# IP(Internet Protocol)이란?
- 인터넷상에서 데이터를 송수신할 목적으로 "컴퓨터에게 부여하는 값"
# PORT 번호란?
- 컴퓨터에게 부여하는 값이 아닌, 프로그램 상에서 생성되는 소켓을 구분하기 위해 "소켓에 부여되는 번호"
# 인터넷에 컴퓨터를 연결해서 데이터를 주고받기 위해서는 IP주소를 부여 받아야 하며,
이러한 IP주소 체계는 두 종류로 나뉨
1) IPv4(Internet Protocol version 4) : 4바이트 주소체계
2) IPv6(Internet Protocol version 6) : 16바이트 주소체계
# IPv4 주소체계(총 4바이트)
- A 클래스 : 1바이트(네트워크ID) + 3바이트(호스트ID)
- B 클래스 : 2바이트(네트워크ID) + 2바이트(호스트ID)
- C 클래스 : 3바이트(네트워크ID) + 1바이트(호스트ID)
- D 클래스 : 4바이트(멀티캐스트 IP주소)
- 네트워크 ID : 네트워크 구분을 위한 IP주소, 이것을 참조하여 "해당 네트워크로" 데이터를 전송함
- 호스트 ID : 특정 네트워크 내의 컴퓨터(호스트) 구분을 위한 IP주소,
해당 네트워크(네트워크를 구성하는 라우터 또는 스위치)는 전송된 데이터의 호스트 ID를 참조하여
"해당 컴퓨터로" 데이터를 전송함
# 네트워크를 구성하려면 데이터를 내외부로 송수신해주는 물리적 장치가 필요
=> 라우터 혹은 스위치(그냥 컴퓨터이지만, 특수한 목적을 가지고 설계 및 운영되는 컴퓨터)
# 클래스 별 네트워크 주소와 호스트 주소의 경계
- 클래스 A의 첫번째 바이트 범위 : 0 - 127 => 상위 1비트가 항상 0
- 클래스 B의 첫번째 바이트 범위 : 128 - 191 => 상위 2비트가 항상 10
- 클래스 C의 첫번째 바이트 범위 : 192 - 223 => 상위 3비트가 항상 110
# "소켓의 구분"에 활용되는 PORT 번호
- IP는 네트워크와 컴퓨터(호스트)를 구분하기 위한 목적으로 존재 => IP가 있으면 목적지 컴퓨터로
데이터 전송은 가능하나, 최종목적지인 응용프로그램에까지는 전송이 불가능
- 응용프로그램에는 데이터 송수신을 위한 서로 다른 PORT 번호의 소켓이 존재할 것임
- NIC(네트워크 인터페이스 카드, 랜카드)에는 데이터 송수신 장치가 하나씩 있는데,
IP는 데이터를 NIC를 통해 컴퓨터 내부로 전송하는데 사용됨
- 컴퓨터 내부로 전송된 데이터를 소켓에 적절히 분배하는 작업은 운영체제가 담당하는데,
이때 운영체제는 PORT 번호를 활용.
- 즉, NIC를 통해서 수신된 데이터 안에는 PORT번호가 존재하는데,
운영체제가 이 정보를 참조해서 일치하는 PORT 번호의 소켓에 데이터를 전달
# PORT 번호는 16비트(2바이트)로 표현됨 => 0 - 65535
- 0 - 1023까지는 "Well-known PORT"로 특정 프로그램에 할당되기로 예약되어 있어,
이 값을 제외한 다른 값을 PORT 번호로 할당해야 함
# PORT 번호는 중복이 불가능하나, TCP소켓과 UDP소켓은 PORT 번호를 공유하지 않기 때문에 중복이 가능
# 데이터 전송의 목적지 주소에는 IP주소뿐만 아니라, PORT 번호도 포함됨.
그래야 최종 목적지인 응용프로그램의 소켓까지 데이터를 전달할 수 있기 때문
# IPv4 기반의 주소표현을 위한 구조체
struct sockaddr_in
{
sa_family_t sin_family; // 주소체계(Address Family)
// (IPv4 전용 구조체라고 했는데) 굳이 별도로 저장하는 이유는..?
// (struct sockaddr *)&sockaddr_in 형태로 형변환을 하여 주소정보를 전달하게 되는데
// sockaddr은 IPv4의 주소 정보만을 담기 위해 정의된 구조체가 아니기 때문
uint16_t sin_port; // 16비트(2바이트) TCP/UDP PORT 번호 : "네트워크 바이트 순서"로 저장해야 함
struct in_addr sin_addr; // 32비트(4바이트) IP주소 : "네트워크 바이트 순서"로 저장해야 함
char sin_zero[8]; // 실질적으로 사용되지 않음(단, 반드시 0으로 채워야 함)
};
struct in_addr
{
in_addr_t s_addr; // 32비트(4바이트) IPv4 인터넷 주소
};
# POSIX(Portable Operating System Interface)
- 유닉스 계열의 운영체제에 적용하기 위한 표준
- POXIS에서는 다음과 같이 추가로 자료형을 정의하고 있음
- 확장성을 확보할 수 있음
자료형 이름 | 자료형에 담길 정보 | 선언된 헤더파일 |
int8_t uint8_t int16_t uint16_t int32_t uint32_t |
signed 8-bit int unsigned 8-bit int signed 16-bit int unsigned 16-bit int signed 32-bit int unsigned 32-bit int |
sys/type.h |
sa_family_t socklen_t |
주소체계(address family) 길이정보(length of struct) |
sys/socket.h |
in_addr_t in_port_t |
IP 주소정보, uint32_t로 정의 PORT 번호정보, uint16_6로 정의 |
neinet/in.h |
# 구조체 sockaddr_in의 멤버에 대한 분석
1) 멤버 sin_famliy
- 프로토콜 체계마다 적용하는 주소체계가 다름
- sin_family에 적용할 주소체계 정보를 저장해야함
주소체계(Address Family) | 의 미 |
AF_INET AF_INET6 AF_LOCAL |
IPv4 인터넷 프로토콜에 적용하는 주소체계 IPv6 인터넷 프로토콜에 적용하는 주소체계 로컬 통신을 위한 유닉스 프로토콜의 주소체계 |
2) 멤버 sin_port
- 16비트 PORT 번호를 저장
- 단, "네트워크 바이트 순서"로 저장
3) 멤버 sin_addr
- 32비트 IP 주소정보를 저장
- 단, "네트워크 바이트 순서"로 저장
4) 멤버 sin_zero
- 특별한 의미를 지니지 않음
- 단순히 구조체 sockaddr_in의 크기를 구조체 sockaddr와 일치시키기 위해 삽입된 멤버, 반드시 0으로 채워야 함!
struct sockaddr
{
sa_family_t sin_family; // 주소체계(Address Family)
char sa_data[14]; // 주소정보
// IP주소와 PORT 번호를 포함하고, 남은 부분은 0으로 채울 것을 bind 함수가 요구
};
- bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) // serv_addr은 struct sockaddr_in의 변수
과 같은 형식으로 형변환을 하면, bind 함수가 요구하는 바이트대로 데이터를 채워넣을 수 있음
- struct sockaddr_in은 IPv4의 주소를 담기위해 정의된 구조체이나,
struct sockaddr은 IPv4의 주소정보만을 담기위해 정의된 구조체가 아님
# 바이트 순서(Order)와 네트워크 바이트 순서
- CPU에 따라 데이터를 메모리에 저장하는 방식(호스트 바이트 순서)이 달라질 수 있음
1) 빅 엔디안 : 상위 바이트 값을 작은 번지수에 저장
2) 리틀 엔디안 : 상위 바이트의 값을 큰 번지수에 저장
- 인텔, AMD 계열의 CPU는 리틀 엔디안 기준으로 데이터를 정렬함
- CPU에 따라 호스트 바이트 순서가 달라 데이터 송수신시에 문제가 발생할 수 있음
# 네트워크 바이트 순서의 약속 : "빅 엔디안 방식으로 통일"
=> 네트워크상으로 데이터를 전송할 때는 데이터의 배열을 "빅 엔디안 기준으로 변경"해서 송수신하기로 약속
# 바이트 순서의 변환
1) unsigned short htons(unsigned short);
2) unsigned short ntohs(unsigned short);
3) unsigned long htonl(unsigned long);
4) unsigned long ntohl(unsigned long);
- h는 host byte order(호스트 바이트 순서), n은 network byte order(네트워크 바이트 순서)를 의미
- s는 short 자료형, l은 long 자료형을 의미
- short 자료형은 운영체제와 관계없이 2바이트이나, long 자료형은 리눅스에서 "4바이트"
- s는 PORT번호(16비트(2바이트)이므로) 변환, l은 IP주소(32비트(8바이트)이므로) 변환에 사용됨
# 데이터 송수신 기준이 비록 네트워크 바이트 순서이지만, 송수신전에 매번 프로그래머가 직접 네트워크 바이트 순서로 바꾸어줄 필요는 없음, 데이터 송수신 과정에서 네트워크 바이트 순서 기준으로 자동 변환이 되기 때문
=> 바이트 순서 변환은 sockaddr_in 구조체 변수에 데이터를 채울 때 이외에는 신경쓰지 않아도 됨
# 문자열 IP정보를 네트워크 바이트 순서의 정수형태의 IP정보로 변환
#include <arpa/inet.h>
in_addr_t inet_addr(const char * string);
// 성공시 빅 엔디안으로 변환된 32비트(4byte) 정수 반환
// 실패시 INADDR_NONE 반환
// 잘못된 IP주소에 대한 오류검출능력도 있는 함수
// 예를들어 "1.2.3.256"은 잘못된 IP주소
// (1바이트는 최대 255)이므로 INADDR_NONE을 반환함
#include <arpa/inet.h>
int inet_aton(const char * string, struct in_addr *addr);
// 성공시 1(true), 실패시 0(false) 반환
- string : 변환할 IP주소를 담고 있는 문자열의 주소값
- addr : 변환된 정보를 저장할 in_addr 구조체 변수의 주소값
# inet_addr 함수는 변환된 IP주소 정보를 sockaddr_in에 선언되어 있는
in_addr 구조체 변수에 대입하는 과정이 추가로 필요.
그러나 inet_atons 함수는 인자로 in_addr 구조체 변수의 주소값을 전달하기 때문에,
변환된 값이 자동으로 in_addr 구조체 변수에 저장됨.
# 네트워크 바이트 순서의 정수형태의 IP정보를 문자열 형태의 IP정보로 변환(위 두 함수와 반대 기능의 함수)
#include <arpa/inet.h>
char* inet_ntoa(struct in_addr adr);
// -> 성공시 변환된 문자열의 주소값, 실패시 -1 반환
- 반환형이 char * 이므로 문자열이 메모리 공간에 저장되었다는 의미
=> 함수 내부적으로 메모리 공간을 할당해서 변환된 문자열 정보를 저장함
- 따라서 이 함수 호출 후에는 가급적 반환된 문자열 정보를 다른 메모리 공간에 복사해두는 것이 좋음
- 그렇지 않고 이 함수를 재호출하면 이전의 정보는 덮어씌워져 사라짐
# 소켓 생성 과정에서의 인터넷 주소의 초기화 방법의 예시
struct sockaddr_in addr;
char *serv_ip="211.217.168.13" // IP주소 문자열
char *serv_port="9190" // PORT번호 문자열
memset(&addr,0,sizeof(addr)); // 구조체 변수 addr의 모든 멤버 0으로 초기화
addr.sin_family=AF_INET; // 주소체계 지정(IPv4)
addr.sin_s_addr=inet_addr(serv_ip); // IP주소 초기화
addr.sin_port=htons(atoi(serv_port));; // PORT번호 초기화
# 서버 프로그램의 인터넷 주소정보 초기화 과정
- sockaddr_in 구조체 변수를 하나 선언해서,
이를 서버소켓이 동작하는 컴퓨터의 IP와 소켓에 부여할 PORT번호로 초기화한 다음에 bind 함수를 호출
# 클라이언트 프로그램의 인터넷 주소정보 초기화 과정
- sockadrr_in 구조체 변수를 하나 선언해서,
이를 연결할 서버 소켓의 IP와 PORT번호로 초기화한 다음에 connect 함수를 호출
# INADDR_ANY 상수
- 소켓의 IP를 이것으로 초기화할 경우, 소켓이 동작하는 컴퓨터의 IP주소가 자동으로 할당됨
- 컴퓨터 내에 두 개 이상의 IP를 할당받아 사용하는 경우(Multi-homed 컴퓨터, e.g. 라우터),
할당 받은 어떤 IP 주소를 통해 데이터가 들어오더라도 PORT 번호만 일치하면 수신이 가능해짐
- 클라이언트 프로그램 구현에서는 사용될 일이 별로 없음
# 서버 소켓 생성시 IP주소가 필요한 이유
- 서버 소켓은 생성시 자신이 속한 컴퓨터의 IP주소로 초기화가 이루어져야 하는 것이 너무나 당연함.
그런데 왜 IP주소가 꼭 필요할까?
=> IP주소는 컴퓨터에 장착되어 있는 NIC(네트워크인터페이스카드, 랜카드)의 개수만큼 부여가 가능
이러한 경우에는, 서버소켓이라 할지라도 어느 IP주소로 들어오는(NIC로 들어오는)
데이터를 수신할지 반드시 결정해야하기 때문에 IP주소가 필요함
- NIC가 하나인 컴퓨터라면 주저없이 INADDR_ANY를 IP주소로 하여 초기화 하는것이 편리
# 소켓에 인터넷 주소 할당하기 : 초기화된 주소정보를 소켓에 할당
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);
// -> 성공시 0, 실패시 -1 반환
- sockfd : 주소정보(IP와 PORT번호)를 할당할 소켓의 파일디스크립터
- myaddr : 할당하고자 하는 주소정보를 지니는 구조체 변수의 주소값
- addrlen : 두번째 인자로 전달된 구조체 변수의 길이정보
- 함수 호출이 성공하면, 첫번째 인자에 해당하는 소켓에, 두번째 인자로 전달된 주소정보가 할당됨
# 서버 소켓의 초기화 과정
int serv_sock;
struct sockaddr_in serv_addr;
char *serv_port="9190";
/*서버 소켓(리스닝 소켓) 역할을 할 소켓을 생성*/
serv_sock=socket(PF_INET,SOCK_STREAM,0); // TCP로 소켓 생성
/*주소정보 초기화*/
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_addr.sin_port=htons(atoi(serv_port));
/*주소정보를 소켓에 할당*/
bind(serv_sock, (struct sockaddr*)&serv_addr,sizeof(serv_addr));
.....
# 윈도우에서 소켓에 인터넷 주소 할당하기
SOCKET serv_sock;
struct sockaddr_in serv_addr;
char *serv_port="9190";
/*서버 소켓(리스닝 소켓) 역할을 할 소켓을 생성*/
serv_sock=socket(PF_INET,SOCK_STREAM,0); // TCP로 소켓 생성
/*주소정보 초기화*/
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_addr.sin_port=htons(atoi(serv_port));
/*주소정보를 소켓에 할당*/
bind(serv_sock, (struct sockaddr*)&serv_addr,sizeof(serv_addr));
.....
# WSAStringToAddress & WSAAddressToString
- 윈속2에서 추가된 변환함수임
- inet_ntoa, inet_addr 함수와 기능은 같으나 다양한 프로토콜에 적용 가능한 장점이 있음
=> 즉, IPv4뿐 아니라 IPv6에서도 사용 가능
- 윈도우 종속적인 함수이기에 다른 운영체제로의 이식성이 떨어진다는 단점도 있음
#include <winsock2.h>
INT WSAStringToAddress(
LPTSTR AddressString, INT AddressFamily,
LPWSAPROTOCOL_INFO lpProtocolInfo, LPSOCKADDR lpAddress,
LPINT lpAddressLength
);
// -> 성공시 0, 실패시 SOCKET_ERROR 반환
- 주소정보를 나타내는 문자열을 가지고 주소정보 구조체 변수를 채워넣을때 호출하는 함수
- AddressString : IP와 PORT번호를 담고 있는 문자열의 주소값
- AddressFamily : 첫번째 인자로 전달된 주소정보가 속하는 주소체계 정보
- lpProtocolInfo : 프로토콜 프로바이더(Provider) 설정, 일반적으로 NULL 전달
- lpAddress : 주소정보를 담을 구조체 변수의 주소값
- lpAddressLength : 네번째 인자로 전달된 주소값의 변수 크기를 담고 있는 변수의 주소값
#include <winsock2.h>
INT WSAAddressToString(
LPSOCKADDR lpsaAddress, DWORD dwAddressLength,
LPWSAPROTOCOL_INFO lpProtocolInfo, LPTSTR lpszAddressString,
LPDWORD lpdwAddressStringLength
);
// -> 성공시 0, 실패시 SOCKET_ERROR 반환
- lpsaAddress : 문자열로 변환할 주소정보를 지니는 구조체 변수의 주소값
- dwAddressLength : 첫번째 인자로 전달된 구조체 변수의 크기 전달
- lpProtocolInfo : 프로토콜 프로바이더(Provider) 설정, 일반적으로 NULL 전달
- lpszAddressString : 문자열로 변환된 결과를 저장할 배열의 주소값 전달
- lpAddressStringLength : 네번째 인자로 전달된 주소값의 배열 크기를 담고있는 변수의 주소값
# 위 두 함수의 사용 예
#undef UNICODE
#undef _UNICODE
#include <stdio.h>
#include <winsock2.h>
int main(int argc, char *argv[])
{
char *strAddr="203.211.218.102:9190";
char strAddrBuf[50];
SOCKADDR_IN servAddr;
int size;
WSADATA wsaData;
WSAStartup(MAKEWORD(2,2), &wsaData);
size=sizeof(serv_adr);
WSAStringToAddress(
strAddr,AF_INET,NULL,(SOCKADDR*)&servAddr,&size);
size=sizeof(strAddrBuf);
WSAAddressToString(
(SOCKADDR*)&servAddr, sizeof(servAddr), NULL, strAddrBuf, &size);
printf("Second conv result: %s \n", strAddrBuf);
WSACleanup();
return 0;
}
[출처] : 윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어
'Programming > 열혈 TCP, IP 소켓 프로그래밍(저자 윤성우)' 카테고리의 다른 글
Ch 04. TCP 기반 서버/클라이언트 1 (0) | 2020.08.06 |
---|---|
Ch 03. 내용 확인문제 (0) | 2020.08.05 |
Ch 02. 내용 확인문제 (0) | 2020.08.04 |
Ch 02. 소켓의 타입과 프로토콜의 설정 (0) | 2020.08.04 |
표준 파일 입출력 함수(고수준 파일 입출력 함수) (0) | 2020.07.30 |