Flutter/Dart - 채팅 앱 만들기(1) - 다트로 웹 소켓 서버/클라이언트 만들기 (WebSocket)

반응형

개요

기존에 Flask를 활용하여 실시간 채팅 기능을 설명 및 구현한 포스팅을 작성한 적이 있다.

이 기능을 이용하여 사내에서 간단히 직원들끼리 사용할 채팅 프로그램을 만들었었는데,

이번에는 조금 더 욕심을 부려서 다양한 플랫폼에서도 사용할 수 있도록

Flutter로도 다시 재구현해 보고자 이 포스팅을 작성하게 되었다.

관련 내용들을 찾아보니 대부분이 NodeJs나 각각의 WebSocket서버를 구축하고 사용하는 것 같더라.

Dart언어를 사용하는 Flutter인 만큼, 이번 포스팅에서는 Dart를 이용하여 웹 소켓 서버를 구축해보고자 한다.

또한 웹 소켓 서버 구축에 대한 각각의 메서드 설명을 같이 포함하고 있다.

정말 급하게 기능 구현 코드만 필요하다면 서운하겠지만.. 각 챕터의 가장 아래 예시 코드만 봐도 된다 :-(

 

[ 시리즈 포스팅 내용 보기 ]

 

포스팅에서 설명하는 프로젝트는 깃허브에서 다운로드할 수 있다.

 

GitHub - luvris2/flutter_chatting_app

Contribute to luvris2/flutter_chatting_app development by creating an account on GitHub.

github.com


서버 (Server)

절차

웹 소켓 서버를 구성하려면 dart.io 라이브러리가 추가로 필요하다.

dart.io 라이브러리는 다트 언어 설치 시 기본으로 제공되는 라이브러리이다.

import 'dart:io';

 

  1. 서버 만들기 (bind)
    • 서버를 열려면 우선 HTTP 서버를 바인딩하여 요청을 수신할 수 있도록 한다.
  2. 클라이언트 요청 웹 소켓으로 변경하기 (upgrade)
    • HTTP 서버에 수신된 요청을 소켓으로 변환하여 소켓 수신을 할 수 있어야 한다.
  3. 클라이언트의 요청 데이터 수신 처리하기 (listen)
    • 핸들러를 정의하여 클라이언트에서 보낸 요청 데이터를 처리할 수 있도록 해야 한다.
  4. 클라이언트에게 요청 데이터 송신 처리하기 (add) 
    • 데이터를 클라이언트에게 보낸다.
    • dart:io를 이용하는 WebSocket은 send 메서드가 아닌 add 메드를 사용하여 데이터를 송신한다. 

서버 바인딩 (HttpServer.bind)

지정된 주소(address) 및 포트(port)에서 HTTP 요청 수신 대기를 시작한다.

 

[ 구문 ]

// bind static method
Future<HttpServer> bind(
    dynamic address,
    int port,
    {int backlog = 0,
    bool v6Only = false,
    bool shared = false}
)

 

[ 속성 설명 ]

  •  address
    • 서버를 바인딩할 주소
    • 일반적으로 IP 주소 또는 호스트 이름
  • port
    • 서버를 바인딩할 포트 번호
    • 값이 0일 경우 시스템에서 임시 포트 사용
  • backlog
    • 소켓 대기열의 최대 길이, 기본값은 0
    • 값이 0일 경우 대기열에 연결 요청하는 대기 시간이 없고 클라이언트 요청 즉시 처리
    • 서버에 너무 많은 처리 요청이 있을 경우를 방지하기 위한 속성
  • v6Only
    • IPv6만 연결할 수 있도록 설정, 기본값은 false
    • true일 경우, IPv4 연결을 허용하지 않음
  • shared
    • 포트가 다른 서비스와 공유될 수 있는지 여부 지정, 기본값은 false
    • 일반적으로 특정 포트에 하나의 서비스를 제공하므로 특별한 이유가 없으면 사용을 권장하지 않음 

 

[ 메서드 설명 ]

서버가 성공적으로 바인딩되면 Future<HttpServer> 객체를 반환한다.

반환된 객체를 사용하여 HTTP 요청을 수신하고 처리할 수 있다.


Http 요청을 웹 소켓으로 변환 (WebSocketTransformer.upgrade)

HttpRequest를 WebSocket 연결로 업그레이드한다.

 

[ 구문 ]

// upgrade static method
Future<WebSocket> upgrade(
    HttpRequest request,
    {dynamic protocolSelector(
    	List<String> protocols
    )?,
    CompressionOptions compression = CompressionOptions.compressionDefault}
)

/* Sample
// binding server
HttpServer server;
server.listen((request) {
  if (...) {
    WebSocketTransformer.upgrade(request).then((websocket) {
      ...
    });
  } else {
    // Do normal HTTP request processing.
  }
});
*/

 

[ 속성 설명 ]

  • request
    • Websocket으로 업그레이드할 HTTP 요청
  • protocolSelector
    • 서버와 클라이언트 간 통신에 사용할 서브 프로토콜 선택
    • 값이 null이면 프로토콜을 따로 선택하지 않음
  • compression
    • 웹 소켓 압축 옵션, 기본값은 CompressionOptions.compressionDefault
    • 대부분 기본값이면 충분하지만 일부 상황에서는 다른 값을 사용할 수도 있음

 

[ 메서드 설명 ]

HTTP 또는 HTTPS 서버의 각 HttpRequest를 WebSocket 프로토콜로 업그레이드한다.

업그레이드된 HttpRequest Stream은 WebSocket Stream으로 변환된다.

웹 소켓의 웹 소켓 프로토콜 RFC6455를 참고하여 구현 및 변환된다.

* RFC6455 : TCP 기반의 양방향 통신을 위한 표준 프로토콜


클라이언트의 데이터 수신 처리 (StreamSubscriptiion<List<int>> listen)

스트림에서 생성되는 새로운 데이터를 수신하고 이에 대한 반응 리스너를 등록한다.

 

[ 구문 ]

// listen method
StreamSubscription<List<int>> listen(
    void onData(
    	List<int> event
    )?,
    {Function? onError,
    void onDone()?,
    bool? cancelOnError}
)

 

[ 속성 설명 ]

  • onData
    • 데이터 이벤트가 발생할 때 호출되는 콜백 함수
    • 스트림에서 수신된 데이터를 처리
  • onError
    • 에러 이벤트가 발생할 때 호출되는 콜백 함수
    • 발생한 에러를 처리
    • 생략 가능
  • onDone
    • 스트림이 닫히고 완료 이벤트가 발생할 때(종료될 때) 호출되는 콜백 함수
    • 생략 가능
  • cancelOnError
    • 에러가 발생하면 스트림을 자동 종료 여부
    • 기본값은 false, 에러 발생 시에도 스트림 유지

 

[ 메서드 설명 ]

제공된 핸들러를 사용하여 스트림의 이벤트를 처리하는 StreamSubscription을 반환한다.

스트림에서 데이터를 수신하고 처리하기 위해 사용한다.


클라이언트로 데이터 송신 처리 (WebSocket.add)

웹 소켓에 연결된 클라이언트에 데이터를 보낸다.

void add(
	dynamic data
)

 


웹 소켓 서버 구성 예시

클라이언트로부터 데이터를 받으면 모든 클라이언트에게 받은 데이터를 다시 출력하는 예시 소스

  • 서버 구성 설정
    • 로컬 호스트 아이피 : 192.168.0.103
    • 포트 : 4001
import 'dart:io';

void main() async {
  // 서버 설정
  HttpServer server = await HttpServer.bind('192.168.10.103', 4001);
  print("서버가 생성되었습니다.");

  // 클라이언트 요청 비동기 처리
  await for (var req in server) {
    // HTTP 요청을 웹 소켓 프로토콜로 업그레이드
    await WebSocketTransformer.upgrade(req).then((WebSocket websocket) {
      // 디버그용 클라이언트 식별 print
      print("클라이언트 접속 : ${req.connectionInfo!.remoteAddress.address}");

      // 클라이언트로 받은 데이터 수신 처리
      websocket.listen((data) {
        // 연결되어 있는 클라이언트에게 보낼 데이터 송신 처리
        websocket.add(data);
      });
    });
  }
}

클라이언트 (Client)

절차

웹 소켓 서버를 연결하려면 이것 또한 dart.io 라이브러리가 추가로 필요하다.

import 'dart:io';

 

  1. 서버 연결하기 (connect)
    • 웹 소켓 서버를 먼저 연결하여야 한다.
  2. 서버로부터 받은 데이터 수신 처리하기 (listen)
    • 핸들러를 정의하여 서버로부터 받은 데이터를 처리할 수 있도록 해야 한다.
  3. 서버로부터 보낼 데이터 송신 처리하기 (add)
    • 데이터를 서버로 보낸다.
  1.  

서버 연결 (WebSocket.connect)

웹 소캣 연결을 설정한다.

 

[ 구문 ]

Future<WebSocket> connect(
    String url,
    {Iterable<String>? protocols,
    Map<String, dynamic>? headers,
    CompressionOptions compression = CompressionOptions.compressionDefault,
    HttpClient? customClient}
)

 

[ 속성 설명 ]

  • url
    • WebSocket 서버의 URL
    • 서버의 주소와 포트 포함
    • URL은 ws 또는 wss 체계를 사용해야 함

* ws : RFC 6455 명세서에 정의된 프로토콜인 웹 소켓을 사용하여 웹 소켓 커넥션을 만들 때 사용되는 특수 프로토콜

* wss : TSL(전송 계층 보안)을 사용하여 보안이 적용된 웹 소켓 커넥션 특수 프로토콜 (http / https 관계와 비슷)

  • protocols
    • 클라이언트가 지원하는 서브 프로토콜 목록
  • headers
    • WebSocket 연결에 포함될 헤더 정보를 지하는 맵
    • 사용자 지정 HTTP 헤더를 추가할 수 있음
  • compression
    • 웹 소켓 압축 옵션, 기본값은 CompressionOptions.compressionDefault
    • 대부분 기본값이면 충분하지만 일부 상황에서는 다른 값을 사용할 수도 있음
  • customClient
    • 사용자 지정 HttpClient를 지정, 연결에 대한 사용자 지정 구성 제공
    • 연결 시간 초과, 인증서 구성, 프록시 설정 등 다양한 네트워크 설정 조정

 

[ 메서드 설명 ]

주어진 url에 대한 WebSocket 연결을 설정한다.

클라이언트가 서버에 연결하고 통신을 시작할 수 있도록 하는 기능을 제공한다.


서버로부터 받은 데이터 수신 처리 (StreamSubscriptiion<List<int>> listen)

서버 챕터에서 이미 설명하였으므로 생략한다.


서버로 데이터 송신 처리 (WebSocket.add)

서버 챕터에서 이미 설명하였으므로 생략한다.


클라이언트 서버 연결 구성 예시

웹 소켓 서버 연결 시, 서버 측으로 'Hello?'라는 메시지를 보내고 서버로부터 받은 데이터를 출력하는 예시 소스

import 'dart:io';

void main() async {
  // 웹 소켓 서버 연결
  WebSocket socket = await WebSocket.connect('ws://192.168.10.103:4001');

  // 소켓 서버에 데이터 송신
  socket.add("Hello?");

  socket.listen((data) {
    print("서버로부터 받은 값 : $data");
  });
}

 


웹 소켓 서버 / 클라이언트 접속 테스트

디렉터리 구성

  • 서버 : server.dart
  • 클라이언트 : lib/main.dart

<서버/클라이언트 구성 디렉토리 예시 화면>


서버 생성 (바인딩)

  • 터미널에서 생성한 서버 다트 파일 실행 (server.dart)
dart server.dart

<VS Code 터미널에서 생성한 서버 파일을 실행한 화면>


클라이언트 연결 (서버 접속)

  • 터미널에서 생성한 클라이언트 다트 파일 실행 (lib/main.dart)
  • 주의할 점은 반드시 서버를 실행한 터미널을 그대로 둔 채 클라이언트 접속을 시도해야 한다.
dart lib/main.dart

<웹 소켓 서버에 연결된 클라이언트 콘솔 화면>


다음 포스팅으로 이동하기

Flutter/Dart - 채팅 앱 만들기(2) - 앱 UI 레이아웃 디자인 및 기능 설계하기


참고

반응형