Posts 학원 #50일차: 자바 프로그램-운영체제-하드웨어 호출관계, 연결방식(Connectionful, Connectionless) 통신방식(Stateful, Stateless)
Post
Cancel

학원 #50일차: 자바 프로그램-운영체제-하드웨어 호출관계, 연결방식(Connectionful, Connectionless) 통신방식(Stateful, Stateless)

어제 복습을 하다가 생긴 궁금증을 수업시간 전에 말씀드렸더니 감사히도 장장 1시간 동안 설명해주셨다. 컴퓨터 구조와 관련되어 있어 완벽히 이해하기는 어려웠지만 이해한 만큼 정리해보았다.

질문

net.ex03.Clinet0110의 out.write(100) 코드 위에 “실제 write()는 소켓의 내부 버퍼로 출력한다.”라는 주석이 있는데, 혹시 이것에 대해 설명해주실 수 있으신가요? Socket 클래스 소스코드에 setSendBufferSize()랑 getSendBufferSize() 메서드가 있긴 한데 실제 버퍼는 없는 것 같아서요.. 여기서 말하는 소켓은 자바의 Socket 클래스가 아닌 건가요?

답변

KakaoTalk_20201002_011440890

랜카드 메모리(RAM)가 버퍼를 쓰고 있다는 말이다. 모든 하드웨어는 RAM과 CPU를 가지고 있다. 프린터기도 RAM이 있어서 미리 출력할 데이터를 프린터 메모리에 올려 두고 출력을 한다.

KakaoTalk_20201002_011441262

byte stream의 경우 socket.getOutputStream으로 리턴받은 객체로 출력할 때는 소켓을 통해 출력할 경우 소켓에 상관 없이 바로 출력한다. 주석에서 문법을 설명하기 위해 소켓의 버퍼로 출력한다는 말을 하였지만, 실제로는 랜카드 내부 RAM의 일부 영역에 있는 버퍼로 출력이 된다. write() 메서드 호출 후 리턴되는 시기는 클라이언트가 read() 메서드를 사용하여 읽는 시점이 아니다. 버퍼에 쌓인 것을 상대편이 데이터를 읽던 말든. 랜카드에 떨어지는 그 순간에 리턴한다. 다시 말해, 출력이 다 이루어진다는 것의 의미는 상대편 컴퓨터에서 읽었느냐가 아니다. out.write()의 의미는 소켓의 버퍼(랜카드 버퍼) 출력되었는지 안 되었는지이다.

KakaoTalk_20201002_011441657

위의 그림을 보면 알 수 있든 App에서는 자바의 Socket 클래스를 사용하여 Socket 클래스를 통제하고, 이 자바의 Socket은 OS 소켓을 통제한다. OS 소켓은 자바가 아니라 C/C++ 언어로 쓰여져 있다. 그리고 OS 소켓은 랜카드를 제어한다.

운영체제를 경유하지 않고 자바에서 하드웨어를 다이렉터로 제어하는 프로그램은 존재하지 않는다. 해킹의 위험이 있기 때문이다. 운영체제가 하드웨어를 제어하는 것이다. 윈도우 JVM은 윈도우에 있는 C, C++ 함수를 호출하며 동작한다.

자바 개발자는 그냥 Socket 객체를 생성해서 입출력을 하면 되지만, 그 소켓 클래스는 내부적으로 운영체제(리눅스, 윈도우, 맥)의 C/C++ 함수를 호출하여 하드웨어를 제어한다. 그렇기 때문에 JVM은 운영체제별로 따로 있다.

JVM은 App를 OS와 연결시켜주는 다리 역할을 한다(App -> JVM -> OS). 운영체제에 따라 호출하는 메서드가 다르기 때문에 App이 운영체제 함수를 다이렉트로 호출할 수 없다.

KakaoTalk_20201002_011441899

그렇다고 JVM이 Driver(하드웨어를 제어하는 소프트웨어. 하드웨어 제조사에서 제공한다)의 메서드를 직접적으로 호출하는 것은 아니다. 운영체제의 구조를 좀 더 자세히 들어가면 HAL(Hardware Abstraction Layer)이라는 추상층이 있다. 자바 언어로 만든 프로그램은 JVM의 메서드를 호출하고, JVM의 메서드는 OS의 HAL 메서드를 호출한다. HAL의 메서드는 하드웨어를 제어하는 Driver의 메서드를 호출한다. Socket 클래스를 사용할 경우, Socket 클래스는 HAL의 OS 소켓 API를 호출하고, OS 소켓 API는 Driver의 Lancard API를 호출하며, 결과적으로 랜카드를 제어한다.

KakaoTalk_20201002_011441790

즉 프로그램이 운영체제에서 제공해주는 함수를 호출하고, 이 함수가 하드웨어를 제어하는 function을 호출하는 구조로 되어 있다. 직접 하드웨어 제조사의 함수를 호출하면 특정 하드웨어에 종속될 수 있으니까 HAL이 있는 것이다. HAL은 프로그래머가 디바이스 독립적인 프로그램을 만들 수 있도록 해준다.

HAL 계층을 타지 않을 수도 있는데, 이것이 Direct X이다. 게임 성능을 위해 하드웨어 제공자가 제공한다.

바이트 스트림은 바로 출력하고, 캐릭터 스트림은 내부 버퍼에 담아놓기 때문에 바로 출력하지 않는다. 따라서 character stream을 사용할 때는 반드시 write() 호출 후 flush()를 호출해야 한다. 그러나 실무에서는 출력할 때는 byte stream이든 character stream이든 flush()를 호출하는 편이 안전하다.

어제자 수업 복습

KakaoTalk_20201002_011440310

KakaoTalk_20201002_011439934

KakaoTalk_20201002_011440599

KakaoTalk_20201002_011439275

KakaoTalk_20201002_011442030

KakaoTalk_20201002_011439553

KakaoTalk_20201002_011444320

연결 방식

KakaoTalk_20201002_011443391

KakaoTalk_20201002_011443507

통신 방식

KakaoTalk_20201002_011443217

Stateful

서버와 연결한 후, 클라이언트에서 연결을 끊을 때까지 계속해서 연결을 유지하며 클라이언트 요청을 처리한다.

KakaoTalk_20201002_011443640

단점

  • 한 번 연결하면 클라이언트가 연결을 끊을 때까지 계속 유지한다.
  • 클라이언트가 작업을 요청하지 않더라도 계속 서버에 연결정보를 계속 유지하기 때문에 서버 메모리를 많이 차지하고, 동시에 많은 클라이언트의 요청을 처리하기 힘들다.
  • 만약 서버가 순차적으로 클라이언트와 연결을 수행한다면 이전에 연결했던 클라이언트가 연결을 끊기 전까지는 다른 클라이언트와 연결되지 못하는 문제가 있다.

장점

  • 한 번 서버에 연결되면 클라이언트가 연결을 끊을 때까지 유지되기 때문에 요청할 때마다 매번 연결 작업을 수행할 필요가 없다. 따라서 요청에 대한 응답이 빠르다.
  • 연결된 상태에서 수행한 작업 정보를 서버에 유지할 수 있어 영속성이 필요한 작업을 처리하는 데 유리하다.
  • 작업시간: 데이터 전송 시간 + 작업 처리 시간 + 데이터 수신 시간
    • 즉 작업을 요청할 때마다 연결할 필요가 없기 때문에 연결하는데 시간이 걸리지 않는다.

대표적인 예

  • 게임 서버 연결: 서버에 한 번 연결되면 게임을 마칠 때까지 데이터를 주고받는다.
  • 화상 통신: 한 번 연결하면 연결을 끊을 때까지 데이터를 주고받는다.
  • 채팅 서버: 전용 클라이언트를 이용한 채팅 ex) 유튜브 라이브 방송 채팅
  • 텔렛: 원격 제어 프로그램
  • FTP: 파일 전송 프로그램
  • 오프라인 예: 건강보험문의, 상담 등

실습

KakaoTalk_20201002_011443774

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Client0120 {
  public static void main(String[] args) {
    Scanner keyScan = new Scanner(System.in);
    try (Socket socket = new Socket("localhost", 8888);
        PrintWriter out = new PrintWriter(socket.getOutputSTream());
        BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())) {
          
          System.out.println("서버와 연결되었음!");
          
          String name = null;
          do {
            System.out.print("이름?");
            name = keyScan.nextLine();
            
            out.println(name);
            out.flush();
            
            String str = in.readLine();
            System.out.println(str);
          } while (!name.equalsIgnoreCase("quit") && !name.equalsIgnoreCase("stop"));
          
        } catch (Exception e) {
          e.printStackTrace();
        }
         
      keyScan.close();
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class Server0120 {
  public static void main(String[] args) {
    try (Scanner keyboard = new Scanner(System.in);
        ServerSocket serverSocket = new ServerSocket(8888)) {
      System.out.println("서버 실행!");
      
      loop: while(true) {
        // 한 클라이언트와 대화가 끝나면 다음 클라이언트와 대화를 한다.
        try (Socket socket = serverSocket.accept();
             BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
             PrintWriter = new PrintWriter(socket.getOutputStream())) {

          System.out.println("클라이언트와 연결되었음!");

          while (true) {
            String name = in.readLine();
            if (name.equalsIgnoreCase("quit")) { //클라이언트와 연결 끊기
              out.println("Goodbye!");
              out.flush();
              break;
            } else if (name.equalsIgnoreCase("stop")) { //서버 종료하기
              out.println("Goodbye!");
              out.flush();
              break loop;
            }
            out.printf("%s님 반갑습니다!\n", name);
            out.flush();
          }
        } catch(Exception e) {
          System.out.println("클라이언트와 통신 도중 오류 발생!");
        }
        System.out.println("클라이언트와의 연결을 끊었음.");
      }

    } catch (Exception e) {
      e.printStackTrace();
    }
    System.out.println("서버 종료!");
  }
}

stateless

서버에 작업을 요청할 때 연결하고, 서버로부터 응답을 받으면 즉시 연결을 끊는다. 즉 요청/응답을 한 번만 수행한다. 따라서 비영속적인 단일 작업을 처리할 때 적합한 통신 방식이다.

KakaoTalk_20201002_011443908

단점

  • 요청할 때마다 매번 서버에 연결해야 하기 때문에 실행 시간이 많이 걸린다.
  • 실행 시간 = 연결하는데 걸린 시간 + 데이터 전송 시간 + 작업 처리 시간 + 데이터 수신 시간
  • 서버와 연결할 때 사용자 인증(authentication; 아이디, 암호 유효 여부 검사)이나 사용권한 확인(authorization; 사용자에게 허락된 작업 범위를 확인) 같은 작업을 반드시 수행하는 경우 연결하는 데 더더욱 많은 시간이 걸린다.

장점

  • 서버가 계속 연결된 상태가 아니기 때문에 서버 입장에서는 메모리를 많이 사용하지 않는다. 클라이언트와 연결을 계속 유지하지 않기 때문에 작업을 처리하는 동안만 연결정보를 유지하기 때문이다.
  • 따라서 같은 메모리라도 stateful 방식보다는 더 많은 클라이언트의 요청을 처리할 수 있다.

대표적인 예

  • HTTP 통신: 웹 브라우저가 서버에 연결한 후 요청을 하고 서버가 응답을 한 후 연결을 끊는다.
  • 메신저: 메신저 서버에 연결하고 메시지 전송 후 연결을 끊는다.
  • 메일 전송: 메일 서버에 데이터 전송 후 연결을 끊는다.
  • 오프라인 예: 114 안내, 배달 등

실습

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Server0210 {
  public static void main(String[] args) {
    try (Scanner keyboard = new Scanner(System.in);
        ServerSocket serverSocket = new ServerSocket(8888)) {
      System.out.println("서버 실행!");
      
      while(true) {
        // 한 클라이언트와 대화가 끝나면 다음 클라이언트와 대화를 한다.
        try (Socket socket = serverSocket.accept();
             BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
             PrintWriter = new PrintWriter(socket.getOutputStream())) {

          System.out.println("클라이언트와 연결되었음!");
          String name = in.readLine();
          out.printf("%s님 안녕하세요!", name);
          out.flush();
        } catch(Exception e) {
          System.out.println("클라이언트와 통신 도중 오류 발생!");
        }
        System.out.println("클라이언트와의 연결을 끊었음.");
      }

    } catch (Exception e) {
      e.printStackTrace();
    }
    System.out.println("서버 종료!");
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Client0210 {
  public static void main(String[] args) {
    Scanner keyScan = new Scanner(System.in);
		while (true) {
      System.out.print("이름? ");
      String name = keyScan.nextLine();
      
      if (name.equalsIgnoreCase("quit"))
        break;
      
      // 요청할 때 마다 서버와 연결한다.
      try (Socket socket = new Socket("localhost", 8888);
          PrintWriter out = new PrintWriter(socket.getOutputStream());
          BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {

        System.out.println("서버와 연결되었음!");

        // 한 번 요청한다.
        out.println(name);
        out.flush();

        // 응답 받으면 서버와 연결을 끊는다.
        String str = in.readLine();
        System.out.println(str);

        System.out.println("서버와 연결 끊음!");

      } catch (Exception e) {
        e.printStackTrace();
      }
    }
    keyScan.close();
  }
}

stateful / stateless 한 계산기 만들기

stateful Calculator

stateful을 사용하면 연결되어 있는 동안 클라이언트 작업 결과를 계속 유지할 수 있다. 클라이언트가 계산기를 사용하겠다는 요청을 하면 서버가 계산을 수행하고, 결과를 result 변수에 저장한다. 그리고 클라이언트에 결과를 반환한다. 서버와 클라이언트는 계속 연결되어 있기 때문에 저장된 result 값을 계속 사용하여 계산을 할 수 있다.

KakaoTalk_20201002_011444031

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class CalcServer {
  public static void main(String[] args) throws Exception {
    System.out.println("서버 실행 중...");
    
    ServerSocket ss = new ServerSocket(8888);
    
    while (true) {
      Socket socket = ss.accept();
      try {
        processRequest(socket);
      } catch (Exception e) {
        System.out.println("클라이언트 요청 처리 중 오류 발생!");
        System.out.println("다음 클라이언트의 요청을 처리합니다.");
      }
    }
  }
  
  static void processRequest(Socket socket) throws Exception {
    try (Socket socket2 = socket;
        DataInputStream in = new DataInputStream(socket.getInputStream());
        PrintStream out = new PrintStream(socket.getOutputStream())) {
      
      // 작업 결과를 유지할 변수
      int result;
      loop: while(true) {
        String op = in.readUTF();
        int value = in.readInt();
        
        switch (op) {
          case "+":
            result += value;
            break;
          case "-":
            result -= value;
            break;
          case "*":
            result *= value;
            break;
          case "/":
            result /= value;
            break;
          case "quit":
            break loop;
        }
        
        out.printf("계산 결과: %d\n", result);
      }
      out.println("Goodbye!");
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class CalcClient {
  public static void main(String[] args) throws Exception {
    Scanner keyScan = new Scanner(System.in);
    
    Socket socket = new Socket("localhost", 8888);
    Scanner in = new Scanner(socket.getInputStream());
    DataOutputStream out = new DataOutputStream(socket.getOutputStream());
    
    while (true) {
      System.out.print("연산자? ");
      out.writeUTF(keyScan.nextLine());
      
      System.out.print("값1? ");
      out.writeInt(keyScan.nextLine());
      
      String str = in.nextLine();
      System.out.println(str);
      
      if (str.equals("Goodbye!"))
        break;
    }
    in.close();
    out.close();
    socket.close();
    keyScan.close();
  }
}

stateless Calculator

연결 상태가 유지되어 작업 결과를 그대로 사용할 수 있는 stateful한 계산기와 달리 stateless한 계산기는 계산을 한 후 바로 연결이 끊어진다. 그렇다면 stateless한 프로그램에서는 어떻게 클라이언트를 구분하고 작업결과를 유지할 수 있을까?

KakaoTalk_20201002_011444152

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
public class CalcServer {
  // 각 클라이언트의 작업 결과를 보관할 맵 객체
  // => Map<clientID, result>
  static Map<Long, Integer> resultMap = new HashMap<>();
  
  public static void main(String[] args) throws Exception {
    System.out.println("서버 실행 중...");
    
    ServerSocket ss = new ServerSocket(8888);
    
    while(true) {
      Socket socket = ss.accept();
      System.out.println("클라이언트 요청 처리!");
      try {
        processRequest(socket);
      } catch (Exception e) {
        System.out.println("클라이언트 요청 처리 중 오류 발생!");
        System.out.println("다음 클라이언트의 요청을 처리합니다.");
      }
    }
    //ss.close();
  }
  
  static void processRequest(Socket socket) throws Exception {
    try (Socket socket2 = socket;
        DataInputStream in = new DataInputStream(socket.getInputStream());
        DataOutputStream out = new DataOutputStream(socket.getOutputStream())) {
      
      //클라이언트를 구분하기 위한 아이디
      // => 0: 아직 클라이언트 아이디가 없다는 의미
      // => x: 서버가 클라이언트에게 아이디를 부여했다는 의미
      long clientId = in.readLong();
      
      // 클라이언트를 위한 기존 값 꺼내기
      Integer obj = resultMap.get(clientId);
      int result = 0;
      
      if (obj != null) {
        System.out.println("%d 기존 고객 요청 처리!\n", clientId);
        result = obj;
      } else {
        clientId = System.currentTimeMillis();
        System.out.printf("%d 신규 고객 요청 처리!\n", clientId);
      }
      
      switch (op) {
        case "+":
          result += value;
          break;
        case "-":
          result -= value;
          break;
        case "*":
          result *= value;
          break;
        case "/":
          result /= value;
          break;
      }
      
      // 클라이언트로 응답할 때 항상 클라이언트 아이디와 결과를 출력한다.
      out.writeLong(clientId);
      out.writeInt(result);
      out.flush();
      
      // 계산 결과를 다시 resultMap에 보관한다.
      resultMap.put(clientId, result);
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class CalcClient {
  public static void main(String[] args) {
    Scanner keyScan = new Scanner(System.in);
    
    //서버에서 이 클라이언트를 구분할 때 사용하는 번호이다.
    // => 0번으로 서버에 요청하면, 서버가 신규 번호를 발급해 줄 것이다.
    long clientId = 0;
    
    while (true) {
      System.out.print("연산자? ");
      String op = keyScan.nextLine();
      
      System.out.print("값? ");
      int value = Integer.parseInt(keyScan.nextLine());
      
      try (Socket socket = new Socket("localhost", 8888);
          DataOutputStream out = new DataOutputStream(socket.getOutputStream());
           DataInputStream in = new DataInputStream(socket.getInputStream())) {
        // => 서버에 클라이언트 아이디를 보낸다.
        out.writLong(clientId);
        
        // => 서버에 연산자와 값을 보낸다.
        out.writeUTF(op);
        out.writeItn(value);
        out.flush();
        
        // => 서버에서 보낸 클라이언트 아이디를 읽는다.
        clientId = in.readLong();
        
        // => 서버에서 보낸 결과를 읽는다.
        int result = in.readInt();
        System.out.printf("계산 결과: %d\n", result);
        
      } catch (Exception e) {
        System.out.println("서버와 통신 중 오류 발생!");
      }
      System.out.print("계속하시겠습니까? (Y/n)");
      if (keyScan.nextLine().equlsIgnoreCase("n"))
        break;
    }
    keyScan.close();
  }
}
This post is licensed under CC BY 4.0 by the author.

학원 #49일차: 네트워크 기초(IP, 랜카드, port fowarding, 패킷, DNS, ISP), Socket과 byte stream

코어 자바스크립트 #2.8: 기본 연산자와 수학

Loading comments from Disqus ...