'개발/자바'에 해당되는 글 41건

  1. 2008.12.11 [JAVA]16진수 ascii문자를 특정 문자로 치환하기
  2. 2008.12.10 jNetServer Socket Framework
  3. 2008.12.09 eclipse TCP/IP 모니터 사용하기
  4. 2008.12.08 [JAVA] NIO 를 이용한 Echo Server / Client 4
  5. 2008.12.08 Stream과 Socket
  6. 2008.10.14 RED5
  7. 2008.10.02 TortoiseSVN 설치 및 사용방법
  8. 2008.10.01 JUnit 을 이용한 단위 테스트
  9. 2008.10.01 JUnit 기본 사용법
  10. 2008.09.30 [Doc] MyEclipse 를 사용한 Struts + Spring + Hibernate 통합에 관련 문서
2008. 12. 11. 14:45

[JAVA]16진수 ascii문자를 특정 문자로 치환하기




내용 : byte array로 저장된 data에 특수문자값을 찾아서 정의된 문자로 치환하기
전제 : 10진수 0부터 32까지 문자만 그 대상으로 한다

process :
I. HashMap에 (1)16진수값 = 이름 (2) 이름 = 16진수값 이 매핑되도록 설정
II. byte array를 parameter로 받아 loop로 array 탐색
III. byte array를 16진수로 변환하여 HashMap에 그 내용이 있는지 확인
IV. 관련된 내용이 발견되면 치환

번외: 위 작업한 내용을 다시 정상 data로 복원 시키는 모듈

(CODE)
1. Integer.toHexString(int i)
설명 : 10진수 int형 i 값을 16진수로 변환한다.

public String hex2string(byte[] b, String prefix){
 if(b == null || b.length <= 0) return null;  
 StringBuffer sb = new StringBuffer();  
 for(int i=0; i<b.length; i++){  
  int j = b[i];
  if(j < 33){
   if(ASCII_HEX_TABLE.get(Integer.toHexString(j)) != null){
    sb.append(prefix+ASCII_HEX_TABLE.get(Integer.toHexString(j))+prefix);
   }
  }else{
   sb.append((char)b[i]);
  }
 }  
 return sb.toString();
}

(추가) 한글로 값이 넘어오는 경우 처리
  public String hex2string(byte[] b, String prefix){
       if(b == null || b.length <= 0) return null;
      
       try{
           String s = new String(b, "MS949");
      
           StringBuffer sb = new StringBuffer();
          
           for(int i=0; i<s.length(); i++){
               int j = s.charAt(i);
               if(j < 33){
                   if(ASCII_HEX_TABLE.get(Integer.toHexString(j)) != null){
                       sb.append(prefix+ASCII_HEX_TABLE.get(Integer.toHexString(j))+prefix);
                   }
               }else{
                   sb.append((char)s.charAt(i));
               }
           }
          
           return sb.toString();
       }catch(Exception e){return null;}
   }


2. (char)Integer.parseInt(String.valueOf(Long.decode("0x"+hexCode)))
설명 : 16진수 문자값을 그에 해당하는 문자로 변환시킴
Long.decode("0x"+hexCode) : 16진수 문자값(: hexCode)을 16진수 숫자값으로 변환

public byte[] string2hex(String s, String prefix){
 if(s == null || s.equals("")) return null;
 
 StringBuffer sb = new StringBuffer();
 
 StringTokenizer st = new StringTokenizer(s,prefix);
 String partString = "";
 String hexCode = "";
 while (st.hasMoreTokens()) {
  partString = st.nextToken();
  if(partString != null && !partString.equals("")){
   if(ASCII_NAME_TABLE.get(partString) != null){
    hexCode = (String)ASCII_NAME_TABLE.get(partString);
    sb.append((char)Integer.parseInt(String.valueOf(Long.decode("0x"+hexCode))));
   }else{
    sb.append(partString);
   }
  }   
 }
 return sb.toString().getBytes();
}
2008. 12. 10. 17:59

jNetServer Socket Framework




jNetServer1.0 Readme

 

     Network Server Application을 구현할 때 마다 느끼는 것이지만 맨날 반복되는 코드에
    지겹다는 생각을 많이 했다. 물론 초보자에게는 Thread, Socket, IO 관련 프로그램
    이 어렵게 느껴지겠고, 그리고, Thread Pool, Concurrency Issue등의 문제도 골치가 
    아플것이다. 아마도 이런 이유 때문에 application server를 쓰는지도 모르겠다. 하
    지만 application server에서는 Socket Connector는 제공하지 않는다.
     어째든 server application을 재활용하기 위한 차원에서 Socket Server Framework
    을 만들어 보았다. 요즘 프로젝트가 다 웹이라 이런 Socket server를 얼마나 많이 개발
    할지는 모르겠지만, 지금 참여하고 있는 프로젝트(EAI)에서는 중요한 부분으로 사용
    되고 있다. (2003.11.23 일요일)
 
 written by Jeon HongSeong [hsjeon70@dreamwiz.com]      
jNetServer1.0 Java Documentation API
 

1. 개 요

 

 ◆ jNetServer1.0 개발 시 이용한 open source List
- MX4J-1.1 : JMX1.0 Reference Implementation - Jakarta Log4J-1.2.8 - Jakarta Common Digester-1.5 - Jakarta Common Pooling-1.1 - Jakarta Tomcat source 일부
 

 ◆ jNetServer Framework은 Java Network Server Programming에 대한 Basic 
    Infrastructure를 제공한다.
 ◆ Network Server Application을 구현할 때 필요한 Thread Pooling이나, Object 
    Pooling, Logger 등의 기능을 제공한다.
 ◆ API에 정의된 NetTask 내 클라이언트로부터 데이터를 read하고, 결과를 write하는
    로직만 간단히 구현하면 된다.
 ◆ Configuration 설정만으로 Network Server Connector, InputAdapter, Object 
    Pool이 생성 초기화 된다.
 ◆ 클라이언트 시스템(IP) 별로 접근 권한 및 통신 프로토콜을 다르게 정의할 수 
    있다.
 ◆ 모든 Java Object들이 JMX MBean object로 관리되고, MX4J에서 제공하는 JMX Http
    Admin 관리 콘솔을 제공한다.
 ◆ 관리 콘솔 상에서 서버 및 모든 MBean object를 제어 할 수 있다.
 ◆ JMX Monitor Bean을 이용해 Connector의 상태를 Counter, Gauge 방식으로 모니터
    링 할 수 있다.
 ◆ jNetServer Framework 내에서 configuration 설정 만으로 SSL(Secure Socket Layer)
    를 지원한다. SSL용 NetTask의 구현은 일반 Socket 일때와 동일하다.
 ◆ NetTask의 확장으로 프락시 서버, 로드 발런스 등의 서버를 쉽게 구현할 수 있다.
	
 

2. 설치 및 실행

 
 ◆ jNetServer1.0-app.jar 파일을 c:\jNetServer1.0 디렉토리 밑에 압축을 해제한다.
	
C:\jNetServer1.0>jar xvf jNetServer1.0-app.jar
C:\jNetServer1.0>dir
C 드라이브의 볼륨에는 이름이 없습니다.
볼륨 일련 번호: 3D26-12D4

C:\jNetServer1.0 디렉터리

2003-11-29  12:48a      <DIR>          .
2003-11-29  12:48a      <DIR>          ..
2003-11-29  12:48a                 637 runSsl.bat
2003-11-29  12:48a                 789 startup.bat
2003-11-29  12:48a                  18 lcp.bat
2003-11-29  12:48a                 705 runMulti.bat
2003-11-29  12:48a                 720 stop.bat
2003-11-29  12:48a                 645 runEcho.bat
2003-11-29  12:48a      <DIR>          logs
2003-11-29  12:48a      <DIR>          docs
2003-11-29  12:48a      <DIR>          server
2003-11-29  12:48a      <DIR>          common
2003-11-29  12:48a      <DIR>          config
              6개 파일           3,514 바이트
              7 디렉터리   2,378,612,736 바이트 남음
 
 ◆ jNetServer1.0/config/server.xml에 Server 태그의 address 속성을 설치 시스템의 
    IP 주소로 변경한다.	

   <ServerGroup>
         <Server info="jNetServer1.0" 	name="svr1" 
                     address="192.168.0.13" 	port="8110" 
                     mode="standalone"	shutdown="SHUTDOWN">

 ◆ startup.bat 파일을 실행하여 jNetServer를 start 시킨다. 이때 keystore file의 
 비밀번호를 입력해야 하는데 "java11"을 입력한다. 

C:\jNetServer1.0>startup
CLASSPATH=.;D:\bea\weblogic81\server\lib\weblogic.jar;server\lib\jNetServer1.0.j
ar;server\lib\jakarta\commons-configuration-0.8.1.jar;server\lib\jakarta\commons
-digester.jar;server\lib\jakarta\commons-net-1.0.1-dev.jar;server\lib\jakarta\co
mmons-daemon.jar;server\lib\jakarta\commons-collections.jar;server\lib\jakarta\c
ommons-logging-api.jar;server\lib\jakarta\commons-beanutils.jar;server\lib\jakar
ta\commons-logging.jar;server\lib\jakarta\commons-pool-1.1.jar;server\lib\jakart
a\commons-dbcp.jar;server\lib\jakarta\commons-lang.jar;server\lib\log4j\log4j-1.
2.8.jar;server\lib\mx4j\mx4j-jmx.jar;server\lib\mx4j\mx4j-tools.jar;common\class
es;config
[00:51:29]  INFO - @ jNetServer1.0 @@@@@@@@@@@@@@@@@@@@@@@@@@
[00:51:29]  INFO - >> jnet.home=.
[00:51:29]  INFO - >> jnet.server=svr1
[00:51:29]  INFO - >> Logger=jnet.logger
Enter the password of the keystore file : java11

[00:54:12]  INFO - >> Including directory C:\jNetServer1.0\.\common\classes
[00:54:12]  INFO - StandardConnector{7130} listen 192.168.0.13
[00:54:12] DEBUG - EchoTask#taskCreate()- EchoTask{echo1}[0]
[00:54:12] DEBUG - EchoTask#taskPassivate()- EchoTask{echo1}[0]
[00:54:12] DEBUG - EchoTask#taskCreate()- EchoTask{echo1}[1]
[00:54:12] DEBUG - EchoTask#taskPassivate()- EchoTask{echo1}[1]
[00:54:12] DEBUG - EchoTask#taskCreate()- EchoTask{echo1}[2]
[00:54:12] DEBUG - EchoTask#taskPassivate()- EchoTask{echo1}[2]
[00:54:12]  INFO - StandardTask{echo1} started
[00:54:12]  INFO - StandardInputAdapter{everyone} started
[00:54:12]  INFO - StandardMonitor[idleHandlers] started
[00:54:12]  INFO - StandardMonitor[curHandlers] started
[00:54:12]  INFO - Handler{7130}[0] has been started
[00:54:12]  INFO - Handler{7130}[1] has been started
[00:54:12]  INFO - Handler{7130}[2] has been started
[00:54:12]  INFO - Handler{7130}[3] has been started
[00:54:12]  INFO - Handler{7130}[4] has been started
[00:54:12]  INFO - StandardConnector{7130} started
[00:54:12]  INFO - StandardConnector{7131} listen 192.168.0.13
[00:54:13] DEBUG - SSLProxyTask#taskCreate()- SSLProxyTask{proxy}[0]
[00:54:13] DEBUG - SSLProxyTask#taskPassivate()- SSLProxyTask{proxy}[0]
[00:54:13] DEBUG - SSLProxyTask#taskCreate()- SSLProxyTask{proxy}[1]
[00:54:13] DEBUG - SSLProxyTask#taskPassivate()- SSLProxyTask{proxy}[1]
[00:54:13] DEBUG - SSLProxyTask#taskCreate()- SSLProxyTask{proxy}[2]
[00:54:13] DEBUG - SSLProxyTask#taskPassivate()- SSLProxyTask{proxy}[2]
[00:54:13] DEBUG - SSLProxyTask#taskCreate()- SSLProxyTask{proxy}[3]
[00:54:13] DEBUG - SSLProxyTask#taskPassivate()- SSLProxyTask{proxy}[3]
[00:54:13] DEBUG - SSLProxyTask#taskCreate()- SSLProxyTask{proxy}[4]
[00:54:13] DEBUG - SSLProxyTask#taskPassivate()- SSLProxyTask{proxy}[4]
[00:54:13]  INFO - StandardTask{proxy} started
[00:54:13]  INFO - StandardInputAdapter{192.168.0.13} started
[00:54:13] DEBUG - SSLEchoTask#taskCreate()- SSLEchoTask{echo2}[0]
[00:54:13] DEBUG - SSLEchoTask#taskPassivate()- SSLEchoTask{echo2}[0]
[00:54:13] DEBUG - SSLEchoTask#taskCreate()- SSLEchoTask{echo2}[1]
[00:54:13] DEBUG - SSLEchoTask#taskPassivate()- SSLEchoTask{echo2}[1]
[00:54:13] DEBUG - SSLEchoTask#taskCreate()- SSLEchoTask{echo2}[2]
[00:54:13] DEBUG - SSLEchoTask#taskPassivate()- SSLEchoTask{echo2}[2]
[00:54:13] DEBUG - SSLEchoTask#taskCreate()- SSLEchoTask{echo2}[3]
[00:54:13] DEBUG - SSLEchoTask#taskPassivate()- SSLEchoTask{echo2}[3]
[00:54:13] DEBUG - SSLEchoTask#taskCreate()- SSLEchoTask{echo2}[4]
[00:54:13] DEBUG - SSLEchoTask#taskPassivate()- SSLEchoTask{echo2}[4]
[00:54:13]  INFO - StandardTask{echo2} started
[00:54:13]  INFO - StandardInputAdapter{everyone} started
[00:54:13]  INFO - Handler{7131}[0] has been started
[00:54:13]  INFO - Handler{7131}[1] has been started
[00:54:13]  INFO - Handler{7131}[2] has been started
[00:54:13]  INFO - Handler{7131}[3] has been started
[00:54:13]  INFO - Handler{7131}[4] has been started
[00:54:13]  INFO - StandardConnector{7131} started
[00:54:13]  INFO - StandardServer{svr1} started
[00:54:13]  INFO - StandardServerGroup started
[00:54:13]  INFO - ConsoleGaugeListener>> MonitorNotification [ sequence=1, time
Stamp=Sat Nov 29 00:54:13 KST 2003, type=jmx.monitor.gauge.low, userData=null, m
essage=, derivedGauge=5, observedObject=jnet.server:name=svr1/con7130, observedA
ttribute=IdleHandlers, trigger=5, source=javax.management.monitor.GaugeMonitor@c
623af ]
[00:54:13]  INFO - ConsoleCounterListener>> MonitorNotification [ sequence=1, ti
meStamp=Sat Nov 29 00:54:13 KST 2003, type=jmx.monitor.counter.threshold, userDa
ta=null, message=, derivedGauge=5, observedObject=jnet.server:name=svr1/con7130,
 observedAttribute=CurHandlers, trigger=3, source=javax.management.monitor.Count
erMonitor@50ca0c ]
	

3. Admin Console

 
 ◆ jNetServer가 실행되면, Http Admin Console에 접근해 관리할수 있다.
 
 	http://192.168.0.13:8080
 	
    브라우저로 8080 포트로 접근해 보면 아래와 같이 로그온 화면이 나타나는데, 
    jlook/jlook으로 오그온을 한다. admin 계정과 비밀번호는 은 config/server.xml
    에 설정되어 있다.
    
 	
 ◆ 로그인하면 start된 jNetServer의 JMX MBean object를 관리할수 있는 기능과 상태
    를 모니터링 할 수 있는 화면을 제공한다. 	
    
	

4. EchoTask Bean 및 테스트

 
 ◆ 설치된 jNetServer에는 예제로 EchoTask Bean이 제공된다. Task Bean의 
    개발은 jlook.jnet.task.NetTask 인터페이스를 구현하면 된다. 다음은 NetTask의 
    소스이다.       
    
     package jlook.jnet.task;
     
     import java.io.IOException;
     import java.io.BufferedInputStream;
     import java.io.BufferedOutputStream;
     
     /**
      * Socket 요청을 받아 처리할 Beans를 구정의하기 위한 NetTask interface 
      *
      * @since 	jNetServer1.0
      * @author 	HongSeong Jeon(hsjeon70@dreamwiz.com)
      */
     public interface NetTask {
     	/**
     	 * NetTask object의 id를 반환한다.
     	 *	
     	 * @return	id
     	 */
     	public String getId();	
     		
     	/**
     	 * NetTask Beans object 생성 후 callback
     	 */
     	public void taskCreate();
     	
     	/**
     	 * NetTask Beans object가 요청에 의해 pooling으로 부터 나와 할당된 
     	 * 후 callback
     	 */
     	public void taskActivate();
     	
     	/**
     	 * NetTask Beans object가 요청을 처리하고, pooling으로 반환된 후 
     	 * callback
     	 */
     	public void taskPassivate();
     	
     	/**
     	 * NetTask Beans object가 pooling으로 부터 deallocated 될때 callback
     	 */
     	public void taskDestroy();
     	
     	
     	/**
     	 * 클라이언트의 요청을 받아 NetContext object를 초기화하기 위해 
     	 * 실행된다.
     	 *
     	 * @param	context	NetContext object
     	 */
     	public void setContext(NetContext context);
     	
     	/**
     	 * 클라이언트의 요청에 대한 처리로직을 위해 실행된다.
     	 *	
     	 * @param	in	BufferedInputStream object
     	 * @param	out	BufferedOutputStream object
     	 * @exception	TaskException
     	 */
     	public void doTask(BufferedInputStream in, BufferedOutputStream out)
     	throws TaskException;
     	
     	/**
     	 * 클라이언트의 요청에 대한 처리로직을 실행한 후 후처리 작업을 
     	 * 위해 실행된다.
     	 *
     	 * @param	success	doTask() 메서드에서 Exception의 발생여부를 
     	 * 		나타내는 flag
     	 */
     	public void doEnd(boolean success);
     }
- taskCreate() : Task Bean이 생성되고, Bean의 초기화 작업을 위해 호출된다. - taskDestory() : Task Bean이 Object Pool에서 삭제될 때 초기화 작업의 undo를 위해 호출된다. - taskActivate() : Task Bean이 Object Pool에 반환될때 호출된다. - taskPassivate() : Task Bean이 Object Pool로 부터 나와 서비스 되기 직전에 호출된다. - setContext() : 클라이언트의 요청 시 클라이언트의 정보를 갖는 Context 정보를 초기화 하기 위해 호출된다.\ - doTask() : 클라이언트 요청시 setContext() 가 실행된 다음 호출된다. 파라미터의 InputStream, OutputStream을 이용해 실재 해당 요청의 처리로직을 구현한다. - doEnd() : 후처리 작업을 위해 마지막에 호출된다. ◆ NetTask의 구현은 실재 NetTaskSupport 클래스를 상속받아 정의한다. 다음은 제공 되는 EchoTask의 소스이다.
    
     package jlook.jnet.task;
     
     import java.io.*;
     
     import jlook.jnet.Keys;
     import jlook.jnet.util.Logger;
     
     public class EchoTask extends NetTaskSupport {
     	private static Logger logger = Logger.getLogger(Keys.LOGGER);
     	
     	public void taskCreate() {
     		logger.debug("EchoTask#taskCreate()- "+id);	
     	}
     	
     	public void taskActivate(){
     		logger.debug("EchoTask#taskActivate()- "+id);	
     	}
     	
     	public void taskPassivate(){
     		logger.debug("EchoTask#taskPassivate()- "+id);	
     	}
     	
     	public void taskDestroy(){
     		logger.debug("EchoTask#taskDestroy()- "+id);	
     	}
     	
     	public void setContext(NetContext context) {
     		super.setContext(context);
     		logger.debug("EchoTask#setContext()- "+id);	
     	}
     	
     	public void doTask(BufferedInputStream in, BufferedOutputStream out)
     	throws TaskException {
     		Logger logger = context.getLogger();
     		logger.debug("EchoTask#doTask()- "+id);	
     		byte[] msg = new byte[100];
     		logger.debug(id+"#doTask()- read....");
     		
     		try {
 		   int len = in.read(msg);
 		   String str = new String(msg,0,len);
 	
 		   logger.debug(id+"#doTask()- received msg>> "+str);
 		   str = "Hi... "+str;
 		   byte[] rt = str.getBytes();
 		   out.write(rt, 0, rt.length);
     		} catch(Exception e) {
     		   throw new TaskException(e.getMessage());
     		}
     	}	
     	
     	public void doEnd(boolean success) {
     		logger.debug("EchoTask#doEnd()- "+id);	
     		
     	}
     	
     	public String toString() {
     		return "SampleNetTask>>"+id;	
     	}
     }
◆ 개발된 NetTask Bean은 config/server.xml에 다음과 같이 설정해야 한다.
    
     <InputAdapter source="everyone" 		
     		 connectionTimeout="10000" 	
     		 sendBufferSize="10240"
     		 receiveBufferSize="10240"
     	  	 className="jlook.jnet.connector.StandardInputAdapter">     
     	<Task 	name="echo1"	initSize="5"	maxSize="50"
     		className="jlook.jnet.connector.StandardTask"
     		beanClass="jlook.jnet.task.EchoTask"/>
     			
    </InputAdapter>
- name : unique name - initSize : object pool의 initial size - maxSize : object pool의 maximum size - className : Tag에 대한 정보를 갖는 Bean으로 위와 같이 반드시 제공되는 StandardTask 클래스를 설정한다. - beanClass : 개발한 NetTask Bean class를 설정한다. ◆ 다음은 EchoTask에 대한 Client 프로그램이다.
    
     package jlook.jnet.task;
     
     import java.io.*;
     import java.net.*;
     
     public class EchoClient implements Runnable {                            
     	private String server;
     	private int    port;
     	private String message;
     	
     	private boolean success;
     	private Socket sck;
     		
     	public EchoClient(String server, int port, String message) 
     	throws Exception {
	   this.server = server;
	   this.port = port;
	   this.message = message;	
	   try {
	   	sck = new Socket(server, port);
	   } catch(Exception e) {
	   	System.out.println(Thread.currentThread().getName() + 
	   	" Error>> "+e.toString());
	   	sck = new Socket(server, port);
	   }
     	}
     	
     	public static void main(String[] args) throws Exception {
	   if(args.length!=3) {
	   	System.out.println("usage : java EchoClient "+
	   	"  ");
	   	return;
	   }
	   
	   int port = -1;
	   try {
	   	port = Integer.parseInt(args[1]);
	   } catch(Exception e) {
	   	System.out.println("invalid port - "+port);
	   	throw e;
	   }
	   
	   EchoClient ec = new EchoClient(args[0], port, args[2]);
	   Thread t = new Thread(ec);
	   t.start();
     	}
     	
     	public boolean isSuccess() {
     		return success;	
     	}
     	
     	public void run() {
   	   InputStream in = null;
   	   OutputStream out = null;
   	   try {
   	     in = sck.getInputStream();
   	     out = sck.getOutputStream();
   	     	
   	     byte[] b = message.getBytes();
   	     out.write(b, 0, b.length);
   	     	
   	     byte[] buff = new byte[100];
   	     int len = in.read(buff);
   	     
   	     System.out.println(Thread.currentThread().getName()+
   	     "] "+ new String(buff, 0, buff.length));
   	     success = true;
   	     } catch(Exception e) {
   	     success = false;
   	     System.out.println(Thread.currentThread().getName() + 
   	     " Error>> "+e.toString());
   	     e.printStackTrace();
   	   } finally {
   	     try { notifyAll();} catch(Exception e){}	
   	     try { if(in!=null) in.close();} catch(Exception e) {}
   	     try { if(out!=null) out.close();} catch(Exception e) {}
   	     try { if(sck!=null) sck.close();} catch(Exception e) {}
   	   }
     	}
     }    
◆ 위 EchoClient를 테스트 하기 위해 runEcho.bat 파일 내 ip를 수정하고, 다음과 같이 runEcho.bat를 실행한다. C:\jNetServer1.0>runEcho CLASSPATH=.;D:\bea\weblogic81\server\lib\weblogic.jar;lib\jNetServer1.0.jar; classes;config Thread-1] Hi... hongseong ◆ 다음은 jNetServer쪽 콘솔에 나타난 로그 내용이다. 이 로그를 통해 NetTask에 정의된 메서드가 언제 실행되는지 이해할 수 있다. [21:32:48] DEBUG - @ request client address : 192.168.0.13 [21:32:48] DEBUG - StandardInputAdapter#doExecute() is started [21:32:48] DEBUG - EchoTask#taskActivate()- EchoTask{echo1}[4] [21:32:48] DEBUG - EchoTask#setContext()- EchoTask{echo1}[4] [21:32:48] DEBUG - EchoTask#doTask()- EchoTask{echo1}[4] [21:32:48] DEBUG - EchoTask{echo1}[4]#doTask()- read.... [21:32:48] DEBUG - EchoTask{echo1}[4]#doTask()- received msg>> hongseong [21:32:48] DEBUG - EchoTask#doEnd()- EchoTask{echo1}[4] [21:32:48] DEBUG - EchoTask#taskPassivate()- EchoTask{echo1}[4] [21:32:49] DEBUG - Handler{7130}[4] process time : 0.38 sec
 

5. SSL 설정 및 테스트

 
 ◆ config/server.xml의 내용을 보면 다음과 같이 7131 포트가 SSL 로 설정되어 있다.	
	
    
<Connector name="con7131" className="jlook.jnet.connector.StandardConnector" 
	    	port="7131" 		ssl="true"
	        enableLookups="true" acceptCount="50"
	        minHandlers="5" 		maxHandlers="50">
	<InputAdapter 	source="everyone" 		
		connectionTimeout="10000" 	
		sendBufferSize="10240"
		receiveBufferSize="10240"
	  	className="jlook.jnet.connector.StandardInputAdapter">
		
		<Task 	name="echo2" initSize="5" maxSize="50"
		  className="jlook.jnet.connector.StandardTask"
		  beanClass="jlook.jnet.task.SSLEchoTask"/>
				
	</InputAdapter>		
	
	<InputAdapter 	source="192.168.0.10" 		
		connectionTimeout="10000" 	
		sendBufferSize="10240"
		receiveBufferSize="10240"
	  	className="jlook.jnet.connector.StandardInputAdapter">
		
		<Task 	name="proxy" initSize="5" maxSize="50"
		  className="jlook.jnet.connector.StandardTask"
		  beanClass="jlook.jnet.task.SSLProxyTask">
		  <Parameter name="target.server" value="192.168.0.13"/>
		  <Parameter name="target.port"	 value="7130"/>
		  <Parameter name="buffer.size"	 value="9"/>
		</Task>
						
	</InputAdapter>
◆ 7131이 SSL로 설정되어 있으므로 앞에서 실행했던 runEcho.bat 파일의 port를 7131로 수정 후 실행해 보면 다음과 같이 에러가 발생한다. ### Client console] C:\jNetServer1.0>runEcho CLASSPATH=.;D:\bea\weblogic81\server\lib\weblogic.jar;server\lib\jNetServer1.0.j ar;common\classes;config Thread-1] ### jNetServer console] [01:16:24] ERROR - Unrecognized SSL message, plaintext connection? javax.net.ssl.SSLException: Unrecognized SSL message, plaintext connection? at com.sun.net.ssl.internal.ssl.InputRecord.b(DashoA6275) at com.sun.net.ssl.internal.ssl.InputRecord.read(DashoA6275) at com.sun.net.ssl.internal.ssl.SSLSocketImpl.a(DashoA6275) at com.sun.net.ssl.internal.ssl.SSLSocketImpl.j(DashoA6275) at com.sun.net.ssl.internal.ssl.SSLSocketImpl.startHandshake(DashoA6275) ◆ runssl.bat 파일을 오픈해 ip{192.168.0.13}를 시스템에 맞게 변경하고, 실행해 보면 다음과 같은 결과를 확인해 볼수 있다. ### Client console] C:\jNetServer1.0>runssl CLASSPATH=.;D:\bea\weblogic81\server\lib\weblogic.jar;server\lib\jNetServer1.0.j ar;common\classes;config connect to the server.[192.168.0.13, 7131] received message>> Hello hongseong ### jNetServer console] [01:27:12] DEBUG - @ request client address : 192.168.0.13 [01:27:12] DEBUG - StandardInputAdapter#doExecute() is started [01:27:12] DEBUG - InputAdapter>>svr1/con7131/everyone [01:27:12] DEBUG - StandardInputAdapter#doExecute()- start handshake [01:27:12] DEBUG - SSLEchoTask#taskActivate()- SSLEchoTask{echo2}[4] [01:27:12] DEBUG - SSLEchoTask#setContext()- SSLEchoTask{echo2}[4] [01:27:12] DEBUG - SSLEchoTask#doTask()- SSLEchoTask{echo2}[4] [01:27:12] DEBUG - SSLEchoTask{echo2}[4]#doTask()- read.... [01:27:12] DEBUG - SSLEchoTask{echo2}[4]#doTask()- received msg>> hongseong [01:27:12] DEBUG - SSLEchoTask#taskPassivate()- SSLEchoTask{echo2}[4] [01:27:12] DEBUG - Handler{7131}[4] process time : 0.431 sec ◆ 클라이언트 시스템이 192.168.0.13 이므로 클라이언트 시스템에 대하 InputAdapter가 설정되어 있지 않기 때문에 default인 "everyone"의 InputAdapter가 동작한 것이다. 그리고, Task beans는 SSLEchoTask가 실행되었고, 그 소스는 아래와 같다. SSL 관련 추가 코드는 필요하지 않다.
package jlook.jnet.task;

import java.io.*;

import jlook.jnet.Keys;
import jlook.jnet.util.Logger;

public class SSLEchoTask extends NetTaskSupport {
	private static Logger logger = Logger.getLogger(Keys.LOGGER);
	
	public void taskCreate() {
		logger.debug("SSLEchoTask#taskCreate()- "+id);	
	}
	
	public void taskActivate(){
		logger.debug("SSLEchoTask#taskActivate()- "+id);	
	}
	
	public void taskPassivate(){
		logger.debug("SSLEchoTask#taskPassivate()- "+id);	
	}
	
	public void taskDestroy(){
		logger.debug("SSLEchoTask#taskDestroy()- "+id);	
	}
	
	public void setContext(NetContext context) {
		super.setContext(context);
		logger.debug("SSLEchoTask#setContext()- "+id);	
	}
	
	public void doTask(BufferedInputStream in, BufferedOutputStream out) 
	throws TaskException {
		Logger logger = context.getLogger();
		logger.debug("SSLEchoTask#doTask()- "+id);	
		byte[] msg = new byte[100];
		logger.debug(id+"#doTask()- read....");
		
		try {
			int len = in.read(msg);
			String str = new String(msg,0,len);
		
			logger.debug(id+"#doTask()- received msg>> "+str);
			str = "Hi... "+str;
			byte[] rt = str.getBytes();
			out.write(rt, 0, rt.length);
		} catch(Exception e) {
			logger.error(e.getMessage(),e);
			throw new TaskException(e.getMessage());
		}
	}	
	
	public void doEnd(boolean success) {
		logger.debug("SSLEchoTask#doEnd()- "+id);	
		
	}
	
	public String toString() {
		return "SampleNetTask>>"+id;	
	}
}
◆ 다음은 위에서 테스트한 ssl Client application 소스이다. 서버와 동일한 keystore 파일을 이용한 것을 알 수 있다.
package jlook.jnet.task;

import java.io.*;
import java.net.*;
import java.security.*;
import javax.net.*;
import javax.net.ssl.*;

public class SSLEchoClient {
	
  public static void main(String[] args) throws Exception {
	  System.setProperty("javax.net.ssl.trustStore", 
	    "config/jnet.keystore"); 
	  
	  if(args.length!=2) {
	  	System.out.println("java SSLEchoClient  ");
	  	return;	
	  }
	  
	  System.out.println("connect to the server.["+
	    args[0]+", "+args[1]+"]");
	  int port = Integer.parseInt(args[1]);
	  
	  SSLContext ctx;
	  KeyManagerFactory kmf;
	  KeyStore ks;
	  char[] passphrase = "java11".toCharArray();
      
	  ctx = SSLContext.getInstance("TLS");
	  kmf = KeyManagerFactory.getInstance("SunX509");
	  ks = KeyStore.getInstance("JKS");
      
	  ks.load(new FileInputStream("config/jnet.keystore"), passphrase);
      
	  kmf.init(ks, passphrase);
	  ctx.init(kmf.getKeyManagers(), null, null);
      
	  SSLSocketFactory factory = ctx.getSocketFactory();
      SSLSocket sck = (SSLSocket)factory.createSocket(args[0], port);
	  sck.setEnabledCipherSuites(sck.getSupportedCipherSuites());
	  sck.startHandshake();
	  
	  BufferedInputStream in = 
	  	new BufferedInputStream(sck.getInputStream());
	  BufferedOutputStream out = 
	  	new BufferedOutputStream(sck.getOutputStream());
	  
	  String msg = "hongseong";
	  byte[] tmp = msg.getBytes();
	  
	  out.write(tmp, 0, tmp.length);
	  out.flush();
	  
	  byte[] buff = new byte[1024];
	  int len = in.read(buff);
	  
	  System.out.println("received message>> "+new String(buff, 0, len));
	  out.close();
	  in.close();
	  sck.close();	

  }
}
 

6. SSLProxyTask Beans

 
 ◆ NetTask 응용으로 Proxy Server Beans를 소개한다. 서버를 7130, SSL 7131로 서비스 할때
    7131로 들어온 요청을 7130쪽으로 forwarding하는 SSLProxyTask의 소스와 설정을 살펴보자.
    다음은 server.xml의 7131쪽 Connector 설정 부분인데, ssl="true"로 설정된 것을 
    확인할 수 있다. 그리고, 앞 예제와는 다르게 InputAdapter의 source가 현 시스템의 
    ip{192.168.0.13}로 변경된것을 확인할 수 있다. 즉, 192.168.0.13 시스템의 클라이
    언트의 요청을 SSLProxyTask 가 처리한다는 것을 설정한 것이다.
	
    
<Connector name="con7131" className="jlook.jnet.connector.StandardConnector" 
	    	port="7131" 		ssl="true"
	        enableLookups="true" acceptCount="50"
	        minHandlers="5" 		maxHandlers="50">
	<InputAdapter 	source="everyone" 		
		connectionTimeout="10000" 	
		sendBufferSize="10240"
		receiveBufferSize="10240"
	  	className="jlook.jnet.connector.StandardInputAdapter">
		
		<Task 	name="echo2" initSize="5" maxSize="50"
		  className="jlook.jnet.connector.StandardTask"
		  beanClass="jlook.jnet.task.SSLEchoTask"/>
				
	</InputAdapter>		
	
	<InputAdapter 	source="192.168.0.13" 		
	   connectionTimeout="10000" 	
	   sendBufferSize="10240"
	   receiveBufferSize="10240"
  	   className="jlook.jnet.connector.StandardInputAdapter">  
		
	   <Task 	name="proxy" initSize="5" maxSize="50"
	     className="jlook.jnet.connector.StandardTask"
	     beanClass="jlook.jnet.task.SSLProxyTask">
	     <Parameter name="target.server" value="192.168.0.13"/>  
	     <Parameter name="target.port"	 value="7130"/>
	     <Parameter name="buffer.size"	 value="9"/>
	   </Task>
						
	</InputAdapter>
◆ 이제 SSLProxyTask Beans의 소스를 살펴 볼것인데, 위에서 Task의 Parameter 태그는 Task Beans에 넘길 key/value 데이터를 설정한 태그이다. 포워딩할 서버 및 포트 정보를 설정했다. 이것을 어떻게 얻어 내었는지 살펴보기 바란다.
 
package jlook.jnet.task;

import java.io.*;
import java.net.*;

import jlook.jnet.Keys;
import jlook.jnet.util.Logger;

public class SSLProxyTask extends NetTaskSupport {
	private static Logger logger = Logger.getLogger(Keys.LOGGER);
	
	private String 	server;
	private int 	port;
	private int 	bufferSize;
	
	public static final String TARGET_SERVER 	= "target.server";
	public static final String TARGET_PORT 	= "target.port";
	public static final String BUFFER_SIZE 	= "buffer.size";
	
	public void taskCreate() {
		logger.debug("SSLProxyTask#taskCreate()- "+id);	
	}
	
	public void taskActivate(){
		logger.debug("SSLProxyTask#taskActivate()- "+id);	
	}
	
	public void taskPassivate(){
		logger.debug("SSLProxyTask#taskPassivate()- "+id);	
	}
	
	public void taskDestroy(){
		logger.debug("SSLProxyTask#taskDestroy()- "+id);	
	}
	
	public void setContext(NetContext context) {
		super.setContext(context);
		logger.debug("SSLProxyTask#setContext()- "+id);	
		server = context.getParameter(TARGET_SERVER);
		if(server == null) server = "127.0.0.1";
		try {
		  port = Integer.parseInt(context.getParameter(TARGET_PORT));
		} catch(Exception e) {
			port=80;
		}
		try {
		  bufferSize = Integer.parseInt(
		  		context.getParameter(BUFFER_SIZE));
		} catch(Exception e) {
			bufferSize=10240;
		}
	}
	
	public void doTask(BufferedInputStream in, BufferedOutputStream out) 
	throws TaskException {
		Logger logger = context.getLogger();
		logger.debug("SSLProxyTask#doTask()- "+id);	
		
		Socket proxy = null;
		BufferedInputStream 	pin  = null;
		BufferedOutputStream 	pout = null;
		try {
		  proxy= new Socket(server,port);
		  pin  = new BufferedInputStream(proxy.getInputStream());
		  pout = new BufferedOutputStream(proxy.getOutputStream());
		} catch(Exception e) {
		  String msg = "Cannot connect to target system>> "+
			e.getMessage();
		  logger.error(msg, e);
		  byte[] tmp = msg.getBytes();
		  try {
			out.write(tmp, 0, tmp.length);
			out.flush();
	  	  } catch(IOException ex){}
		  return;
		}
		
		try {
			byte[] buff = new byte[bufferSize];
			while(true) {
				int len = in.read(buff);
				if(len>"+id;	
	}
}
◆ 다음은 runssl.bat 파일의 실행 결과이다. 서버 콘솔을 보면 클라이언트 요청이 7131{SSLProxyTask}, 7130{EchoTask} 두번 처리된것을 확인할 수 있다. ### Client console] C:\jNetServer1.0>runssl CLASSPATH=.;D:\bea\weblogic81\server\lib\weblogic.jar;server\lib\jNetServer1.0.j ar;common\classes;config connect to the server.[192.168.0.13, 7131] received message>> Hello hongseong ### Server console] [01:54:29] DEBUG - @ request client address : 192.168.0.13 [01:54:29] DEBUG - StandardInputAdapter#doExecute() is started [01:54:29] DEBUG - InputAdapter>>svr1/con7131/192.168.0.13 [01:54:29] DEBUG - StandardInputAdapter#doExecute()- start handshake [01:54:29] DEBUG - SSLProxyTask#taskActivate()- SSLProxyTask{proxy}[4] [01:54:29] DEBUG - SSLProxyTask#setContext()- SSLProxyTask{proxy}[4] [01:54:29] DEBUG - SSLProxyTask#doTask()- SSLProxyTask{proxy}[4] [01:54:29] DEBUG - @ request client address : 192.168.0.13 [01:54:29] DEBUG - StandardInputAdapter#doExecute() is started [01:54:29] DEBUG - InputAdapter>>svr1/con7130/everyone [01:54:29] DEBUG - EchoTask#taskActivate()- EchoTask{echo1}[2] [01:54:29] DEBUG - EchoTask#setContext()- EchoTask{echo1}[2] [01:54:29] DEBUG - EchoTask#doTask()- EchoTask{echo1}[2] [01:54:29] DEBUG - EchoTask{echo1}[2]#doTask()- read.... [01:54:29] DEBUG - EchoTask{echo1}[2]#doTask()- received msg>> hongseong [01:54:29] DEBUG - EchoTask#doEnd()- EchoTask{echo1}[2] [01:54:29] DEBUG - EchoTask#taskPassivate()- EchoTask{echo1}[2] [01:54:29] DEBUG - SSLProxyTask#doEnd()- SSLProxyTask{proxy}[4] [01:54:29] DEBUG - SSLProxyTask#taskPassivate()- SSLProxyTask{proxy}[4] [01:54:29] DEBUG - Handler{7130}[4] process time : 0.01 sec [01:54:29] DEBUG - Handler{7131}[4] process time : 0.37 sec

 
 written by Jeon HongSeong
2008. 12. 9. 11:42

eclipse TCP/IP 모니터 사용하기




파이어폭스의 파이어버그나 ie의 피들러 또는 상용인 httpwatch 등을 통해서 웹브라우저에서 일어나는 통신의 안 보이는 부분을 볼 수 있습니다. 헤더 영역의 정보 같은 것이죠.

이클립스 WTP에서도 같은 기능을 지원합니다. TCP/IP 모니터뷰를 이용하면 지정된 포트에서 발생하는 교신 정보를 눈으로 확인할 수 있습니다.

설정은 다음과 같이 Preferences 에서 tcp 필터 단어를 입력하면 메뉴가 보입니다.
Add 버튼을 클릭하고 상단의 Local Monitoring port 를 8090 등 사용하지 않는 포트를 입력합니다. 모니터링할 서비스 정보를 입력합니다. 로컬의 톰캣을 모니터링하기 위해서 localhost 포트는 8080 이라고 입력했습니다.

사용자 삽입 이미지


등록을 마치면 해당 모니터링 항목을 선택하고 우측의 Start 버튼을 클릭해서 모니터링을 시작합니다.
사용자 삽입 이미지


모니터링하는 포트로 호출을 합니다. 8090이라고 정했기 때문에 http://localhost:8090/index.jsp 주소로 접근하면 TCP/IP Monitor 뷰가 자동으로 뜨게 됩니다.
사용자 삽입 이미지


TCP/IP Monitor 탭을 더블클릭해서 크게 확대해서 보면 서버로 요청한 주소 목록과 선택한 주소의 헤더 정보와 주고받은 내용들이 하단에 나옵니다.
사용자 삽입 이미지

이미지를 선택하면 해당 이미지도 볼 수 있습니다.
사용자 삽입 이미지


유용하게 쓰시기 바랍니다.
2008. 12. 8. 10:38

[JAVA] NIO 를 이용한 Echo Server / Client




모든 네트워크 프로그래밍의 시작은 Echo 인듯 ...

어디서 긁어서 테스트 해본 소스라 올리기 힘듬

크크 누가 들어올라만은...

[ EchoClient.java ]

import java.io.*;
import java.net.*;

class EchoClient
{ public static void main( String[] args )
throws IOException
{
Socket sock = null;
try
{
sock = new Socket(args[0], Integer.parseInt(args[1]));
System.out.println(sock + ": 연결됨");
OutputStream toServer = sock.getOutputStream();
InputStream fromServer = sock.getInputStream();

byte[] buf = new byte[1024];
int count;
while( (count = System.in.read(buf)) != -1 )
{
toServer.write( buf, 0, count );
count = fromServer.read( buf );
System.out.write( buf, 0, count );
}
toServer.close();
while((count = fromServer.read(buf)) != -1 )
System.out.write( buf, 0, count );
System.out.close();
System.out.println(sock + ": 연결 종료");
} catch( IOException ex )

{
System.out.println("연결 종료 (" + ex + ")");
} finally
{
try
{
if ( sock != null )
sock.close();
} catch( IOException ex ) {}
}
}
}


///////////////////////////////////////////////////////////////////////////////////////////////////

[ EchoServer.java ]

import java.nio.channels.*;
import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.SocketAddress;
import java.util.Set;
import java.util.Iterator;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.CharBuffer;

public class NIOServer implements Runnable {

//어떤 채널이 어떤 IO를 할 수 있는지 알려주는 클래스(Seelctor)
Selector selector;
int port = 9999;

//한글 전송용
Charset charset = Charset.forName("EUC-KR");
CharsetEncoder encoder = charset.newEncoder();

public NIOServer() throws IOException {

//Selector를 생성 합니다.
selector = Selector.open();

//ServerSocket에 대응하는 ServerSocketChannel을 생성, 아직 바인딩은 안됨
ServerSocketChannel channel = ServerSocketChannel.open();
//서버 소켓 생성
ServerSocket socket = channel.socket();

SocketAddress addr = new InetSocketAddress(port);
//소켓을 해당 포트로 바인딩
socket.bind(addr);

//Non-Blocking 상태로 만듬
channel.configureBlocking(false);

//바인딩된 ServerSocketChannel을 Selector에 등록 합니다.
channel.register(selector, SelectionKey.OP_ACCEPT);

System.out.println("---- Client의 접속을 기다립니다... ----");
}

public void run() {
//SocketChannel용 변수를 미리 만들어 둡니다.
int socketOps = SelectionKey.OP_CONNECT | SelectionKey.OP_READ | SelectionKey.OP_WRITE;

ByteBuffer buff = null;

try {

//생성된 서버소켓채널에 대해 accept 상태 일때 알려달라고 selector에 등록 시킨 후
//이벤트가 일어날때 까지 기다립니다. 새로운 클라이언트가 접속하면 seletor는
//미리 등록 했던 SeerverSocketChannel에 이벤트가 발생했으므로 select 메소드에서
//1을 돌려줍니다. 즉 Selector에 감지된 이벤트가 있다면
while(selector.select() > 0) {

//현재 selector에 등록된 채널에 동작이 라나라도 실행 되는 경우 그 채널들을 SelectionKey의
//Set에 추가 합니다. 아래에서는 선택된 채널들의 키를 얻습니다. 즉 해당 IO에 대해 등록해
//놓은 채널의 키를 얻는 겁니다.
Set keys = selector.selectedKeys();
Iterator iter = keys.iterator();

while(iter.hasNext()) {
SelectionKey selected = (SelectionKey)iter.next();
//현재 처리하는 SelectionKey는 Set에서 제거 합니다.
iter.remove();

//channel()의 현재 하고 있는 동작(읽기, 쓰기)에 대한 파악을 하기 위한 겁니다.
SelectableChannel channel = selected.channel();
if(channel instanceof ServerSocketChannel) {

//ServerSocketChannel이라면 accept()를 호출해서
//접속 요청을 해온 상대방 소켓과 연결 될 수 있는 SocketChannel을 얻습니다.
ServerSocketChannel serverChannel = (ServerSocketChannel) channel;
SocketChannel socketChannel = serverChannel.accept();

//현시점의 ServerSocketChannel은 Non-Blocking IO로 설정 되어 있습니다.
//이것은 당장 접속이 없어도 블로킹 되지 않고 바로 null을 던지므로
//체트 해야 합니다.
if (socketChannel == null ){
System.out.println("## null server socket");
continue;
}

System.out.println("## socket accepted : " + socketChannel);

//얻어진 소켓은 블로킹 소켓이므로 Non-Blocking IO 상태로 설정 합니다.
socketChannel.configureBlocking(false);

//소켓 채널을 Selector에 등록
socketChannel.register(selector, socketOps);
}
else {
//일반 소켓 채널인 경우 해당 채널을 얻어낸다.
SocketChannel socketChannel = (SocketChannel) channel;
buff = ByteBuffer.allocate(100);

//소켓 채널의 행동을 검사해서 그에 대응하는 작업을 함
if (selected.isConnectable()){
System.out.println("Client와의 연결 설정 OK~");
if (socketChannel.isConnectionPending()){
System.out.println("Client와의 연결 설정을 마무리 중입니다~");
socketChannel.finishConnect();
}
}
//읽기 요청 이라면
else if(selected.isReadable()) {
//소켓 채널로 데이터를 읽어 들입니다.
try {
socketChannel.read(buff);

//데이터가 있다면
if (buff.position() != 0){
buff.clear();
System.out.print("클라이언트로 전달된 내용 : ");

//Non-Blocking Mode이므로 데이터가 모두 전달될때 까지 기다림
while(buff.hasRemaining()) {
System.out.print((char) buff.get());
}

buff.clear();
System.out.println();
//쓰기가 가능 하다면
if (selected.isWritable()){
String str = "이건 서버에서 보낸 데이터...";

//한글 인코딩
socketChannel.write(encoder.encode(CharBuffer.wrap(str + "
" )));
System.out.println("서버가 전달한 내용 : " + str);
}
}
}
catch (IOException ioe)
{
ioe.printStackTrace();
socketChannel.finishConnect();
socketChannel.close();
}
}
}
}
}
}
catch (Exception e)
{
e.printStackTrace();
}
}
public static void main(String[] args)throws IOException {
NIOServer s = new NIOServer();
new Thread(s).start();
}
}
2008. 12. 8. 09:55

Stream과 Socket




Stream과 Socket

작성자: realmove / 작성일: 2000.5.4 / 대상: 자바 기초

차례

1. Stream

1) Stream 이란?

2) Stream 입력

3) Stream의 사용

2. Socket

1) Socket 이란?

2) Project1: 서버 / 클라이언트

3) Project2: 계산기

4) Project3: 채팅 프로그램







Stream for Data Transmision

1. 스트림이란?

stream

n.
1 개울(brook), 시내, 강(river).
a mountain stream 계류(溪流).
2 흐름, 수류, 분류(奔流); 해류; 기류; 광선. FLOW 類語
the Gulf Stream 멕시코 만류
a stream of tears 흐르는 눈물
down (up) [the] stream 하류(상류)에(로).
3 비유적 [때사상 따위의] 흐름, 경향, 형세(trend); 사조.
the stream of history 역사의 흐름
the stream of thought 사조.
4 연속되는 것, 끊임없이 이어지는 것(사람); 쇄도.
a stream of talk 그칠 줄 모르는 이야기.
5 컴퓨터 스트림 [데이터의 흐름].

dic.naver.com에서 찾아보니까 스트림이 이렇게 나오네요. 모든 컴퓨터 프로그램은 통신을 위해서 스트림을 사용합니다. 하나의 프로그램에서는 광역 변수를 사용하거나 함수에 인자를 전달하는 방식으로 어떤 값을 다른 함수로 전달할 수 있습니다. 하지만 서로 다른 두 개의 프로그램인 경우에는 어떨까요? 두 프로그램은 서로 어떤 함수가 있는지도 모를 뿐더러 상대편이 실행되고 있는지 또는 어떤 일을 하는지조차 알 수 없습니다. 그러한 상황에서 하나의 프로그램에서 다른 프로그램으로 어떤 값을 전달하기 위해서 스트림을 사용합니다.

만일 스트림에 대해서 알고 있는 사람이라면 바로 Socket으로 넘어가도 될 것 같네요.

스트림은 자바에서만 지원하는 것이 아니라 OS차원에서 지원을 하는 기능입니다. 이말은 스트림 통신을 하기 위한 두 프로그램이 반드시 자바로 작성될 필요는 없다는 것을 의미합니다. 하나는 자바로, 다른 하나는 C로 작성될 수도 있고, 다른 어떤 언어를 사용하는 것도 가능하다는 의미입니다.

사실 윈도우나 유닉스, 리눅스 등의 OS를 설치하여 컴퓨터를 사용하는 사람이라면 누구나 항상 스트림을 사용합니다. 예를 들어 윈도우의 도스창(command prompt) 또는 유닉스 콘솔에서 화면에 글자를 찍는 것이나, 프린터에 프린트를 하는 것이 그렇습니다. 우리는 간단하게 System.out.println("내용..."); 라는 명령으로 화면에 찍습니다. 그러나 실제로 println()메소드에서는 주어진 인자의 값을 적당한 장소에 스트림을 사용하여 쭉 보내주게 되죠. 스트림, 즉 하나의 연속적인 흐름으로 보내는 것입니다. 화면에 출력하기 위해서는 화면출력을 담당하는 위치에 스트림으로 보내면 되고, 프린터에 출력하고 싶다면 프린터출력을 담당하는 위치에 스트림으로 보내면 됩니다. 화일에 저장하는 것, 하니면 채팅프로그램처럼 두 프로그램이 통신을 하는 것, 모두 사실은 그런 메카니즘을 가지고 있습니다. 다음은 UNIX에서 파이프라인이라는 기능 속에서 스트림이 어떻게 사용되는지를 보여주는 예입니다.

UNIX를 사용해본 사람이라면 파이프라인을 알고 있을 것입니다. windows NT의 명령창에서도 지원하는 기능인데, ls는 도스에서 dir과 같은 기능을 하죠?
SHELL> ls | more
와 같이 같이 명령을 내리면 어떻게 될까요? 화일이 많은 경우 ls라고 그냥 하면 읽을 새도 없이 화면이 쭉 스크롤 되죠. 하지만 앞에서와 같이 하면 한 화면 만큼씩만 출력되게 됩니다. 실제로 이 작업은 두 개의 프로그램이 돌고 있는 것입니다. ls라는 프로그램과 more라는 프로그램이죠. 그냥 ls라고 쳤을 경우에는 결과를 화면출력을 담당하는 곳에 스트림으로 보내지만 지금처럼 파이프라인으로 연결하면, ls는 결과를 스트림으로 보내게 되고, 이것을 more호라는 프로그램에서 스트림으로 받아서 화면에 한 페이지씩 보여주게 되는 것입니다. 두 프로그램이 통신을 하는 경우 중의 대표적인 예가 바로 이러한 파이프라인이죠.
좀더 나아가면 파이프라인을 여러 개 사용할 수도 있습니다.
SHELL> ls | grep txt | more
짐작하다시피 이번에는 프로그램이 3개가 돌게 됩니다. ls가 화일의 명령을 뽑아서 스트림으로 보내면, 그것을 grep이라는 명령이 받아서 그중 txt라는 문자열을 포함하는 라인만을 골라서 다시 스트림으로 보냅니다. 마지막으로 more라는 프로그램이 앞에서와 같이 스트림으로 받아서 앞에서와 같이 한 화면씩 출력하게 됩니다.

2. 스트림 입력/ 스트림 출력

스트림에는 사실 여러 종류가 있습니다. byte스트림,문자열스트림,데이터그램스트림 등이 그것입니다. 여기서는 사실 문자열(String)스트림만을 다루려고 합니다. byte스트림과 문자열 스트림은 12라는 정수가 "12"로 가느냐(문자열스트림) 아니면 0C(12)인 숫자(byte스트림)으로 가느냐의 차이가 있을뿐 거의 비슷합니다. 데이터그램 스트림은 약간 다르지만 문자열 스트림을 알고나면 금방이해할 수 있을 것입니다.

앞의 UNIX의 파이프라인 예제를 보고 알아챈 사람도 있을테지만, 스트림은 입력과 출력이 있습니다. 즉 스트림을 보내는 것과 받는 것, 두 가지 동작이 있을 수 있다는 이야기입니다. 프린터에 출력하는 경우, 프로그램에서는 프린터스트림에 보내는(출력) 역할을 할 것이고, 프린터는 그것을 스트림에서 받아서(입력) 인쇄하게 되겠죠. 앞의 파이프라인의 경우, 앞에 있는 프로그램에서 스트림으로 출력을 하면, 뒤에 있는 프로그램에서 그것을 받게 되는 것입니다.

3. Stream의 사용

C와 같은 다른 언어와 마찬가지로 자바에서도 InputStream과 OutputStream을 지원하고 있습니다. 즉 이 클래스들을 입력 또는 출력하고자하는 적당한 객체에 연결시켜준 다음 read나 write 메소드 등을 사용하여 입력과 출력을 하게 되는 것입니다. 만일 키보드로부터 입력을 받아서 화면에 출력하고자하면,


InputStream in=System.in;
OutputStream out=System.out;

와 같이 선언하여, in.read()문으로 내용을 입력 받아서 out.write()메소드를 사용하여 출력할 수 있다는 말입니다. 만약 in과 out이 각각 화일이나, 프린터 등에 연결되어 있다면 물론 화일에서 내용을 읽거나 쓸 수 있고, 프린터에 출력할 수도 있습니다.

하지만 여기에서는 스트림을 사용하는 일반적인 방법에 대해서는 설명하지 않고, 바로 소켓으로 넘어가도록 하겠습니다. 자바에서는 화면에 출력하기 위해서 콘솔(화면)을 사용하는 경우는 거의 없습니다. 하지만 콘솔(키보드)에서 입력받기 위한 경우나, 프린터에 출력하고자하는 경우, 화일의 내용을 읽거나 쓰는 경우에는 반드시 스트림을 사용합니다. 또한 바로 뒤에서 다룰, TCP/IP Protocol을 이용한 인터넷에서의 통신에서도 물론 스트림을 사용합니다.












Socket in TCP/IP protocol

1. Socket

여기서부터가 이번 글에서의 본론입니다. 사실 swing이나 awt같은 추상윈도우툴킷을 사용하면 키보드로부터 입력받는 것을 직접 작성할 필요도 없고, 프린터 출력 부분도 사실 남이 작성해 놓은 코드를 복사해서 약간 수정하면 쉽게 할 수 있습니다. 화일 입출력도 스트림을 제대로 설정만 해놓으면 화면에 출력하듯이 간단하게 입력과 출력을 사용할 수 있습니다. 하지만 socket을 이용한 프로그램만은 그렇지가 않습니다. 물론 개념은 스트림을 사용하는 다른 작업들과 크게 다르지 않지만 기술적인 면에 있어서 원하는 기능을 구현하는 것이 그리 쉽지만은 않기 때문입니다.

이제부터 정신을 똑바로 차리세요. 이글에서는 소켓의 구조나 TCP/IP와 같은 일반적인 네트워크 정보에 대해서는 생략하도록 하겠습니다. 미리 알고 있어야 한다는 얘기는 아니고 자세히는 몰라도 된다는 말입니다. 이글에서는 먼저 소켓을 정의해서 스트림을 사용하여 입력과 출력을 받는 것을 간단히 구현하는 프로젝트를 진행해보고, 마지막에 가서는 하나의 서버에 여러 개의 클라이언트가 접속하는 멀티 클라이언트 채팅프로그램을 작성하는 프로젝트를 진행하도록 하겠습니다.

프로젝트1. 서버와 클라이언트

여기서 완성될 프로그램은 다음과 같은 순서로 작동을 하게 됩니다.

  1. 서버가 소켓을 생성하고 연결을 기다린다.
  2. 클라이언트에서 서버의 소켓에 연결한다.
  3. 클라이언트에서 데이터를 전송다.
  4. 서버에서 데이터를 받아서 화면에 출력한다.
  5. 서버와 클라이언트의 소켓 연결을 끊는다.

이 프로젝트에서는 물론 두 개의 프로그램을 작성해야 하겠죠. 서버 쪽과 클라이언트 쪽입니다. 각각 SocketServer.java 와 SocketClient.java 로 하기로 하죠.

예제 프로그램: SocketServer.java / SocketClient.java

우선은 위의 예제 프로그램을 다운받아서 실행시켜보기를 바랍니다. 콘솔(윈도우에서는 명령 프롬프트 = 한글 MS-DOS)를 두 개를 띄워야겠죠. 아무 콘솔에서나 두 프로그램을 컴파일 합니다.

SHELL> javac SocketServer.java
SHELL> javac SocketClient.java

컴파일 된 후 실행시킵니다. 하나의 콘솔에서 서버를 먼저 실행시킵니다. 파라미터로 포트값을 주는데, 주지 않으면 5777번이 잡히게 됩니다. 포트번호는 3000번 이상의 값을 주는게 좋습니다.

SHELL> java SocketServer 6000

그러면 포트번호와 클라이언트의 연결을 기다린다는 메시지가 나오게 됩니다. 6000은 포트 번호입니다. 이 값은 생략가능하고 생략하면 5777번 포트를 사용하게 됩니다. 그러면 클라이언트를 실행시킵니다. 파라미터는 IP와 포트번호를 줍니다.

SHELL> java SocketClient 6000 211.35.136.174

포트번호는 물론 앞의 서버에서 설정한 포트번호와 같아야 하겠죠. IP는 자신의 컴퓨터 아이피를 주면 됩니다. IP를 주지 않으면 127.0.0.1에 연결하는데, 보통 이 번호는 로컬 호스트, 즉 자기 자신을 의미합니다. 만약 이 번호로 되지 않거나 다른 컴퓨터에서 SocketServer를 실행시켰다면 IP를 반드시 써주어야 하겠죠. 아무튼 서버와 포트 번호가 일치하고 IP를 적당히 주었다면 클라이언트는 서버에 연결하여 열글자의 데이터를 보낸 후 종료하게 됩니다. 서버도 소켓을 끊고 종료하게 됩니다.

위의 예제를 실행시켜보았다면, 아직 소스는 보지 말고 공부를 시작해봅시다. 소켓은 TCP프로토콜을 사용하여 통신을 하는데 쓰인다고 앞에서 얘기했습니다. 따라서 소켓의 기본은 IP와 port겠죠. 이것들이 각각 무엇을 의미하는지는 알지 못해도 상관없습니다. 아무튼 IP는 컴퓨터마다 가진 고유한 주소이고, port는 통신을 위한 연결 라인과 같은 것이라고만 알고 있으면 됩니다. 아무튼 소켓을 사용하여 어떤 프로그램과 연결을 하기 위해서는 그 프로그램이 있는 컴퓨터의 IP와 소켓을 만든 port번호를 알고 있어야 한다는 것만 기억하면 됩니다.

1) 서버 소켓 준비하기( SocketServer.java )

통신을 하기 위해서는 먼저 서버에서 소켓을 준비해야 합니다. 자바에서는 그것을 위해 ServerSocket이라는 클래스가 제공됩니다. 다음과 같이 초기화를 하면 서버 소켓이 만들어집니다. 이것은 java.net 패키지에 들어있습니다.

ServerSocket 소켓변수이름=new ServerSocket(포트번호);

포트번호는 정수이고, 소켓변수이름은 아무것이나 변수로 쓸 수 있는 것을 써주면 됩니다. 예를 들면 ServerSocket srvSocket=new ServerSocket(5777); 라고 선언할 수 있겠죠.
다음은 이 서버소켓을 사용하여 클라이언트와 통신할 다른 소켓을 만들어주는 작업입니다. 서버 쪽에서는 반드시 두 개의 소켓이 필요합니다. 하나는 서버용 소켓인데 이것으로는 통신을 할 수가 없고 단지 클라이언트의 연결을 기다립니다. 따라서 클라이언트와 연결이 되면 실제로 통신을 하기 위해서는 소켓을 하나 더 만들어서 연결해주어야 한다는 것을 의미한다. 이 소켓은 서버소켓.accept()명령으로 만들어줄 수 있습니다.

Socket soc=srvSocket.accept();

여기서 srvSocket은 아까 선언해준 서버소켓의 이름이 됩니다. 클래스의 이름이 Socket임을 눈여겨 보아야 합니다. 클라이언트에서도 이 소켓을 사용하여 연결하게 됩니다.

다음은 입력을 위한 스트림을 선언하는 부분입니다. 물론 만들어 놓은 소켓에서 스트림을 얻어와야 하겠죠. 스트림은 java.io 패키지에 포함되어 있습니다.

InputStream is=soc.getInputStream();

이렇게 is는 스트림변수 이름이고, soc는 앞에서 선언한 소켓의 이름입니다. 이렇게 되면 입력스트림이 얻어졌습니다. 그리고나서, 이 스트림에서 입력을 받기위해서는 이 스트림을 읽을 수 있는 Reader를 정의해야만 읽을 수가 있겠죠. 마치 비디오테이프와 VTR과 같은 관계입니다. VTR에 비디오 테이프를 넣어주어야 하겠죠.

InputStreamReader isr=new InputStreamReader(is);

여기서 is는 앞에서 선언한 inputstream이고, isr은 InputStreamReader의 이름입니다. 이것으로 서버쪽에서 소켓을 준비하는 것은 모두 끝났습니다. 데이터를 읽기 위해서는 InputStreamReader에 있는 read()메소드를 사용하면 됩니다. 즉, int a=isr.read();와 같이 하면 a에 한 글자가 입력됩니다. int값으로 넘어오는데, 문자로 바꾸기 위해서는 형변환(type casting)을 해주면 되겠죠. char a=(char) isr.read(); 와 같이 하면 되겠죠. 들어오는 문자들을 하나의 문자열에 저장하기 위해서는 str=str+(char)isr.read(); 와 같이 문자열에 더해주면 됩니다.

2) 클라이언트 소켓 (SocketClient.java)

클라이언트 소켓은 서버 소켓보다 훨씬 쉽습니다. 서버 소켓은 필요가 없고, InputStream을 OutputStream으로, InputStreamReader를 OutputStreamWriter로 바꾸기만 하면 됩니다.

먼저 소켓을 만들어야겠죠. 앞에서 생성한 서버의 IP와 port번호를 알고 있어야 합니다.

Socket soc=new Socket("127.0.0.1", 5777);

여기서 "127.0.0.1"은 IP주소입니다. 앞에서 설명한대로 이것은 자기 자신을 의미합니다. 만약 서버가 다른 컴퓨터에 있다면 그 컴퓨터의 IP를 적어야 합니다. 5777은 포트번호입니다. soc는 물론 소켓의 이름으로 변수로 쓸 수 있는 것을 적어주면 됩니다. 여기서 아까 눈여겨봐야 한다던 것을 기억해서 Socket과 동일한 클래스를 사용하고 있다는 것을 확인하세요.

다음은 이 소켓에서 스트림을 생성하는 것입니다. 서버로 데이터를 보내는 것이니까 당연히 InputStream이 아니라 OutputStream을 사용해야 하겠죠.

OutputStream os=soc.getOutputStream();

이 문장이 잘 이해가 안가면 앞의 서버 쪽의 InputStream을 만드는 부분을 읽어보세요. 이제 앞에서와 마찬가지로 OutputStreamWriter를 만들어줘야 하겠죠.

OutputStreamWriter osw=new OutputStreamWriter(os);

이렇게 하면 writer까지 무사히 만들어졌습니다. 이제 데이터를 전송하기 위해서는 write()메소드를 사용하면 됩니다. 구체적으로 어떻게 사용하는지는 다음에서 배우기로 합시다.

3) 스트림을 통한 데이터의 입출력

앞에서 InputStreamReader와 OutputStreamReader를 만들었습니다. 이제 그 클래스에 구현된 메소드(read, write)를 사용해서 그냥 읽고 쓰면 됩니다. 자바 스펙 에서 InputStreamReader와 OutputStreamReader를 찾아 보세요. 찾기가 쉽지는 않겠지만, 앞으로 자바 공부를 하기 위해서는 하루에도 수십번 씩 해야 하는 일이니까 스스로 찾아보세요.

InputStreamReader에 보니까 read 메소드가 두 개로 정의되어 있네요.

  • int read();
  • void read(char[] chr, int offset, int length);

앞의 것은 설명에 한 글자를 얻어온다고 적혀있네요. 즉, a=isr.read();라고 하면 a에는 한 글자만 들어가 있다는 뜻입니다. a는 정수형(int)이어야 합니다. 이것을 문자로 바꾸기 위해서는 형변환(type casting)을 해주어야 합니다. 뒤의 것은 몇 번째 글자(offset)부터 몇 글자(length)를 얻어와서 char의 배열에 넣는다는 의미인 것 같습니다.

public static void main(String args[]) {
	InputStreamReader isr=new InputStreamReader(new ServerSocket(5777).accept().getInputStream());
	String str="";
	for (int i=0; i<10; i++) { str="str+(char)isr.read();" } System.out.println(str); isr.close(); } 

자 프로그램이 하나 완성되었습니다. InputStreamReader를 만들기 위해 ServerSocket을 만들고 그것의 accept()메소드를 이용해서 Socket을 만들고 다시 InputStream을 만드는 모든 과정을 그냥 한 줄로 끝냈습니다. 괄호의 가장 안쪽부터 차근차근 읽어보면 앞에서 설명한 순서로 만들어주었다는 것을 알 수 있습니다. 다음에는 str을 String으로 선언해주고 for문으로 열개의 문자를 읽어와서 str에 더해주고서 다시 str을 출력하는 프로그램이네요.
주의해야 할 점은 이 코드는 실행되지 않는다는 점입니다. 바로 다음에서 할 exception처리를 보고나면 이 코드를 수정하여 실행시킬 수 있습니다. 적당한 패키지(java.io, java.net)를 import하고, exception을 처리해서, 이 클래스를 SocketServer.java 클래스에 넣어주면 서버는 완성됩니다.

클라이언트부분은 좀더 쉽습니다.

public static void main(String args[]) { OutputStreamWriter osw=new 
        OutputStreamWriter(new Socket("127.0.0.1",5777).getOutputStream()); 
		String str="0123456789";
		osw.write(str,0,10);
		osw.flush();
		osw.close();
} 

아까와 마찬가지로 spec에서 찾아보면 OutputStreamWriter클래스에는 write()가 여러 개로 정의되어 있습니다. 한 글자씩 전송하는 것도 있고, 지금처럼 사용할 수도 있습니다. 지금은 str의 0번째 글자부터 10개의 글자를 전송하라는 의미입니다. osw.flush();는 글자를 실제로 전송하라는 의미입니다. 이것을 써주지 않으면 메시지를 받는 쪽에서 실제로 읽을 수가 없습니다.

방금 해보니까, 위의 코드들은 전부 실행이 되네요. 물론 exception처리를 해줘야 합니다. 그럼 시간 낭비하지 말고 어서 exception처리를 배워봅시다.

4) exception처리

exception은 단어 뜻 그대로 예외상황을 의미합니다. 만약 서버에서 서버소켓을 만드는데 사용하고자하는 포트(예를 들면 5777)가 사용중이라고 해봅시다. 그러면 소켓을 만들 수가 없겠죠. 하지만 이것은 컴파일할때는 에러가 나지 않습니다. 컴파일할때 포트를 사용하는지를 검사해보는 것은 의미가 없겠죠? 컴파일할때는 포트가 사용중이 아니더라도 실행할때 사용중일 수도 있으니까요. 클라이언트에서 소켓을 만들때도 마찬가지입니다. 만약 서버를 실행시키지 않은 상황이라면, 클라이언트를 아무리 잘 작성해서 실행시키더라도 소켓을 연결할 수가 없겠죠. 이러한 상황을 예외상황(exception)이라고 합니다. 예외상황은 오류(error)와는 전혀 다른 것입니다. 에러는 사용자가 코딩할때 실수해서 컴파일조차 안되는 상황이지만, 예외상황은 코딩은 잘되었더라도 시스템의 상황에 따라서 발생하는 것입니다. 예컨대, 에러는 컴파일할때, 예외상황은 실행할때 발생한다는 것입니다.

예외상황을 잘 잡아주는 것은 대단히 중요합니다. 지금처럼 서버 소켓을 만들었을때 예외상황이 발생하여 시스템의 자원(메모리, CPU사용 등)을 반환하지 않는다면 컴퓨터를 재부팅하기 전에는 계속해서 시스템의 자원을 잡아먹기 때문에 시스템을 느려지게하는 원인이 될 수 있죠. 자바에서는 물론 그런 일이 발생하지 않습니다. 자바 버추얼 머신(java.exe)을 통해서 실행이 되기 때문에 java.exe가 실행을 중지하면 모든 자원은 자동으로 반환되기 때문입니다. 하지만 C와 같은 프로그램에서는 예외상황에서 적절히 자원을 반환해주지않으면 앞에서 설명한 일이 발생하게 됩니다.

아무튼 예외 상황을 잡아주는 것은 프로그램의 신뢰성을 높이는데에 매우 중요합니다. 소켓을 만들 수 없을때는 최소한 소켓을 만들 수 없다는 메시지 정도는 출력해주는게 프로그래머의 당연한 의무겠죠. 이 소켓 프로그램(SocketServer.java Socket Client.java)에서 예외상황을 발생시킬 수 있는 메소드는 크게 두 가지가 있습니다. 첫째는, 1.소켓을 만들때, 앞에서 소켓을 만들 수 없다는 예외상황이 발생할 수 있습니다. 둘째는, 2.데이터를 입력받거나 출력할때 중간에 소켓이 끊겼거나 하는 상황에서 발생할 수 있습니다. 따라서 SocketServer.java / SocketClient.java에서는 각각 최소한 두 가지 씩의 예외처리를 해주어야 하겠죠.

예외상황의 처리는 try 와 catch문으로 처리할 수 있습니다. finally이라는 예약어도 있지만 이것은 여기서 생략하기로 하죠. 사용 방법은 C에서와 완전히 동일합니다. 예외상황이 발생할 수 있는 메소드를 try { }로 감싸고서 예외상황은 catch에서 잡아서 처리하는 것입니다. 직접 코드를 만들어 봅시다.


public static void main(String args[]) {
	try {
		// 아까 그 내용들....
	} catch (IOException e) {
		System.out.println("Exception이 발생했습니다.");
	}
}

이렇게 하면 됩니다. IOException은 여러 가지 예외상황중 IO(Input/Output)에 관계된 예외상황을 잡아내라는 의미입니다. 스트림이니까 예외상황은 IO에 관계된 것이겠죠. 이러면 실행을 주-욱 하다가 뭔가 예외상황이 발생하면 catch다음에 있는 문장을 실행하고 넘어간다는 의미입니다. 여기서는 "Exception이 발생했습니다."라는 메시지를 출력하겠죠.

물론 이렇게 하면 잘 돌아가지만 이상적인 예외처리라고는 할 수 없습니다. 왜냐면 예외상황을 발생할 수 있는 부분은 두 부분인데, 그냥 전체적으로 try catch로 묶어버려서 어느 곳에서 예외상황이 발생되더라도 그냥 "Exc.."라는 메시지만 출력하고 끝내버려서, 사용자는 도대체 어느 부분에서 예외상황이 발생했는지를 알 수 없다는 점입니다. 이렇게 되면 프로그래머는 코드 중 어디가 틀렸는지 알 수 없고, 또는 사용자는 자신이 뭘 잘못했는지를 알 수가 없겠죠. 따라서 가장 이상적인 방법은 try - catch를 예외를 발생할 수 있는 부분들에서 각각 써주어야 한다는 것입니다. 이렇게 하면 어떤 부분에서 예외 상황이 발생했는지를 알 수 있기 때문입니다.

//SocketClient.java
import java.io.*;    // Stream, Reader, Writer를 사용하기 위해
import java.net.*;   // Socket을 사용하기 위해
 
public class SocketClient {
 
  public static void main(String args[]) {
 
    OutputStreamWriter osw=null;
 
    try {
      osw=new OutputStreamWriter(new Socket("127.0.0.1",5777).getOutputStream());
    } catch (IOException e) {
      System.out.println("소켓을 만드는 데에 실패했습니다.");
      System.exit(-1);
    }

    String str="0123456789";

    try {
      osw.write(str,0,10);
      osw.flush();
    } catch (IOException e) {
      System.out.println("데이터 전송에 실패했습니다.");
    }  

    try {
      osw.close();
    } catch (IOException e) {
      System.out.println("소켓을 닫는데 실패했습니다.");
    }
  }
}

이제는 완전한 프로그램이 작성되었습니다. 막상 코딩해보니 예외상황(exception)이 두 부분이 아니라 세 부분에서 발생할 수 있군요. 소켓을 닫는 부분도 생각을 해야 하겠죠. 첫번째 예외처리에서 System.exit(-1);은 프로그램을 종료하라는 의미입니다. 소켓이 생성되지 않았다면 뒤에 있는 데이터를 전송하는 부분이나 소켓을 닫는 부분이 전부 의미가 없는 일이 되겠죠. 따라서 소켓이 생성되지 않았다면 프로그램을 종료하는 것이 좋겠죠.

서버쪽은 각자가 작성해 볼 수 있을 것입니다. 클래스를 만들어서 적당한 패키지를 import하고 예외처리만 해주면 서버도 잘 작동될 수 있을 것입니다. 그리고나서 여러 가지 상황을 체크해보세요.

  1. 서버를 돌리지 않는 상황에서 클라이언트만 실행해보기도 하고,
  2. 서버를 하나 돌리고, 똑같은 포트를 사용하는 서버를 한 번 더 실행해보세요.

5) 진짜로 만들어보기

앞에서 드디어 완전히 동작하는 소켓 프로그램을 작성하였습니다. 서로 완전히 독립적인 두 프로그램이 서로 통신을 하는 것이 대단히 신기하죠? 사실 프로그래밍에서 제일 재미있는 부분은 윈도우 프로그래밍(swing, awt)하고 소켓 프로그래밍인 것 같습니다. 하지만 이렇게 만들면 사실 동작은 하겠지만, 객체지향언어인 자바 프로그램이라고 말하기 힘들겠죠.

왜냐면 일단 클래스를 사용하지 않았습니다. 클래스는 사실 main메소드를 사용하려고 그냥 써준 것 뿐이지, 만들어놓은 클래스를 어디에서도 사용하지 않아서, 사실 C프로그램과 별로 달라 보이지 않습니다. 그래도 객체지향언어인데 자존심이 있지...

클래스를 제대로 구현하지 못한 이 프로그램은 클래스의 장점들을 사용할 수 없음을 의미합니다. 다른 클래스에서 사용하는 것은 완전히 불가능합니다.

그럼 과제로 생각하고, 제대로된 객체지향 방식에 의해 프로그래밍을 해보세요. 그 한 예가 처음에 제시했던 SocketServer.java / SocketClient.java가 될 수 있을 것입니다. 한 번 해보다가 잘 안되면 제가 작성한 프로그램을 참고해서 반드시 직접 코딩해보세요.




프로젝트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를 사용하는 방식을 사용하도록 하겠습니다. 가장 많이 쓰이는 방식이기 때문일 뿐더러, 자바에서 제공하는 StringTokenizerBufferedReader(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를 생성합니다.

  1. st.nextToken()으로 연산자를 얻습니다.
  2. st.nextToken()으로 첫번째 수를 얻습니다.
  3. st.nextToken()으로 두번째 수를 얻습니다.
  4. 연산자가 +이면 두 수를 더하고, -이면 두수를 빼고...
  5. 계산에 성공하였으면 "s:"+결과값 으로 클라이언트에 전송할 패킷을 만듭니다. 실패이면 "f:"+에러메시지 로 전송할 패킷을 만듭니다.
  6. 만든 패킷을 반환합니다.(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]



프로젝트3. 채팅프로그램

지금까지 프로젝트를 어떻게 진행해왔는지를 생각해봅시다. 첫번째 프로젝트는 그냥 서버를 하나 띄우고, 클라이언트를 실행시키면 서버에 데이터를 한 번 보낸 후 프로그램을 종료했습니다. 두번째 프로젝트는 하나의 서버에 여러 클라이언트가 연결할 수 있었지만, 데이터 전송에 있어서는 하나가 가면 하나가 오고 하는 식이었습니다. 이번 프로젝트가 가지는 차이점은 채팅 프로그램을 생각하면 쉽게 알 수 있는 것들인데, 다음과 같습니다.

  • 이전에는 패킷을 한 번 보내고서 응답을 기다렸지만, 이번 프로젝트에서는 서버에서 비동기적으로(아무때나) 데이터를 보냅니다. 왜냐하면 다른 클라이언트에서 서버로 보낸 메시지도 받아야 하기 때문입니다.
  • 서버에서는 이전에는 패킷을 보낸 클라이언트에게만 결과값(패킷)을 전송했지만, 지금은 하나의 클라이언트에서 데이터가 보내지면 다른 모든 클라이언트에게 메시지를 보내야만 합니다.
  • 서버에서 생성한 thread가 이전에는 독립적으로 클라이언트와 통신하는 모든 역할을 담당했지만, 이번에는 서버로부터의 요구에 의해 언제라도 클라이언트에게 메시지를 보낼 수 있어야 하고, 역으로 클라이언트로부터 메시지를 받으면 서버에게 다른 모든 클라이언트에게 메시지를 전송하라고 요청해야 합니다. 따라서 서버가 생성한 thread가 서버와 통신을 할 수 있어야 합니다.

따라서, 프로젝트2에서 했던 것과 같이 데이터를 보내고서 바로 결과를 기다리거나(클라이언트), 반대로 데이터를 받고 바로 그 클라이언트에게 응답을 보내는(서버) 방식이 아닌 다른 방식이 요구됩니다. 다음과 같은 방식으로 채팅 프로그램을 작성할 수 있습니다.

  • 클라이언트에서도 서버로부터의 데이터를 받기 위해 Thread를 사용합니다. 데이터를 보내는 것은 사용자가 내용을 입력했을 때이지만, 서버로부터의 데이터는 아무때나 올 수 있기 때문에 서버로부터 응답을 받는 부분은 thread로 작성되어야만 서버의 데이터 전송에 바로 응답할 수 있습니다.
  • 서버에서는 클라이언트가 연결되면 단지 Thread를 만들고 thread에게 모든 역할을 일임하는 것이 아니라, 만든 thread의 목록을 가지고서 하나의 thread가 메시지를 받으면, thread는 서버에 메시지를 받았다는 신호를 보내고, 서버는 thread 목록에 의해서 모든 클라이언트에게 데이터를 전송해야 합니다.
  • thread가 서버와 교신할 수 있도록, thread를 생성할때 서버 자신의 레퍼런스를 넘겨줍니다. 그러면 thread에서는 서버에 메시지를 보낼때, 서버의 메소드를 호출할 수 있습니다.

계산기 프로젝트까지 성실히 따라 왔다면 클라이언트에서도 thread를 생성하는 부분은 어렵지 않을 것입니다. 클라이언트에서 서버로 메시지를 보내는 것은 전과 같지만 메시지를 받는 부분은 thread를 생성하여 넘겨주어야 합니다. 이를 위해 thread를 생성할때, 소켓 또는 Reader를 함께 넘겨주어서 데이터를 받도록 해야 합니다. 또한 클라이언트에 있는 화면에 메시지를 출력하기 위해서는 클라이언트의 레퍼런스도 함께 넘겨주어야 합니다.

1) 프로토콜 정의

이 프로그램에서는 별다른 프로토콜 정의가 필요하지 않을 것 같네요. 아무것도 정의하지 않는 것도 하나의 정의라고 할 수 있겠지만서도... 아무튼 클라이언트에서 한 줄 문장을 보내면, 서버에서는 보낸 사람 아이디와 합쳐서 다른 클라이언트들에게 보내주면 되겠죠. 아이디를 바꾸는 것이나, 도움말 보기 같은 기능들은 서버나 클라이언트에서 받은 텍스트를 분석하는 기능을 넣으면 되겠죠.

이번에도 한 줄 씩의 메시지를 주고 받게 되니까 readLine() 메소드가 있는 BufferedReader와 BufferedWriter를 사용하는 것이 좋겠네요.

2) 서버 설계1: 소켓 준비와 Thread리스트 만들기

이 부분도 계산기 프로젝트와 별반 다르지 않습니다. 단지 thread를 생성할때, 인자로 자기 자신을 넘겨주는 것과, 생성한 thread를 목록에 넣어두는 것이 다르죠. 일단 프로젝트2에서와 같이 서버 부분의 소켓과 thread 구동까지 구현하고서, 수정하도록 해봅시다. 다음과 같겠죠.

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

여기까지는 모두 알고 있겠죠? 여기서 더 추가되어야 할 내용은 생성한 thread의 목록에 넣는 것입니다. 목록은 배열로 만들어도 좋고, 리스트를 구현해도 좋겠죠. 배열은 만들기는 쉽지만, 처음부터 사용하고자하는 최대한의 공간을 잡아주어야 하기 때문에 메모리 낭비도 심하고, 중간에 사용자가 나가는 경우, 그 빈공간을 채우기 위해 뒤에 있는 내용을 하나씩 앞으로 당겨주어야 하는 불편이 있습니다. 리스트는 배열의 단점을 극복할 수 있지만, 포인터가 없는 자바에서 구현하는 것이 원칙적으로 불가능하죠. 하지만, 걱정할 것은 없습니다. Vector라는 클래스를 사용하면 간단하게 해결되기 때문입니다. 이 클래스의 세부적인 사양은 자바스펙을 보도록 하고, 지금은 중요한 메소드만 몇 개 알아봅시다.

class Java.util.Vector

  • void addElement(Object obj) : 내용(객체)을 추가할 때 사용합니다.
  • Object elementAt(int index) : 주어진 위치(index)에 있는 내용을 참조합니다.
  • boolean removeElement(Object o) : o와 같은 객체 목록에서 삭제합니다.
  • void removeElementAt(int index) : 주어진 위치에 있는 객체를 삭제합니다..

Vector v=new Vector(); 라고 선언되어 있다면 v에 추가하기 위해서는 v.addElement(obj);를, 삭제하기 위해서는 v.removeElementAt(index)를, 참조하기 위해서는 v.elementAt(index)를 사용할 수 있겠죠. 위의 프로그램에서는 일단, 처음에 Vector클래스를 하나 만들고서, thread를 생성한 후 벡터에 추가해주면 됩니다. 그러면 프로그램은 다음과 같이 수정되겠죠. 내친 김에 exception처리까지 해서 코딩을 해봅시다.

ServerSocket srv=new ServerSocket(port);
Vector client=new Vector();

while (true) {
  try {
    Socket socket=srv.accept();
    ServerThread t=new ServerThread(socket, this);
    t.start();
    client.addElement(t);
  } catch (IOException e) {
    System.out.println("Connect to Fail!!");
  }

}

하나 주의할 점은 Vector가 이 메소드 밖에서 정의되어 있어야 한다는 점입니다. 그렇지 않으면 이 벡터는 이 메소드 안에서만 사용할 수 있게 되겠죠. 또 하나는 thread를 생성할 때 자기 자신(this)를 인자로 함께 넘겨준다는 점입니다. 이것은 개별 thread에서 다른 thread로 메시지를 보낼 때, 이 Server클래스를 사용하여 메시지를 보낼 수 있도록 하기 위해서입니다. 즉 thread가 다른 모든 thread들의 레퍼런스를 가지고서 메시지를 보내는 것이 아니라 그냥 server에 메시지를 전송하도록 요청하면 서버는 목록에 있는 모든 클래스에게 메시지를 보내는 구조입니다. 아래 그림을 다시 보면 좀 이해가 빠를 듯합니다.

3) 서버 설계2: 리스트를 이용하여 모든 thread에 메시지 전송하기

그림에서 보면, 모든 thread에 메시지를 전송하는 메소드가 하나 더 필요하다는 것을 알 수 있죠. 이것의 구현은 아주 간단합니다. 그냥 for문으로 처음부터 끝까지 돌리면서 객체에 하나씩 하나씩 전송해주면 되죠.

public void message(String str) {
  for (int i=0; i<client.size(); i++) {
    ((ServerThread)client.elementAt(i)).sendMessage(str);
  }
}

client.elementAt(i)는 아까 만든 벡터에서 i째에 있는 객체니까, i번째에 만든 thread를 의미하겠죠. sendMessage(str)는 ServerThread에 구현해 놓은 메소드입니다. 이것을 호출하면 인자로 주어진 String을 자신이 연결된 클라이언트에 전송하게 됩니다. 이렇게 서버에서는 일일히 모든 클라이언트에 메시지를 전송하는 것이 아니라, 그냥 연결된 Thread에 있는 메소드를 호출하기만 하면, Thread가 알아서 메시지를 전송합니다. 위의 그림을 보면 이해가 쉬우리라 생각합니다.

그런데, 굵게 표시되어있는 (ServerThread)는 무엇인지 궁금할 것입니다. 이것은 클래스의 상속과 관계된 형변환입니다. 뒤에서 만들겠지만, 우리는 Thread 클래스를 상속하여 ServerThread를 작성할 것입니다. 자바의 모든 클래스는 Object라는 클래스에서 파생됩니다. Thread또한 마찬가지이고, 따라서 ServerThread 또한 Object로부터 파생된 것(Object를 상속한 것)입니다. SuperClass와 SubClass가 있을때, 좋은 용어는 아니지만, 부모 클래스와 자식클래스라고도 하죠. 아무튼 SuperClass를 상속하여 SubClass를 만들었다면, SuperClass로 선언된 변수는 자식 클래스를 받을 수 있습니다. 예를 들어, 다음 코드는 컴파일이 가능하다는 이야기죠.

Superclass s1=new Superclass();
Subclass s2=new Subclass();
s1=s2;

하지만, subclass s2가 s1에 넣어지면, s2가 새로 만든 메소드는 사용할 수 없게 됩니다. 즉, print()라는 메소드가 s2에만 있고, s1에만 있다면 s1=s2라고 s1에는 s2가 들어있지만, s1.print()와 같이 사용할 수 없다는 의미입니다. 따라서 print()메소드를 사용하기 위해서는 s1을 다시 s2로 형변환(type casting)을 해주어야 합니다. ((s2)s1).print()는 가능하다는 이야기입니다.

이런 이유로 위에서도 형변환이 필요합니다. 위에 있는 Vector클래스의 메소드들을 보면 알 수 있지만, addElement(Object obj)와 같이 컴포넌트를 추가할 때 Object로 받게 됩니다. 즉 벡터 안에서는 Object클래스로 변환되어 저장된다는 뜻입니다. elementAt()도 역시 Object로 반환합니다. Object클래스에 sendMessage()라는 메소드가 있을 리가 없죠. 따라서 반환된 클래스를 다시 처음대로 ServerThread로 만들어주기 위해서 앞에서와 같이 형변환을 하는 것이죠.

4) 서버 설계3: ServerThread구현

이 부분 또한 앞의 계산기 프로그램과 비슷합니다. 생성자에서 Server클래스를 인자로 받는 것과 sendMessage()메소드를 따로 구현해주어야 하는 차이점만 있습니다. 먼저 생성자를 만들어봅시다.

ChattingServer server=null;
BufferedReader br=null;
BufferedWriter bw=null;

public ServerThread(Socket socket, ChattingServer c) {
  server=c;
	
  try {
    br=new BufferedReader(new InputStreamReader(socket.getInputStream()));
    bw=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
  } catch (IOException e) {
  }
}

인자로 받은 ChattingServer클래스와 소켓을 클래스 내의 다른 메소드들이 쓸 수 있도록 클래스 인스턴스에 넣어줍니다. 다음에는 run()을 만들어주어야 합니다. 이미 알고 있겠지만, ChattingServer클래스에서 생성자를 사용하여 ServerThread를 만들고, start()를 사용하여 이 Thread를 구동시키면 run()메소드가 실행이 됩니다.

public void run() {
  String str="";

  while (true) {
    try {
      str=br.readLine();
      server.message(username+":"+str);
    } catch (IOException e) {
      server.removeClient(this);
      break;
    }
  }
}

아주 단순하죠? 그냥 한 줄을 읽어서, 그 내용 그대로 ChattingServer에 있는 message()메소드를 호출하였습니다. 앞에서 설명한 듯이 message()메소드에서는 리스트에 있는 모든 Thread들에게 클라이언트로 메시지를 보내라는 요청을 하게 되죠. 클라이언트로부터 데이터를 읽다가 exception이 나면 thread를 종료하도록 하였습니다. ChattingServer의 removeClient라는 메소드를 호출하도록 했네요. 이 메소드에서는 removeElement()를 사용해서 리스트에서 이 Thread를 제거하도록 하면 되겠죠. 그 구현은 각자에게 맡기겠습니다.

서버의 message()메소드를 호출하면 이 메소드에서는 각 thread에 있는 sendMessage(String str)을 호출하여 클라이언트에게 실제로 메시지를 전송하도록 하였던 것을 기억할 겁니다. sendMessage()메소드도 구현해주어야 하겠죠.

	public void sendMessage(String str) {
		try {
			bw.write(str);
			bw.newLine();
			bw.flush();
		} catch (IOException e) {
		}
	}

이렇게 하면 메시지를 클라이언트에 보낼 수 있게 됩니다. 이렇게 하면 ChattingServer가 일단 완성되었습니다. 하지만 채팅 프로그램에 꼭 필요한 기능들은 더 추가해야 합니다. 예컨대 사용자 이름을 어떻게든 받아서, 메시지를 보낼때마다 사용자 이름과 함께 보내도록 해야 하겠죠. 제가 테스트할 때는 클라이언트와 연결되면 클라이언트는 제일 먼저 사용자 이름을 전송하고, ServerThread에서는 그것을 받고 나서 루프를 돌도록 했습니다. 사실 이것은 그냥 클라이언트에서 자신의 이름을 처음부터 붙여서 전송하는 것도 괜찮겠죠.
제일 먼저 클라이언트가 접속을 하면 모든 사용자들에게 사용자가 들어왔다는 메시지를 전송하는 기능은 기본입니다. 또는 사용자 이름을 바꾸거나, 다른 사용자의 리스트를 보거나 하는 등의 기능들은 각자 추가해보시기 바랍니다.

5) 클라이언트1: 소켓 연결, 메시지 전송

소켓 연결과 메시지 전송은 따로 설명이 필요 없을 것 같네요. 그냥 소스만 보도록 하겠습니다.

Socket socket=null;
BufferedReader br=null;
BufferedWriter bw=null;

try {
	socket=new Socket(ip, port);
	br=new BufferedReader(new InputStreamReader(socket.getInputStream()));
	bw=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
	
} catch (IOException e) {
	System.out.println("Fail to connection!!");
	throw e;
}
		
ReceiveMessage r=new ReceiveMessage(br, this);
r.start();

다른 부분은 ReceiveMessage 를 선언해서 구동시키는 부분입니다. 이미 짐작했겠지만, ReceiveMessage는 Thread로 구현되어 있습니다. 이 부분은 앞에서 설명했듯이 계속해서 돌고 있다가 서버로부터 메시지를 받으면 클라이언트의 화면에 전송하는 역할을 합니다.

이번에는 실제와 비슷하게 클라이언트를 GUI로 만들도록 합니다. swing을 사용해서, TextArea와 TextField를 만들어서, TextField에 내용을 입력하면 서버에 전송하고, 서버로부터 메시지를 받으면 TextArea에 붙이도록 합니다. swing에 대한 자세한 내용은 Mahadevi의 Swing강좌를 참조하세요. 객체지향언어는 자료의 형식과 인자의 전달, 이벤트(메소드 호출)가 가장 중요하다고 할 수 있겠죠. 위 그림에서 클라이언트에서 사용자의 메시지를 기다리는 부분은 루프를 돌고 있는 것처럼 그려졌지만, 실제로는 이벤트에 의해서 일어납니다. TextField에 문자열을 입력하고 엔터를 치는 순간(이벤트가 발생하면) 메시지를 서버에 전송하게 되는 것이죠.

어떻게 텍스트필드를 달고 이벤트헨들링을 하는가에 대해서는 스윙이나 awt를 공부해야 하니까 여기서 설명을 하자면 한없이 길어질 것 같아서 생략해야 하겠네요. 아무튼 어떻게어떻게해서 텍스트 필드에서 데이터를 입력하면 sendMessage()라는 메소드가 실행되게 되었다고 칩시다. sendMessage()메소드에서는 텍스트 필드의 내용을 읽어서 서버에 전송하기만 하면 되겠죠.

public void sendMessage() {
  String str=textField.getText();   // 텍스트 필드의 내용을 얻는다.
  try {
    bw.write(str);                    // str을 버퍼에 넣는다.
    bw.newLine();                     // \n 을 추가한다.
    bw.flush();                       // 버퍼의 내용을 서버에 전송한다.
  } catch (IOException e) { 
    // 서버에 전송할 수 없는 상황을 처리할 수 있는 코드.
  }
}

이렇게 하면 클라이언트는 텍스트 필드에 내용을 입력하고서 엔터를 치면 서버로 전송할 수 있게됩니다.

6) 클라이언트2: 메시지 받기

ReceiveMessage 클래스를 만들어야겠군요. 이 클래스는 당연히 Thread로 작성되어야 하겠죠. 인자로는 데이터를 받아야 할테니까, 소켓 같은 것을 넣어주어야 하겠죠. 하지만 받기만 하니까 소켓 말고 BufferedReader를 넘겨주기로 합시다. 그리고 데이터를 받아서 화면에 써주어야 하는데, 이 화면은 ChattingClient클래스에 있는 것이니까 ReceiveMessage가 직접 쓸 수는 없겠죠. 그냥 편하게 Thread를 생성할때 ChattingClient가 자기 자신(this)을 넘겨주는 걸로 합시다. 나머지는 별다는 설명이 필요 없을 것 같으니까 그냥 소스를 봅시다.

class ReceiveMessage extends Thread {
	
	BufferedReader br=null;
	ChattingClient client=null;

	public ReceiveMessage(BufferedReader br, ChattingClient c) {
		this.client=c;
		this.br=br;
	}
	
	public void run() {
		
		String str="";
		
		while (true) {
			try {
				str=br.readLine();
				if (str!=null) {
					client.message(str);
				}
			} catch (IOException e) {
				
			}
				
		}	// end of while
	}	 // end of run ()
}  // end of class ReceiveMessage

소스는 매우 간단합니다. 생성자에서는 그냥 BufferedReader와 ChattingClient를 인자로 받기만 하고, run()메소드에서 무한 루프를 돌면서 한줄씩 받아서 ChattingClient의 message라는 메소드를 호출해주는 것이 다죠. 물론 ChattingClient에는 message()가 구현되어 있어야 하고, TextArea에 문자열을 출력해주는 일을 하겠죠.

7)참고 예제

제가 작성한 예제 화일입니다.

  • ChattingServer.java: 채팅 서버입니다. 컴파일 하시고, java ChattingServer [port-name/default=5777] 로 실행합니다.
  • ChattingClient.java: 클라이언트부입니다. 이것을 실행시키면 로컬호스트의 5777 포트로만 연결합니다. 이것 말고 아래의 RunClient를 실행시키세요.
  • RunClient.java: ChattingClient를 사용하여 실제 클라이언트 GUI를 만듭니다. java ChattingClient 라고 실행하면 ip와 port, username을 입력하는 창이 뜹니다.

먼저 서버를 실행시키시고, RunClient를 실행시키면 됩니다. IP입력창에서, 자신의 컴퓨터에 서버가 구동되고 있다면, 자신의 IP또는 127.0.0.1을 입력하시고, 다른 컴퓨터에서 실행된다면 그 컴퓨터의 IP를 입력하세요. port는 처음 서버를 구동시킬때 사용한 포트를 적어주어야 합니다. 별다른 옵션없이 서버를 구동했다면 포트번호는 5777번 입니다.

실제로 채팅 프로그램은 클라이언트가 애플릿일때 유용성이 클 것입니다. 하지만, 애플릿에서 소켓 통신을 하기 위해서는 보안정책파일을 편집해야 하기 때문에, 일단 그냥 어플리케이션으로 작성했습니다. ChattingClient가 JPanel로 작성되어있기 때문에 Applet으로 만들기 위해서는 애플릿을 상속한 적당한 클래스를 상속받아서 ChattingClient를 붙여주기만 하면 됩니다. 물론 보안정책파일도 편집해야 하겠죠.

2008. 10. 14. 16:50

RED5




(주)웹호스트 http://www.webhost.co.kr

이 문서는 rhel4에서 테스트되었다.

1. red5란 무엇인가?

flash player 중간스트리밍이 가능하도록
리눅스서버에 설치하는 스트리밍 프로그램이다.

2. 설치과정

Installing Red5, version 0.4.1 takes 4 steps
  1. Installing the Java 1.5 JDK
  2. Installing Apache Ant.
  3. Setting the wright Path Variables
  4. Installing Red 5


First of all, you will need to be logged in as root. Were using bash as shell


Step 1. Installing the Java 1.5 JDK

jdk-1_5_0_14-linux-i586.bin

최신버전을 받는다. 리눅스용중에서 rpm자동설치버전을 받아라.

./jdk-1_5_0_14-linux-i586-rpm.bin
자동설치된다.

주의) 설치전
rpm -qa|grep jdk
rpm -qa|grep java
등 해서 모두 제거한다.

기본설치가 되면, rpm 으로 설치된다.

jdk-1.5.0_14-fcs

/usr/java/jdk1.5.0_14


#vi /etc/profile


맨 밑에

JAVA_HOME=/usr/java/jdk1.5.0_14  #자바가 설치된 위치입니다.
export JAVA_HOME
PATH=$PATH:$JAVA_HOME/bin
CLASSPATH=$CLASSPATH:$JAVA_HOME/lib


이런식으로 저장

#source /etc/profile

설치 끝
텔넷사용시, 로그아웃하고나서 다시로그인할 것
java -version
으로 버전확인할 것


Now let's check the Java version:

[root@hostname ~]# java -version
It should read something like this:
java version "1.5.0_14"

Java(TM) 2 Runtime Environment, Standard Edition (build 1.5.0_06-b05)

Java HotSpot(TM) Client VM (build 1.5.0_06-b05, mixed mode, sharing)
OK 설치성공

That was step 1. Still here? let's go to step 2

Step 2. Installing Apache Ant.

드디어, 2장으로 돌입한다.

[root@hostname ~]# cd /usr/local/
wget wget http://archive.apache.org/dist/ant/binaries/apache-ant-1.6.2-bin.tar.gz


[root@hostname ~]# tar xvzf apache-ant-1.6.2-bin.tar.gz
rename the dir to ant
[root@hostname ~]# ln -s apache-ant-1.6.2 ant














압축풀고, 이름바꾼후에 /usr/local/ant 로 링크시킨다.

1.7.0 에 문제가 있는 것 같음. ant는 버전을 테스트해보고 설치할 것.


















ant의 문제는 ant server & 로 할경우 데먼이 올라오지만,
ant 만 실행하면 에러가 남. 1.7.x대역은 데먼이 제대로 실행안됨.
이 부분은 좀 더 연구해 봐야 할 문제













Step 3. Setting the wright Path Variables.





3장 진입


If you'll be running Red5 as root (which you shouldn't) you can add the paths to /root/.bash_profile
otherwise you can add them to /etc/profile add these two lines:

 
즉,
vi /etc/profile
한 다음,

export ANT_HOME=/usr/local/ant
export PATH=$ANT_HOME/bin:$PATH


이렇게 2줄 추가하고
저장한 후
#source /etc/profile





ant 설치 끝





 






Step 4. Installing Red5.




red5 설치하기  http://osflash.org/red5 -> wget http://dl.fancycode.com/red5/0.6.3/src/red5-0.6.3.tar.gz







download Red5




[root@hostname ~]# adduser red5

새 사용자 계정을 만든다. 예를들어 red5라고 하자. 이 아이디로는 flv동영상을 올리게 된다.
cd ~red5
[root@hostname ~]# tar zxf ~/red5/red5-0.6.3.tar.gz

이제 최신버전 다운로드한 것을 푼다.


red5-0.6.3
[root@www red5]#

[root@www red5-0.6.3]# ls
build.properties  doc            ivy.xml      Makefile        red5_debug.sh      red5-shutdown.bat  swf
build.xml         dumps          lib          red5.bat        red5-highperf.bat  red5-shutdown.sh   test
conf              ivyconfig.xml  license.txt  red5_debug.bat  red5.sh            src                webapps
[root@www red5-0.6.3]#




이제 설치하자

ant server &

이 명령으로 red5가 있는 위치를 기본으로 설치가 시작됨
약 15분간 진행됨.


/etc/rc.d/init.d/red5 스크립트 작성

#! /bin/sh
#
# Author: Jake Hilton <red5@jakehilton.com>
# /etc/init.d/red5
#
# Check for missing file
RED5_DIR=/home/red5/red5-0.6.3/
test -x $RED5_DIR/red5.sh || exit 5


case "$1" in
    start)
        echo -n "Starting Red5 Service"
        echo -n " "
        cd $RED5_DIR
        su -s /bin/bash -c "$RED5_DIR/red5.sh &" root
        sleep 2
        ;;
    stop)
        echo -n "Shutting down red5"
        echo -n " "
        su -s /bin/bash -c "killall -q -u red5 java" root
        sleep 2
        ;;
    restart)
        ## Stop the service and regardless of whether it was
        ## running or not, start it again.
        $0 stop
        $0 start
        ;;
esac

---------------------------------------------

굵은 부분은 중요하니 알아서 수정할 것



스크립트 구동

/etc/rc.d/init.d/red5 stop
/etc/rc.d/init.d/red5 start





테스트해보기


[root@www red5-0.6.3]# telnet localhost 1935
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
[WARN] 39168 mina-1:( org.red5.server.net.rtmp.RTMPMinaIoHandler.sessionOpened ) Is tcp delay enabled: false

접속성공함

^] 키보드 Ctrl+] 입력
telnet> quit
Connection closed.
[root@www red5-0.6.3]# [DEBUG] 64717 mina-4:( org.red5.server.BaseConnection.close ) Close, not connected nothing to do.


[root@www red5-0.6.3]#

[root@www red5-0.6.3]# ps -ax|grep red5
Warning: bad syntax, perhaps a bogus '-'? See /usr/share/doc/procps-3.2.3/FAQ
18535 pts/0    Sl     0:01 /usr/bin/java -cp red5.jar:conf: org.red5.server.Standalone
18596 pts/0    S+     0:00 grep red5
[root@www red5-0.6.3]#

[root@www red5-0.6.3]# netstat -an|more
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address               Foreign Address             State     
tcp        0      0 0.0.0.0:8001                0.0.0.0:*                   LISTEN     
tcp        0      0 0.0.0.0:995                 0.0.0.0:*                   LISTEN     
tcp        0      0 0.0.0.0:3306                0.0.0.0:*                   LISTEN     
tcp        0      0 0.0.0.0:110                 0.0.0.0:*                   LISTEN     
tcp        0      0 0.0.0.0:1935                0.0.0.0:*                   LISTEN     
tcp        0      0 0.0.0.0:80                  0.0.0.0:*                   LISTEN     
tcp        0      0 0.0.0.0:21                  0.0.0.0:*                   LISTEN     
tcp        0      0 0.0.0.0:22                  0.0.0.0:*                   LISTEN     
tcp        0      0 0.0.0.0:5080                0.0.0.0:*                   LISTEN     
tcp        0      0 0.0.0.0:8088                0.0.0.0:*                   LISTEN     
tcp        0    104 116.197.138.27:22           211.187.59.252:4231         ESTABLISHED
tcp        0      0 127.0.0.1:35137             127.0.0.1:5080              FIN_WAIT2  
tcp        0      0 127.0.0.1:5080              127.0.0.1:35137             CLOSE_WAIT 



1935포트가 정상적으로 LISTEN중이다.







이제 플레이어를 설치하자.


flash_media_player.zip 첨부한 파일 참고

파일을 풀어서 서버에 올림


drwxrwxrwx  2 root root   4096 Jul 30 18:48 extras
-rwxrwxrwx  1 root root  20920 Apr  8  2005 image.jpg
-rwxr-xr-x  1 root root    754 Jan  5 04:08 mediaplayer.html
-rw-r--r--  1 root root  35041 Dec 18 06:56 mediaplayer.swf
-rwxrwxrwx  1 root root  81222 Dec 27  2006 movie.swf
-rwxrwxrwx  1 root root    343 Jan  5 04:35 playlist.xml
-rwxrwxrwx  1 root root  10055 Feb 10  2007 preview.jpg
drwxrwxrwx  2 root root   4096 Apr 18  2007 readme
-rwxrwxrwx  1 root root  47024 Aug 11  2005 song.mp3
drwxrwxrwx  3 root root   4096 Dec 18 06:58 source
-rwxrwxrwx  1 root root   6880 Mar  1  2007 swfobject.js
-rw-r--r--  1 root root    305 Jan  5 04:37 test.html
-rwxrwxrwx  1 root root 282797 Aug 11  2005 video.flv
[root@www public_html]# pwd
/home/red5/public_html
[root@www public_html]#


/home/red5/public_html 이라고 가정함.

풀어서 올린결과

이 플레이어는 rtmp://를 지원한다.

mv mediaplayer.html index.html


아파치 virtual 셋팅
<VirtualHost *:80>
    DocumentRoot /home/red5/public_html
    ServerName red5.도메인.com
</VirtualHost>

<VirtualHost *:80>
    DocumentRoot /home/red5/red5-0.6.3
    ServerName red5rtmp.도메인.com
</VirtualHost>


2개를 동시에 만든다.

왜냐? 해당폴더에 접근이 가능해야 하기때문이다.
아래것은 rtmp://도메인명:1935/oflaDemo/file.flv 형태로 접근하기 위한 것



vi index.html 수정

------------------------------------------
<html>
<head>



<script type="text/javascript" src="swfobject.js"></script>



</head>
<body>



<h3>single file, with preview image:</h3>



<p id="player2"><a href="http://www.macromedia.com/go/getflashplayer">Get the Flash Player</a> to see this player.</p>
<script type="text/javascript">
        var s2 = new SWFObject("mediaplayer.swf","playlist","640","480","1");
        s2.addParam("allowfullscreen","true");
        s2.addVariable("file","playlist.xml");
        s2.addVariable("displayheight","380");
        s2.addVariable("backcolor","0x000000");
        s2.addVariable("frontcolor","0xCCCCCC");
        s2.addVariable("lightcolor","0x996600");
        s2.addVariable("width","640");
        s2.addVariable("height","480");
        s2.write("player2");
</script>




</body>
</html>
-----------------------------------------------------------



vi playlist.xml수정
------------------------------------------
<?xml version="1.0" encoding="utf-8"?>
<playlist version="1" xmlns="http://xspf.org/ns/0/">
        <trackList>


                <track>
                        <title>FLV Test 2</title>
                        <creator>Postman</creator>
                        <location>rtmp://red5rtmp.도메인.com:1935/oflaDemo/</location>
<identifier>Transformers.flv</identifier>
<meta rel="type">rtmp</meta>
                </track>


        </trackList>
</playlist>
------------------------------------------------

여기가 중요함


[root@www streams]# ll
total 19980
-rw-r--r--  1 root root 4546420 Sep 11 14:31 IronMan.flv
-rw-r--r--  1 root root 8446642 Mar 15  2007 on2_flash8_w_audio.flv
-rw-r--r--  1 root root 7405850 May 19  2007 Transformers.flv
[root@www streams]# pwd
/home/red5/red5-0.6.3/webapps/oflaDemo/streams
[root@www streams]#


이 속에 존재하는 파일중에 연결한다.


rtmp://주소는 oflaDemo/폴더까지만 적어준다.



이제 테스트해보자

http://red5.도메인.com

했을때, 플레이가 되면 끝남





(주)웹호스트 작성 http://www.webhost.co.kr

작성 : 2008년 1월4일

2008. 10. 2. 09:52

TortoiseSVN 설치 및 사용방법




TortoiseSVNSubversion이라는 버전 트롤 시스템을 위한 윈도우용 클라이언트 프로그램입니다. 여기서 버전 컨트롤 시스템이란 폴더 및 파일의 모든 수정 내역을 효율적으로 기록하고 관리하는 시스템을 의미합니다. 버전 컨트롤 시스템을 사용하면 언제든 예전에 수정하였거나 삭제한 파일을 다시 불러와 확인할 수 있습니다. 또한 여러 사용자가 동시에 파일을 열거나 수정할 경우 발생할 수 있는 모든 문제들도 처리하여 줍니다. TortoiseSVN은 이러한 버전 컨트롤 시스템을 사용자가 쉽게 사용할 수 있도록 윈도우의 쉘과 통합된 형태의 인터페이스를 제공하는 클라이언트 프로그램입니다. 게다가 자체적으로 로컬 서버의 기능도 가지고 있어 따로 Subversion 서버를 설치하지 않고도 사용할 있다는 장점을 가지고 있습니다. Subversion은 새로운 버전 컨트롤 시스템으로서 CVS라는 유명한 버전 컨트롤 시스템의 단점을 개선하여 CVS를 대체하기 위해 만들어진 프로그램입니다. 이 문서는 이러한 버전 컨트롤 시스템을 처음 사용하는 사용자가 TortoiseSVN을 사용하여 파일의 버전을 관리하고 여러 작업을 수행할 수 있도록 그 시작을 도와줍니다.
2008. 10. 1. 16:26

JUnit 을 이용한 단위 테스트




유난히도 "버그"를 많이 발생시키는 프로그래밍 패턴때문에, 난 "버그양성소" 역할을 해왔었다. 그동안은 main() 메소드를 통해 클래스와 메소드를 테스트했는데, 그동안의 무식함을 반성하고자 제일 기본적인 JUnit에 대해서 알아보았다.

1. JUnit 이 뭐지?
JUnit 은 테스트 프레임워크 (툴) 이다.
한마디로, 정해진 형식에따라 test 메소드를 작성해 놓으면 자동으로 실행시켜주고 결과도 리턴해주는 놈이다.

그럼 기존 java 애플리케이션 [public static void main(String[] args)] 을 이용한 테스트와는 뭐가 다른가?
정해진 방법대로 작성해 놓으면  테스트 java 애플리케이션 안의 메소드를 호출하지 않아도 자동 실행해주기 때문에, 손이 덜 간다. 그리고, 테스트 Java 애플리케이션이 많을때 이것을 일일이 실행해야 테스트를 해야하는 반면, JUnit은 TestSuit 이라는 개념을 지원해 여러개의 Test 클래스들을 한꺼번에 실행해 볼수 있다.

2. JUnit 을 사용하려면 ?
다운로드 및 참고자료 : http://junit.sourceforge.net/

junit.jar 를 classpath에 추가 하기만 하면 된다.
Eclipse 에는 기본 플러그인으로 설치되어 있어 바로 사용가능하다.

3. 코드 작성법
  1) JUnit의 TestCase 를 상속하여 클래스를 생성한다.
  2) 메소드 실행전 초기화 부분은 setUp() 메소드에, 실행후 초기화는 tearDown() 에 기술한다.
  3) 각각의 테스트할 단위들을 testXXXXX() 로 작성한다.
  4) 각 메소드에 assertXXXX() 메소드를 삽입하여, 테스트의 성공 유무를 판가름한다.
      아래 조건을 만족하지 않으면 실패로 간주
         ex) assertEquals  : 같지 않으면 실패,

assertEquals - 같은지 비교
assertNull - null값을 리턴하는지 비교
assertNotNull - 인자로 넘겨받은 객체가 null인지 판정하고 반대인경우 실패로 처리한다.
assertSame - assertSame 은 expected 와 actual이 같은 객체를 참조하는지 판정하고
                       그렇지 않다면 실패로 처리한다.
assertNotSame - expected 와 actual이 서로 '다른' 객체를 참조하는지 판정하고, 만약
                       같은 객체를 참조한다면 실패로 처리한다.
assertTrue - boolean 조건이 참인지 판정한다. 만약 조건이 거짓이라면 실패로 처리한다.
fail - 테스트를 바로 실패 처리한다.

  
4. 코드샘플과 실행결과
   1) 코드 샘플

import junit.framework.TestCase;
import com.ilikeclick.fw.util.*;

public class testUtilClass extends TestCase
{
    private String testStr1 = null;
    private String testStr2 = null;
      
    public void setUp()
    {
        System.out.println("setUp()");
    }
   
    public void tearDown()
    {
        System.out.println("tearDown()\n");
       
        testStr1 = null;
        testStr2 = null;
    }
   
    public void testCryptUtility()
    {
        System.out.println("testCryptUtility()");
       
        testStr1 = "sokum";
        testStr2 = CryptUtility.encrypt(testStr1, 3);
       
        assertNotNull(testStr2);
        assertEquals(testStr1, CryptUtility.decrypt(testStr2, 2));
    }
   
    public void testRandomUtility()
    {
        System.out.println("testRandomUtility()");       
       
        for(int loopCnt=0; loopCnt < 128; loopCnt++)
        {
            testStr1 = RandomUtility.createNumAlphaCode(loopCnt);
           
            assertNotNull(testStr1);
        }
    }
}


  2) 콘솔 - 실행결과

사용자 삽입 이미지


 








  3) JUnit 결과창
사용자 삽입 이미지



위에서는 testCryptUtility() 에서 테스트가 실패하게 비밀키를 다른게 설정했다. 참고로, CryptUtility 는 시져알고리즘을 구현한 클래스로 비밀키를 이용하여 암호화와 복호화를 지원한다.

JUnit 결과창을 보면, 실행갯수와 오류와 실패 갯수, 실행시간, 실패에 대한 추적을 한눈에 볼수 있다.

  4) 테스할 Class에 대한 TestCase 코드 자동생성하기

  이클립스에서, 테스트하고 싶은 클래스에 마우스 오른쪽 버튼을 누르고 JUnit 테스트 코드를 생성하면, 선택된 클래스의 모든 메소드를 테스트 가능하도록 껍질코드(?)를 만들어 준다.

  5) TestCase 한꺼번에 실행하기

  이러한 TestCase 를 한대에 묶어서, TestSuit 을 만들어 한꺼번에 실행해 볼 수 있다. 추가로 테스트할 TestCase 는 // $JUnit-BEGIN$ 과 // $JUnit-END$ 사이에 클래스이름을 추가해주면 된다.

import junit.framework.Test;
import junit.framework.TestSuite;

public class AllTests
{
    public static Test suite()
    {
        TestSuite suite = new TestSuite("Test for com.ilikeclick.fw.test");
        // $JUnit-BEGIN$
        suite.addTestSuite(testUtilClass.class);
        // $JUnit-END$

        return suite;
    }
}

5. JUnit 을 적용하면 좋은점
  1) Eclipse에 기본으로 플러그인이 포함되어 있어 사용이 쉽다.
  2) JUnit 은 단위테스트의 개념을 초기확립(?) 시킨 놈으로써의 의의와 범용적으로 사용하고 있다.
  3) TestSuit 을 제공함으로써 여러개의 TestCase 클래스를 한번에 실행시킬수 있다.

6. 불편한 점
  1) 메소드 실행전후에 초기화를 담당하는 setUp(), tearDown() 메소드에 사용자 파라메터를 넘길수 없어
      불편하다.
  2) 반드시 TestCase 를 상속해야 하기땜에, 단일상속의 제한이 있는 Java 에서는 다양한 테스트가 불가능
     하다. +_+ 제일 불편한 부분이 아닐까 싶다.
     물론, JUnit 이 다른 클래스나 다른 모듈에 의존성 없이독립적으로 실행되어야 한다는 점은 십분 이해가
     가지만, 그렇다고 확장성마저 없애버린것은 좀 오버인듯 싶다.
     (그래서 다른 테스트 플랫폼도 나오고 있지만...)
  3) Servlet 은 어떻게 테스트하지? +_+;; 물론, 더 찾아봐야 알겠지만 Servlet은 어떻게 해야 할지 감이 안온다.

* JUnit 을 둘러보며 느낀점
  1) 결국 JUnit 은 형태를 정해놓고, 그 방법대로 코드를 작성하면 자동으로 메소드를 실행해주는 역할 밖에
      하지 않을정도로 간단한 놈이다.
  2) 라이브러리를 지원하지 않아도 Reflection 을 사용하면 금방 구현할것 같다. 결국 어떤사람이 먼저
     아이디어를 떠올리고 그것을 실행으로 옮기느냐에 따라 선도자가 될수도 있고, 흐름에 따르는 순응자가
     될수도 있다는 것을 다시 한번 느꼈다.
  3) 버그가 줄어들겠지? 가 아니라, 테스트 메소드를 호출하기에 편할것같다.  
     버그 날만한 것을 testXX() 메소드에 기술하여 테스트하는건 기존과 똑같으므로 결국 프로그래머에 달렸다.
     단, 누가 보더라도 메소드의 의도가 명확하고, 모든 메소드에 대하여 testXXX() 메소드를 반드시 작성해야
     함으로 작성자가 아닌 다른 프로그래머가 해당 메소드를 사용할때 "샘플코드" 로써의 역할도 수행할 수 있
     어 편할 듯 싶다.

예전 프로젝트에서 공통업무를 맡으면서 수많은 버그들을 만들어, 본인이나 쓰는 사람이나 서로 스트레스를 많이 받았었다. 이젠 개발 5년차다. 그만큼 단단한 코드 작성이 필요한 시점이 아닐까 싶다.

작성       : 남경식 / 2008-01-02 / isokum@hotmail.com


2008. 10. 1. 10:04

JUnit 기본 사용법




JUnit은 자바 세계에서 가장 널리 사용되어지고 있는 단위 테스팅 도구이다. Eclipse를 비롯한 대부분은 IDE는 JUnit을 기본으로 지원하고 있다. 단위테스트에 국한되어 있어 모든 테스트를 소화하기엔 부족한 점이 있지만, 기능의 확장을 위해 이를 기반으로 한 Add-ons 이나 Extensions 들이 많이 나와있다. 현재까지 가장 많이 쓰여 왔던 JUnit 3.8 버전의 사용법을 간단히 정리해본다.

1. 기본 테스트 예제

import junit.framework.TestCase;

public class SimpleTest extends TestCase {
	private int x = 3;
	private int y = 2;
	
	public void testAddition() {
		int z = x + y;
		assertEquals(5, z);
	}
}
- junit.framework.TestCase 를 상속한 테스트용 클래스 생성.
- public void 타입으로 선언된 test* 로 시작되는 테스트 메소드 작성.
- assert 로 테스트 결과 검증.

2. setUp과 tearDown을 이용한 각 테스트 메소드 실행시마다 초기화 및 종료화 처리 예제

import junit.framework.TestCase;

public class SetupTest extends TestCase {
	private int x;
	private int y;
	
	protected void setUp() {
		x = 3;
		y = 2;
		System.out.println("setUp()");
	}
	
	public void testAddition() {
		int z = x + y;
		assertEquals(5, z);
	}
	
	public void testSubtraction() {
		int z = x - y;
		assertEquals(1, z);
	}
	
	protected void tearDown() {
		System.out.println("tearDown()");
	}
}
- 각 테스트 마다 매번 setUp() 과 tearDown() 실행.

3. TestSetup를 이용한 클래스내 모든 테스트 실행 전 초기화와 실행 후 종료화 처리 예제

import junit.extensions.TestSetup;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;

public class TestSetupTest extends TestCase {
	private static int x;
	private static int y;

	public static Test suite() {
		TestSetup setup = new TestSetup(
				new TestSuite(TestSetupTest.class)) {
			protected void setUp() {
				x = 3;
				y = 2;
				System.out.println("setUp()");
			}
			
			protected void tearDown() {
				System.out.println("tearDown()");
			}
		};
		return setup;
	}

	public void testAddition() {
		int z = x + y;
		assertEquals(5, z);
	}

	public void testSubtraction() {
		int z = x - y;
		assertEquals(1, z);
	}
}
- 모든 테스트 실행 전과 실행 후 단 1회만 setUp()과 tearDown() 실행.

4. 예외 테스트 예제

import junit.framework.TestCase;

public class ExceptionTest extends TestCase {	
	public void testDivisionByZero() {
		try {
			int n = 2 / 0;
			fail("Divided by Zero!"); // 실패
		}
		catch (ArithmeticException ae) {
			assertNotNull(ae.getMessage()); // 성공
		}
	}
}

5. TestSuite를 이용한 모든 테스트 실행 예제

import junit.framework.Test;
import junit.framework.TestSuite;

public class AllTests {
	public static Test suite() {
		TestSuite suite = new TestSuite("All JUnit Tests");
		suite.addTestSuite(ExceptionTest.class);
		suite.addTestSuite(SetupTest.class);
		suite.addTestSuite(SimpleTest.class);
		suite.addTest(TestSetupTest.suite());
		return suite;
	}

	public static void main(String[] args) {
		junit.textui.TestRunner.run(suite());
	}
}



2008. 9. 30. 16:13

[Doc] MyEclipse 를 사용한 Struts + Spring + Hibernate 통합에 관련 문서