메모장
TCP/IP 소켓 프로그래밍 복습2 본문
참고문헌 - 윤성우 <윤성우의 열혈 TCP/IP 소켓 프로그래밍>
♨ 개인적 해석이 들어간 글임으로, 인지하지 못한 오류가 있을 수 있습니다 ♨
▣ 프로토콜(Protocol)이란 무엇인가
- 대화에 필요한 통신규약을 의미
-> 컴퓨터 상호간의 대화에 필요한 통신규약 / 컴퓨터가 서로 데이터를 주고 받기 위해서 정의해 놓은 약속
▣ 소켓의 생성
윈도우 운영체제의 socket 함수
SOCKET socket(int af, int type, int protocol)
① af(리눅스에선 domain) : 소켓이 사용할 프로토콜 체계(Protocol Family) 정보 전달
② type : 소켓의 데이터 전송방식에 대한 정보 전달
③ protocol : 두 컴퓨터간 통신에 사용되는 프로토콜 정보 전달
이 파라미터 들에 대한 세부적인 내용 기술될 예정
▣ 프로토콜 체계(Protocol Family)
- sys/socket.h에 선언되어 있는 프로토콜 체계
1. PF_INET : IPv4 인터넷 프로토콜 체계
2. PF_INET6 : IPv6 인터넷 프로토콜 체게
3. PF_LOCAL : 로컬 통신을 위한 UNIX 프로토콜 체계
4. PF_PACKET : Low Level 소켓을 위한 프로토콜 체계
5. PF_IPX : IPX 노벨 프로토콜 체계
▣ 소켓의 타입(Type)
- 소켓의 데이터 전송방식
- 프로토콜 체계가 결정되었다고 해서 데이터의 전송 방식까지 결정되는 것은 아니다
? -> 특정 프로토콜 체계 내에 둘 이상의 데이터 전송방식이 존재하기 때문
- 대표적인 데이터 전송 방식
1. 연결 지향형 (SOCK_STREAM)
2. 비연결 지향형 (SOCK_DGRAM)
▣ 연결지향형 소켓 타입
- socket 함수의 두 번째 인자로 SOCK_STREAM을 전달하면 '연결지향형 소켓'이 생성
- 연결지향형 데이터 전송의 특징
1. 중간에 데이터가 소멸되지 않고 목적지로 전송된다
2. 전송 순서대로 데이터가 수신된다
3. 전송되는 데이터의 경계(Boundary)가 존재하지 않는다
4. 소켓 대 소켓의 연결은 반드시 1:1 이어야 한다
- 데이터 경계가 존재하지 않는다?
전송하는 컴퓨터가 세 번의 write 함수호출을 통해 총 100바이트를 전송
그런데 수신하는 컴퓨터는 한 번의 read 함수호출을 통해서 100바이트를 수신한다
-> 데이터를 송수신하는 소켓은 내부적으로 버퍼(buffer, 바이트 배열)을 지니고 있다
그리고 소켓을 통해 전송되는 데이터는 일단 이 배열(버퍼)에 저장된다
버퍼의 용량을 초과하지 않는 한, read 함수호출이나 write 함수호출 횟수는 중요하지 않다(많아도 되고, 적어도 된다)
결론 : 연결지향형 소켓 = 신뢰성 있는 순차적인 바이트 기반의 연결지향 데이터 전송 방식의 소켓
▣ 비 연결지향형 소켓 타입
- socket 함수의 두 번째 인자로 SOCK_DGRAM을 전달하면 '비 연결지향형 소켓'이 생성
- 비 연결지향형 데이터 전송의 특징
1. 전송된 수서에 상관없이 가장 빠른 전송을 지향한다
2. 전송된 데이터는 손실의 우려가 있고, 파손의 우려가 있다
3. 전송되는 데이터의 경계(Boundary)가 존재한다
4. 한 번에 전송할 수 있는 데이터의 크기가 제한된다
- 연결지향형 소켓에 비해 데이터의 전송속도는 빠르나, 데이터의 손실 및 훼손이 발생하지 않음을 보장하지 않는다
한 번에 전송할 수 있는 데이터의 크기가 제한되며 ,데이터의 경계가 존재한다(3번의 write가 있었다면, 3번의 read가 있어야 한다)
결론 : 신뢰성과 순차적 데이터 전송을 보장하지 않는, 고속의 데이터 전송을 목적으로 하는 소켓
※ 비 연결지향형 소켓은 연결지향형 소켓과는 달리 '연결'이라는 개념이 존재하지 않는다
▣ 프로토콜의 최종선택
- socket 함수의 세 번째 인자는 소켓이 사용하게 될 프로토콜 정보를 전달하는 목적으로 존재한다
? : 1, 2번째 인자로는 프로토콜 결정이 충분하지 않는가?
-> 특정 상황에선 3번째 인자까지 필요하다
" 하나의 프로토콜 체계 안에 데이터의 전송방식이 동일한 프로토콜이 둘 이상 존재하는 경우"
- socket 함수에 전달할 수 있는 인자 정보
예1) " IPv4 인터넷 프로토콜 체계에서 동작하는 연결지향형 데이터 전송 소켓 "
IPv4 인터넷 프로토콜 체계 : PF_INET
연결지향형 데이터 전송 : SOCKET_STREAM
이 두가지 조건을 만족시키는 프로토콜 = IPPROTO_TCP
∴ SOCKET tcp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
-> 이때 socket함수에 의해 생성되는 소켓을 'TCP 소켓'이라 부른다
예2) " IPv4 인터넷 프로토콜 체게에서 동작하는 비 연결지향형 데이터 전송 소켓 "
IPv4 인터넷 프로토콜 체계 : PF_INET
연결지향형 데이터 전송 : SOCKET_DGRAM
이 두가지 조건을 만족시키는 프로토콜 = IPPROTO_UDP
∴ SOCKET udp_socket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
-> 이때 socket함수에 의해 생성되는 소켓을 'UDP 소켓'이라 부른다
▣ SOCKET 자료형
- 정수로 표현되는 소켓의 핸들 값을 저장하기 위해 정의된 자료형
- socket 함수에서 반환하는 값이 정수이기 때문에 int형으로도 받을 수 있으나
소켓임을 강조하기 위해 SOCKET 자료형이 사용되기 때문에, 그냥 소켓 핸들 값을 저장하기 위한 자료형으로 인식하는게 좋다
▣ 윈도우 기반 TCP 소켓의 예
예)
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
void ErrorHandling(char* Message);
int main(int argc, char* argv[])
{
WSADATA wsaData;
SOCKET hSocket;
SOCKADDR_IN servAddr;
char message[30];
int strLen = 0;
int idx = 0, readLen = 0;
if (argc != 3)
{
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)// 소켓 라이브러리 초기화
ErrorHandling("WSAStartup() error!");
hSocket = socket(PF_INET, SOCK_STREAM, 0);// TCP 소켓 생성
if (hSocket == INVALID_SOCKET)
ErrorHandling("hSocket() error");
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = inet_addr(argv[1]);
servAddr.sin_port = htons(atoi(argv[2]));
if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)// 서버에 여결 요청
ErrorHandling("connect() error!");
while (readLen = recv(hSocket, &message[idx++], 1, 0))// recv 함수호출을 통해, 수신된 데이터를 1바이트씩 읽는다
{
if (readLen == -1)
ErrorHandling("read() error!");
strLen += readLen;// recv 함수로 1바이트씩 읽고 있기 때문에 strLen에 실제로 더해지는 값은 1, 이는 recv함수의 호출횟수와 같다
}
printf("Message from server : %s \n", message);
printf("Function read call count : %d \n", strLen);
closesocket(hSocket);
WSACleanup();
return 0;
}
void ErrorHandling(char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
○ 주소체계와 데이터 정렬
IP주소 : Internet Protocol의 약자로 인터넷상에서 데이터를 송수신할 목적으로 컴퓨터에게 부여하는 값
Port 번호 : 프로그램상에서 생성되는 소켓을 구분하기 위해 소켓에 부여되는 번호
▣ 인터넷 주소 (Internet Address)
- IPv4(Internet Protocol version 4) 4바이트 주소체계
IPv6(Internet Protocol version 6) 16바이트 주소체계
-> IP주소를 표현할 때 사용되는 바이트 크기 차이로 구분
- IPv4 기준의 4바이트 IP주소는 네트워크 주소와 호스트(컴퓨터를 의미) 조소로 나뉘며, 주소의 형태에 따라서 A~E 클래스로 분류
구글 이미지 참고)
- 네트워크 주소(네트워크 ID)란 네트워크의 구분을 위한 IP주소의 일부
예) www.semi.com 이라는 회사에 무대리에게 데이터를 전송한다(회사 컴퓨터는 로컬 네트워크로 연결) - 203.211.217.202로 데이터 전송
4바이트 IP주소 중에서 네트워크 주소만 참조해서 semi.com의 네트워크로 데이터가 전송된다- semi.com 203.211.217
그리고 semi.com의 네트워크로 데이터가 전송되었다면, 해당 네트워크(를 구성하는 라우터)는 전송된 데이터의 호스트 주소(호스트 IP, 예시의 202)를 참조하여
특정 컴퓨터(무대리에게)로 데이터를 전송해준다
▣ 클래스 별 네트워크 주소와 호스팅 주소의 경계
- IP주소의 첫 번째 바이트만 보면 네트워크 주소가 몇 바이트인지 판단 가능
클래스 A의 첫 번째 바이트 범위0~127
클래스 B``128~191
클래스 C``192~223
=
클래스 A의 첫 번째 비트는 항상 0으로 시작
B10으로 시작
C110으로 시작
-> 이런 기준이 정해져 있기 때문에 소켓을 통해 데이터를 송수신할 때, 별도의 신경을 쓰지 않아도 네트워크로 데이터가 이동하고, 최종 목적지인 호스트로 데이터가 전송된다
▣ 소켓의 구분에 활용되는 PORT 번호
- IP는 컴퓨터를 구분하기 위한 목적으로 존재, PORT번호는 컴퓨터 내에서 응용프로그램을 구분하기 위해 사용
- 둘 이상의 컴퓨터로 부터 데이터를 전송 받으려면 둘 이상의 소켓이 생성되어야 한다
NIC (Network Interface Card)라는 송수신 장치가 IP를 이용하여 데이터를 내부로 전송한다
컴퓨터 내부로 전송된 데이터를 소켓에 적절히 분배하는 작업은 운영체제가 담당한다
-> 이 때 운영체제가 활용하는 것이 PORT 번호
∴ NIC를 통해 수신된 데이터 안에는 PORT 번호가 새겨져 있고 운영체제는 그것을 이용하여 소켓에 데이터를 전달한다
- 하나의 운영체제 내에서 PORT 번호는 중복될 수 없다
- PORT는 16비트로 표현, 0 ~ 65535
-> 0 ~ 1023까지는 "Well-known PORT"라 해서 특정 프로그램에 할당되어 있다
∴ 새로 할당할 때에는 이 범위의 값을 제외해야 한다
- PORT번호는 중복이 불가능하지만, TCP 소켓과 UDP 소켓은 PORT번호를 공유하지 않기 때문에 중복되어도 상관없다
예) TCP 소켓에 9190 PORT를 할당했다면, 다른 TCP 소켓에는 9190 PORT를 공유할 수 없지만
UDP 소켓에는 할당할 수 있다
○ 주소정보의 표현
▣ IPv4 기반의 주소표현을 위한 구조체
- 주소체계(af), IP주소, PORT번호에 대한 정보를 담을 수 있는 구조체 sockaddr_in
struct sockaddr_in
{
sa_family_tsin_family;// 주소체계(Address Family)
uint16_tsin_port;// 16비트 TCP/UDP PORT번호
struct in_addrsin_addr;// 32비트 IP주소
charsin_zero[8];// 사용되지 않음
}
- sockaddr_in 내부에 사용된 in_addr 구조체
struct in_addr
{
in_addr_ts_addr;// 32비트 IPv4 인터넷 주소
}
- 새로운 자료형들, POSIX(Portable Operating System Interface)
-> 유닉스 계열의 운영체제에 적용하기 위한 표준
1. sys/types.h에 선언된 자료형
int8_t : signed 8-bit int
uint8_t : unsigned 8-bit int(unsigned char)
int16_t : signed 16-bit int
uint16_t : unsigned 16-bit int(unsigned short)
int32_t : signed 32-bit int
uint32_t : unsigned 32-bit int (unsigned long)
2. sys/socket.h에 선언된 자료형
sa_family_t : 주소체계(address family)
socklen_t : 길이정보(length of struct)
3. netinet/in.h에 선언된 자료형
in_addr_t : IP주소정보, uint32_t로 정의되어 있다
in_port_t : PORT번호정보, uint16_t로 정의되어 있다
-> 확장성을 고려해 추가한 내용
▣ 구조체 sockaddr_in의 멤버에 대하 ㄴ분석
- sin_family
AF_INET : IPv4 인터넷 프로토콜에 적용하는 주소체계
AF_INET6 : IPv6 인터넷 프로토콜에 적용하는 주소체계
AF_LOCAL : 로컬 통신을 위한 유닉스 프로토콜의 주소체계
- sin_port
16비트 PORT번호를 저장한다 (단, 네트워크 바이트 순서로 저장해야 한다)
- sin_addr
32비트 IP주소정보를 저장한다 (단, 네트워크 바이트 순서로 저장)
이 멤버를 파악하기 위해서는 in_addr 구조체도 함께 봐야한다
그런데 in_addr 구조체의 유일한 멤버가 uint32_t로 선언되어 있기 때문에, sin_addr도 32비트 정수자료형으로 인식할 수 있다
- sin_zero
특별한 의미를 지니지 않는 멤버다
단순히 구조체 sockaddr_in의 크기를 구조체 sockaddr과 일치시키기 위해 삽입된 멤버(단 0으로 채워야 한다)
- bind 함수의 두 번째 전달인자로 sockaddr의 주소값(포인터)를 전달한다
예) bind(hServSock, (SOCKADDR*)&servAddr, sizeof(servAddr))
-> 그러나 sockaddr은 주소체계, PORT번호, IP주소정보를 담기에 불편하게 되어 있다
struct sockaddr
{
sa_family_tsin_familly;// 주소체계
charsa_data[14];// 주소정보
}
- 위 구조체 멤버 sa_data에 저장되는 주소정보에는 IP주소와 PORT번호가 포함되어야 한다
그리고 이 두 가지 정보를 제외한 나머지 부분을 0으로 채울 것을 요구한다
-> 매우 불.편
그래서 sockaddr_in 구조체가 등장한 것이다
▣ 바이트 순서(Order)와 네트워크 바이트 순서
- CPU가 데이터를 메모리에 저장하는 방식 (Host Byte Order, 호스트 바이트 순서)
1. 빅 엔디안 (Big Endian) : 상위 바이트의 값을 작은 번지수에 저장하는 방식
2. 리트 엔디안 (Little Endian) : 상위 바이트 값을 큰 번지수에 저장하는 방식
예) int형 정수 0x12345678을 저장할때
1. 빅 엔디안
-> 0x12 0x34 0x56 0x78
2. 리틀 엔디안
-> 0x78 0x56 0x34 0x12
- CPU에 따라 데이터를 저장하는 방식이 다르기 때문에
서로 다른 CPU에 동일한 값을 주고 받으려면 통일된 기준으로 데이터를 전송해야 한다
-> 네트워크 바이트 순서(Network Byte Order) : 빅 엔디안 방식으로 통일
▣ 바이트 순서의 변환 (Endian Conversions)
- unsigned short htons (unsigned short);// host to network short
unsigned short ntohs (unsigned short);// network to host short
unsigned long htonl (unsigned long);// host to network long
unsigned long ntohl (unsigned long);// network to host long
○ 인터넷 주소의 초기화와 할당
▣ 문자열 정보(192.168.0.115 등)를 네트워크 바이트 순서의 정수(0x4f7cd87f 등)로 변환하기
- 여기서 문자열 정보란 문자열로된 IP주소를 의미하는 것 같다(예제가 그래)
- sockaddr_in 안에서 주소정보를 저장하기 위해 선언된 멤버는 32비트 정수형으로 정의되어 있다
-> IP주로 정보를 할당하기 위해서 32비트 정수형태로 IP주소를 표현할 수 있어야 한다
※ 점이 찍힌 십진수 표현방식(Dotted-Decimal Notation) : 우리가 일반적으로 아는 IP 표현법
- 문자열로 표현된 IP주소를 32비트 정수형으로 변환하면서 네트워크 바이트 순서도 적용하는 inet_addr함수
#include <arpa/inet.h>
in_addr_t inet_addr(const char* string); // 성공시 빅 엔디안으로 변환된 32비트 정수 값, 실패 시 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 구조체 변수의 주소 값 전달
예) char* addr = "127.232.124.79";
struct sockaddr_in addr_inet;
if (!inet_aton(addr, &addr_inet.sin_addr))
ErrorHandling("Convension error");
else
printf("Network ordered integer addr : %#x \n", addr_inet.sin_addr.s_addr);
- 반대로 네트워크 바이트 순서로 정렬된 정수형 IP주소 정보를 문자열 형태로 변환하는 inet_ntoa함수
#include <arpa/inet.h>
char* inet_ntoa(struct in_addr adr);// 성공 시 변환된 문자열의 주소 값, 실패 시 -1 반환
-> in_addr 구조체 내에 있는 정수형 IP주소를 문자열(char*) 형태로 변환
이때 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;// 주소체계 지정
addr.sin_addr.s_addr = inet_addr(serv_ip);// 문자열 기반의 IP주소 초기화
addr.sin_port = htons(atoi(serv_port));// 문자열 기반의 PORT번호 초기화
- memset함수 : 동일한 값으로 바이트단위 초기화를 할 때 호출하는 함수
첫 번째 인자 : 초기화의 대상의 주소값 (예제 - 구조체 변수 addr의 포인터)
두 번째 인자 : 초기화 할 값 (예제 - 0)
세 번째 인자 : 초기화 할 크기? (예제 - addr의 바이트 크기)
-> 0으로 초기화하는 이유 : 0으로 초기화해야 하는 sockaddr_in 구조체 멤버 sin_zero를 0으로 초기화 하기 위해
▣ 클라이언트의 주소정보 초기화
- 인터넷 주소정보의 초기화 과정은 서버 프로그램에서 주로 등장
-> like " IP가 211.217.168.13, PORT가 9190으로 들어오는 데이터를 다 받겠다 "
(주로 bind 함수로 진행)
- 반면 클라이언트에서는
-> like " IP가 211.217.168.13, PORT가 9190으로 연결하라 "의 의미를 갖는다
(주로 connect 함수로 진행)
▣ INADDR_ANY
- 상수, 소켓이 동작하는 컴퓨터의 IP주소가 자동으로 할당된다
- 컴퓨터 내에 두 개이상의 IP를 할당 받아서 사용하는 경우(Multi-homed 컴퓨터, 라우터),
할당 받은 IP 중 어떤 주소를 통해서 데이터가 들어오더라도 PORT 번호만 일치하면 수신할 수 있다
-> 자동 할당시키는게 체고
∴ 서버 프로그램의 구현에 많이 선호되는 방법
-> 클라이언트가 서버의 기능을 일부 포함하는 경우가 아니라면, 클라이언트 프로그램의 구현에서 사용될 일이 거의 없다
▣ 소켓에 인터넷 주소 할당하기
int bind(SOCKET s, const struct sockaddr* name, int namelen);// 성공 시 0, 실패 시 -1 반환
① ② ③
① s : 주소정보(IP와 PORT)를 할당할 소켓 핸들러
② name : 할당하고자 하는 주소정보를 지니는 구조체 주소의 값
③ namelen : 두 번째 인자로 전달된 구조체 변수의 길이정보
예)
SOCKET servSock;
struct sockaddr_in servAddr;
char* servPort = "9190";
/* 서버 소켓 생성 */
servSock = socket(PF_INET, SOCK_STREAM, 0);
/* 주소정보 초기화 */
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
servAddr.sin_port = htons(atoi(servPort));
/* 주소정보 할당 */
bind(servSock, (struct sockaddr*) &servAddr, sizeof(servAddr));
'TCP&IP 복습' 카테고리의 다른 글
TCP/IP 소켓 프로그래밍 복습4 (0) | 2020.10.30 |
---|---|
TCP/IP 소켓 프로그래밍 복습3 (0) | 2020.10.29 |
TCP/IP 소켓 프로그래밍 복습 1 (0) | 2020.10.26 |