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)

댓글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다