저번에 websocket만을 이용해서 실시간 채팅과 이미지 전송을 구현했었다. 하지만 팀 프로젝트 과정에서 stomp와 sockjs를 추가해달란 프론트에 요청이 있었고 그래서 약간 업그레이드 버전으로 stomp, sockjs를 추가해서 만들게 되었다.
Socket s3 + 실시간 채팅 + 이미지전송
https://velog.io/@dirn0568/Socket-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%B1%84%ED%8C%85-%EC%9D%B4%EB%AF%B8%EC%A7%80%EC%A0%84%EC%86%A1
build.gradle
// Web Socket (채팅)
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.webjars:stomp-websocket:2.3.3-1'
implementation 'org.webjars:sockjs-client:1.1.2'
// aws s3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
// test thymeleaf
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
application.properties
cloud.aws.stack.auto=false
cloud.aws.region.static={지역}
cloud.aws.credentials.access-key=I AM 엑세스키
cloud.aws.credentials.secret-key=I AM 시크릿 엑세스키
cloud.aws.s3.bucket=https://s3.ap-northeast-2.amazonaws.com/{버켓 이름}
WebSockConfig.java
@Configuration
@EnableWebSocketMessageBroker
@RequiredArgsConstructor
public class WebSockConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/sub"); // sub으로 들어오는 요청을 처리해주기 위해 추가함
config.setApplicationDestinationPrefixes("/pub"); // pub으로 들어오는 요청을 처리해주기 위해 추가함
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws-stomp").setAllowedOriginPatterns("*").withSockJS(); // Endpoint를 지정해주었고 setAllowedOriginPatterns("*")를 이용해서 요청 url을 전부 허용해주었다. + withSockJs() 함수를 통해 ws, wss로 socket을 연결하는 것이 아닌 http, https로 socket을 연결하도록 바꾸어주었다.
}
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
registration.setMessageSizeLimit(50 * 1024 * 1024); // 메세지 크기 제한 오류 방지(이 코드가 없으면 byte code를 보낼때 소켓 연결이 끊길 수 있음)
}
@EventListener
public void connectEvent(SessionConnectEvent sessionConnectEvent){
System.out.println(sessionConnectEvent);
System.out.println("연결 성공 감지!_!#!#!#!@#!@@#!@!#!$!@");
//return "redirect:chat/message";
}
@EventListener
public void onDisconnectEvent(SessionDisconnectEvent sessionDisconnectEvent) {
System.out.println(sessionDisconnectEvent.getSessionId());
System.out.println("연결 끊어짐 감지!~!!!!!!!!!!!!!!!!!!!!!!!!");
}
}
ChatController.java
@RequiredArgsConstructor
@Controller
public class ChatController {
private final SimpMessageSendingOperations messagingTemplate;
private final ChatMessageService chatMessageService;
@ResponseBody
@MessageMapping("/chats") // MessageMapping을 통하여 socket으로 부터 오는 메세지를 전부 받을 수 있게 해줌
public void message(ChatMessageRequestDto chatMessageRequestDto) {
ChatMessageResponseDto chatMessageResponseDto = new ChatMessageResponseDto();
if (chatMessageRequestDto.getImgCode() != null) {
chatMessageResponseDto = chatMessageService.BinaryImageChange(chatMessageRequestDto);
} else {
chatMessageResponseDto = new ChatMessageResponseDto(chatMessageRequestDto);
}
System.out.println("chatMessageResponseDto.getImgCode() : " + chatMessageResponseDto.getImgCode());
messagingTemplate.convertAndSend("/sub/chat/room" + chatMessageRequestDto.getChatRoomId(), chatMessageResponseDto); // 구독한 방에만 메세지를 뿌려줌
}
}
ChatRoomController.java
@Controller
@RequiredArgsConstructor
public class ChatRoomController {
// socket test
@RequestMapping("/chat/room/{roomId}")
public ModelAndView chat() {
ModelAndView mv = new ModelAndView();
mv.setViewName("chat");
return mv;
}
// socket test
@RequestMapping("/chat/rooms")
public ModelAndView chatRooms() {
ModelAndView mv = new ModelAndView();
mv.setViewName("chatrooms");
return mv;
}
}
ChatMessageRequestDto.java
@Getter
@NoArgsConstructor
public class ChatMessageRequestDto {
private Long chatRoomId;
private String userEmail;
private String message; // 메시지
private String imgCode; // 바이트 코드로 이미지를 받음
}
ChatMessageResponseDto.java
@Getter
@Setter
@NoArgsConstructor
public class ChatMessageResponseDto {
private Long chatRoomId;
private String userEmail;
private String message;
private String imgCode;
public ChatMessageResponseDto(ChatMessageRequestDto chatMessageRequestDto) {
this.chatRoomId = chatMessageRequestDto.getChatRoomId();
this.userEmail = chatMessageRequestDto.getUserEmail();
this.message = chatMessageRequestDto.getMessage();
this.imgCode = chatMessageRequestDto.getImgCode();
}
}
ChatMessageService.java
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.PutObjectRequest;
import org.springframework.beans.factory.annotation.Autowired;
import shop.dodotalk.dorundorun.message.dto.ChatMessageRequestDto;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import shop.dodotalk.dorundorun.message.dto.ChatMessageResponseDto;
import java.util.*;
@Slf4j
@RequiredArgsConstructor
@Service
public class ChatMessageService {
@Autowired // aws img test
AmazonS3Client amazonS3Client;
private String S3Bucket = "mysparta1"; // Bucket 이름
@Transactional
public ChatMessageResponseDto BinaryImageChange(ChatMessageRequestDto chatMessageRequestDto) {
try {
String[] strings = chatMessageRequestDto.getImgCode().split(","); // ","을 기준으로 바이트 코드를 나눠준다
String base64Image = strings[1];
String extension = ""; // if 문을 통해 확장자명을 정해줌
if (strings[0].equals("data:image/jpeg;base64")) {
extension = "jpeg";
} else if (strings[0].equals("data:image/png;base64")){
extension = "png";
} else {
extension = "jpg";
}
byte[] imageBytes = javax.xml.bind.DatatypeConverter.parseBase64Binary(base64Image); // 바이트 코드를
File tempFile = File.createTempFile("image", "." + extension); // createTempFile을 통해 임시 파일을 생성해준다. (임시파일은 지워줘야함)
try (OutputStream outputStream = new FileOutputStream(tempFile)) {
outputStream.write(imageBytes); // OutputStream outputStream = new FileOutputStream(tempFile)을 통해 생성한 outputStream 객체에 imageBytes를 작성해준다.
}
String originalName = UUID.randomUUID().toString(); // uuid를 통해 파일명이 겹치지 않게 해준다
amazonS3Client.putObject(new PutObjectRequest(S3Bucket, originalName, tempFile).withCannedAcl(CannedAccessControlList.PublicRead)); // s3에 tempFile을 저장해준다.
String awsS3ImageUrl = amazonS3Client.getUrl(S3Bucket, originalName).toString(); // s3에 저장된 이미지 불러오기
try {
FileOutputStream fileOutputStream = new FileOutputStream(tempFile); // 파일 삭제시 전부 아웃풋 닫아줘야함 (방금 생성한 임시 파일을 지워주는 과정
fileOutputStream.close(); // 아웃풋 닫아주기
if (tempFile.delete()) {
log.info("File delete success"); // tempFile.delete()를 통해 삭제
} else {
log.info("File delete fail");
}
} catch (Exception e) {
e.printStackTrace();
}
ChatMessageResponseDto chatMessageResponseDto = new ChatMessageResponseDto(chatMessageRequestDto);
chatMessageResponseDto.setImgCode(awsS3ImageUrl); // s3 이미지 url로 수정해준다.
return chatMessageResponseDto;
} catch (IOException ex) {
log.error("IOException Error Message : {}",ex.getMessage());
ex.printStackTrace();
}
return new ChatMessageResponseDto(); // 이 부분은 꼭 수정해야할듯
}
}
chat.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<style>
*{ margin: 0; padding: 0; }
li { list-style: none; }
.header { font-size: 14px; padding: 15px 0; background: #F18C7E; color: white; text-align: center; border-radius: 10px 10px 0 0;}
.chat ul { width: 100%; }
.chat ul li { width: 100%; }
.left { text-align: left; }
.right { text-align: right; }
.chat ul li > div { font-size: 13px; }
.sender { margin: 10px 20px 0 20px; font-weight: bold; }
.message { display: inline-block; word-break:break-all; margin: 5px 20px; max-width: 75%; border: 1px solid #888;
padding: 10px; border-radius: 5px; background-color: #FCFCFC; color: #555; text-align: left; }
</style>
<body>
<div style="position:relative; width:50rem; height:50rem; margin:auto; border: 1px solid #D5D5D5; border-radius: 15px; overflow-y: scroll;">
<div class="header">
들어옴?????
</div>
<div id="chat">
<ul>
</ul>
</div>
<div style="position:absolute; bottom:0; margin-left:2.5rem;">
<input type="text" style="width: 500px;height: 32px;font-size: 15px;border: 0;border-radius: 15px;
outline: none;padding-left: 10px;background-color: rgb(233, 233, 233); text-align: center" placeholder="메세지를 입력해주세요" id="sendText">
<img id="img" src="" />
<input type='file' id="baseFile" />
<button type="button" style="border:none; color: #4C4C4C; height: 32px; padding:0px 20px; border-radius:10px;
background-color:#B2CCFF; box-shadow: 0 15px 35px rgba(0, 0, 0, 0.2); margin-left:15px;" onclick="sendMessage()">
보내기
</button>
</div>
</div>
</body>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
<script src="/webjars/sockjs-client/1.1.2/sockjs.min.js"></script>
<script src="/webjars/stomp-websocket/2.3.3-1/stomp.min.js"></script>
<script>
let userEmail = localStorage.getItem('userEmail');
let roomId = localStorage.getItem('roomId');
let sock = new SockJS("/ws-stomp"); //new SockJS()를 사용하여 아까 핸들러에서 지정해준 endPoint를 사용하여 연결을 해준다.
let ws = Stomp.over(sock);
ws.connect({}, function(frame) {
ws.subscribe("/sub/chat/room" + roomId, function(message) {
let receive = JSON.parse(message.body);
alert(receive.imgCode);
if (receive.imgCode != null) {
receiveImg(receive);
} else {
receiveMessage(receive);
}
});
}, function(error) {
alert("error"+error);
});
function sendMessage() {
let sendText = $('#sendText').val();
ws.send("/pub/chats", {}, JSON.stringify({chatRoomId:roomId, userEmail:userEmail, message:sendText})); //receiver:participant,
}
function receiveMessage(receive) {
let tempHtml;
if (userEmail == receive.userEmail) {
tempHtml = makeHtmlMessageRight(receive);
} else {
tempHtml = makeHtmlMessageLeft(receive);
}
//$('#chat').append(tempHtml);
document.getElementById('chat').innerHTML += tempHtml;
}
function makeHtmlMessageRight(receive) {
return `<li class="right">
<div class="sender">${receive.userEmail}</div>
<div class="message">${receive.message}</div>
</li>`
}
function makeHtmlMessageLeft(receive) {
return `<li class="left">
<div class="sender">${receive.userEmail}</div>
<div class="message">${receive.message}</div>
</li>`
}
function receiveImg(receive) {
let tempHtml;
if (userEmail == receive.userEmail) {
tempHtml = makeHtmlImgRight(receive);
} else {
tempHtml = makeHtmlImgLeft(receive);
}
document.getElementById('chat').innerHTML += tempHtml;
}
function makeHtmlImgRight(receive) {
return `<li class="right">
<div class="sender">${receive.userEmail}</div>
<img src="${receive.imgCode}" style="width:500px; height:500px;">
</li>`
}
function makeHtmlImgLeft(receive) {
return `<li class="left">
<div class="sender">${receive.userEmail}</div>
<img src="${receive.imgCode}" style="width:500px; height:500px;">
</li>`
}
$(document).ready(function(){ // #baseFile이 변할때마다 감지
$("#baseFile").change(function(){
readImage( this );
});
$("#baseFile").trigger("change");
});
function readImage(input) {
if ( input.files && input.files[0] ) {
var FR= new FileReader();
FR.onload = function(e) {
ws.send("/pub/chats", {}, JSON.stringify({chatRoomId:roomId, userEmail:userEmail, imgCode:e.target.result})); //receiver:participant,
//$('#source').text( e.target.result );
};
//console.log(FR.readAsDataURL( input.files[0] ));
FR.readAsDataURL( input.files[0] ); // 이거 없으면 작동 안되나???
}
}
</script>
</html>
트러블 슈팅
문제점
해결방법
결과
글 잘봤습니다. 감사합니다.
혹시 이미지를 바이트코드로 어떻게 변환하나요? 보여주신 프론트 코드에는 바이트 코드로 변환하는 작업이 없는 것 같아서요