Posts BufferedReader의 readLine() 메서드 이후의 코드로 진행이 안되는 현상
Post
Cancel

BufferedReader의 readLine() 메서드 이후의 코드로 진행이 안되는 현상

상황


  • ‘자바 웹 프로그래밍 NextStep’ 3장의 과제 중 하나인 웹 서버를 구현하기 위해 HTTP 요청이 서버에 어떤식으로 들어오는지를 보고싶었다.
  • 따라서, 단순 확인을 위해 다음과 같은 코드를 작성하여 실행시키고 브라우저에서 localhost:8080 으로 요청을 보냈다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    public class TestServer {
      public void start() throws IOException {
          ServerSocket serverSocket = new ServerSocket(8080);
          Socket connection;
    
          while((connection = serverSocket.accept()) !=null) {
              System.out.println(String.format("[connection info]%nIpAddr : %s, Port : %s%n", connection.getInetAddress(), connection.getPort()));
              BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream()));
    
              String line;
              while((line=br.readLine())!=null) { // 여기서 멈춘다
                  System.out.println(line);
              }
    
              System.out.println("END!");
          }
      }
    
      public static void main(String[] args) throws IOException {
         TestServer server = new TestServer();
         server.start();
      }
    }
    
  • 아래와 같이 요청이 들어오는 것을 확인할 수 있었고, ‘Http Method(GET, POST, …)가 뭔지 확인하려면 요청의 첫 번째 라인을 확인하면 되겠구나’ 등의 판단을 할 수 있었다. image

  • 하지만, 문제는 코드의 15번째 라인 System.out.println("END!"); 에 대한 출력이 콘솔에 찍히지 않았다는 것이었다. 디버거로 확인해보니 무한루프를 도는 것도 아니고 위 코드의 11번째 라인에서 대기 상태가 되는 것을 확인할 수 있었다.

해결 과정


  • 스택오버플로우에 있는 글 을 통해 BufferedReader의 readLine() 메서드는 라인이 종료되었다고 판단되지 않으면 값을 리턴하지 않는다는 것을 알게되었다.

  • 그렇다면 라인이 종료되었다는 것을 판단하는 기준은 뭘까??
    • 자바 공식문서 에 나와있는 readLine() 메서드에 대한 설명을 보면 끝에 다음 문자(‘\n’, ‘\r’, ‘\r\n’) 중 하나가 있어야 하나의 라인으로 인식한다고 한다.

      Reads a line of text. A line is considered to be terminated by any one of a line feed (‘\n’),
      a carriage return (‘\r’), or a carriage return followed immediately by a linefeed.

  • 또한, BufferedReader의 readLine() 메서드를 직접 들여다보면 다음과 같은 로직이 있는 것을 확인할 수 있다. eol은 ‘end of line’이지 않을까 싶다.
    1
    2
    3
    4
    5
    6
    7
    8
    
    charLoop:
                  for (i = nextChar; i < nChars; i++) {
                      c = cb[i];
                      if ((c == '\n') || (c == '\r')) {
                          eol = true;
                          break charLoop;
                      }
                  }
    
  • 그렇다면, HTTP 요청의 마지막 라인이 어떻길래 아직 라인이 종료되었다고 판단하지 않는걸까 ?
    • 스택오버플로우에 있는 글
    • RFC 문서 34p
    • HTTP 요청 형태 (HTTP 요청 헤더의 마지막 라인은 공백이었고 이로 인해 readLine() 메서드가 라인이 끝나지 않았다고 판단한 것)
    • CRLF : Carriage Return(커서의 위치를 맨 앞으로 이동) + Line Feed(커서를 한 칸 아래로 이동)
1
2
3
4
5
6
Request-Line
*(( general-header
 | request-header
 | entity-header ) CRLF)
CRLF
[ message-body ]
  • 결과적으로 line이 공백이면 "공백"이라는 문자열을 출력해봄으로써 실제 HTTP 요청이 위와 같이 들어온다는 것을 알 수 있었고, 공백인 경우 break를 통해 while문 내에서 계속 대기상태에 머물러있지 않게 할 수 있었다. image

※ 참고. HTTP Response 포맷

1
2
3
4
5
6
Status-Line
*(( general-header
 | response-header
 | entity-header ) CRLF)
CRLF
[ message-body ]

요약


현상

  • HTTP 요청을 받아서 BufferedReader의 readLine() 메서드로 읽어 올 때 특정 라인에 도달하면 대기 상태에 머문다.

원인

  • readLine() 메서드 내에는 ‘하나의 라인’이라고 판단하는 기준(라인 마지막에 ‘\n’, ‘\r’, ‘\r\n’)이 있는데,
    HTTP 요청의 마지막 라인은 공백이었기 때문에 아직 한 라인이 끝나지 않았다고 판단하여 계속 대기하고 있었다.

배운 것


  • HTTP 요청 형태
  • BufferedReader의 readLine() 메서드가 라인을 인식하는 방법

실제 코드 적용


  • 간단한 웹 서버 구현 레파지토리
  • 오늘 내용 관련 코드
    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
    
      private static HttpRequest processGetRequest(String requestUrl, BufferedReader bufferedReader) throws IOException {
          Map<String, String> cookies = null;
    
          for (String line = bufferedReader.readLine(); (line != null && !line.isEmpty()); line = bufferedReader.readLine()) {
              if (line.contains("Cookie")) {
                  String[] info = line.split(":");
                  cookies = parseCookies(info[1]);
                  break;
              }
          }
    
          if (!requestUrl.contains("?")) {
              return new HttpRequest(HttpMethod.GET, requestUrl, cookies);
          }
    
          String[] info = requestUrl.split("\\?");
          Map<String, String> params = parseQueryString(info[1]);
    
          return new HttpRequest(HttpMethod.GET, info[0], params, cookies);
      }
    
      private static HttpRequest processPostRequest(String requestUrl, BufferedReader bufferedReader) throws IOException {
          int contentLen = 0;
          String contentType = "";
          Map<String, String> params = null;
          Map<String, String> cookies = null;
    
          for (String line = bufferedReader.readLine(); (line != null && !line.isEmpty()); line = bufferedReader.readLine()) {
              if (line.contains("Content-Length")) {
                  String[] info = line.split(":");
                  contentLen = Integer.parseInt(info[1].trim());
              } else if (line.contains("Content-Type")) {
                  String[] info = line.split(":");
                  contentType = info[1].trim(); // ex) application/x-www-form-urlencoded
              } else if (line.contains("Cookie")) {
                  String[] info = line.split(":");
                  cookies = parseCookies(info[1]);
              }
          }
    
          if (contentLen > 0) {
              char[] body = new char[contentLen];
              bufferedReader.read(body);
    
              if (contentType.equals("application/x-www-form-urlencoded")) {
                  String queryString = new String(body);
                  params = parseQueryString(queryString);
              } else if (contentType.equals("application/json")) {
                  // TODO
              }
          }
    
          return new HttpRequest(HttpMethod.POST, requestUrl, params, cookies);
      }
    

더 공부해야할 부분


  • JAVA I/O
  • HTTP 응답 분할(HTTP Response Splitting, CRLF) 취약점

참고자료


This post is licensed under CC BY 4.0 by the author.

화면 리디렉션시 쿠키 송신이 안되는 현상

Multi Page Application vs Single Page Application