반응형

 

1장부터 보실분들은 아래 URL을 참고해주세요!

https://myhappyman.tistory.com/100?category=873296

 

SpringBoot - 스프링부트에서 채팅프로그램(소켓통신) 만들기-1(단순 채팅, 메시지 보내기)

이번엔 소켓통신을 통하여 채팅프로그램을 스프링부트에서 만들어보겠습니다. 간단하게 프로젝트를 생성부터 채팅방 생성 및 채팅하는 과정까지 만들어보겠습니다. 소켓통신을 사용한 채팅프로그램 만들기 스프링..

myhappyman.tistory.com

 

4장(방 구성하여 채팅하기)에 이어서 마지막으로 파일전송을 다뤄보겠습니다.

https://myhappyman.tistory.com/103?category=873296

 

SpringBoot - 스프링부트에서 채팅프로그램(소켓통신) 만들기-4(채팅방 만들기2)

1장부터 확인해보실분들은 아래 url을 확인해주세요. https://myhappyman.tistory.com/100 SpringBoot - 스프링부트에서 채팅프로그램(소켓통신) 만들기-1 이번엔 소켓통신을 통하여 채팅프로그램을 스프링부트에..

myhappyman.tistory.com

 

이번 장은 간단하게 파일을 전송할 수 있다정도만 봐주시면 될 것같습니다.

실제로 사용해야 한다면 제가 제공하는 예제의 형태로 구현하지 않을 것 같기 때문입니다.

 

 

웹소켓으로 파일 전송하기

BinaryMessage

처음에 웹 소켓을 시작하면서 서버단을 구성할 때, 구현체부분인 SockHandler부분을 구성할때 TextWebSocketHandler을 상속받고 메시지 타입에 따라 handleBinaryMessage 또는 handleTextMessage가 실행된다고 했는데, 지금까지 JSON형태의 String메시지만 전송하다보니 바이너리메시지는 다룰일이 없었습니다. 이번 장에서는 바이너리메시지를 사용하여 파일을 받고 서버에 저장도 하고 채팅방에 전송된 이미지를 표출하는부분까지 구성해보겠습니다.

 

 

Server단

@Override
public void handleBinaryMessage(WebSocketSession session, BinaryMessage message) {
}

SocketHandler에 추가할 메소드입니다. BinaryMessage의 데이터가 들어오면 해당 메소드가 실행됩니다.

 

 

SocketHandler.java

package com.psw.chating.handler;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
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;

@Component
public class SocketHandler extends TextWebSocketHandler {
	
	List<HashMap<String, Object>> rls = new ArrayList<>(); //웹소켓 세션을 담아둘 리스트 ---roomListSessions
	private static final String FILE_UPLOAD_PATH = "C:/test/websocket/";
	static int fileUploadIdx = 0;
	static String fileUploadSession = "";
	
	@Override
	public void handleTextMessage(WebSocketSession session, TextMessage message) {
		//메시지 발송
		String msg = message.getPayload(); //JSON형태의 String메시지를 받는다.
		JSONObject obj = jsonToObjectParser(msg); //JSON데이터를 JSONObject로 파싱한다.
		
		String rN = (String) obj.get("roomNumber"); //방의 번호를 받는다.
		String msgType = (String) obj.get("type"); //메시지의 타입을 확인한다.
		HashMap<String, Object> temp = new HashMap<String, Object>();
		if(rls.size() > 0) {
			for(int i=0; i<rls.size(); i++) {
				String roomNumber = (String) rls.get(i).get("roomNumber"); //세션리스트의 저장된 방번호를 가져와서
				if(roomNumber.equals(rN)) { //같은값의 방이 존재한다면
					temp = rls.get(i); //해당 방번호의 세션리스트의 존재하는 모든 object값을 가져온다.
					fileUploadIdx = i;
					fileUploadSession = (String) obj.get("sessionId");
					break;
				}
			}
			if(!msgType.equals("fileUpload")) { //메시지의 타입이 파일업로드가 아닐때만 전송한다.
				//해당 방의 세션들만 찾아서 메시지를 발송해준다.
				for(String k : temp.keySet()) { 
					if(k.equals("roomNumber")) { //다만 방번호일 경우에는 건너뛴다.
						continue;
					}
					
					WebSocketSession wss = (WebSocketSession) temp.get(k);
					if(wss != null) {
						try {
							wss.sendMessage(new TextMessage(obj.toJSONString()));
						} catch (IOException e) {
							e.printStackTrace();
						}
					}
				}
			}
		}
	}
	
	@Override
	public void handleBinaryMessage(WebSocketSession session, BinaryMessage message) {
		//바이너리 메시지 발송
		ByteBuffer byteBuffer = message.getPayload();
		String fileName = "temp.jpg";
		File dir = new File(FILE_UPLOAD_PATH);
		if(!dir.exists()) {
			dir.mkdirs();
		}
		
		File file = new File(FILE_UPLOAD_PATH, fileName);
		FileOutputStream out = null;
		FileChannel outChannel = null;
		try {
			byteBuffer.flip(); //byteBuffer를 읽기 위해 세팅
			out = new FileOutputStream(file, true); //생성을 위해 OutputStream을 연다.
			outChannel = out.getChannel(); //채널을 열고
			byteBuffer.compact(); //파일을 복사한다.
			outChannel.write(byteBuffer); //파일을 쓴다.
		}catch(Exception e) {
			e.printStackTrace();
		}finally {
			try {
				if(out != null) {
					out.close();
				}
				if(outChannel != null) {
					outChannel.close();
				}
			}catch (IOException e) {
				e.printStackTrace();
			}
		}
		
		byteBuffer.position(0); //파일을 저장하면서 position값이 변경되었으므로 0으로 초기화한다.
		//파일쓰기가 끝나면 이미지를 발송한다.
		HashMap<String, Object> temp = rls.get(fileUploadIdx);
		for(String k : temp.keySet()) {
			if(k.equals("roomNumber")) {
				continue;
			}
			WebSocketSession wss = (WebSocketSession) temp.get(k);
			try {
				wss.sendMessage(new BinaryMessage(byteBuffer)); //초기화된 버퍼를 발송한다.
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public void afterConnectionEstablished(WebSocketSession session) throws Exception {
		//소켓 연결
		super.afterConnectionEstablished(session);
		boolean flag = false;
		String url = session.getUri().toString();
		String roomNumber = url.split("/chating/")[1];
		int idx = rls.size(); //방의 사이즈를 조사한다.
		if(rls.size() > 0) {
			for(int i=0; i<rls.size(); i++) {
				String rN = (String) rls.get(i).get("roomNumber");
				if(rN.equals(roomNumber)) {
					flag = true;
					idx = i;
					break;
				}
			}
		}
		
		if(flag) { //존재하는 방이라면 세션만 추가한다.
			HashMap<String, Object> map = rls.get(idx);
			map.put(session.getId(), session);
		}else { //최초 생성하는 방이라면 방번호와 세션을 추가한다.
			HashMap<String, Object> map = new HashMap<String, Object>();
			map.put("roomNumber", roomNumber);
			map.put(session.getId(), session);
			rls.add(map);
		}
		
		//세션등록이 끝나면 발급받은 세션ID값의 메시지를 발송한다.
		JSONObject obj = new JSONObject();
		obj.put("type", "getId");
		obj.put("sessionId", session.getId());
		session.sendMessage(new TextMessage(obj.toJSONString()));
	}
	
	@Override
	public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
		//소켓 종료
		if(rls.size() > 0) { //소켓이 종료되면 해당 세션값들을 찾아서 지운다.
			for(int i=0; i<rls.size(); i++) {
				rls.get(i).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;
	}
}

BhandleBinaryMessage 메소드가 추가되었고, 매개변수 BinaryMessage의 데이터를 ByteBuffer로 받아서 파일을 저장하고, 현재 방에 존재하는 세션에게만 ByteBuffer데이터를 전송하는 예제입니다.

이번에 소켓 파일전송을 하면서 ByteBuffer를 처음 써봤는데, 생소해서 조금 구성하는데 어려움이 있었습니다. 로직에 문제가 있다면 지적해주시고 수정하도록 하겠습니다.

 

 

Client단

chat.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!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);
				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{
					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>");
			}
		}

		document.addEventListener("keypress", function(e){
			if(e.keyCode == 13){ //enter press
				send();
			}
		});
	}

	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.onload = function() {
			var param = {
				type: "fileUpload",
				file: file,
				roomNumber: $("#roomNumber").val(),
				sessionId : $("#sessionId").val(),
				msg : $("#chatting").val(),
				userName : $("#userName").val()
			}
			ws.send(JSON.stringify(param)); //파일 보내기전 메시지를 보내서 파일을 보냄을 명시한다.

		    arrayBuffer = this.result;
			ws.send(arrayBuffer); //파일 소켓 전송
		};
		fileReader.readAsArrayBuffer(file);
	}
</script>
<body>
	<div id="container" class="container">
		<h1>${roomName}의 채팅방</h1>
		<input type="hidden" id="sessionId" value="">
		<input type="hidden" id="roomNumber" value="${roomNumber}">
		
		<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>

파일전송을 할때, 이 파일은 어떤세션에서 넘겼고 어떤파일명을 넘겼는지 등에 대해 정보를 남길 수가 없어서 메시지를 한번 보내서 이제 이런형태의 메시지가 올것이라고 명시를 하고 파일을 넘겼습니다.

여러개의 채팅방이 구성되고 동시에 여러 소켓통신이 발생한다면 해당 로직은 정상적으로 동작하지 않고 다른방으로 전송될수도 있는 위험이 존재합니다.

오로지 파일 전송의 예제를 위해 구성되었음을 알려드립니다.

 

파일 올리기 버튼을 누르면 fileSend함수가 실행되면서 FileReader객체를 통해 파일을 버퍼로 읽고 전송처리해줍니다.

서버에서 파일을 전달받으면 다시 클라이언트로 byteBuffer데이터를 넘겨주는데 Blob객체를 통해 이미지로 파싱하여 채팅방에 표출합니다.

 

파일 업로드 제한, 용량 제한 이런부분은 처리하지 않았습니다.

 

동작  화면

파일이 서버로 전송되었고 특정 경로에 저장하였습니다.

 

채팅방에도 정상적으로 이미지가 출력되는걸 볼 수 있습니다.

반응형