Notice
Recent Posts
Recent Comments
Link
«   2025/06   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Archives
Today
Total
관리 메뉴

메모장

TCP/IP 소켓 프로그래밍 복습2 본문

TCP&IP 복습

TCP/IP 소켓 프로그래밍 복습2

Captic 2020. 10. 28. 18:06

참고문헌 - 윤성우 <윤성우의 열혈 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 클래스로 분류

(https://www.google.com/search?q=ipv4+%ED%81%B4%EB%9E%98%EC%8A%A4&sxsrf=ALeKk01uY4stQajOk7_VVB8TbZnmyEFwKw:1603678567894&source=lnms&tbm=isch&sa=X&ved=2ahUKEwjj4_2amNHsAhXZyYsBHZ2CCjEQ_AUoAXoECAwQAw&biw=1377&bih=876#imgrc=oMPDtxrp1KOsPM)

구글 이미지 참고)

 

- 네트워크 주소(네트워크 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