Post

JSP, Spring: 웹소켓(Web Socket) 2 + 예제: 긴급상황 경보 표시가 나오는 지도

이전 글: JSP, Spring: 웹소켓(Web Socket) 1 참조

클라이언트 측에서는 계속 대기하고 있다가 서버에서 긴급 상황(?)이 일어났다는 신호와 지도 좌표를 보내면 즉시 해당 좌표에 경고 상황이 일어났다고 표시하는 웹 페이지를 만들어볼 것이다. 실제 시스템이 어떻게 동작하는지는 모르겠고 뇌내망상으로 진행함.

브라우저에 지도를 표시하는 방법과 좌표에 플래시 효과를 주는 예제는 다음 페이지를 참조: OpenLayers 4: Custom Animation

 

1. 이전 웹소켓 클래스를 참조해 새로운 웹소캣 클래스를 작성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
package com.example.thymeleaf.websocket;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

import org.springframework.boot.json.BasicJsonParser;
import org.springframework.boot.json.JsonParser;

 
@ServerEndpoint("/ws-alert")    // 웹소켓용 서버 주소 지정 (ws://[서버 주소]/ws)
public class AlertWebSocket {
  
  static final String ENCODED_KEY = "B596A8A830DD4F3AF0ADCBC9FC4F9133124CC3E9C7CEF61154447A0ABC508C51";
  private Session server;
  
    //유저 집합 리스트
    static List<Session> sessionUsers = Collections.synchronizedList(new ArrayList<>());
 
    /**
     * 웹 소켓이 접속되면 유저리스트에 세션을 넣는다.
     * @param userSession 웹 소켓 세션
     */
    @OnOpen
    public void handleOpen(Session userSession){
    	System.out.println("session: " + userSession.getId());
        sessionUsers.add(userSession);
    }
    /**
     * 웹 소켓으로부터 메시지가 오면 호출한다.
     * @param message 메시지
     * @param userSession
     * @throws IOException
     */
    @OnMessage
    public void handleMessage(String message, Session userSession) throws IOException{
    	
    	Map<?, ?> out = null;
    	
    	try {
    		JsonParser jp = new BasicJsonParser();
        	out = jp.parseMap(message);
        } catch(Exception e) {
        	System.err.println(e);
        }
    	
    	if(out != null) {
    		if(out.get("encodeKey").equals(ENCODED_KEY) 
                    && out.get("purpose").equals("init-validate")) {
    			System.out.println("== 이 사람은 좌표 서버입니다. ==");
        		server = userSession;
    		} else if (out.get("encodeKey").equals(ENCODED_KEY) 
                    && out.get("purpose").equals("send-coords")) {    	    	
    			ArrayList<?> coords = (ArrayList<?>) out.get("coords");
    			sendMessageToAll(coords.toString(), sessionUsers);
    		}
    		
    	} else {
    		sendMessageToAll(message, sessionUsers);
    	}  
    	
        System.out.println(userSession.getId() + ": " + message);
        
       
    }
 
    /**
     * 웹소켓을 닫으면 해당 유저를 유저리스트에서 뺀다.
     * @param userSession
     */
    @OnClose
    public void handleClose(Session userSession){
        sessionUsers.remove(userSession);
    }
    
    public void sendMessageToAll(String message, List<Session> sessionUsers) throws IOException {
    	for(Session ss : sessionUsers) {    	        	
      ss.getBasicRemote().sendText(message);
      }
    }
 
}

아직 접속시 정보를 보내는 법을 몰라 handleMessage()에 간단한 인증 과정을 넣었다. 좌표 담당 클라이언트가 접속을 시작하면 인증 요청과 인증 코드를 보낼 것이다.

스프링 부트 시 다음 부분을 추가 작성한다.

1
@Bean public AlertWebSocket alertWebSocket() { return new AlertWebSocket(); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.example.thymeleaf.websocket;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
public class EndpointConfig 
{
    @Bean
    public WebSocket webSocket(){
        return new WebSocket();
    }
    
    @Bean
    public AlertWebSocket alertWebSocket() {
    	return new AlertWebSocket();
    }

    @Bean
    public ServerEndpointExporter endpointExporter(){
        return new ServerEndpointExporter();
    }

}

 

2. 좌표 담당 클라이언트 측의 웹 페이지 작성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//<button onclick="sendCoords()">좌표전송</button>

   var webSocket = new WebSocket('ws://서버주소/ws-alert');
   webSocket.onerror = onError
   webSocket.onopen = onOpen
   webSocket.onmessage = onMessage

   var object = {
     encodeKey: "B596A8A830DD4F3AF0ADCBC9FC4F9133124CC3E9C7CEF61154447A0ABC508C51",
     purpose: "init-validate",
     message: "좌표 서버입니다."
   }

   function onMessage(event) {
     console.log(event.data)
   }

   function onOpen(event) {
     console.log('connected')
     console.log(event)

     console.log(JSON.stringify(object))

     webSocket.send(JSON.stringify(object));
   }

   function onError(event) {}

   function send() {
     webSocket.send("dfnadksfnk")
   }

   function sendCoords() {
     var coords = []
     coords.push([126.97, 37.57])
     coords.push([129.07, 35.18])
     coords.push([127.38, 36.35])

     object.purpose = "send-coords"
     object.coords = coords
     webSocket.send(JSON.stringify(object))
   }

onOpen() 실행 시 한 번 인증 코드를 보내면 웹소켓 서버에서 인증되면 이 사람(좌표 담당) 전용의 작업들을 실행할 것이다.

 

3. 나머지 클라이언트 측 웹 페이지 작성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html>

<head>
  <title>Custom Animation</title>
  <link rel="stylesheet" href="https://openlayers.org/en/v4.6.5/css/ol.css" type="text/css">
  <!-- The line below is only needed for old environments like Internet Explorer and Android 4.x -->
  <script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=requestAnimationFrame,Element.prototype.classList,URL"></script>
  <script src="https://openlayers.org/en/v4.6.5/build/ol.js"></script>
</head>

<body>
  <div id="map" class="map"></div>
  <script>
     //
  </script>
</body>

</html>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
// radius will be START_RAD at start and END_RAD at end.
var START_RAD = 5
var END_RAD = 100

var webSocket = new WebSocket('ws://서버주소/ws-alert');
webSocket.onerror = onError
webSocket.onopen = onOpen
webSocket.onmessage = onMessage

function onMessage(event) {
  try {
    var coords = JSON.parse(event.data)

    for (var i in coords) {
      addFlickeringFeature(coords[i])
    }

  } catch (e) {
    console.warn(e.message)
  }
  console.log(event.data)

}

function onOpen(event) {
  console.log('ws-alert connected')
}

function onError(event) {

}

function send(msg) {
  webSocket.send(msg)
}

var map = new ol.Map({
  layers: [
    new ol.layer.Tile({
      source: new ol.source.OSM({
        wrapX: false
      })
    })
  ],
  controls: ol.control.defaults({
    attributionOptions: {
      collapsible: false
    }
  }),
  target: 'map',
  view: new ol.View({
    center: [14235526.306672268, 4366292.816742867],
    zoom: 8
  })
});

var source = new ol.source.Vector({
  wrapX: false
});
var vector = new ol.layer.Vector({
  source: source
});
map.addLayer(vector);

function addFeature(coord) {
  var geom = new ol.geom.Point(ol.proj.transform(coord,
    'EPSG:4326', 'EPSG:3857'));
  var feature = new ol.Feature(geom);
  source.addFeature(feature);
}

function addFlickeringFeature(coord) {
  window.setInterval(function() {
    addFeature(coord)
  }, 200);
}

var duration = 3000;

function flash(feature) {
  var start = new Date().getTime();
  var listenerKey;

  function animate(event) {
    var vectorContext = event.vectorContext;
    var frameState = event.frameState;
    var flashGeom = feature.getGeometry().clone();
    var elapsed = frameState.time - start;
    var elapsedRatio = elapsed / duration;

    var radius = ol.easing.easeOut(elapsedRatio) * (END_RAD - START_RAD) + START_RAD;
    var opacity = ol.easing.easeOut(1 - elapsedRatio);

    var style = new ol.style.Style({
      image: new ol.style.Circle({
        radius: radius,
        snapToPixel: false,
        stroke: new ol.style.Stroke({
          color: 'rgba(255, 0, 0, ' + opacity + ')',
          width: 0.25 + opacity
        })
      })
    });

    vectorContext.setStyle(style);
    vectorContext.drawGeometry(flashGeom);
    if (elapsed > duration) {
      ol.Observable.unByKey(listenerKey);
      return;
    }
    // tell OpenLayers to continue postcompose animation
    map.render();
  }
  listenerKey = map.on('postcompose', animate);
}

source.on('addfeature', function(e) {
  flash(e.feature);
});

지도 예제를 참조해서 만들며 좌표 담당이 아닌 다른 사람들이 메시지를 보내면 가만히 있다가 좌표 담당이 긴급 상황 신호와 좌표를 보내면 그 좌표를 바탕으로 빨간색 원을 확대하면서 깜빡거린다.

via GIPHY

This post is licensed under CC BY 4.0 by the author.