기본 아이디어입니다:
긴 연결은 각 요청 후 연결을 해제 할 필요가 없으므로 효율성이 짧은 연결보다 훨씬 높지만 긴 연결의 구현은 짧은 연결보다 훨씬 복잡하고 하트 비트를 통해 연결을 유지해야하며 오랜 시간 동안 메시지가 없어 연결을 피하고 닫히기 위해 모바일 네트워크가 매우 불안정하고 네트워크 지터 및 하트 비트 감지에 특정 지연 시간이 있으므로 메시지를 잃기가 매우 쉽기 때문에 메시지를 다시 시도 할 수있는 메시지의 신뢰성을 보장하는 메커니즘이 필요합니다. 따라서 메시지가 손실된 후 메시지를 재시도할 수 있도록 메시지의 신뢰성을 보장하는 메커니즘이 필요합니다.
IM-Service의 여러 인스턴스를 배포 한 후 새로운 문제에 직면하게됩니다. 첫 번째는 라우팅 문제이며 클라이언트는 인스턴스 중 하나에만 연결할 수 있으므로 클라이언트는 IM-Service 인스턴스 목록을 가져 와서 인스턴스 중 하나로 라우팅하고 라우팅 알고리즘을 통해 연결하고 채널을 가져온 다음 나중에 채널을 통해 메시지를 보내야합니다. 또 다른 문제는 다음 그림과 같이 메시지의 발신자와 수신자가 동일한 서비스 인스턴스에 연결되어 있지 않다는 것입니다:
위 그림은 클라이언트 C1이 서비스 인스턴스 S1에 연결하고 클라이언트 C2가 서비스 인스턴스 S2에 연결한 경우, 이제 C1이 C2에 메시지를 보내려고 하고, C1이 먼저 S1에 메시지를 보내지만 S1에 C2 연결을 위한 채널이 없기 때문에 S1이 메시지를 C2에 푸시할 수 없으며, 해결책은 S1이 메시지를 S2에 전달하고 S2가 현재 서비스 인스턴스에서 C2에 연결된 것을 발견하는 것 입니다. 채널에 C2에 대한 연결이 있음을 확인하여 S2가 메시지를 C2로 푸시할 수 있도록 하는 것입니다.
다음은 두 개의 서비스 인스턴스의 예일 뿐이며, 실제 프로덕션 환경에는 두 개 이상의 서비스 인스턴스가 있을 수 있습니다. S1 C2가 어떤 서비스 인스턴스에 연결되어 있는지 확인하는 방법에는 일반적으로 두 가지 솔루션이 있습니다:
1, 서비스 인스턴스 매핑에 연결된 모든 클라이언트를 등록하는 Register 서비스 인스턴스를 추가하고, 각 서비스 인스턴스는 클라이언트가 연결되면 Register 서비스에 등록되고 클라이언트가 오프라인 상태이면 Register 서비스에서 삭제되고, C1이 C2에 메시지를 보내고 C2가 S1 서비스 인스턴스에 없다는 것을 발견하면 S1은 Register 서비스에 쿼리하여 C2가 있는 서비스 인스턴스를 찾습니다. C1이 C2에게 메시지를 보내고 C2가 S1의 서비스 인스턴스에 없는 것을 발견하면 S1은 Register 서비스에 C2가 있는 서비스 인스턴스를 쿼리한 다음 S1이 이 서비스 인스턴스로 메시지를 전송하고, 이 메시지를 클라이언트인 C2에게 푸시합니다. 이렇게 하면 이 메시지와 관련이 없는 다른 서비스 인스턴스는 쓸데없는 작업을 할 필요가 없지만 호출 링크의 길이가 늘어나고 동시에 Register 서비스가 실패하면 전체 시스템이 작동하지 않는 결과를 초래합니다. 서비스는 고가용성을 보장하기 위해 여러 서비스 인스턴스와 함께 배포해야 합니다.
2에서 각 서비스 인스턴스는 시작 시 MQ의 지정된 토픽에 가입하고, S1이 현재 서비스 인스턴스에 C2 연결 채널이 없는 것을 발견하면 MQ의 지정된 토픽에 메시지를 게시하고, 모든 서비스 인스턴스는 이 메시지를 수신한 후 모두 자신의 연결 채널 풀에 클라이언트를 수신할 채널이 있는지 찾아서 있는 경우 이 채널을 통해 클라이언트에 메시지를 푸시합니다. 채널이 있으면 이 채널을 통해 클라이언트에게 메시지를 푸시합니다. 이 접근 방식은 구현이 간단하고 안정성이 뛰어나지만 이 메시지와 관련이 없는 서비스 인스턴스는 쓸데없는 검색을 수행해야 한다는 단점이 있습니다.
이 IM 시스템의 설계에서는 체계 2를 사용하고, 네트워크 통신 프레임 워크는 Netty를 사용하고, MQ는 RabbitMQ, RocketMQ 등을 사용할 수 있으며,이 예에서는 Redis 게시 구독을 사용하며, 다음은 Redis 게시 구독 메커니즘을 소개합니다.
둘째, redis의 게시-구독 메커니즘입니다:
마지막으로 인증이 성공했다는 메시지를 클라이언트에 푸시합니다:
Redis 게시-구독은 발신자가 메시지를 보내고 구독자가 이를 수신하는 메시지 통신 모델입니다.
Redis 클라이언트는 채널 수에 관계없이 구독할 수 있습니다.
다음 다이어그램은 채널1과 이 채널을 구독하는 세 클라이언트(클라이언트2, 클라이언트5, 클라이언트1) 간의 관계를 보여줍니다:
PUBLISH 명령을 통해 채널 채널1에 새 메시지가 전송되면 해당 채널에 가입한 세 명의 클라이언트에게 메시지가 전송됩니다:
를 클릭하고 구독 주문을 발행합니다:
예를 들어 다음은 MyChannel이라는 구독 채널을 만듭니다.
.1:6379> subscribe MyChannel
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "MyChannel"
3) (integer) 1
그런 다음 다른 클라이언트에서 이 채널에 메시지를 게시합니다:
.1:6379> publish MyChannel aaaa
(integer) 1
를 사용하여 SpringBoot에서 Redis로 게시-구독합니다:
첫 번째 단계는 Redis 종속성을 도입하는 것입니다:
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-data-redis</artifactId>
 <scope>provided</scope>
</dependency>
다음으로 채널에 메시지를 게시합니다:
String message = node.toString();
redisTemplate.convertAndSend("SocketCloseTopic", new DistributedMessage(SendType.ALL, channelType, message));
마지막은 메시지를 구독하는 것으로, 프레임워크에서 MessageListener 인터페이스를 정의하므로 이 인터페이스만 구현하면 지정된 채널의 메시지를 수신할 수 있습니다:
@Component
public class WebSocketChannelCloseListener implements MessageListener {
 private RedisSerializer<String> stringSerializer = new StringRedisSerializer();
 @Override
 public void onMessage(@NonNull Message message, @Nullable byte[] bytes) {
 byte[] channel = message.getChannel();
 byte[] body = message.getBody();
 String msgChannel = stringSerializer.deserialize(channel);
 String msgBody = stringSerializer.deserialize(body);
 logger.trace("WS-ChannelClose 메시지, channel을 수신합니다:{},body {}", msgChannel, msgBody);
 DistributedMessage distributedMessage = JSON.parseObject(msgBody, DistributedMessage.class);
 //........논리적 처리 코드 생략...........
 }
}
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(StringRedisTemplate stringRedisTemplate, WebSocketChannelCloseListener webSocketChannelCloseListener) {
 RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
 redisMessageListenerContainer.setConnectionFactory(stringRedisTemplate.getRequiredConnectionFactory());
 redisMessageListenerContainer.addMessageListener(webSocketChannelCloseListener, new ChannelTopic("SocketCloseTopic")); 
 return redisMessageListenerContainer;
}
시스템 설계:
마지막으로 인증이 성공했다는 메시지를 클라이언트에 푸시합니다:
1), 클라이언트는 먼저 유레카 레지스트리에서 모든 IM-서비스 인스턴스 목록 캐시를 가져온 다음 로드 밸런싱 알고리즘을 사용하여 현재 클라이언트를 서비스 인스턴스 중 하나로 라우팅하고 마지막으로 지정된 서비스 인스턴스의 소켓/웹소켓 연결을 통해 SDK를 패키지화합니다;
(2), 전체 Netty 프레임워크에 기반한 서버 측 IM-Service에서 클라이언트의 요청은 먼저 수신된 메시지의 처리 로직을 달성하기 위해 Netty 프레임워크인 IM-Service에 도달합니다;
3), IM-Service는 시작될 때 레지스트리에 등록되고 종료될 때 레지스트리에서 삭제되며, IM-Service는 Redis에서 지정한 토픽에 가입합니다;
4), IM-Service는 메시지를 수신하고, 수신자가 현재 인스턴스에 있으면 Netty를 통해 클라이언트로 직접 푸시하고, 현재 서비스 인스턴스에 없으면 Redis에 메시지를 발행하고, 다른 IM-Service 인스턴스가 메시지를 수신하고, 메시지 수신자가 현재 서비스 인스턴스에 연결되어 있는지 확인한 후 연결되어 있으면 클라이언트로 푸시합니다;
5), 메시지를 클라이언트에 동시에 푸시하고 비동기 작업을 시작하여 메시지를 MySQL 데이터베이스에 저장합니다.
네티 초기화:
public void start() {
	EventLoopGroup bossGroup = new NioEventLoopGroup();
 EventLoopGroup workerGroup = new NioEventLoopGroup();
	
	ServerBootstrap serverBootstrap = new ServerBootstrap();
 serverBootstrap.group(bossGroup, workerGroup)
 .channel(NioServerSocketChannel.class)
 .option(ChannelOption.SO_BACKLOG, 1024)
 .childHandler(nettyChannelInitializer)
 .childOption(ChannelOption.SO_KEEPALIVE, true);
 //포트 바인딩 및 들어오는 연결 수신 시작
 ChannelFuture f = serverBootstrap.bind(new InetSocketAddress(port)).sync(); 
 //서버 소켓이 닫힐 때까지 기다립니다.
 f.channel().closeFuture().sync();
}
//NettyChannelInitializer.initChannel
protected void initChannel(Channel ch) {
 ChannelPipeline pipeline = ch.pipeline();
 // 
 pipeline.addLast(new LengthFieldBasedFrameDecoder(100 * 1024 * 1024, 0, 4, 0, 4));
 pipeline.addLast(nettyReqDecoder);
 // 
 pipeline.addLast(new LengthFieldPrepender(4));
 pipeline.addLast(nettyReqEncoder);
 pipeline.addLast(nettyServerHandler);
}
public class NettyServerHandler extends SimpleChannelInboundHandler<NettyReqWrapper> {
 @Autowired
 private NettySessionManager nettySessionManager;
 @Autowired
 private NettyHandlerManager nettyHandlerManager;
 protected void channelRead0(ChannelHandlerContext ctx, NettyReqWrapper nettyReqWrapper) {
 Channel incoming = ctx.channel();
 Attribute<SessionUser> attribute = incoming.attr(NettyServer.REDIS_ATTR_SOCKET);
 SessionUser sessionUser = attribute.get();
 if (null != sessionUser && sessionUser.isAuthorized()) { 
 //승인 메시지는 아무런 처리 없이 바로 반환됩니다.
 if (nettyReqWrapper.getCmd() < 0) {
 return;
 }
 //handler 
 nettyHandlerManager.process(incoming, sessionUser, nettyReqWrapper);
 } else { 
 checkAndLogin(nettyReqWrapper, incoming, attribute);
 }
 }
 @Override
 public void channelInactive(ChannelHandlerContext ctx) {
 Channel incoming = ctx.channel();
 SessionUser sessionUser = incoming.attr(NettyServer.REDIS_ATTR_SOCKET).get();
 nettySessionManager.removeSession(sessionUser.getUserId(), sessionUser.getChannelType()); 
 ctx.close();
 }
 @Override
 public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
 Channel incoming = ctx.channel();
 SessionUser sessionUser = incoming.attr(NettyServer.REDIS_ATTR_SOCKET).get();
 nettySessionManager.removeSession(sessionUser.getUserId(), sessionUser.getChannelType());
 ctx.close();
 }
}
start 및 NettyChannelInitializer.initChannel 메서드는 Netty 서비스를 시작하고 Netty 파이프라인을 초기화하는 것으로, Netty에 대한 약간의 이해가 있다면 이해하기에 매우 좋은 코드입니다.
초기화는 비즈니스 처리 핸들러 클래스 NettyServerHandler를 지정하고, channelRead0은 클라이언트로부터 메시지를 수신하여 SessionUser를 읽은 다음 처리를 위해 nettyHandlerManager로 넘기고, User를 읽을 수 없는 경우 나중에 설명하는 로그인 처리를 호출합니다. 채널 비활성, 예외 발생 시 세션과 연결을 닫고 삭제합니다.
마지막으로 인증 성공 메시지를 클라이언트에 푸시합니다:
세션 관리를 구현하는 클래스는 현재 연결된 클라이언트에 대한 정보를 읽을 수 있을 뿐만 아니라 이 서비스 인스턴스에 연결된 모든 클라이언트의 채널을 맵에 저장할 수 있는 NettySessionManager입니다. 코드는 다음과 같습니다:
public class SessionUser implements Serializable { 
 private boolean authorized;
 private Long loginTime;
 private String userName;
 private Integer gender;
 private String userHead;
 private String city;
	//...........
}
public class NettySessionManager extends AbstractSessionManager {
 @Override
 public AttributeKey<SessionUser> getAttributeKey() {
 return WebSocketServer.REDIS_ATTR_WEBSOCKET;
 } 
}
public abstract class AbstractSessionManager {
 // 사용자 세션 캐시
 protected static final ConcurrentHashMap<Long, ConcurrentHashMap<ChannelType, Channel>> sessionCache = new ConcurrentHashMap<>();
 
 @Autowired
 private RedisTemplate redisTemplate;
 @Autowired
 private MachineApi machineApi;
	
	public void addSession(Long userId, ChannelType channelType, Channel channel) { 
 ConcurrentHashMap<ChannelType, Channel> userMap = sessionCache.get(userId);
 if (null == userMap) {
 userMap = new ConcurrentHashMap<>();
 userMap.put(channelType, channel);
 sessionCache.put(userId, userMap); 
 return;
 }
 Channel oldSession = userMap.get(channelType);
 // 사용자가 다시 로그인하면 정보를 덮어쓰고 이전 로컬 연결을 끊습니다.
 if (oldSession != null) {
 oldSession.close();
 }
 //분산 서버에서 동일 채널 연결 끊기
 ChannelClose channelClose = new ChannelClose(userId, machineApi.getMachineId());
 redisTemplate.convertAndSend(getSocketCloseTopic(), new DistributedMessage(SendType.ALL, channelType, JSON.toJSONString(channelClose)));
 //새 연결을 위한 로컬 캐시 교체
 userMap.put(channelType, channel); 
 }
	
	public Channel getSession(Long userId, ChannelType channelType) { 
 ConcurrentHashMap<ChannelType, Channel> userMap = sessionCache.get(userId);
 if (null == userMap) {
 return null;
 }
 return userMap.get(channelType);
 }
 
 //...............
}
2), AbstractSessionManager는 이 서비스 인스턴스에 연결된 모든 클라이언트의 채널 목록을 저장하며, 이 맵은 두 개의 레이어로 나뉘는데, 첫 번째 레이어는 UserId에 대한 키, 두 번째 레이어는 채널 유형, 즉 클라이언트의 유형에 대한 키, 즉 앱, 웹 등이 있습니다;
3), addSession은 먼저 동일한 UserId 및 ChannelType 채널이 이미 존재하는지 확인하고, 존재하지 않으면 맵에 직접 추가하고, 존재하면 원래 채널은 닫히고 새 채널이 맵에 추가됩니다.
4), getSession은 userId 및 ChannelType으로 조회를 구현합니다;
마지막으로 인증 성공 메시지를 클라이언트에 푸시합니다:
public <T extends IReq> boolean send(Long receiveId, ChannelType channelType, @NonNull T data) {
 JsonNode node = objectMapper.convertValue(data, JsonNode.class);
 String message = node.toString();
 Channel channel = nettySessionManager.getSession(receiveId, channelType);
 if (null != channel && channel.isActive()) {
 return localSend(receiveId, channelType, message);
 } else { 
 redisTemplate.convertAndSend(NettyServer.REDIS_TOPIC_SOCKET, new DistributedMessage(receiveId, channelType, message));
 return true;
 }
}
public boolean localSend(Long receiveId, ChannelType channelType, String message) {
 Channel channel = nettySessionManager.getSession(receiveId, channelType);
 if (null != channel && channel.isActive()) {
 NettyReqWrapper nettyReqWrapper = new NettyReqWrapper(cmd, message);
 channel.writeAndFlush(nettyReqWrapper); 
 return true;
 }
 return false;
}
보내기 메서드를 먼저 맵에 캐시된 채널의 현재 서비스 인스턴스에서 찾아 채널을 찾고, 채널을 찾으면 localSend를 호출하면 localSend는 실제로 채널을 통해 클라이언트에 메시지를 직접 푸시하고, 찾지 못하면 메시지를 다시 해제하여 다른 서비스 인스턴스가 이 인스턴스에서 이 메시지를 수신합니다. 채널을 찾아 직접 푸시 메시지를 찾습니다:
public class NettySendMessageListener implements MessageListener {
 private RedisSerializer<String> stringSerializer = new StringRedisSerializer();
 @Autowired
 private NettySendManager nettySendManager;
 @Override
 public void onMessage(@NonNull Message message, @Nullable byte[] bytes) {
 byte[] channel = message.getChannel();
 byte[] body = message.getBody();
 String msgChannel = stringSerializer.deserialize(channel);
 String msgBody = stringSerializer.deserialize(body); 
 DistributedMessage distributedMessage = JSON.parseObject(msgBody, DistributedMessage.class);
 nettySendManager.localSend(distributedMessage.getReceiveId(), distributedMessage.getChannelType(), distributedMessage.getMessage());
 }
}
이 클래스는 메시지 리스너 인터페이스를 구현하므로 onMessage의 이전 단계에서 redis에 게시된 메시지를 수신할 수 있습니다. 또한 onMessage는 메시지 내용을 구문 분석하고 localSend를 호출하여 채널을 찾은 다음 채널을 찾으면 채널을 통해 메시지를 푸시합니다;
클라이언트로 전송되는 마지막 푸시 메시지는 인증 성공 메시지입니다:
public class NettyReqWrapper implements Serializable {
 private int cmd;
 private String message;
	//.........
}
public class NettyReqEncoder extends MessageToMessageEncoder<NettyReqWrapper> {
 @Override
 protected void encode(ChannelHandlerContext ctx, NettyReqWrapper nettyReqWrapper, List<Object> out) {
 byte[] byteMsg = new byte[0];
 if (StringUtils.hasText(nettyReqWrapper.getMessage())) {
 byteMsg = nettyReqWrapper.getMessage().getBytes(charset);
 }
 ByteBuf byteBuf = ctx.alloc().buffer(byteMsg.length + 4);
 byteBuf.writeInt(nettyReqWrapper.getCmd());
 if (byteMsg.length > 0) {
 byteBuf.writeBytes(byteMsg);
 }
 out.add(byteBuf);
 }
}
public class NettyReqDecoder extends MessageToMessageDecoder<ByteBuf> {
 @Override
 protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
 NettyReqWrapper nettyReqWrapper = new NettyReqWrapper();
 int len = in.readableBytes();
 if (len < 4) {
 in.skipBytes(len);
 throw new BusinessException("NettyReqDecoder ByteBuf readableBytes is less than 4");
 }
 int cmd = in.getInt(0);
 nettyReqWrapper.setCmd(cmd);
 in.skipBytes(4);
 nettyReqWrapper.setMessage(in.toString(charset));
 out.add(nettyReqWrapper);
 }
}
인코더 NettyReqEncoder에서 메시지 콘텐츠 바로 앞에 4바이트 CmdId가 추가된 것을 볼 수 있으며, 디코더 NettyReqDecoder가 먼저 CmdId를 파싱한 후 메시지 콘텐츠를 다음 단계의 프로세스에 넣는 것을 볼 수 있습니다.
실제로 CmdId는 다음과 같이 정의된 일련의 상수입니다:
// 
public static final int CMD_LOGIN = 1000;
// 
public static final int CMD_HEARTBEAT = 1001;
// 
public static final int CMD_SEND_GIFT = 3000;
//라이브룸 소등
public static final int CMD_SEND_LIGHT = 4000;
//------begin서버 측 요청 ---------
//로그인 인증 결과
public static final int CMD_LOGIN_RESULT = 2000;
//응답의 전환 계수
private static final int CMD_RSP = -1;
클라이언트 측 메시지 프로세서입니다:
클라이언트 측 메시지 핸들러는 넷티핸들러 매니저에서 구현됩니다:
public class NettyHandlerManager extends AbstractHandlerManager {
 @Autowired
 private HeartbeatHandler heartbeatHandler;
 public void process(Channel channel, SessionUser sessionUser, NettyReqWrapper nettyReqWrapper) {
 Cmd cmd = inputMap.get(nettyReqWrapper.getCmd());
 //메시지 수신에 대한 응답
 Req receipt = new Req(CmdConstants.getReceiptCmd(cmd.getUniqueCode()));
 channel.writeAndFlush(new NettyReqWrapper(receipt.getCmd(), JSON.toJSONString(receipt)));
 //하트비트 데이터 기록
 heartbeatHandler.heartbeat(sessionUser); 
 
 //............
 }
}
먼저 메시지의 CmdId에 따라 해당 Cmd 인스턴스를 찾은 다음 클라이언트에게 확인 메시지를 보내며, 이는 클라이언트에게 내가 받은 이 메시지를 보냈다는 것을 알리는 것과 동일합니다. 마지막으로 하트비트 데이터를 redis 캐시에 기록하는데, 사실 여기서는 하트비트 패킷만 하트비트 데이터인 것이 아니라 수신된 모든 메시지를 하트비트 데이터로 간주할 수 있습니다. 여기서 하트비트 데이터를 기록하는 것은 실제로 UserId와 온라인 상태를 redis로 업데이트하여 클라이언트가 온라인 사용자 정보를 쉽게 조회할 수 있도록 하기 위한 것입니다.
입력맵은 CmdId에 따라 Cmd 인스턴스를 조회합니다. 입력맵의 초기화 코드는 다음과 같습니다:
public Set<InputCmd> initInput() {
 Set<InputCmd> cmdSet = new HashSet<>();
 cmdSet.add(new NettyInputCmd<>(CmdConstants.CMD_LOGIN, "시스템", "로그인", LoginReq.class, null));
 cmdSet.add(new NettyInputCmd<>(CmdConstants.CMD_HEARTBEAT, "시스템", "하트비트", Req.class, null));
	//................
 return cmdSet;
}
Cmd는 다음과 같이 정의됩니다:
public interface Cmd<M extends IReq> {
 Protocol getProtocol();
 FromType getFromType();
 int getUniqueCode();
 String getModule();
 String getRemark();
 Class<M> getMClass();
 BiConsumer<SessionUser, M> getConsumer();
}
public abstract class InputCmd<M extends IReq> implements Cmd<M> {
 private Protocol protocol;
 private FromType fromType = FromType.CLIENT;
 private int uniqueCode;
 private String module;
 private String remark;
 private Class<M> mClass;
 private BiConsumer<SessionUser, M> consumer;
	//............
}
현재 인증이 처리되는 방식은 NettyServerHandler.channelRead0의 메시지를 읽고 인증 정보가 없는지 판단한 다음 인증 작업을 수행합니다:
private void checkAndLogin(NettyReqWrapper nettyReqWrapper, Channel incoming, Attribute<SessionUser> attribute) {
 LoginReq loginReq = JSON.parseObject(nettyReqWrapper.getMessage(), LoginReq.class);
	String userId = loginReq.getUserId();
 String token = loginReq.getToken();
 String appVersion = loginReq.getAppVersion();
 String devType = loginReq.getDevType();
 ChannelType channelType = ChannelType.of(loginReq.getDevType());
 
 if (enableAuth) {
		// 
 LoginResultReq loginResultReq = authManager.checkUserToken(tenantId, userId, token, DevType.getEnumByType(loginReq.getDevType()));
 if (!ResponseConstant.SUCCESS.getCode().equals(loginResultReq.getCode())) {
 // 인증 실패 메시지
 incoming.writeAndFlush(new NettyReqWrapper(loginResultReq.getCmd(), JSON.toJSONString(loginResultReq))); 
 incoming.close();
 return;
 }
 }
 
 //세션 정보 저장
 SessionUser sessionUser = new SessionUser();
 sessionUser.setAuthorized(true);
 //..............
 attribute.set(sessionUser);
 nettySessionManager.addSession(Long.valueOf(userId), channelType, incoming);
 //로그인 성공
 LoginResultReq loginResultReq = new LoginResultReq(CmdConstants.CMD_LOGIN_RESULT, ResponseConstant.SUCCESS.getCode(), ResponseConstant.SUCCESS.getMsg());
 incoming.writeAndFlush(new NettyReqWrapper(loginResultReq.getCmd(), JSON.toJSONString(loginResultReq))); 
}
먼저 인증에 필요한 정보를 파싱한 다음 authManager.checkUserToken을 호출하여 인증한 다음, 세션 사용자 정보를 속성으로 구성하고 세션을 추가한 다음 마지막으로 인증 성공 메시지를 클라이언트에 푸시합니다.




