티스토리 뷰

프로그래밍/Network

Socket Introduction (1)

국윤창 2017. 4. 5. 22:58

1. Introduction

이번 챕터에서는 socket API에 대해 설명한다. 이 책의 대부분의 예제에선 나오는 socket address structure들부터 시작한다. 이 구조체들은 process에서 kernel, kernel에서 process 이렇게 두 방향으로 보내진다. 후자의 경우 value-result argument의 경우이다. 우리는 이러한 argument들의 여러가지 예제들을 이책을 통해서 보게 될 것이다.

address conversion 함수는 text로 표현한 주소와 socket address structure에 들어가는 이진값 사이에 변환을 한다. 현존하는 IPv4 코드들은 inet_addr, inet_ntoa들을 사용하고, 새로운 두 개의 함수 inet_pton, inet_ntop는 IPv4와 IPv6 둘 다 지원한다.

이러한 address 변환 함수들의 문제점은 변환될 address의 타입에 종속적이라는 것이다. (IPv4 또는 IPv6) 우리는 socket address structure를 이용해 protocol에 독립적으로 동작하도록 sock_으로 시작하는 이름의 함수들을 개발할 것이다. 이렇게 개발한 함수들을 코드를 protocol에 독립적으로 만들기 위해, 이 책의 나머지 부분에서 사용한다.


2. Socket Address Structures

대부분의 socket 함수들은 매개변수로 socket address structure의 포인터를 요구한다. 각각의 프로토콜은 자신만의 socket address structure를 정의한다. 이러한 구조체들의 이름은 sockaddr_로 시작하고, 각각의 프로토콜에 대해 고유의 접미사를 붙인다.


1) IPv4 Socket Address Structure

보통 "Internet socket address structure"이라고 불리는 IPv4 socket address structure는 sockaddr_in이라는 이름의 구조체이고, <netinet/in.h> 헤더파일 안에 선언되어있다. 아래 그림 1은 POSIX에서 선언되어있는 sockaddr_in을 보여준다.

[그림 1] The Internet (IPv4) socket address structure: sockaddr_in


아래의 예제를 통해 일반적으로 사용되는 socket address structure에 대한 몇가지 특징을 알아보자.

* 길이 멤버 sin_len은 OSI 프로토콜에 대한 지원이 추가되었을 때 4.3BSD-Reno에서 추가되었다. 이러한 사항이 배포되기전에, 첫 번째 멤버는 전통적으로 unsigned short타입인 sin_family였다. 네트워크 구현 배포자들은 socket address structure에 대한 길이 멤버를 항상 지원하도록 한 것은 아니었으며, POSIX의 socket address structure 명세는 이 멤버를 필요로 하지 않는다. (그림 2 참고)

길이 멤버를 가지고 있으면 다양한 길이의 socket address structure를 다루는 것을 간단하게 만들 수 있다.

[그림 2] Datatypes required by the POSIX specification


* 비록 길이 멤버가 존재하더라도 우리는 이 길이 멤버롤 설정해야하거나 조사할 필요는 없다. 하지만 routing socket을 다뤄야 한다면 얘기는 달라진다. (챕터 18에 나옴) 다양한 프로토콜에서 socket address structure을 다루는 것은 routine에 의해 커널에서 이루어진다.

프로세스에서 커널로 socket address structure을 통과시키는 4개의 socket function들 (bind, connect, sendto, sendmsg) 모두 Berkeley derived implementation에서 구현된 sockargs 함수를 거친다. (TCPv2의 p.452에 나온다고한다...) 이 함수는 socket address structure를 프로세스로부터 복사하고, 명시적으로 sin_len 멤버를 위의 4개의 함수에서 매개변수로 받은 구조체의 길이로 설정한다. 커널에서 프로세스로 socket address structure를 통과시키는 함수는 5개가 있으며 (accept, recvfrom, recvmsg, getpeername, getsockname), 모두 프로세스로 반환하기 전에 sin_len 멤버를 설정(set)한다.

안타깝게도 보통 socket address structure에 대한 길이 멤버를 네트워크 구현(예를들면 BSD-derived implementation 같은..)이 지원을 해주는지에 대한 간단한 컴파일타임 테스트가 존재하지 않는다는 것이다. 이 책의 코드에서는 HAVE_SOCKADDR_SA_LEN 상수를 통해 테스트 할 것이다. (부록 D.2의 Config.h 참조) 그러나 이 상수가 정의되었는지는 optional 구조체 멤버와 컴파일이 제대로 되었는지 알아보는 간단한 테스트 프로그램을 사용해봐야한다. 아래 그림 4에서 IPv6 구현들은 socket address structure이 길이 멤버를 가지고 있는지에 대한 SIN6_LEN의 정의가 필요함을 볼 수 있다. 몇몇 IPv4 구현들은 컴파일 타임에 사용할 수 있는 socket address structure의 길이 필드를 어플리케이션에게 제공해준다. (예를들면, _SOCKADDR_LEN) 이러한 특징은 오래된 프로그램에 호환성을 제공해준다.


* POSIX의 명세는 socket address structure가 오직 3개의 멤버만 필요로 한다. (sin_family, sin_addr, sin_port) POSIX-compliant implementation에서 socket address structure의 추가 멤버를 정의하는 것이 가능하며, Internet socket address structure에서도 보통 가능하다. 대부분의 구현들은 socket address structure가 최소 16바이트 이상의 크기를 가지게 하기 위해서 sin_zero 멤버를 추가한다.


* POSIX 데이터 타입 s_addr, sin_family, sin_port 멤버에 대해서 알아보자. in_addr_t 데이터 타입은 최소 32비트 이상의 unsigned integer이어야 하고, in_port_t 는 최소 16비트 이상의 unsigned integer이어야 하고, sa_family_t는 아무 unsigned integer이면 된다. 구현이 길이 멤버를 지원하면 sa_family_t는 보통 8비트의 unsigned integer이다. 만약 길이 멤버를 지원해주지 않으면 16비트의 unsigned integer이다. 그림 2는 위의 세가지 POSIX-defined datatype들을 우리가 이책에서 보게될 다른 POSIX datatype들과 함께 리스트로 만들어놨다.


* 우리는 u_char, u_short, u_int, u_long 데이터타입도 보게될텐데 이들은 모두 unsigned이다. POSIX 명세는 이러한 것들을 note에 정의해놨는데 이들은 더이상 필요가 없다. 이들은 모두 이전의 호환에서 제공된다.


* IPv4 address와 TCP 또는 UDP port는 반드시 network 바이트 순서에 따라 structure에 저장되어야한다. 우리는 이 멤버들을 사용할 때 이 사항을 반드시 알고 있어야한다. Section 3.4에서 호스트의 바이트 순서와 네트워크의 바이트 순서의 차이점을 볼 것이다.


* 32비트의 IPv4 address는 두가지 방법으로 접근할 수 있다. 예를들어 만약 serv가 Internet socket address structure로 정의되어있으면, serv.sin_addr는 32비트의 IPv4 address를 in_addr structure로 참조하게 된다. 반면에 serv.sin_addr.s_addr은 같은 32비트의 IPv4 address를 in_addr_t로 참조하게 된다.(in_addr_t는 보통 32bit의 unsigned integer) 우리는 반드시 IPv4 address 참조를 정확히 해야한다. (특히 함수의 매개변수로 쓸 때) 왜냐하면 컴파일러는 이 구조체들을 integer와는 다르게 통과시키기 때문이다.

sin_addr 멤버가 구조체인 이유와 in_addr_t만 쓰지 않는 이유는 옛날부터(전통적으로) 그래왔기 때문이다. 초기 release(4.2BSD)들은 in_addr 구조체를 다양한 구조체들의 union으로 정의했다. 그 이유는 32비트의 IPv4 주소안의 16-bit 값에 각각 접근이 가능하도록 하기 위해서이다. 이 값들은 class A, B, C 주소들과 함께 주소의 적절한 바이트들을 가지고 오도록 하기 위해 사용되었다. 그러나 subnet의 등장과 함께 이런 다양한 address class들이 사라지고 classless address들이 나오게 되었으며 (Section A.4), union의 필요성이 사라지게 되었다. 오늘날의 대부분의 시스템들은 union을 사용하지 않고 단지 in_addr_t 멤버 하나만 가지고 있는 in_addr 구조체만 사용하게 되었다. 


* sin_zero 멤버는 사용되지 않지만 구조체내에서 항상 0으로 채워진다. 관습에따라 우리는 sin_zero 멤버뿐만아니라, 나머지 구조체 멤버를 채우기전에 0으로 초기화한다.

비록 이 구조체를 사용할 때 대부분은 이 멤버가 0이될 필요가 없지만, non-wildcard IPv4 address를 bind할 때 이 멤버는 반드시 0이 되어야한다. (TCPv2의 p.731~732 참고...)


* Socket address structure들은 주어진 호스트 상에서만 사용된다. 비록 특정한 멤버들이 통신하는데 사용된다고 하더라도 구조체 그 자체는 다른 호스트들과 통신하는데 사용되지 않는다. (뭔 말인지 좀 헷갈림)


2) Generic Socket Address Structure

socket address structure는 socket 함수들에서 매개변수로 넘겨질 때 항상 reference로 넘겨진다.(sockaddr_in 구조체가 socket함수에서 항상 포인터로 넘겨지는 것을 기억하자) 그러나 매개변수로 이런 포인터들을 받는 함수들은 어떠한 프로토콜로부터 socket address structure를 받던지 반드시 다룰 수 있어야한다. (sockaddr은 TCP/IP에서만 쓰이는게 아닌 RFC 주소 표준이다. sockaddr_in은 Internet address family구조에 따라 쓰기 쉽게 하기 위해 있는것이다. 따라서 주소 표준인 sockaddr로 파싱해야한다.)

문제는 어떻게 매개변수로 넘겨지는 포인터의 타입을 선언하냐는 것이다. ANSI C에서는 해결방법이 간단하게 generic pointer인 void*로 파싱하는 것이다. 그러나 socket 함수들은 ANSI C보다 먼저 나왔기 때문에 해결방법은 1982년에 채택되었다. 그 방법은 generic socket address structure를 <sys/socket.h> 안에 선언해서 그것으로 파싱하는 것이다. 아래 그림 3에 sockaddr 이름의 구조체로 나와있다.

[그림 3] The generic socket address structure: sockaddr


그래서 socket 함수들은 generic socket address structure에 대한 포인터를 받도록 정의됐다. 아래의 bind함수의 ANSI C 함수 프로토타입을 보면 알 수 있다.


int bind(int, struct sockaddr*, socklen_t);


이 함수는 호출 시 반드시 protocol-specific socket address structure의 포인터를 generic socket address structure의 포인터가 되도록 형변환을 해야한다. 예를 들어,


struct sockaddr_in serv;    /* IPv4 socket address structure */


/* fill in serv{} */


bind(sockfd, (struct sockaddr*)&serv, sizeof(serv));


만약 우리가 "(struct sockaddr*)" 캐스트를 빠뜨리면, C 컴파일러는 "warning: passing arg 2 of 'bind' from incompatible pointer type." 이라는 경고를 생성할 것이다.

어플리케이션 프로그래머의 관점으로, generic socket address structure는 단지 protocol-specific structure에 대한 포인터를 형변환할 때 사용될 뿐이다.

Section 1.2로 돌아가서 unp.h를 보면 , 우리는 SA를 "struct sockaddr" 문자열로 정의했다. (단지 코드를 짧게하기 위해서)

커널의 관점으로 보면, 매개변수로 generic socket address structure의 포인터를 사용하는 이유는 커널은 반드시 호출자의 포인터를 받아야하고, 이것을 struct sockaddr*로 형변환해서 포인터의 타입을 나타내는 sa_family의 값을 보기 위해서이다. 그러나 어플리케이션 프로그래머의 관점으로 보면 명시적인 형변환 대신 void* 포인터 타입 더 간편하다. (하지만 그래서는 안된다.. 이딴걸 왜 책에 써놨을까?)


3) IPv6 Socket Address Structure

IPv6 socket 주소는 <netinet/in.h>안에 정의되어있다. 아래 그림 4에 나와있다.

[그림 4] IPv6 socket address structure: sockaddr_in6


IPv6에 대한 확장 socket API들은 RFC 3493[Gilligan et al. 2003]에 정의되어있다.

아래는 그림 4에 대한 특징들이다.

* 시스템이 socket address structure에 대한 길이 멤버를 지원한다면 SIN6_LEN 상수는 반드시 정의되어야한다.

* IPv6 family는 AF_INET6인 반면에, IPv4 family는 AF_INET이다.

* 이 구조체의 멤버들은 정렬되어있다. 왜냐하면 sockaddr_in6 구조체가 64비트 정렬이면 sin6_addr 멤버가 128비트여야하기 때문이다. 몇몇 64비트 프로세서들에선, 데이터들이 64비트 경계로 저장돼있으면 64비트 값들에 접근하는것이 최적화되어있다.

* sin6_flowinfo 멤버는 2가지 필드로 나뉜다.

low-order 20비트는 flow label

high-order 12비트는 할당되어있는 공간(reserved)

flow label은 부록A의 그림 A.2에 설명되어있다.(아래 그림으로 첨부함) flow label 필드의 사용은 아직도 연구주제이다.

[그림 A.2] Format of the IPv6 header


* sin6_scope_id는 scope zone을 나타낸다.(identify) scope zone에서 scoped address는 중요하고, 대부분 보통 link-local address (Section A.5)에 대한 interface index이다.


4) New Generic Socket Address Structure

새로운 generic socket address structure는 기존의 sockaddr 구조체의 몇가지 결점들을 극복하기 위해 IPv6 socket API의 일부분으로 정의되었다. sockaddr 구조체와는 달리 새 sockaddr_storage 구조체는 시스템에 의해 지원되는 모든 socket address 타입들을 다룰 수 있을만큼 충분히 크다. sockaddr_storage rnwhcpsms <netinet/in.h> 안에 포함돼있으며, 아래 그림 5에 나와있다.

[그림 5] The storage socket address structure: sockaddr_storage


sockaddr_storage 타입이 제공하는 generic socket address structure는 struct sockaddr이 제공하는 것과 두 가지 다르다.


a. 만약 시스템이 제공하는 socket address 구조체들이 정렬(alignment)을 필요로한다면, sockaddr_storage는 가장 엄격한 정렬을 제공한다. 

b. sockaddr_storage는 시스템이 제공해주는 어떤 socket address 구조체도 포함할 수 있을만큼 충분히 크다.


sockaddr_storage의 필드들은 ss_family와 ss_len을 제외하고 유저에게 비공개임을 알아두자. sockaddr_storage는 반드시 적절한 socket address 구조체로 형변환되거나 복사되어야한다. 왜냐하면 ss_family로 주어진 주소가 다른 필드들에 접근할 수 있도록 해야하기 때문이다.


5) Comparison of Socket Address Structures

아래 그림 6은 우리가 앞으로 보게 될 다섯가지 socket address 구조체들을 비교해 놓은 것이다. (IPv4, IPv6, Unix domain, datalink, storage) 이 그림에서 모든 socket address 구조체는 1바이트의 길이 필드와 1바이트의 family 필드를 가지고 있고, 다른 필드들은 최소한 몇 비트 이상이라고 가정하자.

[그림 6] Comparison of various socket address structures.


socket address 구조체들 중 2가지는 고정길이인 반면, Unix domain 구조체와 datalink 구조체는 가변길이다. 가변길이 구조체를 다루기 위해선, socket 함수에 socket address 구조체의 포인터를 매개변수로 넣을 때, 다른 매개변수로 길이도 함께 넣어줘야 한다. 몇 바이트 크기의 고정길이 구조체들이 각각의 구조체 아래 존재하는 것을 볼 수 있다.


sockaddr_un 구조체 자체는 가변길이가 아니다. 하지만 구조체 안의 pathname이 가변길이이다. 이 구조체의 포인터를 넘길 때, 우리는 반드시 길이필드를 어떻게 다룰지에 대해 고려해야한다. socket address 구조체 안의 길이 필드와 커널로 또는 커널로부터의 길이를 둘 다 고려해야한다.

위 그림에서 우리는 이책에서 앞으로 다룰 스타일을 알 수 있다: 구조체 이름은 항상 굵은 글씨이고, 꺽쇄 괄호가 함께 온다. (sockaddr_in{})

우리가 이전에 봤듯이, 길이 필드들은 4.3BSD Reno 릴리즈와 함께 모든 socket address 구조체에 추가되었다. 초기 릴리즈의 socket에 길이 필드가 존재했지만, 모든 socket 함수들에 길이 매개변수가 필요가 없다. (예를 들어, bind와 connet 함수의 3번째 매개변수) 대신 구조체의 크기는 구조체의 길이 필드에 포함될 수 있다.

'프로그래밍 > Network' 카테고리의 다른 글

Socket Introduction (3)  (0) 2017.06.22
Socket Introduction (2)  (0) 2017.05.22
Transport Layer (3)  (0) 2017.03.13
Transport Layer (2)  (0) 2017.02.24
Transport Layer (1)  (0) 2017.02.21
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/03   »
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
31
글 보관함