2013년 10월 18일 금요일

NIO를 이용한 Client/Server 프로그램

NIO를 이용한 Client/Server 프로그램
 
NIO 프로그래밍을 하기 전에 기존 소켓 프로그래밍에서 주로 사용한 멀티 쓰레드를 이용한 방법에 대해 살펴본 후 NIO를 이용하는 방법을 소개하도록 하겠습니다.
 
멀티쓰레드가 아닌 서버와 클라이언트 프로그램을 생각해 볼 때 read 메소드의 Blocking으로 인해 서버는 오직 하나의 클라이언를 상대 해야만 합니다. 이때 다른 클라이언트가 접속을 해오면 대기하게 되는 거죠
 
이를 해결하기 위해 Multi-Thread를 이용한 서버 프로그래밍을 하는데 소켓을 이용하여 MultiThread로 구현된 클라이언트/서버 프로그램의 경우 서버는 클라이언트가 접속할 때 마다 1개 이상의 스레드를 만들어 돌리기 때문에 블록킹 I/O 문제를 해결해 주기는 하지만 수많은 동시 사용자가 몰리게 되면 이것 역시 메모리를 많이 사용하므로 오버헤드가 발생 할 가능성이 있습니다.
 
이러한 부분을 해결하기 위해 NIO가 좋은 솔루션이 될 수 있다고 이전 강좌에서 말씀 드렸습니다. NIO는 채널이라는 개념을 도입해 하나의 쓰레드가 클라이언트의 요청을 처리하는 방법을 제공 합니다.
 
멀티 쓰레드 환경에서 메인 스레드는 클라이언트의 연결을 받기만 하고 클라이언트와 데이터를 주고 받는 일은 별도의 쓰레드에서 처리 하도록 구성 합니다.
 
아래는 Multi-Thread 환경에서의 EchoClient EchoServer 프로그램 입니다.
 
[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.io.*;
import java.net.*;
 
class MultiThreadEchoServer extends Thread { 
             protected Socket sock;
             //----------------------- Constructor
             MultiThreadEchoServer (Socket sock) {
                           this.sock = sock;
             }
 
             //------------------------------------
             public void run() {
                           try   {               
                System.out.println(sock + ": 연결됨");
                InputStream fromClient = sock.getInputStream();
                OutputStream toClient = sock.getOutputStream();
                byte[] buf = new byte[1024];
                int count;
                while( (count = fromClient.read(buf)) != -1 ) {
                    toClient.write( buf, 0, count );
                                                                  System.out.write(buf, 0, count);
                                                     }
                toClient.close();
                System.out.println(sock + ": 연결 종료");
                           }
                           catch( IOException ex )   {
                                        System.out.println(sock + ": 연결 종료 (" + ex + ")");
                           }
                           finally {
                                        try   {
                                                     if ( sock != null )  sock.close();
                                        }
                                        catch( IOException ex ) {}
                           }       
             }
 
             //------------------------------------
             public static void main( String[] args )   throws IOException    {
        ServerSocket serverSock = new ServerSocket( Integer.parseInt(args[0]) );                
        System.out.println(serverSock + ": 서버 소켓 생성");
        while(true)        {
                                        Socket client = serverSock.accept();
                                        MultiThreadEchoServer myServer = new MultiThreadEchoServer(client);
                                        myServer.start();
        }
    }
}
 
 
이번에는 NIO를 이용한 서버프로그램을 만들어 보도록 하겠습니다.
 
클라이언트 프로그램은 EchoClient.java를 그대로 이용 하시면 되구요, 서버쪽의 프로그램만 아래의 NIOServer.java로 바꾸신 후 돌려 보시면 됩니다. 특별한 설정은 필요 없습니다.
 
서버에서 클라이언트로 데이터를 보낼 때 는 한글 변환을 했지만 클라이언트에서 서버로 데이터를 보낼때는 한글 처리가 되지 않았으니 참고 하시구요
 
소스 코드에 대한 해석은 주석을 통해 이해 할 수 있도록 하시길 바라구요, NIO 프로그래밍과 자바 네트웍 프로그래밍에 대해 공부 하실 분들은 sunny.sarang.net의 김성박씨께서 저술한 자바 IO & NIO 네트워크 프로그래밍(한빛미디어)을 추천해 드립니다.
 
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;
                                                                                

댓글 없음:

댓글 쓰기