JAVA > LIBRARY > Java 소켓통신예제 계산기 프로그램
 
JAVA
Library
Tip&Tech
Q&A
java공식사이트
Java  Platform Standard Edition 6 의 API 스펙
LIBRARY
  HOME > JAVA > LIBRARY
 
Java 소켓통신예제 계산기 프로그램
작성일 : 10-02-04
조회 : 5,121  

프로젝트 2. 계산기 프로그램


이번에 작성할 프로그램은 계산기 프로그램입니다. 클라이언트에서 두 정수(int)와 연산자(+,-,*,/)를 보내면 서버에서 받아서 계산한 후 결과값을 클라이언트에 보내는 프로그램입니다. 이 프로그램은 앞의 프로그램과는 차이가 있습니다.

  - 앞의 프로그램에서 하나의 내용이 전송되고 나면 서버와 클라이언트가 종료되었지만,

    이번 프로그램에서는 하나의 계산이 입력되고 나서도 서버는 계속해서 클라이언트의 입력을

    기다리게 됩니다. 이것은 하나의 서버에 여러 클라이언트가 접속할 수도 있음을 의미합니다.

  - 일방적으로 클라이언트에서 서버로 전송을 하는 것이 아니라, 클라이언트는 서버에 명령을 입력

     하고 나서 서버로부터의 결과를 기다리게 됩니다.
  - 입력받은 내용을 그대로 출력하는 것이 아니라, 어느 것이 수이고 어느 것이 연산자인지를

     구별해야만 한다.

 

이번 프로젝트에서는 세 가지를 배우게 됩니다.

 - 프로토콜을 구성하는 방법
 - 다른 종류의 Reader와 Writer / Tokenizer의 사용
 - 전송과 수신을 동시에 하는 것
 - 서버에서 쓰레드를 사용하여 계속해서 연결을 기다리는 것
 - 소켓의 연결이 끊어졌을때 서버에서도 소켓 연결을 끊는 것

1) 프로토콜의 정의

먼저, 통신규약(프로토콜)을 먼저 정의해봅시다. TCP/IP나 Netbeui같은 것들을 프로토콜이라고 합니다. 프로토콜은 보내는 측과 받는 측에서 미리 약속한 일정한 형식 같은 것을 의미합니다. 물론 우리가 정의한 프로토콜이 TCP/IP같은 수준은 아니지만 아무튼 서버와 클라이언트가 어떠한 형식으로 서로를 구분할 것인지를 약속해야 하니까 일종의 프로토콜이라고 할 수 있습니다. 프로토콜을 잘 정의하는 것은 소켓 통신의 기본입니다. 프로토콜을 얼마나 잘 정의하느냐에 따라서 프로그래밍이 대단히 어려울 수도 있고, 쉬워질 수도 있습니다. 프로토콜을 잘못 정의하면 프로그램 중에 프로토콜을 바꾸어야 하는 경우도 생길 수 있습니다. 이런 경우에는 데이터를 전송하거나 수신하는 부분을 다시 다 바꾸어주어야 하므로 보통 힘든 일이 아니겠죠.

 

아무튼 일단 어떻게 해야할지 생각해봅시다. 소켓을 통해서 보내질 패킷(한 무더기의 데이터)은 두 종류가 있네요. 우선, 클라이언트에서 서버에 보내는 계산을 요청하는 패킷입니다. 여기에는 연산자와 두 정수가 들어가겠죠. 다음은, 서버에서 클라이언트에게 보내는 계산결과 입니다. 여기에는 결과값만 들어가면 되겠죠...라고 생각하면 안됩니다. exception처리! 이것은 프로그래머의 예의입니다. 클라이언트가 요청한 값이 잘못되었을 경우, 예를 들어 정수가 아닌 문자나 소수를 입력했거나, 정수가 두 개가 되지 않았을때, 알 수 없는 연산자를 보냈을 때, 0으로 나누려고 했을때... 등등의 경우에 서버는 요청한 계산이 잘못되었다고 에러메시지를 보낼 수 있어야 하겠죠.

클라이언트에서 서버에 보내는 것은 연산자와 두 정수입니다. 연산자에는 +,-,*,/ 네 종류가 있죠. 프로토콜을 구성하는 방법은 보통 많이 쓰는 것이 토큰의 중간 중간에 구분자를 넣거나, 아니면 하나의 토큰이 일정한 수의 byte를 차지하게 하는 것입니다.

분리자를 이용한 토큰의 구성예: +:123:456:\


 

이 경우에는 패킷의 크기를 알 수 없기 때문에 패킷의 끝에 적당한 문자로 끝임을 알려주는 문자를 넣어야 합니다. 지금은 \를 사용했습니다. 만약 문장의 끝을 나타내는 문자나 분리자(Delemeter)를 1이나 2같은 숫자로 쓴다면 어떨까요? 아마 진짜숫자를 분리자나 패킷의 끝으로 착각하는 경우가 생길 것입니다. 따라서 실제 데이터 속에는 포함될 수 없는 문자를 사용하여 분리자로 사용해야 하겠지요. 지금은 :가 분리자입니다. 패킷을 받는 계산기서버에서는 \가 나타날때 까지 하나씩 읽은 다음, 읽은 데이터를 :를 기준으로 끊어주어야 하겠죠.

일정한 공간을 차지하게 하는 예:+ 0123 0456

 


 

이 경우에는 크기를 정확히 알 수 있기 때문에 분리자나 패킷의 끝을 나타내는 문자 등은 덧붙일 필요가 없습니다. 받는 쪽에서는 첫번째 글자는 연산자이고, 다음 네글자는 숫자, 다음 네 글자는 다음 숫자라는 것을 바로 알 수가 있죠. 하지만, 이렇게 하면 나중에 4자리수 이상을 차지하는 계산기 프로그램으로 바꾸기 위해서는 프로토콜을 다시 정의해야 할 것입니다. 또한 경우에 따라서는 공간의 낭비도 아주 심합니다. 1을 나타내기 위해서도 4자리를 사용해야 하므로 그만큼 데이터의 양도 많아지고 전송 속도도 느려지겠죠.

 

프로토콜을 구성하기 위해서는 여러 가지를 고려해야 합니다. 우선 사용하는 언어에서 구현 가능해야 하고, 언어에서 지원하는 기능들로 쉽게 구현할 수 있어야 하며, 같은 내용을 나타내기 위해서 패킷의 크기가 너무 커지지 않아야 하며, 덧붙여 이후의 확장성까지도 고려해야 합니다. 앞서 얘기했듯이 프로토콜을 잘 정의하고 나면 앞으로의 경로가 순탄해지는 것이죠.

사실 이 프로젝트에 가장 적합한 프로토콜은 프로젝트1에서 사용했던 것입니다. InputStreamReader와 OutputStreamWriter에서는 기본적으로 정수를 기본으로 데이터를 전송하고 수신하는 메소드들이 있습니다. 따라서 앞에 연산자를 보내고, 다음엔 그냥 두 정수(int)를 전송하면 받는 쪽에서도 마찬가지로 받는 것은 read()와 write()메소드로 간단히 구현할 수 있습니다. 바로 정수로 전송하기 때문에 그대로 데이터를 사용할 수 있지만, 위의 두 방식은 받을때 문자열(String)로 받아야 하기 때문에 이것들 다시 정수(int)로 바꾸어주는 과정이 필요합니다.

그러나 이번 프로젝트에서는 첫번째의 delemeter를 사용하는 방식을 사용하도록 하겠습니다. 가장 많이 쓰이는 방식이기 때문일 뿐더러, 자바에서 제공하는 StringTokenizer와 BufferedReader(Writer)를 사용해보기 위해서입니다. 자바에서는 분리자에서 토큰을 분리해내는데에 StringTokenizer라는 클래스를 사용하면 아주 간단히 구현할 수 있기 때문입니다. 또한 BufferedReader는 InputStreamReader와 다르게 한 라인을 읽을 수 있는 기능(readLine())을 제공하기 때문에, 아주 편리합니다. 또한 다른 언어와의 통신에서 InputStreamReader와 OutputStreamWriter는 한글처리에서 가끔 문제가 발생하는데에 반해, BufferedReader(Writer)는 한글처리에 대단히 유리합니다.

클라이언트에서 서버에 보낼때, 분리자는 위에서와 같이 ':'를 사용하도록 하겠습니다. 그리고 패킷은 한라인 단위로 보내면 받는 쪽에서 readLine()을 이용하여 받으면 되겠죠. "연산자:정수:정수" 의 순서로 보내면 될 것 같네요.

서버에서도 분리자 :를 사용합니다. 성공(s)인지 실패인지(f)를 먼저 보내고, 다음에는 성공이면 결과값을 실패이면 에러메시지를 보내도록 합니다. 즉, 성공이면 "s:결과값", 실패이면 "f:에러메시지"의 형식이 되겠죠.

 

 

2) 클래스 설계1 : 클라이언트

클라이언트 부분은 앞에서 프로젝트1과 별반 다르지 않습니다. 소켓을 연결하는 부분은 완전히 동일하고, 단지 패킷을 보낸 다음 다시 서버에서 보낸 패킷(결과값)을 수신하는 부분이 추가될 뿐입니다. 이번 프로젝트에서는 앞에서와 같이 대충 기능만을 구현하는 것이 아니라 최대한 클래스를 효율적으로 설계해보도록 하겠습니다. 프로젝트1에서 클래스 설계를 다시 해서 한 번 작성해보라고 했는데 그말대로 실제로 프로그래밍을 해보았다면, 이번 프로젝트도 별로 어렵지 않을 것입니다. 클래스의 개념을 이해하는 것은 어렵지 않지만, 실제로 프로젝트에 적용하는 데에는 적잖은 고민이 필요합니다. 프로젝트1에서 클래스 설계를 해보았다면 그런 고민들을 한 번 이상씩 해보았을 것이고, 어떻게 하면 효율적인 설계가 될지 많이 생각해보았을 것입니다.

클라이언트의 클래스를 구성하는 방법은 여러 가지가 있습니다. 먼저 생성자에서 어떤 일을 해야 하는지를 생각해볼 수 있습니다. 생성자에서 IP와 port을 받아서 소켓 연결까지 모두 하게 할 수 있고, 생성자는 아무 일도 하지 않고 소켓 연결은 그것을 담당하는 다른 메소드를 정의해서 사용할 수도 있습니다.

또한 계산을 담당하는 메소드 또한 어떻게 인자를 받아야 하고 어떻게 반환을 할 것인지를 고려해야 합니다. 두 정수와 연산자를 모두 String으로 받을 수도 있고, 두 정수는 정수(int)로 연산자는 String이나 char로 받을 수도 있습니다. 계산 결과 또한 String으로 반환할 수도 있고, 정수형으로 반환할 수도 있습니다.

Exception처리 또한 고려의 대상입니다. 어디에서 exception을 처리할지가 문제입니다. 만약 데이터를 전송하는 데에 문제가 생겼다면 그자리에서 바로 예외상황을 처리할 것인지, 아니면 자신을 호출한 곳으로 exception을 넘겨서 거기서 호출하게 할 것인지.... 여러 가지 방법이 있습니다.

사실 클래스를 잘 설계하고도 실제로 코딩에 들어가면 설계할때 미처 생각하지 못한 부분이 생각나서 설계를 다시 수정해야 하는 경우가 한 두 부분이 아닐 것입니다. 이것은 뭐 프로그래밍을 많이 해보고 클래스 설계에 익숙해지고 know-how를 쌓아하는 방법밖에는 달리 지름길이 없습니다. 하지만 분명히 말할 수 있는 것은, 클래스 설계를 고려하지 않고 적당히 되는대로 코딩부터 시작해서는 언젠가 어려움에 봉착하게 될 것이라는 점입니다.

 

 

Example=Sample=...: 제가 한 설계는 다음과 같습니다. 생성자에서는 아무런 일을 하지 않게 하였습니다. 그리고 소켓을 연결하는 메소드(void socket_connect())를 따로 만들어서 여기에서 Reader와 Writer까지 잡게 하였습니다. 계산은 void calcul(int x, int y, String op)에서 하게 하였습니다. 보다시피 인자는 정수 두 개와 연산자를 String으로 넣게 하였습니다. 반환값은 없습니다. 이것은 결과는 정수로 에러는 문자열로 반환되어야 하기 때문에 계산하는 메소드에서는 반환값을 주지 않고, 결과는 int getResult()메소드에서 에러메시지는 String getError()에서 얻을 수 있도록 하였습니다. 계산이 에러 없이 잘 되었는지는 boolean isSuccess()로 알 수 있습니다.

  • public CalculClient(String ServerIP, int port);
  • public void socket_connect();
  • public void calcul(int x, int y, String operator);
  • public boolean isSuccess();
  • public int getResult();
  • public String getError();

각각의 메소드에 대해서는 따로 설명이 필요하지 않을 듯하다. 소켓 연결은 socket_connect()에서 하고, 서버에 데이터를 전송해서 결과값을 받는 일은 calcul()메소드에서 하게 됩니다. 이것을 구현하기 위해서 필요한 인스턴스(클래스 변수)는 다음과 같다.

  • private int result=0; // 결과값이 저장됩니다.
  • private String error; // 에러 메시지가 저장됩니다.
  • private boolean success; // 계산이 성공하면 true, 에러가 나면 false
  • private String ip; // 서버의 IP를 저장합니다.
  • private int port; // 소켓의 포트를 저장합니다.
  • private Socket socket; // 소켓입니다.
  • private BufferedReader; // 데이터를 읽을 Reader입니다.
  • private BufferedWriter; // 데이터를 전송할 Writer입니다.

그리고 IOException은 모두 자신을 호출한 곳에게 던져주기로 했습니다. 그 방법은 다음에 설명을 하기로 하구요, 아무튼 socket_connect()나 calcul()등을 호출하고서는 Exception을 받아서 처리하는 문장을 만들어주어야 하겠죠.

여기서 isSuccess()와 getResult(), getError()메소드는 아주 간단하리라 생각합니다. 그냥 각각, success, result, error값을 return해주기만 하면 되겠죠.

	public boolean isSuccess() {
		return success;
	}
	
	public int getResult() {
		return result;
	}
	
	public String getError() {
		return error;
	}

 

3) 클래스의 구현1: 클라이언트

정수 x, y와 문자열로된 연산자(+,-,*,/)를 입력받아서 서버에 계산을 요청하는 패킷을 보내고서 결과를 받아서 분석하는 부분을 해봅시다.

int x, y;에는 각 수가 들어갈 것이고, String op에는 +,-,*,/ 중의 하나가 들어갈 것입니다. 앞에서 설계한 패킷의 구조를 기억해내보세요. 13+24를 계산하기 위해서는 " +:13:24: "라는 패킷을 만들어야 합니다. 패킷은 아주 간단하게 만들 수 있겠네요. 그냥 스트링에 더하기만 하면 되니까요.

 

String packet=op+":"+x+":"+y+":";

 

op는 String이고 ":"도 String이니까 더하는 것이 이해가 되지만 여기에 x를 더하는 것은 얼핏 이해가 되지 않습니다. x는 정수인데 문자열에 더하고 있으니까요. 자바의 이상한 점 중의 하나인데, 아무튼 String에는 정수(int), 소수(float)를 더해도 String이 되고, 심지어 boolean값을 더해도 그냥 문자열에 더해집니다. 좀 이상하긴 하지만 아무튼 지금은 사용하기에 아주 편리하군요. 다음엔 만든 패킷을 전송한 다음, 결과값을 반환 받는 것을 해봅시다.

 

bw.write(packet);
bw.newLine();
bw.flush();                                // 패킷을 전송한다.

 

String result=br.readLine();         // 결과로 서버에서 날아온 패킷을 받는다.

 

bw.newLine()과 bw.flush()가 하는 역할에 대해서는 저번에 설명을 하였습니다. 서버쪽에서 라인 단위로 읽기 때문에 라인이 끝났다는 표시를 해주어야 하고, 버퍼에 쌓아둔 패킷을 전송하라는 명령을 내려주어야만 합니다.

서버에 패킷을 전송한 다음에 바로, readLine()을 써서 결과값을 얻는 군요. 실행을 시키면 bw.flush()까지 실행을 하고서 서버가 데이터를 보낼때 까지 기다리다가 서버에서 패킷을 보내면 readLine()에서 한 라인을 읽고서는 진행됩니다. 코드상으로는 붙여써주었지만, 실제로는 bw.flush()와 br.readLine()사이에는 많은 시간이 걸리는 것이고, 만약 서버에서 데이터를 보내지 않는다면 이 부분에서 정지해 있게 됩니다.

서버에서 전송한 결과값(result)를 분석해봅시다. 서버에서는 성공했을 경우 " s:결과값 ", 실패했을 경우 " f:에러메시지 " 와 같이 패킷을 보내게 됩니다. 앞에서 13+24라는 메시지를 보냈다면, " s:37"이라는 패킷이 날아오겠죠. 만약 3/0을 계산하라는 패킷을 보내면 "f:0으로 나눌 수 없습니다."라는 메시지가 날아옵니다. 그러면 클라이언트에서 그것을 적절히 분석해서 성공이면 결과값 37을, 실패이면 에러메시지를 내보내면 됩니다.

패킷을 분석하기 위해서는 StringTokenizer를 사용합니다. 프로젝트2를 시작하면서 이 클래스를 사용하겠다고 얘기했던 것이 기억날 것입니다. 이름 그대로 String을 Token으로 만들어주는 일을 합니다. token이란 단어와 비슷한 것이라고 할 수 있습니다. 의미를 가지는 데이터 단위니까요. 서버에서 날아오는 패킷은 토큰을 2개 가지고 있습니다. 성공(s)인지 실패(f)인지를 뜻하는 것과, 결과값 또는 에러메시지 입니다. "s:37"이라면 s와 37이 토큰이 됩니다. StringTokenizer를 사용하는 방법은 매우 간단합니다. 생성자에 스트링과 분리자(Token Delemeter)를 넣어주면 nextToken()이라는 메소드로 토큰을 하나씩 분리해 낼 수 있습니다.


   int re=0;
   String error="";StringTokenizer st=new StringTokenizer(result, ":");
   String success=st.nextToken();

   if ( success.equals("s") ) {

      re=Integer.parseInt(st.nextToken());
   } else {

      error=st.nextToken();

   }

 

아주 간단하죠? 스트링 토크나이저를 만든 다음, 토큰을 한 분리해서 그것이 "s"이면 다음 토큰을 분리해서 정수로 바꾼 다음 결과값에 넣고, "s"가 아니면 다음 토큰을 에러메시지에 넣는 것입니다.

 

 

4) 클래스 설계2 : 서버

 

서버 쪽은 약간 어렵습니다. 계속해서 클라이언트의 데이터를 기다리다가 받아서 계산을 하는 부분이 Thread를 사용하여 구현되어야 하기 때문입니다. 그 이유는 thread를 사용하면 하나의 서버에 여러 개의 클라이언트가 각각 독립적으로 접속할 수 있고, CPU점유율을 줄일 수 있다는 것입니다. 아무튼 그냥 서버 클래스를 설계해보자고 말하면 너무 잔인한 일이 될 것 같으니까, 서버가 돌아가는 메카니즘은 함께 공부해보고서 구체적인 클래스의 설계는 각자 해보고서 클래스 구현 부분에서 저의 설계와 비교해보도록 합시다.

 

Thread는 process와 비슷합니다. process 안에서 돌아가는 별개의 process 같은 것입니다. 윈도우나 리눅스, 유닉스와 같은 멀티태스킹 운영체제에서 에서 여러 개의 process가 돌 수 있는 것처럼 하나의 프로세스에서도 여러 개의 thread가 돌 수 있습니다. 하지만 process와 thread의 차이점은 문맥교환(context switching)이 일어나지 않는다는 것입니다. context란 하나의 프로세스가 돌아가는 환경이라고 볼 수 있습니다. 보통 레지스터 변수들의 값을 말하는데, 하나의 process는 하나의 context를 가지게 되므로, 두 개의 프로세스가 번갈아 실행되기 위해서는 계속해서 context switching이 일어나야 합니다. 그러나 thread는 process내부에서 돌기 때문에 문맥교환은 일어나지 않습니다. 아무튼 자세한 것은 알 필요가 없고 아무튼 하나의 프로그램에서 여러 개의 thread가 돌면서 Multi Tasking을 할 수가 있다는 것입니다.

 


그러나 서버도 소켓을 열고 -> stream과 Reader를 얻어낸 후 -> 데이터를 읽고 -> 계산한 결과값을 전송한다는 기본 구조는 변함이 없습니다. 이번에는 이 과정이 계속해서 반복된다는 점이 다를 뿐이죠. 서버이기 때문에 가장 앞 부분에 서버소켓을 만드는 부분을 더 추가해야 할 것 같군요. 기억하시겠지만, 소켓 연결은 가장 먼저 서버 소켓을 만든 후, 여기에서 소켓을 얻어내고 클라이언트 쪽에서는 이 소켓에 연결을 합니다. 아무튼 기본 구조는 비슷할 테니 서버쪽도 클래스 설계를 해봅시다.

가장 먼저 해야할 일은 적당한 포트에 서버 소켓을 만드는 일입니다. 생성자에서 해도 좋고 메소드를 하나 만들어도 좋습니다.

 

ServerSocket srvSocket=new ServerSocket(6000);

 

과 같은 식으로 하면 되겠죠. 다음에 할 일은 서버 소켓에서 accept()메소드를 이용해서 클라이언트와 연결할 소켓을 만드는 일입니다.

 

Socket socket=srvSocket.accept();

 

와 같이 하면 소켓을 열 수 있습니다. 여기까지는 프로젝트1에서와 같지만, 여기서부터 Thread를 사용합니다. 여기서 만들어준 소켓을 인자로 넣어서 Stream과 Reader, Writer를 만드는 일은 Thread를 하나 생성해서 일을 맞기는 것이죠. 서버는 계속해서 다른 클라이언트의 연결을 기다리고, 클라이언트와의 데이터 송수신은 생성된 Thread가 맡게 되는 것입니다.

즉, 서버는 서버 소켓을 만들어, 클라이언트의 연결을 기다리다가, 클라이언트가 연결이 되면, Thread를 하나 생성해서 만들어진 socket을 넘겨주고나서는 thread를 시작시키고서 다시 다른 클라이언트의 연결을 기다리게 됩니다. 

지금까지의 과정을 정리해 봅시다.

 

ServerSocket srv=new ServerSocket(port);      // 서버 소켓을 만듭니다. port는 포트 번호
while (true) {
  Socket socket=srv.accept();         // 클라이언트의 연결을 기다립니다.
                                                   // 연결되면 다음행으로 진행됩니다.
  ServerThread t=new ServerThread(socket);  // Thread를 생성. 이때 소켓을 인자로 넘김.
  t.start();                                      // 생성된 Thread를 구동시킵니다.
}

 

서버 클래스에서 할 일은 이정도가 다입니다. 서버 소켓을 만들고서는, 클라이언트 소켓을 연결해서 Thread를 생성해서 구동시키는 일은 무한루프로 돌고 있는 것을 알 수 있습니다. while (조건) { 항목 } 문은 괄호 안의 조건이 성립되면 블럭으로 싸인 항목들이 계속해서 실행되는데, 괄호 안의 조건이 true니까 절대로 끝날 일이 없는 무한 루프겠죠. 물론 Control-C를 누르면 종료됩니다. 아무튼 이 부분은 무한 루프로 돌고 있는데, 따라서 서버는 계속해서 돌면서 클라이언트의 연결을 기다리다가 클라이언트가 연결되면 socket을 연결하고 Thread를 만들어서 socket을 넘겨주는 일을 반복하게 되겠죠. 무한히 많은 클라이언트들이 연결될 수 있습니다. 자바가 네트워크 프로그래밍에 강한 이유 중의 하나가 Thread에도 있습니다. 하나의 서버에 많은 클라이언트들이 연결되더라도 이들을 따로 관리할 필요가 없습니다. 그냥 각각 별개의 thread를 만들어주기만 하면 thread내에서 알아서 일을 하기 때문입니다.

 

 

Threadclass에서는 결과값을 전송한 후 다시 처음으로 가서 클라이언트의 데이터 전송을 기다리게 됩니다. 서버는 루프를 돌면서 클라이언트 thread를 구동한 후 다시 다른 클라이언트의 연결을 기다려, 연결이 있다면 새로운 thread를 다시 생성합니다.

ServerThread는 구현해야 하는 클래스입니다. 사실 어려운 부분은 이부분이죠. 물론 다른 이름을 써도 전혀 상관은 없습니다. 그럼 ServerThread가 해야 하는 일을 짚어 봅시다. 클라이언트에서 연결된 소켓을 넘겨받았으니까 이 소켓에서 Stream을 만들어서 StreamReader와 Writer를 만들어내야 합니다.

InputStream is=socket.getInputStream();
OutputStream os=socket.getOutputStream();

InputStreamReader isr=new InputStreamReader(is);
OutputStreamWriter osw=new OutputStreamWriter(os);

그러나 우리는 BufferedReader와 BufferedWriter를 사용하기로 했습니다. 만들어놓은 InputStreamReader와 Writer를 BufferedReader와 Writer의 생성자에 넣어 주면 됩니다.

BufferedReader br=new BufferedReader(isr);
BufferedWriter bw=new BufferedWriter(osw);

is, isr, br / os, osw, bw 같은 많은 변수들을 만들기 귀찮은면 그냥 한 번에 끝내버리는 방법도 있습니다.


BufferedReader br=new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
 


이제 남은 일은 br.readLine()에서 패킷을 읽어서 결과를 계산한 후 bw.write()를 이용해서 전송하기만 하면 됩니다.

String packet=br.readLine();
String result=analyzePacket(packet); // 패킷을 처리한다. String result에 결과 값이 저장된다.
bw.write(result);
bw.newLine();
bw.flush();


이렇게 하고 exception 처리만 하면 사실상 소켓 연결 부분은 모두 끝납니다. 남은 작업은 실제 프로그래밍에서 많이 하게 되는 일인데, 문자열(String)로 입력된 데이터를 분석하여 정수와 연산자로 나누고서 결과를 계산한 다음, 클라이언트에 전송할 패킷을 만드는 일입니다. 바로 analyzePacket()메소드에서 할 일이죠. 앞에서 프로토콜을 정의하고, 소켓을 Buffered로 할 것인지 그냥 StreamReader, Writer로 할 것인지를 고민했던 문제는 바로 이 부분을 쉽게할 수 있도록 하기 위해서라고 해도 틀린 말이 아닙니다. 지금은 데이터가 적어서 많이 어려워지거나 쉬워지지는 않지만, 데이터 량이 많거나 데이터의 종류가 다양하면 프로그래밍에서 가장 어려운 부분은 바로 이 부분이 됩니다.

 

 

5) 클래스 구현2: 서버 데이터 수신과 전송

서버 소켓을 만들고 클라이언트 소켓을 연결해서 쓰레드로 연결시키는 것 까지는 지금까지 설명이 되었다. 한 번 실제 코딩과 비슷하게 알고리즘을 그려보자. 서버 클래스인 CalculServer.java에서 할 일은 간단합니다. 그 알고리즘은 앞에서 설명하였습니다. 하지만 어떤 일을 어떤 메소드에서 해야하고 밖에서는 어떻게 호출해줄 것인지 하는 일들은 클래스 설계에서 각자 해보았을 것입니다. 그럼 제가 한 클래스 설계를 보여드리겠습니다. 뭐 표준안이라고 할 수는 없을테니까 다르더라도 너무 상심하지는 마세요.

// CalculServer.java


public class CalculServer {


  public void socket_connection(int port);    // 서버 소켓 생성
  public void listening();                    // 클라이언트의 연결을 기다리고,
                                              // 연결되면 thread를 구동
}


class ServerThread extends Thread {
  
  public void run();            // 소켓에서 reader, writer를 얻어서 데이터를 받는다.
                                // analyzePacket()을 호출하여 결과를 얻어 전송한다.
  public String analyzePacket(String inputPacket);
                                // 패킷 내용을 분석하여 계산하고
                                // 클라이언트에 전송할 패킷을 만들어 반환한다.

}

이렇게 보니까 클라이언트 클래스보다 더 간단하네요. 하지만 내용은 그리 쉽지 않습니다. analyzePacket()메소드를 구현하는 것이 쉽지가 않겠죠? "+:13:24"라는 String을 분석해서 "s:37"이라는 String을 만들어낼 수 있어야 하니까요.

아무튼 클래스 두 개로 구현되어 있네요. ServerThread클래스는 CalculServer클래스 안에 포함 되어도 좋고 지금처럼 밖에 나와있어도 상관없습니다. 하지만 public클래스는 CalculServer클래스 하나밖에 없어야 합니다. ServerThread도 아예 ServerThread.java라는 다른 화일에 구현하는 것은 상관없습니다.

ServerThread클래스는 Thread를 상속하고 있네요. 이렇게 되면 이 클래스는 thread가 되는 것입니다. CalculServer클래스에서 ServerThread클래스를 생성해서 start()메소드를 호출하면 자동으로 ServerThread의 run()메소드가 호출됩니다. 클라이언트에서 데이터를 입력받아서, analyzePacket()메소드를 사용하여 결과값을 만들어서 클라이언트에 보내주면 됩니다. 이 과정은 물론 루프로 돌고 있어야 하고, 만약 클라이언트의 연결이 종료되면 루프가 끝나면서 run()메소드도 종료되어야 합니다. run()메소드의 실행이 끝나면 thread는 자동으로 끝나게 됩니다.


클래스 구현에 대해서는 특별히 더 할 이야기가 없네요. 어떻게 하는지를 알아야 설계가 가능하기 때문에 구현 방법까지 전부 다 설명을 해버렸더니... 그냥 넘어가면 서운하니까 여기서는 두 가지만 설명을 하도록 하겠습니다. run()메소드에서 소켓의 연결을 기다리는 루프와 소켓 연결이 끊겼을 경우를 찾는 것과 exception처리의 좀더 고급 기술을 설명하겠습니다.

먼저 run()메소드는 앞서 말했듯이 루프를 돌고 있어야 합니다. 왜냐면 클라이언트가 데이터를 하나만 전송하고 끝내는 것이 아니라 계속해서 연결을 유지하면서 데이터를 주고 받아야 하기 때문입니다. 따라서 클라이언트가 접속되어 있는 한 이 Thread는 유지되어야만 하고, 클라이언트의 연결이 종료되면(사용자가 프로그램을 끊거나, 랜선이 끊어졌거나, 컴퓨터가 꺼졌거나.. 등등), 자동으로 Thread는 종료될 수 있어야 합니다. 대단히 어려운 작업인 것 같지만, 실제 구현은 간단합니다. 클라이언트의 접속이 끊기면 데이터를 읽으려고 할때 즉시 IOException이 나기 때문입니다. 즉, 그냥 데이터를 읽다가 exception만 잡아주면 됩니다. 예외상황이 발생하면 클라이언트와 연결이 끊어진 것이니까, 루프에서 빠져나가서 소켓을 끊고 run()메소드를 종료하면 thread도 종료되는 것입니다.


public void run() {
  String rcvPacket="";
  String sndPacket="";
 
  while (true) {

    try {
      rcvPacket=br.readLine();
      sndPacket=this.analyzePacket(rcvPacket);
      bw.write(sndPacket);
      bw.newLine();
      bw.flush();
    } catch (IOException e) {
      System.out.println("클라이언트의 연결이 끊겼습니다. 소켓을 끊겠습니다.");
      break;                   // while 루프 밖으로 나간다.
    }
  }

       // 소켓 연결을 종료한다.
  try {
    br.close();
    bw.close();
    socket.close();
  } catch (IOException e) {
  }

}
while(true) 로 인해 무한 루프를 돌게 되지만, 데이터를 읽거나 쓸때 exception이 나오면 바로 메시지를 출력하고 루프를 빠져 나오게 작성되어 있는 것을 볼 수 있습니다.

다음엔, exception처리를 배워봅시다. 지금까지 try - catch를 이용해서 exception을 잡아냈습니다. 하지만 어떤 메소드는 그냥 호출하면 되지만, 어떤 메소드는 try - catch 로 exception을 잡아주지 않으면 컴파일까지 되지 않는다는 것은 두 종류의 메소드에 어떤 차이점이 존재한다는 말이겠죠. 물론입니다. 그런 메소드를 작성할 수도 있고, 꼭 그렇게 작성해주어야 하는 경우도 있습니다. 다음 메소드를 보기로 합시다.


public void socket_connection() throws IOException {
  try {
    Socket socket=new Socket("127.0.0.1", 6000);
  } catch (IOException e) {
    throw e;
  }

}
설명을 하지 않아도 어떤 내용인지는 금방 이해할 수 있을 것입니다. 메소드를 선언할 때 뒤에 throws <Exception 종류> 를 붙여주었습니다. 이런 메소드를 사용할 때는 반드시 try - catch를 사용하여 exception을 잡아주어야만 합니다. 메소드 중간에 exception이 발생하면 throw e; 라는 명령을 실행시키는 것을 볼 수 있습니다. 이것은 발생한 exception을 그대로 메소드 바깥으로 전달한다는 의미입니다. 이렇게 하면 메소드 안에서 exception이 발생하면 그 exception은 메소드 안에서 처리되지 않고 메소드를 호출한 곳까지 exception이 그대로 전달되게 됩니다.

이런 방법은 대부분의 경우 메소드 내에서 exception을 처리하는 것보다 훨씬 좋은 방법입니다. 다시 말하면 최종적으로 사용하는 곳에서 exception을 처리하는 것이 훨씬 좋다는 의미이죠. 왜냐하면 위의 메소드에서 소켓 연결에 실패해서 exception이 발생했다고 칩니다. 지금처럼 메소드를 전달하지 않고, 이전에 하던 방식으로 System.out.println(e.toString()); 과 같은 식으로 에러 메시지를 내보낼 수도 있습니다. 그러나 awt나 swing을 사용하는 어떤 프로그램에서 이 클래스를 사용하여 네트워크 프로그램을 작성하고자 한다고 생각해보세요. 만약 어떤 exception이 나면 Text Area나 Label 같은 윈도우 안에 나타나게 하고 싶지만, 메소드 바깥에서는 어떤 exception이 났는지 알 수도 없고, System.out.println()명령으로는 Text Area에 출력할 수도 없습니다. 따라서 메소드 내에서 exception이 나면 그 exception은 안에서 처리하는 것보다는 대부분 메소드를 호출한 곳에서 처리해 주는 것이 좋습니다. 물론 이것은 '대부분'의 경우이고, 그렇지 않을 수도 있습니다.

 

 

6) 클래스 구현3: 서버 패킷 분석과 결과값 계산

analyzePacket()메소드를 구현해봅시다. 클래스구현1(클라이언트)에서 했던 것 같이 StringTokenizer를 사용합니다. 과정은 다음과 같습니다.

인자로 넘겨받은 문자열(String rcvPacket)과 분리자( delemeter = : )을 넣어서 StringTokenizer st를 생성합니다.

st.nextToken()으로 연산자를 얻습니다.
st.nextToken()으로 첫번째 수를 얻습니다.
st.nextToken()으로 두번째 수를 얻습니다.
연산자가 +이면 두 수를 더하고, -이면 두수를 빼고...
계산에 성공하였으면 "s:"+결과값 으로 클라이언트에 전송할 패킷을 만듭니다. 실패이면 "f:"+에러메시지 로 전송할 패킷을 만듭니다.
만든 패킷을 반환합니다.(return)
물론 클라이언트에서 항상 올바른 패킷만 넘어오라는 법은 없으므로 에러처리를 해주어야 합니다. 연산자가 틀리거나, 0으로 나누거나, 숫자가 아닌 문자가 넘어오거나 하는 경우들입니다. 에러처리는 각자 구현해보도록 합니다.

 

 

7) 참고예제: 제가 작성한 프로그램입니다.

제가 작성한 프로그램입니다. 한 번 실행시켜보시고, 각자 작성한 코드와 소스를 비교해 보세요.

CalculServer.java : 계산기 서버입니다. 포트 번호를 파라미터로 줄 수 있습니다.(default=5777) 사용방법: java CalculServer [port]
CalculClient.java : 계산기 서버와 통신을 통해 계산을 합니다. main함수는 테스트 용으로만 만들었기 때문에, 사용자 입력을 받을 수 없고 그냥 세 가지 계산을 하고 종료됩니다.
Calculator.java : 계산기입니다. 키보드로부터 입력을 받아서 CalculClient를 사용하여 계산을 수행합니다. 키보드로부터 입력 받는 것도 지금까지의내용을 통해 금방 이해할 수 있습니다.

 

사용방법: java Calculator [port] [Server IP]
먼저 세 화일을 컴파일하고서 서버를 실행시킵니다.

SHELL> java CalculServer [port 번호]

다음은 다른 콘솔에서 Calculator를 실행시킵니다.

SHELL> java Calculator [port 번호] [Server IP]

 

[출처] Java 소켓통신예제|작성자 뮤즈


 
 

Total 22
번호 제   목 조회
22 Java SE 6 한글 API 문서입니다. 4142
21 Stream for Data Transmision 2566
20 setAutoCommit(false) 에 대해서... 7743
19 java.net 패키지 - Url 클래스 5505
18 소켓 연결 샘플 4888
17 Java 소켓통신예제 계산기 채팅프로그램 6756
16 Java 소켓통신예제 계산기 프로그램 5122
15 Java 소켓통신예제 Socket in TCP/IP protocol 14726
14 Java 소켓통신예제 13335
13 자바 컴파일과 실행 에 사용되어지는 javac, java 명령어의 옵션 6049
12 자바 api 메뉴얼 4003
11 java.util 패키지 5310
10 J2SE, J2EE, J2ME 3852
9 이클립스 사용방법 9390
8 리눅스 자바설치 4101
7 IBM 이클립스의 기본적인 사용법을 설명 4611
6 자바언어의 특징 4233
5 자바 API 문서에서 substring 메서드의 사용방법 입니다. 11151
4 JAVA MySQL 연동하기 9271
3 JAVA 데이터 베이스 드라이버 명칭과 데이터베이스 URL 3813
 1  2  
 
개인홈페이지 덤벙닷컴은 프로그래머와 디자이너위한 IT커뮤니티 공간입니다.
Copyright ⓒ www.dumbung.com. All rights reserved.