이 절에서는 Unix 시스템이나 소켓 API를 지원하는 다른 시스템(BSD, Windows, Linux, Mac 등)에서 네트워크 기능에 접근할 수 있게 해 주는 시스템 호출과 라이브러리 호출을 다룹니다. 이런 함수 중 하나를 호출하면 커널이 넘겨받고 여러분을 위해 모든 일을 자동으로 마법같이 처리합니다.
(역자 주: 이 절의 일부 함수 원형은 원문의 오래된 표기입니다. 최신 POSIX 계열에서는 주소 길이 인수에 대개 socklen_t 또는 socklen_t *를 사용합니다.)
대부분의 사람들이 어려워하는 점은 이 함수들을 어떤 순서로 호출해야 하는가 입니다. 이미 찾아보셨겠지만 그런 쪽으로는 man페이지는 아무 쓸모도 없습니다. 그 끔찍한 상황을 해결하기 위해 시스템 콜들을 정확히(대략) 여러분의 프로그램에서 호출해야 하는 순서 그대로 아래에 이어지는 절들에 제시했습니다.
그러니까 여기에 있는 몇몇 예제 코드와 우유, 과자(이것들은 직접 준비하셔야 합니다) 그리고 두둑한 배짱과 용기만 있다면 여러분은 인터넷의 세계에서 존 포스텔의 아들처럼 데이터를 쏘아 보낼 수 있게 될 것입니다. (역자 주: John Postel은 인터넷 초기에 큰 기여를 한 컴퓨터 과학자 중 한 명입니다.)
(아래의 예제 코드들은 대개 간략히 보여주기 위해 필수적인 오류 확인을 생략했음을 기억하세요. 그리고 예제 코드들은 대개 getaddrinfo()의 호출이 성공하고 적절한 연결 리스트를 출력 매개변수에 설정한다고 가정합니다. 이런 상황은 독립 실행형 프로그램에서는 제대로 처리되어 있으니, 그것들을 지침으로 삼으세요.)
getaddrinfo()—발사 준비! 이것은 여러 옵션을 가진 진짜 일꾼입니다. 그러나 사용법은 사실 꽤 간단합니다. 이것은 여러분이 나중에 필요로 하는 struct들을 초기화합니다.
역사 한 토막 : 예전에는 DNS 검색을 위해서 gethostbyname()을 호출해야 했습니다. 그리고 그 정보를 수작업으로 struct sockaddr_in에 담고 이후의 호출에서 사용해야 했습니다.
고맙게도 더 이상은 그럴 필요가 없습니다. (여러분이 IPv4와 IPv6 환경 모두에서 동작하는 코드를 짜고 싶다면 그래서도 안 됩니다!) 요새는 getaddrinfo()라는 함수가 있어서 DNS와 서비스 이름을 검색하고 필요한 struct를 채우는 등 여러분이 필요로 하는 모든 일을 해 줍니다.
이제 살펴봅시다!
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
int getaddrinfo(const char *node, // 예: "www.example.com" 또는 IP
const char *service, // 예: "http" 또는 포트 번호
const struct addrinfo *hints,
struct addrinfo **res);이 함수에는 입력 매개변수 세 개를 넘겨줍니다. 함수는 결과 연결 리스트의 포인터를 res가 가리키는 곳에 설정합니다.
node 매개변수는 접속하려는 호스트 이름이나 IP 주소입니다.
다음 매개변수는 service입니다. 이것은 “80”같은 포트 번호나 “http”, “ftp”, “telnet” 또는 “smtp” 같은 특정한 서비스 이름이 될 수 있습니다. (IANA 포트 목록21 혹은 Unix 시스템을 쓴다면 /etc/services에서 볼 수 있습니다)
마지막으로 hints 매개변수는 관련 정보를 이미 채운 struct addrinfo 를 가리킵니다.
여기에 여러분이 호스트의 IP 주소와 포트 3490에서 리스닝하려는 서버일 때의 함수 호출 예제가 있습니다. 이것이 리스닝 작업이나 네트워크 설정을 하지는 않는다는 점을 기억하세요. 이것은 나중에 사용할 구조체를 설정할 뿐입니다.
int status;
struct addrinfo hints;
struct addrinfo *servinfo; // 결과를 가리킵니다
memset(&hints, 0, sizeof hints); // 구조체를 비웁니다
hints.ai_family = AF_UNSPEC; // IPv4든 IPv6이든 상관없습니다
hints.ai_socktype = SOCK_STREAM; // TCP 스트림 소켓
hints.ai_flags = AI_PASSIVE; // bind()에 쓸 와일드카드 주소를 요청합니다
if ((status = getaddrinfo(NULL, "3490", &hints, &servinfo)) != 0) {
fprintf(stderr, "gai error: %s\n", gai_strerror(status));
exit(1);
}
// servinfo는 이제 1개 혹은 그 이상의
// addrinfo 구조체에 대한 연결 리스트를 가리킵니다
// ... servinfo가 더 이상 필요 없을 때까지 모든 작업을 합니다...
freeaddrinfo(servinfo); // 연결 리스트를 해제ai_family를 AF_UNSPEC으로 설정해서 IPv4든 IPv6이든 신경 쓰지 않음을 나타낸 것에 주목하세요. 만약 특정한 하나를 원한다면 AF_INET이나 AF_INET6을 쓸 수 있습니다.
AI_PASSIVE도 볼 수 있습니다. getaddrinfo()의 첫 번째 인수가 NULL일 때 이 플래그를 지정하면, getaddrinfo()는 bind()에 쓸 와일드카드 주소를 반환합니다. 그러므로 특정 로컬 IP 주소를 하드코딩하지 않고도 서버를 설정할 수 있습니다. 특정한 로컬 IP 주소에만 바인드하려면 첫 번째 매개변수에 그 주소를 넣으세요.
(역자 주: 와일드카드 주소는 IPv4에서는 INADDR_ANY(0.0.0.0), IPv6에서는 in6addr_any(::)입니다. 이 주소로 bind()하면 해당 주소 계열의 모든 로컬 인터페이스에서 그 포트로 들어오는 요청을 받을 수 있습니다. AI_PASSIVE와 getaddrinfo()는 주소를 반환할 뿐 바인드 자체를 수행하지 않습니다. 첫 번째 인수에 특정 노드를 지정하면 AI_PASSIVE는 영향을 주지 않습니다.)
이렇게 함수를 호출합니다. 오류가 있다면(getaddrinfo()가 0이 아닌 값을 반환한다면) 보시다시피 gai_strerror() 함수를 통해 오류를 출력할 수 있습니다. 만약 모든 것이 제대로 동작한다면 servinfo는 우리가 나중에 쓸 수 있는 struct sockaddr이나 비슷한 것을 가진 struct addrinfo의 연결 리스트를 가리킬 것입니다. 근사하네요!
마지막으로 getaddrinfo()가 고맙게도 우리에게 할당해 준 연결 리스트를 이제 필요 없어졌다면 우리는 freeaddrinfo()를 호출해서 그것을 해제할 수 있습니다. (사실 반드시 해야 합니다.)
여기에 여러분이 특정한 주소, 예를 들어 “www.example.net”의 3490 포트에 접속하고자 하는 클라이언트일 경우의 호출 예제가 있습니다. 다시 말씀드리지만 이것으로는 실제 연결이 이루어지지 않습니다. 그러나 이것은 우리가 나중에 사용할 구조체를 설정해 줍니다.
int status;
struct addrinfo hints;
struct addrinfo *servinfo; // 결과를 가리킵니다
memset(&hints, 0, sizeof hints); // 구조체를 비웁니다
hints.ai_family = AF_UNSPEC; // IPv4나 IPv6은 신경 쓰지 않음
hints.ai_socktype = SOCK_STREAM; // TCP 스트림 소켓
// 연결 준비
status = getaddrinfo("www.example.net", "3490", &hints, &servinfo);
// servinfo는 이제 1개 혹은 그 이상의
// addrinfo 구조체에 대한 연결 리스트를 가리킵니다
// 등등.servinfo는 모든 종류의 주소 정보를 가진 연결 리스트라고 거듭 말했습니다. 이 정보를 보기 위한 짧은 시연 프로그램을 작성해 봅시다. 이 짧은 프로그램22 은 여러분이 명령줄에 적는 호스트의 IP 주소들을 출력합니다.
/*
** showip.c
**
** 명령줄에서 주어진 호스트의 주소들을 출력합니다
*/
#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; // IPv4와 IPv6 어느 쪽이든
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;
struct sockaddr_in *ipv4;
struct sockaddr_in6 *ipv6;
// 주소 자체에 대한 포인터를 받습니다.
// IPv4와 IPv6은 필드가 다릅니다.
if (p->ai_family == AF_INET) { // IPv4
ipv4 = (struct sockaddr_in *)p->ai_addr;
addr = &(ipv4->sin_addr);
ipver = "IPv4";
} else { // IPv6
ipv6 = (struct sockaddr_in6 *)p->ai_addr;
addr = &(ipv6->sin6_addr);
ipver = "IPv6";
}
// IP 주소를 문자열로 변환하고 출력합니다
inet_ntop(p->ai_family, addr, ipstr, sizeof ipstr);
printf(" %s: %s\n", ipver, ipstr);
}
freeaddrinfo(res); // 연결 리스트를 해제합니다
return 0;
}보시다시피 이 코드는 명령줄에 넘긴 값을 getaddrinfo()에 전달해 호출해서 res가 가리키는 연결 리스트를 채웁니다. 그래서 우리는 이 목록을 순회해서 출력하거나 다른 일을 할 수 있습니다.
(저 예제 코드에는 IP 버전에 따라 다른 종류의 struct sockaddr을 처리해야 한다는 점에서 흉한 부분이 있습니다. 그 점에 대해서 사과드립니다. 그러나 더 나은 방법이 있는지는 잘 모르겠습니다.)
실행 예제! 모두가 스크린샷을 좋아합니다.
$ 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
이제 저것을 다룰 수 있으니, getaddrinfo()에서 얻은 결과를 다른 소켓 함수에 넘겨 결과적으로 네트워크 연결을 성립시킬 수 있게 해 봅시다! 계속 읽어보세요!
socket()—파일 설명자를 받자!더 이상 미룰 수가 없을 듯합니다. 이제 socket() 시스템 콜에 대해서 이야기해야 합니다. 개요는 이렇습니다.
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);그러면 이 인수들은 뭘까요? 이것들은 어떤 종류의 소켓을 원하는지 정할 수 있게 해 줍니다. (IPv4 또는 IPv6, 스트림 혹은 데이터그램, TCP 혹은 UDP)
예전에는 사람들이 그 값을 직접 적었고, 지금도 물론 그렇게 할 수 있습니다. (domain은 PF_INET이나 PF_INET6이고, type은 SOCK_STREAM 또는 SOCK_DGRAM 이며, protocol은 주어진 type에 적절한 값을 자동으로 선택하게 하려면 0을 넘겨주거나 “tcp”나 “udp” 중 원하는 프로토콜의 값을 얻기 위해서 getprotobyname() 을 쓸 수도 있습니다.)
(이 PF_INET은 sin_family 필드를 초기화할 때 넣어 주는 AF_INET과 가까운 친척입니다. 사실 둘은 아주 가까워서 값도 같습니다. 그래서 많은 프로그래머는 socket()을 호출할 때 첫 번째 인수로 PF_INET 대신 AF_INET을 넘깁니다. 자, 우유와 과자를 준비하세요. 이제 이야기를 하나 할 시간이니까요. 아주 먼 옛날에는 어쩌면 하나의 주소 계열(Address Family) (“AF_INET” 안에 들어 있는 “AF”)이 여러 종류의 프로토콜 계열(Protocol Family)(“PF_INET”의 “PF”)을 지원할 것이라고 생각하던 시절이 있었습니다. 그러나 그런 일은 일어나지 않았습니다. 그리고 모두 행복하게 오래오래 잘 살았다고 합니다. 이런 이야기입니다. 그래서 할 수 있는 가장 정확한 일은 struct sockaddr_in에서 AF_INET을 쓰고 socket()에서 PF_INET을 사용하는 것입니다.
아무튼 이제 충분합니다. 여러분이 정말로 하고 싶은 일은 getaddrinfo()를 호출한 결과로 돌아오는 값을 아래와 같이 socket()에 직접 넘겨주는 것입니다.
int s;
struct addrinfo hints, *res;
// 탐색 시작
// ["hints"구조체는 이미 채운 것으로 가정합니다]
getaddrinfo("www.example.com", "http", &hints, &res);
// 다시 말하자면 원래는 (이 안내서의 예제들이 하듯이) 첫 번째 것이 좋다고
// 가정하는 대신 getaddrinfo()에 대해서 오류 확인을 하고
// "res" 연결 리스트를 순회해야 합니다.
// 클라이언트/서버 절의 진짜 예제들을 참고하세요.
s = socket(res->ai_family, res->ai_socktype, res->ai_protocol);socket()은 이후의 시스템 호출에서 쓸 수 있는 소켓 설명자를 반환합니다. 오류가 있으면 -1을 반환합니다. 전역 변수 errno가 오류 값으로 설정됩니다. (자세한 정보와 멀티스레드 프로그램에서 errno를 사용할 때의 짧은 노트는 errno 맨페이지를 참고하세요.)
좋습니다. 그러면 이제 이 소켓을 어디에 쓸 수 있을까요? 정답은 아직 못 쓴다는 것입니다. 실제로 쓰기 위해서는 안내서를 더 읽고 이것이 동작하게 하기 위한 시스템 호출을 더 해야 합니다.
bind()—나는 어떤 포트에 있나요? 소켓을 가지면 사용 중인 시스템의 포트에 바인드해야 할 수도 있습니다. (이 작업은 보통 여러분이 listen() 으로 특정 포트에서 들어오는 연결을 받으려고 할 때 이루어집니다. 다중 사용자 네트워크 게임들은 “192.168.5.10의 3490 포트에 연결합니다”라고 말할 때 이런 작업을 합니다.)(역자 주: 90년대부터 2000년대 초의 멀티플레이어 게임은 접속할 때 이런 문구를 흔히 보여 줬습니다.) 포트 번호는 커널이 특정 프로세스의 소켓 설명자를 들어오는 패킷과 연관 짓기 위해서 사용합니다. 만약 여러분이 connect()만 할 생각이라면 bind()는 대개 필요하지 않습니다. 그러나 재미를 위해 읽어봅시다.
이것이 bind() 시스템 콜의 개요입니다.
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);sockfd는 socket()이 반환한 소켓 파일 설명자입니다. my_addr은 여러분의 주소, 말하자면 포트와 IP 주소를 가진 struct sockaddr 에 대한 포인터입니다. addrlen은 그 주소의 바이트 단위 길이입니다.
휴. 한 번에 받아들이기에는 조금 많습니다. 프로그램이 실행되는 호스트의 3490번 포트에 소켓을 바인드하는 예제를 봅시다.
struct addrinfo hints, *res;
int sockfd;
// 먼저 getaddrinfo()로 구조체에 정보를 불러옵니다
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC; // IPv4나 IPv6 중 아무 것이나 씁니다
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE; // bind()에 쓸 와일드카드 주소를 요청합니다
getaddrinfo(NULL, "3490", &hints, &res);
// 소켓을 만듭니다.
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
// getaddrinfo()에 넘겼던 포트에 바인드합니다.
bind(sockfd, res->ai_addr, res->ai_addrlen);AI_PASSIVE 플래그와 NULL 첫 번째 인수는 bind()에 쓸 와일드카드 주소를 요청합니다. 특정한 로컬 IP 주소에만 바인드하고 싶다면 AI_PASSIVE를 빼고 getaddrinfo()의 첫 번째 인수로 그 IP 주소를 넣으세요.
bind()도 오류가 발생하면 -1을 반환하고 errno를 오류 값으로 설정합니다.
많은 오래된 코드들이 bind()를 호출하기 전에 struct sockaddr_in을 직접 채워 넣습니다. 이것은 분명히 IPv4 전용이지만 같은 일을 IPv6에 대해서도 못 할 이유는 없습니다. 단지 getaddrinfo()를 쓰는 편이 일반적으로 더 쉽습니다. 어쨌든 예전 코드는 이런 방식입니다.
// !!! 이것은 예전 방식입니다 !!!
int sockfd;
struct sockaddr_in my_addr;
sockfd = socket(PF_INET, SOCK_STREAM, 0);
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(MYPORT); // short, 네트워크 바이트 순서
my_addr.sin_addr.s_addr = inet_addr("10.12.110.57");
memset(my_addr.sin_zero, '\0', sizeof my_addr.sin_zero);
bind(sockfd, (struct sockaddr *)&my_addr, sizeof my_addr);위의 코드에서 모든 로컬 IPv4 인터페이스에 바인드하고 싶었다면(위의 AI_PASSIVE처럼) s_addr 필드에 INADDR_ANY를 대입할 수 있습니다. IPv6에서 이에 해당하는 값은 여러분의 struct sockaddr_in6의 sin6_addr 필드에 대입해야 하는 전역 변수인 in6addr_any입니다. (변수 초기화식에 쓸 수 있는 IN6ADDR_ANY_INIT이라는 매크로도 있습니다.)
bind()를 호출할 때 주의해야 할 점: 포트 번호는 낮은 것을 쓰지 마세요. 1024번 아래의 모든 포트는 예약되어 있습니다(슈퍼유저가 아닌 이상)! 그 위의 포트 번호는 (다른 프로그램이 이미 쓰고 있지 않다면) 65535까지 아무 것이나 쓸 수 있습니다.
눈치챌 수 있듯이 때때로 서버를 다시 실행하려고 하면 bind()가 실패하고 “주소가 이미 사용 중입니다.”라고 할 때가 있습니다. 그것은 연결되었던 소켓 중 일부가 여전히 커널에서 대기 중이고 포트를 사용하고 있다는 것을 의미합니다. 여러분은 그것이 정리될 때까지 1분 정도를 기다리거나 여러분의 프로그램이 포트를 재사용할 수 있도록 하는 코드를 넣을 수도 있습니다.
int yes=1;
//char yes='1'; // 솔라리스에서는 이것을 사용
// "주소가 이미 사용 중입니다"라는 오류 메시지를 제거
setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes); bind()에 대해서 한마디 더 : 이 함수를 호출할 필요가 전혀 없는 경우도 있습니다. connect()를 호출해서 원격 호스트에 연결하려고 하고, 로컬 포트에 대해서는 신경 쓰지 않는다면(telnet의 경우처럼 원격지 포트만 신경 쓰는 경우) connect()가 자동으로 소켓이 바인드되지 않았는지 확인하고 필요하다면 사용하지 않은 로컬 포트에 bind()해 줄 것입니다.
connect()—거기 안녕! 몇 분만 여러분이 텔넷 응용프로그램이 되었다고 생각해봅시다. 여러분의 사용자들이 소켓 파일 설명자를 얻기 위해서 여러분에게 명령을 내립니다 (영화 트론에서처럼요). 여러분은 그에 따라 socket()을 호출합니다. 다음으로 사용자가 여러분에게 “10.12.110.57”의 “23”번 포트(텔넷 표준 포트)에 연결하라고 합니다. 어떻게 해야 할까요?
프로그램 입장에서는 운이 좋게도, 지금 connect()에 대한 절을 읽는 중입니다! 이 절은 원격 호스트에 어떻게 연결하는지에 대해 알려줍니다. 거침없이 읽어봅시다! 낭비할 시간이 없습니다!
connect()에 대한 호출은 아래와 같습니다:
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);sockfd는 socket() 함수 호출이 반환하는 우리의 친근한 이웃인 소켓 파일 설명자입니다. serv_addr는 struct sockaddr이고 목적지 포트와 IP 주소를 담고 있습니다. addrlen은 서버 주소 구조체의 바이트 단위 길이를 담고 있습니다.
모든 정보는 멋진 getaddrinfo() 호출의 결과에서 추출할 수 있습니다.
이해가 되기 시작하나요? 저는 대답을 들을 수 없으니 그럴 것이라 생각하겠습니다. “www.example.com”의 3490 포트로 소켓 연결을 만드는 예제를 살펴봅시다:
struct addrinfo hints, *res;
int sockfd;
// getaddrinfo()로 주소 구조체 목록을 가져옵니다
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
getaddrinfo("www.example.com", "3490", &hints, &res);
// 소켓을 만듭니다
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
// 연결합니다!
connect(sockfd, res->ai_addr, res->ai_addrlen);다시 이야기하자면 구식 프로그램들은 connect()에 넘겨줄 struct sockaddr_in 을 직접 채워 넣었습니다. 그렇게 하고 싶다면 해도 됩니다. 위에 있는 bind() 절 에서 비슷한 내용을 참고하세요.
connect()의 반환값을 확인하는 것을 잊지 마세요. 오류가 발생하면 -1을 반환하고 errno 변수를 설정합니다.
우리가 bind()를 호출하지 않았음에 주목하세요. 간단히 말하자면 우리의 로컬 포트 번호에 대해서는 신경 쓰지 않습니다. 우리가 어디로 가는지만 신경 씁니다 (원격지 포트). 커널이 우리 대신 로컬 포트를 고를 것입니다. 우리가 접속하는 사이트는 이 정보를 자동으로 우리에게서 얻어냅니다. 신경 쓰실 필요가 없습니다.
listen()—누가 연락 좀 해줄래요? 이제 흐름이 변할 때입니다. 우리가 원격지 호스트에 접속하고 싶지 않은 경우라면 어떻게 할까요? 간단히 말해서 들어오는 연결을 기다리고 그것을 어떤 방식으로 처리해야 한다면 어떻게 할까요? 그 과정은 두 단계입니다. 먼저 listen()을 호출하고, accept()를 씁니다.(아래를 참고하세요.)
listen() 함수 호출은 꽤 단순하지만 약간의 설명이 필요합니다:
int listen(int sockfd, int backlog);sockfd는 socket() 시스템 함수 호출로 얻어온 평범한 소켓 파일 설명자입니다. backlog는 들어오는 큐에 허용되는 연결의 수입니다. 이것이 무슨 뜻인지 궁금한가요? 들어오는 연결들은 여러분이 accept()를 해주기 전까지(아래를 참고하세요) 이 큐 안에서 기다릴 것이고 이것은 몇 개의 연결이 대기할 수 있는가를 정합니다. 대개의 시스템은 이 값을 조용히 20 정도로 제한합니다. 그러나 5나 10 정도의 값으로 설정해도 괜찮을 것입니다.
(역자 주: 실제 backlog 상한은 운영체제와 커널 설정에 따라 다릅니다.)
또 평소와 다름없이 listen()도 오류가 발생할 경우 -1을 반환하고 errno를 설정합니다.
아마도 상상하실 수 있겠지만 서버가 특정 포트에서 실행되도록 하기 위해서는 listen()을 호출하기 전에 bind()를 호출해야 합니다. (여러분의 친구들에게 어떤 포트로 연결해야 할지 말해줄 수 있어야 합니다.) 이런 식입니다.
저 코드는 충분히 자명하므로 예제 코드를 대신하게 두겠습니다. (accept() 절의 코드는 좀 더 완전합니다.) 정말로 복잡한 부분은 accept() 을 호출하는 부분입니다.
accept()—“3490 포트에 접속해주셔서 감사합니다.” 각오하세요! accept() 함수는 조금 이상합니다. 지금부터 생길 일은 다음과 같습니다. 아주 먼 곳에 있는 누군가가 여러분의 컴퓨터에 connect() 함수로 연결하려고 합니다. 여러분은 특정 포트에서 listen()을 실행하고 있습니다. 그들의 연결은 accept()로 받아들여질 때까지 대기열에 쌓일 것입니다. 여러분은 accept()를 호출해서 대기 중인 연결을 받아들이겠다고 알려줍니다. accept()는 이 연결에만 쓸 완전히 새로운 소켓 파일 설명자를 반환합니다. 그렇습니다! 갑자기 소켓 하나 가격에 두 개의 소켓을 가지게 되었습니다. 원래의 소켓은 여전히 새 연결들을 리스닝하고 있고, 새롭게 만들어진 것은 send()와 recv() 작업을 위해 준비되었습니다. 이제 다 됐군요!
호출은 아래와 같이 합니다.
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);sockfd는 listen()을 하고 있는 소켓 설명자입니다. 어렵지 않습니다. addr은 대개 로컬 struct sockaddr_storage에 대한 포인터입니다. 여기에 들어오는 연결의 정보가 들어가게 됩니다(그리고 그것을 통해서 어떤 호스트가 어떤 포트에서 여러분을 호출하고 있는지 알 수 있습니다.) addrlen은 로컬 정수 변수로, 그 주소를 accept()에 넘기기 전에 sizeof(struct sockaddr_storage)으로 설정해 두어야 합니다. accept()는 그보다 많은 바이트를 addr에 적지 않을 것입니다. 더 적은 바이트를 적었다면, 그 사실을 반영하도록 addrlen 값을 바꿀 것입니다. accept()도 오류가 발생하면 -1을 반환하고 errno를 설정합니다. 어느 정도는 예상하셨으리라 생각합니다.
전과 마찬가지로 한 번에 이해하기에는 많은 내용입니다. 그래서 여러분의 독서를 위한 예제 코드 조각이 있습니다.
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#define MYPORT "3490" // 사용자들이 접속할 포트
#define BACKLOG 10 // 대기열이 담을 수 있는 연결의 개수
int main(void)
{
struct sockaddr_storage their_addr;
socklen_t addr_size;
struct addrinfo hints, *res;
int sockfd, new_fd;
// !! 이 호출들에 대한 오류 확인을 잊지 마세요 !!
// getaddrinfo()로 주소 구조체 목록을 가져옵니다
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC; // IPv4 또는 IPv6, 아무것이나 씁니다
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE; // bind()에 쓸 와일드카드 주소를 요청합니다
getaddrinfo(NULL, MYPORT, &hints, &res);
// 소켓을 만들고, 바인드하고, 리스닝 시작
sockfd = socket(res->ai_family, res->ai_socktype,
res->ai_protocol);
bind(sockfd, res->ai_addr, res->ai_addrlen);
listen(sockfd, BACKLOG);
// 들어오는 연결을 받습니다
addr_size = sizeof their_addr;
new_fd = accept(sockfd, (struct sockaddr *)&their_addr,
&addr_size);
// new_fd 소켓 설명자에서 통신할 준비 완료!
.
.
.다시 말씀드리지만 모든 send()와 recv() 호출에 대해서 new_fd를 사용할 것입니다. 만약 단 한 개의 연결만을 받아들이길 원하신다면 추가적인 연결이 같은 포트를 통해 들어오는 것을 막기 위해서 sockfd를 close()할 수 있습니다.
send()와 recv()—말 좀 해봐요!이 두 함수들은 스트림 소켓이나 연결된 데이터그램 소켓을 통해 통신하는 데 쓰입니다. 일반적인 연결하지 않은 데이터그램 소켓을 쓰고 싶다면 sendto()와 recvfrom() 절을 보시면 됩니다.
새로울 수도, 아닐 수도 있는 이야기를 하나 하겠습니다. 이 함수들은 블록(대기) 호출입니다. 다시 말해
recv()는 받을 데이터가 준비될 때까지 블록됩니다. “그런데 ’블록’된다는 게 대체 무슨 뜻인가요?!”라는 생각이 들 수 있습니다. 그 말은 누군가가 여러분에게 뭔가를 보낼 때까지 프로그램이 그 시스템 호출 지점에서 멈춰 있다는 뜻입니다. (그 문장에서 “멈춘다”에 해당하는 운영체제 쪽 전문 용어는 사실 _잠든다_라서, 제가 이 표현들을 서로 바꿔 쓸 수도 있습니다.)send()도 여러분이 보내는 내용이 어떤 식으로든 꽉 막혀 있으면 블록될 수도 있지만, 그런 경우는 더 드뭅니다. 이 개념은 나중에 다시 살펴보고, 필요할 때 어떻게 피하는지도 이야기할 것입니다.
send() 함수는 이렇습니다:
int send(int sockfd, const void *msg, int len, int flags);sockfd는 데이터를 보내고 싶은 소켓 설명자(socket()으로 만들었든 accept()로 만들었든)입니다. msg는 여러분이 보낼 데이터에 대한 포인터이며, len은 그 데이터의 바이트 단위 길이입니다. flags는 그냥 0으로 설정하세요. (flags에 대해 더 자세히 알고 싶다면 send()의 맨페이지를 보세요.)
예제 코드는 이렇게 될 수 있겠습니다.
char *msg = "Beej was here!";
int len, bytes_sent;
.
.
.
len = strlen(msg);
bytes_sent = send(sockfd, msg, len, 0);
.
.
.send()는 실제로 전송한 바이트 수를 반환합니다. 이 값은 여러분이 보내라고 말한 수보다 적을 수 있습니다! 때때로 엄청난 크기의 데이터를 전송하라고 요청하면 그것이 다 처리되지 못할 수 있는 것입니다. send()는 보낼 수 있는 만큼을 보내고, 나머지는 여러분이 나중에 다시 보낼 것이라고 믿습니다. send()가 반환한 값이 len과 일치하지 않는다면 문자열의 나머지를 전송하는 일은 여러분에게 달렸음을 기억하세요. 이것에 관한 좋은 소식은 패킷이 작다면(1KB 미만 정도) 그것은 아마도 한 번에 모든 것을 보낼 수 있으리라는 점입니다. 다시 강조하지만 오류가 발생하면 -1이 반환되고 errno가 오류 번호로 설정됩니다.
recv() 함수는 많은 면에서 유사합니다:
int recv(int sockfd, void *buf, int len, int flags);sockfd는 읽어 들일 소켓 설명자이며, buf는 정보를 읽어 들일 버퍼이고, len은 버퍼의 최대 길이이고 flags는 여기서도 0으로 설정될 수 있습니다. (플래그 정보에 대해서는 recv()의 맨페이지를 참고하세요.)
recv()는 실제로 버퍼에 읽어 들인 바이트 수를 반환하고, 오류가 발생하면 errno를 적절한 값으로 설정한 뒤 -1을 반환합니다.
잠깐! recv()는 0을 반환할 수 있습니다. 이것은 한 가지 의미입니다. 상대편에서 여러분과의 연결을 닫은 것입니다! 반환값 0은 recv()가 연결이 끊어졌음을 알려주는 방식입니다.
자, 정말 쉽지요? 이제 여러분은 스트림 소켓에서 데이터를 주고받을 수 있습니다. 와! 이제 여러분은 어엿한 유닉스 네트워크 프로그래머입니다!
sendto()와 recvfrom()—DGRAM 방식으로 말해 봐요“이제 다 깔끔하고 좋네요”라고 말씀하시는 소리가 들립니다. “그렇지만 연결이 없는 데이터그램 소켓은 어떻게 처리하지요?”라고도 하시는군요. 걱정 마세요, 친구여. 딱 맞는 것이 있습니다.
데이터그램 소켓은 원격지 호스트에 연결되어 있지 않으므로, 패킷을 보낼 때에 필요한 정보는 조금 다릅니다. 바로 목적지 주소가 필요합니다. 이런 식입니다.
int sendto(int sockfd, const void *msg, int len, unsigned int flags,
const struct sockaddr *to, socklen_t tolen);보시다시피 send()와 같지만 두 개의 정보가 더 있습니다. to는 목적지의 IP 주소와 포트를 담은 struct sockaddr 에 대한 포인터이며(아마도 여러분이 형변환해서 사용하실 struct sockaddr_in이나 struct sockaddr_in6 또는 struct sockaddr_storage일 것입니다.) tolen은 내부적으로는 int이며 간단하게 sizeof *to나 sizeof(struct sockaddr_storage)로 설정하면 됩니다.
목적지 주소 구조체는 getaddrinfo()나 아래의 recvfrom()으로 얻거나 수작업으로 채울 수 있습니다.
send()와 마찬가지로 sendto()도 실제로 보낸 바이트 수를 반환합니다. (그 말은 보내려고 한 바이트의 수보다 적은 수가 돌아올 수도 있다는 의미입니다.) 오류가 발생하면 -1을 반환합니다.
이와 유사한 관계가 recv()와 recvfrom()입니다. recvfrom()의 개요는 이렇습니다.
int recvfrom(int sockfd, void *buf, int len, unsigned int flags,
struct sockaddr *from, int *fromlen);또 다시 이것은 몇 개의 추가적인 필드가 있는 recv() 같은 것입니다. from은 보낸 호스트의 IP 주소와 포트가 채워지는 로컬 struct sockaddr_storage에 대한 포인터입니다. fromlen은 로컬 int에 대한 포인터이며 sizeof *from이나 sizeof(struct sockaddr_storage)으로 초기화되어야 합니다. 함수가 반환될 때 fromlen은 from에 실제로 저장된 주소의 길이로 설정되어 있을 것입니다.
recvfrom()은 받은 바이트의 개수를 반환하며 오류가 나면 (errno를 적절히 설정하고) -1을 반환합니다.
여기 질문이 하나 있습니다. 왜 우리는 struct sockaddr_storage를 소켓의 타입으로 사용할까요? 왜 그냥 struct sockaddr_in을 쓸 수 없을까요? 이유는 보시다시피 우리가 IPv4나 IPv6 중 하나에 얽매이고 싶지 않기 때문입니다. 그래서 우리는 양쪽 모두에 충분히 크고 일반적인 struct sockaddr_storage를 사용합니다.
(그럼… 여기에서 다른 질문 하나. 왜 struct sockaddr을 모든 주소를 담을 수 있을 정도로 크게 만들지 않았을까요? 우리는 일반 목적의 struct sockaddr_storage 을 다시 일반 목적의 struct sockaddr으로 형변환하고 있습니다! 이런 동작은 과하고 불필요해 보이지 않나요? 여기에 대한 대답은 그냥 이 struct sockaddr은 만들어질 때부터 그렇게 크지 않았다는 것이고, 이제 와서 그것을 바꾸는 것은 문제의 소지가 있다는 것입니다. 그래서 그들은 그냥 새로운 타입을 만들었습니다.)
만약 여러분이 데이터그램 소켓을 connect()하게 되면 모든 통신에 send()와 recv()를 쓸 수 있음을 기억하세요. 소켓 자체는 여전히 데이터그램 소켓일 것이고 패킷은 여전히 UDP를 사용할 것이지만 소켓 인터페이스가 자동으로 여러분을 위해서 목적지와 출발지 정보를 추가할 것입니다.
close()와 shutdown()—이제 그만 가!휴! 여러분은 하루 종일 send()와 recv()를 사용했고, 이제 충분합니다. 이제 여러분의 소켓 설명자를 닫을 준비가 되었습니다. 이건 쉽습니다. 그냥 평범한 유닉스 파일 설명자 닫기 함수인 close() 를 쓸 수 있습니다.
close(sockfd);이것은 해당 소켓에 대한 후속 읽기와 쓰기를 방지할 것입니다. 원격지에서 이 소켓을 쓰거나 읽으려는 모든 시도는 오류를 반환할 것입니다.
소켓이 어떻게 닫히는지 좀 더 조절하고 싶은 경우에는 shutdown() 함수를 사용할 수 있습니다. 이것은 특정 방향으로의 통신만 끊는 일을 할 수 있으며 양쪽 모두 막을 수도 있습니다(마치 close()가 하듯이). 개요는 이렇습니다.
int shutdown(int sockfd, int how);sockfd는 종료하고 싶은 소켓 파일 설명자이고, how는 다음 중 하나입니다:
how |
효과 |
|---|---|
0 |
후속 수신이 금지됩니다. |
1 |
후속 송신이 금지됩니다. |
2 |
후속 송수신이 금지됩니다. (close()처럼) |
shutdown()은 성공 시에 0을 반환하고, 오류가 발생하면 (errno를 적절한 값으로 설정하고) -1을 반환합니다.
연결하지 않은 데이터그램 소켓에 굳이 shutdown()을 해주신다면, 그것은 단순히 해당 소켓에 send()와 recv()를 사용할 수 없도록 만들 것입니다(데이터그램 소켓에 connect()를 사용하면 이 두 함수를 사용할 수 있음을 기억하세요).
shutdown()이 실제로 파일 설명자를 닫지는 않음에 주목하세요. 소켓 설명자를 해제하기 위해서는 close()를 호출해야 합니다.
별것 없군요.
(예외적으로 Windows에서 Winsock을 사용하는 경우 close() 대신 closesocket()을 호출해야 합니다.)
getpeername()—누구세요?이 함수는 너무 쉽습니다.
너무 쉬워서 이 함수에 별도의 절을 주지 않을 뻔했습니다. 아무튼 알려드리겠습니다.
getpeername() 함수는 연결된 스트림 소켓의 반대편 끝에 누가 있는지를 알려줄 것입니다. 개요입니다.
#include <sys/socket.h>
int getpeername(int sockfd, struct sockaddr *addr, int *addrlen);sockfd는 연결된 스트림 소켓의 설명자입니다. addr은 연결의 반대편 끝에 대한 정보를 담을 struct sockaddr (또는 struct sockaddr_in) 에 대한 포인터입니다. addrlen은 int에 대한 포인터이며 sizeof *addr이나 sizeof(struct sockaddr)으로 초기화되어야 합니다.
이 함수는 오류가 발생하면 -1을 반환하고 errno를 알맞게 설정합니다.
여러분이 상대방의 주소를 가지면 그것을 inet_ntop(), getnameinfo() 또는 gethostbyaddr()에 넣어서 화면에 출력하거나 추가적인 정보를 가져올 수 있습니다. 그들의 로그인 이름을 가져올 수는 없습니다. (좋습니다, 좋아요. 만약 저쪽 컴퓨터가 ident 데몬을 실행 중이라면 가능합니다. 그러나 그것은 이 문서의 범위를 넘어섭니다. 더 자세한 정보를 원한다면 RFC 141323을 참고하세요.)
gethostname()—나는 누구인가? getpeername()보다 더 쉬운 것이 바로 gethostname() 함수입니다. 이것은 여러분의 프로그램이 실행되고 있는 컴퓨터의 이름을 반환합니다. 반환된 이름은 위에 있는 getaddrinfo() 을 써서 여러분의 로컬 시스템의 IP 주소를 알아내는 데 쓰일 수 있습니다.
이보다 더 재미있는 일이 있을까요? 사실 몇 가지 생각나긴 합니다만 소켓 프로그래밍에 대한 것이 아니군요. 아무튼 정리하자면 이렇습니다.
#include <unistd.h>
int gethostname(char *hostname, size_t size);인수들은 단순합니다. hostname은 함수가 반환하는 호스트 이름을 담을 char 배열에 대한 포인터입니다. size는 hostname 배열의 바이트 단위 길이입니다.
함수는 성공적인 완료 후에 0을 반환하고, 오류에 대해서는 흔히 그렇듯 errno를 설정하고 -1을 반환합니다.