YJ의 새벽
Spring ( Web Socket ( 메시지 ) ) 본문
- WebSocket
- 브라우저와 웹 서버간의 전이중 통신을 지원하는 프로토콜
전이중 통신(Full Duplex) : 두대의 단말기가 데이터를 동시에 송/수신 하기 위해
각각 독립된 회선을 사용하는 통신 방식(ex. 전화 )
- HTML5 부터 지원
- Java 7 부터 지원 (8 버전 이상 사용 권장)
- Spring Framework 4 이상 부터 지원
WebSocketHandler 인터페이스 : 웹소켓을 위한 메소드를 지원하는 인터페이스
-> WebSocketHandler 인터페이스를 상속받은 클래스를 이용해 웹소켓 기능을 구현
**** WebSocketHandler 주요 메소드 ***
void handlerMessage(WebSocketSession session, WebSocketMessage message)
- 클라이언트로부터 메세지가 도착하면 실행
void afterConnectionEstablished(WebSocketSession session)
- 클라이언트와 연결이 완료되고, 통신할 준비가 되면 실행
void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus)
- 클라이언트와 연결이 종료되면 실행
void handleTransportError(WebSocketSession session, Throwable exception)
- 메세지 전송중 에러가 발생하면 실행
----------------------------------------------------------------------------
TextWebSocketHandler : WebSocketHandler 인터페이스를 상속받아 구현한 텍스트 메세지 전용 웹소켓 핸들러 클래스 handlerTextMessage(WebSocketSession session, TextMessage message)
- 클라이언트로부터 텍스트 메세지를 받았을때 실행
--- Websocket 요청 시 핸들러 클래스와 연결하기 . ( servlet-context.xml 에서 bean 등록 )
<!-- Websocket 요청 시 핸들러 클래스와 연결하기. -->
<beans:bean id="chatHandler" class="edu.kh.comm.chat.model.websocket.ChatWebsocketHandler"></beans:bean>
<websocket:handlers>
<!-- 웹소켓 요청(주소)을 처리할 bean 지정 -->
<websocket:mapping handler="chatHandler"
path="/chat" />
<websocket:handshake-interceptors>
<!-- interceptor: http통신에서 req, resp 가로채는 역할 handshake-interceptors: 요청
관련 데이터 중 HttpSession(로그인 정보, 채팅방번호) 을 가로채서 WebSocketSession에 넣어주는 역할 -->
<beans:bean
class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor" />
</websocket:handshake-interceptors>
<!-- SockJS 라이브러리를 이용해서 만들어진 웹소켓 객체임을 인식 -->
<websocket:sockjs />
</websocket:handlers>
---pom.xml 에 추가.
<!-- Spring WebSocket -->
<!-- https://mvnrepository.com/artifact/org.springframework/spring-websocket -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<!-- jackson bind -->
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.8</version>
</dependency>
---- jsp 를 참고하자 .
---- js 를 참고하자.
----- ChatWebSocketHandler . class
public class ChatWebsocketHandler extends TextWebSocketHandler {
@Autowired
private ChatService service;
// Set<WebSocketSession> 을 왜 만들었는가 ???
// -- WebSocketSession == 웹소켓에 연결된 클라이언트의 세션을 가지고있다.
// --> 세션을 통해서 누가 연결했는지 알수 있다 !!!
// WebSocketSession 을 모아둔다 ???
// -- 현재 웹소켓에 연결되어있는 모든 클라이언트를 알 수 있다.
// --> Set 을 분석해서 원하는 클라이언트를 찾아서 메시지(채팅) 을 전달 할 수 있다 !!!
private Set<WebSocketSession> sessions
= Collections.synchronizedSet(new HashSet<WebSocketSession>());
// synchronizedSet : 동기화된 Set을 반환
// --> 멀티쓰레드 환경에서 하나의 컬렉션 요소에 여러 스레드가 접근하면 충돌이 발생할수있으므로
// 동기화(충돌이 안나도록 줄세움 )를 진행
// 클라이언트와 연결이 완료되고, 통신할 준비가 되면 수행 ( == new SockJS() )
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// WebSocketSession : 웹소켓에 접속/요청한 클라이언트의 세션.
System.out.println(session.getId()+" 연결됨 "); // 세션 아이디 확인
sessions.add(session); // WebSocketSession 을 Set에 추가.
}
// 클라이언트로부터 텍스트 메시지를 전달 받았을때 수행 .
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// TextMessage : 웹소켓을 이용해 텍스트로 전달된 메시지가 담겨있는 객체 .
// payload : 전송되는 데이터
// message.getPayload() : JSON
System.out.println("전달된 메시지 : "+ message.getPayload());
// Jackson 라이브러리 : Java에서 JSON 을 다루기위한 라이브러리
// Jackson-databind 라이브러리 : ObjectMapper 객체를 이용해서
// JSON 데이터를 특정 VO 필드에 맞게 자동 매핑
ObjectMapper objectMapper = new ObjectMapper();
ChatMessage chatMessage = objectMapper.readValue(message.getPayload(), ChatMessage.class);
// 시간 세팅
chatMessage.setCreateDate( new Date( System.currentTimeMillis()));
System.out.println(chatMessage);
// 채팅 메시지 DB 삽입
int result = service.insertMessage(chatMessage);
if ( result > 0 ) {
// 같은 방에 접속중인 클라이언트에게만 메시지 보내기
// --> Set<WebSocketSession> 에서 같은 방 클라이언트만 골라내기
for ( WebSocketSession s : sessions ) {
// WebSocketSession == HttpSession (로그인정보,채팅방정보) 을 가로챈것..
int chatRoomNo = (Integer) s.getAttributes().get("chatRoomNo");
// WebSocketSession에 담겨있는 채팅장 번호와
// 메시지에 담겨있는 채팅방 번호가 같은 경우 === 같은방 클라이언트
if ( chatRoomNo == chatMessage.getChatRoomNo() ) {
//같은방 클라이언트에게 JSON 형식의 메시지를 보냄
s.sendMessage( new TextMessage( new Gson().toJson(chatMessage) ));
}
}
}
}
// 클라이언트와 연결이 종료되면 수행 .
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
sessions.remove(session); // 웹소켓연결 종료시 종료된 WebSocketSession을 Set에서 제거
}
}
---- ChattingController. class
@Controller
@SessionAttributes({"loginMember", "chatRoomNo"})
public class ChattingController {
@Autowired
private ChatService service;
// 채팅방 목록 조회
@GetMapping("/chat/roomList")
public String chattingList(Model model) {
List<ChatRoom> chatRoomList = service.selectChatRoomList();
model.addAttribute("chatRoomList", chatRoomList);
return "chat/chatRoomList";
}
// 채팅방 만들기
@PostMapping("/chat/openChatRoom")
public String openChatRoom(@ModelAttribute("loginMember") Member loginMember, Model model,
ChatRoom room, RedirectAttributes ra) {
room.setMemberNo(loginMember.getMemberNo());
int chatRoomNo = service.openChatRoom(room);
String path = "redirect:/chat/";
if(chatRoomNo > 0) {
path += "room/" + chatRoomNo;
}else {
path += "roomList";
ra.addFlashAttribute("message","채팅방 만들기 실패");
}
return path;
}
// 채팅방 입장
@GetMapping("/chat/room/{chatRoomNo}")
public String joinChatRoom(@ModelAttribute("loginMember") Member loginMember, Model model,
@PathVariable("chatRoomNo") int chatRoomNo,
ChatRoomJoin join,
RedirectAttributes ra) {
join.setMemberNo(loginMember.getMemberNo());
List<ChatMessage> list = service.joinChatRoom(join);
if(list != null) {
model.addAttribute("list", list);
model.addAttribute("chatRoomNo", chatRoomNo); // session에 올림
return "chat/chatRoom";
}else {
ra.addFlashAttribute("message","채팅방이 존재하지 않습니다.");
return "redirect:../roomList";
}
}
// 채팅방 나가기
@GetMapping("/chat/exit")
@ResponseBody
public int exitChatRoom(@ModelAttribute("loginMember") Member loginMember,
ChatRoomJoin join) {
join.setMemberNo(loginMember.getMemberNo());
return service.exitChatRoom(join);
}
}
---- ChatService . class
@Service
public class ChatServiceImpl implements ChatService{
@Autowired
private ChatDAO dao;
// 채팅 목록 조회
@Override
public List<ChatRoom> selectChatRoomList() {
return dao.selectChatRoomList();
}
// 채팅방 만들기
@Override
public int openChatRoom(ChatRoom room) {
return dao.openChatRoom(room);
}
// 채팅방 입장 + 내용 얻어오기
@Override
public List<ChatMessage> joinChatRoom(ChatRoomJoin join) {
// 현재 회원이 해당 채팅방에 참여하고 있는지 확인
int result = dao.joinCheck(join);
if(result == 0) { // 참여하고 있지 않은 경우 참여
dao.joinChatRoom(join);
}
// 채팅 메세지 목록 조회
return dao.selectChatMessage(join.getChatRoomNo());
}
// 채팅 메세지 삽입
@Override
public int insertMessage(ChatMessage cm) {
// cm.setMessage(Util.XSSHandling(cm.getMessage()));
cm.setMessage(Util.newLineHandling(cm.getMessage()));
return dao.insertMessage(cm);
}
// 채팅방 나가기
@Transactional(rollbackFor = Exception.class)
@Override
public int exitChatRoom(ChatRoomJoin join) {
// 채팅방 나가기
int result = dao.exitChatRoom(join);
if(result > 0) { // 채팅방 나가기 성공 시
// 현재 방에 몇명이 있나 확인
int cnt = dao.countChatRoomMember(join.getChatRoomNo());
// 0명일 경우 방 닫기
if(cnt == 0) {
result = dao.closeChatRoom(join.getChatRoomNo());
}
}
return result;
}
}
---- ChatDAO . class
@Repository
public class ChatDAO {
@Autowired
private SqlSessionTemplate sqlSession;
/** 채팅방 목록 조회
* @return chatRoomList
*/
public List<ChatRoom> selectChatRoomList() {
return sqlSession.selectList("chattingMapper.selectChatRoomList");
}
/** 채팅방 만들기
* @param room
* @return chatRoomNo
*/
public int openChatRoom(ChatRoom room) {
int result = sqlSession.insert("chattingMapper.openChatRoom", room);
if(result > 0) return room.getChatRoomNo();
return 0;
}
/** 채팅방 참여 여부 확인
* @param join
* @return result
*/
public int joinCheck(ChatRoomJoin join) {
return sqlSession.selectOne("chattingMapper.joinCheck", join);
}
/** 채팅방 참여하기
* @param join
*/
public void joinChatRoom(ChatRoomJoin join) {
sqlSession.insert("chattingMapper.joinChatRoom", join);
}
/** 채팅 메세지 목록 조회
* @param chatRoomNo
* @return list
*/
public List<ChatMessage> selectChatMessage(int chatRoomNo) {
return sqlSession.selectList("chattingMapper.selectChatMessage", chatRoomNo);
}
/** 채팅 메세지 삽입
* @param cm
* @return result
*/
public int insertMessage(ChatMessage cm) {
return sqlSession.insert("chattingMapper.insertMessage", cm);
}
/** 채팅방 나가기
* @param join
* @return result
*/
public int exitChatRoom(ChatRoomJoin join) {
return sqlSession.delete("chattingMapper.exitChatRoom", join);
}
/** 채팅방 인원 수 확인
* @param chatRoomNo
* @return cnt
*/
public int countChatRoomMember(int chatRoomNo) {
return sqlSession.selectOne("chattingMapper.countChatRoomMember", chatRoomNo);
}
/** 채팅방 닫기
* @param chatRoomNo
* @return result
*/
public int closeChatRoom(int chatRoomNo) {
return sqlSession.update("chattingMapper.closeChatRoom", chatRoomNo);
}
}
--- chatting-mapper .xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="chattingMapper">
<resultMap type="chatRoom" id="chatroom_rm">
<id property="chatRoomNo" column="CHAT_ROOM_NO" />
<result property="title" column="TITLE" />
<result property="status" column="STATUS" />
<result property="memberNo" column="MEMBER_NO" />
<result property="memberNickname" column="MEMBER_NICK" />
<result property="cnt" column="CNT" />
</resultMap>
<resultMap type="chatMessage" id="chatMessage_rm">
<id property="cmNo" column="CM_NO" />
<result property="message" column="MESSAGE" />
<result property="createDate" column="CREATE_DT" />
<result property="chatRoomNo" column="CHAT_ROOM_NO" />
<result property="memberNo" column="MEMBER_NO" />
<result property="memberNickname" column="MEMBER_NICK" />
</resultMap>
<!--=========================================================================================-->
<!-- 채팅방 목록 조회 -->
<select id="selectChatRoomList" resultMap="chatroom_rm">
SELECT CHAT_ROOM_NO, TITLE, MEMBER_NICK,
(SELECT COUNT(*) FROM CHAT_ROOM_JOIN B WHERE A.CHAT_ROOM_NO = B.CHAT_ROOM_NO) CNT
FROM CHAT_ROOM A
JOIN MEMBER_S USING(MEMBER_NO)
WHERE STATUS = 'Y'
ORDER BY CHAT_ROOM_NO DESC
</select>
<!-- 채팅방 만들기 -->
<insert id="openChatRoom" useGeneratedKeys="true">
<selectKey keyProperty="chatRoomNo" resultType="_int" order="BEFORE">
SELECT SEQ_CR_NO.NEXTVAL FROM DUAL
</selectKey>
INSERT INTO CHAT_ROOM VALUES
(#{chatRoomNo}, #{title}, DEFAULT, #{memberNo})
</insert>
<!-- 채팅방 참여 여부 확인 -->
<select id="joinCheck" resultType="_int">
SELECT COUNT(*) FROM CHAT_ROOM_JOIN
WHERE CHAT_ROOM_NO = #{chatRoomNo}
AND MEMBER_NO = #{memberNo}
</select>
<!-- 채팅방 참여하기 -->
<insert id="joinChatRoom">
INSERT INTO CHAT_ROOM_JOIN
VALUES(#{memberNo}, #{chatRoomNo})
</insert>
<!-- 채팅 메세지 목록 조회 -->
<select id="selectChatMessage" resultMap="chatMessage_rm">
SELECT MESSAGE, CREATE_DT, MEMBER_NO, MEMBER_NICK
FROM CHAT_MESSAGE
JOIN MEMBER_S USING(MEMBER_NO)
WHERE CHAT_ROOM_NO = #{chatRoomNo}
ORDER BY CM_NO
</select>
<!-- 채팅 메세지 삽입 -->
<insert id="insertMessage">
INSERT INTO CHAT_MESSAGE
VALUES(SEQ_CM_NO.NEXTVAL, #{message}, DEFAULT, #{chatRoomNo}, #{memberNo})
</insert>
<!-- 채팅방 나가기 -->
<delete id="exitChatRoom">
DELETE FROM CHAT_ROOM_JOIN
WHERE MEMBER_NO = #{memberNo}
AND CHAT_ROOM_NO = #{chatRoomNo}
</delete>
<!-- 채팅방 인원 수 확인 -->
<select id="countChatRoomMember" resultType="_int">
SELECT COUNT(*) FROM CHAT_ROOM_JOIN
WHERE CHAT_ROOM_NO = #{chatRoomNo}
</select>
<!-- 채팅방 닫기 -->
<update id="closeChatRoom">
UPDATE CHAT_ROOM SET
STATUS = 'N'
WHERE CHAT_ROOM_NO = #{chatRoomNo}
</update>
</mapper>
-- 채팅
CREATE TABLE CHAT_ROOM (
CHAT_ROOM_NO NUMBER PRIMARY KEY,
TITLE VARCHAR2(200) NOT NULL,
STATUS CHAR(1) DEFAULT 'Y' CHECK(STATUS IN('Y','N')),
MEMBER_NO NUMBER REFERENCES MEMBER_S
);
COMMENT ON COLUMN CHAT_ROOM.CHAT_ROOM_NO IS '채팅방번호';
COMMENT ON COLUMN CHAT_ROOM.TITLE IS '채팅방제목';
COMMENT ON COLUMN CHAT_ROOM.STATUS IS '채팅방상태(정상:Y, 삭제:N)';
COMMENT ON COLUMN CHAT_ROOM.MEMBER_NO IS '회원번호(방 개설자)';
CREATE TABLE CHAT_MESSAGE (
CM_NO NUMBER PRIMARY KEY,
MESSAGE VARCHAR2(4000) NOT NULL,
CREATE_DT DATE DEFAULT SYSDATE,
CHAT_ROOM_NO NUMBER REFERENCES CHAT_ROOM,
MEMBER_NO NUMBER REFERENCES MEMBER_S
);
COMMENT ON COLUMN CHAT_MESSAGE.CM_NO IS '채팅메세지번호';
COMMENT ON COLUMN CHAT_MESSAGE.MESSAGE IS '작성한 채팅 메세지';
COMMENT ON COLUMN CHAT_MESSAGE.CREATE_DT IS '메세지 작성 시간';
COMMENT ON COLUMN CHAT_MESSAGE.CHAT_ROOM_NO IS '채팅방번호';
COMMENT ON COLUMN CHAT_MESSAGE.MEMBER_NO IS '회원번호';
CREATE TABLE CHAT_ROOM_JOIN (
MEMBER_NO NUMBER REFERENCES MEMBER_S,
CHAT_ROOM_NO NUMBER REFERENCES CHAT_ROOM,
PRIMARY KEY(MEMBER_NO, CHAT_ROOM_NO)
);
COMMENT ON COLUMN CHAT_ROOM_JOIN.MEMBER_NO IS '회원번호';
COMMENT ON COLUMN CHAT_ROOM_JOIN.CHAT_ROOM_NO IS '채팅방번호';
CREATE SEQUENCE SEQ_CR_NO;
CREATE SEQUENCE SEQ_CM_NO;
---mybatis-config .xml
<typeAlias type="edu.kh.comm.chat.model.vo.ChatRoom" alias="chatRoom"/>
<typeAlias type="edu.kh.comm.chat.model.vo.ChatRoomJoin" alias="chatRoomJoin"/>
<typeAlias type="edu.kh.comm.chat.model.vo.ChatMessage" alias="chatMessage"/>
<mapper resource="/mappers/chatting-mapper.xml"/>
'Spring > Spring' 카테고리의 다른 글
Spring ( AOP 관점 지향 프로그래밍 ) (0) | 2023.05.12 |
---|---|
Spring ( @Scheduled ) (0) | 2023.05.11 |
Spring 19 ( 댓글 , 대댓글 삽입,수정,삭제 ) (1) | 2023.05.10 |
Spring 18 ( 게시글 삭제 ) (0) | 2023.05.09 |
Spring 17 ( 게시글 작성/수정) (0) | 2023.05.08 |