본문 바로가기

IT 기술/개념정리

유사 채팅방 구현하기(1)

 

개발목표

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