본문 바로가기
Spring

[웹소켓] 1.

by moca7 2024. 11. 4.

 

 

 

ㅁ 통신 방식

 

 

(1) HTTP통신 (기본통신방식) : 비연결 통신 

 

- HTTP통신은 한번 요청 보내고 처리되면 연결이 끊긴다.

그래서 기본적으로 지속적으로 데이터를 주고받을 수가 없다. 

근데 채팅은 지속적으로 메세지를 주고받아야 한다.

- HTTP통신도 지속적으로 데이터를 주고받을 수는 있다. 

ajax로 데이터를 지속적으로 주고받으면 된다. setInterval로 1초마다 댓글을 조회 했었다.

ajax와 setInterval를 같이 사용해서 주기적으로 요청을 보내 응답을 받으면 된다. (polling 방식) 

1초마다 메세지를 조회하면 마치 메세지를 주고받는 것처럼 표현할 수 가 있다.

그런데 단점이 네트워크 리소스를 많이 사용해서 비효율적이다.

- 요청해서 응답을 받으면 끝이다. 연결이 끊긴다. 이게 반복적으로 진행된다.

 

 

 

(2) webSocket 통신 : 영구적 양방향 통신

 

- WebSocket 통신은 실시간 양방향 통신이다.

- 실시간으로 연결되어 있어서 지속적으로 데이터를 주고받을 수 있다.

- 연결이 한 번 성립되면 클라이언트나 서버가 명시적으로 연결을 끊을 때까지 계속 유지된다.

 

 

 


ㅁ 웹소켓도 통신방법중 하나다.

- 이때까지 사용한 통신방법(url 요청해서 페이지 이동하거나 ajax로 url 요청해서 응답데이터 돌려받기)이 http 통신방식이다. 이게 기본적인 웹에서 통신 방식이다.

 

 

 

 

ㅁ 웹소켓은 채팅과 알람기능에 쓸 수 있다.

- 누가 메일을 발송했다. 기안하기가 결재가됐다. 알림을 띄울때도 웹소켓이 가능하다.

 

 

 

 

 

 

ㅁ main.jsp 전 후

 

 
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
   
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>    
<c:set var="contextPath" value="${pageContext.request.contextPath}" />
   
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>

  <div class="container p-3">

    <!-- Header, Nav start -->
    <jsp:include page="/WEB-INF/views/common/header.jsp" />
    <!-- Header, Nav end -->


    <!-- Section start -->
    <section class="row m-3" style="min-height: 500px">

      <div class="container border p-5 m-4 rounded">
        <h2 class="m-4">해당 페이지의 내용이 보여져야되는 자리</h2>
       
     
      </div>

    </section>
    <!-- Section end -->
     

    <!-- Footer start -->
    <jsp:include page="/WEB-INF/views/common/footer.jsp" />
    <!-- Footer end -->

  </div>
 
</body>
</html>
 

 

 
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
   
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>    
<c:set var="contextPath" value="${pageContext.request.contextPath}" />
   
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>

  <div class="container p-3">

    <!-- Header, Nav start -->
    <jsp:include page="/WEB-INF/views/common/header.jsp" />
    <!-- Header, Nav end -->


    <!-- Section start -->
    <section class="row m-3" style="min-height: 500px">

      <div class="container border p-5 m-4 rounded">
        <h2 class="m-4">webSocket을 이용하여 실시간으로 통신하기</h2>
       
     
        <p>
          실시간으로 알림을 발생시킨다거나 채팅을 할 때 주로 websocket 사용 <br><br>
         
          <c:if test="${ not empty loginUser }">
            <a class="btn btn-secondary" href="${ contextPath }/chat/room.do">채팅방 입장</a>
          </c:if>
         
        </p>
     
      </div>

    </section>
    <!-- Section end -->
     

    <!-- Footer start -->
    <jsp:include page="/WEB-INF/views/common/footer.jsp" />
    <!-- Footer end -->

  </div>
 
</body>
</html>
 

 

 

- 채팅방으로 이동하는 링크를 메인페이지에 둔다.

 

 

 

 

 

 

 

ㅁ 컨트롤러 패키지에 ChatController 생성

 

 
package com.br.spring.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@RequestMapping("/chat")
@Controller
public class ChatController {

  @GetMapping("/room.do")
  public void chatRoomPage() {}
 
}
 

 

 

 

 

ㅁ views 폴더에 chat 폴더 생성

ㅁ views/chat에 room.jsp 생성

 

 
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
   
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>    
<c:set var="contextPath" value="${pageContext.request.contextPath}" />
   
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>

<style>
    .chat{width:400px; margin:auto; padding:10px; border:1px solid lightgray;}
    .chat-area{height:500px; overflow: auto;}

     .chat-message{margin:10px 0px;}
     .chat-message.mine{display: flex; justify-content:flex-end;}
     
    .chat-message .send-message{
       padding: 5px 7px;
       border-radius: 10px;
       max-width: 190px;
       font-size:0.9em;
       white-space: pre-line;
    }
    .chat-message.other .send-message{background: lightgray;}
    .chat-message.mine .send-message{background: #FFE08C;}
 
    .chat-user {
       text-align:center;
       border-radius:10px;  
       background: lightgray;
       opacity: 0.5;
       margin: 20px 0px;
       color: black;
       line-height: 30px;
    }
 </style>

</head>
<body>
<div class="container p-3">

    <!-- Header, Nav start -->
    <jsp:include page="/WEB-INF/views/common/header.jsp" />
    <!-- Header, Nav end -->

    <!-- Section start -->
    <section class="row m-3" style="min-height: 500px">

      <div class="container border p-5 m-4 rounded">
        <h2 class="m-4">채팅방</h2>
       
        <div class="chat">
   
          <div class="chat-area">
         
            <div class="chat-message mine">
              <div class="send-message">내가보낸 메세지내가보낸 메세지내가보낸 메세지내가보낸 메세지내가보낸 메세지</div>
            </div>  
           
            <div class="chat-message other">
              <span class="send-user">상대방</span>
              <div class="send-message">남이보낸 메세지남이보낸 메세지남이보낸 메세지남이보낸 메세지남이보낸 메세지남이보낸 메세지</div>
            </div>
           
            <div class="chat-user entry">
              xxx님이 들어왔습니다.
            </div>
           
            <div class="chat-user exit">
              xxx님이 나갔습니다.
            </div>        
           
          </div>
         
          <div class="input-area">
         
            <div class="form-group">
                <textarea class="form-control" rows="3" id="message" style="resize:none"></textarea>
            </div>
           
            <button type="button" class="btn btn-sm btn-secondary btn-block">전송하기</button>
            <button type="button" class="btn btn-sm btn-danger btn-block">퇴장하기</button>
           
          </div>  
         
        </div>
      </div>

    </section>
    <!-- Section end -->

    <!-- Footer start -->
    <jsp:include page="/WEB-INF/views/common/footer.jsp" />
    <!-- Footer end -->

  </div>
</body>
</html>
 

 

 

- 이 페이지에 접속한 클라이언트들끼리 다대다 채팅이 가능하도록 해본다.

 

 

- 내가 보낸 메세지는 오른쪽에있고 상대방이 보낸건 왼쪽에 있다. 색깔도 다르다. 그렇게 스타일을 적용했다.

 

 

- chat div 안에 chat-area와 input-area div가 있다.

- chat-area 영역에는 메세지가 뜬다.

- 메세지를 감싸는 부모 div를 두고 chat-message 클래스를 준다.

추가로 내가 보낸 메세지는 mine 클래스를 준다. 오른쪽에 노랑으로 뜬다.

추가로 남이 보낸 메세지는 other 클래스를 준다. 왼쪽에 회색으로 뜬다.

- 메세지는 send-message 클래스를 준다.

그리고 상대방이 보낸 메세지는 상대방을 띄우기 위해 span 요소를 추가적으로 준다.

 

- 입장 퇴장은 chat-user 클래스를 준다.

그리고 입장이면 entry, 퇴장이면 exit을 추가로 준다.

 

- input-area 클래스를 보면 메세지를 작성하는 요소가 있다.

하단에는 전송, 퇴장 버튼이 있다.

 

 

 

 

 

ㅁ 서버 start

 

 

 

 

 

- 일단 정적으로 만들어 놓았다.

- 내가보낸 메세지는 노란색이고 이름이 뜨지 않는다.

- 상대방이 보낸 메세지는 회색이고 누가 보냈는지 이름이 뜬다.

프로필 사진도 뜨게끔 코드를 수정해도 된다.

- 누가 입장했을때, 퇴장했을 때 저렇게 보여진다.

- 퇴장하기 버튼을 누르면 이 페이지를 빠져나간다. 전송하기 버튼으로 채팅을 전송할 수 있다.

 

 

 

 

===========================================================================

 

 

 

 

 

ㅁ 웹소켓 세팅

- 웹소켓을 하려면 세팅이 필요하다.

- 만들 클래스도 있고 등록해야 하는 구문들도 있다.

 

 

 

[순서]

1. 웹 소켓 관련 라이브러리 추가 : pom.xml

2. DispatcherServlet 등록 하는 구문에 비동기 작업 관련한 구문 추가 : web.xml

3. EchoHandler 구성 

- 클라이언트가 서버에 연결되었을 때, 서버로 데이터가 전송되었을 때, 클라이언트와의 연결이 끊겼을 때 처리할 구문을 작성할 클래스를 만든다.

4. webSocket관련 등록구문, EchoHandler 등록구문 작성 : servlet-context.xml

5. 웹소켓 사용을 위한 화면 구성 (자바스크립트의 sockjs 이용해서 웹소켓 사용)

 

 

 

 

 

ㅁ pom.xml 

 

 
<?xml version="1.0" encoding="UTF-8"?>
 
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.br</groupId>
  <artifactId>spring</artifactId>
  <name>springWeb</name>
  <packaging>war</packaging>
  <version>1.0.0-BUILD-SNAPSHOT</version>
 
  <properties>
    <java-version>11</java-version>
    <org.springframework-version>5.3.27</org.springframework-version>
    <org.aspectj-version>1.9.19</org.aspectj-version>
    <org.slf4j-version>2.0.7</org.slf4j-version>
  </properties>
 
  <dependencies>
 
    <!-- Spring -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>${org.springframework-version}</version>
      <exclusions>
        <!-- Exclude Commons Logging in favor of SLF4j -->
        <exclusion>
          <groupId>commons-logging</groupId>
          <artifactId>commons-logging</artifactId>
         </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>${org.springframework-version}</version>
    </dependency>

    <!-- Spring Security 모듈 라이브러리 -->
    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-core</artifactId>
      <version>5.7.5</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-web</artifactId>
      <version>5.7.5</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-config</artifactId>
      <version>5.7.5</version>
    </dependency>

    <!-- AspectJ (AOP를 위한 라이브러리)-->
    <dependency> <!-- 원래 있던 것. 이게 있어야 "Advice 동작 시점"의 어노테이션들을 사용할 수 있다. -->
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjrt</artifactId>
      <version>${org.aspectj-version}</version>
    </dependency>
   
    <!-- 위빙 : Advice(공통로직)를 PointCut(핵심로직)에 로딩되도록 함 -->
    <dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjweaver</artifactId>
      <version>${org.aspectj-version}</version>
      <scope>runtime</scope>
    </dependency>  
   
   
   
   
    <!-- Logging (logback) -->
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>1.4.14</version>
      <!-- <scope>test</scope> 이건 지워야 함 -->
    </dependency>

    <!-- (2) slf4j -->
    <dependency> <!-- 기존 dependency 중 이것만 남기고 다 지운다.-->
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>${org.slf4j-version}</version>
    </dependency>
   
    <!-- (3) log4jdbc -->
    <dependency>
      <groupId>org.bgee.log4jdbc-log4j2</groupId>
      <artifactId>log4jdbc-log4j2-jdbc4.1</artifactId>
      <version>1.16</version>
    </dependency>


   

    <!-- @Inject -->
    <dependency>
      <groupId>javax.inject</groupId>
      <artifactId>javax.inject</artifactId>
      <version>1</version>
    </dependency>
       
    <!-- Servlet -->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>4.0.1</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>javax.servlet.jsp</groupId>
      <artifactId>javax.servlet.jsp-api</artifactId>
      <version>2.3.3</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>jstl</artifactId>
      <version>1.2</version>
    </dependency>
 
    <!-- Test -->
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.13.2</version>
      <scope>test</scope>
    </dependency>        
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>${org.springframework-version}</version>
        <scope>test</scope>
    </dependency>
   
   
    <!-- Lombok 라이브러리 추가 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.12</version>
        <scope>provided</scope>
    </dependency>

    <!-- Jackson 라이브러리 추가 -->
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.14.2</version>
    </dependency>


    <!-- db관련 라이브러리 -->
    <!-- (1) ojdbc8 -->
    <dependency>
      <groupId>com.oracle.database.jdbc</groupId>
      <artifactId>ojdbc8</artifactId>
      <version>23.2.0.0</version>
    </dependency>

    <!-- (2) spring-jdbc -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>${org.springframework-version}</version>
    </dependency>

    <!-- (3) mybatis -->
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.5.14</version>
    </dependency>

    <!-- (4) mybatis-spring -->
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis-spring</artifactId>
      <version>2.0.6</version>
    </dependency>

    <!-- (5) commons-dbcp -->
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-dbcp2</artifactId>
      <version>2.9.0</version>
    </dependency>


    <!-- 파일 업로드 관련 라이브러리 -->
    <dependency>
        <groupId>commons-fileupload</groupId>
        <artifactId>commons-fileupload</artifactId>
        <version>1.5</version>
    </dependency>
   
    <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
        <version>2.11.0</version>
    </dependency>
   
    <!-- 스프링 웹소켓 라이브러리 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-websocket</artifactId>
        <version>${org.springframework-version}</version>
    </dependency>

   

  </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-eclipse-plugin</artifactId>
                <version>2.9</version>
                <configuration>
                    <additionalProjectnatures>
                        <projectnature>org.springframework.ide.eclipse.core.springnature</projectnature>
                    </additionalProjectnatures>
                    <additionalBuildcommands>
                        <buildcommand>org.springframework.ide.eclipse.core.springbuilder</buildcommand>
                    </additionalBuildcommands>
                    <downloadSources>true</downloadSources>
                    <downloadJavadocs>true</downloadJavadocs>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>${java-version}</source>
                    <target>${java-version}</target>
                    <compilerArgument>-Xlint:all</compilerArgument>
                    <showWarnings>true</showWarnings>
                    <showDeprecation>true</showDeprecation>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>1.2.1</version>
                <configuration>
                    <mainClass>org.test.int1.Main</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
 

 

 

 

- 맨 마지막에 스프링 웹소켓 라이브러리를 만들었다.

- spring-websocket은 버전이 스프링 버전으로 나온다. 스프링 버전과 맞춰주는게 좋다.

일단 아무 버전이나 dependency를 복붙해온다.

- ${org.springframework-version}

버전을 명시적으로 쓰지 않고 참조하게 한다. 나중에 혹시라도 버전을 수정할 때 용이하게. 

 

- 이 스프링 웹소켓 라이브러리에서 제공하는 클래스를 상속받아서 에코핸들러 클래스를 구성한다.

 

 

 

 

 

 

ㅁ web.xml

 

 

 
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"

  <!-- The definition of the Root Spring Container shared by all Servlets and Filters -->
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>
      /WEB-INF/spring/root-context.xml
      /WEB-INF/spring/spring-security.xml
    </param-value>
  </context-param>
 
  <!-- Creates the Spring Container shared by all Servlets and Filters -->
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>

  <!-- Processes application requests -->
  <servlet>
    <servlet-name>appServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
    <async-supported>true</async-supported>
  </servlet>
   
  <servlet-mapping>
    <servlet-name>appServlet</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>
 
  <!-- 인코딩 필터 등록 -->
  <filter>
    <filter-name>encodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
      <param-name>encoding</param-name>
      <param-value>UTF-8</param-value>
    </init-param>
  </filter>
 
  <filter-mapping>
    <filter-name>encodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
 

</web-app>
 

 

 

- servlet 태그 안에 <async-supported>true</async-supported>를 뒀다.

- 다수의 클라이언트들이 동시에 접속할 수 있게 하려면 비동기를 켜야 한다.

이 설정을 했다고 이때까지 기능구현한게 뭐 달라지는 것은 아니다.

 

- Spring Boot는 기본적으로 웹소켓과 비동기 지원을 활성화 해놨기 때문에 스프링부트에서는 이 설정 불필요)

 

 

 

 

 

 

 

ㅁ com.br.spring.handler 패키지 생성

ㅁ com.br.spring.handler 패키지에 ChatEchoHandler 클래스 생성

 

 
package com.br.spring.handler;

import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.PongMessage;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

public class ChatEchoHandler extends TextWebSocketHandler {

  // 1) afterConnectionEstablished : 웹소켓에 클라이언트가 연결되었을 때 처리할 내용 정의
  @Override
  public void afterConnectionEstablished(WebSocketSession session) throws Exception {
  }

 
  // 2) handleMessage : 웹소켓으로 데이터(텍스트, 파일 등)가 전송되었을 경우 처리할 내용 정의
  @Override
  public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {

  }
 
  // 3) afterConnectionClosed : 웹소켓에 클라이언트가 연결이 끊겼을 때 처리할 내용 정의
  @Override
  public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
  }
 
}
 

 

 

- 지금은 채팅만 할거지만 채팅용 에코핸들러와 알람용 에코핸들러를 따로 둬야 편하게 처리할 수 있다.

- 지금은 파일은 주고받지 않고 텍스트만 주고받는다.

 

- 에코핸들러 역할을 하려면 웹.xml에서 제공하는 클래스를 상속받아야 한다.

- 상속받는 클래스를 ctrl 눌러서 가본다.

- AbstractWebSocketHandler~ 최종적으로는 WebSocketHandler를 구현한다.

- 참고로 바이너리가 파일이다. 

복사해서 가져온다.

- 총 3개의 메소드를 오버라이딩 할 예정이다.

- handleMessage 메소드의 안은 지우고 깔끔하게 다시 작성했다.

- 이 메소드들은 주석에 적은 상황 발생시 자동으로 실행된다.

 

 

- 웹소켓 하나에 다수의 클라이언트들이 실시간으로 연결되어

웹소켓에 연결된 다수의 클라이언트들이 메세지를 주고받을 수 있다.

- 웹소켓 측으로 데이터가 들어 왔다면, 이 웹소켓에 접속된 모든 클라이언트들에게 그 메세지를 다시 발송해야 한다.

그래야 다른 사람들한테 보인다.

 

 

 

 

 

ㅁ servlet- context.xml

 

 
<?xml version="1.0" encoding="UTF-8"?>

  <!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
 
  <!-- Enables the Spring MVC @Controller programming model -->
  <annotation-driven />

  <!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
  <resources mapping="/resources/**" location="/resources/" />
  <resources mapping="/upload/**" location="file:///upload/" />

  <!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
  <beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <beans:property name="prefix" value="/WEB-INF/views/" />
    <beans:property name="suffix" value=".jsp" />
  </beans:bean>
 
  <context:component-scan base-package="com.br.spring" />
 
 
  <!-- webSocket 관련 등록 구문 -->
  <beans:bean class="com.br.spring.handler.ChatEchoHandler" id="chatEchoHandler" />
  <websocket:handlers>
    <websocket:mapping handler="chatEchoHandler" path="/chat" />
    <websocket:handshake-interceptors>
      <beans:bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor" />
    </websocket:handshake-interceptors>
    <websocket:sockjs />
  </websocket:handlers>
 
</beans:beans>
 

 

 

 

(1) <beans:bean class="com.br.spring.handler.ChatEchoHandler" id="chatEchoHandler" />

- 에코핸들러를 빈 등록한다.

- 아래에 웹소켓 관련한 등록 구문을 쓴다.

 

- 스프링 부트는 ChatEchoHandler 클래스 위에 @Component 어노테이션을 붙여서 빈 등록 구문을 대체한다.

 

 

 

(2) <websocket:handlers>
<websocket:mapping handler="chatEchoHandler" path="/chat" />

</websocket:handlers>

 

- 어떤 url 요청을 보낼 시에 ChatEchoHandler가 동작하게끔 할건지 작성한다.

- 다음과 같은 url 요청("/chat")시에 클라이언트가 웹소켓과 연결될 수 있게 하는 구문이다. 

 

 

- 스프링 부트는 WebSocketConfig 클래스를 만들고 클래스 위에 @Configuration, @EnableWebSocket을 붙인다.

그리고 메소드들을 작성해서 (2), (3), (4) 구문들을 대체한다.

 

 

 

 

- 이게 가능하려면 namespaces 탭에서 websocket을 체크 해야한다. 

namespaces 탭의 websocket은 라이브러리를 추가했기 때문에 보여지는 것이다.

 

 

 

 

 

(3)

<websocket:handshake-interceptors>

  <beans:bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor" /> </websocket:handshake-interceptors>

 

- 로그인한 유저 정보는 session의 loginUser라는 멤버 객체에서 가져와야 한다.

- 이건 http session안의 뭐를 쓸 쑤있게 하는 구문이다. 필요없으면 안쓰면 된다.

- 참고로 빈을 만들 때 이름을 안쓰면 알아서 클래스명을 따서 만들어진다. 앞글자가 소문자가 되어서.

- HttpSessionHandshakeInterceptor를 입력하고 ctrl 스페이스로 자동완성시킨다.

- 연결한 클라이언트로부터 http session 정보를 알아낼 수 있게끔 하는 과정이다.

 

 

 

(4) <websocket:sockjs />

 

 

 

 

 

==========================================================================

 

 

 

ㅁ room.jsp

 

 
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
   
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>    
<c:set var="contextPath" value="${pageContext.request.contextPath}" />
   
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>

<style>
    .chat{width:400px; margin:auto; padding:10px; border:1px solid lightgray;}
    .chat-area{height:500px; overflow: auto;}

     .chat-message{margin:10px 0px;}
     .chat-message.mine{display: flex; justify-content:flex-end;}
     
    .chat-message .send-message{
       padding: 5px 7px;
       border-radius: 10px;
       max-width: 190px;
       font-size:0.9em;
       white-space: pre-line;
    }
    .chat-message.other .send-message{background: lightgray;}
    .chat-message.mine .send-message{background: #FFE08C;}
 
    .chat-user {
       text-align:center;
       border-radius:10px;  
       background: lightgray;
       opacity: 0.5;
       margin: 20px 0px;
       color: black;
       line-height: 30px;
    }
 </style>

</head>
<body>
<div class="container p-3">

    <!-- Header, Nav start -->
    <jsp:include page="/WEB-INF/views/common/header.jsp" />
    <!-- Header, Nav end -->

    <!-- Section start -->
    <section class="row m-3" style="min-height: 500px">

      <div class="container border p-5 m-4 rounded">
        <h2 class="m-4">채팅방</h2>
       
        <div class="chat">
   
          <div class="chat-area">
         
            <div class="chat-message mine">
              <div class="send-message">내가보낸 메세지내가보낸 메세지내가보낸 메세지내가보낸 메세지내가보낸 메세지</div>
            </div>  
           
            <div class="chat-message other">
              <span class="send-user">상대방</span>
              <div class="send-message">남이보낸 메세지남이보낸 메세지남이보낸 메세지남이보낸 메세지남이보낸 메세지남이보낸 메세지</div>
            </div>
           
            <div class="chat-user entry">
              xxx님이 들어왔습니다.
            </div>
           
            <div class="chat-user exit">
              xxx님이 나갔습니다.
            </div>        
           
          </div>
         
          <div class="input-area">
         
            <div class="form-group">
                <textarea class="form-control" rows="3" id="message" style="resize:none"></textarea>
            </div>
           
            <button type="button" class="btn btn-sm btn-secondary btn-block" onclick="sendMessage();">전송하기</button>
            <button type="button" class="btn btn-sm btn-danger btn-block" onclick="onClose();">퇴장하기</button>
           
          </div>  
         
        </div>
      </div>


      <script src="https://cdn.jsdelivr.net/sockjs/1/sockjs.min.js"></script>
      <script>
     
        const sock = new SockJS("${contextPath}/chat"); // 이 페이지에 진입한 클라이언트가 웹소켓 서버와 연결된다.
 
        // (스프링) servlet-context에 /chat url 요청시에 ChatEchoHandler가 동작되게 해놨다.
        // 웹소켓 서버와 연결되는 순간 ChatEchoHandler의 afterConnectionEstablished 메소드가 실행된다.
 
       
        sock.onmessage = onMessage; // 웹소켓에서 해당 클라이언트로 메세지 발송시 자동으로 실행할 함수를 지정(매핑)하는 구문
        sock.onclose = onClose; // 웹소켓과 해당 클라이언트간의 연결이 끊겼을 경우 자동으로 실행할 함수를 지정(매핑)하는 구문
        // 이렇게 3줄이 기본세팅이다.
       
       
       
     
        // 메세지를 출력시키는 영역의 요소
        const $chatArea = $(".chat-area");
       
       
        // 메세지 전송시 실행될 함수
        function sendMessage() {
         
        }
     
       
        // 나에게 메세지가 왔을 때 실행될 함수
        function onMessage() {
         
        }
       
       
        // 퇴장시 실행될 함수
        function onClose() {
          location.href = "${contextPath}";
        }
       
       
       
      </script>


    </section>
    <!-- Section end -->

    <!-- Footer start -->
    <jsp:include page="/WEB-INF/views/common/footer.jsp" />
    <!-- Footer end -->

  </div>
</body>
</html>
 

 

 

- sockjs 라이브러리가 필요하다. cdn 방식으로 연동했다. 

스크립트 구문 바로 위에 두는게 정확하다.

 

- SockJS 객체를 만든다. SockJS 객체는 sockjs 라이브러리에 있다. 자바스크립트에서 제공하는 객체가 아니다.

 

 

- sock객체에 onmessage라는 속성이 있다.

이 속성에 웹소켓에서 해당 클라이언트(나)로 메세지 발송시 자동으로 실행할 함수를 지정(매핑)할 수 있다.

- sock객체의 onmessage 속성에 내가 만든 onMessage 함수를 대입한다.

이때는 괄호 열고닫고 구문이 없어야 한다. 있으면 이 함수가 강제로 실행되어 버린다.

 

 

 

 

 

 

ㅁ logback.xml

 

 

 
<?xml version="1.0" encoding="UTF-8"?>
<configuration>


  <appender class="ch.qos.logback.core.ConsoleAppender" name="consoleLog">
    <encoder>
      <pattern>%-5level: [%date{yyyy-MM-dd HH:mm:ss}] [%logger:%line] - %msg%n</pattern>
    </encoder>
  </appender>
 
  <appender class="ch.qos.logback.core.ConsoleAppender" name="websocketLog">
    <encoder>
      <pattern>[%date{HH:mm:ss}] - %msg%n</pattern>
    </encoder>
  </appender>

 
  <logger name="org.springframework" level="INFO" />
  <logger name="com.br.spring" level="DEBUG" />
  <logger name="com.br.spring.handler" level="DEBUG" additivity="false">
    <appender-ref ref="websocketLog" />
  </logger>


  <logger name="jdbc.sqlonly" level="off" />           <!-- 쿼리문만 -->
  <logger name="jdbc.sqltiming" level="INFO" />        <!-- 쿼리문 + 실행시간 -->
  <logger name="jdbc.audit" level="off" />             <!-- JDBC 호출 정보  -->
  <logger name="jdbc.resultset" level="off" />         <!-- ResultSet 호출 정보 -->
  <logger name="jdbc.resultsettable" level="off" />    <!-- ResultSet 결과 (조회결과 테이블) -->
  <logger name="jdbc.conntection" level="off" />       <!-- Connection 호출 정보 -->
 

 
  <root level="WARN">
    <appender-ref ref="consoleLog" />
  </root>  
     

</configuration>


 

 

- 웹소켓 관련 로그도 콘솔에 출력되게 한다.

웹소켓만 별도로 적용시킬 appender를 추가한다. 

- 지금 이건 안해도 되는데 하는 이유는 기존 로그 설정은 앞에 시간정보가 붙어서 보기가 힘들어서 웹소켓 관련 로그는 패턴을 다르게 준 것이다.

 

- additivity="false"가 루트 로거로 전달이 안되게하는 옵션이다. 

 

 

 

 

 

ㅁ 서버 start

 

- 크롬 하나, 크롬 시크릿창(ctrl shift n) 하나로 총 2개의 브라우저에서 로그인한 후, 채팅방에 입장했다.

- 3개로 하고 싶으면 edge 등 다른 브라우저로 또 접속하면 된다. 

 

 

 

 

'Spring' 카테고리의 다른 글

[인터셉터]  (0) 2024.11.05
[웹소켓] 2.  (2) 2024.11.04
[웹프로젝트] 20. 게시글 수정  (1) 2024.10.31
[웹프로젝트] 19. 게시글 삭제  (1) 2024.10.31
[웹프로젝트] 18. 게시글 댓글 작성 (상세페이지)  (1) 2024.10.31