DBILITY

EventSource + Spring Server Sent Event 본문

front-end & ui/javascript

EventSource + Spring Server Sent Event

DBILITY 2021. 5. 12. 22:26
반응형

HTML5 Websocket은 양방향, Server Sent Event는 단방향(Server -> Client)을 지원한다.

완성된 코드로 볼 수 없으나 동작함.

web.xml의 filter와 servlet(3.0이상)설정에  async-supported 추가 필요함

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         id="WebApp_ID" version="3.0">

    <display-name>ws</display-name>

	<filter>
		<filter-name>encodingFilter</filter-name>
		<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
		<async-supported>true</async-supported>
		<init-param>
			<param-name>encoding</param-name>
			<param-value>UTF-8</param-value>
		</init-param>
		<init-param>
			<param-name>forceEncoding</param-name>
			<param-value>true</param-value>
		</init-param>
	</filter>
	<filter-mapping>
		<filter-name>encodingFilter</filter-name>
		<url-pattern>/*</url-pattern>
        <!--<dispatcher>REQUEST</dispatcher>
        <dispatcher>ASYNC</dispatcher>-->
	</filter-mapping>

   <!--
		- Location of the XML file that defines the root application context.
		- Applied by ContextLoaderListener.
	-->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath*:spring/context-*.xml</param-value>
    </context-param>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>


    <!--
		- Servlet that dispatches request to registered handlers (Controller implementations).
	-->
    <servlet>
        <servlet-name>dispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:config/dispatcher-servlet.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
        <async-supported>true</async-supported>
    </servlet>

    <servlet-mapping>
        <servlet-name>dispatcherServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

</web-app>

Service와 Controller Class

@EventListner는 spring 4.2부터 지원됨

@RequestBody에 Map사용시 RequestMappingHandlerAdapter MappingJackson2HttpMessageConverter설정 필요

public class MessageEvent {

    private String message;

    public MessageEvent(String message) {
        this.message = message;
    }

    public String getMessage() {
        return this.message;
    }
}

public interface SseService {

    void add(SseEmitter emitter) throws Exception;
    void remove(SseEmitter emitter) throws Exception;
    void publishMessage(MessageEvent event) throws Exception;
    long count() throws Exception;

}


@Service("sseService")
public class SseServiceImpl implements SseService {

    private static final Logger logger = Logger.getLogger(SseServiceImpl.class);
    private static CopyOnWriteArrayList<SseEmitter> subscribers = new CopyOnWriteArrayList<>();

    @Override
    public void add(final SseEmitter emitter) throws Exception {
            emitter.onCompletion(new Runnable() {
                @Override
                public void run() {
                    synchronized (subscribers) {
                        subscribers.remove(emitter);
                    }
                }
            });
            emitter.onTimeout(new Runnable() {
                @Override
                public void run() {
                    synchronized (subscribers){
                        subscribers.remove(emitter);
                    }
                }
            });
            subscribers.add(emitter);
    }

    @Override
    public void remove(SseEmitter emitter) throws Exception {
        subscribers.remove(emitter);
    }

    @Async
    @EventListener({MessageEvent.class})
    @Override
    public void publishMessage(MessageEvent event) throws Exception {
        List<SseEmitter> deadSubscribers = new ArrayList<>();
        for (SseEmitter subscriber : subscribers) {
            try {
                subscriber.send(event.getMessage(), MediaType.TEXT_EVENT_STREAM);
                subscriber.complete();
            } catch (Exception e) {
                deadSubscribers.add(subscriber);
                throw e;
            }
        }
        subscribers.removeAll(deadSubscribers);
    }

    @Override
    public long count() throws Exception {
        return this.subscribers.size();
    }

}

@Controller
public class DefaultController {

	private static final Logger logger = LoggerFactory.getLogger(DefaultController.class);

	@Resource(name = "sseService")
	private SseService sseService;
    
    private ApplicationEventPublisher eventPublisher;

	@Autowired(required = false)
	public void setEventPublisher(ApplicationEventPublisher eventPublisher) {
		this.eventPublisher = eventPublisher;
	}

	@RequestMapping(value = "/eventstream", method = RequestMethod.GET, produces = MediaType.TEXT_EVENT_STREAM_VALUE)
	public SseEmitter eventSourceConnect(@RequestParam("key") String key, HttpServletRequest request) throws Exception {
		logger.info("1 ---------------> eventstream key {}", key);
		SseEmitter emitter = new SseEmitter(10000L);
		emitter.event().reconnectTime(10000L).id(key);
		sseService.add(emitter);
		logger.info("2 ---------------> add Emitter after {}", sseService.count());
		return emitter;
	}

	@RequestMapping(value = "/sendMessage", method = RequestMethod.POST)
	@ResponseBody
	public Map<String,Object> sendMessage(@RequestBody Map<String, Object> paramMap) {
		logger.info("3 ---------------> sendMessage {}", paramMap.get("message"));
		int result = 1;
		String message = "";
		Map<String, Object> rtMap = new HashMap<>();

		try {
			if(eventPublisher!=null){
				Gson gson = new Gson();
				JsonObject obj = new JsonObject();
				obj.addProperty("message",paramMap.get("message").toString());
				eventPublisher.publishEvent(new MessageEvent(gson.toJson(obj)));
			}
		} catch (Exception e) {
			result = -1;
			message = e.getMessage();
			rtMap.put("message", message);
		}

		rtMap.put("result", result);
		return rtMap;
	}

}

EventSource javascript + html

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!doctype html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<h1>EventSource!</h1>
<div class="container">
  <div><input type="text" id="sendMessage" />
      <button id="btnSend">메시지전송</button></div>
  <ul class="msgList">
  </ul>
</div>
<script>

    let eventSource;

    function log() {
        for (let x of arguments) {
            console.log(new Date().toLocaleTimeString() + " ---> ", x);
        }
    }

    /**
     * 어디선가 보고 베낌
     * @returns {string}
     */
    var fnGenerateUUID = function () {
        let d = new Date().getTime();
        let uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
            var r = (d + Math.random() * 16) % 16 | 0;
            d = Math.floor(d / 16);
            return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);
        });
        return uuid;
    };

    window.addEventListener("load", e => {
        eventSource = new EventSource("/eventstream?key=" + fnGenerateUUID(), {withCredentials: true});
        try {
            eventSource.addEventListener("message", e => {
                let data = JSON.parse(e.data);
                log(data);
                let list = document.getElementsByClassName("msgList")[0];
                let ele = document.createElement("li");
                ele.innerText = data.message;
                list.append(ele);
            }, false);
            eventSource.addEventListener("open", e => {
                log("Connection : " + ((e.returnValue == true) ? "yes" : "no"));
            }, false);
            eventSource.addEventListener("error", e => {
                if (e.readyState == EventSource.CLOSED) {
                    log("Disconnected");
                    e.currentTarget.close();
                }
            }, false);
        } catch (e) {
            log(e);
        }

        let btnSubmit = document.getElementById("btnSend");
        btnSubmit.addEventListener("click", ev => {
            let message = document.getElementById("sendMessage").value;
            if (message.length == 0) {
                e.preventDefault();
                return;
            }

            let xhr = new XMLHttpRequest();
            xhr.onreadystatechange = function () {
                if (this.readyState == 4 && this.status == 200) {
                    log("xhr.onreadystatechange :" + this.responseText);
                    var msg = this.responseText;
                }
            };
            xhr.onerror = function (ev) {
                log("xhr.onerror :" + this.responseText);
            };

            xhr.open("POST", "/sendMessage");
            xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
            xhr.send(JSON.stringify({"message": message}));

        });
    });

    window.addEventListener("unload",function () {
        if(eventSource!=null){
            eventSource.close();
        }
    });

</script>
</body>
</html>

실행결과

반응형

'front-end & ui > javascript' 카테고리의 다른 글

div move + rotate + resize test  (0) 2021.05.20
div rotate test  (0) 2021.05.18
div drag test  (0) 2021.05.14
pure javascript file upload 테스트  (0) 2021.05.12
ajax download  (0) 2019.06.03
Comments