https://kwaksh2319.tistory.com/1004

웹소켓통신 서버 이용하여 간단한 채팅방 만들기2

https://kwaksh2319.tistory.com/1003 웹소켓통신 서버 이용하여 간단한 채팅방 만들기1구조 : STOMPSTOMP은 **"Simple (or Streaming) Text Oriented Messaging Protocol"**의 약자입니다. 웹소켓(WebSocket)을 사용할 때 메시지를

kwaksh2319.tistory.com


이번에 조금 변경해서 ui/ux 조금 수정을 하고 채팅방까지 구현했습니다. 
그리고 채팅로그가 남도록했습니다. 화면있고 코드도 간단히 설명하겠습니다.
화면: 

메세지를 보내면 저장하도록 변경했습니다. 

 @Override
    public void sendMessage(@DestinationVariable Long roomId, MessageDTO message, StompHeaderAccessor headerAccessor) {
        String clientIp = (String) headerAccessor.getSessionAttributes().get("ipAddress");
        if (clientIp == null) {
            clientIp = "unknown"; // 기본값 설정
        }

        try{
            Optional<Room> room =roomService.findById(roomId);
            if(room.isPresent()){
                Message messageEntity = Message.builder()
                        .room(room.get())
                        .sender(message.getSender())
                        .text(message.getContent())
                        .ip(clientIp)
                        .build();
                messageRepository.save(messageEntity);

                String destination = "/topic/room/" + roomId; // 방 ID 경로

                messagingTemplate.convertAndSend(destination, message); // 해당 방의 사용자들에게 메시지 전송

            } else {
            // 예외를 던지면, 아래 @MessageExceptionHandler가 처리합니다.
            throw new IllegalArgumentException("메세지를 보낼 수 없습니다. 올바른 방이 아닙니다.");
        }

        }catch (Exception e){
            log.error("message table save error");
        }

   }

 
그리고 방을 접속할때 메세지들을 가져옵니다. 

 @Override
    public Page<MessageDTO> findAll(@Param("roomId") Long roomId) {
        int pageSize=100;
        int pageNumber=0;
        Page<MessageDTO> messageDTOPage=null;
        try {

            Page<Message> messages =messageRepository.findAll(roomId,PageRequest.of(pageNumber, pageSize, Sort.by("createdAt").ascending()));

             messageDTOPage = messages.map(message ->
                    MessageDTO.builder()
                            .content(message.getText())
                            .sender(message.getSender())
                            .build());
        }catch (Exception e){
            log.error("message  table get content error or roomid not convert string");
        }

        return messageDTOPage;
    }

 

https://kwaksh2319.tistory.com/1003

웹소켓통신 서버 이용하여 간단한 채팅방 만들기1

구조 : STOMPSTOMP은 **"Simple (or Streaming) Text Oriented Messaging Protocol"**의 약자입니다. 웹소켓(WebSocket)을 사용할 때 메시지를 주고받기 위한 프로토콜 중 하나로, 텍스트 기반의 메시징 프로토콜입니다.

kwaksh2319.tistory.com


 
지난번에 이어서~ 
테이블 설계도 조금 변경했습니다. ( 퇴근 후 개발하는건 늘 쉽지 않네요 ~ , 퇴근후 딴짓을 너무 많이하네요 ㅠ ㅠ )
간단하게 방리스트들을 만들었습니다.
 
화면 : 

1:n 관계로 메세지 테이블을 만들어줬습니다. 

지난번에 소켓통신을 통해서 메세지를 가져왔고요.
이번엔 먼저 간단하게 룸이랑 룸 생성후 리스트만 불러오도록 했습니다.
 
room 

@Entity
@Table(name = "Room")
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class Room {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "ROOM_SEQ")
    @SequenceGenerator(name = "ROOM_SEQ", sequenceName = "ROOM_SEQ", allocationSize = 1)
    @Column(name = "ID")
    private Long id;

    private String roomId;

    private String ip;

    @CreationTimestamp
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @OneToMany(mappedBy = "room", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Message> messages = new ArrayList<>();

}

 
 jparepository는 간단하게 생략하고 리스트 불러오는 서비스쪽 코드는 

 @Override
    public Page<RoomListDTO> findAll(int page, int size) {
        Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
        Page<Room> rooms= roomRepository.findAll(pageable);

        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        Page<RoomListDTO> roomDTOs=rooms.map(room ->RoomListDTO.builder()
                .id(room.getId())
                .roomId(room.getRoomId())
                .createdAt(room.getCreatedAt().format(formatter))
                .build());
        return roomDTOs;
    }

리스트들을 불러올거고요. 
dto는 

@Getter
@Setter
@Builder
public class RoomListDTO {
    private Long id;
    private String roomId;
    private String createdAt;
}

해당 파라미터들을 가져와서 룸리스트를 보여줄겁니다. 
프론트코드는 ajax를 통해서 가져올거고요.

function getRoomList(){
    var page = 0;
    var size = 10;

    // Ajax 호출
    $.ajax({
        url: '/room/' + page + '/' + size,
        type: 'GET',
        dataType: 'json',
        success: function(response) {
            var rooms = response.content;
            var tbody = $('#roomTable tbody');
            tbody.empty(); 

            if(rooms && rooms.length > 0) {
                $.each(rooms, function(index, room) {
                    var tr = $('<tr></tr>');
                    tr.append('<td>' + room.id + '</td>');
                    tr.append('<td>' + room.roomId + '</td>');
                    tr.append('<td>' + room.createdAt + '</td>');
                    tbody.append(tr);
                });
            } else {
                tbody.append('<tr><td colspan="3">No rooms available.</td></tr>');
            }
        },
        error: function(xhr, status, error) {
            console.error("Ajax 호출 에러: ", error);
        }
    });
}

 
html 내용입니다. 

<h1 style="text-align: center;">Room List </h1>
<table id="roomTable">
    <thead>
    <tr>
        <th>ID</th>
        <th>방 제목</th>
        <th>등록일</th>
    </tr>
    </thead>
    <tbody>
    <!-- 데이터가 여기 채워집니다 -->
    </tbody>
</table>

 
 

구조 :

 

STOMP

STOMP은 **"Simple (or Streaming) Text Oriented Messaging Protocol"**의 약자입니다. 웹소켓(WebSocket)을 사용할 때 메시지를 주고받기 위한 프로토콜 중 하나로, 텍스트 기반의 메시징 프로토콜입니다.
 
구조를 보시면 수신자는 topic or app을 통해서 request channel을 보냅니다. 
아래 소스 코드를 보시면 구성 요소에서  /topic, /app으로 설정되어 있는걸 알수 있습니다. 

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic"); // 메시지 브로커 경로
        config.setApplicationDestinationPrefixes("/app"); // 클라이언트 요청 경로
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/chat") // WebSocket 엔드포인트
                .addInterceptors(new IpHandshakeHandler())
                .setAllowedOriginPatterns("*")
                .withSockJS(); // SockJS 사용
    }
}

@EnableWebSocketMessageBroker

  • WebSocket 메시지 브로커를 활성화하는 어노테이션입니다.
  • STOMP 프로토콜을 통해 메시지 라우팅과 처리 기능을 제공합니다.

WebSocketMessageBrokerConfigurer

  • WebSocket과 관련된 설정을 커스터마이징할 수 있도록 제공되는 인터페이스입니다.
  • configureMessageBroker와 registerStompEndpoints 메서드를 구현하여 메시지 브로커와 엔드포인트를 설정합니다.

configureMessageBroker(MessageBrokerRegistry config)

메시지 브로커를 설정하는 메서드입니다.

  • config.enableSimpleBroker("/topic")
    • /topic 경로를 사용하는 간단한 메시지 브로커를 활성화합니다.
    • 이 브로커는 구독(Pub/Sub) 모델을 지원하며, 메시지를 특정 대상(destination)으로 전달합니다.
    • 예를 들어, 클라이언트가 /topic/chat을 구독하면 해당 경로로 발행된 메시지를 받을 수 있습니다.
  • config.setApplicationDestinationPrefixes("/app")
    • 클라이언트가 서버로 메시지를 보낼 때 사용할 경로의 접두사를 설정합니다.
    • 클라이언트가 /app/chat으로 메시지를 보내면, 해당 요청은 서버의 컨트롤러에서 처리됩니다.
    • 예를 들어, 서버 측 컨트롤러에서 @MessageMapping("/chat")을 사용해 이 경로를 처리할 수 있습니다.

/topic 을 통해서 채팅 방을 생성합니다. 
관련 roomid가 동일한 구독자들은 그 안에서 메세지를 볼수 이게 됩니다. 
 
메세지를 보내게 되면 /app/chat을 통해서 메세지를 보냅니다. 

백엔드에서 해당 채팅룸의 방이 어디서 보내줘야할지를 보내줍니다.

 

2024.10.09 - [웹/Spring vue 웹 개발] - 소켓 통신 인터페이스 만들기

 

소켓 통신 인터페이스 만들기

요즘 간단하게 주식관련 사이트를 만들어보려고했는데용.흠 친구들도 간간히 주식얘기를 하다보니 퇴근해서 개발을 시작하게 되었습니다. 흠 ... 일단 한국투자증권 open api를 이용을 했고요. 

kwaksh2319.tistory.com

 

 

지난번에 이어서 한투 open api를 통해서 아래 한투 open api

https://apiportal.koreainvestment.com/login

 

 

미국 주식 '애플' 매수 , 매도 데이터를 실시간 소켓통신을 통해서 가져오게했습니다.

이제 사이트에서 데이터를 가져와서 실시간으로 그래프를 그려주고 있습니다.

다음은 간단하게 종목들을 가져오는 것들을 구현할것을 목표로 생각하고 있습니다. 퇴근하고 간간히 하는거라 쉽지 않네요 ㅎ 

 

 

 

 

 

요즘 간단하게 주식관련 사이트를 만들어보려고했는데용.
흠 친구들도 간간히 주식얘기를 하다보니 퇴근해서 개발을 시작하게 되었습니다. 
흠 ...
 
일단 한국투자증권 open api를 이용을 했고요.
 
현재는 대략적으로 실시간으로 소켓통신을 개발을 했습니다. 
머 간략히 얘기해서 소켓통신에 관해서 개발했던게 대학생때 했던거라 기억이 가물가물하기도 해서  간단하게 정리를 해봤습니다. 
대략적인 아이디어는 아래 그림과 같습니다.
 

번호 순대로 일단 가져오고요.
 
예시코드를 간단히 설명하고 끝내겠습니다. 
아래는 코드고 함수 설명은 아래 코드 밑에 작성하겠습니다.
백엔드 코드 : 

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.net.URI;

@RestController
public class WebSocketController {

    @Autowired
    private SimpMessagingTemplate messagingTemplate;

    @GetMapping("/connect")
    public ResponseEntity<String> connectToWebSocket() {
        try {
            URI uri = new URI("ws://external-websocket-url");

            WebSocketClient webSocketClient = new WebSocketClient(uri) {
                @Override
                public void onOpen(ServerHandshake handshake) {
                    System.out.println("WebSocket connected to external server");

                    // WebSocket으로 데이터를 전송
                    send("{\"input\":{\"tr_id\":\"HDFSASP0\",\"tr_key\":\"DNASAAPL\"}}");
                }

                @Override
                public void onMessage(String message) {
                    System.out.println("Received from WebSocket: " + message);  // 받은 데이터 로그 출력

                    // 메시지가 JSON 형식인지 확인
                    if (message.trim().startsWith("{")) {
                        System.out.println("Message is a JSON object");
                    } else {
                        System.out.println("Message is not a JSON object");
                    }

                    // 받은 메시지를 프론트엔드로 전달
                    messagingTemplate.convertAndSend("/topic/stock-data", message);
                }

                @Override
                public void onClose(int code, String reason, boolean remote) {
                    System.out.println("WebSocket connection closed: " + reason);
                }

                @Override
                public void onError(Exception ex) {
                    ex.printStackTrace();
                }
            };

            webSocketClient.connect();

        } catch (Exception e) {
            e.printStackTrace();
            return new ResponseEntity<>("Error occurred while connecting to WebSocket", HttpStatus.INTERNAL_SERVER_ERROR);
        }

        return new ResponseEntity<>("WebSocket connection initiated", HttpStatus.OK);
    }
}

 
onOpen은 WebSocket 연결이 성공적으로 이루어졌을 때 호출되는 콜백 함수입니다. WebSocket 클라이언트STOMP 클라이언트에서 WebSocket 핸드셰이크가 성공하고 연결이 열렸을 때, onOpen 함수가 실행됩니다. 이 시점에서 클라이언트는 서버와 WebSocket 통신을 할 준비가 된 상태
 
onMessageWebSocket 서버로부터 메시지를 수신할 때 호출됩니다. WebSocket을 통해 실시간으로 데이터를 주고받는 데 있어 가장 중요한 이벤트 핸들러 중 하나입니다.
(여기 함수를 통해서 받고 아래 코드에 ws url message를 수신합니다.)

messagingTemplate.convertAndSend("/topic/stock-data", message);

 
 
onClose: WebSocket 연결이 닫힐 때 호출.
 
onError: WebSocket 통신 중 오류가 발생할 때 호출.
 
그렇다면 /topic/stock-data는 어디서 url을 맵핑해주는지 알아야겠죠 

WebSocketMessageBrokerConfigurer

클래스를 사용해서 

@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
    // 클라이언트가 구독할 수 있는 경로 설정
    config.enableSimpleBroker("/topic", "/queue"); // /topic 경로로 구독
    config.setApplicationDestinationPrefixes("/app"); // 클라이언트가 서버에 메시지 전송 시 경로
}

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
    // 클라이언트가 연결할 엔드포인트 설정
    registry.addEndpoint("/ws")
            .setAllowedOriginPatterns("http://localhost:*")  // 포트가 다른 로컬 호스트 허용
            .withSockJS();  // SockJS 사용
}

여기 configure 에 등록해주면 됩니다.
 
프론트 : stompClient에서 커넥트를 하고 subscibe를 통해서 데이터를 받아오면 끝~~ 

const socket = new SockJS('/ws');  // 백엔드에서 설정한 WebSocket 엔드포인트
const stompClient = Stomp.over(socket);

stompClient.connect({}, function (frame) {
    console.log('Connected: ' + frame);

    stompClient.subscribe('/topic/stock-data', function (message) {
        console.log('Received: ', message.body);
    });
}, function (error) {
    console.error('Error in STOMP connection: ', error);
});

 
제가 만든 데이터들을 
가져온 결과물 :

2024.01.06 - [웹/Spring vue 웹 개발] - JWT(json web token) 개발하며 이해하기

 

JWT(json web token) 개발하며 이해하기

JWT 구조 이해헤더(Header): 토큰의 타입 (주로 JWT)과 사용된 해싱 알고리즘 (예: HMAC, SHA256, RSA)을 명시합니다.페이로드(Payload): 토큰에 담을 클레임(claims)을 포함합니다. 클레임은 토큰 사용자에 대

kwaksh2319.tistory.com

 

https://youtu.be/Q7V7R3bhRQg

 

 

이번에 jwt 토큰을 개발하면서 swagger에 간단히 적용해봤습니다.

package kr.co.kshproject.webDemo.Common;

import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.media.StringSchema;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.servers.Server;
import org.springdoc.core.GroupedOpenApi;
import org.springdoc.core.customizers.OpenApiCustomiser;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;

import java.util.ArrayList;
import java.util.List;


@Configuration
public class SwaggerConfig {

    @Bean
    public SecurityScheme customSecurityScheme() {
        return new SecurityScheme()
                .type(SecurityScheme.Type.HTTP)
                .scheme("bearer")
                .bearerFormat("JWT")
                .in(SecurityScheme.In.HEADER)
                .name(HttpHeaders.AUTHORIZATION);
    }

    @Bean
    public GroupedOpenApi commonApi() {
        return GroupedOpenApi.builder()
                .group("api")
                .addOpenApiCustomiser(serverCustomizer())
                .packagesToScan("kr.co.kshproject.webDemo")
                .pathsToMatch("/**")
                .build();
    }

    @Bean
    public OpenApiCustomiser serverCustomizer() {
        return openApi -> {
            // 기존 Components 객체에 접근
            Components components = openApi.getComponents();
            if (components == null) components = new Components();

            // ErrorResult 스키마 추가
            components.addSchemas("ErrorResult", new Schema<>()
                    .type("object")
                    .addProperty("message", new StringSchema())
                    .addProperty("errorCode", new StringSchema()));

            // SecurityScheme 추가
            openApi.addSecurityItem(new SecurityRequirement().addList("bearerAuth")); // 엔드포인트 전부 적용
            components.addSecuritySchemes("bearerAuth", customSecurityScheme());  // swagger auth 버튼 
            openApi.components(components);

            List<Server> servers=new ArrayList<>();
            servers.add(new Server().url("http://localhost:8080").description("for local usages"));
            openApi.setServers(servers);
        };
    }
}

 

 

2024.01.13 - [웹/Spring vue 웹 개발] - jenkins 연결 작업 01

 

jenkins 연결 작업 01

먼저 현재 git 프로젝트와 jenkins을 연결 했는데요. 아직 정확하게 사용법을 잘 모르겠습니다. 사용하는 방법 관련해서는 내일 해보도록 하겠습니다.

kwaksh2319.tistory.com

 

음 현재 계획중인건 

다른 port를 이용해서 docker위에 젠킨슨을 설치후에 다른 도커 이미지에 있는 웹사이트를 빌드할수 있게 하려 햇는데요.

무언가 문제인지 계속 사이트가 느려지거나 e2c 환경이 문제가 생기는것같습니다 .

cpu 이용률이 너무 높아져서 (계속  aws 인스턴스만 껏다 켯네요.. 처음엔 설치 문제에다가 ㅠㅠ ) 

도커에 이미지 두개를 사용하는게 상당히 문제가 되는것같네요.

프리티어의 ram이 1기가? 이 정도로 알고 있어서 아마 도커 이미지 2개의 감당이 안되는듯하다.

 

 

그래서 음 조금 고민 해봣는데요. 아무래도 aws의 인스턴스를 하나 추가하면 프리티어가 아니기 때문에 

제 pc에서 .젠킨슨을 사용하고 

윈도우에서 이용하는 방법으로 하겠습니다. 

 

먼저 현재 git 프로젝트와 jenkins을 연결 했는데요.

 

 

 

아직 정확하게 사용법을 잘 모르겠습니다.

 

사용하는 방법 관련해서는 내일 해보도록 하겠습니다. 

+ Recent posts