Prev | Contents | Next

8 일반적인 질문들

이 헤더파일들은 어디서 찾을 수 있을까요?

여러분의 시스템에 헤더파일이 없다면 아마도 그것이 필요하지 않을 것입니다. 사용중인 플랫폼의 설명서를 참고하세요. Windows를 위해 작업하고 있다면 #include <winsock.h>만 하면 됩니다.

bind()가 “Address already in use” 를 보고하면 어떻게 해야하나요?

리스닝 소켓에 setsockopt()SO_REUSEADDR옵션과 함께 사용해야 합니다. 예제가 필요하다면 bind()에 관한 절select()에 관한 절을 참고하세요.

시스템에 열린 소켓의 목록을 얻으려면 어떻게 해야하나요?

netstat를 사용하세요. 완전한 정보에 대해서는 man 을 참고해야 하지만 아래와 같이 입력해도 약간의 유용한 출력을 얻을 수 있습니다.

$ netstat

비결은 어떤 소켓이 어떤 프로그램과 연결되어 있는지 알아내는 것입니다. :-)

라우팅 테이블을 보려면 어떻게 해야하나요?

route명령(대개의 리눅스 장치에서 /sbin에 있다) 을 실행하세요. 아니면 netstat -r 명령을 실행하세요. 혹은 ip route 명령일 수도 있습니다.

컴퓨터가 하나밖에 없다면 어떻게 클라이언트와 서버 프로그램을 실행하나요? 네트워크 프로그램을 작성하려면 네트워크가 있어야 하는 것 아닌가요?

여러분에게는 다행히고 사실상 모든 장치가 커널에 자리잡고 네트워크 카드인 척 하는 루프백 네트워크 “장치”를 구현합니다. (이것은 라우팅 테이블에서 “lo”라는 이름으로 표시되는 인터페이스입니다.)

여러분이 “goat”라는 이름의 장치에 로그인했다고 합시다. 클라이언트를 하나의 창에서 실행하고 서버를 다른 창에서 실행합시다. 아니면 서버를 백그라운드에서 실행하고(“server &”) 클라이언트를 같은 창에서 실행합시다. 루프백 장치는 여러분이 client goatclient localhost(“localhost”는 여러분의 /etc/hosts파일에 정의되어 있을 것입니다.) 중 어떤 것이든 할 수 있게 해 줄 것이고 네트워크 없이도 서버와 대화하는 클라이언트 프로그램을 시험할 수 있을 것입니다.

간단히 말하자면 네트워크 없는 단일 장치에서 코드를 실행하기 위해서 코드를 변경할 필요는 없습니다.

원격지 측에서 연결을 닫았는지 어떻게 알 수 있을까요?

recv()0을 돌려주는 것으로 알 수 있습니다.

“ping” 유틸리티를 만들려면 어떻게 해야하나요? ICMP는 무엇인가? raw 소켓과 SOCK_RAW 에 대해서는 어디에서 더 알아볼 수 있을까요?

raw 소켓에 대한 모든 질문은 W. Richard Stevens’ UNIX Network Programming books 에서 답을 얻을 수 있습니다. 또한 온라인으로 사용 가능한43 Stevens’ UNIX Network Programming source code에서 ping/ 하위디렉터리를 살펴보세요.

connect()에 대한 제한시간을 변경하거나 단축할 수 있을까요?

W. Richard Stevens이 여러분에게 줄 수 있는 답과 동일한 답을 드리는 대신, UNIX Network Programming source code의 lib/connect_nonb.c44 를 안내해드리겠습니다.

요점은 socket()으로 소켓 설명자를 만든 후 논 블로킹으로 만든 후에 connect()를 호출할 때 모든 것이 잘 돌아간다면 connect()는 즉시 -1을 반환할 것이고 errnoEINPROGRESS로 설정될 것이라는 것입니다. 그 후 select()를 호출할 때 소켓 설명자를 읽기와 쓰기 집합에 모두 넣으면서 여러분이 원하는 제한시간을 지정하면 됩니다. 시간초과가 발생하지 않으면 connect() 이 완료되었다는 의미다. 이 시점에서 getsockopt()SO_ERROR옵션과 함께 호출해서 connect()호출의 반환값을 얻을 수 있고, 오류가 없었다면 그 값은 0이어야 합니다.

마지막으로 여러분은 아마도 해당 소켓에 데이터를 전송하기 전에 소켓을 다시 블로킹 모드로 설정하고 싶을 것입니다.

이 방식은 프로그램이 연결을 시작하는 동안 다른 일을 할 수 있게 해 주는 장점도 있음에 주목하세요. 예를 들어 500ms정도의 짧은 제한시간을 설정한 후 select()를 다시 호출합시다. select()가 20번 정도 시간초과를 일으킨다면 연결을 포기할 때가 되었음을 알 수 있습니다.

위에서도 말씀드렸지만 완벽하게 훌륭한 예제가 필요하다면 Stevens의 코드를 참고하세요.

Windows를 위해 빌드하려면 어떻게 하나요?

먼저 윈도우를 삭제한 후 리눅스나 BSD를 설치하세요. };-). 사실 그럴 필요는 없고, 도입부의 윈도우즈에서 빌드하기를 위한 절을 살펴보세요.

Solaris/SunOS에서 빌드하려면 어떻게 하나요? 컴파일을 시도하면 계속 링커 오류가 발생합니다!

링커 에러는 Sun사의 장치들이 자동적으로 소켓 라이브러리를 링크하지 않기 때문에 발생합니다. 컴파일을 위해서는 도입부의 Solaris/SunOS를 위한 절 을 참고하세요.

select()가 시그널을 받으면 실패하나요?

시그널은 중단된 시스템 콜이 errnoEINTR로 설정하고 -1을 반환하게 만듭니다. sigaction()으로 시그널 핸들러를 설정하면 SA_RESTART 플래그를 설정할 수 있는데 이것이 방해받은(Interrupted) 시스템 콜이 재개되게 해 줄 것입니다.

태생적으로 이런 방식이 늘 작동하는 것은 아닙니다.(역자 주 : 시스템콜/인터럽션과 관련된 처리는 시스템마다 다를 수 있습니다.)

제가 선호하는 해결책은 goto문을 쓰는 방법입니다. 물론 이것이 교수님들을 아주 짜증나게 할 수 있지만 쓸만합니다.

select_restart:
if ((err = select(fdmax+1, &readfds, NULL, NULL, NULL)) == -1) {
    if (errno == EINTR) {
        // 어떤 시그널이 우리에게 인터럽트를 걸었습니다. 그러니 재시작합니다
        goto select_restart;
    }
    // 진짜 오류는 여기서 처리합니다
    perror("select");
}

물론 여기에서 goto를 쓸 필요는 없습니다. 처리를 위해 다른 구조를 쓸 수 있습니다. 그러나 저는 goto문이 사실 더 깔끔하다고 생각합니다. (역자 주 : 필자의 의견에도 불구하고 역자는 goto문을 쓰는 일을 추천하지 않습니다.)

recv()에 대한 호출에 시간제한을 적용하려면 어떻게 해야하나요?

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);

    // 시간 제한을 위한 timeval 구조체 생성
    tv.tv_sec = timeout;
    tv.tv_usec = 0;

    // 데이터를 받거나 시간이 초과될 때까지 기다린다
    n = select(s+1, &fds, NULL, NULL, &tv);
    if (n == 0) return -2; // timeout!
    if (n == -1) return -1; // error

    // 여기에 데이터가 있어야합니다. 그러니 평범한 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 {
    // 버퍼에 데이터가 들어있습니다
}
.
.
.

recvtimeout()이 시간초과 상황에서 -2를 돌려준다는 점에 주목하세요. 왜 0이 아닌지 궁금한가요? recv()가 원격지 연결이 닫혔을 때 0을 돌려준다는 점을 떠올려봅시다. 그러니 그 값은 쓸 수가 없고, -1은 “오류”를 의미합니다. 그래서 저는 시간초과를 가리키는 값으로 -2를 썼습니다.

소켓에 데이터를 보내기 전에 암호화하거나 압축하려면 어떻게 해야하나요?

데이터를 암호화 하기 위한 간편한 방법 중 하나는 SSL (secure sockets layer)를 사용하는 것입니다. 그러나 그것은 이 안내서의 범위를 벗어납니다. (더 많은 정보가 필요하면 OpenSSL 프로젝트45를 확인하세요.)

그러나 만약 여러분이 자신만의 압축기나 암호화 체계를 만들거나 써 보고 싶다면, 여러분의 데이터가 양 끝 사이에서 정해진 단계를 거친다는 점을 생각해보세요. 각 단계는 데이터를 특정한 방법으로 바꿉니다.

  1. 서버가 데이터를 파일(또는 다른 것)에서 읽어들입니다
  2. 서버가 데이터를 암호화/압축합니다(여러분이 이 단계를 추가합니다)
  3. 서버가 암호화된 데이터를 send()합니다

반대편은 이렇습니다.

  1. 클라이언트가 암호화된 데이터를 recv()합니다
  2. 클라이언트가 데이터를 복호화/압축해제합니다(여러분이 이 단계를 추가합니다)
  3. 클라이언트가 데이터를 파일(또는 다른 것)에 씁니다

만약 압축과 암호화를 모두 할 생각이라면 압축을 먼저 해야한다는 점을 기억하세요. :-) (역자 주 : 최소한 순서는 일관되어야 합니다.)

클라이언트가 서버가 했던 작업을 제대로 거꾸로 수행하기만 한다면 여러분이 얼마나 많은 단계를 추가하던 데이터는 무사히 도착할 것입니다.

그러므로 여러분이 저의 코드를 쓰기 위해서 할 일은 데이터를 네트워크에서 읽고 보내는 코드의 중간 지점을 찾아내서 암호화를 하는 코드를 끼워넣는 것입니다.

PF_INET”이 계속 등장하는데 무엇인가요? AF_INET와 관계가 있을까요?

그렇습니다. 관계가 있습니다. 자세한 사항은 socket()에 대한 절을 참고하세요.

클라이언트에게서 셀 커맨드를 받아서 실행하는 서버는 어떻게 만드나요?

단순함을 위해서 클라이언트가 connect()send() 후에 연결을 close() 처리한다고 가정합시다. (즉 클라이언트가 연결을 닫지 않고 후속 시스템 콜을 보내는 일은 없다는 의미입니다.)

클라이언트의 과정은 이렇습니다.

  1. 서버에 connect()
  2. send("/sbin/ls > /tmp/client.out")
  3. 연결에 대한 close() 처리

한편 서버는 데이터를 받아서 실행합니다.

  1. 클라이언트의 연결 요청에 대한 accept()
  2. 명령 문자열에 대한 recv(str)
  3. 연결에 대한 close()
  4. 명령을 실행하기 위해서 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()호출을 통해 완전한 패킷을 수신하는 작업에 대해 자세한 정보가 필요하다면 망할 데이터 캡슐화 절을 참고하세요.

저는 윈도우 장치를 써서 fork()시스템 호출이 없고 struct sigaction같은 것도 없습니다. 어떻게 해야하나요?

이것이 있다면 그것은 컴파일러와 함께 있는 POSIX 라이브러리에 있을 것입니다. 저는 윈도우 장치를 가지고 있지 않으므로 그에 대해 정확한 답을 줄 수 없습니다. 그러나 기억하기로는 마이크로소프트가 POSIX 호환성 계층을 만들었고 fork()도 거기에 있을 것입니다. (어쩌면 sigaction도 있을 것입니다.) (역자 주 : 그러나 윈도우에서는 윈도우의 처리법을 사용하는 것이 의도한 결과를 정확히 만드는 더 나은 방법이 될 것입니다.)

VC++에 딸려오는 도움말에서 “fork”나 “POSIX”를 검색하고 도움이 될만한 것이 있는지 살펴보세요. (역자 주 : VC++ 자체도 Visual Studio가 가진 기능 중 일부의 오래된 이름에 불과합니다. 이 글이 최초에 작성된 것은 90년대임을 기억하세요.)

그것이 전혀 작동하지 않는다면, fork()/sigaction과 관련된 것들을 떼어내고 그것의 Win32 대응인 CreateProcess()로 교체하세요. 저는 CreateProcess()를 어떻게 쓰는지는 모릅니다. 그것은 수억개의 인수를 받지만 아마도 VC++과 같이 오는 문서에 설명이 있을 것입니다.

저는 방화벽 뒤에 있습니다. 방화벽 너머의 사람들이 저의 IP 주소를 알고 저의 장치에 접근하게 하려면 어떻게 해야하나요?

불행히도 방화벽의 목적은 방화벽 바깥의 사람들이 방화벽 안의 장치에 접근하는 것을 막는 것입니다. 그러므로 그것을 허용하는 것은 보안에 헛점을 만들게 됩니다.

그러나 모든 것이 안 된다고 말하려고 이 이야기를 꺼낸 것은 아닙니다. 방화벽이 마스커레이딩이나 NAT처리같은 것을 한다면 여전히 connect()로 방화벽 너머에 접근할 수 있습니다. 여러분의 프로그램이 언제나 연결을 게시하는 쪽이 되도록 한다면 문제는 없을 것입니다.

만약 그것으로는 충분하지 않다면, 시스템 관리자에게 부탁해서 방화벽에 구멍을 내서 여러분에게 연결할 수 있도록 해야합니다. 방화벽은 그것의 NAT프로그램이나 프록시 등을 써서 여러분에게 연결을 전달(Forward) 해줄 수 있습니다.

방화벽의 구멍은 가볍게 볼 것이 아니라는 점을 기억하세요. 나쁜 사람들에게 내부 네트워크에 대한 접근 권한을 주지 않도록 해야합니다. 초보자라면 소프트웨어를 안전하게 만드는 것이 생각보다 어렵다는 것을 알아야합니다.

여러분의 시스템 관리자가 저를 탓하는 일이 없게 해주세요. ;-)

패킷 스니퍼는 어떻게 작성하나요? 어떻게 하면 제 이더넷 인터페이스를 무차별 모드로 설정할 수 있을까요?

모르는 이들을 위해 설명하자면, 네트워크 카드가 “무차별 모드(promiscuous mode)” 일 때 목적지 주소가 실행중인 장치가 아닌 패킷까지 전부 운영체제에 전달합니다. (우리는 IP 주소가 아닌 이더넷 계층 주소에 대해서 이야기하는 것입니다. 그러나 이더넷은 IP보다 낮은 계층이므로, 사실상 모든 IP주소에 대한 통신이 전달됩니다. 더 자세한 내용은 저수준 넌센스와 네트워크 이론을 참고하세요.)

이것이 패킷 스니퍼 동작의 기본 원리입니다. 패킷 스니퍼는 인터페이스를 무차별 모드로 만들고, 운영체제는 그 장치를 통해 전달되는 모든 패킷을 받게 됩니다. 여러분은 이런 데이터를 읽을 수 있는 몇 가지 종류의 소켓을 쓸 수 있습니다.

불행히도 질문에 대한 답은 플랫폼에 따라 다릅니다. 그러나 인터넷을 찾아보면, 예를 들어 “windows promiscuous ioctl”을 검색한다면 도움이 되는 정볼르 얻을 수 있을것입니다. 리눅스를 위해서는 useful Stack Overflow thread46 같은 정보가 있습니다.

어떻게 하면 TCP나 UDP소켓에 대해서 사용자 정의한 제한시간 값을 사용할 수 있을까요?

시스템에 따라 다릅니다. 여러분의 시스템이 어떤 기능을 지원하는지 알아내기 위해서 (그리고 그것을 setsockopt()에 쓰기 위해서) SO_RCVTIMEOSO_SNDTIMEO 같은 것을 인터넷에서 찾아봐야 할 것입니다.

리눅스의 맨페이지는 alarm()setitimer()를 대체재로 쓸 것을 권합니다.

어떤 포트가 사용 가능한 상태인지는 어떻게 알아내나요? “공식적인” 포트 번호 목록같은 것이 있을까요?

보통 이것은 문제가 되지 않습니다. 여러분이 웹 서버를 작성한다고 하면, 80번같이 잘 알려진 포트를 쓰는 것이 좋습니다. 여러분만의 특별한 목적의 서버를 작성한다면 무작위의 포트 번호(그러나 1023보다 큰 것으로)를 고르고 시도해보세요.

만약 포트가 이미 사용중이라면 bind()를 시도할 때 “Address already in use” 오류가 발생할 것입니다. 다른 포트를 고르세요. (여러분의 소프트웨어의 사용자가 설정 파일이나 명령줄 스위치로 대체 포트를 지정할 수 있게 하는 것이 좋습니다.)

인터넷 할당 번호 관리 기관(the Internet Assigned Numbers Authority, IANA) 이 관리하는 공식 포트 번호47 가 있습니다. (1023보다 큰) 어떤 번호가 저 목록에 없다고 해서 그 포트를 쓸 수 없는 것은 아닙니다. 예를 들어 Id Software의 DOOM은 “mdqs”(이것이 무엇이든) 와 같은 포트를 쓴다. 중요한 것은 같은 장치의 누구도 여러분이 그 포트를 쓰고 싶을 때 그 포트를 쓰고 있지 않으면 된다는 것입니다.


Prev | Contents | Next