惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 聂微东
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
L
LangChain Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 司徒正美
WordPress大学
WordPress大学
T
The Blog of Author Tim Ferriss
Blog — PlanetScale
Blog — PlanetScale
J
Java Code Geeks
Y
Y Combinator Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
GbyAI
GbyAI
Vercel News
Vercel News
大猫的无限游戏
大猫的无限游戏
T
Tailwind CSS Blog
Jina AI
Jina AI
B
Blog
Recorded Future
Recorded Future
MyScale Blog
MyScale Blog
I
InfoQ
aimingoo的专栏
aimingoo的专栏
博客园_首页
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Cloudflare Blog
雷峰网
雷峰网
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
腾讯CDC
爱范儿
爱范儿
Last Week in AI
Last Week in AI
博客园 - 三生石上(FineUI控件)
博客园 - Franky
Schneier on Security
Schneier on Security
V
V2EX
TaoSecurity Blog
TaoSecurity Blog
H
Hacker News: Front Page
Cloudbric
Cloudbric
D
DataBreaches.Net
B
Blog RSS Feed
P
Palo Alto Networks Blog
云风的 BLOG
云风的 BLOG
NISL@THU
NISL@THU
I
Intezer
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Cyberwarzone
Cyberwarzone
F
Fortinet All Blogs
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
Cybersecurity and Infrastructure Security Agency CISA
C
Cisco Blogs
K
Kaspersky official blog
Forbes - Security
Forbes - Security

博客园 - hcfalan

Mybatis分页功能 安聊系统1.0发布 VC中发送电子邮件 自绘窗口边框和标题栏 Windows下生产者-消费者问题的解法 Javascript 时间日期转换 Java Threading - Consumer&Producer Boost socket 同步编程示例(服务端,客户端) MFC CSocket的跨线程问题 用于主题检测的临时日志(54b8134e-5e4a-46fc-95af-c75ccf06d66e - 3bfe001a-32de-4114-a6b4-4005b770f6d7) 网络电话 VC 剪贴板操作 The 1st Boost Serial Demo - hcfalan MS Proxy FTP Sample 《斯坦福大学开放课程: 编程方法学》 VC2005下编译log4cpp的修正方法 - hcfalan - 博客园 适合孩子的书籍 一个最经典的线程类(转) 应用MFC开发高级应用程序(转)
安聊服务端Netty的应用
hcfalan · 2021-07-02 · via 博客园 - hcfalan

Netty简介

Netty是一个面向网络编程的Java基础框架,它基于异步的事件驱动,并且内置多种网络协议的支持,可以快速地开发可维护的高性能的面向协议的服务器和客户端。

安聊简介

安聊是一个即时聊天系统,服务端通过点对点与客户端建立TCP链接,接受来自客户端的请求,同时,也可以实时地将消息通知给客户端。

安聊为什么选择Netty

首先是性能和稳定性。在我们内部团队进行过测试,使用Netty框架,单台服务器可以维持10000个客户端长链接,并且稳定性非常高:我们的服务器曾经有过连续六个月稳定运行的记录,并且中断的原因还是因为服务端版本升级。

其次是应用程序的简洁性和易维护性。使用Netty进行网络开发,可以利用框架屏蔽那些网络底层的实现细节,让应用只关注于业务逻辑本身;同时因为pipeline的设计模式,让应用添加对数据/事件的额外处理变得非常简单。

安聊使用Netty的一些技术特点:

1、结合Spring,让端口侦听服务成为一个Bean,结合Bean的生命周期挂钩函数完成端口服务的安装/关闭行为

2、将终端长连接的ChannelHandleContext与对应的用户ID进行绑定,方便消息转发

3、使用自定义的编码/解码器对协议包进行处理

4、通过继承SimpleChannelInboundHandler的类,来处理客户端请求的协议包

5、因为处理客户端包的业务过程中,会涉及到数据库操作,磁盘读写操作,若直接在网络IO线程中处理,则会显著降低网络IO的处理能力,所以把每个业务处理都独立成为一个任务(Task)实例,然后放到线程池中去执行;当任务执行完毕,需要通知回网络IO线程时,使用userEvent的形式通知回去

一些关键代码:

网络服务初始化

public class IMClientServerInitializer extends ChannelInitializer<SocketChannel> {

    private final EventExecutorGroup execGroup;
    private final int pduTimeout;

    public IMClientServerInitializer(EventExecutorGroup execGroup, int pduTimeout) {
        this.execGroup = execGroup;
        this.pduTimeout = pduTimeout;
    }

    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast(new IdleStateHandler(this.pduTimeout, 0, 0, TimeUnit.SECONDS));
        //pipeline.addLast(new LoggingHandler(LogLevel.DEBUG));
        pipeline.addLast(new IMPacketEncoder());
        pipeline.addLast(new IMPacketDecoder());
        pipeline.addLast(new IMClientPacketHandler(execGroup));
    }
}

网络服务类

public class IMClientListenService implements InitializingBean, DisposableBean {

    private static final Logger logger = LoggerFactory.getLogger(IMClientListenService.class);

    @Value("${imcore.client.service.listen}")
    private String clientServiceListenAddress;

    @Value("${imcore.client.service.port}")
    private Integer clientServiceListenPort;

    @Value("${imcore.client.service.pdu.timeout}")
    private Integer clientServicePduTimeout;


    private EventLoopGroup bossEventLoopGroup;
    private EventLoopGroup childEventLoopGroup;
    private EventExecutorGroup eventExecutorGroup;
    private ServerBootstrap serverBootstrap;
    private Channel listenChannel;
    private final IMClientChannelManager imClientChannelManager = new IMClientChannelManager();

    public IMClientChannelManager getImClientChannelManager() {
        return imClientChannelManager;
    }


    @Override
    public void afterPropertiesSet() {
        eventExecutorGroup = new DefaultEventExecutorGroup(128);
        bossEventLoopGroup = new NioEventLoopGroup();
        childEventLoopGroup = new NioEventLoopGroup();
        serverBootstrap = new ServerBootstrap();
        serverBootstrap.group(bossEventLoopGroup, childEventLoopGroup);
        serverBootstrap.channel(NioServerSocketChannel.class);
        serverBootstrap.option(ChannelOption.SO_BACKLOG, 128);
        serverBootstrap.childHandler(new IMClientServerInitializer(eventExecutorGroup, clientServicePduTimeout));
    }

    public void destroy() {
        logger.info("IMClientListenService destroy called");
    }

    public void run() {
        try {
            listenChannel = serverBootstrap.bind(clientServiceListenPort).sync().channel();
            logger.info("Client Listen Service started at port: " + clientServiceListenPort);
            listenChannel.closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void shutdown() {
        try {
            //
            // 先关闭侦听端口
            //
            logger.info("close listen channel...");
            ChannelFuture closeFuture = listenChannel.close();
            closeFuture.sync();
            logger.info("close listen channel...done!");

            //
            // 再关闭所有客户端的连接
            //
            List<ChannelHandlerContext> channels = imClientChannelManager.getAllChannels();
            for (ChannelHandlerContext channel : channels) {
                channel.close().sync();
            }

            logger.info("Client Listen Service stopped");
        }
        catch (Exception ex) {
            logger.error("Client Listen Service close failed", ex);
        }
        finally {
            //
            // 最后关闭所有线程池
            //
            childEventLoopGroup.shutdownGracefully();
            bossEventLoopGroup.shutdownGracefully();
            eventExecutorGroup.shutdownGracefully();
        }
    }
}

网络数据包处理Handler类

public class IMClientPacketHandler extends SimpleChannelInboundHandler<IMPacket> {

    private static final int NEED_LOGIN_FLAG = 1;

    private static final Logger logger = LoggerFactory.getLogger(IMClientPacketHandler.class);

    private final EventExecutorGroup eventExecutor;
    private final IMClientChannelManager imClientChannelManager;
    private final IMClientListenService imClientListenService;

    private int userId;
    private String loginName;
    ...


    public IMClientPacketHandler(EventExecutorGroup eventExecutor) {
        this.eventExecutor = eventExecutor;
        this.imClientListenService = Application.getInstance().getBean(IMClientListenService.class);
        this.imClientChannelManager = imClientListenService.getImClientChannelManager();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        logger.error("ImPacketHandler exception caught, closing...");
        if (cause != null) {
            logger.error(cause.getMessage(), cause);
        } else {
            logger.error("exception object is null");
        }
        ctx.close();

        this.imClientChannelManager.removeChannel(ctx);
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        super.channelActive(ctx);
        InetSocketAddress socketAddress = (InetSocketAddress)(ctx.channel().remoteAddress());
        this.clientIP = socketAddress.getAddress().getHostAddress();

        this.imClientChannelManager.addChannel(ctx);
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        this.imClientChannelManager.removeChannel(ctx);

        if (this.userId != 0 && this.terminal != 0) {
            this.imClientChannelManager.removeLoginChannel(ctx, this.userId, this.terminal);
        }

        super.channelInactive(ctx);
        this.userId = 0;
        this.loginName = null;
        ...
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (logger.isDebugEnabled()) {
            logger.debug("userEventTriggered:" + evt);
        }
        if (evt instanceof IdleStateEvent) {
            // 如果是长时间没有write事件,则尝试去从队列里拿出通知来发送
            IdleStateEvent e = (IdleStateEvent) evt;
            if (e.state() == IdleState.READER_IDLE) {
                // close the channel
                ctx.close();
                return;
            }
        } else if (evt instanceof IMUserLoginEvent) {
            logger.info("user login event thread id " + Thread.currentThread().getId());
            IMUserLoginEvent loginEvent = (IMUserLoginEvent)evt;
            if (loginEvent.isSucceed()) {
                this.userId = loginEvent.getUserInfo().getUserId();
                this.loginName = loginEvent.getUserInfo().getLoginName();
                ...
                if (logger.isDebugEnabled()) {
                    logger.debug("set login succeed in channel handler for user '" + this.loginName + "' with session '" + this.sessionKey + "'");
                }

                this.imClientChannelManager.addLoginChannel(ctx, this.userId, this.terminal);
            }
        }

        super.userEventTriggered(ctx, evt);
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, IMPacket msg) throws Exception {
        if (this.loginName == null && this.userId == 0) {
            if (msg.getCommandId() == IMBaseDefine.LoginCmdID.CID_USER_LOGIN_REQ_VALUE) {
                handleUserLoginRequest(ctx, msg);
            }
            else {
                // 如果未登录的情况下发送其他所有非登录命令,一律返回flag为1的响应头
                IMPacket packet = new IMPacket(NEED_LOGIN_FLAG, null);
                ctx.channel().write(packet).addListener(ChannelFutureListener.CLOSE);
            }
        }
        else {
            if (msg.getCommandId() == IMBaseDefine.LoginCmdID.CID_USER_LOGIN_REQ_VALUE) {
                // 如果已登录的情况下发送登录命令,则返回 ERR_ALREADY_LOGGIN_VALUE 登录错误
                IMLogin.IMLoginRsp.Builder loginRespBuilder = IMLogin.IMLoginRsp.newBuilder()
                        .setErrorCode(IMBaseDefine.CommonErrors.ERR_DUPLICATE_LOGIN_VALUE)
                        .setErrorMsg("duplicate login"));

                IMPacket packet = new IMPacket(loginRespBuilder.build().toByteArray());
                ctx.channel().write(packet).addListener(ChannelFutureListener.CLOSE);
            }
            else {
                switch (msg.getCommandId()) {
                    case IMBaseDefine.OtherCmdID.CID_OTHER_HEARTBEAT_VALUE:
                        handleHeartbeatRequest(ctx, msg);
                        break;
                    case IMBaseDefine.LoginCmdID.CID_USER_LOGOUT_REQ_VALUE:
                        handleUserLogoutRequest(ctx, msg);
                        break;
                    // 创建群聊
                    case IMBaseDefine.GroupCmdID.CID_GROUP_CREATE_REQ_VALUE:
                        handleGroupCreateRequest(ctx, msg);
                        break;
                    ...
                    default:
                        logger.warn("unsupported command: " + msg.getCommandId());
                        break;
                }
            }
        }
    }


    private void handleUserLoginRequest(ChannelHandlerContext ctx, IMPacket msg) {
        if (logger.isDebugEnabled()) {
            logger.debug("user login received thread id " + Thread.currentThread().getId());
        }

        TaskContext taskContext = new TaskContext(ctx, clientIP, userId, loginName, clientType, sessionKey, pushToken);
        try {
            UserLoginTask userLoginTask = new UserLoginTask(taskContext, msg);
            eventExecutor.submit(userLoginTask);
        } catch (CreateTaskException ex) {
            logger.error("create user login task failed", ex);
        }
    }

    private void handleHeartbeatRequest(ChannelHandlerContext ctx, IMPacket msg) {
        if (logger.isDebugEnabled()) {
            logger.debug("heartbeat received");
        }

        TaskContext taskContext = new TaskContext(ctx, clientIP, userId, loginName, clientType, sessionKey, pushToken);
        try {
            HeartbeatTask heartbeatTask = new HeartbeatTask(taskContext, msg);
            eventExecutor.submit(heartbeatTask);
        } catch (CreateTaskException ex) {
            logger.error("create heartbeat task failed", ex);
        }
    }


    private void handleUserLogoutRequest(ChannelHandlerContext ctx, IMPacket msg) {
        if (logger.isDebugEnabled()) {
            logger.debug("session logout received");
        }

        TaskContext taskContext = new TaskContext(ctx, clientIP, userId, loginName, clientType, sessionKey, pushToken);
        try {
            UserLogoutTask userLogoutTask = new UserLogoutTask(taskContext, msg);
            eventExecutor.submit(userLogoutTask);
        } catch (CreateTaskException ex) {
            logger.error("create logout task failed", ex);
        }
    }

    private void handleGroupCreateRequest(ChannelHandlerContext ctx, IMPacket msg) {
        if (logger.isDebugEnabled()) {
            logger.debug("group creation received");
        }

        TaskContext taskContext = new TaskContext(ctx, clientIP, userId, loginName, clientType, sessionKey, pushToken);
        try {
            GroupCreationTask groupCreationTask = new GroupCreationTask(taskContext, msg);
            eventExecutor.submit(groupCreationTask);
        } catch (CreateTaskException ex) {
            logger.error("create group creation task failed", ex);
        }
    }

}

用户登录Task类

/**
 * Task executed in thread pool for user login
 */
public class UserLoginTask extends TaskBase {

    private static final Logger logger = LoggerFactory.getLogger(UserLoginTask.class);

    private final IMPacket request;
    private final IMLogin.IMLoginReq reqBody;
    private final IMUserService userService;

    private int errorCode;
    private String errorMessage;

    public UserLoginTask(TaskContext taskContext, IMPacket request) throws CreateTaskException {
        super(taskContext);

        this.request = request;
        try {
            this.reqBody = IMLogin.IMLoginReq.parseFrom(request.getPayload());
        } catch (InvalidProtocolBufferException e) {
            throw new CreateTaskException("parse pb failed", e);
        }
        this.userService = Application.getInstance().getBean(IMUserService.class);
    }


    @Override
    protected void taskRun() {
        this.errorCode = 0;
        this.errorMessage = "ok";


        IMUserRecord user = userService.findUserByLoginName(reqBody.getUserName());
        if (user == null) {
            this.errorCode = IMBaseDefine.CommonErrors.ERR_USERNAME_OR_PASSWD_INVALID_VALUE;
            this.errorMessage = "bad username or password";
            handleErrorResponse();
            return;
        }


        if (!user.getPassword().equals(reqBody.getPassword())) {
            this.errorCode = IMBaseDefine.CommonErrors.ERR_USERNAME_OR_PASSWD_INVALID_VALUE;
            this.errorMessage = "bad username or password";

            handleErrorResponse();
            return;
        }

            
        handleSucceedResponse(user);
        return;
    }


    private void handleErrorResponse() {
        logger.error("user '" + reqBody.getUserName() + "' login failed with code " + this.errorCode + " '" + this.errorMessage + "'");

        IMLogin.IMLoginRsp.Builder loginResp = IMLogin.IMLoginRsp.newBuilder()
                .setErrorCode(this.errorCode)
                .setErrorMsg(this.errorMessage);
        IMPacket packetResp = new IMPacket(loginResp.build().toByteArray());

        getContext().getChannelContext().writeAndFlush(packetResp);
    }


    private void handleSucceedResponse(IMUserRecord user) {
        IMLogin.IMLoginRsp.Builder loginResp = IMLogin.IMLoginRsp.newBuilder()
                .setErrorCode(0)
                .setErrorMsg("succeed")
                .setUserInfo(IMUserProtobufUtils.toProtobuf(user));


        IMPacket packetResp = new IMPacket(loginResp.build().toByteArray());

        getContext().getChannelContext().writeAndFlush(packetResp);

        // 🛫触发 user event 通知 IO 线程:我们可以异步的改变相关 pipeline 的状态
        IMUserLoginEvent event = new IMUserLoginEvent();
        event.setSucceed(true);
        event.setUserInfo(user);
        getContext().getChannelContext().pipeline().fireUserEventTriggered(event);
    }
}

踩过的一个坑备注一下:

之前使用ChannelHandlerContext向客户端写数据的时候,都是这样子的:

getContext().getChannelContext().write(packetResp);

写完之后,发现有概率性的客户端收不到响应包,原来是写完数据,还需要flush一下:

getContext().getChannelContext().writeAndFlush(packetResp);

这样子就没有问题了。

-------------------------------------------------

本人在企业做过五年的即时聊天系统开发,关注这一块开发的同学,可以一起探讨。

另外,本人独自开发了一套安聊系统,感兴趣的同学可以去下载demo试用一下:安聊系统1.0发布