2018년 11월 27일 화요일

Java 소켓통신으로 echo 출력 결과 확인하기

이번시간에는 소켓을 통해서 작성한 서버로 보내서 다시 출력하는 즉, echo를 프로그램을 작성해보도록 하겠다.

소켓은 데이터, 파일 등 다양하게 통신을 주고받기 위한 통로 역할을 한다.

사용자에게 Application Layer에서 데이터를 다른 통신기기로 전송할 수 있도록 해준다.
즉, low 계층을 거쳐서 데이터를 캡슐화하거나 쪼개서 전송하는 역할을 소켓이 해준다.

그러나 IP주소와 PORT번호를 알고 있어야 이러한 통신이 가능하다는것을 잊지말자.
다음과 같이 서버와 클라이언트 간의 상호작용하기 위한 역할들을 기억하자.


  • 클라이언트 : 단순히 서버의 IP주소와 포트번호를 알고 있어야 함.
  • 서버 : 포트번호에 맞는 접속 요청이 올 때까지 대기함.




서버와 클라이언트의 소켓통신 과정

  1. 서버는 클라이언트에게 접속할 수 있도록 소켓을 열고 대기한다.
(이때, 소켓은 I/O-blocking, NON-blocking모드에 따라 대기방법이 약간 차이가 있을 수도 있다.)

  2. 클라이언트는 서버의 IP와 PORT로 접속을 요청함.

  3. 서버는 클라이언트의 접속요청을 받고 스트림 통신을 해준다.
(이때, 소켓에서는 중요한 역할이 있다. 접속 요청한 클라이언트와 통신을 주고받을 새로운 소켓을 생성하여 연결해준 뒤, 원래의 소켓은 다시 대기상태로 돌아간다. 다른 클라이언트와도 통신을 해야하는 MultiThread 방식에서는 중요한 역할이다. 접속 요청에 따라 쓰레드를 생성해주어 요청한 클라이언트에게 소켓을 제공해준다. 약간의 보완해야할 점이있다. 바로 수 많은 클라이언트들의 요청 혹은 악의적으로 서비스 거부 공격인 DDOS에 대해서는 어떻게 할 것인지를 생각해야한다. 조금 얘기가 길어지지만, 중요하므로 이해해야한다. 다양한 방법이 있지만, 대표적으로 Thread pool을 사용해서 쓰레드의 수를 제한하여 안정된 서버를 유지해야 한다.)

  4. 새로운 소켓으로 클라이언트는 서버와 통신을 주고받고 서버는 원래의 소켓을 통해 다른 요청을 기다린다.






예제 - 클라이언트와 서버의 소켓통신 echo
  1. package org.java.project.socket;
  2.  
  3. import java.io.BufferedReader;
  4. import java.io.BufferedWriter;
  5. import java.io.IOException;
  6. import java.io.InputStreamReader;
  7. import java.io.OutputStreamWriter;
  8. import java.net.ServerSocket;
  9. import java.net.Socket;
  10. import java.util.Date;
  11. import java.util.concurrent.Executor;
  12. import java.util.concurrent.ExecutorService;
  13. import java.util.concurrent.Executors;
  14.  
  15. class MultiThread implements Runnable {
  16.  
  17.     Socket connection;
  18.  
  19.     public MultiThread(Socket connection) {
  20.         this.connection = connection;
  21.     }
  22.  
  23.     public void run() {
  24.         try {
  25.             // 단순히 echo 출력이므로, 텍스트를 입/출력 스트림으로 사용하기 위해 Buffer를 사용함.
  26.             BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
  27.             BufferedWriter out = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream()));
  28.  
  29.             // 소켓통신에 연결된 시간을 출력하기 위한 용도.
  30.             Date now = new Date();
  31.             out.write(now.toString() + "\r\n"); // \r\n으로 한줄의 입력이 끝난걸 의미함.
  32.             out.flush();
  33.  
  34.             // 소켓에 접속한 클라이언트가 있다는 것을 알림
  35.             System.out.println("Client Connect Success!");
  36.  
  37.             for (String data = in.readLine(); data != null; data = in.readLine()) {
  38.  
  39.                 if (data.equals("exit")) { // exit입력을 받으면 클라이언트 측 소켓 종료 됨.
  40.                     out.write("bye bye \r\n"); // 클라이언트측에 접속 종료를 알림.
  41.                     System.out.println("bye bye"); // 서버측에 접속 종룔를 알림.
  42.  
  43.                     in.close();
  44.                     out.close();
  45.  
  46.                     // 소켓 접속 종료. 단순히 이것만으로도 열려있는 입/출력 스트림이 닫힘.
  47.                     connection.close();
  48.                     break;
  49.                 }
  50.                 System.out.println("Client >> " + data); // 서버에 표시
  51.                 out.write("echo : " + data + "\r\n"); // 클라이언트에게 echo 표시함.
  52.                 out.flush();
  53.             }
  54.  
  55.             // 서버에 println()출력
  56.             System.out.println(now.toString());
  57.         } catch (IOException e) {
  58.             e.printStackTrace();
  59.         }
  60.  
  61.     }
  62. }
  63.  
  64. public class EchoExam2 {
  65.     // 포트: 1313
  66.     private static final int PORT = 1314;
  67.     // 호스트: 로컬
  68.     private static final String HOST = "localhost";
  69.     // 멀티태스킹 : 50명의 클라이언트로 제한(쓰레드 풀사용)
  70.     private static final int pool = 50;
  71.  
  72.     public static void main(String[] args) {
  73.         ExecutorService service = Executors.newFixedThreadPool(EchoExam2.pool);
  74.  
  75.         // 서버 소켓생성 : 포트번호만 필요로 한다.
  76.         try (ServerSocket server = new ServerSocket(PORT)) {
  77.             while (true) {
  78.                 System.out.println("wait.....");
  79.                 // 서버는 요청이 올때까지 대기한다. (I/O-blocking 방식)
  80.                 Socket socket = server.accept();
  81.  
  82.                 // 클라이언트의 요청이 오면 실행된다.
  83.                 // 하나의 쓰레드로 처리됨.
  84.                 Runnable task = new MultiThread(socket);
  85.  
  86.                 // run() 실행 == Thread객체의 start메소드를 실행
  87.                 service.submit(task);
  88.             }
  89.         } catch (IOException e) {
  90.             e.printStackTrace();
  91.         }
  92.  
  93.     }
  94.  
  95. }

▶ 실행화면

1) 서버 : 클라이언트의 요청을 대기








2) 클라이언트 : 서버 접속 요청 








 3) 클라이언트-서버 소켓 연결성공











4) 클라이언트 "hi"입력 후 서버로 부터 echo 되는 메시지 확인











5) 클라이언트 : 여러 데이터를 전송 / 서버 : 클라이언트가 전송한 데이터 echo출력












6) 약간의 문제점 확인 Backspace또한 문자처리 됨.












7) 클라이언트 : exit입력 후 종료되었으며, 서버에서는 예외처리 발생






















: 6번째 그림을 보면 코드의 약간의 문제점이 있다. 서버소켓은 무한 루프로 사용자가 입력한 데이터를 readLine한 줄 씩 읽는다. 이때, 잘못된 입력을 해서 지우게된다면 ←(Backspace key) 이 또한 키보드 입력으로 인식이 되서 이러한 오류가 난다. 그래서 test test22 중간에 Backspace key 4번이 입력된 것을 볼 수 있다.

그리고 마지막 7번째 그림을 보면 socket closed가 되었다는 에러를 발생하는데, 이것은 코드 51번째와 52번째를 보면 바로 이해가 될 것이다. 만약 exit입력을 받았을때, 37번째 for문안의 39번째 if문을 만나서 종료가 된다. 그러면 이때, 44번째 out.close()가 된다. 그러면 출력 스트림은 닫혀서 다시 사용할 수 없게 되는데, 51번과 52번에 사용이 되고있다. 이것을 else문으로 만들면 바로 해결이 되는 문제다. (이러한 에러를 보면서 위의 코드를 조금 더 깊이 있게 이해하도록 하자.)


이번시간에는 서버-클라이언트 소켓 통신의 단면적으로 서버쪽에 데이터를 요청하는 클라이언트에 대해 자세히 살펴보았다.

다음에는 클라이언트 - 소켓을 통한 채팅을 주고받는것을 살펴보도록해보자.

댓글 없음:

댓글 쓰기

[Java] N-I/O(Non-Blocking) 파일 읽기 쓰기 - GatheringByteChannel, ScatteringByteChannel, ByteBuffer 사용.

우리는 지금까지 다음과 같이 살펴보았다. 1.  InputStream / OutputStream : 입, 출력 스트림을 바이트로 처리하여 읽기, 쓰기. 2.  FileInputStream / FileOutputStream : 입, 출력 스트림을 ...