개발목표
1. 웹소켓을 사용한 채팅방을 구현한다.
2. 웹소켓 연결 시 토큰 값을 받아 연결 가능 여부를 판단한다.
3. 웹소켓 연결 후 채팅방 입장을 요청하며, 채팅방은 DB로 관리한다.
4. 재연결 시도 시(강제종료/연결불안정등) 3.에서 기록된 정보로 채팅방에 재입장한다.
5. 특정 요청 시 채팅방에 포함된 사용자에게 내용을 서버에서 브로드캐스팅한다.
공통코드 프로토타입
1. @Config
- 채팅방 정보는 WAS메모리에 기록될 예정이다. (추후 변경가능성 높음)
- 웹소켓 연결시 인터셉터가 연결가능 여부를 판단한다.
- 연결가능 여부는 Acess Token으로 판단한다.
- 소켓세션 쓸만하도록 정보를 가공하는 역할을 한다.
- 핸들러를 따로 생성하여, 메세지를 받았을 때, 연결 시작/종료에 대한 이벤트를 메니징 한다.
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private final ServerSocketHandler serverSocketHandler;
@Autowired
HttpHandshakeInterceptor httpHandshakeInterceptor;
//최초 커넥션을 담는 Map (session Id = WebSocketSession 객체 저장)
static Map<String, WebSocketSession> sessionMap = new HashMap<String, WebSocketSession>();
//그룹오더 방을 담는 Map ( room Id = sessionMap Map 저장 )
static Map<String, Map<String, WebSocketSession>> roomSessionMap = new HashMap<String, Map<String, WebSocketSession>>();
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(serverSocketHandler, "/live").setAllowedOrigins("*").addInterceptors(httpHandshakeInterceptor);
// interceptor for adding httpsession into websocket session
}
public static Map<String, WebSocketSession> getSessionMap() {
return sessionMap;
}
public static Map<String, Map<String, WebSocketSession>> getRoomSessionMap() {
return roomSessionMap;
}
}
2. 인터셉터
- HandshakeInterceptor를 구현하며, beforeHandshake를 Override한다.
- 세션정보는 @Config에서 정의한 Map에 담을 수 있도록 가공한다.
@Service
public class HttpHandshakeInterceptor implements HandshakeInterceptor {
@Autowired
JwtAuthBean jwtAuthBean;
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Map<String, Object> httpSession) throws Exception {
boolean check = true;
if (request instanceof ServletServerHttpRequest) {
String env = System.getProperty("spring.profiles.active");
//운영계에서 검증
if (BzUtils.equals("prod", env)) {
// at값 검증
String atValue = UriComponentsBuilder.fromUri(request.getURI()).build().getQueryParams()
.getFirst(ProcessMeta.at.name());
if (BzUtils.isNotEmpty(atValue)) {
Map<String, Object> checkMap = new HashMap<String, Object>();
checkMap.put(ProcessMeta.at.name(), atValue);
check = jwtAuthBean.checkValidJwtAT(checkMap);
}
if (check) {
//at토큰에서 uid를 가져와 Map Key로 구성
SecretKey key = Keys.hmacShaKeyFor(poomKey.getBytes());
Jws<Claims> claims = Jwts.parserBuilder().setSigningKey(key).build()
.parseClaimsJws(String.valueOf(atValue));
//토큰 payload에 uid가 없거나, 비회원이면 소켓연결을 할 수 없음
if(BzUtils.isNotEmpty(claims.getBody().get(DBMeta.uid.name()))&&!BzUtils.equals(claims.getBody().get(DBMeta.uid.name()),"none")){
httpSession.put(ProcessMeta.clientSession.name(),claims.getBody().get(DBMeta.uid.name()));
}else {
check = false;
}
}
}else {
httpSession.put(ProcessMeta.clientSession.name(),"dev");
}
if (check) {
loggingManage(request.getURI().getPath());
}
}
return check;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Exception exception) {
// TODO Auto-generated method stub
}
private void loggingManage(String pathString) {
MDC.put("traceId", UUID.randomUUID().toString());
MDC.put("method", pathString);
}
}
3. 핸들러
- TextWebSocketHandler를 상속받는다.
- 소켓 커넥션 후 주고받는 내용은 TextMessage 객체를 사용하고, 로직에서는 String으로 바꿔 사용한다.
- afterConnectionEstablished/afterConnectionClosed 에서는 소켓 커넥션 시작/종료 시 2.에서 가공한 정보를 정의한 Map에 넣고 빼는 역할을 수행한다.
- client가 전송한 텍스트 메세지가 있으면, 팩토리 패턴으로 Biz 로직을 수행한다.
- Biz 로직 중 채팅방에 포함된 사용자(세션)이 있다면 브로드 캐스팅하는 로직을 만들어야 한다.
public class ServerSocketHandler extends TextWebSocketHandler {
@Autowired
ApplicationContext ctx;
@Autowired
SocketManageFactory SocketManageFactory;
// message
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
Gson gson = new Gson();
Type type = new TypeToken<Map<String, Object>>() {}.getType();
log.info("rcv Sockect {}", message.getPayload());
// Convert JSON string to Map
Map<String, Object> param = gson.fromJson(message.getPayload(), type);
ProcInterface procInterface = (ProcInterface) SocketManageFactory.getBean(ctx, param);
if (BzUtils.isNotEmpty(procInterface)&&procInterface.beforProcess(param)) {
procInterface.doProcess(param);
Map<String, Object> servieRet = procInterface.afterProcess(param);
log.info("broadCast Sockect {}",servieRet);
}
}
// connection established
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
Map<String, Object> map = session.getAttributes();
String httpSessionId = (String) map.get(ProcessMeta.clientSession.name());
super.afterConnectionEstablished(session);
WebSocketConfig.getSessionMap().put(httpSessionId, session);
}
// connection closed
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
Map<String, Object> map = session.getAttributes();
String httpSessionId = (String) map.get(ProcessMeta.clientSession.name());
WebSocketConfig.getSessionMap().remove(httpSessionId);
super.afterConnectionClosed(session, status);
}
}
'IT 기술 > 개념정리' 카테고리의 다른 글
Debezium 커넥터 복구 방법 (1) | 2024.03.28 |
---|---|
유사 채팅방 구현하기(2) (0) | 2024.02.05 |
Debezium 개념정리 (0) | 2024.01.23 |