반응형

http://wiki.kldp.org/wiki.php/BeeJNetworkProgramming


BeeJ's Guide to Network Programming

인터넷 소켓 활용(v.2.3.1, 8-October-2001) http://www.ecst.csuchico.edu/~beej/guide/net

번역 : 박성호(tempter@fourthline.com),1998/8/20

고친 과정
  • 고침 v2.3.1, 수정일: 2003-12-15, 고친이 김수봉
    • html에서 위키로 고치고 2.3.1 버전으로 갱신하였습니다.
    • 원래의 번역글은 [http]여기 입니다.



내용



1 시작

소켓 프로그램이 어렵나요? 그냥 맨페이지만 보고서는 알아내기가 좀 어럽나요? 뭔가 있어보이는 인터넷 프로그램을 만들고 싶지만 bind()를 호출하고 connect()를 호출하고 이런 저런 구조체를 뒤지고 할 시간이 없나요?

글쎄요, 제가 그 지겨운걸 다 해놓았고요, 여러분과 이 정보를 공유하고 싶군요. 바로 찾아오셨습니다. 이 문서가 바로 평균적인 C 프로그래머에게 네트워크 프로그램에 관련된 정보를 드릴겁니다.


1.1 대상

이 문서는 안내서이지 레퍼런스는 아닙니다. 아마도 소켓 프로그래밍을 처음 시작하면서 어디서부터 해야 할지 모르는 사람들에게 도움이 될겁니다. 물론 어떤 의미에서도 이 글은 소켓 프로그래밍에 관한 완벽한 안내서는 아닐 겁니다. 단지 도저히 의미를 알 수 없던 맨페이지들을 조금씩 이해하게 되기만 바랄 뿐입니다.


1.2 사용도구

대부분의 코드는 리눅스 PC에서 GNU의 gcc를 이용하여 컴파일 되었습니다. 아마도 gcc를 사용하는 어떤 플랫폼에서도 컴파일 될 것입니다. 당연히 이것은 윈도우즈를 프로그래밍할때는 적용되지 않습니다. 아래의 윈도우 프로그래밍 섹션을 보십시오. (이하 존칭 생략)

1.3 공식 홈페이지

이 문서의 공식적인 위치는 chico의 캘리포니아 주립대학의 http://www.ecst.csuchico.edu/~beej/guide/net/ 이다.

1.4 Solaris/SunOS 프로그래머를 위한 유의사항

Solaris나 SunOS를 위해 컴파일 할때 몇몇의 적절한 라이브러리를 링크하기 위해 특별한 커맨드 라인 스위치를 지정할 필요가 있다. 아래처럼 "lnsl -lsocket -lresolv"를 컴파일 명령의 끝에 덧붙여라.

$cc -o server server.c -lnsl -lsocket -lresolv

만약 여전히 에러가 난다면 "-lxnet"를 커맨드 라인 끝에 덧붙일 수도 있다. 나는 그것이 정확히 무엇을 하는지는 모르지만 사람들이 그것이 필요하다고 한다.

문제가 발생하는 또다른 지점은 setsockopt()를 호출하는 부분이다. 함수 원형이 내 리눅스 박스와 다르다, 따라서

int yes=1;

대신에

char yes='1';
을 입력하라

내가 Sun박스를 가지고 있지 않기 때문에, 위의 어떤 정보도 테스트해보지 않았다-- 이것은 사람들이 e-mail로 나에게 알려준 것들이다.

1.5 윈도우 프로그래머들을 위한 주의사항


나는 특별히 윈도우를 싫어하고 Linux나 BSD또는 Unix를 써보기를 권하지만 여전히 윈도우에서 사용하겠다는 사람들이 있었다. 첫번째로, 내가 여기서 언급한 꽤많은 헤더파일들은 무시하라. 당신이 포함해야할 것은

#include <winsock.h>

뿐이다.

잠깐! 당신은 또한 소켓라이브러리로 어떤일을 하기 전에 WSAStartup()를 호출해야 한다. 그것을 하기 위한 코드는 다음과 같다.

#include <winsock.h>

{
    WSADATA wsaData;  //if this doesn't work
    //WSAData wsaData; //then try this instead

    if(WSAStartup(MAKEWORD(1,1), &wsaData) != 0){
       fprintf(stderr, "WSAStartup failed.\n");
       exit(1);
    }

당신은 또한 당신의 컴파일러가 일반적으로 wsock3.lib나 winsock32.lib로 불리는 Winsock라이브러리를 링크하도록 해야한다. VC++하에서는 이것들은 Project메뉴의 Setting아래에서 Link탭을 클릭해서 "Object/library modules"를 통해 이루어 진다. "wsock32.lib"를 그 리스트에 덧붙여라. 라고 들었다.

마지막으로 당신은 WSAClenaup()를 소켓라이브러리를 다 쓴후에 호출해야 한다. 자세한 것은 온라인 도움말을 보라.

일단 그것을 하면 이 안내서의 나머지 예제는 몇몇 예외를 제외하고 일반적으로 적용될것이다. 하나 예를 들면, 당신은 close()를 소켓을 닫기위해 사용할수 없고 closesocket()를 대신 사용해야 한다. 또한 select()는 파일기술자(stdin의 0같은)가 아니고 소켓 기술자에만 사용할 수 있다.

당신이 사용할 수 있는 소켓 클래스인 CSocket도 있다. 더 많은 정보는 당신의 컴파일러의 도움말페이지를 참고하라.

Winsock에 대한 더 많은 정보를 얻기위해서는 Winsock FAQ를 읽고 거기서 시작하라.

마지막으로 나는 윈도우즈가 fork() 시스템 호출이 없다고 들었는데 그것은 나의 몇몇 예제에서 쓰이고 있다. 아마도 POSIX라이브러리나 그것이 작동하게하기 위한 어떤것을 링크하거나 매개변수가 없는 fork()대신 48억개의 매개변수를 가지는 CreateProcess()를 사용할 수도 있다. 만약 당신이 그렇게까지 하기 싫다면 CreateThread()는 좀 더 요약하기 쉽다. 불행하게도 멀티스레딩은 이 문서의 범위에서 벗어난다. 내가 이야기 할 수 있는것은 여기까지다.

1.6 이메일 정책


나는 이메일 질문에 답할수 있는 여유가 있으니 편하게 메일을 보내도 되지만 답장을 보낸다고 확신할 수는 없다. 나는 꽤 바쁘게 살고 있어서 당신이 보낸 질문에 답할 수 없을때도 있다. 그럴 경우에는 나는 보통은 메시지를 지운다. 개인적인 감정은 없다; 당신이 요구한 자세한 답변을 할 수 없을 뿐이다.

대개 질문이 복잡할 수록 답변이 없을 확률이 높다. 만약 메일 보내기 전에 당신의 질문을 좁은 범위로 한정해 주고 어떤 적절한 정보( 플랫폼, 컴파일러, 당신이 받은 에러 메시지, 그리고 당신이 생각하기에 오류를 잡기에 적당하다고 생각되는 어떤것)를 포함한다면 답변을 확률은 더 높아진다. 더 많은 질문법은 ESR의 문서, [http]How To Ask Questions The Smart Way를 읽어보라.

만약 당신이 답변을 얻지 못했다면 좀 더 찾아보고 답을 찾기위해 노력해보고 그래도 안된다면 당신이 발견한 정보와 내가 줄 수 있는 충분한 정보를 포함한 메일을 보내라.

나는 당신을 어떻게 메일을 쓰고 안써야 하는지에 대해 잔소리를 했다. 나는 정말로 몇년간 내가 이 안내서에대해 받은 칭찬에 대해 감사하고 싶다. 그것은 정말 사기를 끌어올린다.그리고 그것이 쓸모있다는 말을 듣는것은 나를 정말 기쁘게 한다. ;-) 고맙다!

1.7 미러링

공적으로든 사적으로든 미러해 준다면 정말 고맙겠다. 만약 공식적으로 이 사이트를 미러링하고 내가 메인페이지에서 링크하기를 원한다면 나에게 beej@piratehaven.org로 메일을 보내면 된다.


1.8 번역자에게

만약 당신이 이 글을 번역하기를 원한다면 beej@piratehaven.org로 메일 보내달라. 그러면 나는 메인페이지에서 당신이 번역한 글을 링크할 것이다.

번역물에 당신의 이름과 이메일 주소를 추가하는것은 자유다. 미안하지만 공간의 제약때문에 번역물을 내 스스로 제공할 수는 없다.

1.9 저작권과 배포권


Beej's Guide to Network Programming는 ⓒ 1995-2001 Brian "Beej"Hall에게 저작권이 있다.

이 안내서는 내용물이 변형되지 않고, 완전하게 제공되며, 저작권 정보가 남아있는한 어떠한 매체로도 재출력될 수 있다.

특히교육자들에게 이 안내서를 그들의 학생들에게 추천하거나 복사본을 제공하기를 권한다.

이 안내서는 번역이 정확하고 모든 문서가 재출력된다면 자유롭게 어떤 언어로도 번역될 수 있다. 번역은 번역자의 이름과 접근 정보가 포함 될 수도 있다.

이 문서에 포함된 C 소스코드는 공공의 사용을 허가한다. 더 많은 정보는 beej@piratehaven.org에게 문의하라.



2 소켓이란 무엇인가.

소켓이란 단어는 많이 들었을 것이다. 그리고 아마도 그 소켓이 정확히 무엇인가에 대하여 궁금해 하기도 했을 것이다. 소켓은 정규 유닉스 파일 기술자를 이용하여 다른 프로그램과 정보를 교환하는 방법을 의미한다.

뭐라고라?

좋다. 아마도 유닉스를 잘하는 사람들이 이렇게 얘기하는 것을 들어본 적이 있을 것이다. "유닉스에서는 모든게 파일로 되어있군!" 실제로 그들이 얘기하는 것은 모든 유닉스 프로그램들이 어떤 종류의 입출력을 하더라도 파일 기술자를 통해서 하게 된다는 것이다. 파일 기술자는 사실 열려진 파일을 의미하는 정수일 뿐이다. 그러나 그 파일은 네트워크가 될수도 있고 FIFO, 파이프, 터미널, 실제 디스크상의 파일이 될수도 있으며 그 밖의 무엇도 다 된다는 것이다. 유닉스의 모든것은 파일이다! 따라서 당신이 인터넷을 통하여 멀리 떨어진 다른 프로그램과 정보를 교환하기 위해서는 파일 기술자를 이용하면 된다는 것이다. 믿으쇼~

"똑똑이 양반, 그 파일 기술자는 도대체 어떻게 만드는거요?" 라는게 당신의 맘속에 지금 막 떠오른 질문일 것이다. 여기에 대답이 있다. socket()을 호출하면 소켓 기술자를 얻게 되고 send(), recv()등의 소켓에 관련된 함수를 호출하여 정보를 교환할 수 있다. (man send, man recv를 해봐도 됨)

"잠깐!" 이렇게 이의를 제기하겠지. "그 소켓 기술자가 파일 기술자라면 도대체 왜 read(),write()를 쓰면 안되는거요?" 짧게 말하면 맞다. 그러나 send(),recv()를 쓰는 것이 여러모로 네트워크를 통한 정보전달을 제어하기에 도움이 된다는 것이다.

다음은 뭔가? 소켓의 종류는? DARPA 인터넷 주소(인터넷 소켓), 경로명과 지역노드(유닉스 소켓), CCITT X.25 주소(X.25 소켓, 그냥 무시해도 됨)등이 있고 아마도 당신이 쓰는 유닉스에 따라서 더 많은 종류의 소켓들이 있을 것이다. 이 문서는 첫번째 (인터넷 소켓) 하나만 설명할 것이다.




2.1 두가지 종류의 소켓

인터넷 소켓에 두가지 종류가 있나? 그렇다. 음..사실은 거짓말이다. 좀 더있긴 하지만 겁을 주고 싶지 않기 때문에 이것 두가지만 이야기 하는 것이다. RAW 소켓이라는 매우 강력한 것도 있으며 한번 봐두는 것도 좋다.

두가지 종류는 무엇인가? 하나는 스트림소켓 이고 다른 하나는 데이터그램 소켓이다. 이후에는 SOCK_STREAM, SOCK_DGRAM으로 지칭될 것이다. 데이터그램 소켓은 비연결 소켓이라고도 한다. (비록 그 소켓에서도 원한다면 connect()를 사용할 수도 있다. connect()절을 참조할것)

스트림 소켓은 양측을 신뢰성있게 연결해 주는 소켓이다. 만약 두가지 아이템을 이 소켓을 통하여 보낸다면 그 순서는 정확히 유지될 것이다. 에러까지 교정된다. 만일 에러가 생긴다면 당신 실수이고 당신실수를 막는 방법은 여기서 설명하지 않을 것이다.

스트림 소켓은 어디에 쓰이는가? 아마도 텔넷이라고 들어봤을 것이다. 들어봤느뇨? 그게 이 소켓을 쓴다. 입력한 모든 글자는 그 순서대로 전달이 되야 하는 경우이다. 사실 WWW사이트의 포트 80에 텔넷으로 접속하여 "GET pagename" 을 입력하면 HTML 화일의 내용이 우르르 나올 것이다.

어떻게 스트림 소켓이 이정도의 정확한 전송 품질을 갖추게 되는가? 이 소켓은 TCP를 이용하기 때문이다. (Transmission Control Protocol, RFC-793에 무척 자세하게 나와있다.) 아마도 TCP 보다는 TCP/IP를 더 많이 들어봤을 것이다. 앞부분은 바로 이 TCP이고 뒷부분의 IP는 인터넷 라우팅을 담당하는 프로토콜이다.

괜찮군~ 데이터그램 소켓은 어떤가? 왜 비연결이라고 하는지? 내용에 무슨 관련이 있는지? 왜 신뢰도가 떨어지지? 사실 이 소켓의 경우 당신이 데이터그램을 보낸다면 정확히 도착할 수도 있다. 또는 패킷들의 순서가 바뀌어서 도착할 수도 있다. 그러나 만약 도착한다면 그 내용은 사실 정확한 것이다.

데이터그램 소켓 또한 라우팅에는 IP를 이용하지만 TCP는 이용하지 않는다. 사실은 UDP(RFC-768)을 이용한다.

연결을 안하는가? 스트림 소켓에서처럼 열려있는 연결을 관리할 필요가 없는 것이다. 그냥 데이터 패킷을 만들어서 목적지에 관련된 IP헤더를 붙여서 발송하기만 하면 되는 것이다. 연결이 필요없다. 보통 tftp나 bootp 에 사용되는 것이다.

좋아! 그러면 데이터 패킷이 도착하지 않을지도 모르는 이런 걸 어떻게 실제 프로그램에서 사용하지? 사실 프로그램들은 UDP위에 그 나름대로의 대책을 갖추고 있는 것이다. 예를 들면 tftp같은 경우에는 하나의 패킷을 보낸 후에 상대편이 잘 받았다는 응답 패킷이 올때까지 기다리는 것이다. 만약 일정시간(예를 들면 5초)동안 응답이 없으면 못받은 것으로 간주하고 다시 보내고, 다시 보내고 응답이 있으면 다음 패킷을 보내고 하게 되는것이다. 이 잘받았다는 응답(ACK reply) 방식은 사실 SOCK_DGRAM을 사용할 경우 매우 중요하다.




2.2 네트워크 이론과 저 아래의 알 수 없는 것들

간단히 프로토콜의 레이어에 대해서 언급을 했지만(UDP위에 나름대로의 대책 어쩌구) 이제는 실제로 네트워크가 어떻게 작동하는 지를 알아볼 때가 되었고 실제로 SOCK_DGRAM이 어떻게 구성되는 지를 알아볼 필요가 있을 것같다. 사실 이 절은 그냥 넘어가도 된다.

여러분~ 이제는 데이타 캡슐화에 대하여 배우겠어요~ 사실 이것은 매우 중요하다. 얼마나 중요하냐면 우리 학교에서 네트워크 코스를 통과하려면 반드시 알아야 하는 사항이기 때문이다. (흠..) 내용은 이렇다. 데이터 패킷이 만들어지면 먼저 첫번째 프로토콜(tftp 프로토콜)에 필요한 머리말과 꼬리말이 붙는다. 이렇게 한번 캡슐화된 내용은 다시 두번째 프로토콜(UDP)에 관련된 머리말과 꼬리말이 다시 붙게 된다. 그 다음에는 IP, 그 다음에는 마지막으로 하드웨어 적인 계층으로서 이더넷 프로토콜로 캡슐화가 되는 것이다.


다른 컴퓨터에서 이 패킷을 받게 되면 하드웨어가 이더넷 헤더를 풀고 커널에서 IP와 UDP 헤더를 풀고 tftp 프로그램에서 tftp헤더를 풀고 하여 끝으로 원래의 데이터를 얻게 되는 것이다.

이제 드디어 악명높은 계층적 네트워크 모델(Layered Network Model)을 얘기할 때가 된것 같다. 이 모델은 다른 모델들에 비해서 네트워크의 시스템을 기술하는 측면에서 많은 이점이 있다. 예를 들면 소켓 프로그래밍을 하는 경우 더 낮은 계층에서 어떤 물리적인 방식(시리얼인지 thin ethernet인지 또는 AUI방식인지)으로 전달되는 지에 대하여 전혀 신경을 쓰지 않고도 작업이 가능해 질 수 있다는 것이다. 실제 네트워크 장비나 토폴로지는 소켓 프로그래머에게는 전혀 관계없는 분야이다.

더이상 떠들지 않고 다음 계층들을 일러 주는데 만일 네트워크 코스에서 시험을 보게 될 경우라면 외우는 것이 좋을 것이다.

  • Application
  • Presentation
  • Session
  • Transport
  • Network
  • Data Link
  • Physical

물리적 계층(Physical layer)는 하드웨어(시리얼, 이더넷등) 이다. 어플리케이션 계층은 상상할 수 있듯이 물리적 계층의 반대편 끝이다. 이 계층을 통하여 사용자는 네트워크와 접촉하게 되는 것이다.

사실 이 모델은 자동차 수리 설명서 처럼 실질적인 뭔가를 할 수 있기에는 너무나 일반적인 얘기이다. 유닉스의 경우를 들어 보다 실질적인 얘기를 해 본다면,

  • Application Layer (telnet, ftp, etc.)
  • Host-to-Host Transport Layer (TCP, UDP)
  • Internet Layer (IP and routing)
  • Network Access Layer (was Network, Data Link, and Physical)

이러한 계층으로 살펴 본다면 아까의 데이터 캡슐화가 각각 어떤 계층에 속하는 가를 알 수 있을 것이다.

이렇게 많은 작업이 하나의 데이터 패킷을 만드는데 동원되는 것이다. 이 내용을 당신이 데이터의 패킷 머리 부분에 몽땅 타이핑 해 넣어야 한다는 얘기다. (물론 농담이다.) 스트림 소켓의 경우 데이터를 내보내기 위해 해야 할 일은 오직 send()를 호출하는 것 뿐이다. 데이터 그램의 경우에는 원하는 방식으로 데이터를 한번 캡슐화하고 (tftp방식등) sendto()로 보내버리면 되는 것이다.커널이 전송계층과 인터넷 계층에 관련된 캡슐화를 하고 나머지는 하드웨어가 한다. 아~ 첨단 기술!!

이것으로 간단한 네트워크 이론은 끝이다. 참, 라우팅에 관해서 하고 싶던 얘기들을 하나도 안했다. 흠, 하나도 없다. 정말이지 라우팅에 관해서 하나도 얘기하지 않을 것이다. 라우터가 IP헤더를 벗겨내서 라우팅 테이블을 참조하여 어쩌구 저쩌구...만일 정말로 여기에 관심이 있다면 IP RFC를 참조할 것이며 만약 거기에 대해서 하나도 알지 못한다면! 생명에 지장은 없다.




3 구조체들과 데이타 처리

결국은 여기까지 왔군. 드디어 프로그래밍에 관한 얘기를 할 때이다. 이 절에서는 실제로 꽤나 이해하기 어려운 소켓 인터페이스에서 쓰이는 여러가지 데이터 타입에 대한 얘기를 할 예정이다.

먼저 쉬운것. 소켓 기술자이다.소켓 기술자의 데이터 형은
 int

이다. 그냥 보통 int이다. (정수형)

뭔가 좀 이상하더라도 그냥 참고 읽기 바란다. 이것은 알아야 한다. 정수에는 두 바이트가 있는데 상위 바이트가 앞에 있거나 또는 하위 바이트가 앞에 있게 된다. 앞의 경우가 네트워크 바이트 순서이다. 어떤 호스트는 내부적으로 네트워크 바이트 순서로 정수를 저장하는 경우도 있으나 안그런 경우가 많다. 만일 NBO라고 언급된 정수가 있다면 함수를 이용하여 (htons()함수) 호스트 바이트 순서로 바꾸어야 한다. 만약 그런 언급이 없다면 그냥 내버려 둬도 된다.

첫번째 구조체, struct sockaddr. 이 구조체는 여러가지 형태의 소켓 주소를 담게된다.
    struct sockaddr {
        unsigned short    sa_family;    /* address family, AF_xxx       */
        char              sa_data[14];  /* 14 bytes of protocol address */
    };

sa_family 는 여러가지가 될 수 있는데, 이 문서에서는 그중에서 "AF_INET"인 경우만 다루게 된다. sa_data 는 목적지의 주소와 포트번호를 가지게 된다. 약간 비실용적이군.

sockaddr 구조체를 다루기 위해서는 다음과 같은 parallel structure를 만들어야 한다. ("in"은 인터넷을 의미한다.)
    struct sockaddr_in {
        short int          sin_family;  /* Address family               */
        unsigned short int sin_port;    /* Port number                  */
        struct in_addr     sin_addr;    /* Internet address             */
        unsigned char      sin_zero[8]; /* Same size as struct sockaddr */
    };

이 구조체는 각각의 항을 참조하기가 좀더 쉬운 것 같다. 주의할 점은 sin_zero배열은 sockaddr 과 구조체의 크기를 맞추기 위해서 넣어진 것이므로 bzero()나 memset()함수를 이용하여 모두 0으로 채워져야 한다. 또한 꽤 중요한 점인데, 이 구조체는 sockaddr 의 포인터를 이용하여 참조될 수 있고 그 반대도 가능하다는 것이다. 따라서 socket()함수가 struct sockaddr * 를 원하더라도 struct sockaddr_in을 사용할 수 있고 바로 참조할 수도 있는 것이다. 또한 sin_family는 sa_family에 대응되는 것이며 물론 "AF_INET"로 지정되어야 하며 sin_port, sin_addr은 네트워크 바이트 순서로 되어야 하는 점이 중요한 것이다.

그러나! 어떻게 struct in_addr sin_addr 전체가 NBO가 될 수 있는가? 이 질문은 살아남은 가장 뭣같은 유니온인 struct in_addr 에 대한 보다 신중한 검토가 필요할 것같다.
    /* Internet address (a structure for historical reasons) */
    struct in_addr {
        unsigned long s_addr; /* that's a 32-bit long, or 4 bytes */
    };

음.. 이것은 유니온 "이었었"다. 그러나 그런 시절은 지나갔다. 시원하게 없어졌군! 따라서 만약 "ina"를 struct sockaddr_in형으로 정의해 놓았다면 ina.sin_addr.s_addr 로 NBO 상태의 4바이트 인터넷 어드레스를 정확하게 참조할 수 있을 것이다. 만약 사용하는 시스템이 struct in_addr에 그 끔찍한 유니온을 아직도 사용하고 있더라도 #defines S 덕분에 위에 한것과 마찬가지로 정확하게 참조할 수는 있을 것이다.




3.1 순서 바꾸기

이제 다음 절로 왔다. 네트워크와 호스트 바이트 순서에 대해서 말이 너무 많았고 이제는 실제 움직일 때라고 본다.

좋다. 두가지 형태의 변환이 있는데 하나는 short(2 바이트)와 long(4바이트)의 경우이다. 이 함수들은 unsigned변수에서도 잘 작동된다. 이제 short변수를 호스트 바이트 순서에서 네트워크 바이트 순서로 변환하는 경우를 보자. 호스트의 h 로 시작해서 to 를 넣고 네트워크의 n 을 넣은 후 short의 s 를 넣는다. 그래서 htons()이다. (읽기는 호스트 투 네트워크 쇼트이다.)

너무 쉬운가?

사실 h,n,s,l 의 어떤 조합도 사용가능하다. (물론 너무 바보스러운 조합을 하지는 않겠지..예를 들어 stolh, 쇼트 투 롱 호스트?? 이런건 없다. 적어도 이 동네에서는없다.) 있는 것들은 다음과 같다.

||
  • htons()--"Host to Network Short"
  • htonl()--"Host to Network Long"
  • ntohs()--"Network to Host Short"
  • ntohl()--"Network to Host Long"
||

아마도 이제 상당히 많이 알게된 것같이 생각들을 할 것이다. "char의 바이트 순서를 어떻게 바꾸지?(역자주: 이 질문은 아마 의미없는 질문으로 한 것 같은데 답도 없고 더이상의 언급이 없는 것으로 보아 빼고 싶은 부분이다.)" 또는 "염려마, 내가 쓰는 68000 기계는 이미 네트워크 바이트 순서로 정수를 저장하니까 변환할 필요는 없어 " 라고 생각할 수도 있을 것이다. 그러나 꼭 그렇지만은 않다. 그렇게 작성된 프로그램을 다른 기계에서 작동시킨다면 당연히 문제가 발생할 것이다. 여기는 유닉스 세계고 이기종간의 호환성은 매우 중요한 것이다. 반드시 네트워크에 데이터를 보내기 전에 네트워크 바이트 순서로 바꿔서 보낸다는 것을 기억할 지어다.

끝으로 sin_addr, sin_port는 네트워크 바이트 순서로 기록하는데 왜 sin_family는 안 그러는가? 답은 간단하다. sin_addr과 sin_port는 캡슐화되어 네트워크로 전송되어야 하는 변수인 것이다. 따라서 당연히 NBO여야 한다. 그러나 sin_family는 시스템 내부에서 커널에 의해서만 사용되는 변수이며 네트워크로 전송되지 않는 것이므로 호스트 바이트 순서로 기록되어야 하는 것이다.




3.2 IP주소는 무엇이며 어떻게 다루는가?

다행스럽게도 IP주소를 산정해 주는 수많은 함수들이 있으며 따라서 4바이트의 long변수에 직접 계산해서 << 연산자를 이용해서 집어넣어야 하는 수고는 할 필요가 없다.

먼저 struct sockaddr_IN ina가 정의되어 있고 132.241.5.10 이 IP 주소이며 이 값을 변수에 넣어야 한다고 가정해 보자. inet_addr()함수가 바로 이럴때 사용하는 것이다. 그 함수는 숫자와 점으로 구성된 IP주소를 unsigned long 변수에 집어 넣어 준다. 다음과 같이 하면 된다.
ina.sin_addr.s_addr = inet_addr("132.241.5.10")
inet_addr()는 결과값으로 이미 NBO인 값을 돌려주며 굳이 htonl()을 또 사용할 필요는 없다는 점에 주의해야 한다. 멋지군!

그러나 위의 짤막한 코드는 그렇게 견실해 보이진 않는다. 왜냐하면 inet_addr()은 에러의 경우 -1을 돌려주게 되며 unsigned long에서 -1은 255.255.255.255를 의미한다. 이는 인터넷 브로드캐스트 어드레스가 된다. 나쁜 녀석. 항상 에러 처리를 확실히 하는것이 좋다.

좋다. 이제 IP주소를 long에 넣는것은 알았는데 그 반대는 어떻게 할 것인가? 만약에 값이 들어있는 struct in_addr은 가지고 있는데 이를 숫자와 점으로 표시하려면? 이 경우는 inet_ntoa()를 쓰면 된다.(ntoa 는 네트워크 투 아스키이다.)
    printf("%s",inet_ntoa(ina.sin_addr));

위의 코드는 IP주소를 프린트 해 줄것이다. 이 함수는 long 변수가 아니라 struct in_addr 를 변수로 받아 들인다는 점을 주의해야 한다. 또한 이 함수는 char 에 대한 포인터를 결과로 돌려 주는데 이는 함수내에 static 한 공간에 저장되며 따라서 매번 함수가 호출될 때마다 이 포인터가 가리키는 곳의 값은 변화한다는 것이다. 즉 예를 들면,
    char *a1, *a2;
    .
    .
    a1 = inet_ntoa(ina1.sin_addr);  /* this is 198.92.129.1 */
    a2 = inet_ntoa(ina2.sin_addr);  /* this is 132.241.5.10 */
    printf("address 1: %s\n",a1);
    printf("address 2: %s\n",a2);

의 출력은 이렇게 나올 것이다.
    address 1: 132.241.5.10
    address 2: 132.241.5.10

만약에 이 값을 저장해야 할 필요가 있다면 strcpy()를 이용하여 고유의 char 배열에 저장해야 할 것이다.

이절에서 얘기할 것은 다 했다. 나중에 "whitehouse.gov" 문자열을 해당하는 IP주소로 바꾸는 법을 알려 줄것이다. (DNS절 참조)




4 시스템 콜

4.1 socket() ; 파일 기술자를 잡아라

안하면 맞을것 같아서 socket() 시스템 호출에 대해서 얘기해야만 할것같다. 이걸 잠깐 보자.
    #include <sys/types.h>
    #include <sys/socket.h>

    int socket(int domain, int type, int protocol);

그런데 이 변수들은 또 뭔가? 첫째 domain 은 struct sockaddr_in 에서처럼 AF_INET 로 지정하면 된다. 다음 type 은 SOCK_STREAM이나 SOCK_DGRAM으로 지정하면 된다. 끝으로 protocol은 0으로 지정하면 된다. (언급하지 않았지만 더 많은 domain과 더 많은 type 이 있다는 것을 기억하라. socket() 맨페이지를 참고하고 또한 protocol 에 대해서 좀더 알려면 getprotobyname()을 참조하면 된다.)

socket()은 바로 나중에 사용할 소켓 기술자인 정수값을 돌려주며 에러시에는 -1을 돌려주게 된다. 전역변수인 errno에 에러값이 기록된다. (perror()의 맨페이지를 참조할것.)




4.2 bind() ; 나는 어떤 포트에 연결되었나?

일단 소켓을 열게 되면 이 소켓을 현재 시스템의 포트에 연결시켜 주어야 한다. (이 작업은 보통 listen()함수를 이용해서 외부의 접속을 대기할 때 시행되며 일반적으로 머드게임 사이트들이 telnet *.*.*.* 6969 로 접속하라고 할때도 이 작업을 시행했다는 의미이다. ) 만약에 그저 다른 호스트에 연결하기만 할 예정이라면 그냥 connect()를 사용하여 연결만 하면 되고 이 작업은 필요가 없다.

아래는 bind() 시스템 호출의 선언이다.
    #include <sys/types.h>
    #include <sys/socket.h>

    int bind(int sockfd, struct sockaddr *my_addr, int addrlen);

sockfd는 socket()함수에서 얻은 소켓 기술자이며 my_addr은 IP 주소에 관한 정보(즉, IP 주소와 포트번호)를 담고 있는 struct sockaddr 에 대한 포인터 이고 addrlen은 그 구조체의 사이즈(sizeof(struct sockaddr))이다.

휴~~ 한방에 받아들이기에는 좀 그렇군. 예를 보자.
    #include <string.h>
    #include <sys/types.h>
    #include <sys/socket.h>

    #define MYPORT 3490

    main()
    {
        int sockfd;
        struct sockaddr_in my_addr;

        sockfd = socket(AF_INET, SOCK_STREAM, 0); /* do some error checking! */

        my_addr.sin_family = AF_INET;     /* host byte order */
        my_addr.sin_port = htons(MYPORT); /* short, network byte order */
        my_addr.sin_addr.s_addr = inet_addr("132.241.5.10");
        bzero(&(my_addr.sin_zero), 8);    /* zero the rest of the struct */

        /* don't forget your error checking for bind(): */
        bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));
        .
        .
        .

몇가지 주의할 점은 my_addr.sin_port 는 my_addr.sin_addr.s_addr과 같이 NBO이다. 또한 헤더화일은 각각의 시스템마다 다를 수 있으므로 각자의 시스템의 맨 페이지를 참고해야 할 것이다.

마지막으로 bind()와 관련해서 주소나 포트의 지정이 때에 따라서 자동화 될 수도 있다는 것을 언급해야 할 것같다.
        my_addr.sin_port = 0; /* choose an unused port at random */
        my_addr.sin_addr.s_addr = INADDR_ANY;  /* use my IP address */

my_addr.sin_port를 0으로 지정하면 자동으로 사용되지 않고 있는 포트 번호를 지정해 줄것이며 my_addr.sin_addr.s_addr를 INADDR_ANY로 지정할 경우 현재 작동되고 있는 자신의 IP주소를 자동으로 지정해 주게 된다.

만약 여기서 약간만 주의를 기울였다면 INADDR_ANY를 지정할 때 NBO로 바꾸는 것을 빼먹은 것을 눈치챌 것이다. 나아쁜~~. 그러나 난 내부정보를 알고 있지롱. 사실은 INADDR_ANY는 0이다. 0은 순서를 바꾸어도 0인것이다. 그러나 순수이론적인 측면에서 INADDR_ANY가 그러니까 12정도인 세계가 존재한다면 이 코드는 작동 안할것이다. 그래서? 난 상관없다. 정 그렇다면,
        my_addr.sin_port = htons(0); /* choose an unused port at random */
        my_addr.sin_addr.s_addr = htonl(INADDR_ANY);  /* use my IP address */

이제는 믿기 어려울 정도로 이식가능한 코드가 되었다. 다만 지적하고 싶은 것은 작동하는 데에는 아무 문제가 없다는 점이다.

bind()또한 에러가 났을때 -1을 돌려주며 errno에 에러의 코드가 남게 된다.

bind()를 호출할 때 주의할점 : 절대 제한선 아래로 포트번호를 내리지 말라는 것이다. 1024 아래의 번호는 모두 예약되어 있다. 그 위로는 65535까지 원하는 대로 쓸 수가 있다. (다른 프로그램이 쓰고 있지 않은 경우에 한해서..)

또 하나의 작은 꼬리말 : bind() 를 호출하지 않아도 되는 경우가 있다. 만일 다른 호스트에 연결 (connect())하고자 하는 경우에는 자신의 포트에는 (텔넷의 경우처럼)전혀 신경 쓸 필요가 없다. 단지 connect()를 호출하기만 하면 알아서 bind가 되어 있는지를 체크해서 비어있는 포트에 bind를 해준다.




4.3 connect() ; 어이~ 거기~

이제 잠깐만 마치 자신이 텔넷 프로그램인 것처럼 생각해 보기로 하자. 당신의 사용자는 명령하기를 (TRON영화에서처럼.. (역자: 난 그 영화 안 봤는데..)) 소켓 기술자를 얻어오라 했고 당신은 즉시 socket()를 호출했다. 다음에 사용자는 132.241.5.10 에 포트 23(정규 텔넷 포트번호)에 연결하라고 한다. 윽, 이젠 어떻게 하지?

다행스럽게도 당신(프로그램)은 connect()절(어떻게 연결하는가)를 심각하게 읽고 있으며 당신의 주인을 실망시키지 않으려고 미친듯이 읽어나가는 중이로다~~

connet()는 다음과 같이 선언한다.
    #include <sys/types.h>
    #include <sys/socket.h>

    int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);

sockfd는 이제는 친숙해진 소켓 기술자이며 serv_addr은 연결하고자 하는 목적지인 서버의 주소와 포트에 관한 정보를 담고 있는 struct sockaddr 이며 addrlen은 앞에서 이야기 한것과 같이 그 구조체의 크기이다.

뭔가 좀 이해가 갈 듯 하지 않은가? 예를 들어 보자.
    #include <string.h>
    #include <sys/types.h>
    #include <sys/socket.h>

    #define DEST_IP   "132.241.5.10"
    #define DEST_PORT 23

    main()
    {
        int sockfd;
        struct sockaddr_in dest_addr;   /* will hold the destination addr */

        sockfd = socket(AF_INET, SOCK_STREAM, 0); /* do some error checking! */

        dest_addr.sin_family = AF_INET;        /* host byte order */
        dest_addr.sin_port = htons(DEST_PORT); /* short, network byte order */
        dest_addr.sin_addr.s_addr = inet_addr(DEST_IP);
        bzero(&(dest_addr.sin_zero), 8);       /* zero the rest of the struct */

        /* don't forget to error check the connect()! */
        connect(sockfd, (struct sockaddr *)&dest_addr, sizeof(struct sockaddr));
        .
        .
        .

다시 말하건데 connect()의 결과값을 한번 체크해 봐야 한다. 에러일 경우 -1을 돌려주고 errno를 세팅하기 때문이다.

또한 bind()를 호출하지 않은 것에 주의해야 한다. 기본적으로 여기서는 자신의 포트 번호에는 결코 관심이 없기 때문이다. 단지 어디로 가는가만이 중요하다. 커널이 알아서 로컬 포트를 선정해 줄 것이며 우리가 연결하고자 하는 곳에서는 자동으로 이 정보를 알게 될 것이다.




4.4 listen() ; 누가 전화 좀 걸어주지~

이제 보조를 바꾸어서, 만약에 어디론가 연결하고자 하는 것이 아니라 외부로부터의 접속을 대기해서 접속이 올 경우 어떤 방식으로든지 간에 처리를 해 주어야 하는 경우라면 어찌 할 것인가. 이 작업은 두 단계로 이루어진다. 먼저 listen()을 해야 되고 그 다음에 accept()를 해야 된다는 것이다.

listen()은 상당히 간단하지만 약간의 설명은 필요하다.
    int listen(int sockfd, int backlog);

sockfd는 보통의 소켓 기술자이며 backlog는 접속대기 큐의 최대 연결 가능 숫자이다. 그건 또 뭔 얘기인가? 외부로부터의 연결은 이 대기 큐에서 accept()가 호출될 때까지 기다려야 한다는 것이며 숫자는 바로 얼마나 많은 접속이 이 큐에 쌓여질 수 있는가 하는 것이다. 대부분의 시스템은 이 숫자를 조용하게 20정도에서 제한하고 있으며 보통은 5에서 10 사이로 지정하게 된다.

또 다시 listen()도 에러의 경우 -1을 돌려주며 errno를 세팅한다.

아마 상상할 수 있듯이 listen()보다 앞서서 bind()를 호출해야 하며 만약에 bind()가 되지 않으면 우리는 랜덤하게 지정된 포트에서 외부의 접속을 기다려야 한다. (포트를 모르고서 누가 접속할 수 있겠는가? 우엑~~) 따라서 외부의 접속을 기다리는 경우라면 다음 순서대로 작업이 진행되어야 하는 것이다.
    socket();
    bind();
    listen();
    /* accept() goes here */

위의 것만으로도 이해가 갈만하다고 보고 예제에 대신하겠다. (accept()절에 보다 괜찮은 코드가 준비되어 있다.) 이 모든 sha-bang(역자: 이 뭐꼬?)중에서 가장 헷갈리는 부분은 accept()를 부르는 부분이다.




4.5 accept() ; 포트 3490에 전화걸어주셔서 감사합니다.

준비! accept()를 호출하는 것은 뭔가 좀 수상하긴 하다. 과연 뭐가 벌어지는가? 저 멀리 떨어진 곳에서 누군가가 connect()를 호출하여 당신이 listen()을 호출하고 기다리는 포트에 접속을 시도한다. 그들의 연결은 바로 accept()가 호출되기 까지 큐에서 바로 당신이 accept()를 호출하여 그 연결을 지속하라고 명령할 때까지 대기하게 된다. 그러면 이 함수는 오로지 이 연결을 위한 완전히 신제품 소켓 파일 기술자를 돌려주게 된다. 갑자기 당신은 하나값으로 두개의 소켓 기술자를 갖게 되는 것이다. 원래의 것은 아직도 그 포트에서 연결을 listen()하고 있다. 또 하나는 새롭게 창조되어 드디어 send()와 recv()를 할 준비가 되도록 하는 것이다.

드디어 여기까지 왔다! 감격~~

선언은 아래와 같다.
     #include <sys/socket.h>

     int accept(int sockfd, void *addr, int *addrlen);

sockfd는 listen()하고 있는 소켓의 기술자이다. 뻔하지 뭐.. addr은 로컬 struct sockaddr_in의 포인터이다. 여기에 들어온 접속에 관한 정보가 담겨지게 되고 이를 이용해서 어느 호스트에서 어느 포트를 이용해서 접속이 들어왔는지를 알 수 있게 된다. addrlen은 로컬 정수 변수이며 이 정수에는 struct sockaddr_in의 크기가 미리 지정되어 있어야 한다. 이 숫자보다 더 많은 바이트의 정보가 들어오면 accept()는 받아 들이지 않을 것이며 적데 들어온다면 addrlen의 값을 줄여 줄 것이다.

accept() 는 에러가 났을 경우에 어떻게 한다고? -1을 돌려주고 errno 를 세팅한다.

아까 맨치로 한방에 받아들이기에는 좀 그러니까 예제를 열심히 읽어 보자.
    #include <string.h>
    #include <sys/types.h>
    #include <sys/socket.h>

    #define MYPORT 3490    /* the port users will be connecting to */

    #define BACKLOG 10     /* how many pending connections queue will hold */

    main()
    {
        int sockfd, new_fd;  /* listen on sock_fd, new connection on new_fd */
        struct sockaddr_in my_addr;    /* my address information */
        struct sockaddr_in their_addr; /* connector's address information */
        int sin_size;

        sockfd = socket(AF_INET, SOCK_STREAM, 0); /* do some error checking! */

        my_addr.sin_family = AF_INET;         /* host byte order */
        my_addr.sin_port = htons(MYPORT);     /* short, network byte order */
        my_addr.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */
        bzero(&(my_addr.sin_zero), 8);        /* zero the rest of the struct */

        /* don't forget your error checking for these calls: */
        bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));

        listen(sockfd, BACKLOG);

        sin_size = sizeof(struct sockaddr_in);
        new_fd = accept(sockfd, &their_addr, &sin_size);
        .
        .
        .

이제 new_fd를 이용해서 send()와 recv()를 이용할 수 있다는 것이다. 만약 원한다면 더이상의 연결을 받아들이지 않고 하나의 연결만 이용하기 위해서 close()를 이용하여 원래의 sockfd를 막아 버릴 수도 있다.




4.6 send(), recv() ; 말 좀 해봐~

이 두 함수는 스트림 소켓이나 연결된 데이터그램 소켓위에서 정보를 주고 받을때 사용하는 것들이다. 만약 보통의 비연결 데이터그램 소켓을 사용한다면 sendto()와 recvfrom()절을 참조하도록 한다.

send() 호출의 선언은 아래와 같다.
    int send(int sockfd, const void *msg, int len, int flags);

sockfd는 socket()를 통해서 얻었거나 accept()를 통해서 새로 구한, 데이터를 보낼 소켓의 기술자이며, msg는 보낼 데이터를 가리키는 포인터, len은 보낼 데이터의 바이트 수 이며 flags 는 그냥 0으로 해야 한다. (플래그에 관한 보다 자세한 내용은 send()의 맨 페이지를 참조할것.)

약간의 예제가 다음과 같다.
    char *msg = "Beej was here!";
    int len, bytes_sent;
    .
    .
    len = strlen(msg);
    bytes_sent = send(sockfd, msg, len, 0);
    .
    .
    .

send()는 결과값으로 보내진 모든 바이트 수를 돌려주는데 이것은 보내라고 한 숫자보다 작을 수도 있다. 가끔은 보내고자 하는 데이터의 크기가 미처 감당하지 못할 만한 숫자인 경우도 있으며 이 경우 send()는 자기가 감당할 수 있는 숫자만큼만 보내고 나머지는 잘라 버린후 당신이 그 나머지를 다시 보내 줄 것으로 기대하는 것이다. 만약에 보내라고 한 데이터의 크기보다 작은 숫자가 결과값으로 돌아 왔다면 그 나머지 데이터를 보내는 것은 전적으로 당신의 책임인 것이다. 그나마 희소식은 데이터의 사이즈가 작다면 (1k 이내라면) 아마도 한번에 모두 보낼 수 있을 것이다. 또한 에러의 경우 -1을 돌려주며 errno를 세팅한다.

recv()의 경우도 상당히 유사하다.
    int recv(int sockfd, void *buf, int len, unsigned int flags);

sockfd는 읽어올 소켓의 기술자이며 buf는 정보를 담을 버퍼이다. len은 버퍼의 최대 크기이고 flags는 0으로 세팅해야 한다. (자세한 flags의 정보는 recv() 맨 페이지를 참조할것.)

recv()는 실제 읽어들인 바이트 숫자를 돌려주며 에러의 경우는 -1, errno를 세팅한다.

쉬웠을까? 쉬웠지.. 이제 당신은 스트림 소켓을 이용해서 데이터를 보내고 받을 수 있게 되었다. 우와~ 유닉스 네트워크 프로그래머네~~




4.7 sendto(), recvfrom() ; 말 좀 해봐~ 데이터그램 방식

괜찮은걸, 이라고 말하고 있는줄로 생각하겠다. 그런데 데이터그램에 관한 나머지는 어딨지? 노프라블레모~ 아미고~(역자: 터미네이터2가 생각나는군~~) 이제 할 것이다.

데이터그램 소켓은 연결을 할 필요가 없다면 데이터를 보내기 전에 주어야 할 나머지 정보는 어떻게 주어야 하는가? 맞다. 목적지의 주소를 알려주어야 한다. 여기에 예제가 있다.
    int sendto(int sockfd, const void *msg, int len, unsigned int flags,
               const struct sockaddr *to, int tolen);

보다시피 이 함수는 두가지 부가정보가 더 들어간 것 이외에는 기본적으로 send()와 동일하다. to 는 struct sockaddr의 포인터이며(아마도 struct sockaddr_in) 여기에는 목적지의 주소와 포트번호가 담겨 있어야 할 것이다. tolen은 그 구조체의 크기인 것이다.

send()와 마찬가지로 sendto()도 보내어진 데이터의 바이트수를 결과로 돌려주며(실제 보내라고 준 데이터의 크기보다 작을지도 모르는), 에러의 경우 -1을 돌려준다.

비슷하게 recvfrom()도 아래와 같다.
    int recvfrom(int sockfd, void *buf, int len, unsigned int flags
                 struct sockaddr *from, int *fromlen);

역시 이것도 두가지 변수가 더 주어지게 된다. from은 데이터를 보내는 장비의 주소와 포트를 담고 있는 struct sockaddr 이며 fromlen은 로컬 정수변수로서 구조체의 크기가 세팅되어 있어야 한다. 함수가 호출된 뒤에는 fromlen에는 실제 from의 크기가 수록되게 된다.

recvfrom()은 실제 받은 데이터의 바이트수를 돌려주며 에러의 경우는 -1, errno를 세팅하게 된다.

만약 connect()를 이용하여 데이터그램 소켓을 연결한 후의 상황이라면 간단히 send(), recv() 를 사용해도 상관 없으며 소켓 인터페이스는 자동으로 목적지와 소스에 관한 정보를 함수에 추가해서 작동되게 될 것이다.




4.8 close(), shutdown() ; 꺼지쇼.

휴~~ 하루종일 데이터를 보내고 받았더니..이제는 소켓을 닫을 때가 된 것이다. 이건 쉽다. 정규 파일 기술자에 관한 close()를 사용하면 되는 것이다.
    close(sockfd);

이것으로 더이상의 입출력은 불가능 해지며 누구든지 원격지에서 이 소켓에 읽고 쓰려고 하는 자는 에러를 받게 될 것이다.

약간 더 세밀한 제어를 위해서는 shutdown()을 사용하면 된다. 이것을 이용하면 특정방향으로의 통신만을 끊을 수도 있게 된다.
    int shutdown(int sockfd, int how);

sockfd는 소켓 기술자이며 how는 다음과 같다.

0 - 더이상의 수신 금지 1 - 더이상의 송신 금지 2 - 더이상의 송수신 금지(close()와 같은 경우)

shutdown() 은 에러의 경우 -1을 돌려주며 errno를 세팅한다.

황송하옵게도 연결도 되지않은 데이터그램 소켓에 shutdown()을 사용한다면 단지 send(), recv()를 사용하지 못하게만 만들 것이다. connect()를 사용한 경우에만 이렇게 사용할 수 있다는 것을 기억해야 한다. (역자: 그렇다면 sendto, recvfrom은 사용이 된다는 얘기인가??테스트가 필요할듯.)

암것도 아니군.




4.9 getpeername() ; 누구십니까?

이 함수는 되게 쉽다.

너무 쉬워서 절을 따로 만들 필요가 없지않나 고민했지만 여기 있는 걸 보니까..

getpeername()은 상대편 쪽 스트림 소켓에 누가 연결되어 있는가를 알려준다.
    #include <sys/socket.h>

    int getpeername(int sockfd, struct sockaddr *addr, int *addrlen);

sockfd는 연결된 스트림 소켓의 기술자이며 addr은 상대편의 정보를 담게 될 struct sockaddr(또는 struct sockaddr_in)의 포인터 이며 addrlen은 정수를 가리키는 포인터로서 구조체의 크기가 지정되어 있어야 한다.

에러의 경우는 -1을 돌려주고 errno를 세팅한다. (외우겠군.)

일단 주소를 알게되면 inet_ntoa()나 gethostbyaddr()을 이용하여 좀더 많은 정보를 알아낼 수 있게 되지만 상대편의 login name을 알게되는 것은 아니다. (만일 상대편에 ident 데몬이 돌고 있다면 알아낼 방법이 없는 것은 아니지만 이 내용은 이 글의 취지를 벗어나는 내용이므로 RFC-1413을 참조하라고 말하고 싶다.)




4.10 gethostname() ; 난 누구인가?

getpeername()보다 더 쉬운 것이 이 함수이다. 결과로 프로그램이 돌고 있는 컴퓨터의 이름을 알려준다. 이름은 gethostbyname()을 이용하여 로컬 장비의 IP주소를 알아내는데 사용될 수도 있다.

뭐가 더 재미있는가? 몇가지 생각해 볼 수 있는데 이 문서에는 적절하지 않은 내용이다(역자: 과연 뭘까..되게 궁금하네..). 어쨌거나,
    #include <unistd.h>

    int gethostname(char *hostname, size_t size);

hostname은 문자열의 포인터이며 함수가 돌려주는 값을 담게 될 변수이다. size는 그 문자열의 크기이다.

성공적이면 0을, 에러의 경우 -1을 리턴하고 errno를 세팅한다.




4.11 DNS ; whitehouse.gov - 198.137.240.100

모르는 사람을 위하여 DNS는 Domain Name Service 라는 것을 먼저 얘기 하겠다. 간결하게 얘기한다면 DNS에다가 사람이 읽을 수 있는 주소를 말해주면 DNS는 bind,connect,sendto,어쨌거나 IP주소가 필요한 것들에서 사용할 수 있는 IP주소를 돌려준다. 즉 누군가가 이렇게 입력했다면
    $ telnet whitehouse.gov

telnet 은 connect()에 사용하기 위해서 198.137.240.100이라는 IP주소를 찾아내게 된다. 그런데 어떻게 그렇게 하는 것인가? gethostbyname()을 사용하면 된다.
    #include <netdb.h>

    struct hostent *gethostbyname(const char *name);

보다시피 결과로 struct hostent의 포인터가 돌아온다. 그 구조는 아래와 같다.
    struct hostent {
        char    *h_name;
        char    **h_aliases;
        int     h_addrtype;
        int     h_length;
        char    **h_addr_list;
    };
    #define h_addr h_addr_list[0]

각 필드에 대한 설명은 다음과 같다.

  • h_name - 호스트의 공식적인 이름
  • h_aliases - 호스트의 별명으로서 NULL 로 끝맺음된다.
  • h_addrtype - 주소의 종류, 보통 AF_INET
  • h_length - 주소의 바이트 수
  • h_addr_list - 0으로 끝나는 네트워크 주소들, NBO로 되어 있다.
  • h_addr - h_addr_list속의 첫번째 주소

gethostbyname()은 위의 구조체의 포인터를 돌려주게 되며 에러의 경우 NULL을 돌려준다. errno는 세팅되지 않고 h_errno가 세팅이 된다. (아래의 herror()참조)

그런데 이걸 어떻게 사용하는가? 보통 컴퓨터 매뉴얼들 처럼 독자 앞에 정보를 마구 쌓아놓은 것만으로는 부족한 법이다. 이 함수는 사실 보기보다는 쓰기가 쉬운 편이다.

예제를 보자.
    #include <stdio.h>
    #include <stdlib.h>
    #include <errno.h>
    #include <netdb.h>
    #include <sys/types.h>
    #include <netinet/in.h>

    int main(int argc, char *argv[])
    {
        struct hostent *h;

        if (argc != 2) {  /* error check the command line */
            fprintf(stderr,"usage: getip address\n");
            exit(1);
        }

        if ((h=gethostbyname(argv[1])) == NULL) {  /* get the host info */
            herror("gethostbyname");
            exit(1);
        }

        printf("Host name  : %s\n", h->h_name);
        printf("IP Address : %s\n",inet_ntoa(*((struct in_addr *)h->h_addr)));

        return 0;
    }

gethostbyname()에서는 errno가 세팅되지 않는 까닭으로 perror()를 사용할 수 없고 herror()을 사용해야 한다.

간단히 호스트의 이름을 담고 있는 스트링을 gethostbyname() 함수에 넣어 줌으로써 바로 struct hostent 를 얻게 되는 것이다.

남아있는 한가지 수상한 점은 위의 방법으로 어떻게 주소를 숫자와 점으로 출력할 것인가 하는 문제이다. h->h_addr 은 문자 포인터( char *) 인데 inet_ntoa()는 변수로서 struct in_addr 을 원하기 때문이다. 따라서 h->h_addr 을 struct in_addr * 으로 형변환을 하고 결과값을 얻기 위해 다시 역참조 하면 된다는 것이다.




5 클라이언트-서버의 배경

요즘은 클라이언트-서버가 판치는 세상이죠~~ 네트워크에 관한 모든 것은 서버 프로세스를 요청하는 클라이언트 프로세스로서 다루어진다. 텔넷을 이용하여 23번 포트에 접속하는 (클라이언트)것은 서버프로그램(telnetd)을 작동시키게 되는 것이며 이 서버 프로그램은 들어오는 각종 신호를 받아들여서 당신의 텔넷 접속을 위하여 로그인 프롬프트를 주게 되는 것이다. 등등..
그림2. 클라이언트-서버간의 관계

클라이언트와 서버간의 정보 교환의 모델이 그림에 잘 나와있다.

주목할 점은 클라이언트와 서버간에는 SOCK_STREAM이든, SOCK_DGRAM이든지간에 같은 것으로만 된다면 의사소통이 된다는 것이다. 좋은 예들은 telnet-telnetd, ftp-ftpd, 또는 bootp-bootpd 등이다. ftp를 쓴다면 반드시 상대편에 ftpd가 돌고 있다는 것이다.

보통 호스트에는 하나의 서버 프로그램이 돌고 있게 된다. 그리고 그 서버는 fork()를 이용하여 다중의 클라이언트를 받게 되는 것이다. 기본적인 루틴의 구조는 다음과 같다. 서버는 접속을 대기하다가 accept()를 호출하게 되며 그 때 fork()를 이용하여 자식 프로세스를 만들어내어 그 접속을 처리하게 된다. 이것이 바로 다음에 소개될 예제 서버 프로그램의 구조이다.




5.1 간단한 스트림 서버

이 서버가 하는 일은 오직 스트림 접속을 하게 되는 모든 클라이언트에게 "Hello, World!\n"을 출력해 주는 것이다. 이 서버를 테스트하기 위해서는 하나의 윈도우에서 이 서버를 실행시켜 놓고 다른 윈도우에서 텔넷 접속을 시도해 보는 것이다.
    $ telnet remotehostname 3490

hostname 은 서버 프로그램이 작동된 호스트의 이름이다.

서버 프로그램 코드
    #include <stdio.h>
    #include <stdlib.h>
    #include <errno.h>
    #include <string.h>
    #include <sys/types.h>
    #include <netinet/in.h>
    #include <sys/socket.h>
    #include <sys/wait.h>

    #define MYPORT 3490    /* the port users will be connecting to */

    #define BACKLOG 10     /* how many pending connections queue will hold */

    main()
    {
        int sockfd, new_fd;  /* listen on sock_fd, new connection on new_fd */
        struct sockaddr_in my_addr;    /* my address information */
        struct sockaddr_in their_addr; /* connector's address information */
        int sin_size;

        if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
            perror("socket");
            exit(1);
        }

        my_addr.sin_family = AF_INET;         /* host byte order */
        my_addr.sin_port = htons(MYPORT);     /* short, network byte order */
        my_addr.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */
        bzero(&(my_addr.sin_zero), 8);        /* zero the rest of the struct */

        if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) \
                                                                      == -1) {
            perror("bind");
            exit(1);
        }

        if (listen(sockfd, BACKLOG) == -1) {
            perror("listen");
            exit(1);
        }

        while(1) {  /* main accept() loop */
            sin_size = sizeof(struct sockaddr_in);
            if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr, \
                                                          &sin_size)) == -1) {
                perror("accept");
                continue;
            }
            printf("server: got connection from %s\n", \
                                               inet_ntoa(their_addr.sin_addr));
            if (!fork()) { /* this is the child process */
                if (send(new_fd, "Hello, world!\n", 14, 0) == -1)
                    perror("send");
                close(new_fd);
                exit(0);
            }
            close(new_fd);  /* parent doesn't need this */

            while(waitpid(-1,NULL,WNOHANG) > 0); /* clean up child processes */
        }
    }

이 코드는 문법상의 단순함을 위하여 하나의 커다란(내 생각에) main()에 모든 것이 들어가 있다. 만약에 이것을 잘게 잘라서 작은 여러개의 함수로 구성을 하는것이 좋다고 생각된다면 그래도 된다.

다음의 클라이언트 코드를 이용한다면 이 서버로부터 문자열을 받아 낼수도 있다.




5.2 간단한 스트림 클라이언트

이녀석은 서버보다 더 쉬운 코드이다. 이 프로그램이 하는 일은 명령행에서 지정된 주소에 3490번 포트에 접속하여 서버가 보내는 문자열을 받는 것 뿐이다.

    /*
    ** client.c -- a stream socket client demo
    */

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <errno.h>
    #include <string.h>
    #include <netdb.h>
    #include <sys/types.h>
    #include <netinet/in.h>
    #include <sys/socket.h>

    #define PORT 3490 // the port client will be connecting to 

    #define MAXDATASIZE 100 // max number of bytes we can get at once 

    int main(int argc, char *argv[])
    {
        int sockfd, numbytes;
        char buf[MAXDATASIZE];
        struct hostent *he;
        struct sockaddr_in their_addr; // connector's address information 

        if (argc != 2) {
            fprintf(stderr,"usage: client hostname\n");
            exit(1);
        }

        if ((he=gethostbyname(argv[1])) == NULL) {  // get the host info 
            perror("gethostbyname");
            exit(1);
        }

        if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
            perror("socket");
            exit(1);
        }

        their_addr.sin_family = AF_INET;    // host byte order 
        their_addr.sin_port = htons(PORT);  // short, network byte order 
        their_addr.sin_addr = *((struct in_addr *)he->h_addr);
        memset(&(their_addr.sin_zero), '\0', 8);  // zero the rest of the struct 

        if (connect(sockfd, (struct sockaddr *)&their_addr,
                                              sizeof(struct sockaddr)) == -1) {
            perror("connect");
            exit(1);
        }

        if ((numbytes=recv(sockfd, buf, MAXDATASIZE-1, 0)) == -1) {
            perror("recv");
            exit(1);
        }

        buf[numbytes] = '\0';

        printf("Received: %s",buf);

        close(sockfd);

        return 0;
    }


이 클라이언트를 작동하기에 앞서서 서버를 작동시켜놓지 않았다면 connect()함수는 "Connection refused"를 돌려주게 될것이다. 쓸만하군!




5.3 데이터그램 소켓

이에 관해서는 그다지 얘기할 것이 많지 않다. 따라서 그냥 두개의 프로그램을 보여 주겠다.

listener는 호스트에 앉아서 4950포트에 들어오는 데이터 패킷을 기다린다. talker는 지정된 호스트의 그 포트로 뭐든지 간에 사용자가 입력한 데이터를 보낸다.

listener.c
    #include <stdio.h>
    #include <stdlib.h>
    #include <errno.h>
    #include <string.h>
    #include <sys/types.h>
    #include <netinet/in.h>
    #include <sys/socket.h>
    #include <sys/wait.h>

    #define MYPORT 4950    /* the port users will be connecting to */

    #define MAXBUFLEN 100

    main()
    {
        int sockfd;
        struct sockaddr_in my_addr;    /* my address information */
        struct sockaddr_in their_addr; /* connector's address information */
        int addr_len, numbytes;
        char buf[MAXBUFLEN];

        if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
            perror("socket");
            exit(1);
        }

        my_addr.sin_family = AF_INET;         /* host byte order */
        my_addr.sin_port = htons(MYPORT);     /* short, network byte order */
        my_addr.sin_addr.s_addr = INADDR_ANY; /* auto-fill with my IP */
        bzero(&(my_addr.sin_zero), 8);        /* zero the rest of the struct */

        if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) \
                                                                       == -1) {
            perror("bind");
            exit(1);
        }

        addr_len = sizeof(struct sockaddr);
        if ((numbytes=recvfrom(sockfd, buf, MAXBUFLEN, 0, \
                           (struct sockaddr *)&their_addr, &addr_len)) == -1) {
            perror("recvfrom");
            exit(1);
        }

        printf("got packet from %s\n",inet_ntoa(their_addr.sin_addr));
        printf("packet is %d bytes long\n",numbytes);
        buf[numbytes] = '\0';
        printf("packet contains \"%s\"\n",buf);

        close(sockfd);
    }

결국 socket()를 호출할 때 SOCK_DGRAM을 사용하게 된 것을 주의하고, listen()이나 accept()를 사용하지 않은것도 주의해 봐야 한다. 이 코드가 바로 비연결 데이터그램 소켓의 자랑스러운 사용예인 것이다.

talker.c
    #include <stdio.h>
    #include <stdlib.h>
    #include <errno.h>
    #include <string.h>
    #include <sys/types.h>
    #include <netinet/in.h>
    #include <netdb.h>
    #include <sys/socket.h>
    #include <sys/wait.h>

    #define MYPORT 4950    /* the port users will be connecting to */

    int main(int argc, char *argv[])
    {
        int sockfd;
        struct sockaddr_in their_addr; /* connector's address information */
        struct hostent *he;
        int numbytes;

        if (argc != 3) {
            fprintf(stderr,"usage: talker hostname message\n");
            exit(1);
        }

        if ((he=gethostbyname(argv[1])) == NULL) {  /* get the host info */
            herror("gethostbyname");
            exit(1);
        }

        if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
            perror("socket");
            exit(1);
        }

        their_addr.sin_family = AF_INET;      /* host byte order */
        their_addr.sin_port = htons(MYPORT);  /* short, network byte order */
        their_addr.sin_addr = *((struct in_addr *)he->h_addr);
        bzero(&(their_addr.sin_zero), 8);     /* zero the rest of the struct */

        if ((numbytes=sendto(sockfd, argv[2], strlen(argv[2]), 0, \
             (struct sockaddr *)&their_addr, sizeof(struct sockaddr))) == -1) {
            perror("sendto");
            exit(1);
        }

        printf("sent %d bytes to %s\n",numbytes,inet_ntoa(their_addr.sin_addr));

        close(sockfd);

        return 0;
    }

이것이 전부다. listener를 한 호스트에서 실행 시키고 다른 곳에서 talker를 실행시킨다. 핵가족시대에 어울리는 가족용 오락이 될 수도...

앞에서도 얘기했었지만 한가지 작은 내용을 더 말해야 할것 같다. 만약 talker에서 connect()를 호출해서 연결을 했다면 그 다음부터는 sendto(), recvfrom()이 아니라 그냥 send().recv()를 사용해도 된다는 것이다. 전달되어야 하는 호스트의 주소는 connect()에 지정된 주소가 사용되게 된다.




6 약간 수준 높은 기법들

6.1 블로킹

블로킹. 아마 들어봤겠지. 그런데 도대체 그게 뭘까? 사실 "잠들다"의 기술용어에 불과한 것이다. 아마도 listener를 실행시키면서 눈치를 챘겠지만 그 프로그램은 그저 앉아서 데이터 패킷이 올때까지 기다리는 것이다. 잠자면서.. recvfrom()을 호출했는데 데이터가 들어온 것이 없다면? 바로 뭔가 데이터가 들어올 때까지 블로킹이 되는 것이다(그냥 거기서 자고 있는 것이다.).

많은 함수들이 블로킹이 된다. accept()는 블록이 된다. recv*()종류들이 모두 블록이 된다. 그들이 이렇게 할 수 있는 이유는 그렇게 할 수 있도록 허락을 받았기 때문이다. 처음에 socket()으로 소켓이 만들어질때 커널이 블록 가능하도록 세팅을 했기 때문이다. 만일 블록할 수 없도록 세팅하려면 fcntl()을 사용한다.
    #include <unistd.h>
    #include <fcntl.h>
    .
    .
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    fcntl(sockfd, F_SETFL, O_NONBLOCK);
    .
    .

소켓을 블록할 수 없도록 세팅함으로써 정보를 추출하는 데에 효과적으로 socket을 이용할 수 있다. 만일 데이터가 접수되지 않은 소켓에서 데이터를 읽으려고 시도한다면 -1을 결과로 돌려주고 errno를 EWOULDBLOCK 으로 세팅하게 된다.

일반적으로는 이런 식으로 정보를 뽑아 내는 것은 별로 좋은 방식은 아니다. 만일 들어오는 데이터를 감시하기 위하여 이런 방식으로 바쁘게 데이터를 찾는 루틴을 만든다면 이는 CPU 시간을 소모하게 되는 것이다. 구식이다. 보다 멋진 방법은 다음절에 나오는 select()를 사용하여 데이터를 기다리는 식이다.




6.2 select() ; 동기화된 중복 입출력. 대단하군!

이건 뭔가 좀 이상한 함수이다. 그러나 상당히 유용하므로 잘 읽어보기 바란다. 다음 상황을 가정해 보자. 지금 서버를 돌리고 있으며 이미 연결된 소켓에서 데이터가 들어오는 것을 기다리고 있다고 하자.

문제없지, 그냥 accept()하고 recv()몇개면 될텐데.. 서둘지 말지어다, 친구. 만일 accept()에서 블로킹이 된다면? 동시에 어떻게 recv()를 쓸 것인가? 블로킹 못하게 세팅한다고? CPU시간을 낭비하지 말라니까. 그러면 어떻게?

더이상 떠들지 말고 다음을 보여주겠다.
       #include <sys/time.h>
       #include <sys/types.h>
       #include <unistd.h>

       int select(int numfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);

이 함수는 화일 기술자의 "집합", 특별히 readfds,writefds,exceptfds 등을 관리한다. 만일 일반적인 입력이나 소켓 기술자로부터 읽어 들일수 있는가를 확인하려면 단지 화일 기술자 0과 sockfd를 readfds에 더해주기만 하면 된다. numfds는 가장 높은 파일 기술자에다가 1을 더해서 지정해야 하며 이번 예제에서는 정규 입력의 0보다 확실히 크게 하기 위해서 sockfd+1 을 지정해야 한다.

select()의 결과값이 나올때 readfs는 선택한 파일 기술자 중에 어떤 것이 읽기 가능한가를 반영할 수 있도록 수정되며 FD_ISSET() 매크로를 이용하여 체크할 수 있다.

너무 멀리 나가기 전에 이 "집합"들을 어떻게 관리하는 가에 대해서 얘기를 해야 할것 같다. 각각의 "집합"은 fd_set형이며 다음의 매크로들로 이를 제어할 수 있다.

FD_ZERO(fd_set *set) - 파일기술자 집합을 소거한다. FD_SET(int fd, fd_set *set) - fd 를 set에 더해준다. FD_CLR(int fd, fd_set *set) - fd 를 set에서 빼준다. FD_ISSET(int fd, fd_set *set) - fd가 set안에 있는지 확인한다.

끝으로 이 수상한 struct timeval은 또 무엇인가? 아마도 누군가가 어떤 데이터를 보내는 것을 무한정 기다리기를 원치는 않을 것이다. 특정 시간마다 아무 일도 안 벌어지더라도 "현재 진행 중..."이라는 메시지를 터미널에 출력시키기라도 원할 것이다. 이 구조체는 그 시간 간격을 정의하기 위해서 사용되는 것이다. 이 시간이 초과되고 그 때까지 select()가 아무런 변화를 감지하지 못한 경우라면 결과를 돌려주고 다음 작업을 진행 할수 있도록 해준다.

struct timeval의 구조는 다음과 같다.

    struct timeval {
        int tv_sec;     /* seconds */
        int tv_usec;    /* microseconds */
    };

기다릴 시간의 초를 지정하려면 그냥 tv_sec에 지정하면 된다. tv_usec에는 마이크로 초를 지정한다. 밀리초가 아니고 마이크로초이다. 마이크로초는 백만분의 일초이다. 그런데 왜 usec인가? u는 그리스 문자의 Mu를 닮았고 이는 마이크로를 의미하는데 사용된다. 함수가 끝날때 timeout에 남은 시간이 기록될수도 있으며 이 내용은 유닉스마다 다르기는 하다.

와우~ 마이크로 초 단위의 타이머를 가지게 되었군! 만일 timeval에 필드들을 0으로 채우면 select()는 즉시 결과를 돌려주며 현재 set들의 내용을 즉시 알려주게 된다. timeout을 NULL로 세팅하면 결코 끝나지 않고 계속 파일 기술자가 준비되는 것을 기다리게 되며 끝으로 특정한 set에 변화에 관심이 없다면 그 항목을 NULL로 지정하면 된다.

다음은 정규 입력에 무언가 나타날 때까지 2.5초를 기다리는 코드이다.

       #include <sys/time.h>
       #include <sys/types.h>
       #include <unistd.h>

       #define STDIN 0  /* file descriptor for standard input */

       main()
       {
           struct timeval tv;
           fd_set readfds;

           tv.tv_sec = 2;
           tv.tv_usec = 500000;

           FD_ZERO(&readfds);
           FD_SET(STDIN, &readfds);

           /* don't care about writefds and exceptfds: */
           select(STDIN+1, &readfds, NULL, NULL, &tv);

           if (FD_ISSET(STDIN, &readfds))
               printf("A key was pressed!\n");
           else
               printf("Timed out.\n");
       }


만일 한줄씩 버퍼링하는 터미널이라면 엔터키를 치지 않는 이상은 그냥 타임아웃에 걸릴 것이다.

이제 아마도 이 훌륭한 방법을 데이터그램 소켓에서 데이터를 기다리는 데에 사용할 수 있으리라고 생각할 것이다. 맞다. 그럴 수도 있다. 어떤 유닉스에서는 이 방법이 되지만 안되는 것도 있다. 자세한 것은 맨페이지를 참조해야 할 것이다.

어떤 유닉스들은 중단하기 전에 아직 남아있는 시간의 양을 반영하기 위해 당신의 timeval 구조체 내의 시간을 갱신하기도 하고 또 어떤 것들은 아니다. 이식성을 원한다면 그것에 의지하면 안된다.(만약 경과한 시간을 추적할 필요가 있다면 gettimeofday()를 사용하라. 나도 그것이 게으름뱅이라는것을 안다. 하지만 그게 원래 그렇다.)

read 집합의 소켓이 연결을 끊는다면 어떤 일이 생길까? 음, 그 경우에는 select()가 소켓 기술자를 "읽을 준비가 됐음"상태로 설정하고 반환된다. 당신이 실제적으로 recv()를 하면 recv()는 0을 반환할 것이다. 그것이 당신이 클라이언트가 연결을 끊었는지를 아는 방법이다.

select()에 관한 흥미로운 점 한가지: 만약 당신이 listen()하고 있는 소켓을 가지고 있다면 그 소켓의 파일기술자를 redfds 집합에 집어넣음으로써 새로운 연결이 있는지를 알아볼 수 있다.

그리고 이것이 전능한 select()함수의 대략적인 개요이다.

하지만 대중의 요구에 의해서 여기 심도있는 예제가 있다. 불행하게도, 위의 더럽게 간단한 예제와 여기있는 것과의 차이는 상당하다. 하지만 일단 보고 설명을 보자.

이 프로그램은 간단한 다중 이용자 채팅 서버처럼 동작한다. 하나의 윈도우에서 그것을 시작하고 다른 여러개의 윈도우에서 telnet("telnet hostname 9034") 하라. 당신이 하나의 텔넷 세션에서 무언가를 입력하면 다른 것들에서도 그것이 나타날 것이다.

/*
** selectserver.c -- a cheezy multiperson chat server
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 9034 // port we're listening on

int main(void)
{
    fd_set master;   //master file descriptor list
    fd_set read_fds;  //temp file descriptor list for select()
    struct sockaddr_in myaddr;   //server address
    struct sockaddr_in remoteaddr;  //client addresss
    int fdmax;   //maximum file descriptor number
    int listener;   /listening socket descriptor
    int newfd; //newly accept()ed socket descriptor
    char buf[256];  //buffer for client data
    int nbytes;
    int yes=1;
    int nbytes;
    int yes=1;   //for setsockopt() SO_REUSEADDR, below
    int addrlen;
    int i,j;

    FD_ZERO(&master);    //clear the master and temp sets
    FD_ZERO(&readfds);

    //get the listener
    if((listener = socket(AF_INET, SOCK_STREAM, 0)) == -1){
        perror("socket");
        exit(1);
    }

    //lose the pesky "address already in use"error message
    if(setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1){
        perror("setsockopt");
        exit(1);
    }

    //bind

    myaddr.sin_family = AF_INET;
    myaddr.sin_addr.s_addr = IN_ADDR_ANY;
    myaddr.sin_port = htons(PORT);
    memset(&(myaddr.sin_zero), '\0',8);
    if(bind(listener,(struct sockaddr *)&myaddr, sizeof(myaddr)) == -1){
        perror("bind");
        exit(1);
    }

    //listen
    if(listen(listener, 10) == -1){
        perror("listen");
        exit(1);
    }

    //add the listener to the master set
    FD_SET(listener, &master);

    //keep track of the biggest file descriptor
    fdmax = listener; // so far, it's this one

    //main loop
    for(;;){
        read_fds = master; //copy it
        if(select(fdmax+1,&readfds,NULL,NULL,NULL) == -1){
            perror("select");
            exit(1);
        }

        //run through the existing connections looking for data to read
        for(i=0;i<=fdmax;i++){
            if(FD_ISSET(i,&read_fds)){  //we got one!!
                if(i==listener){
                    //handle new connections
                    addrlen = sizeof(remoteaddr);
                    if(newfd=accept(listener, (struct sockaddr *)&remoteaddr, &addrlen) == -1){
                        perror("accept");
                        exit(1);
                    }else{
                        FD_SET(newfd,&master); //add to master set
                        if(newfd>fdmax){   /keep track of the maximum
                        fdmax = newfd;
                    }
                    printf("selectserver: new connection form %s on socket %d\n",inet_ntoa(remoteaddr.sin_addr),newfd);
                }
            }else{
                //handle data from a client
                if((nbytes = recv(1,buf,sizeof(buf),0)) <= 0){
                    //got error or connection closed by client
                    if(nbytes == 0){
                        //connection closed
                        printf("selectserver: socket %d hungup\n",i);
                    }else{
                        perror("recv");
                    }
                    close(1); //bye!
                    FD_CLR(i, &master); //remove from master set
                }else{
                    //we got some data from a client
                    for(j=0;j<=fdmax;j++){
                        //send to everyone!
                    if(FD_ISSET(j, &master)){
                        //except the listener and ourselves
                        if(j != listenr && j != i){
                            if(send(j,buf,nbytes, 0) == -1){
                            perror("send");
                        }
                    }
                }
            }
        }
    }
    return 0;
}


내가 두개의 파일 기술자 집합을 만들었음을 주의하라: master와 read_fds이다. 첫번째로 master는 새로운 연결을 위해 대기중인 소켓 기술자를 포함해서 현재 연결된 모든 소켓 기술자를 포함한다.

내가 master 집합을 만든 이유는 select()가 어떤 소켓이 읽어들일 준비가 됐는지 반영하기 위해 당신이 넘겨준 집합을 변경시키기 때문이다. 내가 하나의 select() 호출에서 다음 호출까지의 연결을 추적해야 하기 때문에, 나는 이것들을 안전하게 저장해 놓아야 한다. 마지막 부분에서 나는 master를 read_fds에 복사해 놓고 select()를 호출했다.

하지만 이것은 새로운 연결을 할때마다 그것을 master 집합에 추가해야한다는 것을 의미하지 않는가? 그렇다! 그리고 접속이 끊어질 때마다 그것을 master 집합에서 제거해야 하는가? 그렇다.

listener 소켓을 읽을 준비가 됐는지 알기위해 언제 점검하는지를 주의해서 보라. 접속요청이 오면 그것을 accept()하고 그것을 master 집합에 추가한다. 비슷하게, 클라이언트와의 연결을 읽을 준비가 되고 recv()가 0을 반환하면, 클라이언트가 접속을 끊었다는 것을 알게 되고, 그것을 master 집합에서 제거해야 한다. 만약 클라이언트 recv()가 0이 아닌 수를 반환하면 나는 새로운 데이터가 도착했다는 것을 알게된다. 그리고 나는 그것을 받아서 master 리스트를 통해서 그 데이타를 나머지 모든 연결된 클라이언트에 보내준다.

그리고 이것이, 친구들이여, 간단치 않지만 위대한 select()함수의 개요이다.




6.3 일부분의 send()들을 다루기


내가 send()가 당신이 요청한 모든 바이트를 send()하지 않을수도 있다고 한 send()에 관한 섹션을 기억하는가? 만약 당신이 512바이트를 보내기를 원할 때 412가 리턴되었다면 나머지 100바이트에게 어떤 일이 일어나는가?

음, 그것들은 여전히 당신의 작은 버퍼에서 내보내지기를 기다리고 있다. 당신이 통제할 수 있는 범위를 벗어나는 환경 때문에 커널이이 한 덩어리로 데이타를 모두 보내지 않기로 결정했다면, 이제, 친구여, 데이타를 거기로 내보내는 것은 모두 당신에게 달려있다.

당신은 그것을 하기 위해 함수를 아래와 같이 작성할 수 있다:

    #include <sys/types.h>
    #include <sys/socket.h>

    int sendall(int s, char *buf, int *len)
    {
        int total = 0;        // how many bytes we've sent
        int bytesleft = *len; // how many we have left to send
        int n;

        while(total < *len) {
            n = send(s, buf+total, bytesleft, 0);
            if (n == -1) { break; }
            total += n;
            bytesleft -= n;
        }

        *len = total; // return number actually sent here

        return n==-1?-1:0; // return -1 on failure, 0 on success
    }



이 예제에서 s는 당신이 데이타를 보내기를 원하는 소켓이고, buf는 데이타를 담고 있는 버퍼이며 len은 버퍼에 담겨있는 바이트수를 담고 있는 int에 대한 포인터이다.

이 함수는 에러시에 -1을 반환한다(그리고 errno는 send()호출시에 설정된다) 또한, 실제적으로 보내진 바이트 수는 len에 저장된다. 이것은 오류가 없다면 당신이 보내기를 요청한 바이트 수와 같을 것이다. sendall()은 최선을 다해서 데이타를 보낼 것이지만 만약 오류가 있다면 그것은 바로 당신에게 돌아갈 것이다.

완벽을 기하기 위해 위의 함수를 호출하는 예제도 여기 있다.

    char buf[10] = "Beej!";
    int len;

    len = strlen(buf);
    if (sendall(s, buf, &len) == -1) {
        perror("sendall");
        printf("We only sent %d bytes because of the error!\n", len);
    }

패킷을 받는 쪽에서는 패킷의 한 부분이 도착했을 때 어떤 일이 일어나는가? 만약 그 패킷이 가변적인 크기라면 받는 쪽은 언제 하나의 패킷이 끝나고 또 다른 것이 시작될지 아는가? 이럴때는 Data Encapsulation을 해야한다.(이 글 초반부의 [http]Data Encapsulation 부분이 기억나는가?) 상세한 설명을 원하면 좀 더 읽어라.




6.4 Son of Data Encapsulation


그런데 데이타를 캡슐화 한다는것은 실제로는 무엇을 의미할까? 가장 간단한 예로, 그것은 당신이 거기에 구별할 수 있는 어떤 정보나 패킷 정보, 또는 그 둘다를 가진 헤더를 붙이는 것을 말한다.

당신의 헤더는 어떤 모양일까? 음, 그것은 그냥 당신이 당신의 프로젝트를 완성하는데 필요하다고 느끼는 어떤 것이든지 표현할수 있는 어떤 바이너리 데이타이다.

우와, 애매한데.

좋다. 예를 들어, 당신이 SOCK_STREAM을 사용하는 다중 사용자 채팅 프로그램을 만든다고 하자. 유저가 어떤 것을 입력하면 서버에 전송될 때 두가지의 정보가 필요하다. 무엇을, 누가 말했는가 이다.

여기까진 괜찮지? "뭐가 문제지" 라고 말할지도 모른다.

문제는 그 메시지는 가변적인 크기를 가질수 있다는 것이다. "tom"이라는 어떤사람이 "Hi"라고 말하고, "Benjamin"이라는 또다른 사람이 "Hey guys what is up?"라고 말할 수 있다.

그래서 당신은 이 것들이 들어오는 대로 모든 것들을 send()한다. 당신이 내보내는 데이타 스트림은 이렇게 보일 것이다:

tomHiBenjaminHeyguyswhatisup?

등등. 클라이언트가 하나의 메시지가 언제 시작하고 끝날지 어떻게 아는가? 당신은 원한다면 모든 메시지를 동일한 크기로 만들어서 그냥 위의 sendall()을 호출할 수도 있다. 하지만 우리는 "tom"이 단지 "Hi"라고 말하는데 1024 바이트를 send()하게 하고 싶지 않다.

그래서 우리는 데이타를 작은 헤더와 패킷구조체에 캡슐화 한다. 클라이언트와 서버 둘다 이 데이타를 캡슐화하는 방법과 데이타를 빼내는 방법을 알고 있다. 놀랍게도 우리는 클라이언트와 서버가 통신하는 법을 묘사하는 프로토콜을 정의하려고 한다.

이 예에서, 유저 이름은 8자의 고정길이(이름이 8자 미만일때는 '/0'으로 채워지는) 라고 가정하자. 그리고 데이타는 128자까지의 가변길이라고 가정하자. 이제 우리가 이 경우에 사용할 예제 패킷 구조체를 살펴보자.

  1. len(1 byte, unsigned) -- 패킷의 총 길이, 8바이트의 유저 이름과 채팅 데이타를 포함한다.
  2. name(8 bytes) -- 유저의 이름, 필요하다면 NULL로 채워진다.
  3. chatdata(n-bytes)-- 데이타 그 자체, 128 바이트 이하. 패킷의 길이는 이 데이타에 8(위의 이름 필드의 길이)을 더해서 계산된다.

왜 내가 필드에 8바이트와 128 바이트의 제한을 선택했을까? 나는 그것들이 충분히 길 거라고 생각해서 별 의미없이 선택했다. 아마도 당신에겐 8바이트가 너무 제한적이라면 30바이트나 어떤 길이든지 당신의 name 필드를 가질 수 있다.

위의 패킷 정의를 사용하면 첫번째 패킷은 아래의 정보로 구성될 것이다.(hex와 ASCII)

0A 74 6F 6D 00 00 00 00 00 48 69
(length) T o m (padding) H i

그리고 두번째는 비슷하게

14 42 65 6E 6A 61 6D 69 6E 48 65 79 20 67 75 79 73 20 77 ...
(length) B e n j a m i n H e y g u y s w ...

(물론, 길이는 네트웍 바이트 순서로 저장된다. 이 예제에서는 한 바이트 뿐이므로 그것은 문제가 안되지만 일반적으로 당신은 모든 바이너리 정수가 당신의 패킷에서 네트웍 바이트 순서로 저장되게 해야 한다.

당신이 이 데이타를 보낼 때, 안전하게 처리할 필요가 있으므로 만약 데이타를 모두 내보내는데 send()를 여러번 호출한다해도 모든 데이타를 확실하게 전송하기 위해 위의 sendall()과 비슷한 명령을 사용해야 한다.

비슷하게, 이 데이타를 수신할 때도 약간의 특별한 작업을 할 필요가 있다. 안전하게 하기 위해, 당신은 패킷의 한 부분을 받았다고 가정해야 한다. 우리는 recv()를 계속해서 패킷이 모두 완전하게 받아질 때까지 호출해야 한다.

하지만 어떻게? 음, 우리는 패킷을 완성하기 위해 받아야 하는 바이트 수가 패킷의 맨 앞에 붙어있으므로 받아야 하는 바이트 수를 알고 있다. 우리는 또한 최대 패킷 사이즈가 1+8+128 또는 137이라는 것도 알고 있다. (왜냐하면 우리가 패킷을 그렇게 정의했으니까.)

당신이 할 수 있는 일은 두개의 패킷을 받아들일 수 있을만큼 충분히 큰 배열을 선언하는 것이다. 그것이 패킷들이 도착할때 조립할 작업 배열이다.

당신이 데이타를 recv() 할 때마다 당신은 그것을 작업 버퍼에 넣고 패킷이 완료되었는지 알아내야 한다. 패킷 전송이 완료되었다면 그 버퍼 내의 바이트 수가 헤더에 지정된 길이(+1, 왜냐하면 헤더 내의 길이는 길이 그 자체를 나타내는 바이트는 포함하지 않기 때문이다)보다 크거나 같다. 만약 버퍼내의 바이트 수가 1보다 작다면, 패킷은 명백하게 완성되지 않은 것이다. 하지만 당신은 첫번째 바이트가 잘못되서 정확한 패킷 길이를 구할 수 없는 이런 경우에 대비해서 특별한 대비를 해야 한다.

일단 패킷이 완성되면, 당신은 그것을 가지고 하고 싶은대로 할 수 있다. 그것을 사용하고 작업 버퍼에서 지워버려랴.

휴! 아직도 당신의 머릿 속에서 이해하려고 애쓰고 있나? 음, 여기 원-투 펀치의 두번째가 있다: 당신은 하나의 패킷의 끝을 지나서 다음 패킷을 하나의 recv() 호출에서 읽어 들였을 수도 있다. 그것은 하나의 완전한 패킷과 다음 패킷의 불완전한 패킷이 작업 버퍼에 있는 것이다! (하지만 이것이 작업 버퍼를 두개의 패킷을 가질 정도로 크게 만들어 놓아야 하는 이유이다.)

당신이 첫번째 패킷의 길이를 헤더에서 알 수 있기 때문에 작업 버퍼 내의 바이트 수를 알아내서, 빼기를 하면 두번째(불완전한) 패킷이 작업 버퍼에서 얼마나 많이 속하고 있는지 알 수 있다. 당신이 첫번째를 처리하고 나면 그것을 작업 버퍼에서 지우고 나서 두번째 패킷 부분을 버퍼의 앞 쪽으로 당기면 다음 recv()에 대해 대비할 수 있다.

(독자들 중 몇몇은 실제적으로 부분적인 두번째 패킷을 작업 버퍼의 앞쪽으로 옮기는 것이 시간이 걸리고 프로그램에서 순환 버퍼를 사용하면 옮길 필요가 없게 코딩할 수 있다는 것을 알고 있을것이다. 나머지 독자들에게는 불행하게도, 순환버퍼에 대한 토의는 이 글의 범위를 벗어난다. 만약 아직 궁금하다면 데이타 구조론 책을 읽고 거기서 더 알아보라.)

나는 이것이 쉽다고 하지는 않았다. 좋다, 내가 쉬울 것이라고 했다. 그리고 실제로 그렇다; 당신에겐 단지 연습이 필요할 뿐이고 곧 그것들이 자연스럽게 다가올 것이다. 엑스칼리버에 걸고 맹세한다!


7 참고사항

여기까지 와서는 아마 좀더 새로운 다른 것은 없는가 할 것이다. 또 어디서 다른 무언가를 더 찾을 수 있는가를 알고자 할 것이다.

7.1 맨 페이지

초보자라면 다음의 맨페이지를 참고하는 것도 좋다.

socket()

bind()

connect()

listen()

accept()

send()

recv()

sendto()

recvfrom()

close()

shutdown()

getpeername()

getsockname()

gethostbyname()

gethostbyaddr()

getprotobyname()

fcntl()

select()

perror()

7.2 참고 서적

다음 책들도 도움이 될것이다.

Internetworking with TCP/IP, volumes I-III
by Douglas E. Comer and David L. Stevens. Published by Prentice Hall. Second edition ISBNs: 0-13-468505-9, 0-13-472242-6, 0-13-474222-2. There is a third edition of this set which covers IPv6 and IP over ATM.


Using C on the UNIX System
by David A. Curry. Published by O'Reilly & Associates, Inc. ISBN 0-937175-23-4.


TCP/IP Network Administration
by Craig Hunt. Published by O'Reilly & Associates, Inc. ISBN 0-937175-82-X.


TCP/IP Illustrated, volumes 1-3
by W. Richard Stevens and Gary R. Wright. Published by Addison Wesley. ISBNs: 0-201-63346-9, 0-201-63354-X, 0-201-63495-3.

Unix Network Programming
by W. Richard Stevens. Published by Prentice Hall. ISBN 0-13-949876-1.


7.3 웹 상의 참고서


웹 상에는 다음과 같은 것들이 있을 것이다.

BSD Sockets: A Quick And Dirty Primer (http://www.cs.umn.edu/~bentlema/unix/--has other great Unix system programming info, too!)


Intro to TCP/IP (gopher) (gopher://gopher-chem.ucdavis.edu/11/Index/Internet_aw/Intro_the_Internet/intro.to.ip/)

Internet Protocol Frequently Asked Questions (France) (http://web.cnam.fr/Network/TCP-IP/)

The Unix Socket FAQ (http://www.ibrado.com/sock-faq/)


7.4 RFC


끔찍하지만..RFC도 봐야 하겠다.

RFC-768 -- The User Datagram Protocol (UDP) (ftp://nic.ddn.mil/rfc/rfc768.txt)

RFC-791 -- The Internet Protocol (IP) (ftp://nic.ddn.mil/rfc/rfc791.txt)

RFC-793 -- The Transmission Control Protocol (TCP) (ftp://nic.ddn.mil/rfc/rfc793.txt)

RFC-854 -- The Telnet Protocol (ftp://nic.ddn.mil/rfc/rfc854.txt)

RFC-951 -- The Bootstrap Protocol (BOOTP) (ftp://nic.ddn.mil/rfc/rfc951.txt)

RFC-1350 -- The Trivial File Transfer Protocol (TFTP) (ftp://nic.ddn.mil/rfc/rfc1350.txt)



8 많이 하는 질문들


  • Q:이런 헤더파일들은 어디서 얻죠? A:만약 당신이 당신의 시스템에 이미 갖고 있지 않다면 아마 그것들이 필요없을 것이다. 당신의 플랫폼을 위한 매뉴얼을 찾아보라. 만약 당신이 윈도우즈를 위해 만들고자 한다면 단지 #include <winsock.h>만 있으면 된다.
  • Q:bind()가 "Address already in use"를 보고한다면 어찌해야 하는가? A:listening 소켓에 SO_REUSEADDR옵션과 함께 setsockopt()를 사용해야 한다. bind() 섹션과 select()섹션을 참고하라.
  • Q:시스템상에 열린소켓의 리스트는 어떻게 얻을 수 있는가? A:netstat를 사용하라. 자세한 것은 man페이지를 참고하라. 하지만 그냥 아래와 같이 입력해도 쓸만한 출력을 얻을 수 있다.

 $netstat

  • Q:라우팅 테이블을 어떻게 볼 수 있는가? A:route(대부분의 리눅스에서 /sbin에 있다) 명령을 실행하거나 netstat -r을 실행하라.
  • Q:만약 내가 단 하나의 컴퓨터만 가지고 있다면 어떻게 클라이언트와 서버 프로그램을 실행 시킬 수 있는가? 네트웍 프로그램을 위해서는 네트웍이 필요 없는가? A:다행스럽게도, 가상적으로 모든 컴퓨터는 커널상에 들어앉아서 네트웍카드인 척 하는 루프백 네트웍 "장치"를 구현해 놓았다.(이것은 라우팅 테이블에서 "lo"라고 나열된 인터페이스 이다)

    당신이 "goat"라는 기계에 로그인했다고 한다면. 하나의 창에서 클라이언트를 실행하고 다른 창에서는 서버를 실행하라. 아니면 서버를 백그라운드에서 실행하고("server &") 같은 창에서 클라이언트를 실행하라. 루프백 디바이스의 결과는 client goat 또는 client localhost( "localhost"가 당신의 /etc/hosts 파일에 정의되어 있는한)이고 당신은 네트웍 없이 클라이언트가 서버에게 이야기 하게 할 수 있다.

    짧게 말해서, 하나의 네트웍에 연결되지 않은 기계에서 실행하기 위해서 어떤 코드도 바꿀 필요가 없다.
  • Q: 반대편이 연결을 끊었는지 어떻게 알수 있나? A:recv()가 0을 반환하면 알 수 있다.
  • Q:"ping" 유틸리티는 어떻게 구현할 수 있나? ICMP는 무엇인가? 어디서 raw소켓과 SOCK_RAW에 대해서 더 알 수 있나? A:당신의 raw 소켓에 관한 모든 질문은 W. Richard Stevens 의 UNIX Network Programming책들에서 답을 얻을 수 있다. 이 안내서의 책에 관한 섹션을 보라
  • Q:윈도우즈용은 어떻게 만들 수 있나? A:첫번째로, 윈도우즈를 지우고 Linux나 BSD를 설치하라. );-). 아니다. 단지 초반의 윈도우즈용으로 개발하기 섹션을 보라.
  • Q:Solaris/SunOS용으로 어떻게 개발할 수 있나? 컴파일 하려고 하면 linker에러가 발생한다. A:linker에러는 선 박스들이 소켓 라이브러리에서 자동적으로 컴파일 하지 않기 때문이다. 초반부의 Solaris/SunOS용으로 개발하기 섹션을 보라.
  • Q:왜 select()가 시그널에 중단되는가? A:시그널은 블록된 시스템 콜이 errno를 EINTR로 설정하고 -1을 반환하게 만든다. 당신이 sigaction()으로 시그널 핸들러를 설정할때 당신은 시스템 콜이 인터럽트 된 후에 재시작 하게 해주는 SA_RESTART 플래그를 설정 할 수 있다.

    당연히, 이것은 항상 동작하는 것은 아니다.

    내가 이 문제를 해결하기 위해 가장 좋아하는 해결법은 goto문장이다. 당신도 이것이 당신의 교수들을 괜시리 짜증나게 할것이라는것은 알것이다. 그래도 일단 해보라!

    select_restart:
    if((err=select(fdmax+1, &readfds, NULL,NULL,NULL)) == -1){
        if(errno == EINTR){
        //some signal just interrupted us, so restart
        goto select_restart;
        }
    //handle the real error here:
    perror("select");
    }

확실히 당신은 여기서 goto문을 써야할 필요는 없다; 이것을 처리하기 위해 다른 구조를 사용할 수도 있다. 하지만 나는 이 방법

이 실질적으로 제일 깨끗하다고 생각한다.


  • Q:recv()시에 시간제한을 어떻게 구현할수 있는가? A:select()를 사용하라! 그것은 당신이 읽어들이려고하는 소켓기술자를 위한 timeout 매개변수를 지정할 수 있게 해준다. 또는 아래처럼 하나의 함수에 모든 기능을 집어넣을 수도 있다.

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

    //set up the file descriptor set
    FD_ZERO(&fds);
    FD-SET(s,&fds);

    //set up the struct timeval for the timeout
    tv.tv_sec = timeout;
    tv.tv_usec = 0;

    //wait until timeout or data received
    n=select(s+1,&fds,NULL,NULL,&tv);
    if(n==0) return -2; //timeout!
    if(n==-1) return -1; //error 

    //data must be here, so do a normal recv()
    return recv(s,buf,len,0);
}

    //sample call to recvtimeout();

.
.
    n=recvtimeout(s,buf,sizeof(buf),10); //10second timeout

    if(n == -1){
    //error occurred
    }else{
    //got some data in buf
    }
.
.

recvtimeout()이 시간종료시에 -2를 반환하는 것에 주의하라. 왜 0을 반환하지 않을까? 음, 회상해보면 recv()호출시에 0의 반환값은 원격 연결지에서 접속을 끊었다는 것을 의미한다. 그래서 반환값 -1은 "error"를 의미하도록 선언되었기 때문에 나는 -2를 시간 종료를 의미하도록 지정했다.


  • Q:어떻게 데이타를 소켓을 통해 보내기 전에 압축하거나 암호화 할 수 있나
    A:하나의 쉬운 방법은 SSL(secure sockets layer)를 이용하는것이지만, 이 안내서의 범위를 벗어난다. 하지만 당신이 당신의 압축기나 암호화 시스템을 끼워넣거나 구현한다고 가정하면 데이타가 양 끝단을 오가는 순서상의 차이일뿐이다. 데이타는 각각의 단계를 지나면서 어떤식으로든 바뀌게 된다.
    1. 서버가 파일(또는 어디에서든지)에서 데이타를 읽어들인다.
    2. 서버가 데이타를 암호화한다.(당신이 이 부분을 더한다)
    3. 서버가 암호화된 데이타를 send()한다.

    이제 반대편에서는

    1. 클라이언트가 암호화된 데이타를 recv()한다.
    2. 클라이언트가 데이타를 해독한다(당신이 이 부분을 끼워 넣는다.)
    3. 클라이언트가 데이타를 파일(또는 어떤곳이든지)에 기록한다.

    또 당신은 암호화/해독를 하는 똑같은 지점에서 압축을 할 수 있다. 또는 당신은 둘 다 할 수도 있다! 단지 암호화 하기전에 압축해야한다는 것을 잊지마라.

    클라이언트가 서버가 한것을 무효화 할 수만 있다면 얼마나 많은 중간단계를 더한다해도 그 데이타는 결국에는 변함없을 것이다 그래서 당신이 내 코드를 사용하기위해 필요한 것은 데이타가 읽혀지고 네트웍을 통해 보내어지는(send()를 사용해서) 중간에 암호화 코드를 끼워넣을 만한 장소를 찾는 것이다.
  • Q:내가 계속 보고 있는 PF_INET은 무엇인가? AF_INET과 관계있는것인가?
    A:그렇다. 자세한것은 socket()섹션을 보라.
  • Q:클라이언트에서 쉘 명령어를 받아서 실행하는 서버를 어떻게 만들 수 있나?
    A:간단히 말해서, 클라이언트가 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(!strcmp(str,"foobar")){
        sprintf(sysstr, "%s > /tmp/server.out",str);
        system(sysstr);
    }
    

  • 하지만 불행하게도 이것도 안전하지 않다: 만약 클라이언트가 "foobar; rm -rf ~"를 입력한다면? 가장 안전한 것은 명령어를 위한 옵션의 알파벳과 숫자를 제외한 모든 문자(스페이스도 포함해서)의 앞에 이스케이프("\") 문자를 집어넣는 것이다.

    보다시피 서버가 클라이언트가 보내주는명령을 실행하게 만든다면 보안이 아주 큰 문제가 된다.
  • Q:나는 많은 데이타를 보내는 중인데 내가 recv()했을때 그것은 단지 한번에 536바이트나 1460바이트밖에 받지 않는다. 하지만 내 로컬 머신에서 실행시키면 그것은 한번에 모든 데이타를 받는다. 어떻게 된건가?
    A: MTU 문제다. MTU는 물리적인 매체가 취급할수 있는 최대의 크기인데 로컬 머신에서는 8K나 그이상을 루프백을 사용해서 처리해도 문제없다. 하지만 헤더를 포함해서 1500 바이트만을 처리할 수 있는 이더넷에서는 당신은 그 한계에 도달한 것이다. 모뎀을 통해서는 더 작은 576 MTU(다시한번, 헤더포함)가 한계이다.

    무엇보다 당신은 모든 데이타가 보내졌는지 확인해야 한다.(자세한 설명은 sendall()함수 구현을 보라.) 일단 그것만 확실하면 모든 데이타가 받아질때까지 recv()를 루프내에서 호출할 필요가 있다.

    다중의 recv()호출을 이용한 완전한 데이타를 얻는 자세한 설명은 Son of Data Encapsulation 섹션을 읽어보라.

  • Q: 나는 윈도우 박스위에 있고 fork() 시스템 호출이나 sigaction 구조체 종류를 사용할 수 없다. 어떻게 해야하나?
    A:만약 그것들이 어딘가 있다면 그것들은 POSIX라이브러리안에 있을것이고 당신의 컴파일러에 탑재되었을것이다. 나는 윈도우 박스를 가지고 있지 않아서 답을 줄 수가 없지만 Microsoft는 POSIX 호환성 계층을 가지고 있다고 기억하고 있고, 거기에 fork()가 있을것 같다.(sigaction도 마찬가지다) VC++에 동봉된 도움말에 "fork"나 "POSIX"를 찾아보면 어떤 단서가 있을것이다.

    만약 그것이 소용없다면, fork()/sigaction같은 것들을 찾아내서 Win32의 CreateProcess()에 해당하는 것들로 대체하라. 나는 CreateProcess()를 사용하는 방법을 모른다. 그것은 수많은 매개변수를 가진다, 하지만 그것은 VC++에 포함된 문서들이 다루고 있을것이다.

  • Q: 암호화를 해서 안전하게 TCP/IP로 데이타를 보내는 방법은?
    A:OpenSSL 프로젝트를 참고하라.

  • Q:나는 방화벽 뒤에 있다-- 방화벽 외부의 사람이 내 IP주소를 알아내서 내 컴퓨터에 접속하게 하는 방법은?
    A:불행하게도, 방화벽의 목적은 방화벽 외부의 사람들이 방화벽 내부의 컴퓨터에 접속하는것을 막는 것이다. 그러므로 외부에서 접속하게 하는것은 기본적으로 보안을 침해하는것으로 여겨진다.

    이것은 모든것이 막혀있다고 하는것은 아니다. 예를 들자면, 당신은 여전히 때때로 그것이 masquerading나 NAT등등의 어떤 일을 할때 방화벽을 통해 connect()할 수 있다. 단지 당신의 프로그램의 연결을 초기화 하는 유일한 사람이기만 하면 당신은 괜찮을 것이다.

    만약 그것이 불만족스럽다면, 당신은 당신의 시스템 관리자에게 사람들이 당신에게 접속할 수 있게 방화벽에 구멍을 하나 만들어 달라고 요청할 수도 있다. 방화벽은 그것의 NAT소프트웨어나 프록시같은 것들을 통해서 당신에게 포워딩 해 줄 수 있을것이다.

    방화벽의 구멍을 가볍게 생각해서는 안된다. 당신은 나쁜사람들이 내부 네트웍에 접속할 수 없게 해야한다. 만약 초보자라면, 소프트웨어가 보안성을 가지게 만드는 것은 당신이 상상하는 것보다 어렵다.

    당신의 시스템 관리자가 나에게 화나지 않도록 하라.;-)




  • 9 주의사항 및 연락처

    이상이 전부이며 문서 상에서 크게 틀린 곳이 없기만을 바랄 뿐이다. 하지만 실수는 항상 있는 법이다.

    만약 실수가 있다면 부정확한 정보를 주어 헷갈리게 만듯 것에 대하여 사과하지만 사실상 나한테 책임을 물을수는 없다. 이 얘기는 법적인 경고이며 사실상 이 모든 글들이 몽땅 거짓말일 수도 있는 것이다.

    그래도 설마 그렇지는 않을 것이다. 사실 난 이 모든 것들 때문에 상당히 많은 시간을 소모했고 윈도우용 TCP/IP네트워크 유틸리티(예를 들어 텔넷 등)을 방학숙제로 했었다. 난 소켓의 신이 아니라 그냥 보통 사람일 뿐이다.

    그건 그렇고 생산적인 (혹은 파괴적이라도) 비평이 있는 분은 beej@ecst.csuchico.edu 앞으로 메일을 주기 바란다. 참고하여 고쳐나가도록 노력을 해 보겠다.

    왜 이 일을 했는가 궁금하다면, 돈벌려고 했다. 하하~ 사실은 아니고 많은 사람들이 소켓에 관련된 질문을 해대는 바람에 그들에게 이 내용을 웹에 올리려고 생각 중이라고 말했더니 "바로 그거야~"라고들 해서 썼다. 아무리 고생해서 얻은 정보라도 만일 다른 사람과 공유하지 않는다면 쓰레기일 뿐이라고 생각한다. WWW는 바로 적당한 수단이 된 것 뿐이다. 다른 사람도 이런 정보의 제공이 가능하다면 이렇게 해주길 바란다.

    끝났다. 프로그램이나 짜러가자. ;-)

    번역한 사람의 말: 우연히 이 글을 발견하게 되어 번역을 하고 보니 나름대로 가치가 있어 보여서 홈페이지에 올려 놓았습니다. 번역상의 실수가 있었다면 사과드리며 지적해 주신다면 고쳐 나가겠습니다. 좋은 프로그램을 만드는 데에 이 글이 작으나마 도움이 되길 바랍니다.



    Copyright © 1995, 1996 by Brian "Beej" Hall. This guide may be reprinted in any medium provided that its content is not altered, it is presented in its entirety, and this copyright notice remains intact. Contact beej@ecst.csuchico.edu for more information. 좋은 내용의 글을작성하고 한글판 번역을 허락해준 원작자에게 감사하며 번역자로서의 모든 권리는 읽어주신 분들께 드리겠습니다. 번역상의 실수나 생산적인 지적은 tempter@fourthline.com 으로 보내주시면 되겠습니다. 감사합니다.
    반응형
    Posted by Real_G