정영준/자유기고가
|
바야흐로 인터넷/통신을 모르면 컴맹이라는 소릴 듣는 시대가 왔다. 컴퓨터의 주요 기능 또한 네트웍으로 되어 가고 있다. 네트웍이 강점인 유닉스/리눅스가 기승을 부릴 때이다. 유닉스의 꽃이라 할 수 있는 네트웍 프로그래밍을 공부함으로써 진정한 유닉스 프로그래머가 되어 보자.
Connection-oriented socket 이란? 유닉스의 모든 것은 파일이라는 말이 있다. 이
말은 유닉스 프로그래밍을 해 보면 모든 I/O작업이 파일에
쓰고 읽음으로써 이루어 진다는 말이다. 네트웍의 작동 또한 마찬가지이다.
양쪽의 컴퓨터가 각각 파일, 즉 socket 을 열어서 그 파일을 연결시켜
통신이 이루어지는 것이다. Stream socket은 안정적인 양방향의 연결을 만들
때 필요한 소켓이다. 이 연결은 양방향으로 통신이 가능하며, 데이터들의
순서가 정확하다. 또한 이 연결은 tcp 프로토콜을 사용하므로 에러
체킹이 되어 안정적인 데이터 전송이 가능하다. telnet 이나 http.ftp
등과 같이 상호 작용이 필요하고 안정적인 연결이 요구될 때 바로
이 소켓을 사용한다. 이 소켓을 다른 말로 connection-oriented
socket 이라고도 한다. 반면 datagram socket은 단방향 일회성이라 할
수 있다. 하나의 packet(데이터 덩어리)에 주소를 집어넣어 보내면
그 패킷이 혼자서 목적지로 찾아가는 방식이다. 이렇기 때문에 연결을
시켜놓을 필요가 없어 일회성이며, 이 패팃이 전송 도중 분실될
수도 있다. tftp나 bootp같은 프로그램이 이러한 소켓을 사용한다.
이 강좌에서 우리는 몇 가지 예제를 통하여 connection-oriented socket에 대해 심도있게 공부를 하게 될 것이다.
소켓 관련 시스템 호출 connection-oriented socket을 만들어 연결을 하기 위해서는 다음과 같은 과정이 필요하다. Server: socket() -> bind() ->listen()
-> accept() 이 시스템 호출들은 대부분 sockaddr 구조체를 인수로 필요로 한다. sockaddr 구조체는 다음과 같다. #include <sys/socket.h> 여러 종류의 소켓이 같은 시스템 콜을 이용하기
때문에 sockaddr 구조체에서 소켓 종류에 의존적인 부분은 sa_data[]와
같이 배열로 남겨 두었다. Sa_family는 프로토콜의 종류를 결정하는
것으로, 우리가 사용할 것은 AF_inet(Internetfamily) 이다. family의
종류로는 af_INET 외에 AF_UNIX, AF_NS등이 있다. #include <netinet/in.h> sin_family는 AF_INET로 지정될 것이며, sin_port는 포트번호, sin_addr은 인터넷 주소를 지정한다. Sin_zero[]는 sockaddr과 크기를 맞추기 위한 배열이기 때문에 sockaddr_in을 사용하기 전에 bzero()나 memset()을 이용해서 0으로 초기화한다. Byte Ordering Routine 컴퓨터 마다 byte들을 변수에 저장하는 순서가 다르다. 그래서 다른 컴퓨터끼리 통신이 이루어 지려면 이러한 순서를 맞춰 주어야 한다. 네트웍 상에서의 데이터의 byte order를 Network byte order라고 하며, 컴퓨터 내부에서 사용되는 byte order를 host byte order라고 한다. 그러므로 sin_addr에 들어갈 숫자는 network byte order 이다. 우리가 sin_port와 sin_addr에 값을 지정하려면 컴퓨터의 host byte order인 변수 값을 network byte order 로 바꾸어서 저장해야 한다. 이러한 순서 변환 함수는 4가지가 있다. htons()-"Host to Network Short" 이름 그대로 htons() 는 short 형의 변수를 host
byte order에서 network byteorder로 바꿔준다. 나머지도 마찬가지이다.
Address Conversion Routine IP 어드레스의 지정을 위해서는 sin_addr.s_addr에 주소를 지정해야 한다. sa라는 sockaddr-in 구조체가 있다고 하자. 여기에 "127.0.0.1"라는 주소를 지정하고 싶을 때에는 다음과 같이 한다. sa.sin_addr.s_addr =inet_addr("127.0.0.1")
; printf("%s",inet_ntoa(sa.sin_addr)); socket() int socket(int family, int type, int protocol); sckfd =socket(AF_INET,SOCK_STREAM,0); bind() int bind(int sockfd, struct sockaddr *myaddr,
int addrlen); lsten() int listen(int sockfd, int baklog); Accept() int accept(int sockfd, struct sockaddr * peer,int
*addrlen); connect() int connect(int sockfd, struct sockaddr
*servaddr, int addrlen); socket을 통한 read/write 소켓은 하나의 파일이므로 모든 파일 I/O 함수들을
사용할 수 있다. 여기서 우리는 read(), write() 시스템 호출을
이용하게 될 것이다. 1. 상대방이 닫혀진 소켓에 read()를 실행하면
0을 리턴한다. 이 시그널이 무시되거나 이 시그널을 위한 시그널
핸들러가 설치되었을 경우에는 에러가 발생하고(-1을 리턴), errno가
EPIPE로 세팅된다. echo 서버 프로그램 이 서버 프로그램은 포트 4000번으로부터 연결을 받아들여서, 데이터를 읽고 그것을 도로 반송하는 작업을 반복한다. /*echo server*/ #define ECHO_PORT4000 int newsock: 프로그램의 흐름은 소켓을 만들고, sock=socket(AF_INET,
SOCK_STREAM, 0); 이제 이 프로그램을 테스트 해보자. 주소변환하기 www.linux-kr.org과 같이 문자로 이루어진 주소는 127.0.0.1과 같이 숫자로 이루어진 주소로 변환되어서 처리되어야 한다. 이러한 작업은 DNS(Domain Name Service)에 의해 시스템에서 이루어진다. 프로그램에서 이를 이용하기 위해서는 gethostbyname() 이라는 함수를 사용한다. #include <netdb.h> gethostbyname()은 문자로 이루어진 주소를 인수로 받아서 hostent 구조체를 리턴한다. 이 구조체의 이용 예를 통해 사용법을 살펴보자. #include <stdio.h> int main(int argc, char *argv[]) return 0; 여기서 볼 수 있듯이 h_name은 문자로 이루어진
호스트의 이름을, h_addr은 in_addr현 구조체에 들어갈 Network
byte order인 숫자를 저장한다. h_addr은 실제적으론 h_Addr_list[0]의
매크로이다. 여기서 genthostbyname()에 에러가 발생하여 NULL을 반환할 때 perror()가 아닌 herror()함수를 사용하였다. 이는 gethostbyname()은 에러 시에 errno가 아닌 h_errno에 에러를 세팅하므로, perror() 대신 herror()함수를 사용하여 에러 내용을 확인한다. <주소변환 알고리즘> echo 클라이언트 프로그램 클라이언트 프로그램은 소켓 연결 후 fork()를
실행시킨다. fork()가 실행되면 자식과 부모는 하나의 소켓을 동시에
열어 놓을 수 있으므로, 하나의 소켓에 읽기와 쓰기를 동시에 수행할
수 있다. 자식 프로세서는 서버로부터 데이터를 읽어서 출력하는
작업을 반복한다. 부모 프로세서는 터미널을 cbreak 모드로 전환한
뒤 입력된 문자를 곧바로 서버에 전송하는 작업을 반복한다. cbreak모드에서는
자신이 친 문자를 화면에 표시하지 않고, 한번에 한 문자씩 읽어
들인다(echo off, noncanonical input). /* echo client*/ #define ECHO_PORT 4000 int tty_cbreak(int fd, int set); 주소를 변환하는 과정을 살펴보자. If((serv_addr.sin_addr.s_addr=inet_addr(addr))!=INADDR_NONE) 만약 addr이 "127.0.0.1"과 같이 숫자로 이루어진 주소값이라면 inet_addr()함수는 정상적으로 작동한다. 그렇지 않을 경우는 INADDR_NONE(-1)라는 값이 리턴된다. 에러나 났으면 문자로 이루어진 주소인지를 체크한다. else gethostbyname()함수가 성공을 하면 우리는 host->h_addr을
serv_addr.sin_addr에 복사하여 serv_addr 구조체를 완성시킬 수
있다. Hostname은 문자로 이루어진 주소의 완전한 값을 가진다.(매크로
같은 이름이 아닌...) 전영준
<네트웍 프로그래밍의 시작을 위한 안내> 네트웍 프로그래밍은 기본적인 유닉스 프로그래밍
주제가 아니다. 유닉스 시스템 프로그래밍에 대한 기본적인 지식이
없이는 네트웍 프로그래밍을 제대로 배워나가기가 어려울 것 이다.
수많은 파일 콘트롤 기법들을 다루게 될 것이고, 유닉스 시스템의
내부적인 동작을 기본지식으로 필요로 할 것이다. 아직 유닉스 시스템
프로그래밍에 익숙치 않은 분은 유닉스 시스템 프로그래밍 관련
전문 서적을 일독하기를 권한다. Stevens씨가 지은 Unix Network Programming이란
책은 유닉스 네트웍 프로그래밍의 바이블이라 할 만한 책이다. Stevens씨는
이 책 외에도 Advanced Programming in the UNIX Environment라는
유명한 책도 지었다. 그의 이 네트웍 서적은 네트웍 프로그래밍에
관련된 거의 모든 것이 망라되어 있어 필수적이라 할 만한 책이지만
초보자가 혼자서 읽기에는 다소 힘든 점이 많다. 잠시 이 책에 관한 안내를 하겠다. 이 책의 앞부분에는 유닉스 시스템과 네트웍의
기본 지식으로 250페이지에 걸쳐 많은 설명을 해 놓았다. 그러나
이 설명은 "유닉스 시스템에 익숙치 않은 사람에겐 다른 좋은
책을 읽는 것이 더 낫고, 유닉스 시스템에 익숙한 프로그래머는
이 부분이 필요가 없기" 때문에 저자가 책의 완결성을 높이기
위해서 넣은 부분이라고 생각하면 될 것이다. 다만 4장과 5장부분의
네트웍 배경지식에 대해서는 간단히 훑어 보는 것도 좋을 것이다.
네트웍 프로그래밍의 핵심은 6장과 8장에 모두
다 있다. 6장과 8장에 있는 부분만 모두 이해를 하고 있다면 일반적인
소켓 프로그래밍시 전혀 어려운 점이 없을 것이다. 유닉스 시스템과
네트웍에 관해 깊은 지식을 얻고자 하시는 분은 14장 또는 15장에
있는 부분을 공부하면 상당한 도움이 되리라 생각된다. (이 책의 한글판을 읽으면 용어에 대한 심각한
혼동이 생길 것이다. 영어에 부담이 없으신 분은 영어로 된 원서를
구입하길 추천한다.) 유닉스 소켓 프로그래밍에 관련 의문사항이 생기면
우선 socket FAQ를 살펴보기 바란다. 소켓 프로그래밍을 처음 공부하시는
분이 가질 수 있는 모든 질문과 답이 여기에 적혀있다. 다음의 site에서
구할 수 있다. ftp://rtfm.mit.edu/pub/usenet/news.answers/unix-faq/socket |