반응형

MQTT 프로토콜은 IOT, M2M을 위한 프로토콜로 최근 IOT에서 많이 사용되는 프로토콜이라고 하여 간단하게 다뤄봤습니다.

 

Publisher와 Brocker, Subscriber 구조로 나뉘어있고 Publisher는 Topic을 발행하고 Subscriber는 특정 Topic을 감시 구독합니다. Brocker는 이를 중계하는 역할을 하며 한개의 Topic에 여러 Subscriber가 존재할 수 있는 구조를 가집니다.

 

여기서 Brocker로 ActiveMQ를 사용해보았습니다.

 

ActiveMQ는 위에서도 말했듯이 메시지 브로커로 오픈소스입니다.

 

dependency 추가를하여 사용하기 위해 Maven프로젝트로 구성하여 작성하였습니다.

 

mqttv3 - dependency추가

<dependency>
  <groupId>org.eclipse.paho</groupId>
  <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
  <version>1.2.0</version>
</dependency>

 

 

MqttService.java

package com.psw.mqtts.service;

public interface MqttService{
	public String connect(String connect_url, String clientId, String username, String password, String[] subscribe);
	public String sendMsg(String topic, String msg);
	public void disConnect();
}

interface로 연결 메시지발송 종료를 담당하는 메소드를 작성하였습니다.

MQTT.java에서 상속받아 사용할 것입니다.

 

 

MQTT.java

package com.psw.mqtts.utils;

import java.util.function.Consumer;
import java.util.function.Function;

import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;

import com.psw.mqtts.service.MqttService;

public class MQTT implements MqttCallback, MqttService{
	private String receiveMsg;
	private String inputMsg;
	private MqttClient mqttClient;
	
	private Consumer<MQTT> receiver;
	private Consumer<String> delivery;
	private Consumer<String> lost;
	
	final static Function<Object, Boolean> STRING_NVL = str -> str != null && !((String) str).trim().equals("") ? false : true;
	
	final static Function<Object, Boolean> STRING_ARR_NVL = str -> str != null && ((String[]) str).length > 0 ? true : false;
	
	public MQTT() {
		this.receiveMsg = null;
		this.inputMsg = null;
		this.mqttClient = null;
		this.receiver = null;
		this.delivery = null;
		this.lost = null;
	}

	public void setReceiver(Consumer<MQTT> receiver) {
		this.receiver = receiver;
	}

	
	public void setDelivery(Consumer<String> delivery) {
		this.delivery = delivery;
	}
	
	
	public void setLost(Consumer<String> lost) {
		this.lost = lost;
	}


	public String getInputMsg() {
		return inputMsg;
	}

	
	public void setInputMsg(String inputMsg) {
		this.inputMsg = inputMsg;
	}
	
	
	public String getReceiveMsg() {
		return receiveMsg;
	}
	
	
	public void setReceiveMsg(String receiveMsg) {
		this.receiveMsg = receiveMsg;
	}
	
	
	public MqttClient getMqttClient() {
		return mqttClient;
	}
	
	
	public void setMqttClient(MqttClient mqttClient) {
		this.mqttClient = mqttClient;
	}
	
	
	/**
	 * mqtt 연결처리 함수
	 * @param connect_url 연결정보
	 * @param clientId 연결 클라이언트 ID
	 * @param username 유저정보
	 * @param password 비밀번호
	 * @param subscribe 구독할 토픽 배열 //데이터가 없으면 구독을 하지 않음
	 * @return Map
	 */
	@Override
	public String connect(String connect_url, String clientId, String username, String password, String[] subscribe) {
		String res = "";
		if(STRING_NVL.apply(connect_url)) { return "연결정보를 입력해주세요.";}
		if(STRING_NVL.apply(clientId)) { return "클라이언트 ID를 입력해주세요.";}
		if(STRING_NVL.apply(username)) { return "유저정보를 입력해주세요.";}
		if(STRING_NVL.apply(password)) { return "비밀번호를 입력해주세요.";}
		
		if(mqttClient == null) {
			try {
				mqttClient = new MqttClient(connect_url, clientId);
				MqttConnectOptions connOpts = new MqttConnectOptions();
				connOpts.setUserName(username);
				connOpts.setPassword(password.toCharArray());
				
				mqttClient.connect(connOpts);
    	        mqttClient.setCallback(this);
    	        if(STRING_ARR_NVL.apply(subscribe)) {
    	        	mqttClient.subscribe(subscribe);
    	        }
    	        setMqttClient(mqttClient);
    	        res = "정상정으로 연결 되었습니다.";
			} catch (MqttException e) {
				e.printStackTrace();
				return e.getMessage();
			}
		}else {
			res = "기존에 연결되어 있는 정보가 존재합니다.";
		}
		return res;
	}
	
	
	/**
	 * 메시지 전송 함수
	 * @param topic 바라볼 토픽
	 * @param msg 전송할 메시지
	 * @return String
	 */
	@Override
	public String sendMsg(String topic, String msg) {
		String res = "";
		if(STRING_NVL.apply(topic)) { return "전송하고자 하는 Topic을 입력하세요."; }
		if(STRING_NVL.apply(msg)) { return "전송하고자 하는 메시지를 입력하세요."; }
		if(STRING_NVL.apply(topic)) { return "연결하고자 하는 토픽 "; }
		
		if(mqttClient != null && !STRING_NVL.apply(msg)) {
			if(msg.equals("exit")) {
				disConnect();
			}else {
				try {
	        		MqttMessage message = new MqttMessage(msg.getBytes());
	        		mqttClient.publish(topic, message);
	        		res = "1";
				} catch (MqttException e) {
					e.printStackTrace();
					res = e.getMessage();
				}
			}
    	}else {
    		res = "activeMq 연결을 먼저 해주세요.";
    	}
		return res;
	}
	
	@Override
	public void disConnect() {
		if(mqttClient != null) {
			try {
				mqttClient.disconnect();
				mqttClient.close();
			} catch (MqttException e) {
				e.printStackTrace();
			}
		}
	}

	@Override
	public void connectionLost(Throwable cause) {
		if(lost != null) {
			lost.accept("연결이 종료되었습니다.");
		}
	}

	@Override
	public void messageArrived(String topic, MqttMessage message) throws Exception {
		String res = "topic : " + topic + "  ||  message : " + message;
		setReceiveMsg(res);
		if(receiver != null) {
			receiver.accept(this);
		}
	}

	@Override
	public void deliveryComplete(IMqttDeliveryToken token) {
		if(delivery != null) {
			delivery.accept("메시지가 정상적으로 전달되었습니다.");
		}
	}
}

의존성 추가로 생성된 MqttCallback과 MqttService를 상속받아 작성하였습니다.

receiver, delivery, lost Consumer들은 콜백함수로 구현하여 각각 사용부분에서 메소드가 동작하면 데이터를 받아와서 동작시키도록 처리하였습니다.(connectionLost, messageArrived, deliveryComplete)

 

connect메소드에서는 subscribe배열이 존재하는지 체크하고 존재한다면 구독을 할 수 있도록 설정하였습니다.

 

sendMsg메소드에서는 입력으로 들어오는 문자열값이 exit가 들어오면 종료메소드를 실행하도록하여 연결을 종료하고 닫도록 처리하였고, 그 외에는 입력 topic으로 메시지를 전달합니다.

 

 

App.java

package com.psw.mqtts.main;

import java.util.Scanner;
import java.util.function.Consumer;

import org.eclipse.paho.client.mqttv3.MqttClient;

import com.psw.mqtts.utils.MQTT;

public class App{
	
	private static String input = "";
	
    public static void main( String[] args ){
    	String res = "";
    	MQTT mqtt = new MQTT();
    	
    	Consumer<MQTT> recv = (cons)->{
    		System.out.println("Received message // " +  cons.getReceiveMsg());
    	};
    	
    	Consumer<String> deli = (cons)->{
    		System.out.println(cons);
    	};
    	
    	Consumer<String> lost = (cons)->{
    		System.out.println(cons);
    	};
    	
    	mqtt.setReceiver(recv); //메시지가 수신되면 동작
    	mqtt.setDelivery(deli); //메시지가 정상적으로 전달되면 동작
    	mqtt.setLost(lost); //연결이 끊기면 동작
    	
    	
    	// 연결정보
    	String connect_url = "tcp://localhost:61616"; // "tcp://localhost:61616"
    	String clientId = "psw_test"; 
    	String username = "admin"; 
    	String password = "admin";
    	String[] subscribe = {"psw", "kjg"};
    	
    	//activemq 연결
    	res = mqtt.connect(connect_url, clientId, username, password, subscribe);
    	System.out.println(res); //연결 메시지 출력
    	
    	//연결 후 클라이언트 정보를 가져옴
    	MqttClient mqttClient = mqtt.getMqttClient();
    	if(mqttClient != null) { //정상적으로 접근되어 클라이언트가 비어있지 않다면 메시지 발송 진행
    		Scanner sc = new Scanner(System.in); //입력을 위해 Scanner 객체 생성
        	while(input != null && !input.equals("exit")) { //exit 문자열 입력이 들어올 때까지 동작
        		System.out.println("발송하실 메시지를 입력하세요. 연결을 종료하시려면 \"exit\"를 입력하세요");
        		input = sc.nextLine(); //입력 받기
        		mqtt.setInputMsg(input); //입력 데이터 저장
        		mqtt.sendMsg("psw", mqtt.getInputMsg()); //메시지 전송하고자 하는 topic으로 발송
        	}
        	
        	//종료가 되면 Scanner 닫기
        	try {
        		sc.close();
        	}catch(Exception e) {
        		e.printStackTrace();
        	}
    	}else {
    		System.out.println("연결 할 수 없습니다.");
    	}
    	
    	//연결 종료
    	mqtt.connectionLost(new Throwable());
    	
    	//시스템 종료
    	System.exit(0);
    }
}

사용예제입니다.

 

연결 후 메시지를 출력하고 Scanner를 통해 입력받은 문자열을 특정 topic으로 전달합니다.

subscribe로 psw, kjg를 처리하여 해당 topic에 메시지가 들어오면 messageArrived메소드가 실행되고 콜백함수로 인해 receiver가 동작하면서 구독한 메시지를 출력합니다.

반응형
반응형

자바 interface로 구현된 라이브러리를 상속받아 사용하던 중 특정 메소드가 수신받으면 동작하는 메소드가 존재하였습니다.

 

메인문에서 해당 메소드가 동작하는 것을 감지하고 싶었고, 찾아본 내용은 콜백함수였습니다.

그 동안 사용한 interface의 특징을 제대로 파악하지 못하고 기능을 제대로 사용하지 못하고 있다는걸 느끼게 되었습니다.

 

 

콜백을 공부하면서 CALLER(호출자), CALLEE(피호출자)로 불리는 단어가 존재하였는데, 해당 단어를 가지고 정리를 하면 다음과 같습니다.

일반적인 호출 CALLER가 CALLEE에게 요청하여 수행

콜백 CALLEE가 CALLER에게 요청으로 이해하시면 될 것 같습니다.

 

 


관련 예제를 확인해보겠습니다. Scanner를 통해 메시지를 입력받고 콜백함수를 호출해보겠습니다.

 

CallBack

Interface를 활용하여 콜백 구현하기

Callee.java

package com.psw.callback;

import java.util.Scanner;

public class Callee {
	private String msg;
	private CallBack callback;
	
	@FunctionalInterface
	public interface CallBack{
		public void onGetMessage(Callee callee);
	}
	
	public Callee() {
		this.msg = "";
		this.callback = null;
	}

	public String getMsg() {
		return msg;
	}

	public void setCallback(CallBack callback) {
		this.callback = callback;
	}
	
	public void onInputMessage() {
		Scanner scanner = new Scanner(System.in);
		this.msg = ""; //초기화
		System.out.print("메시지를 입력하세요 : ");
		this.msg = scanner.nextLine();
		
		if(this.callback != null) { //callback처리
			this.callback.onGetMessage(Callee.this);
		}
	}
}

 

CallBack.java

package com.psw.callback;

public class CallBack {

	public static void main(String[] args) {
		Callee callee = new Callee();
		callee.setCallback(new Callee.CallBack() {
			
			@Override
			public void onGetMessage(Callee callee) {
				//callback
				System.out.println("입력받은 메시지 >" + callee.getMsg());
			}
		});
		
		for(int i=0; i<5; i++){ //메시지 발송을 5번까지 보낸다
			callee.onInputMessage();
		}
	}

}

 

callback이 null이 아니면 onGetMessage를 수행하고 전달합니다.

메시지를 입력해보겠습니다.

잘받아지는 모습!

메시지를 입력하는 함수만 동작시키고 있지만 override 처리한 onGetMessage에서 콜백 동작을 하면서 정상적으로 메시지를 받는 모습을 볼 수 있습니다.

 

 


Functional Interface 방식으로 구현하기

동일한 동작이지만 java8에서 추가된 함수형 인터페이스를 사용한 표현방법을 알아보겠습니다.

 

Callee.java

package com.psw.callback;

import java.util.Scanner;
import java.util.function.Consumer;


public class Callee {
	private String msg;
	private Consumer<Callee> callback;
	
	public Callee() {
		this.msg = "";
		this.callback = null;
	}

	public String getMsg() {
		return msg;
	}

	public void setCallback(Consumer<Callee> callback) {
		this.callback = callback;
	}

	public void onInputMessage() {
		Scanner scanner = new Scanner(System.in);
		this.msg = ""; //초기화
		System.out.print("메시지를 입력하세요 : ");
		this.msg = scanner.nextLine();
		
		if(this.callback != null) { //callback처리
			this.callback.accept(Callee.this);
		}
	}
}

 

CallBack.java

package com.psw.callback;

public class CallBack {

	public static void main(String[] args) {
		Callee callee = new Callee();
		callee.setCallback((arg)->{
			System.out.println(arg.getMsg());
		});
		
		for(int i=0; i<5; i++){ //메시지 발송을 5번까지 보낸다
			callee.onInputMessage();
		}
	}
}

 

동일한 동작이지만 정의부분이나 사용부분이 많이 간결해진 것을 볼 수 있습니다.

리턴값이 없는 메소드였기때문에 Consumer를 사용하여 정의하였고, 람다식을 통해 인자값만 넘겨주어 바로 표현식으로 sysout처리를 하였습니다.

 

동일하게 잘 작동!

 

반응형
반응형

납품한 스프링 프로젝트 중 오류가 발생하는 이슈가 있었습니다.

 

저장을 할때 마다 오류가 발생한다는 메시지가 출력되었고, 소스를 분석하는데, 꽤 오랜시간을 소요되었습니다. XSS악성 스크립트를 막고자 인터셉터 설정을 하였고 모든 파라미터를 확인해서 특정 키워드가 존재하면 동작을 멈추게 하도록 설정해뒀는데, 해당 키워드에서 계속 걸려서 진행이 불가한 상황이였습니다.

 

해당 스크립트에 걸려도 괜찮고 저장할 수 있는 로직을 구성하기 위해 자바스크립트단에서 문자열데이터를 utf8 바이트 배열로 변환하고 컨트롤러단에서는 바이트 배열을 바이트로 형변환 후 new String메소드를 통해 문자열로 변환시키는 방식으로 해결하였습니다.

 

문자열데이터를 바이트배열로 변경하고 자바에서 바이트배열을 문자열로 변경하기

 

javascript단

function stringToUtf8Bytes(text){
    var result = [] ;
    if(text != null){
    	for (i = 0; i<text.length; i++) {
            var c = text.charCodeAt(i);
            if (c <= 0x7f) {
                result.push(c);
            } else if (c <= 0x07ff) {
                result.push(((c >> 6) & 0x1F) | 0xC0);
                result.push((c & 0x3F) | 0x80);
            } else {
                result.push(((c >> 12) & 0x0F) | 0xE0);
                result.push(((c >> 6) & 0x3F) | 0x80);
                result.push((c & 0x3F) | 0x80);
            }
        }
    }
    return result;
}

위의 함수 파라미터에 형변환을 할 문자열을 넣으면 utf8 바이트 배열로 변환됩니다.

변환 된 데이터를 컨트롤러에 넘기시면 됩니다. (ajax또는 form태그 데이터)

 

java단

@ResponseBody
@RequestMapping(value="addMember", method = RequestMethod.POST)
public String addMember(HttpServletRequest request, HttpServletResponse response) {
    String email = request.getParameter("email");
    //변환 시작
    String[] tmpEmailArr = email != null ? email.split(",") : null;
    String convEMail = "";
    if(tmpEmailArr != null) {
    	byte[] bta = new byte[tmpEmailArr.length];
    	for(int i=0;i<tmpEmailArr.length;i++) {
    		bta[i] = Byte.parseByte(tmpEmailArr[i]);
    	}
    	convEMail = new String(bta, StandardCharsets.UTF_8);
    }
    email = convEMail;
    //변환 완료
    
    ...
    //이후 동작 처리

파라미터로 받은 데이터는 문자열 덩어리로 들어와 있을텐데 (ex: 100, 105, 205, 88, 77 ...)

,을 통해 split처리 후 각각 데이터를 byte배열에 담아준 후 new String메소드를 통해 문자열로 파싱해줍니다.

 

 

바이트배열을 문자열로 변경

test@naver.com이라는 문자열을 자바스크립트단에서 변환하여

116, 101, 115, 116, 64, 110, 97, 118, 101, 114, 46, 99, 111, 109로 변환했고

 

자바에서 해당 데이터를 받아서 파싱하는것을 볼 수 있습니다.

반응형
반응형

 

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객체를 통해 이미지로 파싱하여 채팅방에 표출합니다.

 

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

 

동작  화면

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

 

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

반응형
반응형

1장부터 확인해보실분들은 아래 url을 확인해주세요.

 

https://myhappyman.tistory.com/100

 

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

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

myhappyman.tistory.com

 

방을 만들었고, 채팅방으로 넘어가는 로직까지 구성하였지만 여전히 채팅시 모든 소켓의 데이터가 서로에게 전송될 겁니다. 이제 방정보를 기준으로 데이터를 저장하여 구분하고 방별로 채팅이 되는 프로그램으로 업그레이드 해보겠습니다.

 

server단

구현체를 등록하였던 url정보를 방번호에 따라 구분될 수 있도록 변경해줍니다.

webSocketConfig.java

package com.psw.chating.config;

import org.springframework.beans.factory.annotation.Autowired;
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 com.psw.chating.handler.SocketHandler;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer{

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

url에서 chating/ 이후 들어오는 {roomNumber}값은 앞으로 방을 구분하는 값이 될 예정입니다.

 

 

SocketHandler.java

package com.psw.chating.handler;

import java.io.IOException;
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.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 {
	
	//HashMap<String, WebSocketSession> sessionMap = new HashMap<>(); //웹소켓 세션을 담아둘 맵
	List<HashMap<String, Object>> rls = new ArrayList<>(); //웹소켓 세션을 담아둘 리스트 ---roomListSessions
	
	@Override
	public void handleTextMessage(WebSocketSession session, TextMessage message) {
		//메시지 발송
		String msg = message.getPayload();
		JSONObject obj = jsonToObjectParser(msg);
		
		String rN = (String) obj.get("roomNumber");
		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값을 가져온다.
					break;
				}
			}
			
			//해당 방의 세션들만 찾아서 메시지를 발송해준다.
			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();
					}
				}
			}
		}
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public void afterConnectionEstablished(WebSocketSession session) throws Exception {
		//소켓 연결
		super.afterConnectionEstablished(session);
		boolean flag = false;
		String url = session.getUri().toString();
		System.out.println(url);
		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;
	}
}

구현체 부분의 handler입니다. 텍스트로 설명은 한계가 있어서 주석을 달아두었습니다.

변경 된 부분을 나열하겠습니다.

1. 세션을 관리하던 map객체에서 list, hashmap형태로 변경되었습니다. hashmap의 value자료형도 WebSocketSession에서 Object형으로 변경되었습니다.

2. 세션을 저장할때, 현재 접근한 방의 정보가 있는지 체크하고 존재하지 않으면 방의 번호를 입력 후 세션들을 담아주는 로직으로 변경되었습니다.

3. 마찬가지로 종료시에도 list컬랙션을 순회하면서 해당 키값의 세션들을 삭제하도록 변경되었습니다.

4. 메시지를 발송하는 handleTextMessage메소드에서는 현재의 방번호를 가져오고 방정보+세션정보를 관리하는 rls리스트 컬랙션에서 데이터를 조회한 후에 해당 Hashmap을 임시 맵에 파싱하여 roomNumber의 키값을 제외한 모든 세션키값들을 웹소켓을 통해 메시지를 보내줍니다.

 

말로 정리가 어려운데, 결국 4번에서 하는 처리로 인해 방구분을 하고 해당 방에 존재하는 session값들에게만 메시지를 발송하여 구분이 됩니다.

 

 

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;
		}
	</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.trim() != ''){
				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!")
				}
			}
		}

		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("");
	}
</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>
			</table>
		</div>
	</div>
</body>
</html>

마찬가지로 변경된 점을 나열하겠습니다.

1. 방의 번호값을 모델에서 저장한 값을 jstl을 통해 파싱합니다. (id=roomNumber)

2. 접속한 방의 이름을 모델에서 저장한 값을 가져와서 채팅방 이름을 추가해줍니다. ${roomName}

3. 메시지를 보내는 send함수에 roomNumber 키 값이 추가되었습니다.

방의 번호를 보내줌으로써 소켓서버는 어느방에서 메시지를 보냈는지 구분합니다.

 

 

동작 화면

채팅방 구현

화면의 짤이 작아서 잘 안보이지만 방1, 방1에 접근한 1번, 2번 브라우저만 서로 메시지가 오고

방3은 혼자 채팅되는 모습을 볼 수 있습니다.

 

방3는 메시지가 안온다.

반응형
반응형

1장부터 차례대로 진행하고 싶은 분들은 아래 url을 이용해주세요.

 

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

 

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

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

myhappyman.tistory.com

 

2장에 이어서 3장입니다. 이번엔 채팅방을 개설하고 채팅방별로 채팅을 구분해보겠습니다.

 

먼저 채팅방을 구성하기 위해 서버단에 뷰페이지 작성과 방을 관리하는 리스트배열 VO객체를 생성하고 제어해보겠습니다. 이번장에서는 소켓관련된 내용이 없을 예정이며, 다음 장에서 방을 생성 후 채팅방에서 접속한 방의 정보를 통해 소켓 데이터를 구분 처리하도록 하겠습니다.

 

Server단

VO 객체 생성하기

Room.java

package com.psw.chating.vo;

public class Room {
	int roomNumber;
	String roomName;
	
	public int getRoomNumber() {
		return roomNumber;
	}
	public void setRoomNumber(int roomNumber) {
		this.roomNumber = roomNumber;
	}
	public String getRoomName() {
		return roomName;
	}
	public void setRoomName(String roomName) {
		this.roomName = roomName;
	}
	
	@Override
	public String toString() {
		return "Room [roomNumber=" + roomNumber + ", roomName=" + roomName + "]";
	}	
}

방의 정보를 담아둘 Room객체를 생성하였습니다.

 

 

뷰페이지 작성 및 제어하기

MainController.java

package com.psw.chating.controller;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;

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

import com.psw.chating.vo.Room;

@Controller
public class MainController {
	
	List<Room> roomList = new ArrayList<Room>();
	static int roomNumber = 0;
	
	@RequestMapping("/chat")
	public ModelAndView chat() {
		ModelAndView mv = new ModelAndView();
		mv.setViewName("chat");
		return mv;
	}
	
	/**
	 * 방 페이지
	 * @return
	 */
	@RequestMapping("/room")
	public ModelAndView room() {
		ModelAndView mv = new ModelAndView();
		mv.setViewName("room");
		return mv;
	}
	
	/**
	 * 방 생성하기
	 * @param params
	 * @return
	 */
	@RequestMapping("/createRoom")
	public @ResponseBody List<Room> createRoom(@RequestParam HashMap<Object, Object> params){
		String roomName = (String) params.get("roomName");
		if(roomName != null && !roomName.trim().equals("")) {
			Room room = new Room();
			room.setRoomNumber(++roomNumber);
			room.setRoomName(roomName);
			roomList.add(room);
		}
		return roomList;
	}
	
	/**
	 * 방 정보가져오기
	 * @param params
	 * @return
	 */
	@RequestMapping("/getRoom")
	public @ResponseBody List<Room> getRoom(@RequestParam HashMap<Object, Object> params){
		return roomList;
	}
	
	/**
	 * 채팅방
	 * @return
	 */
	@RequestMapping("/moveChating")
	public ModelAndView chating(@RequestParam HashMap<Object, Object> params) {
		ModelAndView mv = new ModelAndView();
		int roomNumber = Integer.parseInt((String) params.get("roomNumber"));
		
		List<Room> new_list = roomList.stream().filter(o->o.getRoomNumber()==roomNumber).collect(Collectors.toList());
		if(new_list != null && new_list.size() > 0) {
			mv.addObject("roomName", params.get("roomName"));
			mv.addObject("roomNumber", params.get("roomNumber"));
			mv.setViewName("chat");
		}else {
			mv.setViewName("room");
		}
		return mv;
	}
}

방으로 접근시킬 뷰페이지 room()메소드와 방을 생성하는 createRoom()메소드 방정보를 가져오는 getRoom()메소드를 작성하였습니다.

 

현재 프로젝트는 DB에 데이터를 담아놓거나, 파일등에 저장하는게 아니므로 방의 정보를 담아둘 List<Room>컬랙션을 생성하였고, 메소드에 따라 방을 생성하거나 방의 정보를 가져오도록 처리하였습니다.

나중에 찾아볼 room.jsp에서는 접속버튼에 따라 채팅방을 이동시킬 예정인데, moveChating()메소드에서는 전달받은 파라미터값으로 방이 생성되었는지 필터함수로 체크후 존재하는 방이면 해당방으로 이동시켜주고, 강제로 url을 바꾼 값이면 이동하지 못하도록 처리하였습니다.

 

 

Client단

방을 생성하는 jsp 구성

room.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>Room</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;
		}
		.roomContainer{
			background-color: #F6F6F6;
			width: 500px;
			height: 500px;
			overflow: auto;
		}
		.roomList{
			border: none;
		}
		.roomList th{
			border: 1px solid #FFBB00;
			background-color: #fff;
			color: #FFBB00;
		}
		.roomList td{
			border: 1px solid #FFBB00;
			background-color: #fff;
			text-align: left;
			color: #FFBB00;
		}
		.roomList .num{
			width: 75px;
			text-align: center;
		}
		.roomList .room{
			width: 350px;
		}
		.roomList .go{
			width: 71px;
			text-align: center;
		}
		button{
			background-color: #FFBB00;
			font-size: 14px;
			color: #000;
			border: 1px solid #000;
			border-radius: 5px;
			padding: 3px;
			margin: 3px;
		}
		.inputTable th{
			padding: 5px;
		}
		.inputTable input{
			width: 330px;
			height: 25px;
		}
	</style>
</head>

<script type="text/javascript">
	var ws;
	window.onload = function(){
		getRoom();
		createRoom();
	}

	function getRoom(){
		commonAjax('/getRoom', "", 'post', function(result){
			createChatingRoom(result);
		});
	}
	
	function createRoom(){
		$("#createRoom").click(function(){
			var msg = {	roomName : $('#roomName').val()	};

			commonAjax('/createRoom', msg, 'post', function(result){
				createChatingRoom(result);
			});

			$("#roomName").val("");
		});
	}

	function goRoom(number, name){
		location.href="/moveChating?roomName="+name+"&"+"roomNumber="+number;
	}

	function createChatingRoom(res){
		if(res != null){
			var tag = "<tr><th class='num'>순서</th><th class='room'>방 이름</th><th class='go'></th></tr>";
			res.forEach(function(d, idx){
				var rn = d.roomName.trim();
				var roomNumber = d.roomNumber;
				tag += "<tr>"+
							"<td class='num'>"+(idx+1)+"</td>"+
							"<td class='room'>"+ rn +"</td>"+
							"<td class='go'><button type='button' onclick='goRoom(\""+roomNumber+"\", \""+rn+"\")'>참여</button></td>" +
						"</tr>";	
			});
			$("#roomList").empty().append(tag);
		}
	}

	function commonAjax(url, parameter, type, calbak, contentType){
		$.ajax({
			url: url,
			data: parameter,
			type: type,
			contentType : contentType!=null?contentType:'application/x-www-form-urlencoded; charset=UTF-8',
			success: function (res) {
				calbak(res);
			},
			error : function(err){
				console.log('error');
				calbak(err);
			}
		});
	}
</script>
<body>
	<div class="container">
		<h1>채팅방</h1>
		<div id="roomContainer" class="roomContainer">
			<table id="roomList" class="roomList"></table>
		</div>
		<div>
			<table class="inputTable">
				<tr>
					<th>방 제목</th>
					<th><input type="text" name="roomName" id="roomName"></th>
					<th><button id="createRoom">방 만들기</button></th>
				</tr>
			</table>
		</div>
	</div>
</body>
</html>

최초 페이지 접근시 비동기 ajax를 통해 방의 정보를 가져오고 접속요청한 버튼에 따라 서버에서 검증후 결과에 따라 페이지가 이동되는 jsp입니다.

 

채팅방

 

소켓관련된 jsp가 아니므로 자세한 설명은 생략하겠습니다.

다음장에서는 작성한 room.jsp의 데이터를 가지고 방정보를 저장 후 소켓통신시 해당 데이터들을 활용하여 구분처리하도록 하겠습니다.

반응형
반응형

스프링부트에서 소켓통신을 통한 채팅프로그램 만들기 2번째입니다.

 

이전까지의 소스는 아래 URL에서 참고해주세요.

https://myhappyman.tistory.com/100

 

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

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

myhappyman.tistory.com

 

 

 

1장에서는 단순하게 String 메시지 자체를 보냈는데, 이번엔 JSON형태로 메시지를 보내고 서버에서도 JSON형태의 메시지를 파싱하여 구분처리를 해보도록 하겠습니다.

추가적으로 내가보낸 메시지와 상대방을 구분해보겠습니다.

채팅구분처리


Server단

simple json 라이브러리 추가

pom.xml

<!-- json simple  -->
<dependency>
  <groupId>com.googlecode.json-simple</groupId>
  <artifactId>json-simple</artifactId>
  <version>1.1.1</version>
</dependency>
<!-- json simple  -->

pom.xml에 json파싱을 위해 json-simple 라이브러리를 추가합니다.

 

 

jsonToObjectParser 함수 추가

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;
}

이전에 생성하였던 SocketHandler.java에 JSON파일이 들어오면 파싱해주는 함수를 추가하였습니다.

json형태의 문자열을 파라미터로 받아서 SimpleJson의 파서를 활용하여 JSONObject로 파싱처리를 해주는 함수입니다.

 

 

핸들러 로직 추가

SocketHandler.java

package com.psw.chating.handler;

import java.util.HashMap;

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.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 {
	
	HashMap<String, WebSocketSession> sessionMap = new HashMap<>(); //웹소켓 세션을 담아둘 맵
	
	@Override
	public void handleTextMessage(WebSocketSession session, TextMessage message) {
		//메시지 발송
		String msg = message.getPayload();
		JSONObject obj = jsonToObjectParser(msg);
		for(String key : sessionMap.keySet()) {
			WebSocketSession wss = sessionMap.get(key);
			try {
				wss.sendMessage(new TextMessage(obj.toJSONString()));
			}catch(Exception e) {
				e.printStackTrace();
			}
		}
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public void afterConnectionEstablished(WebSocketSession session) throws Exception {
		//소켓 연결
		super.afterConnectionEstablished(session);
		sessionMap.put(session.getId(), session);
		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 {
		//소켓 종료
		sessionMap.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;
	}
}

1. 소켓이 연결되면 동작하는 afterConnectionEstablished() 메소드부분에 로직이 추가되었습니다.

생성된 세션을 저장하면 발신메시지의 타입은 getId라고 명시 후 생성된 세션ID값을 클라이언트단으로 발송합니다.

클라이언트단에서는 type값을 통해 메시지와 초기 설정값을 구분할 예정입니다.

 

2. 메시지 전송시 JSON파싱을 위해 message.getPayload()를 통해 받은 문자열을 만든 함수 jsonToObjectParser에 넣어서 JSONObject값으로 받아서 강제 문자열 형태로 보내주는부분이 추가되었습니다.

 

 

Client 단

이제 발송하는 chat.jsp를 수정하겠습니다.

 

클라이언트단 뷰페이지 추가 및 json형태로 요청 및 파싱하기

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;
		}
	</style>
</head>

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

	function wsOpen(){
		ws = new WebSocket("ws://" + location.host + "/chating");
		wsEvt();
	}
		
	function wsEvt() {
		ws.onopen = function(data){
			//소켓이 열리면 동작
		}
		
		ws.onmessage = function(data) {
			//메시지를 받으면 동작
			var msg = data.data;
			if(msg != null && msg.trim() != ''){
				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!")
				}
			}
		}

		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",
			sessionId : $("#sessionId").val(),
			userName : $("#userName").val(),
			msg : $("#chatting").val()
		}
		ws.send(JSON.stringify(option))
		$('#chatting').val("");
	}
</script>
<body>
	<div id="container" class="container">
		<h1>채팅</h1>
		<input type="hidden" id="sessionId" value="">
		
		<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>
			</table>
		</div>
	</div>
</body>
</html>

 

 

아래는 chat.jsp의 로직 설명입니다.


css부분은 따로 설정하진 않겠습니다.

1. html 소스부분은 현재의 세션값을 저장해놓기 위해 sessionId가 ID인 input태그를 추가하였습니다.

2. send() 함수의 발송하기전에 단순 String데이터가 아닌 obj값으로 값을 세팅하고 JSON형태로 발신처리로 변경되었습니다.

메시지를 보낼땐 type값을 message로 구분하여 발송합니다.

 

3. Socket.onmessage의 메시지 전달시 받는 로직이 변경되었습니다.

서버에서도 데이터를 JSON형태로 전달해주기 때문에 받은 데이터를 JSON.parse메소드를 활용하여 파싱을 합니다.

파싱한 객체의 type값을 확인하여 getId값이면 초기 설정된 값이므로 채팅창에 값을 입력하는게 아니라 추가한 태그 sessionId에 값을 세팅해줍니다.

id값은 소켓이 종료되기 전까지 자기자신을 구분할 수 있는 session값이 될 예정입니다.

 

4. type이 message인 경우엔 채팅이 발생한 경우입니다.

상대방과 자신을 구분하기 위해 여기서 sessionId값을 사용합니다.

최초 이름을 입력하고 연결되었을때, 발급받은 sessionId값을 비교하여 같다면 자기 자신이 발신한 메시지이므로 오른쪽으로 정렬하는 클래스를 처리하고 메시지를 출력합니다.

비교하여 같지 않다면 타인이 발신한 메시지이므로 왼쪽으로 정렬하는 클래스를 처리하고 메시지를 출력합니다.

 

 

다음장에서는 방구분 처리를 해보겠습니다.

 

 

반응형
반응형

이번엔 소켓통신을 통하여 채팅프로그램을 스프링부트에서 만들어보겠습니다.

 

프로젝트를 생성해서 단순한 채팅방과 추가적으로 방생성에 따른 채팅 구분 등의 과정까지 만들어보겠습니다.

 

 

소켓통신을 사용한 채팅프로그램 만들기

스프링부트 프로젝트 생성하기

먼저 프로젝트부터 만들어야겠죠?

Spring Starter Project로 생성합니다.

 

값을 채워넣었습니다.

프로젝트명, group, artifact, package명 등 위 내용으로 채워넣었습니다.

생성하고자 하는 프로젝트로 만드세요~

 

WebSocket 사용

라이브러리는 WebSocket을 사용할거니까 검색 후 추가해주고, 완료해주겠습니다.

 

스프링부트 프로젝트 생성!

 

부트 프로젝트 설정하기(pom.xml)

view페이지는 jsp를 사용할 예정입니다.

사용을 위해 pom.xml 설정을 하겠습니다. 아래의 dependency도 추가해주세요.

 

pom.xml

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- View JSP -->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-jasper</artifactId>
    <scope>provided</scope>
</dependency>
<!-- View JSP -->

 

 

다음은 properties의 설정값을 추가하겠습니다.

톰캣 포트를 80으로 변경하고, jsp를 바라볼 수 있도록 설정하겠습니다.

또한, 재시작없이 jsp가 적용되는 설정까지 하도록 하겠습니다.

 

application.properties

 

resources/application.properties

#Tomcat Server Setting
server.port=80

#JSP, HTML ModelAndView Path Setting
spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp

#JSP to Modify Not Restart Server
server.servlet.jsp.init-parameters.development=true

 

 

뷰 설정

디렉토리 생성 및 chat.jsp파일 생성

뷰 페이지 구조

뷰페이지를 생성합니다.

이제 컨트롤러를 구성하여 넘어가는 것을 보고 바로 소켓통신 설정을 진행해보겠습니다.

 

 

컨트롤러 추가

MainController 추가

controller패키지 추가 및 MainContoller.java파일을 생성합니다.

package com.psw.chating.controller;

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

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

chat파일을 넘겨주는 view컨트롤러를 생성 후 서버 구동하여 정상적으로 jsp페이지에 접근이 되는지 확인합니다.

 

127.0.0.1/chat 접근

localhost/chat으로 접근하니 정상적으로 chat.jsp페이지에 접근하는 모습을 볼 수 있습니다.

이제 본격적으로 WebSocket처리를 연결하고 통신 예제를 진행해보겠습니다.

 

WebSocket 설정

웹소켓 구현체와 구현체등록

웹소켓 구현체와 등록해주는 config파일을 생성해보겠습니다. 위 그림처럼 패키지와 자바파일을 생성합니다.

 

SocketHandler.java

@Component
public class SocketHandler extends TextWebSocketHandler {
	
	@Override
	public void handleTextMessage(WebSocketSession session, TextMessage message) {
		//메시지 발송
	}
	
	@Override
	public void afterConnectionEstablished(WebSocketSession session) throws Exception {
		//소켓 연결
	}
	
	@Override
	public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
		//소켓 종료
	}
}

구현체에 등록할 SocketHandler입니다.

afterConnectionEstablished 메소드는 웹소켓 연결이 되면 동작합니다.

acafterConnectionClosed 메소드는 반대로 웹소켓이 종료되면 동작합니다.

handleTextMessage 메소드는 메시지를 수신하면 실행됩니다. 상속받은 TextWebSocketHandler는 handleTextMessage를 실행시키며, 메시지 타입에따라 handleBinaryMessage또는 handleTextMessage가 실행됩니다.

 

소스를 완성해보겠습니다.

 

SocketHandler.java

package com.psw.chating.handler;

import java.util.HashMap;

import org.springframework.stereotype.Component;
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 {
	
	HashMap<String, WebSocketSession> sessionMap = new HashMap<>(); //웹소켓 세션을 담아둘 맵
	
	@Override
	public void handleTextMessage(WebSocketSession session, TextMessage message) {
		//메시지 발송
		String msg = message.getPayload();
		for(String key : sessionMap.keySet()) {
			WebSocketSession wss = sessionMap.get(key);
			try {
				wss.sendMessage(new TextMessage(msg));
			}catch(Exception e) {
				e.printStackTrace();
			}
		}
	}
	
	@Override
	public void afterConnectionEstablished(WebSocketSession session) throws Exception {
		//소켓 연결
		super.afterConnectionEstablished(session);
		sessionMap.put(session.getId(), session);
	}
	
	@Override
	public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
		//소켓 종료
		sessionMap.remove(session.getId());
		super.afterConnectionClosed(session, status);
	}
}

 

 

 

구현체를 이제 등록해보겠습니다.

WebSocketConfig.java

package com.psw.chating.config;

import org.springframework.beans.factory.annotation.Autowired;
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 com.psw.chating.handler.SocketHandler;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer{

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

먼저 생성하였던 구현체를 등록하는 부분입니다.

 

마지막으로 chat.jsp를 완성하고 메시지가 주고 받아지는지 확인해보겠습니다.

 

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 p{
			color: #fff;
			text-align: left;
		}
		input{
			width: 330px;
			height: 25px;
		}
		#yourMsg{
			display: none;
		}
	</style>
</head>

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

	function wsOpen(){
		ws = new WebSocket("ws://" + location.host + "/chating");
		wsEvt();
	}
		
	function wsEvt() {
		ws.onopen = function(data){
			//소켓이 열리면 초기화 세팅하기
		}
		
		ws.onmessage = function(data) {
			var msg = data.data;
			if(msg != null && msg.trim() != ''){
				$("#chating").append("<p>" + msg + "</p>");
			}
		}

		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 uN = $("#userName").val();
		var msg = $("#chatting").val();
		ws.send(uN+" : "+msg);
		$('#chatting').val("");
	}
</script>
<body>
	<div id="container" class="container">
		<h1>채팅</h1>
		<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>
			</table>
		</div>
	</div>
</body>
</html>

 

동작 확인

3개의 브라우저 생성

부트 어플리케이션을 구동하고 localhost/chat으로 접근하면 위와같은 뷰를 확인 할수 있습니다.

혼자서 3개의 브라우저를 열고 사용자명을 입력 후 테스트를 해보았습니다.

채팅 테스트

 

다음장에는 상대방과 자신을 구분을 처리해보겠습니다.

반응형