날마다 새롭게 또 날마다 새롭게

TCP/IP 소켓프로그래밍 C 내용 정리 본문

프로그래밍/Network

TCP/IP 소켓프로그래밍 C 내용 정리

아무유 2013. 2. 8. 18:22

1.서론

1.1 네트워크 패킷 그리고 프로토콜

호스트 : 웹브라우저나 메신저 프로그램 또는 파일 공유 프로그램들을 구동하는 컴퓨터

라우터 : 하나의 통신 채널(이더넷, WiFi)로부터 온 정보들을 다른 통신 채널로 교체, 전달하는 일을 도맡은 장비이다.

패킷 : 연속된 바이트로 이루어진 정보를 담고 있는 집합체

프로토콜 : 통신 프로그램 사이에서 교환되는 패킷에 대한 약속

TCP/IP

정의

프로토콜 집합체(protocol suite). IP(Internet Protocol), TCP(Transmission Control Protocol), UDP(User Datagram Protocol)이 있다.

동작 흐름

응용프로그램 → 소켓(TCP→IP)→채널→라우터→채널 →소켓(IP→TCP)→응용프로그램 

계층

Network layer : IP - 패킷들은 자신의 목적지 주소를 포함하고 있다.

Transport layer : 

TCP, UDP - 프로그램에서 프로그램으로 데이터를 전달하는 프로토콜(end-to-end transport protocol), 세분화된 주소 지정(addressing)기능이 있다.(port number)

TCP : 연결 지향적(connection-oriented) 프로토콜이다. 

    IP가 제공하는 정보의 손실, 복제, 기타 오류 등의 검출과 복구 기능을 수행한다.

UDP : IP 전송 서비스를 확장하여 호스트 간의 전달이 아닌 두 개의 응용 프로그램 간의 전달이 가능하도록 한다.

1.2 주소에 대하여

Internet address : 네트워크 인터페이스 사이에 존재하는 네트워크 연결을 의미한다. 호스트(PC)의 주소

표기법 : dotted-quad 표기법. IP 주소를 4바이트로 나타낸 것이다. ex)10.1.2.3

port address : 프로그램의 주소. 1~65535의 값을 갖는다. ~1023까지는 보통 특정 프로그램이 사용하도록 되어있으므로 그 이외의 주소를 사용하는 것이 좋다. ex)80은 웹 서버 역할을 하는 프로그램이 사용하도록 정해져있다.

특별한 주소들 

loopback address : loopback interface라고 하는 가상의 디바이스에 할당 되어 있음. 전송한 패킷을 즉시 전송자에게 돌려보내는 기능을 가지고 있다. 디버깅시 많이 사용한다. (localhost, 127.0.0.1)

사설 용도로 할당된 주소들 : 192.168.*.*, 172.16~31.*.*, Link-local 주소, 멀티캐스트 주소

1.3 네임 주소에 대하여

DNS(Domain Name Service)는 TCP나 UDP를 이용하여 인터넷을 통해 이러한 정보를 검색하게 하는 프로토콜이다.

local configuration database(지역 설정 정보) 는 운영체제에 의존적인 방법으로 시스템마다 고유의 네임-IP 주소 맵핑 정보를 가지고 있다.

1.4 클라이언트와 서버

클라이언트 프로그램은 통신을 개시하는 반면, 서버 프로그램은 수동적으로 개시를 기다리고 있따가 클라이언트의 접근이 오면 응답한다.

1.5 소켓이란 무엇인가?

소켓 : 응용 프로그램이 이를 통해 데이터를 송수신할 수 있는 추상화된 개념이다.

    포트가 특정 호스트의 소켓을 구분하고, 소켓은 응용 프로그램을 구분한다.

TCP/IP 소켓 타입

스트림 소켓 : 종단 간 전송 프로토콜로 TCP를 사용

데이터그램 소켓 : UDP를 사용

2. TCP 소켓

2.1 TCP 클라이언트

순서 : 

1. socket(), TCP 소켓 생성

2. connect(), 서버와 연결 설정

3. send(), recv(), 통신 수행

4. close(), 연결 종료

2.2 TCP 서버

순서 :

1. socket(), TCP 소켓 생성

2. bind(), 소켓에 포트 번호를 할당

3. listen(), 해당 포트가 연결을 받아들이도록 시스템에 알림

4. 다음과 같은 일을 계속적으로 반복

accept()를 통해 각 클라이언트와 통신에 필요한 새로운 소켓을 획득

send(), recv()를 호출하여 통신을 수행

close()를 통해 클라이언트와 연결을 종료

2.3 소켓의 생성과 해지

int socket(int domain, int type, int protocol) - socket instance 생성 요청

1)domain : 프로토콜 패밀리. AF_INET(IPv4), AF_INET6(IPv6), PF_INET, PF_INET6, PF_LOCAL, PF_PACKET

2)type : SOCK_STREAM스트림 타입의 전송), SOCK_DGRAM(데이터그램), SOCK_RAW(IP서비스를 직접 이용)

3)protocol : IPPROTO_TCP(TCP), IPPROTO_UDP(UDP), IPPROTO_ICMP(ICMP)

4) return : socket descriptor, 실패 시 -1

int close(int socket)

return : 성공 시 0, 실패 시 -1

2.4 주소 지정

2.4.1 범용 주소 형식

struct sockaddr {

sa_family_t sa_family;    // 주소 패밀리(예: AF_INET)

char sa_data[14];        // 주소 패밀리 의존적인 주소 정보, 주소 패밀리에 따라서 비트의 구성 결정

};

2.4.2 IPv4 주소

struct in_addr {

uint32_t s_addr;        // 인터넷 주소(32비트)

};


struct sockaddr_in {

sa_family_t sin_family;        // 인터넷 프로토콜(AF_INET)

in_port_t sin_port;              // 주소 포트(16비트)

struct in_addr sin_addr;        // IPv4 주소 (32비트)

char sin_zero[8];              // 사용 안함

};

2.4.3 IPv6 주소

struct in6_addr {

uint32_t s_addr[16];        // 인터넷 주소(32비트)

};


struct sockaddr_in6 {

sa_family_t sin6_family;       // 인터넷 프로토콜(AF_INET6)

in_port_t sin6_port;              // 주소 포트(16비트)

uint32_t sin6_flowinfo;        // IPv6 플로우 레이블 정보

struct in6_addr sin6_addr;    // IPv6 주소(128비트)

uint32_t sin6_scope_id;       // 인터페이스 Scope identifier

};

2.4.4 범용 주소 스토리지

sockaddr_storage는 모든 주소 형식을 위한 충분한 공간을 가지고 있다.

struct sockaddr_storage {

sa_family_t

...

// 알맞은 길이와 정렬을 위해서 여러 필드와 채워넣기가 포함된다.

...

};

2.4.5 이진/문자열 주소 변환

pton = printable to numeric, 주소의 형식을 이진 형식으로 변환한다. (printable 형식 : 192.168.1.1)

int inet_pton(int addressFamily, const char *src, void *dst)

1)addressFamily : 변환될 주소의 패밀리, AF_INET, AF_INET6

2)src : null로 종료되는 변환될 주소를 담고 있는 문자열 (printable 형식)

3)dst : 변환되는 숫자 형식의 주소가 저장될 메모리의 위치 (numeric 형식)

4)return : 성공 시 0, 실패 시, -1

nton = numeric to printable, 이진 형식에서 출력 형식으로 변환한다.

const char *inet_ntop(int addressFamily, const void *src, char *dst, socklen_t dstBytes)

1)addressFamily : 변환될 주소의 형식 지정

2)src : 변환할 숫자 형식의 주소를 포함하는 메모리 주소

3)dst : 출력 형식의 주소가 저장될 메모리 주소

4)dstBytes : 출력 형식의 주소가 저장될 메모리 크기, INET_ADDRSTRLEN, INET6ADDRSTRLEN

2.4.6 소켓의 연관 주소를 얻는 법

socket 핸들을 사용하여 주소 정보를 반환하는 함수를 사용한다.

int getpeername(int socket, struct sockaddr *remoteAddresss, socklen_t *addressLength) - 원격 주소 정보

int getsockname(int socket, struct sockaddr *localAddress, socklen_t *addressLength) - 지역 주소 정보

2.5 소켓에 연결

TCP소켓은 데이터가 소켓을 통해 전송되기 전에 반드시 다른 종단점의 소켓에 연결되어야 한다.

클라이언트는 연결을 시도하고, 서버는 클라이언트가 접속하기를 수동적으로 기다린다.

int connect(int socket, const struct sockaddr *foreignAddress, socklen_t addressLength)

1)socket : 소켓 식별자

2)foreignAddress : sockaddr 포인터 형태로 IP주소와 포트 번호를 포함하는 구조체 포인터이다.

2.6 소켓을 주소와 바인딩하기

클라이언트와 서버를 연결하기 위해서 서버는 먼저 bind()를 통해 해당 주소와 포트를 소켓에 연결해야 한다.

아래 명령을 실행하면 서버 소켓은 주소를 가지게 된다.

int bind(int socket, struct sockaddr *localAddress, socklen_t addressSize)

1)socket : 소켓 식별자

2)localAddress :  주소 파라미터

3)addressSize : 주소 구조체의 크기

2.7 클라이언트의 연결 요청 처리 (listen, accept)

listen() 함수는 대상 소켓의 내부상태 변화를 일으켜서 클라이언트의 연결 요청이 버려지지 않고 accept()을 위해 대기화(queue)한다.

listen 하고 있는 소켓은 실제로 절대 송수신에 사용되지 않으며 각 클라이언트를 연결하기 위한 새로운 소켓을 획득하기 위한 수단으로 사용된다.

int listen(int socket, int queueLimit)

1)socket : 소켓 식별자

2)queueLimit : 연결 개수의 상한선 ( 시스템 의존적이므로 지역 시스템의 기술 문서를 참조해야 한다.)

3)return : 성공 시 0, 실패 시 -1 반환

서버는 새로운 소켓을 통해 송수신을 하게 되는데, 클라이언트의 연결을 윟나 새로운 소켓을 생성하기 위해서 accept()을 호출한다. queue에 대기하고 있는 다음 연결(socket)을 꺼내 클라이언트의 주소와 포트를 연결한다.

int accept(int socket, struct sockaddr *clientAddresss, socklen_t *addressLength)

1)socket : 대기 상태에 있는 서버 socket

2)clientAddress : 연결을 요청한 클라이언트의 주소를 반환 함

3)addressLength : 연결을 요청한 클라이언트의 주소 크기

4)return : 성공 시 클라이언트와 연결 된 새로운 소켓의 식별자를 반환, 실패 시 -1을 반환한다.

2.8 데이터 주고 받기

소켓이 연결되면 데이터의 송수신이 가능하다. 연결이 되면 클라이언트와 서버의 구분은 소켓 API 관점에서 볼 때 큰 의미가 없게 된다.

ssize_t send(int socket, const void *msg, size_t msgLength, int flags)

1)msg : 송신한 바이트들

2)msgLength : 보내질 바이트의 수

3)flags : 동작 특성, 0인 경우 기본 동작

ssize_t recv(int socket, void *rcvBuffer, size_t bufferLength, int flags)

1)rcvBuffer : 수신할 데이터가 위치하는 일종의 문자 배열 형태의 메모리

2)bufferLength : 한번에 수신할 수 있는 최대 크기

3)flags : 동작 특성, 0인 경우 기본 동작

send()나 recv()의 동작 특성은 모든 데이터가 전송될 때까지 블로킹된다는 점이다.

3. 도메인 네임 서비스와 주소 패밀리

3.1 도메인 네임 주소를 숫자 주소로 매핑하기

호스트 네이밍 서비스의 대표적인 두 개의 제공처는 도메인 네임 시스템(DNS, Domain Name System)과 지역 설정 데이터베이스이다.

DNS : 분산 데이터 베이스, 도메인 네임을 인터넷 주소 및 기타 정보로 매핑한다.

지역 설정 데이터베이스 : 네임 주소를 IP 주소로 매핑하기 위한 시스템이다.

 3.1.1 도메인 네임 서비스에 접근하기

호스트 주소/서비스 이름 모두의 이름을 입력으로 받아들이고, 그 이름에 해당하는 호스트 주소/서비스 이름 대상에 연결 가능한 소켓을 생성하기 위해서 필요한 모든 정보를 담은 구조체의 연결 리스트를 반환한다.

int getaddrinfo(const char *hostStr, const char *serviceStr, const struct addrinfo *hints, struct addrinfo **results)

1)hostStr : 호스트 네임 주소나 IP주소

2)serviceStr : 서비스 이름이나 포트 번호

3)hints : 반환되어야 하는 정보의 형태

4)results : 결과가 저장될 연결 리스트를 가리키게 된다.

5)return : 성공 시 0, 실패 시 0이 아닌 에러 코드

void freeaddrinfo(struct addrinfo *addrList) : 리스트에 할당된 메모리 반환

const char *gai_strerror(int errorCode) : getaddrinfo 에서 반환한 error 내용 출력

struct addrinfo {

int ai_flags;        // 제어 정보 해설을 위한 flag

int ai_family;      // 패밀리 : AF_INET, AF_INET6, AF_UNSPEC

int ai_socktype;    // 소켓 형태 : SOCK_STREAM, SOCK_DGRAM

int ai_protocol;    // 프로토콜 : 0(default) 또는 IPPROTO_XXX

socklen_t ai_addrlen;    // 소켓 주소인 ai-addr의 길이

struct sockaddr *ai_addr;    // 소켓을 위한 소켓 주소

char *ai_canonname;    // Canonical 네임

struct addrinfo *ai_next;    // 연결리스트에서 다음 addrinfo의 위치

};

4. UDP(User Datagram Protocol) 소켓

UDP의 기능

- IP 주소 위에 또 다른 계층의 주소(포트)를 제공한다.

- 전송 중에 일어날 수 있는 데이터 변조를 감지하여 변조된 데이터 그램을 제거한다.

UDP의 특성

- 사용하기 위해 연결할 필요가 없다.

- 메시지의 경계를 보존한다.

- 최선 전달(best effort) 서비스이다. (목적지 주소로 전달할 뿐, 메시지를 잘 받았는지 확인하지 않는다.)

4.1 UDP 클라이언트

순서 : socket() - sendto() - recvfrom() - close()

4.2 UDP 서버

순서 : socket() - bind() - recvfrom() - sendto() - close()

4.3 UDP 소켓을 이용한 데이터 송신 및 수신

ssize_t sendto(int socket, const void *msg, size_t msgLength, int flags, const struct sockaddr *destAddr, socklen_t addrLen)

ssize_t recvfrom(int socket, size_t msgLength, int flags, struct sockaddr *srcAddr, socklen_t *addrLen)

4.4 UDP 소켓의 연결

소켓을 통해 앞으로 전송 할 데이터그램의 목적지 주소를 고정하기 위해서 UDP 소켓 상에서 connect()를 사용하는 것이 가능하다.

5. 데이터의 송수신

 소켓을 사용하여 어떤 정보들을 다른 프로그램에 제공하거나, 혹은 다른 프로그램이 제공한 정보를 사용하기 위해서는 그 정보들이 어떤 비트열의 형태로 인코딩될 것인지에 대해 사전에 약속이 이루어져야 한다. 이러한 약속을 프로토콜이라고 부른다.

프로토콜은 연속적인 필드로 구성된 분리된 메시지의 형태로 정의가 된다. 각 필드의 비트들이 송신자에 의해서 어떻게 해석 또는 파싱(분석) 되는지 정확히 명시해서 수신자가 비트의 순서열에서 각 필드의 올바른 의미를 추출하도록 해야 한다.

- 소켓 프로그램 개발 경우

1) 소켓 양단에서 동작하는 프로그램을 모두 설계하고 개발하는 경우

2) 프로토콜 표준과 같이 이미 다른 사람이 정의해놓은 프로토콜을 구현하는 경우

5.1 정수 인코딩

5.1.1 정수의 크기

- TCP/IP는 8비트 단위 정보로 송수신한다. → 255 이상의 값을 보내야할 때 여러 바이트를 사용해서 인코딩되어야 한다.

  → 보내려는 정수의 크기를 결정해야 한다.

- sizeof(char) = 1, '1 바이트' 공간을 의미한다.

- CHAR_BIT = char(1바이트) 형의 값을 표현하기 위해 몇 비트를 사용했는지 알려주는 상수로 보통 8이다.

- C99 언어 표준 규격

int8_t, int16_t, int32_t, int64_t : 비트 단위로 크기가 명시됨

5.1.2 바이트 순서화

송신자와 수신자가 전송하려는 정수값의 크기를 정한 후, 바이트의 정수값을 어떠한 순서로 보낼지 결정해야 한다.

- little-endian : 숫자의 오른쪽 끝(하위 순서) 바이트로 시작하는 순서

- big-endian : 숫자의 왼쪽 끝(상위 순서) 바이트로 시작하는 순서

ex) 123456787654321L → 0x0000704885F926B1 → 0, 0 , 112, 72, 133, 249, 38, 177

-*big-endian 순서 전송(왼쪽 끝에서 시작) : 0, 0 , 112, 72, 133, 249, 38, 177

- little-endian 순서 전송(오른쪽 끝에서 시작) : 177, 38, 249, 133, 72, 112, 0, 0

네트워크 바이트 순서 : 인터넷에서 사용되는 대부분의 프로토콜들은 big-endian 바이트 순서를 사용한다. 

네이티브 바이트 순서 : 하드웨어가 사용하는 바이트 순서화 기법(big-endian or little-endian)

순서 변환 함수 : htons(), htonl(), ntohl(), ntohs()

※ - l : long, 32bit - s : short, 16bit - h : host - n : network

소켓 API는 네트워크 바이트 순서로만 되어있는 주소와 포트를 다룬다. 소켓 API 간 전송되는 주소와 포트 번호는 항상 네트워크 바이트 순서이다.

5.1.3 부호화와 부호 확장

송신자와 수신자가 동의해야 할 마지막 내용은 전송하는 숫자에 부호가 있는지 또는 부호가 없는지이다. 그리고 각기 다른 부호를 사용하는 정수형을 다룰 경우, 부호 확장을 주의해야 한다.

5.1.4 정수 인코딩을 직접 해보자

쉬프트(shifting)와 마스킹(masking)연산을 이용하여 메시지를 만든다.

예제에는 brute-force 방법을 사용함.

5.1.5 TCP 소켓을 스트림으로 포장하기

TCP 소켓에 복수바이트의 정수값을 인코딩하는 방법으로 내장된 파일 스트림 기능을 사용하는 방법이 있다.

FILE *fdopen(int socketdes, const char *mode) - 소켓을 스트림으로 포장한다.

int fclose(FILE *stream) - 하부 소켓과 함께 스트림을 닫는다

int fflush(FILE *stream) - 스트림에 버퍼링 되어 있는 데이터를 하부 소켓으로 밀어낸다.


size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)

5.1.6 구조체 오버레이 : 정렬과 채우기

이진 데이터를 포함하는 메시지를 구성하는 가장 흔한 방법은 일정 구역의 메모리에 C 구조체를 오버레이하고 구조체의 각 필드에 직접 할당하는 것이다.

- 정렬 : 데이터 구조 배치에 관한 규칙. 어떤 구조체 내부의 특정 필드는 그 자료형에 따라 구조체 내부의 특정 경계에서 시작 위치가 결정된다.

- 데이터 구조체는 최대한으로 정렬된다. 어떠한 구조체의 인스턴스 주소는 구조체 내부의 가장 큰 기본 정수 자료형으로 나뉘어 져야한다.

- 필드의 자료형이 2 바이트 이상의 다중바이트 정수형이라면 그 필드의 크기로 정렬된다.

이러한 제한 조건을 만족시키기 위해 컴파일러는 구조체 내부의 필드 사이에 채우기를 수행한다.

5.1.7 문자열과 텍스트

- 텍스트는 기호 혹은 문자의 순서열로 구성이 된다.

C 언어에서 문자열은 전통적으로 char의 배열로 표현된다. C 언어에서 하나의 char 값은 내부적으로는 정수값이다.

- 코드화된 문자 집합(coded character set) : 기호 집합과 정수 집합의 연관 관계

- C99 확장 표준은 기호당 1 바이트가 넘는 문자셋의 문자를 저장하기 위해 wchar_t 자료형을 지원한다.

- wchar_t로 이루어진 문자열과 네트워크 전송을 위해 인코딩된 바이트 순서열 간의 상호 변환을 하려면 wcstombs, mbstowcs를 사용한다.

size_t wcstombs(char *restrict s, const wchar_t *restrict pwcs, size_t n);

size_t mbstowcs(wchar_t *restrict pwcs, const char *restrict s, size_t n);

- 바이트 순서열로 인코딩하는 법

- 빅-엔디언 순서로 표현

- 리틀-엔디언 순서로 표현

- ASCII 내부에 있는 문자는 1 바이트 인코딩을 하고, ASCII에 없는 기호는 2 바이트 인코딩을 한다.

- C99 표준의 와이드 문자 기능은 프로그래머에게 인코딩 방식에 대한 명시적인 제어권을 제공하도록 설계되지 않았다.

플랫폼의 '로케일(locale)'에 따라 정의된 하나의 고정된 문자셋을 가정하고 있다.

5.1.8 비트 조작 : 참, 거짓 값의 인코딩

- 비트값을 조작하기 위해서는 C 언어의 '비트 조작(bit-diddling)'을 이용하여 개별 비트를 1로 설정하거나 0으로 해제한다.

5.2 메시지 생성, 프레이밍 그리고 파싱

- 메시지-처리 코드 기능

- 메시지 구조체의 정보를 인코딩한다.

- 스트림 소켓을 통해 전송한다.

- 스트림 소켓으로부터 데이터를 받아서 파싱한다.

- 메시지 구조체에 채우는 기능을 담당한다.

- 메시지-처리 코드 설계 시 보통 두 부분으로 분리하여 설계한다.

- 프레이밍하는 부분: 수신자가 스트림 내부에서 메시지를 구분할 수 있도록 한다.

- 인코딩하는 부분

- 프레이밍 코드

int GetNextMsg(FILE *in, uint8_t *buf, size_t bufSize);

주어진 스트림으로부터 데이터를 읽고, 버퍼에 저장한다. 버퍼에 저장된 바이트의 수를 반환한다.

int PutMsg(uint8_t buf[], size_t msgSize, FILE *out);

주어진 버퍼에 포함되어 있는 메시지에 프레임 정보를 추가하고 메시지와 프레임 정보를 주어진 스트림에 전송한다.

- 메시지 인코딩과 파싱 코드

bool Decode(uint8_t *inBuf, size_t mSize, VoteInfo *v);

VoteInfo(메시지) 구조체를 입력으로 받아들여서 특정 와이어 형식 인코딩에 따라 그것을 바이트 순서열로 변환하고 인코딩된 바이트 순서열의 크기를 반환한다.

size_t Encode(VoteInfo *v, uint8_t *outBuf, size_t bufSize);

특정 크기의 바이트 순서열을 받아들여 프로토콜에 따라서 이를 파싱하고 VoteInfo 구조체에 메시지 내용을 저장한다.

5.2.1 프레이밍

수신자가 메시지의 경계를 찾을 수 있도록 하는 일반적인 방법을 말한다.

- 구분자 기반(Delimiter-based) 방식 : 메시지의 끝을 특별한 기호로 나타낸다. (텍스트로 인코딩된 메시지에 자주 사용)

- 길이 명시(Explicit length) 방식 : 크기를 나타내는 길이 필드는 가변 길이의 필드나 메시지에 앞서 위치한다. (메시지의 최대크기에 대한 제한이 존재한다.)

5.2.2 텍스트 기반의 메시지 인코딩

5.2.3 이진 형식의 메시지 인코딩



 







Comments