Skip to content

Commit

Permalink
Merge pull request #141 from My-Own-Weapon/feature/#140-WebRTCStreaming
Browse files Browse the repository at this point in the history
/chat 관련 web Socket 구현
  • Loading branch information
HyeokJoon authored Aug 8, 2024
2 parents 38e95bc + ffd2278 commit 8d8586e
Show file tree
Hide file tree
Showing 13 changed files with 411 additions and 54 deletions.
64 changes: 32 additions & 32 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,41 +73,41 @@ jobs:
script: |
sudo docker login -u ${{ secrets.DOCKERHUB_USERNAME }} -p ${{ secrets.DOCKERHUB_PASSWORD }}
sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/github-actions-demo
sudo docker stop github-actions-demo
sudo docker run --name github-actions-demo --rm --env-file ./.env -p 8080:8080 -d ${{ secrets.DOCKERHUB_USERNAME }}/github-actions-demo
sudo docker system prune -f
# sudo docker stop github-actions-demo
# sudo docker run --name github-actions-demo --rm --env-file ./.env -p 8080:8080 -d ${{ secrets.DOCKERHUB_USERNAME }}/github-actions-demo
# sudo docker system prune -f
# # 블루-그린 배포를 위한 컨테이너 실행
# if [ $(docker ps -q -f name=github-actions-demo-green) ]; then
# # 그린 컨테이너가 실행 중인 경우, 블루 컨테이너로 새 버전 실행
# docker run -d --name github-actions-demo-blue --rm --env-file ./.env_blue -p 8080:8080 ${{ secrets.DOCKERHUB_USERNAME }}/github-actions-demo
# NEW_CONTAINER=github-actions-demo-blue
# OLD_CONTAINER=github-actions-demo-green
# NEW_PORT=8080
# OLD_PORT=8081
# else
# # 블루 컨테이너가 실행 중인 경우, 그린 컨테이너로 새 버전 실행
# docker run -d --name github-actions-demo-green --rm --env-file ./.env_green -p 8081:8081 ${{ secrets.DOCKERHUB_USERNAME }}/github-actions-demo
# NEW_CONTAINER=github-actions-demo-green
# OLD_CONTAINER=github-actions-demo-blue
# NEW_PORT=8081
# OLD_PORT=8080
# fi
# 블루-그린 배포를 위한 컨테이너 실행
if [ $(docker ps -q -f name=github-actions-demo-green) ]; then
# 그린 컨테이너가 실행 중인 경우, 블루 컨테이너로 새 버전 실행
docker run -d --name github-actions-demo-blue --rm --env-file ./.env_blue -p 8080:8080 ${{ secrets.DOCKERHUB_USERNAME }}/github-actions-demo
NEW_CONTAINER=github-actions-demo-blue
OLD_CONTAINER=github-actions-demo-green
NEW_PORT=8080
OLD_PORT=8081
else
# 블루 컨테이너가 실행 중인 경우, 그린 컨테이너로 새 버전 실행
docker run -d --name github-actions-demo-green --rm --env-file ./.env_green -p 8081:8081 ${{ secrets.DOCKERHUB_USERNAME }}/github-actions-demo
NEW_CONTAINER=github-actions-demo-green
OLD_CONTAINER=github-actions-demo-blue
NEW_PORT=8081
OLD_PORT=8080
fi
# sleep 30
sleep 40
# # 잘 동작하면 로드밸런서 변경
# if curl -s http://localhost:$NEW_PORT > /dev/null; then
# aws elbv2 register-targets --target-group-arn "arn:aws:elasticloadbalancing:ap-northeast-2:767398017748:targetgroup/wagu-book-lb-tg/cbd583c7310ea316" --targets Id=i-09037c296cda9b574,Port=$NEW_PORT
# aws elbv2 deregister-targets --target-group-arn "arn:aws:elasticloadbalancing:ap-northeast-2:767398017748:targetgroup/wagu-book-lb-tg/cbd583c7310ea316" --targets Id=i-09037c296cda9b574,Port=$OLD_PORT
# docker stop $OLD_CONTAINER || true
# exit 0
# else
# docker stop $NEW_CONTAINER || true
# echo "새 애플리케이션이 실행되지 않음"
# exit 1
# fi
# sudo docker system prune -f
# 잘 동작하면 로드밸런서 변경
if curl -s http://localhost:$NEW_PORT > /dev/null; then
# aws elbv2 register-targets --target-group-arn "arn:aws:elasticloadbalancing:ap-northeast-2:767398017748:targetgroup/wagu-book-lb-tg/cbd583c7310ea316" --targets Id=i-09037c296cda9b574,Port=$NEW_PORT
# aws elbv2 deregister-targets --target-group-arn "arn:aws:elasticloadbalancing:ap-northeast-2:767398017748:targetgroup/wagu-book-lb-tg/cbd583c7310ea316" --targets Id=i-09037c296cda9b574,Port=$OLD_PORT
docker stop $OLD_CONTAINER || true
exit 0
else
docker stop $NEW_CONTAINER || true
echo "새 애플리케이션이 실행되지 않음"
exit 1
fi
sudo docker system prune -f
Expand Down
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ dependencies {
implementation 'com.drewnoakes:metadata-extractor:2.18.0'

implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.12.5'

// webRTC
implementation 'org.springframework.boot:spring-boot-starter-websocket'
}

tasks.named('test') {
Expand Down
17 changes: 0 additions & 17 deletions src/main/java/com/chimaera/wagubook/config/JacksonConfig.java

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public WebMvcConfigurer corsConfigurer() {
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")

.allowedOrigins("http://localhost:3000","https://www.wagubook.shop","http://3.37.8.147:3000") // React 앱의 주소
.allowedOrigins("http://localhost:3000","https://www.wagubook.shop","http://3.37.8.147:3000", "https://895c-168-126-208-153.ngrok-free.app") // React 앱의 주소
.allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
.allowedHeaders("Authorization", "Cache-Control", "Content-Type") // 필요한 헤더만 허용. allowedHeaders("*")
.allowCredentials(true);
Expand Down
19 changes: 19 additions & 0 deletions src/main/java/com/chimaera/wagubook/config/WebSocketConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.chimaera.wagubook.config;
import com.chimaera.wagubook.controller.ChatSignalHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new ChatSignalHandler(), "/chat")
.addInterceptors(new HttpSessionHandshakeInterceptor()) //웹 소켓 세션에서 httpSession정보를 가져오기 위해 HttpSession에 있던 정보들을 copy
.setAllowedOrigins("*");
}
}
190 changes: 190 additions & 0 deletions src/main/java/com/chimaera/wagubook/controller/ChatSignalHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package com.chimaera.wagubook.controller;
import com.chimaera.wagubook.dto.request.WebSocketRequest;
import com.chimaera.wagubook.dto.response.WebSocketResponse;
import com.chimaera.wagubook.entity.Member;
import com.chimaera.wagubook.exception.CustomException;
import com.chimaera.wagubook.exception.ErrorCode;
import com.chimaera.wagubook.repository.webRTC.SessionRepository;
import com.chimaera.wagubook.service.MemberService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static com.chimaera.wagubook.exception.ErrorCode.SESSION_ROOM_NOT_FOUND;

public class ChatSignalHandler extends TextWebSocketHandler {
@Autowired
private MemberService memberService;
private final SessionRepository sessionRepository = SessionRepository.getInstance();
private final ObjectMapper objectMapper = new ObjectMapper();
private List<WebSocketSession> sessionList = new ArrayList<>();

// 데이터 타입
private static final String MSG_TYPE_JOIN_ROOM = "join_room";
private static final String MSG_TYPE_CHAT = "chat";

/**
* 웹 소켓이 연결되면 실행되는 메서드
* 어떤 session인지 출력
*/
@Override
public void afterConnectionEstablished(WebSocketSession session){
System.out.println("웹 소켓 연결 성공 session : " + session);
Long memberId = (Long)session.getAttributes().get("memberId");
System.out.println("member Id : " + memberId);
if (memberId == null) {
throw new CustomException(ErrorCode.REQUEST_LOGIN);
}
// sessionList.add(session);
}

@Override
protected void handleTextMessage(final WebSocketSession session, final TextMessage message) throws Exception {
Long memberId = (Long)(session.getAttributes().get("memberId"));
if (memberId == null) {
throw new CustomException(ErrorCode.REQUEST_LOGIN);
}

// 수신된 메시지를 JSON 객체로 파싱
String payload = message.getPayload();
WebSocketRequest data = new ObjectMapper().readValue(payload, WebSocketRequest.class);
String type = data.getType();
String roomURL = data.getRoomURL();

// 타입에 따라 다른 로직을 적용할 수 있다 (P2P연결을 위한 기능)
//SDP 오퍼(SDP Offer): 피어 A가 연결을 시작하기 위해 자신의 미디어 세션 설정을 피어 B에게 보내는 메시지
//SDP 응답(SDP Answer): 피어 B가 SDP 오퍼를 수신하고 자신의 설정을 포함하여 응답하는 메시지
//ICE 후보자(ICE Candidates): P2P 연결을 설정하기 위해 피어들이 교환하는 네트워크 경로 정보
//React에서 P2P연결을 하기 위해 필요하다 특정 누군가에 대한게 아니라 브로드케스드


switch (type){
/**
* MSG_TYPE_JOIN_ROOM : 처음 방에 입장했을 때
* 방을 시작하는 경우 (스트리머) - 새로운 방을 생성하고 Client(방장)의 세션 정보를 저장
* 방에 참여하는 경우 (시청자) - sessionList에 Client 세션 정보를 저장
*/
case MSG_TYPE_JOIN_ROOM:
//방이 존재할 때
if(sessionRepository.isExistRoom(roomURL)){
sessionRepository.addClient(roomURL, session);
}
//방을 새로 만들 때
else{
sessionRepository.addClientInNewRoom(roomURL, session);
}
//이 세션이 어느 방에 들어가 있는지 저장
sessionRepository.saveRoomIdHashMapBySession(session, roomURL);

//방 안에 유저 이름 저장
sessionRepository.addUsernameInRoom(session.getId(), data.getSenderName());

Map<String, WebSocketSession> joinClientList = sessionRepository.getClientList(roomURL);

//방 안 참가자에게 접속한 사람 정보 전달
List<String> participantSessionList = new ArrayList<>();
for (Map.Entry<String, WebSocketSession> entry : joinClientList.entrySet()) {
if(entry.getValue() != session){
participantSessionList.add(entry.getKey());
sendMessage(entry.getValue(),new WebSocketResponse().builder()
.type("join")
.senderId(data.getSenderId())
.senderName(data.getSenderName())
.data(data.getData())
.build());
}
}

//방에 참여하고 있던 참가자 리스트(username, sessionId)
List<String> participantNameList = new ArrayList<>();
for (Map.Entry<String, WebSocketSession> entry : joinClientList.entrySet()) {
if(entry.getValue() != session)
participantNameList.add(sessionRepository.getUsernameInRoom(entry.getKey()));
}

//접속한 사람에게 방 안 참가자들 정보를 반환
sendMessage(session, new WebSocketResponse().builder()
.type("all_users")
.senderId(data.getSenderId())
.senderName(data.getSenderName())
.data(data.getData())
.allUsersNickNames(participantNameList)
.build());
break;

case MSG_TYPE_CHAT:

if (sessionRepository.isExistRoom(roomURL)) {
Map<String, WebSocketSession> ClientList = sessionRepository.getClientList(roomURL);

//방에 존재하는 모든 사람들에게 메시지를 보냄
for (WebSocketSession ws : ClientList.values()) {
sendMessage(ws,
new WebSocketResponse().builder()
.type(data.getType())
.senderId(data.getSenderId())
.senderName(data.getSenderName())
.data(data.getData())
.build());
}
} else {
throw new CustomException(SESSION_ROOM_NOT_FOUND);
}
break;


default:
}
}

private void sendMessage(WebSocketSession session, WebSocketResponse message) {
try {
String json = objectMapper.writeValueAsString(message);
session.sendMessage(new TextMessage(json));
} catch (IOException e) {
System.out.println("============== 발생한 에러 메세지: {}" + e.getMessage());
}
}

// 웹소켓 연결이 끊어지면 실행되는 메소드
@Override
@Transactional
public void afterConnectionClosed(final WebSocketSession session, final CloseStatus status) {
Long memberId = (Long)session.getAttributes().get("memberId");
if (memberId == null) {
throw new CustomException(ErrorCode.REQUEST_LOGIN);
}
Member findMember = memberService.findById(memberId);

// 끊어진 세션이 어느방에 있었는지 조회
String roomUrl = sessionRepository.getRoomUrlToSession(session);

// 2) 방 참가자들 세션 정보들 사이에서 삭제
sessionRepository.deleteClient(roomUrl, session);

// 3) 별도 해당 참가자 세션 정보도 삭제
sessionRepository.deleteRoomIdToSession(session);

// 4) 별도 해당 닉네임 리스트에서도 삭제
sessionRepository.deleteUsernameInRoom(session.getId());
// 본인 제외 모두에게 전달
for(Map.Entry<String, WebSocketSession> client : sessionRepository.getClientList(roomUrl).entrySet()){
sendMessage(client.getValue(),
new WebSocketResponse().builder()
.type("leave")
.senderId(findMember.getUsername())
.senderName(findMember.getName())
.build());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ public ResponseEntity<Map<String, Object>> initializeSession(@RequestBody(requir

// storeLocation 객체 생성
String address = (String) storeLocationMap.get("address");
double posx = ((Number) storeLocationMap.get("posx")).doubleValue();
double posy = ((Number) storeLocationMap.get("posy")).doubleValue();
double posx = Double.parseDouble((String)storeLocationMap.get("posx"));
double posy = Double.parseDouble((String)storeLocationMap.get("posy"));
Location storeLocation = new Location();
storeLocation.setAddress(address);
storeLocation.setPosx(posx);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ public static class MenuCreateRequest {
@NotNull(message = "메뉴 가격은 필수 값입니다.")
private int menuPrice;

@Size(max = 5000, message = "메뉴 설명은 최대 5000자까지 가능합니다.")
private String menuContent;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.chimaera.wagubook.dto.request;

import lombok.Data;

@Data
public class WebSocketRequest {
private String senderId;
private String senderName;
private String type;
private String data;
private String roomURL;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.chimaera.wagubook.dto.response;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.List;
import java.util.Map;

@Builder
@Getter
@JsonInclude(JsonInclude.Include.NON_NULL)
@NoArgsConstructor
@AllArgsConstructor
public class WebSocketResponse {
private String senderId; // sender 로그인 아이디
private String senderName; // sender 이름
private String type; // [""]
private String data; // 메시지
private String roomURL; // 방 url
private List<String> allUsersNickNames; // 방에 참여하고 있는 사람들 리스트(처음 입장 시)
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public enum ErrorCode {
NOT_FOUND_STORE(NOT_FOUND, "해당 스토어를 찾을 수 없습니다."),
NOT_FOUND_URL(NOT_FOUND, "해당 url이 존재하지 않습니다."),
NOT_FOUND_LIVE_ROOM(NOT_FOUND, "해당 라이브룸을 찾을 수 없습니다."),
SESSION_ROOM_NOT_FOUND(NOT_FOUND, "해당 세션룸을 찾을 수 없습니다."),



Expand Down
Loading

0 comments on commit 8d8586e

Please sign in to comment.