http://www.javaservice.net/~java/bbs/read.cgi?m=devtip&b=javatip&c=r_p&n=1009171849


제목 : TCP/IP Socket 프로그램 구현시 고려사항
글쓴이: 이원영(javaservice)   2001/12/24 14:30:49  조회수:6479  줄수:96
  
TCP/IP Socket 프로그램 구현시 고려사항

CPU나 메모리와 같은 자원은 항상 한계점이 있기 마련입니다. 그 한계점에 도달했을 때,
어떻게 동작케 하도록 조정하겠느냐의 문제가 매우 중요합니다. 이러한 고민의 여부가
때론 프로그래머의 수준이 실무적인 경험이 있느냐 그렇지 않느냐의 차이로 나타납니다.

TCP/IP Socket 프로그램을 짤때, 크게 수준에 따라 네가지 방법이 있습니다.

첫째, ServerSocket에서 accept()상태에서 대기하다가 accept()에서 요청이 떨어지면
그제서야 new YourThread(client) 를 생성하여 해당 Socket 요청을 처리하는 Thread를
만들고, ServerSocket은 다시 accept()의 while loop로 돌아가는 것이지요.
이것의 문제는 두가지인데, 하나는 매 요청마다 Thread가 생성되고 처리가 끝나면
GC에 의해 사라지므로 부하를 매우 많이 받는 구조라는 것입니다. 두번째 문제는, 이렇게
짰다는 것은 한계상황에 대한 처리구조가 없다는 것입니다. 몇개의 Thread가 Job을
동시에 처리하면 해당 H/W의 한계점에 이르를 것인지를 확인하고, 그 개수제한을 두어야
하는데, 전혀 이러한 고려가 없으니, 일정한 부하 이상의 상황에 직면하게 되면 요청이
지속적으로 큐잉된다거나 혹은 생각지 못했던 Exception들을 만나게 될 수도 있습니다.
어떤 자원이 고갈되어 한계상황에 직면했을 땐, 추가적인 요청들에 대해서는 fail을
발생하여 곧바로 return케 하여야만이 장애가 일어 나지 않습니다.
PS: 이를 Peek Point Contron 기법이라고 명명하였었습니다
PPC(Peek Point Control) 기법
http://www.javaservice.net/~java/bbs/read.cgi?m=qna&b=consult&c=r_p&n=996642473

두번째 방법은 Thread Pooling을 사용하는 방식입니다. 대부분의 웹어플리케이션서버의
엔진소스에서 서블렛/JSP를 처리하는 내부적인 방식이 이렇게 되어 있습니다.
요청을 처리하는 Thread는 해당 프로그램이 처음 뜰 때 이미 50개면 50개, 100개면
100개가 모두 기동됩니다. 그 Thread들은 run()메소드에서 while loop를 돌며 무한루프에
빠져 있는데, run() 메소드 첫부분에서 모든 Thread들이 어떤 lock 객체에 synchronized
되어 wait()상태에 빠져 있습니다.
만약, ServerSocket의 accept()에서 요청이 들어오면, 기본적인 정보만 추출하여 이를
적당한 queue에 해당 client socket을 큐잉하고, Thread들이 wait()되어 있는 lock을
notify()/notifyAll() 시킵니다. 그러면 수 많은 Thread들 중 한 Thread가 깨어나
queue의 값을 꺼내어 SocketClient 요청을 처리하고 자신은 다시 무한루프에 의해
다시 wait() 됩니다.
이 같은 구조는 앞서의 문제를 모두 해결합니다. 즉, 매번 Thread를 생성하지 않으니
부하가 없고, 또, 초기에 몇개의 Thread들을 기동시켜두느냐는 결국 동시에 수행할
개수제한의 효과가 되는 것이지요. 그 개수는 Performance Tuning의 방법론에 따라
해당 H/W와 S/W환경에 맞게 적정값으로 조정되는 것이지요.

세번째 방법은 Socket Server측에서 Thread Pooling을 사용하는 것은 두번째 방법과
동일한데, Socket Client와 Server 사이의 TCP/IP Socket연결자체를 어떻게 Pooling
할 것인가의 문제가 추가됩니다. 지금까진 매번 Socket을 open하고 close하는 구조로
가져갔을 겁니다. 그러나, socket open은 부하가 걸리는 작업이므로 미리 socket을
열어두고 이를 pool에 넣어 둔 후, 필요시 pool에서 꺼내와 사용하는 것이 보다 효율적일
것입니다. 이는 Socket client와 server 측 모두에서 고려되어야 합니다. 5개면 5개,
10개면 10개 미리 Socket들이 맺어져 있고, 필요시 가져다 사용하고 모두 사용하고 난
뒤 곧바로 close하는 것이 아니라 pool로 돌려보내는 방법이지요.
이 방법은 socket을 매번 open/close하지 않으니 성능향상을 볼 수 있습니다. 그러나
단점은 Pool에 연결되어 있는 Socket들이 어떤 N/W 장애로 인해 물리적인 단절이 됐을
경우, 이를 어떻게 Recovery 할 것인가의 이슈가 추가로 고려되어야 합니다.

네번째 방법은 앞서 세번째가지의 기법이 모두 적용된 상태에서 다시 추가 됩니다.
즉, 앞서의 방법은 socket pool을 사용하므로, send를 날린 후 아직 receive를 받지 않는
동안 이 TCP/IP socket은 멍청하게 놀리고 있는 결과가 되고 맙니다. 즉, 실질적인
데이타는 이용되고 있지 않으면서도 매우 많은 수의 TCP/IP Socket이 필요하게 됩니다.
그래서 send 전용 socket과 receive전용 Socket을 별도로 구분하게 됩니다. 결국, send용
queue와 receive용 queue가 만들어져야 하고, client는 send용 queue에 넘길 데이타를
채운 후, send용 Thread들을 notify시킵니다. 곧바로 recevice queue에서 자신의 데이타가
오기를 기다려야 겠지요. Recieve용 Socket에 달려 있는 receive 용 Thread는 데이타가
오는 즉시 receive용 queue에 값을 채워넣고, 자신의 데이타가 오기를 기다리는 Thread
의 lock을 notify 시킵니다. (잘 깨워야겠지요, 모두 깨울것이냐, 혹은 해당 데이타에
관련된 것만 깨울 것인가가 고민되겠지요)
이 경우는 또한, send후 receive하기까지의 Timeout옵션을 지정할 수 있는 잇점도 가질
수 있습니다.
단, 이 경우의 단점은 성능은 가장 빠르지만, 서로 다른 데이타들이 영향을 미칠 수
있습니다. 앞서 보낸 데이타에 이용된 send socket이 장애를 일으키면 뒤따라 보낸 데이타
역시 깨어질 수 있는 것이지요.
또, 자칫 구현을 잘못하면, 동일한 Socket으로만 계속 데이타를 보내려는 시도를 하게
됩니다. OutputStream의 write() 는 순식간에 끝나지만, 실질적인 데이타는 OS나 네트웍
카드의 Send Queue에 대기중에 있을 수 있으며 아직 상대방에 받지 않았을 수 있습니다.
이 때, 같은 Socket에다 또 다시 데이타를 날리면 계속 큐잉만 일어나고, send용과
receive용을 구분했던 장점들을 제대로 살리지 못하게 되는 것이지요.


주고 받는 데이타는 반드시 header와 body로 구분된 약정된 프로토콜을 정의해서
사용해야 합니다. 예를 들면 10byte을 먼저 받아서 기본적인 정보와 body에 따라올
실제 데이타의 길이을 확인하여 해당 길이만큼의 body 데이타를 다시 읽는 것과 같은
동작이지요. 만약, header가 부정확한 정보들로 채워져 있거나 header에서 명시된 것보다
body데이타가 짧다면 적절한 에러처리를 해야 겠지요. OutputStream으로 단 한번에
write()를 한 byte[] 데이타들이, 받는 측에서 InputStream의 read(buf)를 통해 곧바로
받을 것이라고 여기는 경향이 있습니다.
N/W의 상황에 따라 몇 byte씩 나눠서 날아가지요. 에러가 나기전까지, timeout이 되기
전까지, 그리고 모든 데이타가 올 때 까지 loop를 돌면서 끝까지 받아내야 한다는 것을
아셔야 합니다. 아래글을 참고하세요.

Java Socket Client  read 시 data 한계
http://www.javaservice.net/~java/bbs/read.cgi?m=devtip&b=javatip&c=r_p&n=976117970


PS: 시간이 나면 참한 샘플들을 제공하겠지만, .....

================================================
  자바서비스넷 이원영
  E-mail: javaservice@hanmail.net
  PCS:011-898-7904
================================================


제목 : Re: TCP/IP Socket 프로그램 구현시 고려사항
글쓴이: 최용하(smashing)   2001/12/28 14:59:04  조회수:3121  줄수:23
  
안녕하세요. 이원영 과장님의 글 읽어보니 참 좋은 글이네요.
이 글을 애초에 볼 수 있었다면 그 수많은 시행착오를 거치지 않을 수 있었을텐데 ^^

밑에 첨부한 소스는 실제 모은행에서 인터넷뱅킹과 eCRM 시스템에서 사용되는 TCP/IP
Socket 통신 프로그램입니다.
제가 직접 구현하였으며 그동안 수많은 시행착오를 거치면서 정제되었습니다.
스레드풀링 방식으로 세번째에 해당하는 모델이고요 미약하나마 N/W 장애나
프로세스 상태로 인해 커넥션이 단절되었을 경우에 대한 Recovery 기능도 있습니다.

Peek Point Control 은 다음 네가지 부분에서 적절히 처리됩니다.
1. Established 되어있는 Socket Connection 을 항상 일정갯수 유지 (30개)
2. notify 를 받으려고 wait 하고 있는 스레드 갯수를 한정 (20개)
3. wait 하고 있는 스레드 시간 제약 (20초)
4. 호스트와의 타임아웃은 30초

- 소스역할
Connection.java - 호스트와 통신부분(send & receive 및 에러처리시 자원반환부분 중요)
BankObject.java - 생성된 소켓객체
BankQueue.java - 커넥션풀링을 할수 있게 소켓객체들을 담아놓은 큐

도움이 되었음 좋겠네요.
자바 스크립트가 실행할 메소드는 public로 잡아야 함.

http://mjava.net

188:         //sleep 할 시간을 초기화,, 쪽지가 도착했을때, 쪽지를 보냈을때..실행..
189:         //javascript에서 호출함으로 반드시 public이어야 함..
190:         public void initTime() {
191:                 this.startTime = System.currentTimeMillis(); //시작시간을 현재시간으로 설정
192:                 this.dtime = this.checkNewMemoTime; //기본 checkNewMemoTime을 thread의 dtime(sleep)으로 설정
193:                
194:                 thread.interrupt();
195:         }
스레드를 실행 시키는 간격이 0.2초 정도라면
거의 cpu를 소모 하지 않는다고 함.

대신 쪽지 클라이언트일때만 가능할 것임.

( 서버라면 순식간에 들어 오는 사람을 모두 처리해야 함으로 )
( 채팅 클라이언트라도 빠른 속도를 위해서라면 ?  잘 모르겠음 )

http://mjava.net

114:         public void run() {
115:                 System.out.println("webclient run");
116:                 int ttime = 200; //thread sleep time;
117:                 int mtime = 0; //memo time;
118:                 int ctime = 0; //check time;
119:                 while(!isStop) {
120:                         try {
121:                        
122:                                 // 0.2초단위로 쓰레드가 쓰레드가 실행되도록 함..
123:                                 // 0.2초라하지만 로컬 시스템의 cpu를 거의 소모하지 않는다.
124:                                 if(this.isLoginCheck) {
125:                                         ctime += ttime;
126:                                         if(ctime > checkLoginTime) {
127:                                                 ctime = 0;
128:                                                 checkLoginUser();
129:                                         }
130:                                 }
131:                                
132:                                 if(this.isNewMemoCheck) {
133:                                         mtime += ttime;
134:                                         if(mtime > dtime) {
135:                                                 mtime = 0;                                        
136:                                                 checkNewMemo();
137:                                         }        
138:                                 }
139:                                
140:                                 thread.sleep(ttime);
141:                                
142:                        
143:                                
144:                         } catch(InterruptedException e) {
145:                                
146:                         }
147:                                
148:                 }
=======================================================
서버에서 클라이언트로 메세지를 보내기 전에 먼저
접속이 현재 제대로 이루어져 있는지 확인해 보아야 한다.


/**
   * 메세지를 모든 다른 클라이언트에게 보낸다.
   */

private void broadcast(Client fom, String message) {
    // 연결이 끊겨진 클라이언트들을 잠시 저장하기 위해 사용
    Vector zombies = new Vector(5);

    Enumeration enum = clients.elements();
    while ( enum.hasMoreElements() ) {
        // 클라이언트 객체가 발송자인 경우 무시
        if (client == from) {
            continue;
        }

        if (client.socket == null) {
            zombies.addElement(client);
            continue;
        }
       client.sendMessage(message);
    }

    enum = zombies.elements();
    while ( enum.hasMoreElements() ) {
        Client client  = (Client) enum.nextElement();
        clients.removeElement(client);
    }
}


메세지를 보낼때 문제가 생기면 try catch 부문에서 잡을 수도 있을것 같다.
하지만 어느게 성능이 더 좋은지 잘 모르겠다.

현재 있는 것은 다음과 같이 움직인다.
1. 살아 있나?
2. 살아 있으면 메세지 보냄
3. 죽어 있으면 Enumeration 에 저장
4. 메세지 전부 보낸후 각 Client 삭제 처리

하지만 메세지를 보낼때 문제가 생긴다면 다음과 같을것이다.
1. 메세지 보냄
2. 죽어 있으면
3. 죽어 있는 클라이언트 삭제
4. 살아 있으면 메세지 보냄.

첫번째것은 각 메세지를 보낼때마다 Enumeration 를 생성하는 로드.
두번째것은 죽은 클라이언트들에게 보내는 메세지 로드와 예외 처리 부분의 로드.


아직은 실력이 없어서 확인을 못 해 본다.
채팅 서버의 경우 처럼 쓰레드별로 클아이언트의 소켓 연결을 전담해서 처리하면
프로그램은 아주 간단해 진다. 하지만 쓰레드를 생성하는 것이 프로세스의 생성보다는
훨씬 가벼운 일이지만, 많은 클라이언트의 동시 접속을 처리하기 위해 네트웍 연결마다
쓰레드를 생성하게 되면 메모리 등 리소스 오버헤드가 커지게 된다. 이런 경우에는
쓰레드 하나에 몇 개의 네트웍 연결을 할당하는 방식으로 개선할 수 있다.

쓰레드 하나가 여러 개의 네트웍 연결을 처리하려면 이 채팅 서버처럼 readLine()과 같은
블로킹 메소드를 사용하면 메시지가 들어올 때까지 이 메소드에서 블록되어 버리기 때문에
그동안 다른 네트웍 연결을 처리할 수가 없다. 따라서 이런 경우에는 넌블로킹 메소드인
InputStream의 available() 메소드나 Reader의 ready()를 사용하여 입력이 있는지 여부를
조사해보고, 입력이 있을 경우 현재 가능한 만큼만 읽어들이는 메소드인 read(byte[]) 혹은
read(char[]) 메소드를 사용해야 한다.

단일 쓰레드로 위의 채팅 서버를 고친다면 단일 쓰레드 부분은 다음과 같은 코드로 실행
될 수 있따. 책의 부록에 포함됨 NonBlockingChatServer.java 소스 코드를 참고하기 바란다.


/**
   * 각 클라이언트의 소켓 입력 상태를 검사한다.
   */

public void run() {
    wile (true) {
        Enumeration enum = clients.elements();
        while ( enum.hasMoreElements() ) {
            Client client = (Client) enum.nextElement();
            try {
                if ( client.in.ready() ) {
                    client.getMessage();
                }
            } catch (IOException e) {
                System.err.println(client.getname()+ " 출력 에러 : " + e.getMessage() ) ;
                client.closeSocket();
            }
        }
        try {
            Thread.sleep(10); // 잠깐씩 쉰다.
        } catch ( InterrupteExecption e) { }
    }
}


서버에서 유령사용자 처리는 클라이언트의 일방적인 PING (Not ICMP but send dummy byte)을 사용하도록 함.
irc를 이용해서 서버를 만들고,
클라이언트는 java applet 과 javascript로 만들어 낸다.

irc에 대한 자료는 나중에 추가하도록 한다.

세이클럽, cafe24 등이 irc 기반 이라고 함
페이지 이동
pageList (target, start, scale, view, total, URL)
target = 표시할 객체
start = 글위치 (페이지 단위가 아니고 순차적인 글의 위치)
scale = 페이지의 글리스트 갯수
view = 표시될 페이지 갯수
total = 전체 글 갯수
URL = 링크URL (마지막에 '&'은 빼고... 안빼도 상관없지만^^)


<TABLE bgColor=#999999 height=30><TR><TD><DIV id=pageListDIV></DIV></TD></TR></TABLE>
<SCRIPT>
  function pageList (target, start, scale, view, total, URL) {
    var html = "";
    var reLoading = " <a href=\"javascript:pageList(" +target.id+ ",{page}," +scale+ "," +view+ "," +total+ ",'" +URL+ "');\">{PAGE}</a>";

    if (total % scale) add = 1; else add = 0;
    maxPage = Math.floor(total / scale) + add;

    begin = Math.floor(Math.floor(start/(scale * view)) * view + 1);
    end = Math.floor(begin + view - 1);
    if(end > maxPage) end = maxPage;

    if (begin > 1) {
      html += " <a href='" +URL+ "&start=0'>[1]</a>";
      html += reLoading.replace ("{page}", (begin-2)*scale).replace ("{PAGE}", "◀:");
    }
    for(var i=begin; i<=end; i++) {
      page = (i - 1) * scale;
      if(start != page) {
        html += " <a href='" +URL+ "&start=" +page+ "'>[" +i+ "]</a>";
      } else {
        html += " <b>" +i+ "</b>";
      }
    }
    if (end < maxPage) {
      if (end < maxPage-1) {
        html += reLoading.replace ("{page}", end*scale).replace ("{PAGE}", ":▶");
      }
      page = maxPage * scale;
      html += " <a href='" +URL+ "&start=" +page+ "'>[" +maxPage+ "]</a>";
    }

    target.innerHTML = html;
  }
  pageList (self.pageListDIV, 0, 30, 10, 2000, 'http://phpschool.com/bbs2/inc_board.html?mode=&field=&period=&s_que=&code=tnt2&operator=&category_id=');
</SCRIPT>

+ Recent posts