Controller
@PostMapping("/cad")
@ResponseStatus(HttpStatus.CREATED)
public Response cad(
@Valid @ModelAttribute
CadRequest req,
HttpServletRequest request
) throws Exception {
..................................................................
// 1) cad upload service 를 통해서 socket 반환값 제작
DesignSocketDto designSocketDto = designService.cadUpload(req);
// 2) socket 에 담아줄 메시지 형태 제작 (소켓 방 아이디, 메시지 , 타입 필요)
ChatMessage chatMessage = new ChatMessage(
req.getRoomId(),
Long.parseLong(req.getItemId())
);
// 3) SOCKET 에 담을 MESSAGE 형태로 제작
// SOCKET 에 MSG 로 담아줄 아이는 JSON 형식이라 JSON 변환
Gson gson = new Gson();
String chatMsgJson = gson.toJson(chatMessage);
TextMessage roomIdMessage = new TextMessage(chatMsgJson);
// 4) handle message => socket 에게 메시지 보내는 것
webSockChatHandler.handleMessage(
null,
roomIdMessage // 보낼 메시지
);
return Response.success(
designSocketDto
);
}
ChatMessage
@Getter
@Setter
public class ChatMessage {
// 고도화된 소통을 해야한다면 방 type 도 커스텀할 수 있지만 제 코드에선 삭제했습니다.
// (ex)
// public enum MessageType {
// ENTER, ENTER2
// }
// private MessageType type; // 메시지 타입
private String roomId; // 방 식별 번호
private String message; // 보낼 메시지
private Long itemId;
public ChatMessage(String roomId, Long itemId) {
this.roomId = roomId;
this.itemId = itemId;
}
}
WebSocketChatHandler
- 상속받아서 커스텀을 진행합니다.
- 웹소켓 클라이언트로부터 채팅 메시지를 전달받아 채팅 메시지 객체로 변환합니다.
- 전달받은 메시지에 담긴 채팅방 Id로 발송 대상 채팅방 정보를 조회합니다.
- 해당 채팅방에 입장해있는 클라이언트(Websocket session)에게 타입에 따른 메시지를 발송합니다.
/**
* 1. 웹소켓 클라이언트로부터 채팅 메시지를 전달받아 채팅 메시지 객체로 변환합니다.
* 2. 전달받은 메시지에 담긴 채팅방 Id로 발송 대상 채팅방 정보를 조회합니다.
* 3. 해당 채팅방에 입장해있는
클라이언트(Websocket session)에게
타입에 따른 메시지 발송
**/
@Slf4j
@RequiredArgsConstructor
@Component
public class WebSockChatHandler extends TextWebSocketHandler {
private final ObjectMapper objectMapper;
private final ChatService chatService;
private final NewItemRepository newItemRepository;
private final DesignRepository designRepository;
@Override
protected void handleTextMessage
(WebSocketSession session, TextMessage message)
throws Exception {
// 1) chat message 읽어옵니다. (ChatMessage 객체로 변환된 상태입니다.)
String payload = message.getPayload();
ChatMessage chatMessage =
objectMapper.readValue(payload, ChatMessage.class);
// 2) chat message 에 넘어온 room id 로 지정받은 room 을 찾습니다.
// 즉 서버가 메시지를 보낼 채팅방을 찾는 것입니다.
ChatRoomDto room = chatService.findRoomById
(chatMessage.getRoomId())
.orElseThrow(SocketRoomRefreshException::new);
// 3) 서버는 handle temp action으로 메시지를 클라이언트에게 보냅니다.
room.handleTempActions
(
session,
chatMessage,
chatService,
newItemRepository ,
designRepository
);
}
}
ChatRoomDto
WebSocketChatHandler
에서 찾은 방에다가 메시지 보내는 작업을 수행합니다. @Getter
@Transactional
public class ChatRoomDto {
private String roomId;
private Set<WebSocketSession> sessions = new HashSet<>();
@Builder
public ChatRoomDto(String roomId) {
this.roomId = roomId;
}
@Transactional
public void handleTempActions(
// web socket session 은 WebSocket connection이 맺어진 세션
// 해당 session을 통해서 메시지를 보낼( sendMessage() ) 수 있지만
// 저는 방번호(roomId)로 소켓방 식별할 것이며 ,
// 외부 프로그램에서 요청이 들어오면 수행하고 소켓 연결해서
// 세션을 어차피 사용할 수 없는 상황이기에 세션이 아닌 방 번호 기준으로 room을 식별해주었습니다.
@Nullable WebSocketSession session,
@Nullable ChatMessage chatMessage,
ChatService chatService,
NewItemRepository newItemRepository,
DesignRepository designRepository) throws ParseException {
NewItem newItem = null;
// chat message 에 message를 지정해줍니다.
chatMessage.setMessage(
.......................................................................
);
// 현재 서버와 연결된 클라이언트의 ws 세션에다가 메시지 보내줍니다.(push)
sendMessage(chatMessage, chatService);
}
/**
* 클라이언트의 ws 세션에 메시지를 보내는 메소드입니다.
**/
@Transactional
public <T> void sendMessage(T message, ChatService chatService) {
sessions.parallelStream().forEach(session -> chatService.sendMessage(session, message));
}
}
location / {
proxy_pass http://도메인:8180;
# 받는 대상 서버(WAS)
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# socket - upgrade 코드 추가
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
}
nginx - socket 설정 글을 토대로 코드를 추가했습니다.
# hop-by-hop 헤더 추가 코드
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
WebSocket을 지원할 때 리버스 프록시 서버가 직면하는 몇 가지 문제가 있습니다.
하나는 WebSocket이 hop-by-hop 프로토콜이므로 프록시 서버가 클라이언트의 Upgrade 요청을 가로챌 때 적절한 헤더를 포함하여 WAS 서버에 업그레이드 요청을 보내야 한다는 것입니다.
또한 HTTP의 단기 연결과 달리 WebSocket은 오래 지속되기에, 리버스 프록시는 연결을 닫지 않고 열린 상태로 유지하는 것을 허용해야 합니다.
이때 Nginx는 클라이언트와 WAS 간 터널(소켓)을 설정할 수 있도록 WebSocket을 지원합니다.
Nginx는 클라이언트에서 WAS로 업그레이드 요청을 보내려면 Upgrade 및 Connection 헤더를 명시적으로 설정해야 하기에 위의 코드를 추가했습니다.
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
- 왼쪽에 노란색 화살표가 붙은 것은 클라이언트가 보내는 REQUEST 입니다.
- 왼쪽에 하늘색 화살표가 붙은 것은 서버가 보내는 RESPONSE 입니다.
클라이언트
- exe (외부 프로그램)
- 서버
간의 비동기 통신을 구현하는 방식이 완료되었습니다.