전체적인 코드 흐름 소개

 

1) 프로젝트에 db를 사용해야 합니다

 이유: 메시지를 읽지않은 사람이 누구인지를 기록해야 합니다.

 

2) 현재 방에 접속중인 사람의 수를 알아야합니다.

   줄여서 "접사수"라 하겠습니다.

 

★3) 접사수가 2인 상황에서는, 읽음 표시를 0으로 보냅니다.

     ++) 모든 사람이 이 메시지를 읽은 상황입니다. (1:1 채팅 기준)

 

      접사수가 1인 상황에서는, 읽음 표시를 1으로 보냅니다. 

      ++) 나만 이 메시지를 읽은 상황입니다. (1:1 채팅 기준)

 

★4) 내가 메시지를 보내고 방에 접속중인 상황

   

       상대방이 방에 들어와서 내가 보냈던 메시지를 읽습니다.

       그리고 보낸 메시지를 읽었다고 실시간으로 나에게 알려줍니다.

       그리고 내 화면에서 상대방이 읽은 메시지의 읽음 표시를 -1 합니다.

 

5) 내가 메시지를 보내고 방에 나간후, 상대방이 방에 들어와서 메시지를 읽은 경우

 

    상대방이 메시지를 읽으면 db에는 읽음표시가 0으로 됩니다.

    그러므로, 내가 방에 들어갔을때는 나에게도 그 메시지의 읽음표시가 자동으로 0으로 되어있습니다.

 

github.com/burnaby033/multiRoomV2

 

burnaby033/multiRoomV2

스프링 다중채팅방, 메신저 읽음 표시, 1 사라짐. Contribute to burnaby033/multiRoomV2 development by creating an account on GitHub.

github.com

 

      프로젝트 다운 주소입니다.

 

 

 

 

 

 

 

뷰 관점에서 본 코드 흐름

 

컨트롤러 패키지 구조입니다.

뷰 폴더 구조입니다.

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.dms.controller;
 
 
import java.util.Collection;
import javax.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import com.dms.chat.ChatRoom;
import com.dms.chat.ChatRoomRepository;
 
@Controller
@RequestMapping("/multiRoom")
public class home {
 
    @GetMapping("/home")
    public String homeController(Model model, HttpServletRequest request) {
        Collection<ChatRoom> chatRooms = ChatRoomRepository.chatRooms;
        
        model.addAttribute("collection", chatRooms);
        return "home";
    }
}
 
cs

home 컨트롤러(클래스)입니다.

모델로 chatRooms 변수를 사용합니다.

++) ChatRoomRepository 클래스의 chatRooms 변수에 public static을 붙여놓았습니다.

그러므로 초기화된 chatRooms 변수를 home 클래스에서 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@page import="java.util.*"%>    
<%@page import="com.dms.chat.ChatRoom"%>    
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>대기방</title>
</head>
<body>
<%
    Collection<ChatRoom> col = (Collection<ChatRoom>) request.getAttribute("collection");
    Iterator<ChatRoom> it = col.iterator();    
    while(it.hasNext()) {        
        String roomId = it.next().getId();
%>
<p>
<div><a href="room?id=<%=roomId%>"><%=roomId%></a></div>
</p>
<%} %>
</body>
</html>
cs

home 뷰(jsp) 입니다.

모델로 받은 collection변수안에 있는 roomId를 가져옵니다.   

collection변수에는 초기화된 String 타입의 id 변수를 가진 클래스 두개가 들어있습니다.

Iterator 의 next메소드를 통해 클래스를 가져옵니다.

그리고 그 클래스의 getId 메소드로 id 변수(roomId)를 가져옵니다.

 

가져온 roomId는 <a>태그에서 url 파라미터로 사용합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.dms.controller;
 
import javax.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
 
@Controller
@RequestMapping("/multiRoom")
public class room {
 
    @GetMapping("/room")
    public String roomController(Model model, HttpServletRequest request) {
        String roomId = request.getParameter("id");
        
        model.addAttribute("roomId", roomId);
        return "room";
    }
}
 
cs

room 컨트롤러(클래스) 입니다.

<a> 태그에서 적어준 id(roomId)를 roomId 변수에 넣습니다.

 그리고 roomId 변수를 모델에 추가합니다.

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
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>대화방</title>
<script src="/webjars/sockjs-client/sockjs.min.js"></script>
<script>
var roomId = "${roomId}";
var sock = new SockJS("/ws/multiRoom");
 
/*onopen 함수는 페이지가 로드되면 자동실행됨*/
sock.onopen = function () {
    sock.send( JSON.stringify({chatRoomId: roomId, type: "JOIN"}) );
}                                                                            
/*onmessage 함수는 메시지가 오면 자동실행됨*/    
    sock.onmessage = function (e) {
        var content = JSON.parse(e.data);
        var message = content.message;
        var type = content.type;
        var chatLog = document.getElementById("chatLog");     
        if(type == "SEND")
            chatLog.innerHTML = chatLog.innerHTML + "<p>" + message + "</p>";
}
    
function send(){
    var textarea = document.getElementById("textarea");
    var myMessage = textarea.value;
    sock.send( JSON.stringify({chatRoomId: roomId, type: "SEND", message: myMessage}) );
}    
</script>    
</head>
<body>
<h1>대화방</h1>
<div id="chatLog"></div>
<textarea id="textarea"></textarea>
<input type="button" value="전송" onclick="send()">
</body>
</html>
cs

room 뷰(jsp) 입니다.

room 뷰에서는 두가지 일이 일어납니다.

1) onopen

2) onmessage

 

onopen

room에 들어가면 sock.onopen 함수가 실행됩니다.

그리고, sock.send 함수가 실행됩니다.

js 변수인 roomId는 모델로 초기화 됩니다.

sock.send가 실행될 때, roomId와 type이 사용됩니다.

이 데이터들은 chatHandler 을 거쳐서 ChatRoom 클래스에 오게됩니다.

type이 "JOIN" 이므로 ChatRoom 클래스의 ssessions변수에 접속한 사용자 세션 정보를 등록합니다.

 

onmessage

room 뷰 화면

메시지를 입력하고 전송 버튼을 클릭하면  send 함수(js)가 실행됩니다.

그리고 sock.send 함수가 실행됩니다.

sock.send가 실행될 때, roomId, type, messsage 들이 사용됩니다.

이 데이터들은 chatHandler 을 거쳐서 ChatRoom 클래스에 오게됩니다.

type이 "SEND" 이므로 ChatRoom 클래스의 send메소드가 실행됩니다.

 

같은 방끼리만 메시지를 공유하는 다중채팅방이 완성되었습니다. 

github.com/burnaby033/springboot_multiroom

 

burnaby033/springboot_multiroom

스프링부트 다중채팅방, 스프링부트 멀티 채팅방. Contribute to burnaby033/springboot_multiroom development by creating an account on GitHub.

github.com

프로젝트 다운 주소입니다.

 

++) supawer0728.github.io/2018/03/30/spring-websocket/

 

Spring WebSocket 소개

서론Web Browser에서 Request를 보내면 Server는 Response를 준다. HTTP 통신의 기본적인 동작 방식이다. 하지만 Server에서 Client로 특정 동작을 알려야 하는 상황도 있다. 예를 들어 Browser로 Facebook에 접속해

supawer0728.github.io

이 프로젝트의 원본 코드입니다.

(SockJS 부분)

서버(자바 파일) 관점에서 본 코드 흐름

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
plugins {
    id 'org.springframework.boot' version '2.3.4.RELEASE'
    id 'io.spring.dependency-management' version '1.0.10.RELEASE'
    id 'java'
}
 
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
 
repositories {
    mavenCentral()
    mavenLocal()
}
 
 dependencies {
 
    implementation 'org.springframework.boot:spring-boot-starter'
    implementation('org.springframework.boot:spring-boot-starter-test'
    compile('org.springframework.boot:spring-boot-starter-web')
    compile('org.springframework.boot:spring-boot-starter-websocket')
    compile('org.webjars:sockjs-client:1.1.2')
    compile('org.webjars:webjars-locator:0.30')
    runtime('org.springframework.boot:spring-boot-devtools')
    testCompile('org.springframework.boot:spring-boot-starter-test')
    compile('org.springframework.boot:spring-boot-starter-test')
    compile('org.apache.tomcat.embed:tomcat-embed-jasper')
 
 
}
 
test {
    useJUnitPlatform()
}
 
cs
gradle
1
2
3
4
5
6
7
8
server.port = 8016
 
spring.mvc.view.prefix=/WEB-INF/view/
 
spring.mvc.view.suffix=.jsp
 
}
 
cs
application.properties

자바 패키지 구조입니다.

 

chat패키지 구조입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.dms.chat;
 
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;
 
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
   
    ChatHandler chatHandler = new ChatHandler();
 
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(chatHandler, "/ws/multiRoom").setAllowedOrigins("*").withSockJS();
    }
}
 
cs

WebSocketConfig 클래스입니다.

chatHandler클래스를 웹소켓 핸들러로 정합니다.

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
package com.dms.chat;
 
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;
import com.fasterxml.jackson.databind.ObjectMapper;
 
public class ChatHandler extends TextWebSocketHandler {
    
     ObjectMapper objectMapper = new ObjectMapper();
     ChatRoomRepository repository = new ChatRoomRepository();
    
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {  
        String payload = message.getPayload();
        ChatMessage chatMessage = objectMapper.readValue(payload, ChatMessage.class);
        ChatRoom chatRoom = repository.getChatRoom(chatMessage.getChatRoomId());   
        chatRoom.handleMessage(session, chatMessage, objectMapper);
 
    }
    
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        repository.remove(session);
        
    }
    
}
 
cs

chatHandler 클래스 입니다. 

room.jsp 에서 sock.send()가 실행되면 데이터가 오게되는 곳입니다.

 

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
package com.dms.chat;
 
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import org.springframework.web.socket.WebSocketSession;
 
public class ChatRoomRepository {
 
    Map<String, ChatRoom> chatRoomMap = new HashMap<String, ChatRoom>(); 
    /*home 컨트롤러에서 모델로 사용하기위해서 public static을 붙였습니다.*/
    public static Collection<ChatRoom> chatRooms;
    
    public ChatRoomRepository() {
        for(int i=0;i<2;i++) {  
            String uuid = UUID.randomUUID().toString();
            ChatRoom chatRoom = new ChatRoom(uuid);
            chatRoomMap.put(chatRoom.getId(), chatRoom);
            System.out.println("chatRoom 클래스를 복제하고 있습니다.");
            System.out.println("chatRoom -> "+chatRoom);
           }            
         chatRooms = chatRoomMap.values();
    }
        
    public ChatRoom getChatRoom(String id) {
        return chatRoomMap.get(id);
    }
    
    public Map<String, ChatRoom> getChatRooms() {
        return chatRoomMap;
    }
        
    public void remove(WebSocketSession session) {
        this.chatRooms.parallelStream().forEach(chatRoom -> chatRoom.remove(session));
    } 
    
}
 
cs

ChatRoomRepository 클래스 입니다.

chatHandler 클래스에서 repository 객체를 만들면 생성자가 실행됩니다.

그러므로 자동으로 초기화 됩니다.

chatRooms변수는 Map의 values 메소드로 해당 Map의 values만을 꺼냅니다.

 

++) map은 key, value로 데이터를 저장하는 자료구조 입니다.

이 코드에서는 key는 roomId(uuid) 입니다.

그리고, value는 chatRoom 클래스입니다.

 

++) i<2 이므로 채팅방을 2개가 만들어집니다. 

즉, chatRoom 클래스가 두개가 만들어집니다.

클래스마다 방의 접속자를 관리하기 위해서 만듭니다.

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
package com.dms.chat;
 
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
 
public class ChatRoom {
     String id;
     Set<WebSocketSession> sessions = new HashSet<>();
 
    public ChatRoom(String room_id) { 
        this.id = room_id;
    }
   
    public void handleMessage(WebSocketSession session, ChatMessage chatMessage, ObjectMapper objectMapper) throws JsonProcessingException {
        if (chatMessage.getType().equals("JOIN")) 
            join(session); 
        else
            send(chatMessage, objectMapper);
    }
 
    private void join(WebSocketSession session) {
        sessions.add(session);
    }
    
    private <T> void send(T messageObject, ObjectMapper objectMapper) throws JsonProcessingException {
        TextMessage message = new TextMessage(objectMapper.writeValueAsString(messageObject));
  
        sessions.parallelStream().forEach(session -> {
            try {
                session.sendMessage(message);
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        });
    }
    
    public void remove(WebSocketSession target) {
      String targetId = target.getId();
      sessions.removeIf(session -> session.getId().equals(targetId));
     }
 
    public String getId() {
        return id;
    }
 
    public Set<WebSocketSession> getSessions() {
        return sessions;
    }
 
}
 
cs

ChatRoom 클래스 입니다.

ChatRoomRepository 클래스의 chatRoomMap 변수를 채우는데 사용합니다.

사용자가 방에 들어오면, 사용자 정보(세션)가 join 메소드를 통해 sessions 변수에 담깁니다.

사용자가 메시지를 보내면 send 메소드를 통해 sessions 변수에 있는 모든 사용자들에게 메시지를 보냅니다.

 ++) 메시지 공유대상은 같은 방에 있는 사용자들 입니다.

 

※예시※

a가 방에 들어가서 join을 통해  session 변수에 a를 넣었습니다.

b가 방에 들어가서 뷰에서 메시지를 보냈습니다.

그러면, 메시지가 chatHandler -> chatRoom 순으로 클래스를 거칩니다.

 sessions.parallelStream().forEach(session -> { session.sendMessage(message) } )

위 코드가 실행됩니다.

  

다음 포스팅은 컨트롤러와 뷰를 소개하겠습니다.

+ Recent posts