현재강좌 : UNIX BSD 소켓 시스템 콜 이전: 2.3 클라이언트 프로그램 작성절차 다음: 2.5 소켓 관련 UNIX 시스템 콜


2.4 서버 프로그램 작성 절차

▶ 본 절에서는 echo 기능을 갖는 서버 프로그램 myecho_ server.c를 구축해 본다.

2.4.1 연결형 서버 프로그램 작성 절차

▶ 그림 2-11에 TCP(연결형) 서버를 iterative 형태 즉, 서비스 요구가 들어오는 순서대로 처리해 주는 형태로 구축하는 절차를 나타냈다.

▶ 서버는 socket()으로 소켓을 개설하고 bind()를 수행한 후 listen()으로 소켓을 수동 대기모드로 만든다.

▶ 다음에는 accept()를 호출하여 자신에게 연결을 요청하는 클라이언트의 연결을 처리하도록 한다.

▶ 클라이언트와 연결된 후에는 클라이언트가 요구하는 요청(request)을 처리하고 결과(response)를 전송해 주는 방식으로 서비스를 처리하고 하나의 서비스를 완료하면 다음 요청을 처리한다.

그림 2-11 Iterative 모델의 TCP(연결형) 서버 프로그램 작성 절차

(1) socket(), 소켓의 생성

▶ 서버 프로그램도 클라이언트와 마찬가지로 통신을 하기 위하여는 트랜스포트 프로토콜을 지정하여 소켓을 만들어야 하는데 이를 위해 socket() 함수를 사용한다.

▶ socket()의 사용 방법은 클라이언트의 경우와 같으며 myecho_ server.c에서도 연결형 소켓을 사용하므로 프로토콜 체계로 SOCK_STREAM을 지정한다.

socket(PF_INET, SOCK_STREAM, 0);

 

(2) bind(), 소켓번호와 소켓주소 구조체 연결

▶socket() 시스템 콜을 통해서 생성된 소켓은 그 응용 프로그램내에서 유일한 번호인 소켓번호를 하나 배정받는다.

▶ 그러나 이 번호는 응용 프로그램만 알고 사용하는 번호이므로 이 프로그램이 컴퓨터 외부와 통신하려면 이 소켓번호와 TCP/IP 시스템이 제공하는 소켓주소(IP 주소 + 포트번호)를 연결해 두어야 하며 이를 위하여 bind()를 사용한다.

▶ 그림 2-12에 bind() 호출시의 IP 주소, 포트번호, 그리고 소켓번호의 관계를 나타냈다.

▶ bind()는 응용 프로그램 자신의(local) 주소와 소켓번호를 연결하는 작업이라고 할 수 있다.

▶ 서버에서 bind()가 반드시 필요한 이유는 임의의 클라이언트가 서버의 특정 프로그램이 만든 소켓과 통신을 하려면 그 소켓을 찾을 수 있어야 하며, 따라서 서버는 소켓번호와 클라이언트가 알고 있을 서버의 IP 주소 및 포트번호(즉, 서버의 소켓주소)를 미리 서로 연결(bind)시켜 두는 것이 필요하기 때문이다.

그림 2-12 bind() 호출시 소켓 번호와 소켓 주소의 관계

▶ 아래는 bind() 시스템 콜의 사용 문법이며 bind()는 성공하면 0을, 실패하면 -1을 리턴한다.

▶ 인자 s는 bind시킬 소켓번호로서 socket() 시스템 콜이 리턴한 것이며 *addr은 소켓주소를 담고 있는 구조체이다.

int bind (

int s, /* 소켓번호 */

struct sockaddr *addr, /* 서버 자신의 소켓주소 구조체 포인터 */

int len); /* *addr 구조체의 크기 */

▶ 아래의 프로그램 코드는 소켓을 만들고 이것을 IP 주소가 203.252.65.3이고 포트번호가 3000번인 소켓주소 구조체와 bind()하는 것을 보였다.

▶아래에서 inet_addr()은 문자열로 된 dotted decimal IP 주소 203.252.65.3를 4바이트 IP 주소 1010111 10110010 00101101 00000011로 바꾸는 함수이며(2.2.3절 참조), htons()는 호스트 바이트 순서의 숫자 3000번을 네트웍 바이트 순서로 바꾸기 위하여 사용되었다.

#define SERV_IP_ADDR "203.252.65.3"

#define SERV_PORT 3000

/* 소켓 생성 */

s = socket(PF_INET, SOCK_STREAM, 0);

struct sockaddr_in server_addr;

/* 소켓주소 구조체 내용 */

server_addr.sin_family = AF_INET;

server_addr.sin_addr.s_addr = inet_addr(SERV_IP_ADDR);

server_addr.sin_port = htons(SERV_PORT);

/* 소켓번호와 소켓주소를 bind */

bind(s, (struct sockaddr *)&server_addr, sizeof(server_addr));

▶ 앞의 bind()문에서 소켓주소 구조체를 나타내는 함수 인자로 server_addr을 바로 사용하지 않고(struct sockaddr*) &server_addr을 사용한 것을 알 수 있다.

▶ 대부분의 인터넷 소켓 프로그램에서는 인터넷 주소를 편리하게 다루기 위하여(즉, IP 주소와 포트번호를 직접 기록하거나 읽을 수 있도록) sockaddr_in 구조체를 사용하고 있다.

▶ bind() 함수를 비롯한 각종 소켓 함수의 정의에서는 일반적인 소켓주소 구조체인 sockaddr를 사용하도록 정의되어 있기 때문에 구조체 타입을 바꾸는 casting이 필요한 것이다.

▶ 위에서 자신의 IP 주소로 203.252.65.3을 구체적으로 지정하였다.

▶ 응용 프로그램이 수행되는 컴퓨터 자신의 IP 주소를 자동으로 가져다 쓰려면 INADDR_ANY라는 변수를 다음과 같이 사용하면 된다.

server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

 

▶ 위에서는 포트번호로 3000번을 지정하여 사용하였지만, 포트번호를 0으로 하고 bind()를 호출하면 시스템(즉, TCP/IP)이 포트번호를 자동으로 배정해 준다.

▶ 다음은 이것을 확인하는 예제 프로그램 test_bind.c인데 소켓을 두 개 개설하고 각각 포트번호를 0으로 하여 bind()를 호출한 후 시스템이 배정한 포트번호를 화면에 출력하고 있다.

/*-----------------------------------------------------------------------------

파일명 : test_bind.c

: 시스템이 자동으로 배정한 포트번호를 출력하는 프로그램

컴파일 : cc -o test_bind test_bind.c -lsocket -lnsl

실행예 : test_bind

---------------------------------------------------------------------------- */

#include <sys/types.h>

#include <sys/socket.h>

#include <stdio.h>

#include <stdlib.h>

#include <netinet/in.h>

#include <arpa/inet.h>

#include <limits.h>

#define ADDRESS "192.203.144.11"

#define PORT 7

#define MSG "Test Message"

int main() {

int sd1, sd2 ; /* 소켓번호 */

struct sockaddr_in sin1, sin2 ; /* 소켓주소 구조체 */

int addr_len ; /* 소켓주소 구조체의 크기 */

u_short rtn1, rtn2; /* 포트번호 */

/* TCP UDP 두가지 소켓 생성 */

sd1 = socket(AF_INET, SOCK_STREAM, 0) ;

sd2 = socket(AF_INET, SOCK_DGRAM, 0) ;

sin1.sin_family = PF_INET ;

sin1.sin_addr.s_addr = inet_addr(ADDRESS) ;

sin1.sin_port = htons(PORT) ;

if(connect(sd1, (struct sockaddr*)&sin1, sizeof(sin1))<0)

{

printf("Error : Connect failed!!!\n") ;

exit(1) ;

}

addr_len = sizeof(sin2);

if (getsockname(sd1, (struct sockaddr*)&sin2, &addr_len) < 0){

printf("getsockname error\n");

}

rtn1 = sin2.sin_port ;

if(sendto(sd2, MSG, strlen(MSG), 0, (struct sockaddr*)&sin1, sizeof(sin1))<0)

{

printf("Error : sendto failed!!\n") ;

exit(1) ;

}

addr_len = sizeof(sin2);

if(getsockname(sd2, (struct sockaddr*)&sin2, &addr_len)<0)

printf("Error : getsockname error\n") ;

rtn2 = sin2.sin_port ;

/* 배정된 포트번호 출력 */

printf("stream socket's bind return = %d\n", rtn1 ) ;

printf("datagram socket's bind return = %d\n", rtn2) ;

close(sd1) ;

close(sd2) ;

}

▶ 서버에서 socket()과 bind()를 호출하여 통신을 할 준비가 된 후, 데이터를 송수신하는 절차는 소켓 개설시 지정한 트랜스포트 프로토콜의 종류에 따라 다르다.

▶ 연결형 통신(TCP)에서는 listen(), accept()의 호출이 필요하고 비연결형 통신(UDP)에서는 바로 데이터의 송수신이 가능하다.

▶ 우선 연결형 서버의 경우에 대하여 설명하겠다.

(3) listen(), 클라이언트로부터의 연결요청을 기다리기

▶ 서버는 클라이언트로부터의 연결요청을 받아들이기 위하여 이를 기다리고 있어야 하는데 이를 위하여 listen()을 호출하며 listen()의 사용문법은 아래와 같다.

 

int listen (

int s, /* 소켓번호 */

int log); /* 연결을 기다리는 클라이언트의 최대 */

▶ 위에서 인자 log는 서버에서 (다음에 설명할) accept()를 처리하는 동안 대기시킬 수 있는 connect()의 요청 수를 지정한다. 즉, 클라이언트가 요구한 연결요청을 최대 log개까지 기다리게 할 수 있다는 것이다.

▶ 예를들어 아래의 코드는 서버가 최대 2개의 connect() 요청을 대기시킬 수 있으며, 세 번째 이후의 connect() 요청은 거절하여 클라이언트가 이 사실을 바로 알 수 있도록 해준다.

listen(s, 2);

▶ 한편 listen()은 소켓을 단지 수동 대기모드로 바꾸는 것이므로 listen()의 호출은 즉시 리턴되는데 성공시에는 0, 실패시에는 -1이 리턴된다.

(4) accept(), 클라이언트로부터의 연결요청 수락

▶ 서버가 listen()을 호출한 이후에 어떤 클라이언트에서 connect()로 이 서버에 연결요청을 보내오면 이를 처리하기 위해 서버는 accept()를 호출해 두어야 한다.

▶ accept()의 수행이 성공한 경우에는 접속된 클라이언트와의 일 대 일 통신에 사용할 새로운 소켓이 만들어지고 accept()는 이 소켓번호를 리턴하며 실패시에는 -1을 리턴한다.

▶ accept()는 또한 접속된 클라이언트의 소켓주소 구조체와 구조체의 길이의 포인터를 함수인자 addr과 addrlen으로 각각 리턴한다.

▶ accept()의 사용 문법은 아래와 같고 accept() 호출시에 얻는 값들을 그림 2-13에 나타냈다.

int accept (

int s, /* 소켓번호 */

struct sockaddr *addr, /* 연결요청을 상대방의 소켓주소 구조체 */

int *addrlen); /* *addr 구조체 크기의 포인터 */

 

그림2-13 accept() 호출시 얻는 소켓 주소 정보와 새로운 소켓

(5) close(), 소켓 종료

▶ 소켓을 닫을 때 close()를 호출하는데 데이터그램(UDP) 소켓에서 close()를 호출하면 단순히 사용하던 소켓을 닫는 작업만 수행한다.

▶ 그러나 스트림(TCP)소켓은 연결형 서비스이므로 현재 미처리된 패킷들(송신 버퍼에 있으나 아직 송신이 안 된 패킷 또는 현재 송수신중에 있는 패킷)을 모두 처리한 후에 소켓을 닫게 된다.

▶ 그러나 이러한 미처리 패킷을 즉시 모두 버리게 하거나, 지정한 시간동안 처리되기를 기다릴 수 있는데 이를 위하여는 setsockopt() 시스템 콜을 사용한다(자세한 내용은 5.2.1절 참조).

2.4.2 서버 프로그램 작성

▶ 여기서는 echo 서비스를 제공하는 서버 프로그램 myecho_ server.c를 소개한다.

▶ myecho_server.c는 클라이언트의 접속요청을 수락한 후 이를 단말기에 표시한 다음 echo 서비스를 한 번 수행한다.

▶ myecho_server.c의 실행결과는 다음과 같다.

# myecho_server 2049

Server : waiting connection request.

Server : client connected.

Server : waiting connection request.

▶ 예를들어 포트번호 2049를 지정하여 myecho_server.c를 서버에서 실행한 후 어떤 클라이언트에서 이 서버로 접속을 하려면 포트번호를 2049로 하여 접속을 요구하여야 한다.

▶ 이러한 클라이언트 프로그램은 2.3.4절의 myecho.c에서 서버의 포트번호를 2049로 하여 사용하면 된다.

▶ myecho_server.c 프로그램은 두 개의 소켓번호를 필요로 하는데 하나(server_fd)는 클라이언트의 연결요청을 기다리기 위해 사용되고 다른 하나(client_fd)는 클라이언트와 데이터를 주고 받기 위해 사용된다.

▶ 서버는 accept() 시스템 콜을 호출하여 클라이언트의 접속요청을 기다리는데 프로그램의 진행은 accept() 시스템 콜에서 멈추게 된다. 이 때 어떤 클라이언트의 접속요청이 수신되면 accept()는 새로운 소켓(client_fd)을 생성하여 리턴한다.

▶ accept()는 또한 두번째 인자로 지정된 소켓주소 구조체 client_addr에, 접속된 클라이언트의 소켓주소 정보를 기록하여 리턴한다.

client_fd = accept(server_fd, (struct sockaddr *)&client_addr, int &len);

 

▶ 서버는 클라이언트가 보내오는 메시지를 read()로 읽고 write()로 echo해 준 다음 소켓 client_fd를 닫는다.

myecho_server.c 프로그램 리스트


현재강좌 : UNIX BSD 소켓 시스템 콜 이전: 2.3 클라이언트 프로그램 작성절차 다음: 2.5 소켓 관련 UNIX 시스템 콜