Socket s3 + 실시간 채팅 + 이미지전송

코딩을 합시다·2023년 2월 10일
0

build.gradle

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'

    // websocket
    implementation 'org.springframework.boot:spring-boot-starter-websocket'
    implementation group: 'com.googlecode.json-simple', name: 'json-simple', version: '1.1.1'

    // websocket html test
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

    // websocket aws
    implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'

    // file upload
    implementation group: 'commons-fileupload', name: 'commons-fileupload', version: '1.4'
    compileOnly 'commons-io:commons-io:2.11.0'
}

cloud.aws.stack.auto=false
cloud.aws.region.static=ap-northeast-2
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/{s3 Bucket 이름}

WebSocketConfig.java

package shop.dodotalk.dorundorun.message.config;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
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.standard.ServletServerContainerFactoryBean;
import shop.dodotalk.dorundorun.message.handler.SocketHandler;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer { // (1)
    @Autowired
    SocketHandler socketHandler;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(socketHandler, "/chating/{roomNumber}"); // (2)
    }

    @Bean
    public ServletServerContainerFactoryBean createWebSocketContainer() {
        ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean(); // (3)
        container.setMaxTextMessageBufferSize(500000); // (4)
        container.setMaxBinaryMessageBufferSize(500000); // (5)
        return container;
    }
}

우선 WebSocketConfig 부터 봐보자
(1) WebSocketConfigurer를 implements 해줌으로써 WebSocketConfigurer와 연관된 것들을 사용할 수 있게 했다
(2) addHandler를 사용하여 {roomNumber}에 따라 요청을 핸들링 할 수 있게 만들었다.
(3) ServletServerContainerFactoryBean을 추가해준다
(4) 텍스트 메세지의 최대 크기를 정해준다. (오류 방지용)
(5) 바이너리 메세지의 최대크기를 정해준다. (오류 방지용)


SocketHandler.java

package shop.dodotalk.dorundorun.message.handler;

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.util.*;

import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import org.springframework.web.socket.BinaryMessage;
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;

// img multipartfile switch test
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItem;
import java.io.FileInputStream;
import java.io.File;
import java.io.OutputStream;
import org.apache.commons.io.IOUtils;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.commons.CommonsMultipartFile;

// aws test
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;

@Component
public class SocketHandler extends TextWebSocketHandler { // (1)

    List<HashMap<String, Object>> sessions = new ArrayList<>(); // (2)
    static int roomIndex = -1;
    private String S3Bucket = "{s3 Bucket 이름}"; // Bucket 이름 aws img test
    @Autowired // aws img test
    AmazonS3Client amazonS3Client;

    // 파일 저장 경로
    private static final String FILE_UPLOAD_PATH = "src/main/resources/static";

    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { 
        //메시지 발송 // 구독 개념이 아닌 세션들을 찾아서 찾은 세션들한테만 보내주는 개념인듯함
        String msg = message.getPayload();
        JSONObject obj = jsonToObjectParser(msg);

        String roomNumber = (String) obj.get("roomNumber");
        String msgType = (String) obj.get("type");
        HashMap<String, Object> sessionMap = new HashMap<String, Object>();

        if (sessions.size() > 0) {
            for (int i=0; i<sessions.size(); i++) {
                String tempRoomNumber = (String) sessions.get(i).get("roomNumber");
                if (roomNumber.equals(tempRoomNumber)) {
                    sessionMap = sessions.get(i);
                    roomIndex = i;
                    break;
                }
            }

            if(!msgType.equals("file")) {
                // 해당 방에 있는 세션들에만 메세지 전송
                for (String sessionMapKey: sessionMap.keySet()) {
                    if(sessionMapKey.equals("roomNumber")) { // 다만 방번호일 경우에는 건너뛴다.
                        continue;
                    }

                    WebSocketSession webSocketSession = (WebSocketSession) sessionMap.get(sessionMapKey); // webSocketSession == StandardWebSocketSession[id=9b6f63f7-c1b1-9a09-5110-1fdff5973f86, uri=ws://localhost:8080/chating/abc]
                    webSocketSession.sendMessage(new TextMessage(obj.toJSONString()));
                }
            }
        }
    }

    @Override
    public void handleBinaryMessage(WebSocketSession session, BinaryMessage message) {
        //바이너리 메시지 발송
        ByteBuffer byteBuffer = message.getPayload(); // (3)
        String fileName = "file.jpg";

        File dir = new File(FILE_UPLOAD_PATH);
        if(!dir.exists()) {
            dir.mkdirs();
        }

        File Old_File=new File(FILE_UPLOAD_PATH + "/file.jpg"); // (4) 삭제할 파일을 찾아준다
        try {
            FileOutputStream fileOutputStream = new FileOutputStream(Old_File);
            fileOutputStream.close(); // (5) 파일 삭제시 FileOutputStream을 닫아줘야함
            Old_File.delete(); // 삭제
        } catch (Exception e) {
            e.printStackTrace();
        }

        File file = new File(FILE_UPLOAD_PATH, fileName); // (6) 파일을 새로 생성해준다

        FileOutputStream fileOutputStream = null;
        FileChannel fileChannel = null;
        try {
            byteBuffer.flip(); // (7) byteBuffer를 읽기 위해 세팅
            fileOutputStream = new FileOutputStream(file, true); // (8) 생성을 위해 OutputStream을 연다.
            fileChannel = fileOutputStream.getChannel(); // (9) 채널을 열고
            byteBuffer.compact(); // (10) 파일을 복사한다.
            fileChannel.write(byteBuffer); // (11) 파일을 쓴다.
        }catch(Exception e) {
            e.printStackTrace();
        }finally {
            try {
                if(fileOutputStream != null) {
                    fileOutputStream.close();
                }
                if(fileChannel != null) {
                    fileChannel.close();
                }
            }catch (IOException e) {
                e.printStackTrace();
            }
        }

        String imageurl = "";
        try { 
            FileItem fileItem = new DiskFileItem("mainFile", Files.probeContentType(file.toPath()), false, file.getName(), (int) file.length(), file.getParentFile()); // (12) File을 MultiPartFile로 변환하는 과정 우선 file을 DiskFileItem에 넣어준다

            try {
                InputStream input = new FileInputStream(file); // (13) 파일에서 바이트 파일로 읽을 수 있게 해줌
                OutputStream os = fileItem.getOutputStream(); // (14) OutputStream을 생성해준다
                IOUtils.copy(input, os); // (15) 복사
                // Or faster..
                // IOUtils.copy(new FileInputStream(file), fileItem.getOutputStream());
            } catch (IOException ex) {
                // do something.
            }

            MultipartFile multipartFile = new CommonsMultipartFile(fileItem); (16) File을 MultipartFile로 변환시키기

            String originalName = UUID.randomUUID().toString(); // aws s3 저장과정
            long multipartFileSize = multipartFile.getSize(); // aws s3 저장과정

            ObjectMetadata objectMetaData = new ObjectMetadata(); // aws s3 저장과정
            objectMetaData.setContentType(multipartFile.getContentType()); // aws s3 저장과정
            objectMetaData.setContentLength(multipartFileSize); // aws s3 저장과정

            amazonS3Client.putObject(new PutObjectRequest(S3Bucket, originalName, multipartFile.getInputStream(), objectMetaData).withCannedAcl(CannedAccessControlList.PublicRead)); // aws s3 저장과정

            imageurl = amazonS3Client.getUrl(S3Bucket, originalName).toString(); // aws s3 저장된 이미지 불러오기
            System.out.println("imageurl : " + imageurl);
        } catch (IOException e) {
            e.printStackTrace();
        }

        byteBuffer.position(0); // (17) 파일을 저장하면서 position값이 변경되었으므로 0으로 초기화한다.
        //파일쓰기가 끝나면 이미지를 발송한다.

        HashMap<String, Object> sessionMap = sessions.get(roomIndex);
        for(String sessionMapKey : sessionMap.keySet()) {
            if(sessionMapKey.equals("roomNumber")) {
                continue;
            }
            WebSocketSession webSocketSession = (WebSocketSession) sessionMap.get(sessionMapKey);
            try {
                JSONObject obj = new JSONObject();
                obj.put("type", "imgurl");
                obj.put("sessionId", session.getId());
                obj.put("imageurl", imageurl);

                webSocketSession.sendMessage(new TextMessage(obj.toJSONString())); //초기화된 버퍼를 발송한다.
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        //소켓 연결
        boolean sessionExist = false;

        String sessionUrl = session.getUri().toString();

        String roomNumber = sessionUrl.split("/chating/")[1];
        int roomIndex = -1;
        if (sessions.size() > 0) {
            for (int i=0; i<sessions.size(); i++) {
                String tempRoomNumber = (String) sessions.get(i).get("roomNumber");
                if (roomNumber.equals(tempRoomNumber)) {
                    sessionExist = true;
                    roomIndex = i;
                    break;
                }
            }
        }

        if (sessionExist) {
            HashMap<String, Object> sessionMap = sessions.get(roomIndex);
            sessionMap.put(session.getId(), session);
        } else {
            HashMap<String, Object> sessionMap = new HashMap<String, Object>();
            sessionMap.put("roomNumber", roomNumber);
            sessionMap.put(session.getId(), session);
            sessions.add(sessionMap);
        }

        //세션등록이 끝나면 발급받은 세션ID값의 메시지를 발송한다.
        JSONObject obj = new JSONObject();
        obj.put("type", "getId");
        obj.put("sessionId", session.getId());
        session.sendMessage(new TextMessage(obj.toJSONString()));

        super.afterConnectionEstablished(session);
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        //소켓 종료
        if(sessions.size() > 0) { //소켓이 종료되면 해당 세션값들을 찾아서 지운다.
            for (HashMap<String, Object> stringObjectHashMap : sessions) {
                stringObjectHashMap.remove(session.getId());
            }
        }
        super.afterConnectionClosed(session, status);
    }

    private static JSONObject jsonToObjectParser(String jsonStr) {
        JSONParser parser = new JSONParser();
        JSONObject obj = null;
        try {
            obj = (JSONObject) parser.parse(jsonStr);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return obj;
    }
}

우선 aws의 s3를 사용해서 이미지 파일을 저장했고 aws에 관한 내용은 설명하지 않겠다
(1) TextWebSocketHandler을 상속해준다.
(2) roomNumber별로 세션 저장
(3) BinaryMessage를 getPayload() 함수를 통해 ByteBuffer로 변환시켜준다
(4) 삭제할 파일을 찾아준다
(5) 파일을 삭제하기 위해선 FileOutputStream을 닫아줘야 한다
(6) 파일을 새로 생성해준다
(7) byteBuffer를 읽기 위해 세팅
(8) 생성을 위해 OutputStream을 연다
(9) 채널 오픈
(10) 파일을 복사한다
(11) 파일 작성
(12) File을 MultiPartFile로 변환하는 과정 우선 file을 DiskFileItem에 넣어준다
(13) 파일에서 바이트 파일로 읽을 수 있게 해줌
(14) OutputStream을 생성해준다
(15) 복사
(16) File을 MultipartFile로 변환시키기
(17) 파일을 저장하면서 position값이 변경되었으므로 0으로 초기화한다.


AwsConfig.java

package shop.dodotalk.dorundorun.message.config;

import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class TestAwsConfig {

    /**
     * Key는 중요정보이기 때문에 properties 파일에 저장한 뒤 가져와 사용하는 방법이 좋습니다.
     */
    @Value("${cloud.aws.credentials.access-key}")
    private String iamAccessKey = "IAM 생성할 때 확인했던 AccessKey 입력"; // IAM Access Key
    @Value("${cloud.aws.credentials.secret-key}")
    private String iamSecretKey = "IAM 생성할 때 확인했던 SecretKey 입력"; // IAM Secret Key
    //@Value("${cloud.aws.region.static}")
    private String region = "ap-northeast-2"; // Bucket Region

    @Bean
    public AmazonS3Client amazonS3Client() {
        BasicAWSCredentials basicAWSCredentials = new BasicAWSCredentials(iamAccessKey, iamSecretKey);
        return (AmazonS3Client) AmazonS3ClientBuilder.standard()
                .withRegion(region)
                .withCredentials(new AWSStaticCredentialsProvider(basicAWSCredentials))
                .build();
    }
}

ChatMessageController.java

package shop.dodotalk.dorundorun.message.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class ChatMessageTestController {
    @RequestMapping("/chat")
    public ModelAndView chat() {
        ModelAndView mv = new ModelAndView();
        mv.setViewName("chat");
        return mv;
    }
}

chat.html

<!DOCTYPE html>
<html>
<head>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
    <meta charset="UTF-8">
    <title>Chating</title>
    <style>
        *{
            margin:0;
            padding:0;
        }
        .container{
            width: 500px;
            margin: 0 auto;
            padding: 25px
        }
        .container h1{
            text-align: left;
            padding: 5px 5px 5px 15px;
            color: #FFBB00;
            border-left: 3px solid #FFBB00;
            margin-bottom: 20px;
        }
        .chating{
            background-color: #000;
            width: 500px;
            height: 500px;
            overflow: auto;
        }
        .chating .me{
            color: #F6F6F6;
            text-align: right;
        }
        .chating .others{
            color: #FFE400;
            text-align: left;
        }
        input{
            width: 330px;
            height: 25px;
        }
        #yourMsg{
            display: none;
        }
        .msgImg{
            width: 200px;
            height: 125px;
        }
        .clearBoth{
            clear: both;
        }
        .img{
            float: right;
        }
    </style>
</head>

<script type="text/javascript">
    var ws;

    function wsOpen(){
        //웹소켓 전송시 현재 방의 번호를 넘겨서 보낸다.
        ws = new WebSocket("ws://" + location.host + "/chating/"+$("#roomNumber").val());
        wsEvt();
    }

    function wsEvt() {
        ws.onopen = function(data){
            //소켓이 열리면 동작
        }

        ws.onmessage = function(data) {
            //메시지를 받으면 동작
            var msg = data.data;
            if(msg != null && msg.type != ''){
                //파일 업로드가 아닌 경우 메시지를 뿌려준다.
                var d = JSON.parse(msg);
                console.log(d);
                if(d.type == "getId"){
                    var si = d.sessionId != null ? d.sessionId : "";
                    if(si != ''){
                        $("#sessionId").val(si);
                    }
                }else if(d.type == "message"){
                    if(d.sessionId == $("#sessionId").val()){
                        $("#chating").append("<p class='me'>나 :" + d.msg + "</p>");
                    }else{
                        $("#chating").append("<p class='others'>" + d.userName + " :" + d.msg + "</p>");
                    }

                } else if (d.type == "imgurl") {
                    alert("여기가 실행되긴함???");
                    if(d.sessionId == $("#sessionId").val()){
                        $("#chating").append("<div class='me'><img class='msgImg' src="+d.imageurl+"></div><div class='clearBoth'></div>");
                        // $("#chating").append("<div class='me'>나 :" + <img class='msgImg' src="+imageurl+"></div><div class='clearBoth'></div>");
                    }else{
                        $("#chating").append("<div class='others'><img class='msgImg' src="+d.imageurl+"></div><div class='clearBoth'></div>");
                        //$("#chating").append("<p class='others'>" + d.userName + " :" + d.msg + "</p>");
                    }
                }
                else{
                    console.warn("unknown type!")
                }
            }else{
                //파일 업로드한 경우 업로드한 파일을 채팅방에 뿌려준다.
                var url = URL.createObjectURL(new Blob([msg]));
                $("#chating").append("<div class='img'><img class='msgImg' src="+url+"></div><div class='clearBoth'></div>");
            }
        }
    }

    function chatName(){
        var userName = $("#userName").val();
        if(userName == null || userName.trim() == ""){
            alert("사용자 이름을 입력해주세요.");
            $("#userName").focus();
        }else{
            wsOpen();
            $("#yourName").hide();
            $("#yourMsg").show();
        }
    }

    function send() {
        var option ={
            type: "message",
            roomNumber: $("#roomNumber").val(),
            sessionId : $("#sessionId").val(),
            userName : $("#userName").val(),
            msg : $("#chatting").val()
        }
        ws.send(JSON.stringify(option))
        $('#chatting').val("");
    }

    function fileSend(){
        var file = document.querySelector("#fileUpload").files[0];
        var fileReader = new FileReader();
        fileReader.readAsArrayBuffer(file);
        fileReader.onload = function() {
            // var param = {
            //     type: "file",
            //     file: file,
            //     roomNumber: $("#roomNumber").val(),
            //     sessionId : $("#sessionId").val(),
            //     msg : $("#chatting").val(),
            //     userName : $("#userName").val(),
            // }
            // ws.send(JSON.stringify(param)); //파일 보내기전 메시지를 보내서 파일을 보냄을 명시한다.

            // alert(this.result);
            // console.log(this.result);

            arrayBuffer = this.result;
            //console.log(arrayBuffer);

            //arrayBuffer2 = this.result;
            //console.log(arrayBuffer2);

            var param = {
                type: "file",
                file: file,
                roomNumber: $("#roomNumber").val(),
                sessionId : $("#sessionId").val(),
                msg : $("#chatting").val(),
                userName : $("#userName").val(),
            }
            ws.send(JSON.stringify(param));

            ws.send(arrayBuffer); //파일 소켓 전송
        };
    }
</script>
<body>
<div id="container" class="container">
    <h1>${roomName}의 채팅방</h1>
    <input type="text" id="sessionId" value="">
    <input type="text" id="roomNumber" value="abc">

    <div id="chating" class="chating">
    </div>

    <div id="yourName">
        <table class="inputTable">
            <tr>
                <th>사용자명</th>
                <th><input type="text" name="userName" id="userName"></th>
                <th><button onclick="chatName()" id="startBtn">이름 등록</button></th>
            </tr>
        </table>
    </div>
    <div id="yourMsg">
        <table class="inputTable">
            <tr>
                <th>메시지</th>
                <th><input id="chatting" placeholder="보내실 메시지를 입력하세요."></th>
                <th><button onclick="send()" id="sendBtn">보내기</button></th>
            </tr>
            <tr>
                <th>파일업로드</th>
                <th><input type="file" id="fileUpload"></th>
                <th><button onclick="fileSend()" id="sendFileBtn">파일올리기</button></th>
            </tr>
        </table>
    </div>
</div>
</body>
</html>

실시간 채팅 + s3 이미지 저장이 잘 되는것을 확인 할 수 있다

Postman에서도 잘 작동되는 것을 확인 할 수 있었다.


참고 : https://myhappyman.tistory.com/104

0개의 댓글