Dart Programmer 되기 [29]

< HTTP 서버개발 – HTTP Client Development >

darttutorial-28-01.dart 프로그램까지 우리는 HTTP Server를 만들고, 이에 접속하는 방법으로 웹브라우저를 사용했습니다. Dart 언어로 HTTP Server를 만들었다면, 당연히 HTTP Client도 만들수 있게다는 생각이 들겠지요. 본 글에서는 Dart 언어를 통해서 darttutorial-28-01.dart에 접속하여 HTTP GET, HTTP GET RESTful, HTTP POST를 수행하는 프로그램을 만들고자 합니다.

Simple HTTP Client

상당부분이 HTTP Server를 만들때 사용한 사항과 겹치거나 유사합니다. 따라서 Dart로 만든 HTTP Client의 소스 코드를 아래의 darttutorial-29-01.dart으로 첨부하였으니 먼저 시작부터 끝까지 읽어 보기 바랍니다.

// darttutorial-29-01.dart

import "dart:io";
import "dart:convert";

Future main() async {
  // #1 Create HttpRequest & HttpResponse objects 
  HttpClientRequest httpRequest;
  HttpClientResponse httpResponse;

  // #2 Create HTTP server network information related objects
  var serverIP = InternetAddress.loopbackIPv4.host;
  var serverPort = 4049;
  var serverPath;

  // #3 Get request transfer example
  stdout.writeln('${DateTime.now()}: Send GET');
  serverPath = "";
  httpRequest = await HttpClient().get (serverIP, serverPort, serverPath);
  httpResponse = await httpRequest.close();
  await utf8.decoder.bind(httpResponse).forEach(print);

  // #4 Get request transfer with RESTful example
  stdout.writeln('${DateTime.now()}: Send GET RESTful');
  serverPath = '?q=4&p=5.0&r="Dart"';
  httpRequest = await HttpClient().get (serverIP, serverPort, serverPath);
  httpResponse = await httpRequest.close();
  await utf8.decoder.bind(httpResponse).forEach(print);

  // #5 POST request transfer with JSON data example
  stdout.writeln('${DateTime.now()}: Send POST JSON');

  Map jsonContent  ={
    'C++' : 'Bjarne Stroustrup',
    'Dart' : 'Lars Bak and Kasper Lund',
    'Go' : 'Robert Griesemer, Rob Pike, Ken Thompson',
    'JavaScript' : 'Brendan Eich'
  };

  serverPath = "bin/example.txt";
  httpRequest = await HttpClient().post (serverIP, serverPort, serverPath)
  ..headers.contentType = ContentType.json
  ..write(jsonEncode(jsonContent));
  httpResponse = await httpRequest.close();
  await utf8.decoder.bind(httpResponse).forEach(print);
}

이제 Dart로 만든 HTTP Client를 첫줄부터 한줄한줄 세세하게 설명을 하고자 합니다. 이를 위해서 각각의 줄에 줄번호를 매긴 다음의 그림을 사용합니다. 내용을 앞서의 darttutorial-29-01.dart와 동일합니다.

[그림] darttutorial-29-01.dart 소스 프로그램

3~4번 줄을 보면 자주 사용했던 dart:io와 dart:convert를 사용합니다. dart:io를 통해서 서버와 동일하게 클라이언트에서의 HTTP 처리를 합니다.

8~9번 줄은 HTTP Client에서 HTTP Request와 HTTP Response를 처리하기 위한 객체를 각각 HttpClientRequest와 HttpClientResponse 클래스들로부터 만드는 것을 볼 수 있습니다.

12~13번 줄은 HTTP 서버의 네트워크 주소와 포트번호를 저장하기 위함이며, 서버 개발에서도 동일한 코드가 있었습니다. 14번 줄은 GET을 통해서 화일(혹은 경로명과 화일)이나 RESTful를 통해서 클라이언트에서 서버로 전달하는 정보를 저장하는 용도로 만들었습니다.

#3의 부분이 앞서 가장 간단한 형태의 Simple “Hello World!” HTTP Server의 작업을 요청하기 위한 용도입니다. 즉, 단순하게 GET 메시지만 만들어서 서버에 전달했습니다. 20번 줄에서, 서버에서 본 경우와 동일하게, HttpRequest 객체를 close()하면, 해당 객체가 Stream 객체로서 메시지를 만들어서 전송하는 효과가 나타납니다. 이의 결과를 비동기식으로 기다렸다가 httpResponse 객체에 담는 것을 볼 수 있습니다. 이는 서버에서는 없었던 것으로, HTTP Client가 서버로부터의 답변을 받아 저장한 것입니다. 마지막 21번 줄에서는 utf8.decoder.bind(httpResponse) 처럼 하여, 서버에서와 같이 서버로부터 온 답변을 모으고 UTF-8로 변환합니다. 그리고 이에 대한 결과를 forEach(print);를 통해서 화면에 출력합니다.

#4도 #3과 모두 동일하며, 유일하게 25번 줄에서 경로의 값을 RESTful의 형태로 저장할 변수와 값들로 만들어서 전달합니다. 이후 서버의 동작을 보면, RESTful 정보가 제대로 분리되어 인식되는 것을 볼 수 있습니다.

#5는 JSON 타입으로 Map 정보를 보내는 예제입니다. 33~38번 줄을 통해서 Map으로 4가지 프로그래밍 언어와 이들의 저자를 Map으로 만들었습니다. 41번 줄에서 전송할 메시지가 post 임으로 바뀌었습니다. 그리고 주의 깊게 봐야할 부분은 42/43번 줄 입니다. 42번 줄은 전달하는 데이타가 JSON 타입임을 명시하였습니다. 그리고 43번 줄에서 Map 정보를 JSON 코딩 타입으로 변환후, 전송할 메시지에 저장해 달라고 요청했습니다.

HTTP Client & Server Execution

darttutorial-28-01.dart 서버를 수행한 후, darttutorial-29-01.dart 프로그램을 수행하면, 서버에서의 출력을 다음과 같이 볼 수 있습니다. 웹브라우저를 통해서 수행한 것과 같은 결과가 나타납니다.

2020-02-14 17:29:00.679269: GET {}
2020-02-14 17:29:00.716868: GET {q: 4, p: 5.0, r: "Dart"}
2020-02-14 17:29:00.735167: POST /bin/example.txt

darttutorial-29-01.dart 프로그램의 수행 결과가 아래와 같이 있습니다. 3가지 케이스에 대한 수행 결과가 하나 하나 실행되면서 화면에 출력되는 모습을 볼 수 있습니다.

2020-02-14 17:29:00.590662: Send GET
2020-02-14 17:29:00.679541: Hello World!
2020-02-14 17:29:00.713488: Send GET RESTful
2020-02-14 17:29:00.717095: Hello World!
2020-02-14 17:29:00.721787: Send POST JSON
2020-02-14 17:29:00.737065: Wrote data for {C++: Bjarne Stroustrup, Dart: Lars Bak and Kasper Lund, Go: Robert Griesemer, Rob Pike, Ken Thompson, JavaScript: Brendan Eich}.

마무리

간단하게 나마 HTTP Client를 직접 Dart 언어로 만들어 보았습니다. 대부분의 함수와 기능이 서버의 경우와 비슷한 것을 볼 수 있습니다. 물론 dart:io 라이브러리를 동일하게 사용한 것도 볼수 있습니다. 설명한 내용은 향후 스마트폰과 같은 모바일 기기에서 동작하는 프로그램을 작성할 때 다시 등장할 내용이니 참조하기 바랍니다.

Creative Commons License (CC BY-NC-ND)

Dart Programmer 되기 [28]

< HTTP 서버개발 – GET & POST in HTTP Server >

상세한 설명을 하기전에 간단한 작업을 해 봅니다. 먼저, 앞서에서 작성한 darttutorial-27-02.dart를 다시 불러 옵니다. 그리고 handleRequest() 함수안의 case ‘GET’: 구문이 stdout.writeln(“${DateTime.now()}: GET ${req.uri.path}”);를 stdout.writeln(“${DateTime.now()}: GET ${req.uri}”);로 변경합니다. 즉 URI의 path 정보만 보던 것을 URI 전체를 출력하도록 바꾼 겁니다. 그리고 실행합니다. 다음으로는 웹브라우저를 실행한 후, 주소창에 http://localhost:4040/?q=4 라고 타이핑하곤 엔터를 타이핑 합니다. 그러면 서버쪽 화면에 “2020-02-14 11:34:06.907564: GET /?q=4″와 같이 출력이 됩니다. 이런 식으로 q라는 변수가 4라는 값을 갖도록 하라는 정보를 HTTP Client에서 HTTP Server로 전달한 것입니다. Get 명령을 통해서 Client가 Server로 정보를 보낼수 있는 것을 확인한 셈입니다. 이와 같은 방식에 대해서 오늘의 글에서 좀 더 알아가 보도록 하겠습니다.

RESTful GET Request

REST는 REpresentational State Transfer의 약어로써, 웹서비스에서 HTTP Client가 HTTP Server로 (마치 함수 호출시, 입력 파라메타를 전달하듯이) 기능 동작에 필요한 파라메타(들)을 전달하는 방법입니다. 주고 받는 변수와 이의 값을, 영어 이름 그대로, 표현해주는 방식이며, 어떤 표준이 존재하는 것은 아닙니다.

darttutorial-27-02.dart 프로그램의 앞서 수정한 부분을 한번 더 수정해 봅니다. 앞서 수정했던 구문을 stdout.writeln(“${DateTime.now()}: GET ${req.uri.queryParameters[‘q’]}”);으로 변경합니다. 의미는 URI 정보가 map 형태로 관리하는 parameter 정보 중 ‘q’를 key 값으로 하는 경우의 value를 도출한다는 의미입니다. 즉, 사용자가 보낸 REST 타입의 값들이 map 형태로 저장이 되는데, 이중 ‘q’에 해당하는 값만 추출해서 출력한다는 의미입니다. 다시 서버를 수행하고, Client는 앞서와 동일하게 http://localhost:4040/?q=4로 동작해 봅니다. 그러면, 서버의 출력으로 “GET 4″가 출력되는 것을 볼 수 있습니다. 이렇게 서버에서는 client가 보낸 일종의 변수들의 값을 추출함으로서, 동적으로 client가 요청한 동작과 파라메타에 따른 동적이 가능합니다.

만약 HTTP Client가 복수의 변수 값을 서버에 전달하고 싶다면 어떻게 할까요? 어렵지 않습니다. http://localhost:4040/?q=4&p=5.0&r=”Dart” 처럼 하면, q/p/r 변수에 각각 4/5.0/”Dart”를 값으로 해서 서버에 전달한 것입니다. 이를 제대로 보기 위해서, 이번에는 서버의 Get을 처리하는 위의 코드를 stdout.writeln(“${DateTime.now()}: GET ${req.uri.queryParameters}”);으로 변경합니다. 그러면, 2020-02-14 12:16:48.918590: GET {q: 4, p: 5.0, r: “Dart”} 처럼 Map 타입의 값이 출력 됩니다. 이렇게 HTTP Client와 서버는 REST 방식을 사용해서 간단하게 복수 파라메타의 정보를 전달할 수 있고, 이를 통한 동적인 동작이 가능합니다.

HTTP Classes

HTTP 관련 Class들에 대해서 부언 설명을 할 시점 입니다. 먼저 HttpRequest는 계속 등장하고 있지만, Client가 전달한 요청에 대한 정보를 저장합니다. method Property는 HTTP의 표준 명령을 의미하여, GET/PUT/POST 등의 정보를 갖습니다. uri Property는 Uri 객체를 저장하는데, scheme, host, port, querystring과 같이 HTTP Request 메시지안에 포함된 정보들을 포함하고 있습니다. response Property는 HttpResponse 객체를 저장하여, HttpRequest에 대한 응답 정보를 저장하고 있습니다. 마지막으로 headers Property는 HttpHeaders 객체를 저장하여, HTTP Request의 헤더 정보를 포함하는데, ContentType, content length, date 등의 정보를 저장하고 있습니다. 이미 우리는 darttutorial-27-02.dart 에서 req.method 문법을 통해서 “GET”과 매칭이 되는지를 확인 했으며, 다음과 같은 코드를 통해서, HTTP Request에 대한 응답의 코드와 Body 내용을 채우는 것을 보았습니다. 그리고 이번 글의 req.uri.queryParameters 문법으로 HTTP Request의 URI 내의 Map 정보를 접근하는 것도 확인 했습니다.

res
  ..statusCode = HttpStatus.methodNotAllowed
  ..write('${DateTime.now()}: Unsupported request: ${req.method}.');

HTTP Request에 대한 응답으로 만들어지는 HTTP Response 정보는 HttpResponse 객체로 만들어 집니다. 가장 기초적으로 함수의 리턴 값이 있듯이, HTTP Request에 대한 성공/실패 여부를 알리는 정보가 있으며, 이는 HttpResponse의 statusCode Property에 저장합니다. 가장 대표적인 두 값은 성공적으로 Request가 처리되었음을 의미하는 HttpStatus.ok이고, 서버가 처리할 수 없다는 의미인 (그리고 darttutorial-28-01.dart에서 등장했었던) HttpStatus.methodNotAllowed 입니다. HttpResponse안에는 그외에 유용한 property들이 있습니다. contentLength Property는 HTTP Response의 크기를 의미합니다. cookies Property는 HTTP Response를 받은 HTTP Client가 저장해야 하는 Cookies의 리스트 정보를 포함합니다. encodingProperty는 JSON이나 UTF-6과 같은 문자열을 저장할 때 사용하며, headers Property는 response의 헤더를 HttpHeaders 객체로 저장합니다.

HTTP POST Request Support in HTTP Server

앞서에서 URI에 RESTful의 형태로 함수를 호출하는 것처럼, 복수의 파라메타에 대한 값을 전달하는 방법을 보았습니다. 더 많은 정보를 전달하고자 한다면 HTTP POST Request를 사용할 수 있습니다. 예를들어 HTTP Client로부터 Map 타입의 정보를 HTTP Server가 수신하여, 이들을 화일에 저장하는 예제를 생각할 수 있습니다.

이럴때 언어와 독립적으로 정보를 저장하는 대표적인 형태가 JSON(JavaScript Object Notation) 입니다. 단어는 생소하지만, Dart 언어의 Map과 거의 같은 형태라고 보면 됩니다. 아래는 위키페디아에서 한 사람의 정보를 저장하는 JSON의 예제입니다 [출처].

 {
    "이름": "홍길동",
    "나이": 25,
    "성별": "여",
    "주소": "서울특별시 양천구 목동",
    "특기": ["농구", "도술"],
    "가족관계": {"#": 2, "아버지": "홍판서", "어머니": "춘섬"},
    "회사": "경기 수원시 팔달구 우만동"
 }

위와 같은 JSON 타입의 데이타를 HTTP 프로토콜로 전송할 때, ‘application/json’ 라고 지칭합니다. JSON 타입의 데이타를 HTTP Client에서 받아서 화일에 저장하는 기능을 darttutorial-27-02.dart 프로그램에 추가하는 함수 handlePostRequest()를 다음과 같이 만들 수 있습니다.

// Handler for HTTP POST Request.
Future handlePostRequest(HttpRequest req) async {
  // #1 Retrieve an associated HttpResponse object in HttpRequst object.
  HttpResponse res = req.response;

  // #2 Do something : Example - Write file based on the reseived JSON.
  if (req.headers.contentType?.mimeType == 'application/json') {
    try {
      // #2.1 utf8.decoder.bind(req) : Retrieve multiple chunks within a same HTTP Request.
      // #2.2 .join() : Puts the chunks together.  
      String jsonContent = await utf8.decoder.bind(req).join();

      // #2.3 Retrieve file name from HTTP POST Request
      var jsonFileName = req.uri.pathSegments.last; 

      // #2.4 Save the received JSON content into a file in a current working directory
      await File(jsonFileName).writeAsString(jsonContent, mode: FileMode.write);

      // #2.5 Decode the received JSON content and concert into MAP format
      var jsonData = jsonDecode(jsonContent) as Map;

      // #2.6 Make a response for success case
      req.response
        ..statusCode = HttpStatus.ok
        ..write('${DateTime.now()}: Wrote data for ${jsonData}.');
    } catch (e) {
      // #2.7 Make a response for failed case
      res
        ..statusCode = HttpStatus.internalServerError
        ..write('${DateTime.now()}: Exception during file I/O: $e.');
    }
  } else {
    // 2.8 Make a response for not supported POST Request (not a JSON format)
    res
    ..statusCode = HttpStatus.methodNotAllowed
    ..write('${DateTime.now()}: Unsupported POST type: ${req.headers.contentType?.mimeType}.');
  }

  // #3 Close the response and send it to the client.
  await res.close();
}

handlePostRequest() 함수에 대해서 자세히 알아 봅니다. 입력 파라메타는 다른 HTTP Request와 마찬가지로 HTTP Client로부터 받은 메세지가 포함된 HttpRequest 객체를 받습니다. 그리고 비동기 모드 동작을 위한 기능이 포함되었기에, async와 Future의 문법을 사용하고 있습니다. #1과 #3은 GET을 처리하는 함수와 동일 합니다. #2의 구문을 보면, req.headers.contentType?.mimeType == ‘application/json’ 구문을 통해서, 당장은 JSON 데이타 타입만 처리하겠다고 선언 했습니다. 따라서, 다른 타입으로 POST가 수신되면, #2.8에 의해서 “지원하지 않는 타입”이라는 메세지를 회신하게 됩니다. JSON 타입이라면, 후속작업을 하게 됩니다. #2.1으로 이 작업을 시작하는데, HTTP POST Request에 포함된 정보들을 UTF-8 형태로 변환하고, (통신상에서 같은 HTTP POST Request 메시지가 여러개의 통신 메시지로 나뉘어져서 수신될 수 있으니) 같은 HTTP Request에 속한 메시지를 하나로 묶어 주는 작업을 #2.2에서 join()으로 수행합니다. 이렇게 해서 jsonContent은 HTTP Client가 전달한 HTTP POST Request의 모든 정보를 하나의 문자열에 담습니다.

다음으로 POST를 통해서 전달된 Map 형식의 데이타를 저장할 화일이름을 HTTP Client가 전송했으며, 이를 받아야 합니다. 이를 위해서 익숙한 req.uri Property가 다시 등장했고, 이번에는 URI에서 마지막에 포함되어 있을 화일이름만 추출하는 작업을 #2.3에서 pathSegments.last Property를 통해서 확보합니다.

#2.4에서 드디어 화일에 저장하는 작업을 (단순하게 한줄이지만) 성공적으로 수행합니다. 저장할 화일 이름을 File 클래스의 Constructor에 전달하여, File 객체를 만들면서, JSON 형태로 받은 정보를 File 객체에 저장하도록 writeAsString()를 실행합니다. 이렇게 해서 JSON 타입으로 전달 받은 정보는 서버에 화일 형태로 저장됩니다.

#2.5는 혹시라도 있을 서버에서의 데이타 처리를 위하여, JSON 형태의 데이타를 아예 Dart 언어의 Map 형태로 바꾼후 jsonData 변수에 저장합니다. 이후 #2.6에서 성공적인 응답이라는 코드와 함께 저장한 내용을 다시 HTTP Client에게 HTTP Response 메시지로 전달하게 됩니다.

POST Request를 다루는 함수를 추가했으니, 앞서 만들었던 handleRequest()에서 handlePostRequest()를 호출할 수 있도록 추가해야 합니다. 아래는 case “POST”:를 통해서 HTTP POST Request가 수신되면, handlePostRequest()를 수행하도록 수정한 함수 입니다.

// Handler for HTTP Request.
Future handleRequest(HttpRequest req) async {
    // #1 Do something based on HTTP request types.
    switch (req.method) {
      // #1.1 GET Request.
      case 'GET':
        // Print log message and activate HTTP Get Request handler.
        stdout.writeln("${DateTime.now()}: GET ${req.uri}");
        await handleGetRequest(req);
        break;
      // #1.2 GET Request.
      case 'POST':
        // Print log message and activate HTTP Post Request handler.
        stdout.writeln("${DateTime.now()}: POST ${req.uri}");
        await handlePostRequest(req);
        break;
      // #1.3 Other Requests.
      default:
        stdout.writeln("${DateTime.now()}: ${req.method} not allowed");
        await handleNotAllowedRequest(req);
    }
}

자 이제 기존의 darttutorial-27-02.dart에 위에서 수정한 함수들을 모두 포함하도록 변경한 darttutorial-28-01.dart 프로그램을 만들었습니다. 아래의 소스 프로그램은 POST Reqeust를 수정과 함께, 다른 부분에 대해서 일부 개선한 형태입니다. 이렇게 해서, 우리의 HTTP Server는 GET과 POST를 처리하도록 향상 되었습니다.

// darttutorial-28-01.dart

import "dart:io";
import 'dart:async';
import 'dart:convert';

// Handler for HTTP GET Request.
Future handleGetRequest(HttpRequest req) async {
  // #1 Retrieve an associated HttpResponse object in HttpRequst object.
  HttpResponse res = req.response;

  // #2 Do something : Example - Write text body in the response.
  res
  ..statusCode = HttpStatus.ok
  ..write('${DateTime.now()}: Hello World!');

  // #3 Close the response and send it to the client.
  await res.close();
}

// Handler for HTTP POST Request.
Future handlePostRequest(HttpRequest req) async {
  // #1 Retrieve an associated HttpResponse object in HttpRequst object.
  HttpResponse res = req.response;

  // #2 Do something : Example - Write file based on the reseived JSON.
  if (req.headers.contentType?.mimeType == 'application/json') {
    try {
      // #2.1 utf8.decoder.bind(req) : Retrieve multiple chunks within a same HTTP Request.
      // #2.2 .join() : Puts the chunks together.  
      String jsonContent = await utf8.decoder.bind(req).join();

      // #2.3 Retrieve file name from HTTP POST Request
      var jsonFileName = req.uri.pathSegments.last; 

      // #2.4 Save the received JSON content into a file in a current working directory
      await File(jsonFileName).writeAsString(jsonContent, mode: FileMode.write);

      // #2.5 Decode the received JSON content and concert into MAP format
      var jsonData = jsonDecode(jsonContent) as Map;

      // #2.6 Make a response for success case
      req.response
        ..statusCode = HttpStatus.ok
        ..write('${DateTime.now()}: Wrote data for ${jsonData}.');
    } catch (e) {
      // #2.7 Make a response for failed case
      res
        ..statusCode = HttpStatus.internalServerError
        ..write('${DateTime.now()}: Exception during file I/O: $e.');
    }
  } else {
    // 2.8 Make a response for not supported POST Request (not a JSON format)
    res
    ..statusCode = HttpStatus.methodNotAllowed
    ..write('${DateTime.now()}: Unsupported POST type: ${req.headers.contentType?.mimeType}.');
  }

  // #3 Close the response and send it to the client.
  await res.close();
}

// Handler for not allowed HTTP Request.
Future handleNotAllowedRequest(HttpRequest req) async {
  // #1 Retrieve an associated HttpResponse object in HttpRequst object.
  HttpResponse res = req.response;

  // #2 Do something : Example - Write text body in the response.
  res
  ..statusCode = HttpStatus.methodNotAllowed
  ..write('${DateTime.now()}: Unsupported request: ${req.method}.');

  // #3 Close the response and send it to the client.
  await res.close();
}

// Handler for HTTP Request.
Future handleRequest(HttpRequest req) async {
    // #1 Do something based on HTTP request types.
    switch (req.method) {
      // #1.1 GET Request.
      case 'GET':
        // Print log message and activate HTTP Get Request handler.
        stdout.writeln("${DateTime.now()}: GET ${req.uri.queryParameters}");
        await handleGetRequest(req);
        break;
      // #1.2 GET Request.
      case 'POST':
        // Print log message and activate HTTP Post Request handler.
        stdout.writeln("${DateTime.now()}: POST ${req.uri}");
        await handlePostRequest(req);
        break;
      // #1.3 Other Requests.
      default:
        stdout.writeln("${DateTime.now()}: ${req.method} not allowed");
        await handleNotAllowedRequest(req);
    }
}

Future main() async {
  // #1 Specify HTTP Server address (localhost) and port.
  final HOST = InternetAddress.loopbackIPv4; // or "0.0.0.0" to allow access from other machines
  final PORT = 4049;

  // #2 Starts listening for HTTP requests on the address and port.
  var httpServer = await HttpServer.bind(HOST, PORT);
  stdout.writeln("${DateTime.now()}: HTTP Server running at ${HOST.address}:$PORT");

  // #3 Listening for HTTP requests and handle requests.
  await for (HttpRequest httpRequest in httpServer) {
    try {
      // #3.1 Activate a HTTP Request handler
      handleRequest(httpRequest);
    } catch(e) {
      // #3.2 Print message at exception handling case
      stdout.writeln('${DateTime.now()}: Exception in handleRequest: $e');
    }
  }
}

Secure Connection using HTTPS

HttpServer 클래스는 암호화된 보안 통신을 지원하는 HTTPS (Hyper Text Transfer Protocol with Secure Sockets Layer) 통신을 지원합니다. 이를 위해서 bindSecure() 메소드를 지원합니다. 아래의 예제를 보면 기존 bind() 메소드와의 차이점으로 serverConect에 보안 정보가 포함되어야 한다는 점 입니다.
Certificate Authority (CA)라고 불리우는 정보로서, 이는 서버를 운영하는 개발자가 직접 보안 정보를 획득해야 합니다. 이에 대한 자세한 사항은 What is SSL and What are certificates [참조]를 참조하기 바랍니다.

var server = await HttpServer.bindSecure(
    'localhost',
    4047,
    serverContext,
  );

Dart 공식 사이트에서 HTTPS를 지원하는 예제 프로그램이 다음과 같이 등록되어 있습니다. 보안과 관련한 정보를 담은 pem 화일을 개발자가 생성한 후, 이를 토대로 bindSecure()를 실행하는 예제이니 참조하기 바랍니다.

// Source: https://dart.dev/tutorials/server/httpserver#using-https

import 'dart:io';

String certificateChain = 'server_chain.pem';
String serverKey = 'server_key.pem';

Future main() async {
  var serverContext = SecurityContext(); /*1*/
  serverContext.useCertificateChain(certificateChain); /*2*/
  serverContext.usePrivateKey(serverKey, password: 'dartdart'); /*3*/

  var server = await HttpServer.bindSecure(
    'localhost',
    4047,
    serverContext, /*4*/
  );
  print('Listening on localhost:${server.port}');
  await for (HttpRequest request in server) {
    request.response.write('Hello, world!');
    await request.response.close();
  }
}

마무리

뭔가 이상합니다. POST Request로 JSON 형태의 자료를 받아서, 화일로 저장도 하고 Dart 언어의 Map 자료 형태로 변경도 해주는 서버 프로그램을 개발했지만, 이에 대한 실행 동작은 보이지 않았습니다. 이 프로그램을 수행하려면 HTTP Client가 HTTP POST Request 메세지를 HTTP Server에게 전달 할 수 있어야 합니다. 다음 글에서 darttutorial-28-01.dart 프로그램과 대화를 나눌 HTTP POST Client 프로그램을 만들어 보도록 하겠습니다.

Creative Commons License (CC BY-NC-ND)

Dart Programmer 되기 [27]

< HTTP 서버개발 – Basic HTTP Server & Frameworks >

HTTP(Hyper Transport Protocol)은 90년대 Tim Bernas Lee 경에 의해서 제안되고 만들어졌으며, 현재 우리가 웹 혹은 월드 와이드 웹 혹은 인터넷(이건 잘못된 이해 이지만)을 구현하는 아주 중요한 기술입니다. 여기서는 가장 기본적인 HTTP Ver 1.1에 기반하는 Client와 Server 프로그램을 Dart 언어를 사용하여 만드는 방법을 설명 합니다. 단, HTTP에 대한 이론적인 이해가 반드시 있어야 하는데, 이에 대해서는 이 글에서는 설명하지 않습니다. HTTP에 대한 이해는 통칭 ‘컴퓨터 네트워크’ 류의 제목을 갖는 전통적인 대학의 교과서 들에서도 찾아볼 수 있으며, 다음의 웹 사이트들을 통해서도 충분히 이해할 수 있으니, 미리 사전에 공부를 하고, 이 글을 이어가기 바랍니다. 이해해야 하는 단어들은 Web, IP, IP Address, TCP, Port Number, HTTP, HTML, CSS, Web Browser, Web Server, DNS 정도입니다. 깊이있는 이해보다는 단순하게 단어가 의미하는 뜻을 충분하게 이해할 수 있으면 됩니다. 추천하는 인터넷 상의 자료들은 다음과 같습니다.

  • Khan Academy – 컴퓨터과학 인터넷 입문 [참조, 튜토리얼]
  • Tutorials Point – HTTP Tutorial [참조, 튜토리얼]
  • W3C – HTTP (Hypertext Transfer Protocol) [참조, 표준문서]

참고로 이글은 Dart 공식 사이트의 “Write HTTP clients & servers” 게시물을 근간으로 해서 설명하고자 합니다 [참조]. HTTP 서버 개발을 이해가기 위해서는 반드시 앞서의 Dart 기초문법에 대한 이해가 필요합니다.

Basic Operations of HTTP Client & Server

HTTP Client와 Server에 대한 기초개념을 이해 했다면, HTTP Server는 프로그램 동작시 본인의 IP 주소 등의 정보와 Port 번호를 토대로 Client 들의 요청을 수신할 준비를 하는데, 이 과정을 bind 라고 합니다. Client 들이 이후 활성화되어 Server에 HTTP protocol을 통해서 여럿 요청을 하게 됩니다. 이를 정리하면 다음과 같습니다.

  • Server는 프로그램 시작후 Listen 상태를 유지함
  • Client가 Server로 TCP를 통해서 연결을 수행함
  • Client가 Server로 HTTP Request를 보내고, Server가 이를 수신함
  • Server는 Client의 HTTP Request 요청을 처리하면서, 다른 Client를 Listen함
  • Server는 HTTP Request를 처리한 후, HTTP Response를 회신함
  • Server는 HTTP Resposne를 회신한 Client와의 TCP 연결을 해제함

이후 예제 프로그램을 만들고 수행할때에도 위의 동작을 동일하게 진행할 것이므로 숙지하시기 바랍니다. 그리고 Dart 언어로 HTTP 기반의 Server 프로그램을 만들때에는 dart:io를 사용하여 기초부터 작성하는 방법과, HTTP 기반의 서버 패키지를 활용하는 방법이 있습니다 [참조]. 일단 dart:io를 사용하는 것은 CLI 기반 서비스의 개발시 사용하며, 향후 Web Browser나 스마트폰 기반의 Web App을 작성하는 경우는 dart:html을 사용하여야 합니다. 따라서 HTTP 서버개발 글에서는 dart:io를 주로 활용하여, HTTP Client와 Server를 개발하도록 할 겁니다. 하지만 Dart 언어에서 HttpRequest class가 dart:html에 포함되어 있기에, dart:html도 함께 사용합니다. Dart에서 HTTP를 구현할때 관련 Class에 대해서 이해하려면 다음의 사이트들을 참조하기 바랍니다.

  • HttpServer 클래스 [참조]
  • HttpRequest 클래스 [참조]
  • HttpResponse 클래스 [참조]
  • HttpServer 패키지 [참조]

Simple “Hello, World!” HTTP Server

가장 간단한 HTTP Server를 만들어 보겠습니다. Dart 공식 홈페이지에 있는 예제로써, MS Visual Code에서 다음의 darttutorial-27-01.dart ( hello_world_server.dart ) 프로그램을 입력하여 수행합니다. 그러면 “Listening on localhost:4040″라고 출력이 되는 것을 확인 할수 있습니다.

// darttutorial-27-01.dart ( hello_world_server.dart )

import 'dart:io';

Future main() async {
  var server = await HttpServer.bind(
    InternetAddress.loopbackIPv4,
    4040,
  );
  print('Listening on localhost:${server.port}');

  await for (HttpRequest request in server) {
    request.response.write('Hello, world!');
    await request.response.close();
  }
}

HTTP Client는 아직 별도로 개발하지 않습니다. 다만 웹브라우저를 실행 시킨후, 주소창에 localhost:4040 이라고 타이핑 한후, enter 키를 치면 됩니다. 이 의미는 지금 개발자가 작업하는 컴퓨터(localhost)에서 동작하는 프로그램인데, port 번호가 4040인 프로그램(darttutorial-27-01.dart)이라는 의미힙니다. 그러면 HTTP Client인 웹브라우저에 “Hello, world!”의 아주 짧은 출력이 나타나는 것을 볼 수 있습니다. 이렇게 최초의 HTTP 서버를 만들고 수행하는 것에 성공했습니다.

Advanced “Hello, World!” HTTP Server

darttutorial-27-01.dart 프로그램과 하는 일은 같지만, 프로그램의 구조를 바꾸도록 하겠습니다. 이는 앞으로 보다 의미있는 기능을 수행하기 위한 형태로 프로그램을 미리 준비해 놓는다고 생각하면 되겠습니다. 개선한 프로그램은 darttutorial-27-02.dart 프로그램으로 다음과 같습니다.

// darttutorial-27-02.dart

import "dart:io";
import 'dart:async';

// Handler for HTTP Get Request.
void handleGetRequest(HttpRequest req) {
  // #1 Retrieve an associated HttpResponse object in HttpRequst object.
  HttpResponse res = req.response;

  // #2 Do something : Example - Write text body in the response.
  res.write('${DateTime.now()}: Hello World!');

  // #3 Close the response and send it to the client.
  res.close();
}

// Handler for not allowed HTTP Request.
void handleNotAllowedRequest(HttpRequest req) {
  // #1 Retrieve an associated HttpResponse object in HttpRequst object.
  HttpResponse res = req.response;

  // #2 Do something : Example - Write text body in the response.
  res
  ..statusCode = HttpStatus.methodNotAllowed
  ..write('${DateTime.now()}: Unsupported request: ${req.method}.');

  // #3 Close the response and send it to the client.
  res.close();
}

// Handler for HTTP Request.
Future handleRequest(HttpRequest req) async {
    // #1 Do something based on HTTP request types.
    switch (req.method) {
      // #2 GET Request.
      case 'GET':
        // Print log message and activate HTTP Get Request handler.
        stdout.writeln("${DateTime.now()}: GET ${req.uri.path}");
        await handleGetRequest(req);
        break;
      // #3 Other Requests.
      default:
        stdout.writeln("${DateTime.now()}: ${req.method} not allowed");
        await handleNotAllowedRequest(req);
    }
}

Future main() async {
  // #1 Specify HTTP Server address (localhost) and port.
  final HOST = InternetAddress.loopbackIPv4;
  final PORT = 4040;

  // #2 Starts listening for HTTP requests on the address and port.
  var httpServer = await HttpServer.bind(HOST, PORT);
  stdout.writeln("${DateTime.now()}: HTTP Server running at ${HOST.address}:$PORT");

  // #3 Listening for HTTP requests and handle requests.
  await for (HttpRequest httpRequest in httpServer) {
    try {
      // #3.1 Activate a HTTP Request handler
      handleRequest(httpRequest);
    } catch(e) {
      // #3.2 Print message at exception handling case
      stdout.writeln('${DateTime.now()}: Exception in handleRequest: $e');
    }
  }
}

실행 방법은 앞서의 darttutorial-27-01.dart 프로그램과 동일합니다. 서버에서의 출력은 다음과 같습니다. 즉, 서버 프로그램이 시작하면, 본인의 네트워크 주소와 포트 번호를 출력합니다. 그리고 웹브라우저를 통해서 Client가 접속하면, Client가 요청한 정보를 서버 프로그램의 출력으로 나타내도록 하였습니다.

2020-02-13 16:25:43.693213: HTTP Server running at 127.0.0.1:4040
2020-02-13 16:25:48.618858: GET /

darttutorial-27-02.dart 프로그램에 대한 자세한 설명은 다음과 같습니다.

handleGetRequest() 함수는 HTTP Client가 HTTP Get Request를 HTTP Server에게 전달하였을때, Server가 이를 처리하고자 호출하는 함수입니다. 입력 파라메타로 HTTP Server가 수신한 HTTP Request 정보를 포함합니다. (HttpRequest 클래스는 참조로 Stream 타입입니다). #1에서는 수신한 정보에서 다시 Client에게 전달할 HttpResponse를 추출하는 작업을 합니다. 이를 res 변수에서 접근 하도록 합니다. #2에서는 res 변수의 HTTP Response 메시지의 Body에 현재의 시간 정보와 “Hello World!” 문자열을 저장합니다. #3에서 HttpResponse 객체인 res를 close()하는데, 이는 이 함수가 전달 받은 HttpRequest에 대한 HttpResponse 생성을 마쳤고, HTTP Client에게 전송하는 효과를 나타냅니다. 이를 통해서 HTTP Client인 웹브라우저에는 “2020-02-13 16:25:59.147779: Hello World!”와 같이 시간과 문자열이 나타나게 됩니다. 앞으로 HTTP 서버가 HTTP Client로부터 HTTP Get Request를 받으면, 이 함수를 개선해서 처리하도록 할 것 입니다.

handleNotAllowedRequest() 함수는 HTTP Client가 HTTP Get Request외의 요청을 HTTP Server에게 전달하였을때, Server가 이를 처리하고자 호출하는 함수입니다. handleGetRequest() 함수와 마찬가지로 입력 파라메타로 HTTP Server가 수신한 HTTP Request 정보를 포함합니다. 지금은 HTTP 서버에서 Get에만 반응하도록 하였기에, Get 이외의 모든 처리는 여기서 담당하지만, 향후 Get외의 다양한 기능에 대해서 프로그램을 수정해 가면서, 이 함수는 서버에서 처리하지 않는 기능들에 대한 제한적인 처리만 담당하게 될될 것 입니다. 내부 동작은 handleGetRequest() 함수과 거의 같으며, 추가적으로 HTTP Response 상태 정보를 포함하도록 하였습니다.

handleRequest() 함수는 async 함수로 선언하였으며, 앞으로 설명할 main()에서 HTTP Request를 수신하면, 이를 전담하는 함수로 동작합니다. 내부는 복잡하지 않으며, HTTP Request의 타입에 맞춰서, 위에서 설명한 적합한 함수들을 호출하기만 합니다.

main() 함수는 일단 서버의 네트워크 정보를 #1에서 별도의 정의로 분리했습니다. 그리고 #2에서 앞서 Simple 버전과 같이 화면에 본인의 네트워크 정보와 함께 최초 동작 시간을 나타내도록 했습니다. #3은 await for 구문을 사용하여 HTTP Server가 수신하는 HTTP Request들 및 기타 필요한 작업을 반복적으로 하도록 하였으며, 현재는 HTTP Request 메시지를 handleRequest() 함수에 전달하는 역할만 합니다. 혹시 모를 오류에 대응해서 try – catch 구문으로 에러에 대한 대응을 하도록 한 부분도 볼 수 있습니다.

마무리

HTTP Client와 Server를 Dart 언어로 개발하는 첫단추를 끼웠습니다. HTTP 자체에 대한 이론적 공부는 별도로 해야 하며, 본 글에서는 가장 간단한 Simple “Hello World!” HTTP Server를 통해서, Dart 언어를 통한 HTTP 서버를 개발하기 위한 가장 기초적인 코드들을 살펴보았습니다. 그리고 점차 지능적인 작업을 하기 위한 형태로 변경한 Advanced “Hello World!” HTTP Server를 만들었습니다. 하는 일은 둘이 별반 차이가 없지만, 앞으로 새로운 사항을 공부하고 반영할때, 개선한 코드가 보다 깔금하게 진화하는 모습을 볼 수 있을 겁니다.

우리는 Dart 언에에서 제공하는 코어 라이브러리 만으로 HTTP 서버를 개발하는 방식을 설명 하였습니다. 하지만 제대로된 HTTP 서버에서는 기본적으로 제공해야 하는 기술이 매우 많습니다. 따라서 HTTP 서버를 실제로 운영하는 데에 필요한 기술들을 미리 구현한 HTTP 서버 Framework들이 존재하며, Dart 언어의 경우도 최근 하나 둘 HTTP 서버 Framework 들이 등장하고 있습니다. Dart 언어를 사용하여 Web 서버를 구현하고 운영하고자 하는 경우에 코어 라이브러리 만으로 부족한 경우는 아래의 프레임워크 들을 살펴보기 바랍니다.

Creative Commons License (CC BY-NC-ND)