서버를 통해 클라이언트가 에코 메시지를 보내는 예제를 넘어서 서버와 클라이언트가 콘솔에 입력한 문자열을 송수신 되도록 해보자. 송신과 수신을 동시에 가능하도록 하기 위해서는 여러가지 방법이 있을 수 있다. 그 중 fork 함수를 이용하여 부모 프로세스와 자식 프로세스를 생성한 후, 송신과 수신을 역할을 분담하여 수행하도록 해보자. 부모 프로세스와 자식 프로세스는 소켓 API 를 공유하여 사용하므로 부모 프로세스는 키보드로 부터 입력받아 송신 하는 역할을 하도록 구성하고, 자식 프로세스는 송신된 메시지를 출력하는 역할을 하도록 하면 될 것이다. 다음 그림을 통해 살펴 보면 이해가 쉽다.
서버에서 부모 프로세스는 표준입력을 통해 키보드로 입력 받은 문자열을 소켓을 통해 클라이언트로 메시지를 전송한다. 클라이언트에서 자식 프로세스가 서버에서 전송한 메시지를 읽어들인다. 표준출력으로 클라이언트의 콘솔창에 출력해준다. 반대로 클라이언트에서 부모 프로세스는 키보드로 입력 받은 문자열을 전송하면, 서버의 자식 프로세스가 클라이언트에서 전송한 메시지를 읽어서 서버의 콘솔창에 출력해준다.
소스코드를 통해 살펴보자. 소스코드는 서버와 클라이언트 각각 두개의 파일로 구성되어 있다.
★ 소스 코드
< talk_server.c >
#include <stdio.h>#include <string.h>#include <stdlib.h>#include <sys/types.h>#include <signal.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include <unistd.h>#define MAXLINE 512char *escapechar = "exit"; // 종료 문자열int main(int argc, char *argv[]){int server_sock;int client_sock;int clntlen;int num;char sendline[MAXLINE];char recvline[MAXLINE+1];int size;pid_t fork_ret;struct sockaddr_in client_addr;struct sockaddr_in server_addr;if( 2 != argc ){printf("Usage : %s PORT \n", argv[0]);exit(0);}/* 소켓 생성 */server_sock = socket(PF_INET, SOCK_STREAM, 0);if( 0 > server_sock ){printf("Server : can't open stream socket. \n");exit(0);}/* 소켓 주소 구조체에 접속할 주소 셋팅 */bzero((char *)&server_addr, sizeof(server_addr)); // 소켓 주소 구조체 초기화server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = htonl(INADDR_ANY);server_addr.sin_port = htons(atoi(argv[1]));/* 소케에 서버 주소 연결 */if( 0 > bind(server_sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) ){printf("Server : can't bind local address. \n");exit(0);}printf("Server started. \nWaiting for client.. \n");listen(server_sock, 2);/* 클라이언트의 연결요청 수락 */clntlen = sizeof(client_addr);client_sock = accept(server_sock, (struct sockaddr *)&client_addr, &clntlen);if( 0 > client_sock ){printf("Server : failed in accepting. \n");exit(0);}fork_ret = fork();if( 0 < fork_ret ){/* 부모 프로세스는 키보드 입력을 서버로 송신 */while( 0 != fgets(sendline, MAXLINE, stdin) ){size = strlen(sendline);if( size != write(client_sock, sendline, strlen(sendline)) ){printf("Error in write. \n");}if( 0 != strstr(sendline, escapechar) ) // 종료 문자열 입력시 처리{printf("Good bye. \n");close(client_sock);exit(0);}}}else if( 0 == fork_ret ){/* 자식 프로세스는 서버로부터 수신된 메시지를 화면에 출력 */while(1){size = read(client_sock, recvline, MAXLINE);if( 0 > size ){printf("Error if read. \n");close(client_sock);exit(0);}recvline[size] = '\0';if( 0 != strstr(recvline, escapechar) ) // 종료 문자열 입력시 처리{break;}printf("%s", recvline); // 화면 출력}}close(server_sock);close(client_sock);return 0;}
< talk_client.c >
#include <stdio.h>#include <string.h>#include <stdlib.h>#include <sys/types.h>#include <signal.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#define MAXLINE 1024char *escapechar = "exit"; // 종료 문자열int main(int argc, char *argv[]){char line[MAXLINE];char sendline[MAXLINE];char recvline[MAXLINE+1];int n;int size;int comp;int addr_size;pid_t fork_ret;static int s;static struct sockaddr_in server_addr;if( 3 != argc ){printf("Usage : %s serverIP secverPORT \n", argv[0]);exit(0);}/* 소켓 생성 */s = socket(PF_INET, SOCK_STREAM, 0);if( 0 > s ){printf("Client : can't open stream socket. \n");exit(0);}/* 소켓 주소 구조체에 접속할 서버 주소 셋팅 */bzero((char *)&server_addr, sizeof(server_addr)); // 소켓 주소 구조체 초기화server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = inet_addr(argv[1]);server_addr.sin_port = htons(atoi(argv[2]));/* 서버에 연결 요철 */if( 0 > connect(s, (struct sockaddr *)&server_addr, sizeof(server_addr)) ){printf("Client : can't connect to server. \n");exit(0);}fork_ret = fork();if( 0 < fork_ret ){/* 부모 프로세스는 키보드 입력을 서버로 송신 */while( 0 != fgets(sendline, MAXLINE, stdin) ){size = strlen(sendline);if( size != write(s, sendline, strlen(sendline)) ){printf("Error in write. \n");}if( 0 != strstr(sendline, escapechar) ) // 종료 문자열 입력시 처리{printf("Good bye. \n");close(s);exit(0);}}}else if( 0 == fork_ret ){/* 자식 프로세스는 서버로부터 수신된 메시지를 화면에 출력 */while(1){size = read(s, recvline, MAXLINE);if( 0 > size ){printf("Error if read. \n");close(s);exit(0);}recvline[size] = '\0';if( 0 != strstr(recvline, escapechar) ) // 종료 문자열 입력시 처리{break;}printf("%s", recvline); // 화면 출력}}close(s);return 0;}
★ 소스 코드 설명
프로세스를 생성하는 코드를 살펴보자.
fork_ret = fork();
fork 함수는 부모 프로세스에게 자식 프로세스의 PID 를 리턴하고 자식 프로세스에게는 0 을 리턴한다. 실패할 경우에는 -1 을 부모 프로세스에게 리턴한다. 자식 프로세스는 생성되지 않았기 때문에 리턴되는 것은 없다. 서버와 클라이언트 모두 부모 프로세스가 송신의 역할을 한다. 서버의 부모 프로세스가 동작하는 부분이지만 클라이언트의 부모 프로세스와 거의 흡사하며 원리는 똑같다.
fork_ret 가 0 보다 크면 자식 프로세스가 성공적으로 생성되고 fork_ret 의 값은 자식 프로세스의 PID 를 의미한다. 따라서 위의 동작은 서버의 부모 프로세스가 키보드로 입력 받은 문자열을 전송한다. 전송된 메시지는 클라이언트 자식 프로세스가 읽어서 콘솔에 전송된 문자열을 출력한다. 자식 프로세스는 수신 역할을 하며 다음과 같다. 역시 서버의 자식 프로세스의 동작 코드이며, 클라이언트의 자식 프로세의 동작 코드와 원리는 똑같다.
else if( 0 == fork_ret ){/* 자식 프로세스는 서버로부터 수신된 메시지를 화면에 출력 */while(1){size = read(client_sock, recvline, MAXLINE);if( 0 > size ){printf("Error if read. \n");close(client_sock);exit(0);}recvline[size] = '\0';if( 0 != strstr(recvline, escapechar) ) // 종료 문자열 입력시 처리{break;}printf("%s", recvline); // 화면 출력}}
fork_ret 의 값이 0 이라면 자식 프로세스를 의미한다. 따라서 서버의 자식 프로세스는 클라이언트의 부모 프로세스가 전송한 메시지를 읽어서 콘솔에 출력한다. 서버의 기준이였고, 클라이언트의 기준으로 해도 역시 똑같다. 다음과 같이 문자를 입력하면 서버에서는 클라이언트로 클라이언트는 서버로 문자들을 전송한다. 다음은 서버와 클라이언트 사이에 메시지가 송수신 되는 콘솔 화면이다.
현재 프로세스의 상태를 살펴보면 서버에 2 개의 프로세스와 클라이언트의 2 개의 프로세스가 존재하고 있다.
exit 를 입력하게 되면 종료하게 된다. 하지만 프로그램은 종료되지만 프로세스가 종료되지 않은 상태로 남아있게 된다. 이로 인해서 이상현상이 발생한다. 서버에서 exit 로 종료를 하지만 클라이언트에서 계속해서 서버에 메시지를 전송한다.
exit 가 된 상태에서의 프로세스 상태를 살펴보자.
서버의 프로세스가 종료되었다. 이는 서버의 부모 프로세스가 종료된 것이다. exit 입력 받아 종료하는 프로세스는 부모 프로세스이기 때문이다. 자식 프로세스가 종료되기 전에 부모 프로세스가 종료되어 문제가 발생한다. 이를 해결하기 위해서는 부모 프로세스와 자식 프로세스의 역할을 바꾸면 어느 정도 해결할 수 있다. 자식 프로세스를 먼저 종료시킨 후에 부모 프로세스를 종료되게 구현을 하면된다.
★ JAVA
JAVA 는 객체지향 프로그래밍 언어이다. 객체지향 프로그래밍이란 모든 데이터를 오브젝트 (object: 물체) 로 취급하여 프로그래밍 하는 방법으로, 처리 요구를 받은 객체가 자기 자신의 안에 있는 내용을 가지고 처리하는 방식을 말한다. 1991년 Sun Microsystem 의 Green project 라는 연구진으로부터 오크 (Oak) 라는 언어가 개발되면서 자바가 시작 되었다. 하드웨어는 빠르게 발전하는 반면 소프트웨어의 발전은 하드웨어에 비해 많은 발전을 하지 못하였다. C 언어를 아직도 사용하고 있는 것만봐도 알 수 있다. 물론 C 언어가 잘 만들어진 언어임은 틀림없다. 하드웨어는 제품들의 모듈화와 부품화가 잘 되어 있다. 제품의 표준이 정해져 있으므로 제조사와 상관없이 사용이 가능하다. 하지만 소프트웨어는 하나만 수정하더라도 전체적으로 많은 문제를 발생할 수 있고, 새로운 기능을 추가하면 다른 곳에서 문제가 발생하기도 한다. 이를 Ripple Effect (파급 효과) 라 한다. 이러한 문제점을 해결하기 위해 객체지향 프로그래밍이 도입되었다. 기존의 C 언어는 프로그램을 수정하기 위해서는 전체를 수정하거나 새로 만들어야 하는 문제점이 있어 프로그램을 만드는 시간이 오래 걸렸다. 반면에 자바와 같이 객체지향 프로그래밍은 소스코드를 재활용하여 시간과 생산량을 증가시켰다. 자바는 약 30 % 정도는 소스코드가 제공되며 이를 재활용 하여 사용하면 된다. 하지만 생산량이 증가하더라도 2 ~ 3 배 만큼 엄청나게 증가하지는 않는다. 또한 새로운 신기술은 새로운 것을 배워야 하는 문제점과 새로운 개념의 도입이 되는 위험 요소들이 존재한다. 자바는 리눅스, 윈도우, 매킨토시 등 어떠한 환경에서도 같은 결과를 볼 수 있다. 그 이유는 자바는 JVM (Java Virtual Machine) 이라는 가상머신을 이용한다. 자바는 다음 그림과 같이 구성되어 있다.
자바는 하드웨어를 제어하기 위한 목적으로 만들어졌다. 따라서 소프트웨어지만 하드웨어의 기능을 할 수 있다. 하드웨어와 소프트웨어의 중간에 미들웨어의 역할을 한다. 예를 들면 하드웨어의 그래픽카드가 없다면, 소프트웨어로 그래픽카드가 있는 것처럼 사용할 수 있도록 할 수있다. 자바 플랫폼은 자바 프로그램을 실행되기 위해 제공되는 하드웨어적 프로그램이다. 가상 머신 위에 자바 API 를 이용하여 소스코드를 작성한다.
참고로 자바의 개발사 Sun Microsystem 는 Oracle 이라는 기업에 인수되어 Oracle 사이트에 접속해야한다. 사용 할 버전은 Java SE 6 Update 20 이다. 현재는 Java SE 6 Update 26 까지 업데이트 되어있다.
< Java SE 6 Update 20 설치하기 >
다음 아이콘을 더블 클릭하여 실행하자.
라이센스에 관한 설명이 나온다. Accept 를 선택하고 진행한다.
설치 할 경로를 선택하자. 기본적으로 설치 하는 경로에 설치한다. 임의로 설치하고 싶다면 경로를 수정하면 된다.
설치를 진행한다.
설치가 완료되면 제품 등록을 하라는 창이 뜬다. 그냥 Finish 를 눌러 설치를 완료한다.
★ Java 환경변수 설정
[내컴퓨터] 아이콘에 마우스오른쪽 클릭하여 [속성] 메뉴를 선택한다. [시스템 등록 정보]라는 창이 뜬다. 단축키는 [윈도우] + [break] 를 누르면 된다. [시스템 등록 정보]의 [고급] 탭을 클릭하고 환경 변수를 클릭하자.
[시스템 변수] 부분에 [새로 만들기] 를 클릭하자. [새 시스템 변수] 창이 나타나면 변수 이름에 JAVA_HOME 를 입력하고 변수 값에 자바를 설치한 경로를 입력한다. 설치 시 기본 경로라면 다음과 같다.
C:\Program Files\Java\jdk1.6.0_20
입력이 끝이나면 [확인] 을 누른다.
다시 [시스템 변수] 부분에 [Path] 를 찾아서 [편집] 을 클릭하자. [시스템 변수 편집] 창이 나타나고 변수 값에서 가장 마지막 부분으로 커서를 이동한 후 설치한 경로를 입력하자. 주의 할 점은 ; 를 먼저 넣어야 하는 것이다. 또한 설치 경로에서 bin 폴더까지 포함해야 한다.
;C:\Program Files\Java\jdk1.6.0_20\bin
위의 경로 대신에 다음과 같이 변수 값을 넣어도 된다.
;%PATH%\bin
입력이 끝이나면 [확인] 을 눌른다. 환경변수 설정이 완료되었다.
★ Java 설치 확인법
자바 설치가 옳바르게 되었는지 확인해보자.
[시작] - [실행] - [cmd]
cmd 창을 실행하여 javac 를 입력하고 엔터를 누르자. 다음과 같은 결과가 나타나면 정상적으로 설치가 된 것이다.
★ 자바 컴파일
C:\ 에 적당히 테스트 할 폴더(또는 디렉토리) 를 만들자. javatest 라는 이름으로 만들었다. javatest 하위 폴더에 work 폴더를 하나 더 만들자. 굳이 따라서 할 필요는 없다. 예시를 제공할 뿐이다. work 폴더에서 새로운 파일을 만들어 이름과 확장자를 다음과 같이 정하자.
HellowWorld.java
메모장에서 불러와서 다음과 같이 작성하자.
cmd창을 실행하여 HelloWorld.java 가 저장된 위치에서 다음과 같이 입력하자.
javac [파일이름].java
실행은 다음과 같다.
java [파일이름]
위의 소스 코드를 작성하여 컴파일과 실행을 하면 다음과 같다.
만약 잘못 작성을 하게 되면 다음과 같이 에러가 발생할 수 있다. 동일한 에러가 발생하지 않을 수 있다. 여기서는 ; 을 입력하지 않아 발생한 에러이다.
★ Eclipse 설치 및 컴파일
여러 가지 자바 개발툴 중에 많이 사용하는 eclipse 를 설치 해보자. 다음 사이트에 접속하면 다운을 받을 수 있다.
Eclipse IDE for Java Developers 를 다운 받으면 된다. 사용할 버전은 Helios Service Release 2 이다.
< 자바 개발툴 eclipse 설치 및 컴파일 하기 >
별도로 설치할 필요가 없으므로 적당한 곳에 이동하여 실행하면 된다. 컴파일 할 때 만들어 두었던 폴더에 eclipse 폴더를 이동하여 사용해보자.
eclipse 폴더 안에 [eclipse.exe] 를 실행하자.
실행을 하면 다음과 같이 로딩화면을 보여준다.
로딩이 끝이나면 다음과 같이 작업할 경로를 설정하라는 창이 뜬다. 컴파일 했었던 폴더로 경로를 설정해보자. 참고로 Use this as the default and do not ask again 을 체크 하면 다음 실행 때는 이 창을 띄우지 않는다. 경로를 설정하였으면 [ok] 를 누른다.
eclipse 실행화면이다. welcome 탭이 우리를 환영(?!) 해준다. 탭에 X 를 눌러줘도 무방하다.
X 를 누르게 되면 개발툴 같은 느낌을 보여준다.
왼쪽 위의 [File] - [New] - [Java Project] 를 클릭하여 새로운 프로젝트를 만들어 보자.
다음과 같은 창이 뜨며 프로젝트 이름은 적당히 정하자. 그리고 [Next] 를 클릭한 후 [Finish] 를 클릭하자. [Next] 를 하지 않고 바로 [Finish] 해도 큰 문제는 없다.
Package Exploer 에 만들어진 HelloWorld1 을 마우스 오른쪽 클릭을 한 후 [New] - [Class] 를 클릭하자.
팩키지 (Package) 는 폴더의 의미를 나타낸다. 또한 자바의 가상머신에서는 고유의 키를 의미한다. 보통 도메인 네임을 반대로 적는게 일반적이다. 이름을 적당히 정한 후에 public 을 선택하자. public static void main(String[] arg) 부분을 체크하자. 모두 작성하였다면 [Finish] 를 클릭한다.
왼쪽에 생성한 프로젝트와 파일들을 보여준다. 그리고 이전에 메모장에서 작성한 코드를 작성해보자.
컴파일을 해보자. 버튼을 누르면 다음과 같이 창이 뜬다. 파일이 선택되어 있지 않으면 선택하고 [OK] 를 클릭하자.
툴의 아래쪽을 살펴보면 console 탭에 Hello World! 가 출력 되었음을 알 수 있다.