이 헤더 파일들은 어디서 찾을 수 있을까요?
여러분의 시스템에 헤더 파일이 없다면 아마도 그것이 필요하지 않을 것입니다. 사용 중인 플랫폼의 설명서를 참고하세요. Windows를 위해 작업하고 있다면 #include <winsock.h>만 하면 됩니다. (역자 주: winsock.h는 Winsock 1.1용입니다. 현재 Windows 소켓 프로그램에서는 보통 winsock2.h를 사용하며, getaddrinfo() 계열을 사용한다면 ws2tcpip.h도 포함합니다.)
bind()가 “Address already in use” 를 보고하면 어떻게 해야 하나요?
리스닝 소켓에 setsockopt()를 SO_REUSEADDR 옵션과 함께 사용해야 합니다. 예제가 필요하다면 bind()에 관한 절과 select()에 관한 절을 참고하세요.
시스템에 열린 소켓의 목록을 얻으려면 어떻게 해야 하나요?
netstat를 사용하세요. 자세한 정보는 man을 참고해야 하지만 아래와 같이 입력해도 약간의 유용한 출력을 얻을 수 있습니다.
$ netstat
비결은 어떤 소켓이 어떤 프로그램과 연결되어 있는지 알아내는 것입니다. :-)
라우팅 테이블을 보려면 어떻게 해야 하나요?
route 명령(대개의 Linux 시스템에서 /sbin에 있습니다) 을 실행하세요. 아니면 netstat -r 명령을 실행하세요. 혹은 ip route 명령일 수도 있습니다.
컴퓨터가 하나밖에 없다면 어떻게 클라이언트와 서버 프로그램을 실행하나요? 네트워크 프로그램을 작성하려면 네트워크가 있어야 하는 것 아닌가요?
여러분에게는 다행히도 사실상 모든 컴퓨터가 커널에 자리 잡고 네트워크 카드인 척하는 루프백 네트워크 “장치”를 구현합니다. (이것은 라우팅 테이블에서 “lo”라는 이름으로 표시되는 인터페이스입니다.)
여러분이 “goat”라는 이름의 컴퓨터에 로그인했다고 합시다. 클라이언트를 하나의 창에서 실행하고 서버를 다른 창에서 실행합시다. 아니면 서버를 백그라운드에서 실행하고(“server &”) 클라이언트를 같은 창에서 실행합시다. 루프백 장치는 여러분이 client goat와 client localhost(“localhost”는 여러분의 /etc/hosts 파일에 정의되어 있을 것입니다.) 중 어떤 것이든 할 수 있게 해 주므로 네트워크 없이도 서버와 대화하는 클라이언트 프로그램을 시험할 수 있습니다.
간단히 말하자면 네트워크 없는 단일 컴퓨터에서 코드를 실행하기 위해 코드를 변경할 필요는 없습니다. 만세!
원격 쪽에서 연결을 닫았는지 어떻게 알 수 있을까요?
recv()가 0을 반환하는 것으로 알 수 있습니다.
“ping” 유틸리티를 만들려면 어떻게 해야 하나요? ICMP는 무엇인가요? raw 소켓과 SOCK_RAW 에 대해서는 어디에서 더 알아볼 수 있을까요?
raw 소켓에 대한 모든 질문은 W. Richard Stevens’ UNIX Network Programming books 에서 답을 얻을 수 있습니다. 또한 온라인으로 사용 가능한46 Stevens’ UNIX Network Programming source code에서 ping/ 하위 디렉터리를 살펴보세요.
connect()에 대한 제한 시간을 변경하거나 단축할 수 있을까요?
W. Richard Stevens가 여러분에게 줄 수 있는 답과 동일한 답을 드리는 대신, UNIX Network Programming source code의 lib/connect_nonb.c47 를 안내해드리겠습니다.
요점은 socket()으로 소켓 설명자를 만든 후 논블로킹으로 설정한 뒤 connect()를 호출할 때 모든 것이 잘 돌아간다면 connect()는 즉시 -1을 반환할 것이고 errno는 EINPROGRESS로 설정될 것이라는 것입니다. 그 후 select()를 호출할 때 소켓 설명자를 읽기와 쓰기 집합에 모두 넣으면서 여러분이 원하는 제한 시간을 지정하면 됩니다. 시간 초과가 발생하지 않으면 connect() 이 완료되었다는 의미입니다. 이 시점에서 getsockopt()를 SO_ERROR 옵션과 함께 호출해서 connect() 호출의 반환값을 얻을 수 있고, 오류가 없었다면 그 값은 0이어야 합니다.
마지막으로 여러분은 아마도 해당 소켓에 데이터를 전송하기 전에 소켓을 다시 블로킹 모드로 설정하고 싶을 것입니다.
이 방식은 프로그램이 연결을 시작하는 동안 다른 일을 할 수 있게 해 주는 장점도 있음에 주목하세요. 예를 들어 500ms 정도의 짧은 제한 시간을 설정한 후 시간 초과가 일어날 때마다 화면의 표시를 갱신하고, select()를 다시 호출할 수 있습니다. select()가 20번 정도 시간 초과를 일으킨다면 연결을 포기할 때가 되었음을 알 수 있습니다.
위에서도 말씀드렸지만 완벽하게 훌륭한 예제가 필요하다면 Stevens의 코드를 참고하세요.
Windows를 위해 빌드하려면 어떻게 하나요?
먼저 윈도우를 삭제한 후 리눅스나 BSD를 설치하세요. };-). 사실 그럴 필요는 없고, 도입부의 Windows에서 빌드하기에 관한 절을 살펴보세요.
Solaris/SunOS에서 빌드하려면 어떻게 하나요? 컴파일을 시도하면 계속 링커 오류가 발생합니다!
링커 오류는 Sun 시스템이 자동으로 소켓 라이브러리를 링크하지 않기 때문에 발생합니다. 컴파일을 위해서는 도입부의 Solaris/SunOS를 위한 절 을 참고하세요.
왜 select()가 시그널을 받으면 실패하나요?
시그널은 블로킹 중인 시스템 콜이 errno를 EINTR로 설정하고 -1을 반환하게 만듭니다. sigaction()으로 시그널 핸들러를 설정하면 SA_RESTART 플래그를 설정할 수 있는데 이것은 중단된 시스템 콜을 다시 시작하게 해 줄 것입니다.
물론 이런 방식이 늘 작동하는 것은 아닙니다.
제가 선호하는 해결책은 goto문을 쓰는 방법입니다. 이것이 교수님들을 아주 짜증나게 할 수 있다는 것도 잘 알 테니, 한번 해 보세요!
select_restart:
if ((err = select(fdmax+1, &readfds, NULL, NULL, NULL)) == -1) {
if (errno == EINTR) {
// 어떤 시그널이 우리에게 인터럽트를 걸었습니다. 그러니 재시작합니다
goto select_restart;
}
// 진짜 오류는 여기서 처리합니다
perror("select");
}물론 여기에서 goto를 쓸 필요는 없습니다. 다른 구조로 제어할 수도 있습니다. 그러나 저는 이 경우 goto 문이 사실 더 깔끔하다고 생각합니다.
recv() 호출에 시간 제한을 적용하려면 어떻게 해야 하나요?
select()를 사용하세요! select()는 읽어들이려는 소켓 설명자에 시간 제한 매개변수를 지정할 수 있게 해 줍니다. 아니면 모든 기능을 아래와 같이 하나의 함수에 감쌀 수 있습니다.
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>
int recvtimeout(int s, char *buf, int len, int timeout)
{
fd_set fds;
int n;
struct timeval tv;
// 파일 설명자 집합을 설정합니다
FD_ZERO(&fds);
FD_SET(s, &fds);
// 시간 제한을 위한 struct timeval을 설정합니다
tv.tv_sec = timeout;
tv.tv_usec = 0;
// 시간이 초과되거나 데이터를 받을 때까지 기다립니다
n = select(s+1, &fds, NULL, NULL, &tv);
if (n == 0) return -2; // 시간 초과!
if (n == -1) return -1; // 오류
// 여기에 데이터가 있어야 하므로 일반 recv()를 수행합니다
return recv(s, buf, len, 0);
}
.
.
.
// recvtimeout() 호출 예시
n = recvtimeout(s, buf, sizeof buf, 10); // 10초 제한 시간
if (n == -1) {
// 오류가 발생했습니다
perror("recvtimeout");
}
else if (n == -2) {
// 시간이 초과되었습니다
} else {
// buf에 데이터가 들어 있습니다
}
.
.
. recvtimeout()이 시간 초과 상황에서 -2를 반환한다는 점에 주목하세요. 왜 0이 아닌지 궁금한가요? recv()가 원격 연결이 닫혔을 때 0을 반환한다는 점을 떠올려봅시다. 그러니 그 값은 쓸 수가 없고, -1은 “오류”를 의미합니다. 그래서 저는 시간 초과를 가리키는 값으로 -2를 썼습니다.
소켓에 데이터를 보내기 전에 암호화하거나 압축하려면 어떻게 해야 하나요?
데이터를 암호화하기 위한 간편한 방법 중 하나는 SSL(secure sockets layer)를 사용하는 것입니다. 그러나 그것은 이 안내서의 범위를 벗어납니다. (더 많은 정보가 필요하면 OpenSSL 프로젝트48를 확인하세요.) (역자 주: 현재는 SSL이라는 명칭 대신 TLS를 사용하며, 새 구현에서는 TLS 1.2 이상을 사용하는 것이 일반적입니다.)
그러나 만약 여러분이 자신만의 압축기나 암호화 체계를 만들거나 써 보고 싶다면, 여러분의 데이터가 양 끝 사이에서 정해진 단계를 거친다고 생각해 보세요. 각 단계는 데이터를 특정한 방법으로 바꿉니다.
send()합니다반대편은 이렇습니다.
recv()합니다만약 압축과 암호화를 모두 할 생각이라면 압축을 먼저 해야 한다는 점을 기억하세요. :-)
클라이언트가 서버가 했던 작업을 제대로 거꾸로 수행하기만 한다면 여러분이 얼마나 많은 단계를 추가하든 데이터는 결국 무사할 것입니다.
그러므로 제 예제 코드를 바탕으로 작업하려면, 데이터를 읽어 온 뒤 send()로 네트워크에 보내기 전에 실행되는 부분을 찾아 그 사이에 암호화 코드를 끼워넣으면 됩니다.
“PF_INET”이 계속 등장하는데 무엇인가요? AF_INET과 관계가 있을까요?
그렇습니다. 관계가 있습니다. 자세한 사항은 socket()에 대한 절을 참고하세요.
클라이언트에게서 셸 명령을 받아서 실행하는 서버는 어떻게 만드나요?
단순함을 위해서 클라이언트가 connect()와 send() 후에 연결을 close() 처리한다고 가정합시다. (즉 클라이언트가 다시 연결하지 않는 한 후속 시스템 콜은 없다는 의미입니다.)
클라이언트의 과정은 이렇습니다.
connect()send("/sbin/ls > /tmp/client.out")close()로 닫음한편 서버는 데이터를 받아서 실행합니다.
accept()recv(str)close()로 닫음system(str) 주의하세요! 클라이언트가 말하는 것을 서버가 실행한다는 것은 원격 셸 접근 권한을 주는 것과 비슷한 일이고, 사람들이 서버에 접속할 때 여러분의 계정으로 무엇이든 할 수 있다는 의미입니다. 위의 예제에서 클라이언트가 “rm -rf ~”를 보내면 어떻게 될까요? 여러분의 계정이 가진 모든 것을 삭제할 것입니다!
그러니 여러분이 현명하다면 안전하다고 확신하는 몇 개의 유틸리티, 예를 들어 foobar 외의 것을 클라이언트가 실행하지 못하도록 하는 것이 좋습니다.
if (!strncmp(str, "foobar", 6)) {
sprintf(sysstr, "%s > /tmp/server.out", str);
system(sysstr);
}그러나 불행히도 이것만으로는 여전히 위험합니다. 클라이언트가 “foobar; rm -rf ~” 를 입력한다면 어떻게 될까요? 가장 안전한 방식은 명령의 매개변수에 들어가는 숫자나 영문자가 아닌 모든 문자(필요하다면 공백 문자도) 앞에 이스케이프(“\”) 문자를 붙이는 것입니다.
보시다시피 보안은 클라이언트가 보낸 것을 서버가 실행할 때에 큰 문제가 됩니다.
제가 꽤 큰 데이터를 보내는데 recv()를 해보면 한 번에 536바이트나 1460바이트 씩만 받아옵니다. 그러나 이것을 로컬 컴퓨터에서 실행하면 한 번에 모든 데이터를 받아옵니다. 왜 이런 것인가요?
MTU에 도달한 것입니다. 이것은 물리 매체가 처리할 수 있는 최대 크기입니다. 로컬 컴퓨터에서는 루프백 장치를 쓰기 때문에 8K나 그 이상의 크기도 문제없이 다룰 수 있습니다. 그러나 이더넷에서는 헤더를 포함해 1500바이트가 한계입니다. 모뎀을 쓴다면 (마찬가지로 헤더를 포함해) 576바이트가 한계입니다.
일단 모든 데이터가 전송되었음을 확실히 해야 합니다. (자세한 정보는 sendall() 함수의 구현을 확인하세요.) 전송이 잘 되었음이 확실하다면 모든 데이터를 읽어들일 때까지 recv()를 반복문 내부에서 호출해야 합니다.
여러 번의 recv() 호출을 통해 완전한 패킷을 수신하는 작업에 대해 자세한 정보가 필요하다면 망할 데이터 캡슐화 절을 참고하세요.
저는 Windows 시스템을 써서 fork() 시스템 호출이 없고 struct sigaction 같은 것도 없습니다. 어떻게 해야 하나요?
이것이 있다면 그것은 컴파일러와 함께 있는 POSIX 라이브러리에 있을 것입니다. 저는 Windows 시스템을 가지고 있지 않으므로 그에 대해 정확한 답을 줄 수 없습니다. 그러나 기억하기로는 마이크로소프트가 POSIX 호환성 계층을 만들었고 fork()도 거기에 있을 것입니다. (어쩌면 sigaction도 있을 것입니다.) (역자 주: 최신 Windows 환경에서는 보통 POSIX 호환 계층에 기대기보다 Windows의 고유 API나 WSL 같은 별도 실행 환경을 검토하는 편이 더 나을 수 있습니다.)
VC++에 딸려오는 도움말에서 “fork”나 “POSIX”를 검색하고 도움이 될 만한 것이 있는지 살펴보세요. (역자 주: 여기서 VC++는 Visual Studio 및 Microsoft C/C++ 개발 도구의 예전 이름입니다. 이 안내서가 처음 쓰인 시점을 감안해서 읽는 편이 좋습니다.)
그것이 전혀 작동하지 않는다면, fork()/sigaction과 관련된 것들을 떼어내고 Win32에 대응하는 CreateProcess()로 교체하세요. 저는 CreateProcess()를 어떻게 쓰는지는 모릅니다. 그것은 인수를 엄청나게 많이 받지만 아마도 VC++과 함께 제공되는 문서에 설명이 있을 것입니다.
저는 방화벽 뒤에 있습니다. 방화벽 너머의 사람들이 저의 IP 주소를 알고 저의 시스템에 접근하게 하려면 어떻게 해야 하나요?
불행히도 방화벽의 목적은 방화벽 바깥의 사람들이 방화벽 안의 시스템에 접근하는 것을 막는 것입니다. 그러므로 그것을 허용하는 것은 보안에 구멍을 내는 일로 여겨집니다.
그러나 아무 방법도 없다고 말하려고 이 이야기를 꺼낸 것은 아닙니다. 방화벽이 마스커레이딩이나 NAT 처리 같은 것을 한다면 여전히 connect()로 방화벽 너머에 접근할 수 있습니다. 여러분의 프로그램이 언제나 연결을 개시하는 쪽이 되도록 한다면 문제는 없을 것입니다.
만약 그것으로는 충분하지 않다면, 시스템 관리자에게 부탁해서 방화벽에 구멍을 내서 여러분에게 연결할 수 있도록 해야 합니다. 방화벽은 자체 NAT 소프트웨어나 프록시 등을 써서 여러분에게 연결을 전달 해줄 수 있습니다.
방화벽의 구멍은 가볍게 볼 것이 아니라는 점을 기억하세요. 나쁜 사람들에게 내부 네트워크에 대한 접근 권한을 주지 않도록 해야 합니다. 초보자라면 소프트웨어를 안전하게 만드는 것이 생각보다 어렵다는 것을 알아야 합니다.
여러분의 시스템 관리자가 저를 탓하는 일이 없게 해주세요. ;-)
패킷 스니퍼는 어떻게 작성하나요? 어떻게 하면 제 이더넷 인터페이스를 프로미스큐어스 모드로 설정할 수 있을까요?
모르는 이들을 위해 설명하자면, 네트워크 카드가 “프로미스큐어스 모드(promiscuous mode)” 일 때 목적지 주소가 현재 컴퓨터가 아닌 패킷까지 전부 운영체제에 전달합니다. (우리는 IP 주소가 아닌 이더넷 계층 주소에 대해서 이야기하는 것입니다. 그러나 이더넷은 IP보다 낮은 계층이므로, 사실상 모든 IP 주소에 대한 통신이 전달됩니다. 더 자세한 내용은 저수준 넌센스와 네트워크 이론을 참고하세요.)
이것이 패킷 스니퍼 동작의 기본 원리입니다. 패킷 스니퍼는 인터페이스를 프로미스큐어스 모드로 만들고, 운영체제는 그 인터페이스를 통해 전달되는 모든 패킷을 받게 됩니다. 여러분은 이런 데이터를 읽을 수 있는 몇 가지 종류의 소켓을 쓸 수 있습니다.
불행히도 질문에 대한 답은 플랫폼에 따라 다릅니다. 그러나 인터넷을 찾아보면, 예를 들어 “windows promiscuous ioctl”을 검색한다면 도움이 되는 정보를 얻을 수 있을 것입니다. Linux를 위해서는 유용해 보이는 Stack Overflow 스레드49도 있습니다.
어떻게 하면 TCP나 UDP 소켓에 대해서 사용자 정의한 제한 시간 값을 사용할 수 있을까요?
시스템에 따라 다릅니다. 여러분의 시스템이 어떤 기능을 지원하는지 알아내기 위해서 (그리고 그것을 setsockopt()에 쓰기 위해서) SO_RCVTIMEO나 SO_SNDTIMEO 같은 것을 인터넷에서 찾아봐야 할 것입니다.
Linux 맨페이지는 alarm()이나 setitimer()를 대체재로 쓸 것을 권합니다.
어떤 포트가 사용 가능한 상태인지는 어떻게 알아내나요? “공식적인” 포트 번호 목록 같은 것이 있을까요?
보통 이것은 문제가 되지 않습니다. 여러분이 웹 서버를 작성한다고 하면, 80번같이 잘 알려진 포트를 쓰는 것이 좋습니다. 여러분만의 특별한 목적의 서버를 작성한다면 무작위의 포트 번호(그러나 1023보다 큰 것으로)를 고르고 시도해보세요.
만약 포트가 이미 사용 중이라면 bind()를 시도할 때 “Address already in use” 오류가 발생할 것입니다. 다른 포트를 고르세요. (여러분의 소프트웨어의 사용자가 설정 파일이나 명령줄 스위치로 대체 포트를 지정할 수 있게 하는 것이 좋습니다.)
인터넷 할당 번호 관리 기관(the Internet Assigned Numbers Authority, IANA) 이 관리하는 공식 포트 번호50 가 있습니다. (1023보다 큰) 어떤 번호가 저 목록에 있다고 해서 그 포트를 쓸 수 없는 것은 아닙니다. 예를 들어 Id Software의 DOOM은 “mdqs”(그게 무엇이든) 와 같은 포트를 씁니다. 중요한 것은 같은 컴퓨터의 누구도 여러분이 그 포트를 쓰고 싶을 때 그 포트를 쓰고 있지 않으면 된다는 것입니다.