WebSocket
- http 나 https 는 연결형 프로토콜이기는 하지만 한 번 요청을 해서 응답을 받으면 연결이 끊어집니다.
채팅과 같은 애플리케이션을 만들 때 http 나 https 를 이용하는 것은 어려운 작업이다.
- 데이터를 여러번 주고 받고자 하는 경우 연결하고 끊고 다시 연결하고 끊고 하는 작업을 반복해야 한다.
- 데이터를 주고 받는 시간보다 연결과 해제에 많은 자원을 소모한다.
- 그래서 등장방법 중의 하나는 애플리케이션을 설치하는 방법이었습니다.
순수 웹 환경에서 실시간 양방향 통신을 위한 Spec이 HTML5에서 추가가 되었는데 이 Spec이 WebSocket 입니다.
- WebSocket은 한번 연결하면 close 할 때 까지 계속 연결되어 있다.
최근에는 Android나 iOS에서도 웹 소켓에 접속하는 API가 추가
1.클라이언트(웹브라우저)에서의 사용 방법(자바스크립트)
1) WebSocket 객체 생성
new WebSocket("ws://domain/demo")
2) 데이터 전송
WebSocket객체.send("데이터")
3) 데이터 전송 받기 - 콜백 사용
WebSocket객체.addEventListener("message", function(e)){
//매개변수 e가 전송된 데이터입니다.
}
4) 기타 이벤트
open, close
이벤트
5) 연결 해제
WebSocket객체.close()
2.Spring을 이용한 WebSocket 서버 설정
WebSocketHandler 인터페이스를 구현한 클래스를 생성하고 메소드 재정의
클래스위에
@EnableWebSocket
이나 설정 파일에websocket:handlers
태그를 이용해서 웹 소켓 서버 설정
3.WebSocketHandler 인터페이스를 구현한 클래스
- TextWebSocketHandler, AbstractWebSocketHandler
4.의존성 라이브러리
spring-websocket
5.주의할 점
- spring의 websocket은 서블릿 3.0부터 사용 가능
6.실습
1) pom.xml 파일에 spring-websocket 의존성 설정
<!-- 웹 소켓 사용을 위한 의존성 라이브러 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>${org.springframework-version}</version>
</dependency>
2) WebSocket 서버 클래스를 생성
- TextWebSocketHandler 클래스를 상속
- 기본패키지 안에 생성
- com.gmail.hiwony7933.util.ChatHandler
//Bean 생성을 자동으로 해주기 위한 어노테이션
@Component
// 웹 소켓 채팅서버 클래스
public class ChatHandler extends TextWebSocketHandler {
// 접속한 유저 목록을 가질 list를 생성
private static List<WebSocketSession> users = new ArrayList<WebSocketSession>();
// 클라이언트가 접속 했을 때 호출될 메소드
// 매개변수로 대입된 데이터가 접속한 클라이언트
@Override
public void afterConnectionEstablished(WebSocketSession session) {
// List에 추가
users.add(session);
}
// 클라이언트가 접속을 해제했을 때 호출될 메소드
// 매개변수로 대입된 데이터가 접속을 해제한 클라이언트
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
// List에서 제거
users.remove(session);
}
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) {
// 전송된 메시지를 출력
System.out.println(message.getPayload() + "가 전송됨 ");
// 전송받은 메시지를 모든 유저에게 다시 전송
for (WebSocketSession ses : users) {
try {
ses.sendMessage(new TextMessage(message.getPayload()));
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
1) ChatHandler 클래스를 URL과 매핑하는 코드를 servlet-context.xml 파일에 작성
- sevlet-context.xml 파일에 websorcket 네임스페이스를 추가
<!-- WebSocket 클래스와 URL 매핑 -->
<websocket:handlers>
<!-- handler에서 bean의 id를 작성하고 path는 클라이언트가 접속할 URL -->
<websocket:mapping handler="chatHandler" path="/chat-ws"/>
</websocket:handlers>
4) hom.jsp 파일에 채팅 페이지로 이동할 링크를 추가
<a href="chat">WebSocket을 이용한 채팅구현 </a><br/>
5) HomeController 클래스의 위의 요청을 처리하는 메소드를 작성
//chat 이라는 요청이 오면 chat이라는 문자열을 가지고 ViewReslover 설정을 확인해서
// 뷰 페이지를 결정 - WEB-INF/views/?.jsp
@RequestMapping(value="/chat", method=RequestMethod.GET)
public String chat(HttpServletRequest request, Model model ) {
// 뷰 이름을 리턴
return "chat";
}
6) views 디렉토리에 chat.jsp 파일을 만들고 작성
pom.xml 의 servlet 버전이 2.5 -> 3.1 버전으로 교체 했다.
<!-- Servlet -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>채팅</title>
<style>
#chatArea{
width:200px;
height:100px;
overflow-y:auto;
border:1px solid black;
}
</style>
</head>
<body>
이름<input type="text" id="nickname" />
<input type="button" id="enterbtn" value="입장" />
<input type="button" id="exitbtn" value="퇴장" />
<h1>채팅창</h1>
<div id="chatArea">
<div id="chatmessagearea"></div>
</div>
<br />
<input type="text" id="message" />
<input type="button" id="sendbtn" value="전송" />
<script>
//웹 소켓 변수
var wsocket
//문자열을 출력하는 함수
var appendMessage = function(msg){
document.getElementById("chatmessagearea").innerHTML =
msg + "<br/>" + document.getElementById("chatmessagearea").innerHTML
}
// 이벤트 처리 함수
var onOpen = function(){
appendMessage("연결되었습니다");
}
var onClose = function(){
appendMessage("연결 해제되었습니다.");
wsocket.close();
}
var onMessage = function(evnt) {
var data = evnt.data;
appendMessage(data);
}
var send = function(){
// 입력한 내용을 WebSocket 서버에게 전달하고 message 란은 클리어
var nickname = document.getElementById("nickname").value;
var msg = document.getElementById("message").value;
wsocket.send(nickname + ":" + msg);
document.getElementById("message").value='';
}
//웹 소켓 연결함수
var connect = function(){
//http://localhost:8080/hiwony7933/chat-ws - 자기 컴퓨터에서만 접속
wsocket = new WebSocket("ws://localhost:8080/hiwony7933/chat-ws")
//이벤트 핸들러 연결
wsocket.addEventListener("open", onOpen);
wsocket.addEventListener("message", onMessage);
};
//message 입력란에서 키보드 이벤트가 발생하면
document.getElementById("message").addEventListener("keypress", function(e){
//enter를 누르면 send() 호출
event = e || window.event;
var keycode=(event.keyCode?event.keyCode:event.which);
if(keycode == 13) {
send()
}
event.stopPropagation();
})
//버튼들의 이벤트 처리
document.getElementById('sendbtn').addEventListener("click",function(e){
send();
})
document.getElementById('enterbtn').addEventListener("click",function(e){
connect();
})
document.getElementById('exitbtn').addEventListener("click",function(e){
onClose();
})
</script>
</body>
</html>
로그인 기능 추가
1.데이터베이스에 접속해서 로그인 처리를 테이블을 생성하고 샘플 데이터를 입력
CREATE TABLE member(
num number(10),
userud varchar2(100) UNIQUE,
userpw varchar2(100) NOT NULL,
nickname varchar2(100),
PRIMARY KEY (num)
);
INSERT INTO MEMBER values(1, 'root', '1234', '관리자');
INSERT INTO MEMBER values(2, 'ggangpae', '1234', '두목');
INSERT into MEMBER values(3, 'hiwony', '1234', '나');
COMMIT;
SELECT * FROM MEMBER;
2.테이블과 매핑할 DTO 클래스를 생성
- com.gmail.hiwony7933.domain.Member
package com.gmail.hiwony7933.domain;
import lombok.Data;
@Data
public class Member {
private int num;
private String userid;
private String userpw;
private String nickname;
}
3.dao 패키지와 클래스와 데이터베이스 테이블을 매핑시키는 xml 파일을 생성
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="com.gmail.hiwony7933.domain">
<class name="Member" table="Member">
<id name="num" column="num">
</id>
<property name="userid" column="userid" />
<property name="userpw" column="userpw" />
<property name="nickname" column="nickname" />
</class>
</hibernate-mapping>
4.Hibernate 설정 파일을 등록하는 코드를 추가 root-context.xml 파일에 추가
<!-- 추가 -->
<value>com/gmail/hiwony7933/dao/member.hbm.xml</value>
5.Hibernate 사용을 위한 Dao 클래스를 만들고 로그인 관련 메소드를 작성
- dao/MemberDao
@Repository
public class MemberDao {
// 하이버네이트 사용 관련 객체를 주입받기
@Autowired
private SessionFactory sessionFactory;
// 로그인 관련 메소드
// id를 매개변수로 받아서 일치하는 데이터가 있는지 찾아옵니다.
public Member login(Member member) {
// userid가 기본키가 아니라서
// SQL을 이용해서 직접 조회
List<Member> list =
(List<Member>) sessionFactory.getCurrentSession()
.createSQLQuery("select * from member where userid=:userid")
.addEntity(Member.class)
.setString("userid", member.getUserid()).list();
// 조회된 데이터가 없으면 null을 리턴하고 데이터가 없으면 데이터를 리턴
if(list.size() == 0) {
return null;
}else {
return list.get(0);
}
}
}
6.home.jsp 파일에 로그인와 로그아웃 링크를 생성
- 로그인에 성공하면 member 라는 속성에 회원정보를 저장할 것입니다.
- 로그인 여부는 session에 member 라는 속성에 데이터가 있으면 로그인 된것이고 그렇지 않으면 로그인이 되지 않은 것으로 간주
<!-- if 나 forEach를 사용하기 위한 태그 라이브러리를 설정 -->
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<c:if test="${member == null}">
<a href="login">로그인 </a><br/>
</c:if>
<c:if test="${member != null }">
${member.nickname}님 <a href="logout">로그아웃 </a><br/>
</c:if>
7.login 요청이 GET 방식으로 오면 login 페이지로 포워딩 하도록 메소드를 HomeController에 작성
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>로그인</title>
</head>
<body>
<form method="post" id="loginform">
아이디<input type="text" id="userid" name="userid"
required="required"/><br/>
비밀번호<input type="text" id="userpw" name="userpw"
required="required"/><br/>
<input type="submit" value="전송"/>
</form>
</body>
</html>
8.MemberService 인터페이스를 만들고 로그인 처리 메소드를 선언
public interface MemberService {
//로그인 처리 메소드
public Member login(HttpServletRequest request);
}
9.MemberService 인터페이스를 구현한 MemberServiceImpl 클래스를 만들고 로그인 처리 메소드를 구현
@Service
public class MemberServiceImpl implements MemberService {
@Autowired
private MemberDao memberDao;
@Override
@Transactional
public Member login(HttpServletRequest request) {
Member member = null;
//파라미터 읽기
String userid = request.getParameter("userid");
String userpw = request.getParameter("userpw");
//member에 인스턴스를 대입하고 userid를 설정
member = new Member();
member.setUserid(userid);
//userid에 해당하는 데이터 찾아오기
member = memberDao.login(member);
//userid에 해당하는 데이터가 있다면
if(member != null) {
if(member.getUserpw().equals(userpw)) {
//로그인 성공한 경우는 session에 로그인 정보를 저장
member.setUserpw(null);
request.getSession().setAttribute("member", member);
}else {
//로그인 실패
member = null;
}
}
return member;
}
}
10.HomeController에서 login 요청이 POST 방식으로 오면 처리하는 메소드를 생성
1) 새로운 서비스를 주입받는 코드를 작성
@Autowired
private MemberService memberService;
2) 요청을 처리하는 메소드를 생성
@RequestMapping(value="/login", method=RequestMethod.POST)
public String login(HttpServletRequest request, Model model,
RedirectAttributes attrs) {
//RedirectAttributes는 redirect 할 때 1번만 사용하는 데이터를
//저장할 수 있는 Spring이 제공하는 클래스
//서비스의 메소드 호출
Member member = memberService.login(request);
//로그인 처리도 redirect로 이동
if(member == null) {
//로그인 실패의 경우 msg를 저장하고 login으로 다시 이동
attrs.addFlashAttribute(
"msg", "없는 아이디이거나 잘못된 비밀번호입니다.");
return "redirect:login";
}else {
//로그인 성공이면 메인 페이지로 이동
return "redirect:./";
}
}
11.login.jsp 파일에 로그인 실패 메시지를 출력할 영역을 생성
<div>${msg}</div>
12.로그아웃 구현
- login 된 사실을 을 Session에 데이터를 저장해서 그 데이터를 확인해서 하게 됩니다.
- logout은 Session에 저장한 데이터를 삭제하면 됩니다.
- 데이터 1개를 삭제할 때는
session.removeAttribute("key");
- 세션 전체를 삭제하고자 할 때는
session.invalidate();
- 일정 시간 동안 세션을 사용하지 않으면 세션을 자동삭제자 하는 경우에는 web.xml 파일에 session-config 설정을 추가하면 되는데 시간의 단위는 분입니다.
- 데이터 1개를 삭제할 때는
1) HomeController 클래스에 logout을 처리하는 메소드 추가
@RequestMapping(value = "/logout", method = RequestMethod.GET)
public String logout(HttpServletRequest request, HttpSession session) {
//Session 객체 만들기
//request.getSession() 해도 되고 Controller의 요청 처리 메소드에 매개변수로 추가해도 된다.
// 세션 초기화
session.invalidate();
// 로그인과 로그아웃도 redirect를 합니다.
return "redirect:./";
}
2) web.xml 파일에 5분동안 세션을 사용하지 않으면 세션을 자동 초기화 해주는 설정을 추가
- 모바일 페이지는 기렉 설정하는 편이고, 일반 PC용 페이지는 짧게 설정한다.
<!-- 세션을 자동 초기화 해주는 설정 -->
<session-config>
<session-timeout>5</session-timeout>
</session-config>
13.메인 화면의 데이터 출력하는 부분을 수정
1) 메인화면의 데이터 출력 영역의 크기를 제한
출력하는 데이터를 div 태그로 감싸고 div 태그의 높이를 설정한 후 overflow 속성을 이용해서 스크롤 바가 생기도록 만들어주면 됩니다.
- div로 감싸고