레이블이 Java인 게시물을 표시합니다. 모든 게시물 표시
레이블이 Java인 게시물을 표시합니다. 모든 게시물 표시

2018년 12월 4일 화요일

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

우리는 지금까지 다음과 같이 살펴보았다.

1.  InputStream / OutputStream : 입, 출력 스트림을 바이트로 처리하여 읽기, 쓰기.
2.  FileInputStream / FileOutputStream : 입, 출력 스트림을 바이트로 파일에 읽기, 쓰기.
3.  BufferedInputStream / BufferedOutputStream : 입, 출력을 버퍼를 사용해서 읽기, 쓰기.
4.  BufferdReader / bufferdWriter : 입, 출력을 텍스트형태로 한 라인씩 읽고 쓰기.
5.  DataInputStream / DataOutputStream : 자바의 데이터 형식으로 읽기, 쓰기 사용.

이제는 ByteBuffer를 통해서, 입, 출력 문자들을 저장한 뒤, Non-Blocking방식으로
저장한 데이터를 읽기, 쓰기 형태로 바꾸는 방법을 사용해보도록 하겠다.

다음시간에는 소켓을 통한 방법을 살펴 볼 예정이며, 이번시간에는 파일에 읽기 쓰기 형태로 알아보도록 하겠다. 

- 소켓에 적용 : Channel을 통해서 읽고 쓰기할땐, 하나의 버퍼를 사용하면됨. (selector라는 역할을 주의 깊게 봐야함.)

- 파일에 적용 : ByteBuffer를 2개 사용하여, 조금 더 버퍼사용 방법에 대해서 살펴보도록 하 겠다. 왜냐하면 버퍼를 사용할땐 지금까지 스트림을 통해 read, write한것과는 사뭇 다르기 때문이다.

예제를 살펴보기 전, 하나만 기억하자.
1. ByteBuffer는 위치를 가리키는 position이 존재한다. (default : 0번에 위치함.)
2. 이 position값은 데이터를 읽기, 쓰기할때 데이터의 위치를 가리킨다.
3. ByteBuffer로 데이터를 읽을 때, position은 1씩 증가함.
(position : 0번에 위치 -> 데이터를 읽음 -> 1번에 위치 즉, 다음 읽을 값을 가리킴.)
4. 마찬가지로 write할때도 3번 형태와 같아짐.
5. 소켓에서는 하나의 ByteBuffer를 사용하게 되는데, 읽고 쓰기할때는, position값을 항상 0번에 위치시키기 위해서 .flip()메소드를 사용함.



예제 - A.txt파일에 ByteBuffer를 사용해서 데이터를 읽어 B.txt파일에 저장하기.(저장할때도 ByteBuffer를 사용.)


▶ A.txt












ByteBuffer_Exam2.java
  1. package org.java.project.hanbat.chapter11;
  2.  
  3. import java.io.FileInputStream;
  4. import java.io.FileOutputStream;
  5. import java.io.IOException;
  6. import java.nio.ByteBuffer;
  7. import java.nio.channels.GatheringByteChannel;
  8. import java.nio.channels.ScatteringByteChannel;
  9.  
  10. public class ByteBuffer_Exam2{
  11.     public static void main(String[] args) {
  12.         String aFile = "A.txt";
  13.         String bFile = "B.txt";
  14.        
  15.         // ByteBuffer크기를 20으로 지정함. -> 최대 20Byte저장가능.
  16.         ByteBuffer buffer = ByteBuffer.allocate(20); // 읽기, 쓰기 용도의 버퍼
  17.        
  18.         try {
  19.             // 파일에 버퍼의 데이터를 read할때,
  20.             ScatteringByteChannel inChannel = (new FileInputStream(aFile)).getChannel();
  21.            
  22.             // 파일에 버퍼의 데이터를 write할때
  23.             GatheringByteChannel outChannel = (new FileOutputStream(bFile)).getChannel();
  24.            
  25.             // A.txt 파일의 내용을 읽어와서 Buffer에 저장 이때 position은 읽어들인 바이트 수 만큼 위치함.
  26.             int readCnt = inChannel.read(buffer);
  27.             System.out.println("read Count : " + readCnt);
  28.             inChannel.close();
  29.            
  30.             // 읽어온 A.txt의 내용을 콘솔로 출력해보기. byte배열을 사용하여 저장한뒤 출력함.
  31.             // position위치를 0으로 수정함.
  32.             buffer.flip();
  33.             byte[] rByte = new byte[20];
  34.             buffer.get(rByte); // 이때 position값은 rByte에 저장한 만큼 이동하게 된다.
  35.            
  36.             System.out.println("A.txt : " + new String(rByte)); // A.txt파일 내용을 콘솔에 출력
  37.            
  38.             // 다시 버퍼의 position값을 0으로 변경.(B.txt파일에 write해야하므로)
  39. //          buffer.flip();
  40.            
  41.             // B.txt에 읽어온 바이트 버퍼를 write함. 이때 버퍼의 position은 write한 만큼 이동함.
  42.             outChannel.write(buffer); //  
  43.             outChannel.close();
  44.         } catch(IOException e) {
  45.             e.printStackTrace();
  46.         }
  47.     }
  48. }




▶실행결과
B.txt가 빈 파일로 생성 되었으며, 다음과 같은 예외가 발생함.









: 읽어 들인 데이터의 수는 17이며 A.txt의 내용 17바이트만큼 잘 읽어왔다. 그러나 예외처리가 발생했다.
34번 라인을 보면, 읽어온 바이트 버퍼를 바이트 배열에 저장할때, BufferUnderflowException이 발생한다. 왜냐? 읽어온 바이트는 17바이트 인데, 20바이트의 공간을 갖는 배열에 저장하려고 하기 때문이다. 반대로 읽어온 바이트가 20바이트 이상이라면, 20바이트 크기를 갖는 배열에 저장할 때는 아무런 문제가 없다.

즉, 바이트 버퍼를 .get을 통해 바이트 배열에 저장할 때는 바이트 배열의 크기만큼 버퍼의 데이터를 저장하는데, 만약 버퍼의 크기가 더 작다면, 이는 바이트 배열의 공간이 낭비되므로 BufferUnderflowException이 발생한다. 
바이트 버퍼를 바이트 배열에 저장할 때 항상 바이트 배열의 공간보다 적게 데이터를 저장하지 않도록 주의하자.


- 텍스트의 크기를 다시 수정한 결과.


▶ A.txt






▶ 실행결과







▶ B.txt
















: B.txt에 20Byte만큼 저장된 결과를 확인할 수 있다. (띄어쓰기도 포함 되는 것을 주의!)

이로써 바이트 버퍼 사용하는 방법에 대해 살펴보았으며, 다음시간에는 
소켓을 바이트 버퍼를 통해서 서버와 클라이언트가 읽고 쓰는 예제에 대해서 살펴보도록 하겠다.














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 : 입, 출력 스트림을 ...