DevBoi

[Flutter + Socket] Flutter + NestJs + Socket 서비스 개발하기 본문

Infra/[AWS]

[Flutter + Socket] Flutter + NestJs + Socket 서비스 개발하기

HiSmith 2024. 4. 24. 01:03
반응형

채팅 관련 웹 서비스를 개발했다.

다뤄보지 않은 프레임워크를 선택해서 극단으로 몰았던게 조금 후회되지만 무튼 관련 개발 내용을 정리해서 포스팅한다.

 

우선 개발한 서비스의 아키텍처는 아래와 같다. 그림은 귀찮으니 대충

 

1. Flutter web : 사용자가 사용하는 화면이다.

2. nestJs 포트 두개 (3000,8080) 을 기반으로 채팅과 기초정보를 제공하는 백엔드 프레임워크이다.

3. mongo : 비정형디비로 RDB를 쓰려다가 채팅에 특화된 디비를 쓰기로 했다.

4. mongo express : mongo디비를 볼수있는 콘솔이다.

5. fcm : 사용자가 백그라운드, 포그라운드 일때 메시지 알림을 웹푸시로 주거나 미수신 메시지를 카운트를 증가시킨다.

 

 

아래의 로직으로 간단하게 구현했다. 더 심도있는 로직은 추후 개발 예정이지만, 비즈니스 마다 다르기때문에 포스팅에서는 제외한다.

 

1. 사용자가 채팅방에 입장 

몽고디비에서 입장한 채팅방의 메시지를 정렬하여 제공한다 

import { Controller, Get, Post, Body, Param, Query } from '@nestjs/common';
import { ChatService } from './chat.service';
import { Chat } from '../schemas/chat.schema';

@Controller('/api/chat')
export class ChatController {
  constructor(private readonly chatService: ChatService) {}

  //채팅방 메시지 리스트 조회
  @Get('/list')
  create(@Query('roomId') roomId: string,@Query('page') page: string,@Query('size') size: string): Promise<Chat[]> {      
    console.log('chat controller');
    const messages = this.chatService.findRoomMessages(roomId,page,size);    
    return messages;
  }

}

 

import { Inject, Injectable } from '@nestjs/common';
import { Chat } from '../schemas/chat.schema';
import { ReturnModelType } from '@typegoose/typegoose';
import { InjectModel } from 'nestjs-typegoose';

@Injectable()
export class ChatService {
  constructor(@InjectModel(Chat) private readonly chatModel: ReturnModelType<typeof Chat>,){}

  async create(chat: Chat): Promise<Chat> {    
    //const obj = JSON.parse(Chat);  
    const createdCat = new this.chatModel(chat);
    return createdCat.save();
  }

  async findRoomMessages(roomId:string ,page:string ,size:string): Promise<Chat[]> {   
    return this.chatModel.find({      
      roomId: roomId,    
    })
    .skip(+page * +size)
    .limit(+size)
    .sort([['time', -1]]);
  }
}

 

import { Document } from 'mongoose';

export interface Chat extends Document {
  content: string;
  userId: string;
  time: Date;
  roomId: string;
}
import { prop } from "@typegoose/typegoose";
import { TimeStamps } from "@typegoose/typegoose/lib/defaultClasses";
import { Timestamp } from "typeorm";

export class Chat {
@prop()
userId: string;
@prop()
userName: string;
@prop()
content: string;
@prop()
roomId: string;
@prop()
time: Date;

constructor(chat?: Partial<Chat>){
  Object.assign(this,chat);
}

}

 

2. 사용자가 채팅방에 입장함과 동시에, socket 채널의 수신자로 등록을 진행한다.(Flutter)

 @override
  void initState() {

    WebSocketManager.instance.enterChatRoom(roomId,widget.page,widget.size,() =>setState(() {
    }));
    WebSocketManager.instance.listen(roomId,() =>setState(() {
    }));
    scrollController.addListener(() {
      if (scrollController.offset >=
          (scrollController.position.maxScrollExtent)) {
        widget.page ++;
        WebSocketManager.instance.enterChatRoom(roomId,widget.page,widget.size,() =>print(''));
      }
    });
    super.initState();
  }
void listen(String roomId,VoidCallback setState){
  //입장한 방의 메시지를 리슨하고 있음
  socket.on("rsv_message:"+roomId, (data) {
  allMessageList.insert(0,MessageModel.fromJson(data));
  chatController.add(allMessageList);
  });
}

 

3. 사용자가 메시지를 보내면 해당 소켓 수신자들에게 메시지를 send할수 있는 eventgateway이다.

import { plainToClass } from '@nestjs/class-transformer';
import { Bind, Logger } from '@nestjs/common';
import {
  MessageBody,
  SubscribeMessage,
  WebSocketGateway,
  WebSocketServer,
  OnGatewayConnection,
  OnGatewayDisconnect,
  ConnectedSocket,
} from '@nestjs/websockets';
import { log } from 'console';
import { Server, Socket } from 'socket.io';
import { ChatService } from 'src/chat/chat.service';
import { Chat } from 'src/schemas/chat.schema';

@WebSocketGateway(8080, {
  namespace: 'api-socket',
  cors: { origin: '*' },
  transports: ['websocket'] ,
})
export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect{
  @WebSocketServer() server: Server;
  private logger: Logger = new Logger('EventsGateway');
  constructor(
    private readonly chatService: ChatService,
  
    ) {}

  //발송
  @Bind(MessageBody(),ConnectedSocket())
  @SubscribeMessage('send_message')
  async getAllMessage(chat: Chat){   
    console.log('send_message');   
    this.chatService.create(chat);
    this.server.emit('rsv_message:'+chat.roomId, chat);    
  }

  afterInit(server: Server) {
    console.log('afterInit');
    this.logger.log('afterInit');
  }

  handleConnection(client: Socket) {
    console.log('handleConnection');
    this.logger.log(`Client connected: ${client.id}`);
  }
  handleDisconnect(client: Socket) {
    
    console.log('handleDisconnect');
    this.logger.log(`Client disconnected: ${client.id}`);
  }
}

 

 

4. 사용자가 메시지를 보내면 소켓 서버의 send_message라는 내용의 이벤트를 발행한다.

아래 sendMessage를 통해서 발행하고, 이외 필요한 로직들이 구현되어있다.

import 'dart:async';

import 'package:chatflutter/ext/http/config.dart';
import 'package:chatflutter/ext/http/http-manager.dart';
import 'package:chatflutter/view/chat/widget/message-model.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:socket_io_client/socket_io_client.dart' as IO;
import 'package:socket_io_common/src/util/event_emitter.dart';

final class WebSocketManager {
//I want init one time connection so I using singleton pattern
static final WebSocketManager instance = WebSocketManager();
StreamController<List<MessageModel>> chatController = StreamController<List<MessageModel>>();

List<MessageModel> allMessageList = [];

String _fetchBaseUrl() {
//Product host url
  return webSocketBackend;
}
late IO.Socket socket;
//Our ext object
// IO.Socket get socket => IO.io(
// _fetchBaseUrl(), IO.OptionBuilder()
//     .disableAutoConnect()
//     .setTransports(['websocket']).build());

Future initializeSocketConnection(String roomId) async{
socket = IO.io(
_fetchBaseUrl(), IO.OptionBuilder()
    .disableAutoConnect()
    .setTransports(['websocket']).build());
try {
  socket.connect();
  socket.onConnect((_) {
  debugPrint("Websocket connection success");
});
} catch (e) {
debugPrint('$e');
}
}

disconnectFromSocket() {
  socket.disconnect();
  socket.onDisconnect((data) => debugPrint("Websocket disconnected"));

}
enterChatRoom(String roomId,int page,int size,VoidCallback setState){
  enterChatList(roomId,page,size).then((value) => {
    allMessageList.addAll(value),
    chatController.add(allMessageList),
    setState()
});
}

//처음에 들어왔을때 초기 메시지를 전달 받음
void listen(String roomId,VoidCallback setState){
  //입장한 방의 메시지를 리슨하고 있음
  socket.on("rsv_message:"+roomId, (data) {
  allMessageList.insert(0,MessageModel.fromJson(data));
  chatController.add(allMessageList);
  });
}

//메시지를 보냄
void webSocketSender( dynamic body) {
  socket.emit("send_message", body);
}
}

 

 

 

 

이제 진짜 중요한 nginx 설정이다.

https front에서는 http 를 일반적으로 호출할 수 없다. mixed content로 분류되어 통신이 막힌다.

이를 nginx 설정으로 극복한다.

server {
	listen 80 default_server;
	listen [::]:80 default_server;

	ssl_certificate /etc/letsencrypt/live/rabbithole.gotdns.ch/fullchain.pem; # managed by Certbot
        ssl_certificate_key /etc/letsencrypt/live/rabbithole.gotdns.ch/privkey.pem; # managed by Certbot
        include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
        ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
	

	root /home/ubuntu/smith/web;
	index index.html index.htm index.nginx-debian.html;

	server_name _;

	location / {
	 try_files $uri $uri/ /index.html;
	 proxy_hide_header Access-Control-Allow-Origin;
	 add_header 'Access-Control-Allow-Origin' '*';
         add_header	Content-Security-Policy "upgrade-insecure-requests";
	 proxy_http_version 1.1;
	}
	location /api{
	  proxy_connect_timeout 300s;
          proxy_read_timeout 600s;
          proxy_send_timeout 600s;
          proxy_buffers 8 16k;
          proxy_buffer_size 32k;
          proxy_pass https://[ip]:3000;
        }
    location ^~ /socket{
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_pass http://[ip]:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
	}

 

위의 경로 상으로 나뉜 부분은 80,443 부분에 각각 적용하여 pass시켜주면 정상적으로 통신이 가능하다.

 

이렇게 해서 위의 아키텍처대로의 동작을 할수 있다.

 

반응형