개인참고자료/자바(네트워크)

TCP 프로그래밍 - 간단한 에코 클라이언트/서버 프로래밍

경진 2008. 7. 17. 19:01
간단한 에코 클라이언트/서버 프로래밍

에코(Echo)는 말 그대로 메이리를 의미한다. 클라이언트가 보낸 데이터를 서버 쪽에서 받아들여, 클라이언트에게 그대로 다시 보내주는 것을 의미한다.

에코 클라이언트와 서버는 각각 다음과 같은 방식으로 동작한다.

에코서버

1. 10001번 포트에서 동작하는 ServerSocket을 생성한다.
2. ServerSocket의 accept() 메소드를 실행해서 클라이언트의 접속을 대기한다.
3. 클라이언트가 접속할 경우 accept() 메소드는 Socket 객체를 반환한다.
4. 반환 받은 Socket으로 InputStream과 OutputStream을 구한다.
5. InputStream은 BufferedReader 형식으로 변환하고 OutputStream은 PrintWriter 형식으로 변환한다.
6. BufferedReader의 readLine() 메소드를 이용해서 클라이언트가 보내는 문자열 한 줄을 읽어 들인다.
7. 6에서 읽어 들인 문자열을 PrintWriter에 있는 Println() 메소드를 이용해서 다시 클라이언트로 전송한다.
8. 6,7의 작업은 클라이언트가 접속을 종료할 때까지 반복된다. 클라이언트가 접속을 종료하게 되면 BufferedReader에 있는 readLine() 메소드는 null값을 반환하게 된다.
9. IO 객체와 소켓의 close() 메소드를 호출한다.

에코 클라이언트

1. Socket 생성자에 서버의 IP와 서버 동작 포트값(10001)을 인자로 넣어 생성한다. 소켓이 성공적으로 생성되었다면, 서버와 접속이 성공적으로 접속 되었다는 것을 의미한다.
2. 생성된 Socket으로 부터 InputStream과 OutputStream을 구한다.
3. InputStream은 BufferedReader 형식으로 변환하고 OutputStream은 PrintWriter 형식으로 변환한다.
4. 키보드로부터 한 줄씩 입력 받는 BufferedReader 객체를 생성한다.
5. 키보드로부터 한줄을 입력받아 PrintWriter에 있는 println() 메소드를 이용해서 서버에게 전송한다.
6. 서버가 다시 반환하는 문자열을 BufferedReader에 있는 readLine() 메소드를 이용해서 읽어 들인다. 읽어 들인 문자열은 화면에 출력한다.
7. 4, 5, 6을 키보드로부터 quit 문자열을 입력 받을 때까지 반복한다.
8. 키보드로부터 quit 문자열이 입력되면 IO 객체와 소켓의 close() 메소드를 호출한다.

에코 서버 프로그래밍

import java.net.*;           
import java.io.*;            
           
public class EchoServer {            
           
    public static void main(String[] args) {       
        try{   
            ServerSocket server = new ServerSocket(10001);
            System.out.println("접속을 기다립니다.");
            Socket sock = server.accept();
            InetAddress inetaddr = sock.getInetAddress();
            System.out.println(inetaddr.getHostAddress() + " 로 부터 접속하였습니다.");
            OutputStream out = sock.getOutputStream();
            InputStream in = sock.getInputStream();
            PrintWriter pw = new PrintWriter(new OutputStreamWriter(out));
            BufferedReader br = new BufferedReader(new InputStreamReader(in));
            String line = null;   
            while((line = br.readLine()) != null){   
                System.out.println("클라이언트로 부터 전송받은 문자열 : " + line);
                pw.println(line);
                pw.flush();
            }   
            pw.close();   
            br.close();   
            sock.close();   
        }catch(Exception e){       
            System.out.println(e);   
        }       
    } // main           
}               

클라이언트의 접속을 대기하려고 10001번 포트에서 대기하는 ServerSocket을 생성한다. ServerSocket이 생성되었다면 accept()메소드를 이용해서 클라이언트의 접속에 대기한다. accept() 메소드는 클라이언트가 접속할 때까지 블로킹(멈춤)상태가 된다.

블로킹 상태에 있던 서버는 클라이언트가 접속할 경우, 클라이언트와 통신할 수 있게 도와주는 Socket 객체를 반환하게 된다.

            ServerSocket server = new ServerSocket(10001);
            System.out.println("접속을 기다립니다.");
            Socket sock = server.accept();

접속한 클라이언트의 정보를 알아내려고 Socket 객체에 있는 getInetAddress() 메소드를 사용했다.
getInetAddress()는 소켓을 통해 연결된 상대방의 IP 주소등을 구할 수 있다.

            InetAddress inetaddr = sock.getInetAddress();
            System.out.println(inetaddr.getHostAddress() + " 로 부터 접속하였습니다.");

Socket에 있는 getOutputStream()과 getInputStream()은 각각 연결된 상대방과 통신할 수 있도록 도와주는 OutputStream과 InputStream을 얻을 수 있게 한다. OutputStream과 InputStream을 구했다면, 좀더 편하게 입출력 작업을 할 수 있도록 다른 IO 객체로 변환시킬 수도 있다. 아래의 소스를 보면 알겠지만 OutputStream은 PrintWriter로 InputStream은 BufferedReader로 변환해서 사용한다.

            OutputStream out = sock.getOutputStream();
            InputStream in = sock.getInputStream();
            PrintWriter pw = new PrintWriter(new OutputStreamWriter(out));
            BufferedReader br = new BufferedReader(new InputStreamReader(in));

BufferedReader로부터 클라이언트가 전송하는 문자열 한 줄을 읽어 들인다. readLine() 메소드는 읽어 들일 때 개행문자를 삭제하기 때문에 다시 클라이언트 쪽으로 문자열을 전송할 때에는 문자열 뒤에 개행문자를 추가해서 전송해야 한다.

            String line = null;   
            while((line = br.readLine()) != null){   
                System.out.println("클라이언트로 부터 전송받은 문자열 : " + line);
                pw.println(line);
                pw.flush();
            }   

클라이언트 쪽에서 접속응ㄹ 해지하면, readLine() 메소드는 null 값을 반환한다. 이때 앞서 설명한 while문을 빠져나가게 된다. 접속이 끊어졌다면 IO 객체와 소켓의 close() 메소드를 호출한다.

            pw.close();   
            br.close();   
            sock.close();  

에코 클라이언트 프로그래밍

import java.net.*;               
import java.io.*;                
               
public class EchoClient{                
               
    public static void main(String[] args) {           
        try{       
            Socket sock = new Socket("127.0.0.1", 10001);   
            BufferedReader keyboard = new BufferedReader(new InputStreamReader(System.in));   
            OutputStream out = sock.getOutputStream();   
            InputStream in = sock.getInputStream();   
            PrintWriter pw = new PrintWriter(new OutputStreamWriter(out));   
            BufferedReader br = new BufferedReader(new InputStreamReader(in));   
            String line = null;   
            while((line = keyboard.readLine()) != null){   
                if(line.equals("quit")) break;
                pw.println(line);
                pw.flush();
                String echo = br.readLine();
                System.out.println("서버 로부터 전달받은 문자열 :" + echo);
            }   
            pw.close();   
            br.close();   
            sock.close();   
        }catch(Exception e){       
            System.out.println(e);   
        }       
    } // main           
}               

다음과 같이 서버의 IP와 서버의 동작 포트를 인자로 지정해서 Socket 객체를 생성한다.

            Socket sock = new Socket("127.0.0.1", 10001);   

키보드로부터 한 줄씩 입력 받기 위해서, 표준 입력 장치인 System.in을 BufferedReader 형식으로 변환한다.

            BufferedReader br = new BufferedReader(new InputStreamReader(in));   

키보드로 부터 한 줄을 입력 받아 PrintWriter의 println()메소드를 이용해서 서버 쪽에서 한 줄의 문자열을 전송한다. 이때도 반드시 flush()메소드를 호출해야 한다. 서버에 문자열 한 줄을 전송한 후, 다시 서버 쪽에서 보내는 문자열을 BufferedReader의 readLine() 메소드를 이용해서 읽어 들인다. 이와 같은 동작을 키보드로 부터 quit 문자열을 입력 받을 때까지 반복한다.

            while((line = keyboard.readLine()) != null){   
                if(line.equals("quit")) break;
                pw.println(line);
                pw.flush();
                String echo = br.readLine();
                System.out.println("서버 로부터 전달받은 문자열 :" + echo);
            }   

키보드로부터 "quit"문자열을 입력 받으면, while문으로부터 빠져 나오게 된다. 더 이상 서버와 통신하지 않을 것이기 때문에 IO 객체와 소켓에 있는 close() 메소드를 호출하게 된다. 이때 서버 쪽에서는 BufferedReader의 readLine() 메소드가 null 값을 반환하게 된다.

            pw.close();   
            br.close();   
            sock.close();  

에코 서버와 클라이언트 실행

에코 서버와 에코 클라이언트를 모두 작성했으면 소스가 있는 디렉토리에서 다음과 같이 컴파일한다.

javac *.java

컴파일이 끝나면 서버를 실행시킨다. 서버가 실행되면 accept() 메소드가 호출 되면서 멈춰있는 상태가 된다. 이때 새로운 도스 창을 열고 같이 클라이언트를 실행한다.

클라이언트가 실행되었다면 문장을 입력하고 엔터 키를 입력한다. 서버로 문자열이 전송되고 다시 클라이언트에 전송해주는 것을 확인 할 수 있다.

결과화면

결화화면

에코 클라이언트와 에코 서버의 문제점

방금 소개한 에코 클라이언트와 에코 서버에는 중요한 문제점이 있다. 바로 서버가 단하나의 클라이언트 접속만을 처리할 수 있다는 것이다. accept() 로 대기하고 있다가 클라이언트의 접속 요청이 오면 클라이언트와 통신할 수 있는 소켓을 반환한 후, 다시 accept() 하지 않기 때문이다.