최근에 (1년 전에?) 제일 정리하고 싶었던 글인데, 실제로 network programming이 절실할 때가 와서야 하게 되었다 (흑흑). 원문은 Network programming에 대해 모르는 개발자에게 딱 좋은 튜토리얼이라고 생각한다. 여러 미사여구와 필자의 썰은 가지쳐내고 핵심적인 내용만 정리해본다 (Part 2 대기 중).
2. What is a Socket?
- Socket은 standard Unix file descriptor를 통해서 다른 프로그램과 소통하는 통로이다. 이 file descriptor는
socket()
system routine을 호출해서 얻는다. 그리고send()
와recv()
socket call을 이용해 통신할 수 있다. 다른 file descriptor처럼read()
와write()
를 통해서도 가능하지만,send()
와recv()
가 데이터 송수신에 대해 더 많은 제어권을 제공해준다. - Unix file descriptor, "Everything is a file!"에 대한 자세한 이야기는 원문을 참조 바란다.
- 여러 종류의 socket이 있지만, 여기서는 Internet Socket (DARPA Internet address)에 대해서만 다룬다.
2.1. Two Types of Internet Sockets
Stream Sockets
SOCK_STREAM
, Datagram SocketsSOCK_DGRAM
(connectionless sockets)Stream sockets
Datagram sockets
- Datagram을 보낸다면, 도착할 수도 안 할 수도 있다 (unreliable). 순서대로 오지 않을 수도 있다. 만약 도착했다면, 그 packet이 담고 있는 데이터에는 오류가 없다.
- UDP (User Datagram Protocol, RFC 768 참조)를 사용하고, routing을 위해 IP도 사용한다.
- Connectionless인 이유? (Stream socket과 다르게) 연결을 유지할 필요가 없기 때문. Packet을 만들어서 IP header에 목적지를 적고 보내버릴 뿐이다.
- TCP를 사용할 수 없는 환경일 때, 혹은 몇 packet이 도착하지 못한다고 해서 세상이 망하지 않을 때 사용한다.
- 사용 예: tftp (trivial file transfer protocol), dhcpcd (DHCP client), multiplayer game, streaming audi, video conferencing 등등.
- tftp는 UDP를 기반으로 독자적인 protocol을 사용해서 packet이 도착했음을 송신자에게 알려준다 ("ACK" packet). ACK packet이 도착할 때까지 재시도를 하므로 데이터의 온전한 통신을 보장한다.
- Unreliable의 단점이 있는 만큼, 빠르다는 장점이 있다.
채팅 메세지를 보낸다면? 입력 순서를 지키는 TCP가 좋겠다, 전세계 플레이어들의 위치 정보를 1초에 40개 씩 업데이트할거라면? (그리고 그 중 몇 개는 업데이트를 실패해도 괜찮다면?) UDP가 훨씬 좋겠지?
- TCP, UDP 외에 "Raw Socket"이라는 것도 있다고 한다.
2.2. Low Level Nonsense and Network Theory
아래 그림은
SOCK_DGRAM
이 어떻게 만들어지는지 표현한 그림이다.
Data Encapsulation
Data Encapsulation
- 태초에 한 packet이 있었고, 여기에 첫 번째 protocol에 의해 (위 그림에서는 TFTP protocol) header가 붙어서 (매우 드물게 footer도 붙는다) 포장이 된다 ("encapsulated").
- 그렇게 포장된 packet은 다음 protocol (그림에서는 UDP protocol)에 의해 encapsulated 되고, 그 다음 protocol (IP), 그리고 마지막 protocol으로는 하드웨어 (물리적) layer (여기서는 Ethernet)에 의해서 encapsulated 된다.
- 다른 컴퓨터가 이 packet를 받았다면, 우선 하드웨어가 Ethernet header를 풀고, kernel이 IP와 UDP header를 풀고, TFTP 프로그램이 TFTP header를 풀어서 드디어 데이터를 볼 수 있게 된다.
Layered Network Model (a.k.a. "ISO/OSI")
- 본인이 집중하고자 하는 layer 이외에는 신경쓰지 않아도 된다는 이점이 있다. e.g., socket 프로그래머는 어떻게 데이터가 물리적으로 전송되는지 (serial, thin Ethernet, AUI, 뭐든지간에) 몰라도 socket 프로그램을 작성할 수 있다는 것이다.
- Application / Presentation / Session / Transport / Network / Data Link / Physical
- Unix 환경에는 이렇게 4계층으로 나누는 것이 좀 더 적절하다.
- Application Layer (telnet, ftp, 등)
- Host-to-Host Layer (TCP, UDP)
- Internet Layer (IP 및 routing)
- Network Access Layer (Ethernet, wi-fi, whatever)
- 이런 layer들이 encapsulation과 연결이 됨을 알 수 있다.
- IP와 routing에 대해서는 몰라도 된다. 정 알고 싶다면 IP RFC를 참조.
3. IP Adresses, struct
s, Data Munging
3.1. IP Addresses, Versions 4 and 6
- 기존 network routing system인 IPv4 (Internet Protocol Version 4)는 4 bytes (a.k.a. 4 "octets")으로 이루어진 주소를 사용했다. 숫자와 점으로 이루어진 형식으로 흔히 쓰인다,
192.0.2.111
처럼. - Vint Cerf (Internet의 아버지)가 "IP 주소가 다 떨어지면 어떡할래?" 라고 의문을 제기해서 IPv6가 만들어짐.
- IPv6는 128 bit의 주소를 사용하기 때문에 천문학적인 개수의 주소를 제공한다 (2의 128 제곱).
- 2 byte 묶음을 16진법으로, colon으로 연결해서 표기한다,
2001:0db8:c9d2:aee5:73e3:934a:a5ae:9551
처럼. - 숫자 0은 생략될 수 있어서, 다음 주소 쌍들은 같은 주소가 된다.
2001:0db8:c9d2:0012:0000:0000:0000:0051 == 2001:db8:c9d2:12::51
2001:0db8:ab00:0000:0000:0000:0000:0000 == 2001:db8:ab00::
0000:0000:0000:0000:0000:0000:0000:0001 == ::1
::1
주소는 _loopback address_로, "지금 작동하고 있는 이 컴퓨터"를 뜻한다. IPv4로는127.0.0.1
이다.
- IPv6는 IPv4에 대해서 호환성이 있다.
192.0.2.33
은::ffff:192.0.2.33
으로 표기할 수 있다.
3.1.1. Subnets
- IP 주소의 첫 몇 bit는 network 부분이고 그 이후의 bit는 host 부분이라고 분리하는 것이 구조적인/조직적인 관리 면에서 (for organizational reasons) 편리할 때가 있다.
192.0.2.12
에서 첫 세 byte는 network이고 마지막 byte는 host라고 할 수 있다. Host 부분을 0으로 지운192.0.2.0
network에서의 12번째 host라고 말할 수도 있다.- 고대에는 subnet의 "class"라는 것이 있었다. "Class A" network는 첫 byte가 network / 나머지 세 byte가 host 부분이고, "Class C" network는 첫 세 byte가 network / 나머지 한 byte가 host 부분이다. "Class B" network는 그 중간에 있다. 따라서 Class C network는 256개의 host를 가질 수 있고, Class A network는 2의 24 제곱 개의 host를 가질 수 있다.
- IP 주소의 network 부분은 _netmask_라는 것으로 표기할 수 있는데, IP 주소와 netmask에 bitwise-AND를 적용시켜서 나오는 주소가 network 부분이 된다. IP 주소가
192.0.2.12
이고 netmask가255.255.255.0
이라면, network 부분은192.0.2.0
이 된다. - 현재는 8, 16, 24 bit의 class 구분보다는 더 세밀하게 한 bit 단위로 network / host 부분을 나눌 수 있다. Netmask가
255.255.255.252
라면, 30 bit가 network 부분이고 나머지 2 bit가 host 부분이 된다. Netmask는 항상 1의 연속 이후의 0의 연속이 된다. - 직관적으로 알아보기 어려운 netmask 대신 새로운 표기법으로 IP 주소 뒤에 network bit 개수를
192.0.2.12/30
처럼 쓸 수도 있다. IPv6의 경우2001:db8::/32 or 2001:db8:5413:4028::9db9/64
와 같은 형식이 된다.
3.1.2. Port Numbers
- Layered Network Model에서 Internet Layer (IP)와 Host-to-Host Transport Layer (TCP와 UDP)가 분리되어 있었던 것을 기억하자.
- IP 주소에 더해서, TCP와 UDP가 사용하는 또 다른 주소가 있는데, 이것이 port number다.
- Port number는 16 bit의 숫자로, 한 연결의 local address 같은 것이다.
- IP 주소가 호텔의 복도 주소라면, port number는 해당 복도의 방 번호이다.
- 하나의 컴퓨터에서 메일도 받고, 그리고 웹 서비스도 제공하고 싶은데, 하나의 IP 주소를 가진 컴퓨터가 어떻게 이 두 가지를 구분할 수 있을까?
- Internet의 여러 서비스는 익히 알려진 port number를 사용하는데, the Big IANA Port List에서 이를 죄다 확인할 수도 있고, Unix 계열에서는
etc/services
파일에서 볼 수도 있다.- HTTP는 port 80, telnet은 port 23, SMTP는 port 25, 게임 DOOM)은 port 666을 사용한다.
- 1024 이하의 port number는 특별한 것으로 종종 인식하며, 이를 사용하려면 대개 특별한 OS 권한을 필요로 한다.
3.2. Byte Order
- 인터넷 세상에서 2-byte hex 숫자
b34f
를 저장할 때, - Big-Endian은 순서대로 저장한다, 즉
b3
을 먼저 저장하고4f
를 저장한다. 이는 Wilford Brimley가 말하길 제대로 된 방법이다. - Little-Endian은 반대로 저장한다. 즉
4f
를 먼저, 그 다음에b3
을 저장한다. 인텔 계열의 프로세서가 주로 이렇게 한다. - Network Byte Order는 네트워크가 byte를 저장하는 순서이고, Host Byte Order는 우리가 쓰는 컴퓨터가 사용하는 순서이다.
- Network Byte Order는 Big-Endian이다.
- Host Byte Order는 기계에 따라 다르다. Intel 80x86이라면 Little-Endian, Motorola 68k라면 Big-Endian, PowerPC라면... 그 때 그 때 다르다!
- 내 컴퓨터의 Host Byte Order가 뭔지 모르는데 어떻게 Network Byte Order에 맞는 packet을 만들 수 있을까? 이를 담당하는 함수들을 사용해서, endianness가 다른 문제를 해결할 수 있다.
- 이 함수들이 변환하는 숫자로는
short
(2 bytes)과long
(4 bytes) 이렇게 두 가지 형태만 가능하다. 그리고unsigned
숫자이어야 한다. - 함수는
h
(host),n
(network),to
,s
(short),l
(long)의 조합으로 이름지어져 있다. 예를 들어볼까htons()
: Host to Network Shorthtonl()
: Host to Network Longntohs()
: Network to Host Shortntohl()
: Network to Host Long
- 앞으로 이 문서에서 말하는 숫자들은 Host Byte Order를 따른다고 가정한다.
3.3. Struct
s
Socket descriptor는
int
타입이다.addrinfo
structure는 socket address structure를 자주 사용할 수 있도록 만들어진 것이다. Host name이나 service name을 찾거나 다른 여러 용도에 쓰이지만, 일단은 네트워크 연결을 위해서 가장 처음으로 만들어야 할 것이라고만 알아두자.
struct addrinfo {
int ai_flags; // AI_PASSIVE, AI_CANONNAME, etc.
int ai_family; // AF_INET, AF_INET6, AF_UNSPEC
int ai_socktype; // SOCK_STREAM, SOCK_DGRAM
int ai_protocol; // use 0 for "any"
size_t ai_addrlen; // size of ai_addr in bytes
struct sockaddr *ai_addr; // struct sockaddr_in or _in6
char *ai_canonname; // full canonical hostname
struct addrinfo *ai_next; // linked list, next node
};
- 이 struct를 가져와서
getaddrinfo()
를 실행하면, 우리가 필요한 모든 정보들이 담은 structure들의 linked list를 만들어서 그 pointer를 반환한다. ai_family
를 통해 IPv4 / IPv6를 사용할지, 아니면AF_UNSPEC
으로 제한을 두지 않을지 설정할 수 있다. 이는 코드가 IP 버전에 따라 다르게 행동을 하도록 할 수 있다는 좋은 점이다.- 우리가 받은 것이 linked list이므로 여러 structure가 들어올 수 있음을 항상 생각하자. 어떤 주소를 사용하는지는 당신이 판단할 몫이다.
대부분의 경우
getaddrinfo()
를 호출해서 이미 내용물이 채워진struct addrinfo
를 받기 때문에 이 structure를 손수 만들 필요는 없다.struct sockaddr
은 여러 타입의 socket address 정보를 담고 있다.
struct sockaddr {
unsigned short sa_family; // address family, AF_xxx
char sa_data[14]; // 14 bytes of protocol address
};
sa_family
는 여러 값이 올 수 있지만, 여기서는AF_INET
(IPv4)와AF_INET6
(IPv6)만 사용할 것이다.sa_data
는 socket의 목적지 주소와 포트를 담는 곳이다. 이를 손수 만드는 것은 매우 더러운 일이다.struct sockaddr
를 다루기 위해서는 이에 병행하는struct sockaddr_in
("Internet"의 "in")을 만들어야 한다.- 여기서 매우 중요한 점은,
struct sockaddr_in
의 pointer는struct sockaddr
의 pointer로 바꿔서(type casting) 쓸 수 있고, 그 반대도 마찬가지라는 점이다. 그래서connect()
가struct sockaddr*
을 필요로 하더라도, 우리는 계속struct sockaddr_in
을 쓰다가 마지막에 cast해서 넘겨주면 된다!
// (IPv4 only--see struct sockaddr_in6 for IPv6)
struct sockaddr_in {
short int sin_family; // Address family, AF_INET
unsigned short int sin_port; // Port number
struct in_addr sin_addr; // Internet address
unsigned char sin_zero[8]; // Same size as struct sockaddr
};
- 이 structure는 socket address의 원소들을 가리키는 것을 쉽게 해준다.
sin_zero
는struct sockaddr
과 그 길이를 똑같이 맞춰주기 위한 필드로, 반드시memset()
을 통해 모두 0으로 값을 가지도록 해야 한다.sin_family
는struct sockaddr
의sa_family
와 일치해야 하므로, 여기서는AF_INET
이어야 한다.sin_port
는 반드시 Network Byte Order를 따라야 한다.htons()
를 쓰면 된다!sin_addr
는struct in_addr
이라고 나와있는데, 이것은 역대 최고로 무시무시한 union 중 하나였지만, 다행히도 바뀌었다.
// (IPv4 only--see struct in6_addr for IPv6)
// Internet address (a structure for historical reasons)
struct in_addr {
uint32_t s_addr; // that's a 32-bit int (4 bytes)
};
ina
를struct sockaddr_in
타입으로 선언했다면,ina.sin_addr.s_addr
은 4-byte의 IP 주소를 (Network Byte Order에 따라서) 나타낸다.위 structure들의 IPv6 버전은 원문을 참조하길 바란다.
struct sockaddr_storage
는 IPv4와 IPv6 구조를 모두 담을 수 있도록 크게 만들어진 것이다.
struct sockaddr_storage {
sa_family_t ss_family; // address family
// all this is padding, implementation specific, ignore it:
char __ss_pad1[_SS_PAD1SIZE];
int64_t __ss_align;
char __ss_pad2[_SS_PAD2SIZE];
};
- IPv4일지 IPv6일지 모르는 상황에서 우선 이 parallel structure를 사용하면 된다.
ss_family
가AF_INET
인지AF_INET6
인지 확인했다면, 이에 맞게struct sockaddr_in
이나struct sockaddr_in6
로 cast하면 되는 것이다.
3.4. IP Address, Part 2
- IP 주소를 다루는 함수들이 여럿 있어서 손수 조작할 필요는 없다.
struct sockaddr_in ina
가 있고, 여기에 넣고 싶은 IP 주소가10.12.110.57
또는2001:db8:63b3:1::3490
이라고 하자.inet_pton()
은 위 주소를struct in_addr
이나struct in6_addr
에 맞게 넣어준다.pton
은 "presentation (또는 printable) to network"을 의미한다. 아래 코드 예를 보면 더 쉽게 이해할 수 있다.
struct sockaddr_in sa; // IPv4
struct sockaddr_in6 sa6; // IPv6
inet_pton(AF_INET, "10.12.110.57", &(sa.sin_addr)); // IPv4
inet_pton(AF_INET6, "2001:db8:63b3:1::3490", &(sa6.sin6_addr)); // IPv6
위 예제는 오류를 확인하지 않으므로 좋은 코드는 아니다.
inet_pton()
함수는 오류일 때-1
을, 주소값이 이상할 때는0
을 반환하므로, 이를 항상 먼저 확인하도록 하자!그 반대의 경우는
inet_ntop()
("network to presentation/printable")를 쓰면 된다.
// IPv4:
char ip4[INET_ADDRSTRLEN]; // space to hold the IPv4 string
struct sockaddr_in sa; // pretend this is loaded with something
inet_ntop(AF_INET, &(sa.sin_addr), ip4, INET_ADDRSTRLEN);
printf("The IPv4 address is: %s\n", ip4);
// IPv6:
char ip6[INET6_ADDRSTRLEN]; // space to hold the IPv6 string
struct sockaddr_in6 sa6; // pretend this is loaded with something
inet_ntop(AF_INET6, &(sa6.sin6_addr), ip6, INET6_ADDRSTRLEN);
printf("The address is: %s\n", ip6);
INET_ADDRSTRLEN
과INET6_ADDRSTRLEN
은 macro로 지정되어 있는 상수이므로 이를 사용하는 것이 매우 편리하다.- 지금까지 봤던 함수들은 오직 숫자형 IP 주소에 대해서만 제대로 작동한다. "www.example.com"과 같은 주소를 DNS에서 찾아서 hostname을 가져오지는 않는다는 것이다. 이를 위해서는
getaddrinfo()
를 사용하게 될 것이다.
3.4.1. Private (또는 Disconnected) Networks
- 바깥 세상으로부터 보호하기 위해 네트워크를 숨기는 방화벽을 사용하는 곳이 많이 있다.
- 그리고 방화벽은 Network Address Translation (NAT)라는 과정을 통해 "내부 (internal)" IP 주소를 "외부 (external)" IP 주소로 바꿔주기도 한다.
- 질문: 우리 집에 방화벽이 있다. 두 개의 정적 IPv4 주소를 DSL 회사로부터 할당받아서 쓰고 있는데, 네트워크 안에는 7개 컴퓨터가 돌고 있다. 이게 어떻게 가능할까? 같은 IP 주소를 여러 컴퓨터가 동시에 가질 수 없지 않나? 만약 동시에 가지게 된다면, 데이터가 어느 컴퓨터로 갈지 알 수가 없다!
- 답: 하나의 IP 주소를 동시에 여러 컴퓨터가 가질 수 없는 것이 맞다. 이 컴퓨터들은 이천 사백만 IP 주소를 할당할 수 있는 private network 위에 있었다. 이 네트워크는 오로지 나만을 위한 것이다.
- 외부 컴퓨터에 접속을 해보면, 나는
192.0.2.33
에서 접속하는 것으로 나타나는데, 이 IP 주소는 나의 ISP가 제공한 public IP 주소이다. 하지만 실제로 접속을 시도한 내 컴퓨터의 IP는10.0.0.5
이다. 방화벽이 NAT를 해서 IP 주소를 변환해준 것이다! 10.x.x.x
는 완전히 단절된(fully disconnected) 네트워크나 방화벽 뒤의 네트워크에 사용할 수 있도록 지정된(reserved) 네트워크 중 하나이다. RFC 1918에 보면 이러한 네트워크 목록을 모두 볼 수 있다.10.x.x.x
,192.168.x.x
,172.16-31.x.x
등이 있다.- NAT를 하는 방화벽 뒤의 네트워크는 위에 나열한 지정된 네트워크일 필요는 없지만, 보통은 그 지정된 네트워크를 사용한다.
- IPv6에서도 역시 private network가 있다. RFC 4193에 따르면 그 주소는
fdxx:
로 (혹은 미래에fcxx:
일 수도) 시작한다. - 보통 NAT와 IPv6는 같이 쓰이지 않는다.
4. Jumping from IPv4 to IPv6
- 원문을 참조 바란다.
5. System Calls or Bust
- 시스템의 네트워크 기능을 사용할 수 있도록 하는 system call (+ library call)을 어떤 순서로 써야하는지 정리한다.
5.1. getaddrinfo()
- 발사 준비!
- 많은 옵션을 가진 레알 참 일꾼이지만, 사용법은 꽤 간단하다. 이 함수는 우리가 필요한
struct
들을 세팅해준다. - 옛날 얘기를 잠깐 하자면, DNS lookup을 위해서
gethostbyname()
함수를 사용했었어야 했다. 그렇게 받은 정보를struct sockaddr_in
안에 손수 넣어줘서 사용해야 했다. 다행히도 이제는getaddrinfo()
가 그 모든 것을 해준다. 그리고 이는 IPv4와 IPv6 모두에 문제 없는 코드를 위해서는 이렇게 되어야 한다!
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
int getaddrinfo(const char *node, // e.g. "www.example.com" or IP
const char *service, // e.g. "http" or port number
const struct addrinfo *hints,
struct addrinfo **res);
node
는 host name이나 IP 주소이면 된다.service
는 "80"처럼 port number이거나, 특정 service 이름("ftp", "telnet" 등 The IANA Port List에 기록된 것들)이 들어간다.hints
는 관련된 정보가 이미 채워져 있는struct addrinfo
를 가리키는 pointer를 넣어줄 수 있다.- 그 결과로
res
에 linked list의 pointer가 할당되게 된다. - 아래 예제는 서버가 당신의 host IP 주소 및 포트 3490을 기다리고자 할 때 쓸 수 있는 코드이다.
int status;
struct addrinfo hints;
struct addrinfo *servinfo; // will point to the results
memset(&hints, 0, sizeof hints); // make sure the struct is empty
hints.ai_family = AF_UNSPEC; // don't care IPv4 or IPv6
hints.ai_socktype = SOCK_STREAM; // TCP stream sockets
hints.ai_flags = AI_PASSIVE; // fill in my IP for me
if ((status = getaddrinfo(NULL, "3490", &hints, &servinfo)) != 0) {
fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(status));
exit(1);
}
// servinfo now points to a linked list of 1 or more struct addrinfos
// ... do everything until you don't need servinfo anymore ....
freeaddrinfo(servinfo); // free the linked-list
- 위 예제에서는
ai_family
를AF_UNSPEC
으로 두어서 IP 버전에 상관없음을 나타냈지만, 필요에 따라AF_INET
이나AF_INET6
를 사용해도 된다. AI_PASSIVE
flag가 보인다. 이는getaddrinfo()
로 하여금 나의 localhost의 IP를 알아서 socket structure에 할당하도록 한다. 다른 주소를 넣고 싶다면getaddrinfo()
의 첫 번째 argument를NULL
대신 넣어주면 된다.- 함수를 호출해서 오류가 있다면 (
getaddrinfo()
반환값이 0이 아니라면)gai_strerr()
를 사용해서 오류 내용을 출력할 수 있다. - 오류 없이 함수 작업이 끝났다면
servinfo
는struct addrinfo
의 linked list를 가리킬 것이다. 그리고 이struct addrinfo
는 우리가 나중에 사용하게 될struct sockaddr
를 담고 있다! getaddrinfo()
가 우리를 위해서 linked list가 필요한 메모리를 할당해 줬듯이,freeaddrinfo()
를 통해 그 메모리를 해제시킬 수 있다.아래 예제는 클라이언트 입장에서 특정 서버의 포트 3490에 접속하고 싶을 때 사용할 수 있는 코드이다.
int status;
struct addrinfo hints;
struct addrinfo *servinfo; // will point to the results
memset(&hints, 0, sizeof hints); // make sure the struct is empty
hints.ai_family = AF_UNSPEC; // don't care IPv4 or IPv6
hints.ai_socktype = SOCK_STREAM; // TCP stream sockets
// get ready to connect
status = getaddrinfo("www.example.net", "3490", &hints, &servinfo);
// servinfo now points to a linked list of 1 or more struct addrinfos
// etc.
- 아래 예제는 명령줄에서 당신이 입력하는 IP 주소나 host name을 받아서 그 정보를 출력하는 작은 프로그램의 코드이다.
/*
** showip.c -- show IP addresses for a host given on the command line
*/
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main(int argc, char *argv[])
{
struct addrinfo hints, *res, *p;
int status;
char ipstr[INET6_ADDRSTRLEN];
if (argc != 2) {
fprintf(stderr,"usage: showip hostname\n");
return 1;
}
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC; // AF_INET or AF_INET6 to force version
hints.ai_socktype = SOCK_STREAM;
if ((status = getaddrinfo(argv[1], NULL, &hints, &res)) != 0) {
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status));
return 2;
}
printf("IP addresses for %s:\n\n", argv[1]);
for(p = res;p != NULL; p = p->ai_next) {
void *addr;
char *ipver;
// get the pointer to the address itself,
// different fields in IPv4 and IPv6:
if (p->ai_family == AF_INET) { // IPv4
struct sockaddr_in *ipv4 = (struct sockaddr_in *)p->ai_addr;
addr = &(ipv4->sin_addr);
ipver = "IPv4";
} else { // IPv6
struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)p->ai_addr;
addr = &(ipv6->sin6_addr);
ipver = "IPv6";
}
// convert the IP to a string and print it:
inet_ntop(p->ai_family, addr, ipstr, sizeof ipstr);
printf(" %s: %s\n", ipver, ipstr);
}
freeaddrinfo(res); // free the linked list
return 0;
}
- 실제 사용 예는 아래처럼 나올 것이다.
$ showip www.example.net
IP addresses for www.example.net:
IPv4: 192.0.2.88
$ showip ipv6.example.com
IP addresses for ipv6.example.com:
IPv4: 192.0.2.101
IPv6: 2001:db8:8c00:22::171
5.2. socket()
- File Descriptor를 받아라!
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
- Argument인
domain
,type
,protocol
은 원하는 socket이 각각 IPv4/IPv6인지, stream/datagram인지, TCP/UDP인지를 의미한다.domain
은PF_INET
이나PF_INET6
type
은SOCK_STREAM
이나SOCK_DGRAM
protocol
은0
을 주면type
에 따라 적절한 것으로 알아서 정한다. 아니면getprotobyname()
을 통해 "tcp"/"udp"에 맞는 값을 얻어서 넣을 수도 있다.PF_INET
과AF_INET
가 거의 같다는 옛날 이야기에 대해선 원문을 참조바란다.
- 아래 예제처럼
getaddrinfo()
로 얻은 값을socket()
에 집어넣어주면 된다.
int s;
struct addrinfo hints, *res;
// do the lookup
// [pretend we already filled out the "hints" struct]
getaddrinfo("www.example.com", "http", &hints, &res);
// [again, you should do error-checking on getaddrinfo(), and walk
// the "res" linked list looking for valid entries instead of just
// assuming the first one is good (like many of these examples do.)
// See the section on client/server for real examples.]
s = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
socket()
은 다음 단계에 사용할 _socket descriptor_을 반환해줄 뿐이다. 오류일 경우-1
을 반환하고, 전역 변수errno
에 오류값이 기록된다 (errno man page를 참조바란다).
5.3. bind()
- 어느 포트에서 기다려야 하죠?
- 위에서 얻은 socket file descriptor를 당신의 local machine의 어떤 포트에 연결하고 싶을 때가 있다 (보통
listen()
을 통해 특정 포트로 들어오는 연결을 받고자 할 때가 그렇다). - kernel은 들어오는 packet을 어느 process의 socket descriptor에 주어야 하는지를 이 포트 숫자를 통해 판단한다.
- 반대로 당신이 서버가 아니고 클라이언트라서
connect()
를 하는 상황이라면bind()
는 아마도 필요하지 않을 것이다.
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
sockfd
는socket()
에서 받은 socket file descriptor이다.my_addr
은 당신의 IP주소와 포트 정보를 가지고 있는struct sockaddr
의 pointer이다.addrlen
은 그 주소의 byte 길이이다.
struct addrinfo hints, *res;
int sockfd;
// first, load up address structs with getaddrinfo():
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC; // use IPv4 or IPv6, whichever
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE; // fill in my IP for me
getaddrinfo(NULL, "3490", &hints, &res);
// make a socket:
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
// bind it to the port we passed in to getaddrinfo():
bind(sockfd, res->ai_addr, res->ai_addrlen);
bind()
은 오류가 났을 경우-1
을 반환하고errno
에 오류값을 저장한다.- 손수
struct sockaddr_in
을 만들어서 사용하는 옛날 이야기가 원문에 있으니 참조바란다. - 1024 이하의 포트 숫자는 별도의 용도를 위해 보존된다 (superuser가 아니라면). 따라서 1024부터 65535까지의 포트 숫자를 사용하는 것이 안전하다.
- 서버를 재실행할 때면 가끔 "Address already in use."라며
bind()
가 실패할 때가 있다. 이는 연결되어 있었던 socket이 아직 완전히 포트를 떠나지 못하고 kernel 안에서 떠돌고 있는 상황이다. 이럴 때는 잠시 정리되기를 기다리거나 (1-2분 정도), 아래 코드처럼 포트를 재사용하도록 할 수도 있다.
int yes=1;
//char yes='1'; // Solaris people use this
// lose the pesky "Address already in use" error message
if (setsockopt(listener,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof yes) == -1) {
perror("setsockopt");
exit(1);
}
- 반드시
bind()
를 하지 않아도 되는 상황이 있음을 명심하자. telnet에서는 접속하고자 하는 원격 포트만이 중요한 것처럼,connect()
로 원격 서버에 접속하고자 하는 상황에서는 당신의 local port가 무엇인지는 신경쓸 필요가 없는 것이다. 이 상황에서connect()
는 알아서 비어있는 포트를 찾아서 사용한다.
5.4. connect()
- Hey, You!
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
sockfd
는socket()
을 통해 받은 socket file descriptorserv_addr
은 목적지의 IP 주소와 포트 정보를 담고 있는struct sockaddr
addrlen
은 서버 주소 strucutre의 byte 길이이다.- 이 argument들은 모두
getaddrinfo()
를 통해 가져올 수 있다. 멋지다. - 아래 예제는 "www.example.com"의 포트 3490에 연결하는 예제이다.
struct addrinfo hints, *res;
int sockfd;
// first, load up address structs with getaddrinfo():
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
getaddrinfo("www.example.com", "3490", &hints, &res);
// make a socket:
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
// connect!
connect(sockfd, res->ai_addr, res->ai_addrlen);
- 오류가 났을 경우
-1
을 반환하고,errno
에 오류값을 넣어준다. 오류 여부를 꼭 확인하자. - 위 예제에서
bind()
를 사용하지 않은 것도 알아두자. Local 포트 따위 신경쓰지 않았다.
5.5. listen()
- 연락 좀 주세요
- 연결이 들어오길 기다렸다가, 들어오면 이를 원하는 대로 처리하는 서버의 입장에서 보면 두 과정을 거쳐야 한다. 일단
listen()
을 하고 있다가accept()
를 하면 된다.
int listen(int sockfd, int backlog);
listen()
signature는 단순하지만 설명이 좀 필요하다.backlog
는 incoming queue로부터 허락된 연결 개수이다.- 무슨 말이냐? 당신이
accept()
하기 전까지, 들어오는 연결들은 이 queue에서 계속 대기하고 있다는 것이다. 그리고backlog
숫자는 이렇게 대기하는 연결들의 한계점을 의미한다. - 대부분의 시스템에서 이 숫자는 대략 20 정도로 제한하지만, 직접 5나 10 정도로 바꿀 수도 있다.
listen()
도 역시 오류 시-1
값을 반환하고,errno
에 오류값을 넣어준다.listen()
하기 전에bind()
가 필요하다. 그래야 서버가 특정 포트에 대해서 돌아갈 수가 있다. 결국 다음과 같은 순서로 system call을 사용하게 된다.
getaddrinfo();
socket();
bind();
listen();
/* accept() goes here */
5.6. accept()
- 3490 포트로 연락주셔서 감사합니다
- 서버사이드 스토리
- 누군가 멀리서 특정 포트를
listen()
하고 있는 나의 서버에connect()
하러 온다. - 이 연결은
accept()
되기를 기다리며 queue에 쌓인다. - 내 서버는 이 연결을
accept()
해서 queue에서 가져온다. accept()
는 이 연결 하나를 위한 새로운 socket file descriptor로 반환해준다!- 그렇다, 갑자기 내 서버는 2개의 socket file descriptor를 가지게 된다. 하나는 기존에 새로운 연결을 기다리는 것이고, 새로 만들어진 다른 하나는 드디어
send()
와recv()
를 할 수 있는 단계에 온 것이다.
- 누군가 멀리서 특정 포트를
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd
는listen()
하고 있는 socket descriptor이다.addr
은 주로 localstruct sockaddr_storage
를 가리키는 pointer이다. 여기서 접속해 오는 연결에 대한 정보가 들어오는데, 이 struct를 통해 어느 host가 어떤 port를 통해서 연결해오고 있는지 알 수 있다.addrlen
은 local integer 변수로,accept()
가 실행되기 전에sizeof(struct sockaddr_storage)
로 들어가야 한다.accept()
는 이 숫자만큼의 byte만을addr
에 입력한다. 만약 그보다 적은 byte가 입력됐다면, 그 숫자를addrlen
에 반영시켜준다.accept()
는 오류 시-1
값을 반환하고,errno
에 오류값을 넣어준다.- 예제를 보면서 다시 곱씹어보자.
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define MYPORT "3490" // the port users will be connecting to
#define BACKLOG 10 // how many pending connections queue will hold
int main(void)
{
struct sockaddr_storage their_addr;
socklen_t addr_size;
struct addrinfo hints, *res;
int sockfd, new_fd;
// !! don't forget your error checking for these calls !!
// first, load up address structs with getaddrinfo():
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC; // use IPv4 or IPv6, whichever
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE; // fill in my IP for me
getaddrinfo(NULL, MYPORT, &hints, &res);
// make a socket, bind it, and listen on it:
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
bind(sockfd, res->ai_addr, res->ai_addrlen);
listen(sockfd, BACKLOG);
// now accept an incoming connection:
addr_size = sizeof their_addr;
new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &addr_size);
// ready to communicate on socket descriptor new_fd!
.
.
.
5.7. send()
와 recv()
- Talk to Me, Baby!
- 이 두 함수는 stream socket이나 connected datagram socket 위에서 통신할 때 쓰인다. Unconnected datagram socket의 경우는 아래
sendto()
와recvfrom()
을 사용하게 된다.
int send(int sockfd, const void *msg, int len, int flags);
sockfd
는 데이터를 보내고자 하는 목적지의 socket descriptor로,socket()
이나accept()
의 반환물이 사용된다.msg
는 보내고자하는 데이터를 가리키는 pointerlen
은 그 데이터의 byte 길이이다.flags
는 그냥0
으로 주면 된다 (궁금하다면send()
man page를 보라).예제
char *msg = "Beej was here!";
int len, bytes_sent;
.
.
.
len = strlen(msg);
bytes_sent = send(sockfd, msg, len, 0);
.
.
.
send()
는 반환값으로 실제로 보내진 데이터의 byte 길이를 준다. 따라서 이 값은len
argument보다 작을 수도 있다! 당신이 엄청난 양의 데이터를 한 방에 보내려고 해서, 시스템이 이를 완수해낼 수 없을 수도 있는 것이다. 이럴 때는 최대한 보낼 수 있는 만큼만 보내고 그 보낸 양을 알려주어서, 시스템은 당신이 그 나머지를 나중에 다시 잘 처리하리라 믿는다.- 다행히도, packet이 작다면 (1K 이하 정도) 대부분의 경우는 한 방에 모든 데이터가 전송될 것이다.
send()
는 오류 시-1
값을 반환하고,errno
에 오류값을 넣어준다.
int recv(int sockfd, void *buf, int len, int flags);
sockfd
는 읽고자 하는 socket descriptorbuf
는 읽어낸 정보가 들어가는 bufferlen
은 buffer의 최대 byte 길이flags
는0
이면 된다 (궁금하다면recv()
man page를 보라).recv()
는 받은 데이터를 읽어서 buffer에 넣은 byte 길이를 반환한다. 오류일 경우-1
를 반환하고errno
에 오류값을 넣어준다.recv()
가0
을 반환한다면? 이는 접속을 시도한 상대방이 연결을 끊었다는 의미이다!
5.8. sendto()
와 recvfrom()
- Talk to Me, DGRAM-style
- Datagram socket은 원격 host와 연결되어 있지 않으므로, packet를 보내기 전에 어떤 정보가 꼭 필요할까? 답은 목적지의 주소이다!
int sendto(int sockfd, const void *msg, int len, unsigned int flags,
const struct sockaddr *to, socklen_t tolen);
send()
와 거의 비슷한데, 추가적으로 두 개의 argument가 더 있다.to
는struct sockaddr
을 가리키는 pointer로, 목적지의 IP 주소와 port 정보를 담고 있어야 한다.tolen
은 정수형 변수로, 그저sizeof *to
아니면sizeof(struct sockaddr_storage)
로 넣어주면 된다.
- 이 목적지 정보를 알기 위해서는
getaddrinfo()
를 사용하거나, 뒤에 설명할recvfrom()
을 통해서도 가능하다. send()
와 똑같이,sendto()
역시 반환값으로 실제로 보내진 데이터의 byte 길이를 준다. 오류 시-1
값을 반환하고,errno
에 오류값을 넣어준다.
int recvfrom(int sockfd, void *buf, int len, unsigned int flags,
struct sockaddr *from, int *fromlen);
Argument에 대한 설명은 많은 내용이 겹치므로 생략한다. 원문을 참조 바란다.
Datagram socket에
connect()
한 상황이라면, 데이터 송수신을 위해send()
와recv()
를 사용하면 된다는 점을 명심하자. UDP protocol을 사용하는 datagram socket이더라도, 연결되어 있는 socket interface가 이미 목적지와 발신지 정보를 알고 있는 것이다.
5.9. close()
와 shutdown()
- 이제 꼬졍
- Socket 연결을 닫고 싶다면, 일반적인 Unix file descriptor인 경우와 마찬가지로
close()
를 사용하면 된다.
close(sockfd);
이제 이 socket에 모든 읽고 쓰는 작업이 금지된다. 누군가 그런 시도를 한다면 오류를 받아갈 것이다.
Socket을 닫는 과정에서 좀 더 세밀한 조정을 필요로 한다면
shutdown()
함수를 사용하면 된다. 이 함수는close()
처럼 양방향 연결을 끊을 수도 있고, 단방향만을 끊을 수도 있게 해준다.
int shutdown(int sockfd, int how);
how
값은- 수신만 끊겠다면
0
- 송신만 끊겠다면
1
close()
와 같이 송수신 모두 끊겠다면2
로 주면 된다.
- 수신만 끊겠다면
shutdown()
은 반환값으로 성공 시0
, 오류 시-1
값을 반환하고,errno
에 오류값을 넣어준다.- Unconnected datagram socket에
shutdown()
을 적용시킨다면, 이는 단순하게 해당 socket에 대해send()
와recv()
작업을 하지 못하도록 만들 뿐이다. - 중요한 점.
shutdown()
은 socket의 사용을 불가능하게 할 뿐 실제로 file descriptor를 닫지는 않는다. Socket descriptor를 해제하려면close()
해야 한다 (Windows에서 Winsock을 사용할 경우에는close()
대신closesocket()
을 사용해야 한다).
5.10. getpeername()
- 넌 누구니
#include <sys/socket.h>
int getpeername(int sockfd, struct sockaddr *addr, int *addrlen);
sockfd
는 연결되어 있는 stream socket의 descriptoraddr
은struct sockaddr
(또는struct sockaddr_in
) pointer로, 연결 상대방이 정보를 담을 곳이다.addrlen
은 정수 pointer로,sizeof *addr
이나sizeof(struct sockaddr)
로 초기화되어 있어야 한다.- 오류 시
-1
을 반환하고,errno
에 오류값을 넣어준다. - 이렇게해서 상대방의 주소 정보를 알았다면,
inet_ntop()
나getnameinfo()
,gethostbyaddr()
를 이용해서 더 자세한 정보를 알아낼 수 있다.
5.11. gethostname()
- 난 누구니
gethostname()
은 이 프로그램을 돌리고 있는 컴퓨터의 hostname을 받아낸다. 이렇게 받은 hostname은gethostbyname()
을 통해서 IP 주소를 알아내는데 쓰일 수 있다.
#include <unistd.h>
int gethostname(char *hostname, size_t size);
hostname
이 자신의 hostname을 받을 char pointer이다.size
는hostname
의 byte 길이이다.- 성공 시
0
, 오류 시-1
을 반환하고errno
에 오류값을 넣어준다.
6. Client-Server Background
- 네트워크 상에서 일어나는 모든 일은 서버와 클라이언트 간에 오고가는 대화 과정이다.
- telnet의 예를 보면
- 클라이언트에서 원격 host의 23 포트로 연결을 시도한다.
- 그 원격 host (telnetd라고 불리는 서버)는 깨어나서
- 들어오는 telnet connection을 받아 로그인 프롬프트를 띄워주는 등의 일을 한다.
Client-Server Interaction
클라이언트와 서버 커플은
SOCK_STREAM
이나SOCK_DGRAM
, 아니면 다른 무엇이든지 간에 서로 같은 종류의 socket이라면 통신할 수 있다. 예를 몇 가지 들어보면- telnet/telnetd
- ftp/ftpd
- Firefox/Apache
- 당신이 ftp를 사용할 때마다, 여기에 응대하는 ftpd가 어딘가 멀리에 있는 것이다.
원문에 TCP stream server-client의 예제 코드와 datagram socket의 예제 코드가 있으니 꼭 보길 바란다 (원문에도 코드 자체에 대한 설명이 없으므로 그대로 옮겨와봤자 큰 의미가 없다고 생각해서 생략하지만, 반드시 보고 이해하시길 바란다).
'Programming' 카테고리의 다른 글
Check Expert + VBA로 종목별 Intra 데이터 누적쌓기 (0) | 2020.12.17 |
---|---|
Beej's Guide to Network Programming 요약, Part 2. Slightly Advanced Techniques (0) | 2019.09.14 |
GNU/Linux 사용자를 위한 Tistory CLI 프로그램 (0) | 2018.08.29 |