+ 00 00 0000

Have any Questions?

Websocket chat sample

Websocket chat sample

📃 요약

Websocket chatting 샘플입니다.
Stomp( Simple Text Oriented Messaging Protocol ) 이용해서 진행해 보도록 하겠습니다.
기본 메세징 방식은 publisher( 발행자 ) – subscribe( 구독자 ) 방식입니다.
메세지 브로커( 중개인 )가 메세지를 수신하면 구독자에게 전달해 주는 방식입니다.
간단한 예제를 위해 채팅방 개설 같은 기능은 제외하고 회원 ID / 채팅메세지 만으로 샘플을 구성합니다.
Spring 공식홈페이지에서 제공하는 Websocket Stomp 예제는 아래 URL 을 참고하십시요
( https://spring.io/guides/gs/messaging-stomp-websocket )

프론트는 Vue 를 사용하고, 벡엔드는 spring boot 를 사용합니다.

요소 기술 :

– 프론트엔드 : Vue

  • 프론트 패키지 : stompjs, sockjs-client, sockjs, webstomp-client

– 벡엔드 : Spring Boot

  • 벡엔드 패키지 : websocket 패키지

결과 화면 :

  • 채팅 화면

프로젝트 탐색기 : Vue

프로젝트 탐색기 : String Boot

Backend : URL 정보

컨트롤러 함수 어노테이션URL설명
@MessageMapping/pub메세지를 전달 받는 주소 ( frontend -> backend )
@SendTo/sub메세지를 전송하는 주소 ( backend -> frontend )

Stomp 간단 메세징 절차

  • Stomp 메세징 frame 구조
    • Command
    • header:value
    • Body
  • 메세징을 전송하는 규격이 미리 정의되어 있어 편하게 활용할 수 있습니다. 개발자가 메세징 구조를 설계할 필요가 없습니다.
  • /pub.으로 메세지를 전송하면 메세지브로커가( 중개인 ) /sub 으로 구독자들에게 메세지를 모두 전송해 줍니다.
    • 각 주소를 고유하게 설계해서 채널 또는 채팅방으로 활용할 수도 있습니다.

📃 기술 구현

스펙 :

Vue 3.x
jdk 17
spring boot 3.x
intellij IDEA & gradle
logging tool : logback

Spring build.gradle : dependencies 블럭 내 추가

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-websocket'
        ... 생략
}

WebSocketConfig 설정 클래스

-WebSocketConfig.java

package org.example.simplechatting.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

/**
 * @author : kangtaegyung
 * @fileName : WebSocketConfig
 * @since : 24. 6. 1.
 * description :
 *     1) sub 구독한 수신자에게 메세지 broadcasting
 */
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Value("${simpleDms.app.front-url}")
    private String frontUrl;

//    메시지를 구독할 주소를 정의
    @Override
    public void configureMessageBroker(MessageBrokerRegistry messageBrokerRegistry) {
        messageBrokerRegistry.enableSimpleBroker("/sub");               // 1)
    }

//    웹 소켓 연결설정
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/")
                .setAllowedOrigins(frontUrl)                                            // vue 주소
                .withSockJS();
    }
}
  • 1) sub 구독한 수신자에게 메세지 broadcasting

메세지 브로커 컨트롤러 :  /pub 메세지를 전달받아 간단하게 다시 /sub 구독자에게 전달합니다.

-ChatController.java

package org.example.simplechatting.controller;

import org.example.simplechatting.dto.ChatMemberDto;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author : kangtaegyung
 * @fileName : ChatController
 * @since : 24. 6. 1.
 * description :
 *     @MessageMapping("/pub") : /pub 를 메시지를 받을 URL
 *     @SendTo("/sub") : /sub 로 메시지를 전달할 URL
 *     => 의미 : /pub 로 메시지를 받고, /sub 로 메시지를 보내줍니다.
 */
@RestController
public class ChatController {
    @MessageMapping("/pub")
    @SendTo("/sub")
    public ChatMemberDto startChatting(ChatMemberDto chatMember) {
        String userId = chatMember.getUserId();
        String chatMessage = chatMember.getChatMessage();

        ChatMemberDto chatMember2 = new ChatMemberDto(userId, chatMessage);

        return chatMember2;
    }
}

– ChatMemberDto.java

package org.example.simplechatting.dto;

import lombok.*;

/**
 * @author : kangtaegyung
 * @fileName : ChatMemberDto
 * @since : 24. 5. 27.
 * description : 채팅 회원 DTO
 */
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class ChatMemberDto {
    private String userId;
    private String chatMessage;
}

– CommonExceptionAdvice

공통 Exception 클래스 : 컨트롤러에서 어떤 예외가 발생하더라도 이 클래스의 함수가 실행됨
package org.example.simplechatting.exceptions;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * @author : kangtaegyung
 * @fileName : ExControllerAdvice
 * @since : 24. 5. 27.
 * description : 공통된 예외 처리 함수
 *  @ResponseStatus(HttpStatus.상태코드) : 상태코드가 발생하면
 *  @ExceptionHandler(Exception.class) : Exception 예외 클래스에 대해 처리한다.
 */
@Slf4j
@RestControllerAdvice
public class CommonExceptionAdvice {

//  컨트롤러에서 어떤 에러가 발생하더라도 이 함수가 실행됨
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(Exception.class)
    public ResponseEntity<?> internalServerErrorException(Exception e) {
        log.debug("벡엔드 에러: " + e.getMessage());

        return new ResponseEntity<>("벡엔드 에러: " + e.getMessage()
                , HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

Vue 페이지 : 간단한 채팅 화면입니다.

– Vue 패키지 추가 :

  "dependencies": {
    "sockjs": "^0.3.24",
    "sockjs-client": "^1.6.1",
    "stompjs": "^2.3.3",
    "webstomp-client": "^1.2.6"
     ...생략
  },

1) 공통 js

– utils/constraints.js : axios 기본 설정 파일

const SERVER_URL = "http://localhost:8000";

const constraints = {
    SERVER_URL,
}

export default constraints;

– router/index.js

import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  {
    path: '/',
    component: () => import('../views/HomeView.vue')
  }
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router

Vue 페이지

결과 화면 :

– HeaderCom.vue

<template>
  <div>
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
      <div class="container-fluid">
        <a class="navbar-brand" href="#">simple-coding</a>
        <!-- 단축메뉴(햄버거 메뉴) -->
        <button
          class="navbar-toggler"
          type="button"
          data-bs-toggle="collapse"
          data-bs-target="#navbarSupportedContent"
          aria-controls="navbarSupportedContent"
          aria-expanded="false"
          aria-label="Toggle navigation"
        >
          <span class="navbar-toggler-icon"></span>
        </button>

        <div class="collapse navbar-collapse" id="navbarSupportedContent">
          <ul class="navbar-nav me-auto mb-2 mb-lg-0">
            <!-- 1st : Home -->
            <li class="nav-item">
              <a class="nav-link active" aria-current="page" href="#">Home</a>
            </li>
          </ul>
        </div>
      </div>
    </nav>
  </div>
</template>
<script>
export default {};
</script>
<style></style>

App.vue

<template>
  <div>
    <!-- 머리말 -->
    <HeaderCom />

    <!-- 본문 -->
    <div class="container">
      <router-view />
    </div>
  </div>
</template>

<script>
import HeaderCom from "@/components/HeaderCom.vue"

export default {
  components: {
    HeaderCom
  }
}
</script>

<style lang="scss"></style>

HomeView.vue

<template>
  <div class="mt-2">
    <div class="col-3">
      <label for="userId" class="col-form-label"> userId </label>
    </div>

    <div class="col-3">
      <input
        type="text"
        required
        class="form-control"
        placeholder="userId"
        v-model="chatMember.userId"
      />
    </div>

    <div class="col-3 mt-5">
      <label for="chatMessage" class="col-form-label"> chatMessage </label>
    </div>

    <div class="input-group mb-3">
      <input
        type="text"
        required
        class="form-control"
        placeholder="chatMessage"
        v-model="chatMember.chatMessage"
        @keyup.enter="sendMessage"
      />
      <button class="btn btn-secondary" @click="sendMessage">전송</button>
    </div>

    <div class="phone-wrap mt-5">
      <div>
        <img :src="require('@/assets/img/iphone.png')" alt="아이폰" />
      </div>
      <div class="text-wrap">
        <div v-for="(data, index) in messageArray" :key="index">
          <h3>유저이름: {{ data.userId }}</h3>
          <h3>내용: {{ data.chatMessage }}</h3>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import Stomp from "webstomp-client";
import SockJS from "sockjs-client";
import constraints from "@/utils/constrains";

export default {
  data() {
    return {
      messageArray: [], // 전송한 내용을 모두 저장할 배열
      chatMember: {
        userId: "",
        chatMessage: "",
      },
      connected: false,
      stompClient: null,
    };
  },
  methods: {
    connectSocket() {
      let socket = new SockJS(constraints.SERVER_URL);                     // 벡엔드 주소로 접속
      this.stompClient = Stomp.over(socket);
      this.stompClient.connect(
        {},
        () => this.receiveMessage(),
        (e) => this.errorSocket(e)
      );
    },
    receiveMessage() {                                            
      this.connected = true;
      this.stompClient.subscribe("/sub", (response) => {                    // sub 주소로 메세지 받기
        this.messageArray.push(JSON.parse(response.body));
      });
    },
    // 접속시 에러 출력
    errorSocket(e) {
      console.log("소켓 연결 실패", e);
      this.connected = false;
    },
    sendMessage() {
      if (
        this.stompClient &&
        this.connected &&
        this.chatMember.chatMessage !== ""
      ) {
        this.stompClient.send("/pub", JSON.stringify(this.chatMember), {});  // pub 주소로 메세지 전송
        this.chatMember.chatMessage = "";
      }
    },
  },
  mounted() {
    this.connectSocket();
  },
};
</script>

<style scoped>
.phone-wrap {
  width: 20vw;

  position: relative;
}

.phone-wrap img {
  width: 100%;
}

.text-wrap {
  position: absolute;
  top: 51%;
  left: 51%;
  width: 80%;
  height: 71%;
  transform: translate(-50%, -50%);

  background-color: black;

  font-size: 20px;
  color: wheat;
  font-family: Arial, Helvetica, sans-serif;

  padding: 10px;
  overflow: scroll;
}
</style>

📃 결론

Websocket stomp 이용해서 채팅을 진행하는 예제를 살펴보았습니다.

화면에 메세지를 타이핑하고 엔터키 또는 전송 버튼을 클릭하면 /pub 으로 메세지가 전송됩니다.
연달아서 /sub 으로 구독하고 있는 사용자에게 메세지가 전송됩니다.

메세지를 전송받고 전송하는 컨트롤러는 1개만 사용했습니다.

Websocket 과 Stomp 를 이용한 채팅에 관심 있으시다면 Source 는 아래에서 찾을 수 있습니다.

감사합니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다