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发布

安聊服务端Netty的应用的更多相关文章

  1. [Python 网络编程] TCP编程/群聊服务端 (二)

    群聊服务端 需求分析: 1. 群聊服务端需支持启动和停止(清理资源); 2. 可以接收客户端的连接; 接收客户端发来的数据 3. 可以将每条信息分发到所有客户端 1) 先搭架子: #TCP Serve ...

  2. 服务端NETTY 客户端非NETTY处理粘包和拆包的问题

    之前为了调式和方便一直没有处理粘包的问题,今天专门花了时间来搞NETTY的粘包处理,要知道在高并发下,不处理粘包是不可能的,数据流的混乱会造成业务的崩溃什么的我就不说了.所以这个问题 在我心里一直是个 ...

  3. Netty(6)源码-服务端与客户端创建

    原生的NIO类图使用有诸多不便,Netty向用户屏蔽了细节,在与用户交界处做了封装. 一.服务端创建时序图 步骤一:创建ServerBootstrap实例 ServerBootstrap是Netty服 ...

  4. Netty学习笔记(二) 实现服务端和客户端

    在Netty学习笔记(一) 实现DISCARD服务中,我们使用Netty和Python实现了简单的丢弃DISCARD服务,这篇,我们使用Netty实现服务端和客户端交互的需求. 前置工作 开发环境 J ...

  5. 原理剖析-Netty之服务端启动工作原理分析(下)

    一.大致介绍 1.由于篇幅过长难以发布,所以本章节接着上一节来的,上一章节为[原理剖析(第 010 篇)Netty之服务端启动工作原理分析(上)]: 2.那么本章节就继续分析Netty的服务端启动,分 ...

  6. Netty搭建服务端的简单应用

    Netty简介 Netty是由JBOSS提供的一个java开源框架,现为 Github上的独立项目.Netty提供异步的.事件驱动的网络应用程序框架和工具,用以快速开发高性能.高可靠性的网络服务器和客 ...

  7. Netty 学习(一):服务端启动 & 客户端启动

    Netty 学习(一):服务端启动 & 客户端启动 作者: Grey 原文地址: 博客园:Netty 学习(一):服务端启动 & 客户端启动 CSDN:Netty 学习(一):服务端启 ...

  8. zabbix服务端安装

    1.安装zabbix服务(1)先rpm安装lamp环境 yum install -y httpd mysql mysql-libs php php-mysql mysql-server php-bcm ...

  9. 适合新手:从零开发一个IM服务端(基于Netty,有完整源码)

    本文由“yuanrw”分享,博客:juejin.im/user/5cefab8451882510eb758606,收录时内容有改动和修订. 0.引言 站长提示:本文适合IM新手阅读,但最好有一定的网络 ...

随机推荐

  1. 3. java基础语法

    3.1 注释(理解) 注释是对代码的解释和说明文字,可以提高程序的可读性,因此在程序中添加必要的注释文字十分重要.Java中的 注释分为三种: 单行注释.单行注释的格式是使用//,从//开始至本行结尾 ...

  2. 使用CSS样式的三种方法

    一.内联样式 内联样式通过style属性来设置,属性值可以任意的CSS样式. 1 <!DOCTYPE html> 2 <html lang="en"> 3 ...

  3. 搭建LAMP环境部署Nextcloud私人网盘

    搭建 LAMP 环境部署 Nextcloud 私人网盘 前言 Nextcloudd 是一个开源的.基于本地的文件共享和协作平台,它允许您保存文件并通过多个设备(如PC.智能手机和平板电脑)访问它们. ...

  4. C++知识点案例 笔记-2

    1.友元函数 2.友元类 3.继承(公有继承) 4.公有继承的访问权限 5.私有继承的访问权限 6.保护继承的访问权限(两次继承) ==友元函数== #include <iostream> ...

  5. 解决SecureCRTPortable和SecureFXPortable的中文乱码问题

    我们使用客户端连接Linux服务器时会出现中文乱码的问题,解决方法如下: 一.修改SecureCRTPortable的相关配置 步骤一:[选项]->[全局选项] 步骤二:[常规]->[默认 ...

  6. PID参数

    大家奉上一篇关于PID算法及参数整定的知识! 1.位置表达式 位置式表达式是指任一时刻PID控制器输出的调节量的表达式. PID控制的表达式为 式中的y(t)为时刻t控制器输出的控制量,式中的y(0) ...

  7. ubuntu 20.04 编译安装 p 详解

    事情的起因 实验需要安装 p4 环境 我考虑到我自己的电脑性能不足,因此打算在本机安装 github上官方仓库的安装教程老旧,都是在 ubuntu14.04或者ubuntu16.04 我长时间用的li ...

  8. 重新整理 .net core 实践篇—————配置系统之简单配置中心[十一]

    前言 市面上已经有很多配置中心集成工具了,故此不会去实践某个框架. 下面链接是apollo 官网的教程,实在太详细了,本文介绍一下扩展数据源,和简单翻翻阅一下apollo 关键部分. apollo 服 ...

  9. modelMapper使用,将数据库查询对象直接转成DTO对象

    1.pom引入 <dependency> <groupId>org.modelmapper</groupId> <artifactId>modelmap ...

  10. css——圣杯布局

    圣杯布局要求 header和footer各自占领屏幕所有宽度,高度固定 中间dontainer部分为左中右三栏式布局 三栏布局中左右两侧宽度固定,中间部分自动填充 实现方式 1.浮动 先定义heade ...