본문 바로가기

UNIX_LINUX_C_C++

[펌] 제4장 UNIX 소켓 응용 프로그래밍

소강좌제목 : 컴퓨터 네트웍 프로그래밍 이전 강좌: 제3장 클라이언트 서버 토크 프로그램 다음강좌: 제5장 Winsock 프로그래밍


제4장 UNIX 소켓 응용 프로그래밍

4장에서는 소켓 응용 프로그램 작성법을 소개한다. 먼저 소켓을 비동기 모드로 사용하는 기술을 배우고 이의 응용 프로그램으로 채팅 프로그램을 구현하는 것을 설명한다. 다음에는 하나의 프로그램에서 두가지 서비스를 동시에 제공하는 멀티서버 구현기술을 소개하고, 소켓관련 시스템 콜들을 처리하는데 걸리는 시간을 측정하는 프로그램을 소개한다. 끝으로 네트웍을 통하여 데이타베이스를 엑세스하는 응용 프로그램을 작성한다.

4.1 소켓의 동작모드

▶ 소켓의 동작모드에는 blocking, non-blocking 그리고 비동기(asynchronous) 모드 세 가지가 있으며 소켓을 처음 생성하면 blocking 모드의 소켓이 생성된다.

▶ blocking 모드의 소켓이란 이 소켓에 어떤 소켓 관련 시스템 콜을 호출하였을 때 네트웍 시스템(즉, TCP/IP)이 동작을 완료할 때까지 응용 프로세스가 멈추어 있게(block) 되는 소켓을 말한다.

▶ 응용 프로그램에서는 필요에 따라 이 blocking 모드의 소켓을 non-blocking 모드 또는 비동기 모드로 변경하여 사용하여야 한다.

▶ Non-blocking 모드의 소켓이란, 소켓 관련 시스템 콜에 대하여 네트웍 시스템이 일단 결과를 바로 리턴하여 응용 프로그램이 block되지 않게 하는 소켓을 말한다.

▶ 소켓관련 시스템 콜 중에 block될 수 있는 것은 listen(), connect(), accept(), send(), recv(), close() 등이다.

▶ 한편 비동기 모드는 non-blocking 모드에서처럼 block될 수 있었던 소켓 시스템 콜에 대해서 일단 리턴을 하고 시스템 콜의 해당 동작이 완료되면 비동기적으로(asynchronously) 그 결과를 응용 프로그램에게 알려주는 소켓의 동작 모드이다.

▶ 4.2절과 4.3절에서 각각 소개할 채팅 서버와 클라이언트 프로그램에서는 여러 가지 I/O 작업을 병행하여 처리하는 것이 필요하다.

▶ 이때 select() 시스템 콜을 사용하여 소켓을 비동기 모드로 사용하는 것에 대하여 자세히 설명한다.

▶ 한편 소켓의 동작모드 변경은 윈도우 소켓(윈속) 프로그래밍에서도 자세히 다룬다(5.1.1절과 5.2.2절 참조).

4.2 채팅 서버 프로그램

▶ 채팅 서버(chat_server.c)는 임의의 클라이언트가 채팅에 참가하는 요청을 하면 이를 채팅 참가 리스트에 추가하며, 채팅에 참가하고 있는 클라이언트들이 보내오는 메시지를 모든 채팅 참가자에게 다시 방송하는 기능을 수행한다.

4.2.1 채팅 서버 구조

▶ 채팅 서버는 임의의 클라이언트로부터의 채팅 참가요구를 처리하면서 동시에 어떤 클라이언트가 보내온 채팅 메시지를 모든 클라이언트에게 방송하는 일을 처리하여야 한다.

▶ 이와같이 서버가 두 가지 일을 병행하여 처리하기 위해서 새로운 클라이언트가 접속될 때마다 프로세스를 만들는 concurrent 서버로 구현하는 방법이 가능하다.

▶ 그러나 처리해야 할 작업이 생길 때마다 프로세스를 만들어 나가는 방식은 프로그램 작성은 편리하지만 시스템 자원의 활용면이나 프로그램의 안정적인 동작면에서는 불리한 경우가 많다.

▶ 여기서 소개하는 채팅 서버 프로그램 chat_server.c에서는 apparent concurrent 서버 구조를 사용한다.

▶Apparent concurrent 서버는 concurrent 서버처럼 여러 클라이언트에게 병행하여 서비스를 제공하면서도 프로세스를 클라이언트 수만큼 계속 복제하지 않는 방식이다.

▶ 본 예제에서는 하나의 프로세스가 여러 클라이언트와의 통신을 담당하도록 한다.

▶ 한편 채팅 서버 프로그램은 클라이언트의 접속요청을 처리하는 동시에 다른 클라이언트들이 보내온 메시지를 모든 채팅 참가자 클라이언트에게 전달해야 하므로 프로세스가 어느 한 곳에 멈추어 있을 수 있는 blocking 모드로 동작하면 안 된다.

▶ 따라서 소켓을 통한 I/O를 비동기 모드로 처리하여야 하는데 이를 위하여 select() 시스템 콜을 사용한다.

▶ 그림 4-1에 apparent concurrent 서버 모델을 이용한 채팅 서버의 동작을 나타냈다.

▶ 서버는 먼저 socket()을 호출하여 채팅 참가자를 접수할 소켓을 개설하고(이 소켓을 초기소켓이라고 부르겠다) 이 소켓을 자신의 소켓주소와 bind()해 둔다.

▶ 다음에 이 초기소켓을 대상으로 select()를 호출하여 초기소켓에 어떤 I/O 변화가 생길 때까지 기다린다.

그림 4-1 apparent concurrent 모델의 채팅 서버

▶ 초기소켓에서 처음 발생할 수 있는 I/O 변화는 채팅 참가자가 연결요청을 보내왔을 때인데 이때 서버는 accept()를 호출하여 새로운 참가자 접속을 처리하고 accept()가 리턴하는 소켓번호를 참가자 리스트(client_s[])에 등록시킨다.

▶ 서버는 이 새로 생긴 소켓과 초기소켓을 대상으로 하여 다시 select()를 호출한다.

▶ select() 함수의 기능은 소켓에서 발생하는 I/O 변화를 기다리다가 지정된 I/O 변화가 감시 대상 소켓들 중 하나에서라도 발생하면 select() 문이 리턴된다.

▶ 응용 프로그램에서는 select()가 리턴되었을 때 어떤 소켓에서 어떤 I/O 변화가 발생하였는지를 확인하여 필요한 작업을 처리하면 된다.

▶ 그림 4-1에 서버가 초기소켓과 하나 이상의 채팅 참가자 리스트(client_s[])를 대상으로 다시 select()를 호출하고 select() 문이 리턴되면 이것이 초기소켓에서 발생한 것인지(즉, 새로운 채팅 참가자가 연결요청을 한 것인지) 아니면 기존의 참가자 중 누군가 채팅 메시지를 보낸 것인지를 구분하여 필요한 기능을 수행하는 것을 나타냈다.

▶ 그림 4-2에 채팅 프로그램의 전체적인 구성을 나타냈다.

▶ 서버는 초기소켓 s를 통하여 새로운 채팅 참가자를 접수(accept)하며 새로 참가한 클라이언트들의 소켓번호는 배열 client_s[]에 들어 있게 된다.

▶ 예를들어 그림 4-2에는 현재 4명의 채팅 참가자가 있는데 서버 프로그램은 초기소켓 s를 포함하여 모두 다섯 개의 소켓에서 발생하는 I/O 변화를 감지하도록 select()를 호출하여야 한다.

그림 4-2 채팅 서버와 클라이언트의 연결관계

(s: 초기소켓, ,client_s[]: 채팅 참가자의 소켓 번호 배열)

4.2.2 select() 시스템

▶ select() 시스템 콜의 사용 문법은 다음과 같다.

int select (

int maxfdp1, /* 최대 파일( 소켓)번호 크기 + 1 */

fd_set *readfds, /* 읽기상태 변화를 감지할 소켓 지정 */

fd_set *writefds, /* 쓰기상태 변화를 감지할 소켓 지정 */

fd_set *exceptfds, /* 예외상태 변화를 감지할 소켓 지정 */

struct timeval *tvptr); /* select() 시스템 콜이 기다리는 시간 */

▶ select()의 첫번째 인자 maxfdp1은 'I/O 변화를 감시할 총 소켓의 개수 +1'의 값을 지정하여야 하는데 보통 현재 open된 소켓번호 중 가장 큰 값에 1을 더한 값을 사용한다.

▶ 한편 시스템내에서 개설할 수 있는 파일기술자의 최대값은 <sys/types.h>에 FD_SETSIZE로 정의되어 있다.

▶ fd_set(file descriptor set) 타입의 인자 readfds, writefds, exceptfds는 각각 읽기, 쓰기, 예외상황 발생과 같은 I/O 변화가 발생하였을 때 이를 감지할 대상이 되는 소켓들을 지정하는 배열형 구조체이다.

▶ 즉, 이 세 가지 구조체를 통하여 어떤 소켓에서 어떤 I/O 변화 발생을 감지할지를 선택하여 지정할 수 있다(뒤에서 자세히 설명함).

▶ 마지막 인자 tvptr은 select() 시스템 콜이 기다리는 시간을 지정하는데 tvptr이 NULL인 경우에는 지정한 I/O 변화가 발생할 때까지 계속 기다리며, 0인 경우에는 기다리지 않고 바로 리턴되고 그 외의 값인 경우에는 지정된 시간만큼 또는 도중에 I/O 변화가 발생할 때까지 기다린다.

▶ fd_set 타입의 구조체(위에서 readfds, writefds, exceptfds)와 소켓번호와의 관계는 그림 4-3과 같다.

▶ 예를들어 그림 4-3에서 readfds 구조체의 소켓번호 0번과 3번이 1로 세트되어 있으므로 키보드나(파일번호 0번), 소켓번호 3에서 어떤 데이터가 입력되어 응용 프로그램이 이를 읽을 수 있는 상태가 되면 select()문이 리턴된다.

▶ 한편 writefds에서는 소켓번호 1번과 3번이 1로 세트되어 있으므로 파일기술자 1번(표준 출력)이나 소켓번호 3번이 write를 할 수 있는 상태로 변하면(예를들면 송신 버퍼가 비워짐) select()문이 리턴된다.

▶ 이와같이 각각 fd_set 타입 구조체에 I/O변화를 감지할 소켓(또는 파일)번호를 1로 세트하여 select()를 호출해 두면 해당 조건이 만족되는 순간 select()문이 리턴된다.

▶ 한편 UNIX에서는 fd_set 타입의 구조체를 편리하게 처리할 수 있도록, 예를들면 특정 소켓의 I/O 변화 감지를 쉽게 선택하거나 취소할 수 있도록 매크로를 제공하고 있다(표 4-1 참조).

그림 4-3 id_set 타입의 구조체와 소켓번호와의 관계

FD_ZERO(fd_set *fdset)*fdset의 모든 비트를 지운다.
FD_SET(int fd, fd_set *fdset)*fdset 중 소켓 fd에 해당하는 비트를 1로 한다.
FD_CLR(int fd, fd_set *fdset)*fdset 중 소켓 fd에 해당하는 비트를 0으로 한다.
FD_ISSET(int fd, fd_set *fdset)*fdset 중 소켓 fd에 해당하는 비트가 세트되어 있으면 양수값인 fd를 리턴한다.

표 4-1 fd_set를 사용하기위한 매크로

▶ select()를 호출하려면 먼저 fd_set 구조체를 만들어야 하는데 본 절에서 소개할 chat_server.c의 경우에는 읽기에 대한 I/O 변화만 확인하면 되므로 fd_set 타입의 구조체 read_fds 하나만 선언하고 FD_ZERO(&read_fds)를 호출하여 read_fds의 모든 소켓번호 위치를 disable시킨다.

▶ 다음에는 I/O 변화에 관심을 갖는 소켓번호(또는 파일기술자)들을 선택하여야 하는데, chat_server.c에서는 클라이언트와의 접속 요구를 처리할 초기소켓 s와 각 채팅 클라이언트마다 배정된 소켓번호들의 배열 client_s[] 두 가지의 소켓이 있다.

▶ 현재 채팅에 참가하고 있는 클라이언트 수를 num_chat이라고 하면 아래와 같이 FD_ZERO를 사용하여 read_fds를 초기화하고 FD_SET를 사용하여 I/O 변화를 감지할 소켓들을 선택한다.

fd_set read_fds;

FD_ZERO(&read_fds); /* *read_fds의 모든 소켓을 0으로 초기화 */

FD_SET(s, &read_fds); /* 초기소켓 선택 */

for(i = 0; i < num_chat; i++) /* 모든 클라이언트 접속 소켓 선택 */

FD_SET(client_s[i], &read_fds);

▶위에서 초기화한 read_fds를 select()의 두번째 인자(즉, 읽기 변화 감지용 fd_set)로 지정하고, 쓰기 및 예외 발생에 해당하는 fd_set는 NULL로 지정하여 다음과 같이 select()를 호출한다.

select(maxnfdsp1, &read_fds, (fd_set *)0, (fd_set *)0, (struct timeval *)0);

▶ 한편 select() 문의 첫번째 인자 'I/O 변화를 감시할 총 소켓의 개수+1'의 값으로는 현재 개설된 '최대 소켓번호 +1'을 사용하였다.

▶ 위의 select()는 두 가지 종류의 입력에 대하여 리턴되므로 select()가 리턴되면 어떤 입력이 발생하였는지를 판단하여야 하며 이를 위하여 FD_ISSET 매크로를 사용한다.

▶ FD_ISSET의 사용법을 아래에 보였는데 매크로 FD_ISSET는 read_fds 구조체에서 소켓번호 s에서 I/O 변화가 있었으면 양수값인 소켓번호 s를 리턴한다.

FD_ISSET(s, &read_fds);

 

▶ 아래에서 FD_ISSET 실행 결과가 양수이면 클라이언트로부터 연결요청(즉, 채팅 참가요청)이 온 것이므로 accept()를 불러 채팅 참가요청을 처리한다.

▶ 한편 client_s[] 중 하나의 소켓이 세트되었다면 어떤 채팅 참가자가 채팅 메시지를 전송한 것이므로 이 메시지를 받아 모든 참가자에게 방송하면 된다.

if (FD_ISSET(s, &read_fds)) {

/* 초기소켓 s에서 입력 발생 */

clilen =sizeof(client_addr);

client_fd = accept(s, (struct sockaddr *)&client_addr, &clilen);

}

/* 클라이언트 소켓번호 배열 client[] 차례로 검색 */

for(i = 0; i < num_chat; i++) {

if (FD_ISSET(client_s[i], &read_fds)) {

/* 소켓 client_s[i]에서 채팅 메시지 수신 */

/* 모든 참가자에게 채팅 메시지 방송 */

}

}

한편 클라이언트가 채팅에서 탈퇴하려면 종료문자(: exit) 보내도록 하였고 서버는 종료문자를 수신한 경우 이를 확인하고(exitCheck() 함수 사용) client_s[] 배열에서 해당 클라이언트의 소켓번호를 삭제한 채팅 참가자 num_chat 1 감소시킨다.

▶ 아래는 채팅 서버를 수행한 결과를 보이고 있다.

# chat_server 4001

대화방 서버 실행..

4.2.3 프로그램 주요부분 설명

▶ chat_server.c에서 소켓을 개설할 때 인터넷 프로토콜 체계(PF_INET)를 사용하였으며 스트림 형태의(TCP) 프로토콜을 선택하였다.

▶ 아래는 소켓을 만들고 소켓에 주소를 부여하는(bind) 과정을 보였는데 소켓주소 구조체의 내용을 널(NULL)로 초기화하기 위하여 함수 bzero()를 사용하였다.

▶ IP 주소로는 현재 채팅 서버 프로그램이 실행되는 호스트의 IP 주소를(INADDR_ANY), 그리고 서비스를 제공할 포트번호는 서버 프로그램 실행시 사용자가 입력한 값(예를들면 4001)을 사용하였다.

s = socket(PF_INET, SOCK_STREAM, 0);

bzero((char *)&server_addr, sizeof(server_addr));

server_addr.sin_family = PF_INET;

server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

server_addr.sin_port = htons(atoi(argv[1]));

▶ 아래는 어떤 클라이언트가 보낸 메시지를 다른 모든 클라이언트에게 전송하는 동작을 나타냈다.

▶ 여기서 배열 client_s[]에는 각 채팅 참가자들과 연결되는 소켓번호들이 들어 있다.

▶ 첫번째 for() 루프는 현재 채팅에 참가하는 모든 클라이언트를 대상으로 보내온 채팅 메시지가 있는지 검색하며, 두번째 for() 루프에서는 채팅 메시지를 모든 클라이언트에게 방송한다

/* 채팅 메시지가 도착했는지 검사 */

for(i = 0; i < num_chat; i++) {

if(FD_ISSET(client_s[i], &read_fds)) {

if((n = recv(client_s[i], rline, MAXLINE,0)) > 0) {

rline[n] = '\0';

/* 종료문자 입력시 채팅 탈퇴 처리 */

if(exitCheck(rline, escapechar, 5) == 1) {

shutdown(client_s[i], 2);

/* client_s[] 크기 조절 */

if(i != num_chat - 1)

client_s[i] = client_s[num_chat - 1];

num_chat--;

continue;

}

/* 모든 채팅 참가자에게 메시지 방송 */

for(j = 0; j < num_chat; j++)

send(client_s[j], rline, n, 0);

printf("%s", rline);

}

}

}

한편 3장에서 소개한 토크 프로그램과 달리 채팅 프로그램에서는 채팅 참가자(클라이언트)들이 키보드에서 입력한 문자열만 서버로 전송하는 것이 아니라 자신(채팅 참가자) 이름을 다음과 같이 메시지 앞에 붙여서 전송한다.

[철수] hello everybody ?

 

따라서 클라이언트가 채팅을 탈퇴하기 위하여 exit 같은 종료문자를 입력하면 서버는 이것을 찾아내기 위하여 클라이언트가 전송한 메시지 전체를 검색하여 종료문자열이 들어 있는지를 확인하여야 한다.

chat_server.c에서는 이것을 처리하기 위하여 사용자 정의 함수 exitCheck() 사용한다.

chat_server.c 프로그램 리스트

4.3 채팅 클라이언트 프로그램

4.3.1 프로그램 개요

▶ 채팅 클라이언트 프로그램(chat_client.c)은 사용자의 입력 메시지를 서버로 전송하고, 서버가 보내온 모든 메시지를 사용자 화면에 출력한다.

▶ 즉, 3.1절의 토크 클라이언트 프로그램 talk_client.c와 유사한 동작을 한다.

▶ 그러나 talk_client.c에서는 사용자의 키보드 입력 처리와 수신 메시지 출력 두 가지 일을 동시에 수행하기 위해서 fork()를 이용하여 두 개의 프로세스를 만들어 각 프로세스가 이 두 가지 일을 담당하였으나, chat_client.c에서는 chat_server.c에서와 같이 select() 시스템 콜을 이용하여 소켓을 비동기 모드로 바꾸어 두 가지 입출력을 하나의 프로세스에서 처리하도록 구현하였다.

▶ chat_client.c가 수행되기 위하여는 4.2절에서 설명한 채팅 서버 프로그램 chat_server.c가 서버에서 먼저 실행되어야 하며 chat_server.c가 사용하는 포트번호와 서버의 IP 주소를 클라이언트에서 알고 있어야 한다.

▶ 또한 클라이언트에서는 채팅 참가자의 이름을 모든 메시지 앞에 붙여서 서버로 전송하여야 하기 때문에 자신의 이름을 프로그램 실행시 입력하도록 하였다.

chat_client server_IP server_Port my_name

▶위에서 server_IP 채팅 서버의 IP 주소 (dotted deci- mal)이고 server_Port 채팅 서버 프로그램(chat_server .c) 실행시 지정한 포트번호이며 my_name 채팅에서 사용할 자신의 이름이다.

▶ 아래는 chat_client.c의 실행 예이다.

# chat_client 203.252.65.3 4001 철수

접속에 성공했습니다..

대화방에 오신걸 환영합니다...

4.3.2 프로그램 주요부분 설명

▶ chat_client.c에서는 먼저 채팅 메시지 앞에 항상 붙여서 전송할 자신의 이름(argv[3])을 구조체 Name에 기록해 둔다.

struct Name {

char n[20]; /* 채팅에서 사용할 이름*/

int len; /* 이름의 실제 크기 */

} name;

/* 참가자 이름을 Name 구조체에 기록 */

sprintf(name.n, "[%s]", argv[3]);

name.len = strlen(name.n);

▶ chat_client.c에서 select()를 호출하려면 먼저 fd_set 구조체를 만들어야 하는데 chat_client.c에서는 읽기에 대한 I/O만 확인하면 되므로 fd_set 구조체로 read_fds 하나만 선언한다.

▶ FD_ZERO(&read_fds)를 호출하여 read_fds의 모든 소켓번호를 disable시킨다.

▶ 다음에는 I/O 변화에 관심을 갖는 소켓번호(파일기술자)들을 세트하여야 하는데, chat_client.c에서는 키보드 입력을 위한 파일기술자 0과 서버와 접속되어 있는 소켓 s 두 개의 파일기술자가 있다.

▶ 따라서 다음과 같이 FD_SET를 사용하면 된다.

fd_set read_fds; /* 읽기 변화를 감지하기 위한 fd_set 구조체 선언 */

FD_ZERO(&read_fds) /* 초기화 */

FD_SET(0, &read_fds); /* 키보드 입력용 파일기술자(0) 세트 */

FD_SET(s, &read_fds); /* 서버와 연결된 소켓번호(s) 세트 */

▶ select()를 호출할 때, read_fds를 select()의 두번째 인자(즉, 읽기 변화 감지용 fd_set)로 지정하고, 세번째와 네번째 인자 즉, 쓰기 및 예외발생에 해당하는 fd_set는 NULL로 지정한다.

▶ 한편 select() 문의 첫번째 인자로 사용할 '최대 소켓번호 +1'의 값으로는 s+1을 사용하였다.

select(nfds, &read_fds, (fd_set *)0, (fd_set *)0, (struct timeval *)0);

▶ select()가 리턴되었을 때, read_fds 내의 소켓번호 s 값이 세트되어 있다면 서버에서 메시지를 보내온 것이므로 이 메시지를 읽어 화면에 출력한다.

▶ 파일기술자 0이 세트된 경우는 자신의 키보드 입력이 발생한 것이므로 이 메시지를 서버에게 전송하면 된다.

if (FD_ISSET(s, &read_fds)) /* 서버가 보내오는 메시지를 수신하여 출력 */

if (FD_ISSET(0, &read_fds)) /* 키보드 입력 데이터를 서버로 전송 */

chat_client.c 프로그램 리스트

4.4 멀티서버

▶ 멀티서버란 하나의 프로세스에서 두 가지 이상의 서비스를 동시에 제공하거나, 동일한 서비스를 두가지 이상의 프로토콜(즉, TCP와 UDP)로 제공하는 서버를 말한다.

▶ 멀티서버의 장점은 하나의 프로세스가 하나의 서비스를 제공하는 것에 비해 시스템의 자원을 적게 사용하면서 동일한 서비스를 제공한다는 것이다.

▶ 본 절에서는 TCP를 통해 두 개의 서비스 echo와 daytime을 제공하는 멀티서버 프로그램을 작성하며, 두 가지 서비스에 대한 요청을 구분하기 위해 select() 시스템 콜을 사용한다.

4.4.1 프로그램 개요

▶ 본 절에서 소개할 멀티서버 프로그램 multi_server.c에서는 두 가지 서비스 echo와 daytime를 제공하기 위하여 두 개의 소켓을 사용한다.

▶ 즉, echo 서비스는 소켓번호 echo_fd를, daytime 서비스는 소켓번호 daytime_fd를 아래와 같이 만들어 사용한다.

/* echo 서비스를 위한 소켓 생성 */

echo_fd = socket(PF_INET, SOCK_STREAM, 0);

/* daytime 서비스를 위한 소켓 생성 */

daytime_fd = socket(PF_INET, SOCK_STREAM, 0);

▶ 소켓을 생성한 다음 무한루프를 돌며 클라이언트의 서비스 요구를 기다린다.

▶ FD_SET 매크로를 사용하여 소켓 echo_fd와 daytime_fd를 fd_set 타입의 구조체 read_fds에 지정하며 향후에 이 소켓에서 읽기 변화가 발생하면 select() 문이 리턴되도록 한다.

▶ 프로그램은 echo_fd 또는 daytime_fd가 읽기 가능한 상태가 될 때까지 select() 문에서 기다린다.

while(1) {

FD_SET(echo_fd, &read_fds);

FD_SET(daytime_fd, &read_fds);

select(nfds, &read_fds, (fd_set *)0, (fd_set *)0, (struct timeval *)0);

}

▶ select() 문이 리턴되었을 때 어떤 서비스 요구가 발생하였는지를 구분하기 위하여 FD_ISSET 매크로를 사용한다.

▶ FD_ISSET는 read_fds 구조체에서 해당 소켓이 세트되어 있으면 양수값인 소켓번호를 리턴한다.

/* echo 클라이언트가 접속을 요구해 경우 */

if(FD_ISSET(echo_fd, &read_fds))

echo_process(s); /* echo 서비스 수행 */

/* daytime 클라이언트가 접속을 요구해 경우 */

if(FD_ISSET(daytime_fd, &read_fds))

daytime_process(s); /* daytime 서비스 수행 */

▶ echo_process()는 echo를 처리하도록 멀티서버에서 구현한 사용자 정의 함수로서 소켓번호 echo_fd를 함수 인자로 받으며 소켓을 통해 입력된 문자열을 그 소켓으로 그대로 출력한다.

▶ 아래는 echo_process() 구현 부분이다.

int echo_process(int echo_fd) {

while((len = read(echo_fd, buf, sizeof(buf))) != 0)

write(echo_fd, buf, len);

return 0;

}

▶ daytime_process()는 소켓번호 daytime_fd를 인자로 받으며, time() 시스템 콜을 이용하여 시스템의 현재 시각을 얻는다.

▶ctime() 시스템 콜을 이용하여 아래와 같은 형태의 시각을 나타내는 문자열로 변경한 후 이 문자열을 클라이언트로 전송한다 (time()에 관하여는 4.5절에서 다시 설명한다).

Wed May 21 12:45:35 1997

 

▶ 아래는 daytime_process() 구현 부분이다.

int daytime_process(int daytime_fd) {

time(&now);

sprintf(buf, "%s\n", ctime(&now));

write(daytime_fd, buf, strlen(buf));

return 0;

}

multi_server.c 프로그램 리스트

4.5 소켓 함수 처리시간 측정

▶ 본 절에서는 UNIX 커널이 제공하는 시간 관련 함수의 사용법에 관해 알아보고, 이를 이용하여 각종 소켓 관련 시스템 콜의 수행에 소요되는 시간을 측정하는 프로그램을 작성한다.

4.5.1 UNIX 시간측정 함수

▶ UNIX가 제공하는 대표적인 시간측정 함수는 time()인데 time()은 1970년 1월 1일 0시부터(이를 Epoch라고 한다) 현재까지 경과된 시간을 초단위로 정수값으로 리턴한다.

▶ time() 함수가 리턴한 값을 Calendar time이라 하는데 이는 초단위로 누적된 값이므로 사용자가 쉽게 알아볼 수 있는 값이 아니다. 따라서 시간을 쉽게 인식할 수 있도록 표현해 주는 여러 시스템 콜이 제공되고 있다.

▶ 먼저 ctime() 시스템 콜은 time_t 형 포인터를 인자로 받아, 날짜와 시간을 나타내는 문자열로 변환해 준다. 예를들어 아래 프로그램 코드는 현재의 날짜와 시간을 화면에 출력한다.

#include <time.h>

void main(void) {

time_t now;

time(&now);

printf("Now : %s", ctime(&now));

}

▶ 위 프로그램의 실행 결과 예는 다음과 같다.

Now : Fri June 6 12:43:39 1997

 

▶ 한편 초단위가 아니라 nano second 단위의 시간을 측정하기 위해서는 clock_gettime()이 주로 사용되는데 clock_ gettime()의 선언은 다음과 같다.

#include <time.h>

int clock_gettime(clockid_t clock_id, struct timespec *tp);

typedef struct timespec {

time_t tv_sec; /* second () 단위 */

long tv_nsec; /* nano second 단위 */

} timespec_t;

▶ clock_gettime()의 첫번째 인자 clock_id로 CLOCK_REALTIME을 선택하여 시스템 콜을 호출하며 두번째 인자인 구조체 timespec에 January 1, 1970 00:00시 이후의 시간이 초단위와 nano second [nsec] 단위로 기록된다.

4.5.2 소켓 함수 처리시간 측정 프로그램

▶ 여기서는 소켓에 관련된 여러 함수들을 수행하는데 소요되는 시간을 clock_gettime() 시스템 콜을 이용하여 측정하는 예제 프로그램 socket_delay.c를 소개한다.

▶ 다음은 socket_delay.c의 실행 결과로 socket(), connect() 등의 소켓 관련 함수를 실행하는데 걸리는 시간을 측정한 결과를 보여주고 있다.

# socket_delay

For 'socket' call : 8045500 nsec

For 'connect' call : 3622500 nsec

For 'write' call : 222000 nsec

For 'read' call : 23732000 nsec

For 'close' call : 657500 nsec

#

socket_delay.c에서는 시간을 측정하기 위하여 timespec 타입 변수의 배열 myclock[2] 사용하는데 myclock[0]에는 어떤 소켓 함수를 호출하기 직전의 시각을 기록하고 myclock[1]에는 소켓 함수를 수행한 직후의 시각을 기록한 다음 이들의 시간차이를 계산해 주는 사용자 정의 함수 calclock() 호출한다.

▶예를들어 socket() 함수의 처리 시간은 다음과 같이 측정한다.

struct timespec myclock[2];

long nano_time;

clock_gettime(CLOCK_REALTIME, &myclock[0]);

s = socket(PF_INET, SOCK_STREAM, 0);

clock_gettime(CLOCK_REALTIME, &myclock[1]);

nano_time = calclock( myclock );

printf("For 'socket' call : %10ld nsec \n", nano_time);

▶ 위에서 calclock()은 myclock[] 배열에 들어 있는 두 개의 시각의 차이를 [nsec] 단위로 계산하여 리턴하는 사용자 정의 함수로 아래에 calclock() 코드를 나타냈다.

/* 배열 myclock[]로부터 시간차를 구해서 리턴한다 */

long calclock (struct timespec *myclock) {

long nano_time, temp, temp_n;

if (myclock[1].tv_nsec >= myclock[0].tv_nsec) {

temp = myclock[1].tv_sec - myclock[0].tv_sec;

temp_n = myclock[1].tv_nsec - myclock[0].tv_nsec;

nano_time = 1000000000 * temp + temp_n;

} else {

temp = myclock[1].tv_sec - myclock[0].tv_sec - 1;

temp_n = 1000000000 + myclock[1].tv_nsec - myclock[0].tv_nsec;

nano_time = 1000000000 * temp + temp_n;

}

return nano_time;

}

socket_delay.c 프로그램 리스트

4.6 네트웍 데이터베이스 액세스 응용

▶ 4.6절에서는 네트웍을 통하여 서버에 있는 데이터베이스(DB: database) 자료를 검색하거나 새로운 자료를 추가하는 프로그램을 작성한다.

▶ 데이터베이스에 자료를 입출력하기 위해서 DBMS(DataBase Management System)와 SQL(Structured Query Language)문을 사용한다.

▶ 본 예제의 서버 프로그램에서는 SQL을 지원하는 c 언어인 ESQL/C을 사용한다.

4.6.1 표준 SQL

▶ SQL은 설치된 DBMS 종류에 무관하게 DB 응용 프로그램을 작성할 수 있도록 하는 명령문의 집합으로 표준 SQL이 제공하는 주요 명령문과 기능을 표 4-2에 정리하였다.

구분

SQL 문

사용법

기 능

자 료

정의문

CREATE DATABASECREATE DATABASE <DB명>DB를 새로 만든다.
DATABASEDATABASE <DB명> [EXCLUSIVE]사용할 DB를 선택한다.

[EXCLUSIVE] 옵션은 현재 사용자만 DB에 접근할 수 있도록 한다.

CREATE TABLECREATE TABLE <테이블명>

(<칼럼명>, <자료형태> )

현행 DB에 새로운 테이블을 생성한다.
DROP TABLEDROP TABLE <테이블명>현행 DB에서 지정한 테이블을 삭제한다.
DROP DATABASEDROP DATABASE <DB명>지정한 DATABASE를 삭제한다.

자 료

처리문

DELETEDELETE FROM <테이블명>

WHERE <칼럼명> = <값>

FROM에 지정한 테이블에서 WHERE 조건을 만족하는 행을 삭제한다.
INSERTINSERT INTO <테이블명> ( <칼럼명>) VALUES (<값>)INTO에 지정한 테이블에 새로운 행을 추가한다.
SELECTSELECT <칼럼명>

FROM <테이블명>

WHERE <칼럼명> = <값>

FROM에 지정한 테이블에서 WHERE 조건을 만족하는 행을 선택한다.
UPDATEUPDATE <테이블명>

SET <칼럼명> = <값>

WHERE <칼럼명> = <값>

UPDATE에 지정한 테이블에서 WHERE 조건을 만족하는 행의 내용을 SET 절에서 지정한 것과 같이 변경한다.

표 4-2 주요 표준 SQL 문

▶ 예를들어 새로운 DB를 하나 만들 때에는 CREATE DATA- BASE문을 사용한다.

▶ DB내에 새로운 테이블을 생성할 때에는 CREATE TABLE문을 이용하고 테이블에 어떤 항목을 추가하려면 INSERT문을 사용한다.

▶ 아래는 UNIV(대학)이라는 DB를 새로 만들고 여기에 DEPT(부서)라는 테이블을 만든 후 DEPT의 ID, dept_ID가 01이고 부서명 dept_Name이 '총무과'인 데이터 항목을 추가하는 표준 SQL 문을 나타냈다.

▶ 아래의 테이블 DEPT에서 dept_ID는 두 글자이고, dept_ Name을 20자로 지정하였다.

CREATE DATABASE UNIV

CREATE TABLE DEPT (dept_ID char(2), dept_Name char(20))

INSERT INTO DEPT VALUES ('01', '총무과')

▶ 테이블내의 어떤 데이터 항목 값을 바꿀 때는 UPDATE를 사용하는데 다음은 DEPT 테이블에서 dept_ID가 01인 행의 dept_Name을 '경리과'로 변경하는 예제이다.

UPDATE DEPT

SET dept_Name = '경리과'

WHERE dept_ID = '01'

▶ 테이블에서 어떤 칼럼이 어떤 조건을 만족하는 데이터를 검색하기 위해서는 SELECT를 이용한다.

▶ SELECT 뒤에는 출력할 칼럼의 목록이, FROM 뒤에는 검색할 테이블 명이, 그리고 WHERE 뒤에는 검색 조건을 지정한다.

▶ 예를들어 테이블 DEPT에서 dept_Name 칼럼이 '총무과'인 행을 검색하기 위한 SQL문은 다음과 같다.

SELECT dept_Name, dept_ID

FROM DEPT

WHERE dept_Name = '총무과'

4.6.2 ESQL/C

▶ ESQL/C는 c 언어 프로그램에서 SQL 명령문을 편리하게 사용할 수 있도록 기능을 확장한 언어로 c 언어의 일반적인 기능을 지원하며 SQL문을 사용할 수 있는 함수가 추가되었다(ESQL/C로 작성한 파일은 .ec확장자를 갖는다).

▶ ESQL/C 프로그램 내에서 SQL문은 $로 시작해야 하는데 예를들어 DEPT라는 테이블에 dept_ID 값이 01인 '총무과' 항목을 추가하려면 다음과 같이 하면 된다.

$insert into DEPT values ( '01', '총무과');

 

▶ 또는 $대신 exec sql을 사용할 수도 있다.

exec sql insert into DEPT values ( '01', '총무과');

 

▶ SQL문 내에서 c 언어 프로그램에서 정의한 변수들을 사용할 수 있는데 이러한 c 언어 변수를 호스트 변수라고 한다.

▶ 호스트 변수를 선언할 때에는 변수 타입 앞에 $를 붙여야 하며, SQL문에서 이 변수를 사용할 경우에는 : 를 붙여야 한다.

▶ 예를들어 호스트 변수를 사용하여 DEPT 테이블에 '총무과'를 추가하기 위해서는 다음과 같이 하면 된다.

▶ 아래에서 호스트 변수는 id와 name 두 개이다.

# ID_LEN 2

# NAME_LEN 20

$char id[ID_LEN], name[NAME_LEN];

strcpy(id, "01");

strcpy(name, "총무과");

$insert into DEPT values ( :id, :name ) ;

▶ 표 4-3에 SQL 문에서 사용하는 변수의 종류와 이를 지원하기 위한 ESQL/C의 호스트 변수들을 정리하였다.

SQL 변수

호스트 변수

설 명

CHAR(n),

CHARACTER(n)

char[n+1],

char*

1부터 32,767까지 가변 길이의 문자열을 저장한다. C언어에서 문자열은 널(NULL)로 끝나야 하므로, CHAR(n) 값을 받는 호스트 배열은 (n+1) 크기를 가져야 함
SMALLINTshort int-32,767에서 32,767의 범위를 갖는 정수형
INTEGERlong int-2,147,483,647에서 2,147,483,647의 범위를 갖는 정수형
SMALLFLOAT REALfloat7자리의 유효 자리수를 갖는 부동 소수점 수
FLOAT,

DOUBLE PRECISION

double14자리의 유효 자릿수를 갖는 부동 소수점 수
SERIALlong intSERIAL 자료형태는 사용자가 INSERT 문을 실행할 때 ESQL/C가 자동으로 배정하는 순차 정수
DATElong int1899년 12월 31일 이후의 날짜를 표현하며 4바이트 int에 저장
DECIMAL

DEC

NUMERIC

dec_t,

struct decimal

소수점의 유무에 관계없이 32개의 유효숫자를 갖는 수를 나타냄
MONEYdec_t,

struct decimal

MONEY(m,n)의 형태로 나타내며, 고정된 정밀도 m과 소수자리 수 n을 의미
DATETIMEdtime_t,

struct dtime

한 시점을 "날짜 시:분:초"로 나타냄
INTERVALintrvl_t, struct intrvl기간을 나타내는 변수

표 4-3 호스트 변수 종류

▶ ESQL/C에서는 처음 SQL문을 실행하기 전에 database 문을 사용하여 앞으로 사용할 데이터베이스를 선언해야 한다.

▶ 프로그램을 종료하기 전에 close database 문으로 선택했던 데이터베이스를 닫아야 한다.

▶ 예를들어 Test_DB라는 DB를 선택하고 닫으려면 다음과 같이 한다.

$database Test_DB;

{ /* Test_DB 사용 */ }

$close database;

▶ 한편 ESQL/C로 작성된 프로그램의 컴파일은 다음과 같이 한다.

esql -o 출력파일명 소스파일명

 

4.6.3 프로그램 개요

▶ 여기서는 네트웍을 통하여 데이터베이스 자료를 검색하거나 자료를 추가하는 서비스를 제공하는 서버 프로그램, db_server.ec와 이를 이용하는 클라이언트 프로그램 db_ client.c의 개요를 설명하겠다.

▶ db_server.ec에서는 사용할 데이터베이스를 선언하고 클라이언트와 접속하기 위한 소켓을 개설한 후 클라이언트에서 요구하는 DB 액세스를 서비스한다.

▶ 본 예제에서는 클라이언트가 요구하는 동작으로 1) 자료추가와 2) 자료검색 두 가지만 구현하였다.

▶ 서버는 두 가지 중 하나의 동작을 실행하고 그 결과를 클라이언트에게 알려준다.

▶ 클라이언트 프로그램 db_client.c에서는 먼저 소켓을 생성하고 서버에 접속한다.

▶ 사용자 화면에 1) 자료추가와 2) 자료검색 메뉴를 보여주고 메뉴선택에 따라 필요한 데이터를 추가로 입력받는다.

▶ 자료 입력이 끝나면 서버로 동작을 요구(request)하고 그 결과(response)를 수신하여 화면에 출력한다.

▶ 아래는 서버 프로그램의 실행 예로 서버가 포트번호로 3000을 사용하는 경우이다.

db_server 3000

▶ 다음은 클라이언트 프로그램(db_client.c)의 실행화면으로 먼저 메뉴 1을 선택하여 부서명(dept_Name)을 추가하는 것을 보이고 있다.

# db_client

>> 자료추가 및 조회 메뉴

1. 자료추가

2. 자료조회

>> 메뉴 선택 : 1

>> 추가할 부서 ID : 01

>> 부서명 : 총무과

▶ 아래는 메뉴 2를 선택하고 부서 ID가 01인 부서명을 조회하는 것을 보이고 있다.

>> 자료추가 조회 메뉴

1. 자료추가

2. 자료조회

>> 메뉴 선택 : 2

>> 조회할 부서 ID : 01

==> 조회 결과 : 총무과

4.6.4 서버 프로그램 주요부분 설명

▶ 먼저 db_server.ec에서 사용할 헤더 파일을 선언해야 한다.

▶ ESQL/C 프로그램에서는 SQL 문이 실행된 뒤 결과 코드가 항상 전역변수 sqlca에 저장되며 이 변수를 사용하기 위하여는 헤더 파일 sqlca.h를 포함해야 한다.

▶ sqlca.h외에 sqlda.h, sqltypes.h 등의 헤더 파일을 포함할 수 있는데 sqlda.h에는 동적으로 정의된 SQL 문을 실행하기 위한 구조체가 정의되어 있다.

▶ sqltypes.h에는 SQL 칼럼의 종류가 정의되어 있다.

▶ 한편 SQL 관련 헤더 파일은 아래와 같이 #include 문이 아닌 $include 문을 사용해야 한다.

$include sqlca.h;

▶클라이언트는 DB 액세스를 할 때마다 '메뉴번호\n부서ID\부서명\n'의 형태로 구성된 메시지를 서버로 보내도록 하였다.

▶메뉴번호는 1이면 자료추가이고 2이면 자료조회를 나타낸다.

▶ 서버는 먼저 클라이언트가 어떤 메뉴를 선택했는지를 알아내야 하는데 이를 위해서 사용자 정의 함수 readline()을 사용한다.

▶ readline()은 소켓에 도착한 데이터를 지정한 크기만큼 읽는 함수인데 예를들어 소켓번호 s를 통해서 입력된 데이터의 앞에서부터 세 바이트(즉, 메뉴번호\n)를 읽어 menu라는 변수에 저장하려면 아래와 같이 한다(메뉴번호는 한 자리 수라고 가정).

▶ 한편 readline()은 user_func.h에 정의되어 있다.

n = readline(s, menu, 3);

▶ 같은 방법으로 dept_ID와 dept_Name을 readline()을 이용하여 읽는다.

▶예를들어 클라이언트가 메뉴 1) 자료추가를 선택한 경우 서버는 아래와 같이 추가할 자료를 읽은 후 insert를 실행한다.

▶ 메뉴 2) 자료조회를 선택한 경우에는 select를 사용하여 DEPT DB에서 dept_ID가 id인 부서를 조회하여 해당 부서명(dept_Name)을 변수 name에 저장한다.

switch((int)menu[0] - '0') {

/*------------------------------------------ 자료 추가 요구 ------------------------------------------*/

case 1:

/* 부서명 읽기 */

n = readline(s, name, NAME_LEN+2);

name[n-1] = '\0';

/* SQL 수행 */

$insert into DEPT values (:id, :name);

/*------------------------------------------- 자료 조회 요구 ----------------------------------------*/

case 2:

/* SQL 수행 */

$select dept_Name into :name from DEPT where dept_ID = :id ;

▶ 메뉴에 따른 처리가 끝났으면 서버는 클라이언트에게 SQL 문의 실행결과를 알려주기 위하여 클라이언트에게 전송할 모든 메시지를 문자 스트링 변수인 msg에 저장하여 이를 클라이언트로 전송한다.

▶ SQL 문 처리중 오류가 발생한 경우는 msg에 오류 메시지를 실어 전송한다.

db_server.ec 프로그램 리스트

4.6.5 클라이언트 프로그램 주요부분 설명

▶ 클라이언트 프로그램 db_client.c에서는 먼저 화면에 메뉴를 보여주고 메뉴를 선택하도록 하는 사용자 정의 함수 printMenu()를 호출한다.

▶ 선택한 메뉴값을 사용자 정의 함수 getMenu() 함수를 통해 입력받는데 그 값이 유효한 경우에는 해당 메뉴 번호를 리턴하고, 유효하지 않은 경우에는 0을 리턴한다.

▶ printMenu()와 getMenu()는 아래와 같다.

▶ 여기서 gets()는 키보드로부터 한 행의 문자열을 입력받는 함수이다.

void printMenu(void) {

printf("\n >> 자료추가 조회 메뉴\n");

printf("\t1. 자료추가\n");

printf("\t2. 자료조회\n");

printf("\n >> 메뉴 선택 : ");

}

int getMenu(void) {

char tmp[2];

gets(tmp);

tmp[1] = '\0';

if (tmp[0] != '1' && tmp[0] != '2' ) return 0;

else return (int)(tmp[0] - '0');

}

▶ 사용자가 선택한 메뉴 종류에 따라 필요한 자료를 추가로 입력받는다.

▶ 1) 자료추가를 선택한 경우에는 추가할 부서의 ID와 부서명을 입력받고 2) 자료조회를 선택한 경우에는 조회할 부서의 ID를 입력받는다.

case 1:

/* 자료추가를 위해 부서ID 이름을 입력받는다 */

printf("\n >> 추가할 부서 ID : ");

gets(dept_ID);

printf("\n >> 부서명 : ");

gets(dept_Name);

case 2:

/* 조회할 부서 ID 입력 */

printf("\n >> 조회할 부서 ID : ");

gets(dept_ID);

▶ 아래는 사용자가 입력한 명령문을 문자열로 만들고 이를 서버로 전송하는 부분이다.

sprintf(msg, "%d\n%s\n%s\n", menu, dept_ID, dept_Name);

msgLen = strlen(msg);

send(s, msg, msgLen, 0) != msgLen);

▶ 아래는 2) 자료조회 메뉴를 선택한 경우에 서버가 보내온 조회결과를 recv()를 이용하여 수신하고 이를 화면에 출력하는 부분이다.

recv(s, msg, MSG_LEN+1, 0);

printf("\t조회 결과 : %s\n\n", msg);

db_client.c 프로그램 리스트


소강좌제목 : 컴퓨터 네트웍 프로그래밍 이전 강좌: 제3장 클라이언트 서버 토크 프로그램 다음강좌: 제5장 Winsock 프로그래밍