일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- Kafka
- 기술공부
- 플러터 공부
- JPA공부
- 자료구조공부
- 플러터 개발
- 스프링
- 코테공부
- Flutter
- Axon framework
- querydsl
- 스프링부트
- 알고리즘공부
- JPA예제
- JPA
- 스프링공부
- JPA스터디
- JPA 공부
- K8S
- nestjs공부
- 자바공부
- 카프카
- 기술면접공부
- 코테준비
- 스프링부트공부
- 프로그래머스
- nestjs스터디
- DDD
- nestjs
- 스프링 공부
- Today
- Total
DevBoi
[Flutter + Socket] Flutter + NestJs + Socket 서비스 개발하기 본문
채팅 관련 웹 서비스를 개발했다.
다뤄보지 않은 프레임워크를 선택해서 극단으로 몰았던게 조금 후회되지만 무튼 관련 개발 내용을 정리해서 포스팅한다.
우선 개발한 서비스의 아키텍처는 아래와 같다. 그림은 귀찮으니 대충
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시켜주면 정상적으로 통신이 가능하다.
이렇게 해서 위의 아키텍처대로의 동작을 할수 있다.
'Infra > [AWS]' 카테고리의 다른 글
[AWS] Github Actions로 NextJs,EC2,Docker CI/CD 구축 (0) | 2024.06.06 |
---|---|
[AWS] Nginx + LetsEncrypt(https) + NestJs + Flutter web (0) | 2024.05.15 |
[Aws] Https 인증서 세팅하기 (0) | 2024.04.05 |
[AWS] RDS too many Connection 오류 (0) | 2024.02.03 |
[AWS] EC2 메모리 이슈분석 및 Swap memory 설정 (0) | 2024.01.25 |