Netty
background
I have always wanted to do a Netty project before, integrating functions such as message sending, message receiving, active push, identity verification, and automatic reconnection, which are convenient for direct use in actual project use. Because the time issue has been delayed until now o(╯□╰)o, just recently the company has related business functions, and it has been vigorously promoted by itself, so today it will be completed in one go while the iron is hot, and it can be used directly at that time.
introduce
Netty is an asynchronous event-driven network application framework for rapid development of maintainable high-performance protocol servers and clients.
Netty is an NIO client-server framework that enables quick and easy development of network applications such as protocol servers and clients. It greatly simplifies and streamlines network programming, such as TCP and UDP socket servers.
Simple does not mean that the resulting application will suffer from maintainability or performance issues. Netty was carefully designed from experience gained from implementing many protocols such as FTP, SMTP, HTTP, and various legacy binary and text-based protocols. So Netty managed to find a way to achieve ease of development, performance, stability and flexibility without compromise.
characteristic
design
1. Unified interface for multiple transport types - blocking and non-blocking
2. Simple but more powerful threading model
3. True connectionless datagram socket support
4. Link logic supports multiplexing
performance
1. Better throughput and lower latency than the core Java API
2. Less resource consumption, thanks to shared pool and reuse
3. Reduce memory copy
Safety
1. Full SSL/TLS and StartTLS support
2. Run in a restricted environment such as Applet or OSGI
combat
First go to the spring official website to initialize and download a SpringBoot project, and add three submodules: common、client、server
server: Netty service provider
client: Netty service connector
common: general package, tool class
Add Netty dependency information, version 5 is not recommended , because version 4 is no longer officially supported, so version 4 is selected
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.70.Final</version>
</dependency>
server
1. Write Netty service configuration
package com.whl.mq.server;
import com.whl.mq.handle.NettyServerChannelInitializer;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
/**
* @author hl.Wu
*
* @date 2022/07/17
* @description: netty server info
**/
@Slf4j
@Component
public class NioNettyServer {
@Value("${nio.netty.server.port: 8099}")
private int port;
@Autowired
private NettyServerChannelInitializer nettyServerChannelInitializer;
@Async
public void start() {
log.info("start to netty server port is {}", port);
// 接收连接
EventLoopGroup boss = new NioEventLoopGroup();
// 处理信息
EventLoopGroup worker = new NioEventLoopGroup();
try {
// 定义server
ServerBootstrap serverBootstrap = new ServerBootstrap();
// 添加分组
serverBootstrap.group(boss, worker)
// 添加通道设置非阻塞
.channel(NioServerSocketChannel.class)
// 服务端可连接队列数量
.option(ChannelOption.SO_BACKLOG, 128)
// 开启长连接
.childOption(ChannelOption.SO_KEEPALIVE, Boolean.TRUE)
// 流程处理
.childHandler(nettyServerChannelInitializer);
// 绑定端口
ChannelFuture cf = serverBootstrap.bind(port).sync();
// 优雅关闭连接
cf.channel().closeFuture().sync();
} catch (Exception e) {
log.error("connection error",e.getMessage(), e);
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
2. Write business processing flow NettyServerChannelInitializer
package com.whl.mq.handle;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author hl.Wu
* @date 2022/7/14
**/
@Component
@Slf4j
public class NettyServerChannelInitializer extends ChannelInitializer {
@Autowired
private ClientMessageHandler clientMessageHandler;
@Autowired
private ClientUserHandler clientUserHandler;
@Override
protected void initChannel(Channel channel) {
// 设置编码类型
channel.pipeline().addLast("decoder",new StringDecoder(CharsetUtil.UTF_8));
// 设置解码类型
channel.pipeline().addLast("encoder",new StringEncoder(CharsetUtil.UTF_8));
// 用户校验处理逻辑
channel.pipeline().addLast("ClientTokenHandler", clientUserHandler);
// 通过校验最终消息业务处理
channel.pipeline().addLast("ClientMessageHandler",clientMessageHandler);
}
}
3. Write user Token identity information verification logic
ChannelHandler.Sharable: If an annotation ChannelHandler
is used @Sharable
, only one instance can bootstrap
be created in it, and it can be added to one or more pipeline中
without competition, which can reduce the sum handler
of the same class , save resources and improve efficiency.new
GC
package com.whl.mq.handle;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.whl.common.bo.MessageBO;
import com.whl.common.bo.ResponseBO;
import com.google.common.base.Preconditions;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author hl.Wu
* @date 2022/7/14
**/
@Slf4j
@Component
@ChannelHandler.Sharable
public class ClientUserHandler extends SimpleChannelInboundHandler<String> {
/**
* k:token,v:userId
*/
public static Map<String,String> userMap = new ConcurrentHashMap<>();
/**
* 通道信息 k:userId v:通道信息
*/
public Map<String, Channel> channelMap = new ConcurrentHashMap<>();
@Override
public void channelRead0(ChannelHandlerContext ctx, String msg) {
// 解析消息对象 校验token信息
log.info("receive message info is {}", msg);
String token = (String)JSONUtil.getByPath(JSONUtil.parse(msg), MessageBO.Fields.token);
// 校验token是否失效
// TODO 根据业务场景添加身份校验
Preconditions.checkArgument(userMap.containsKey(token),"抱歉,Token已失效");
if(!channelMap.containsKey(userMap.get(token))){
channelMap.put(userMap.get(token), ctx.channel());
}
// 开启访问后面的处理逻辑
ctx.fireChannelRead(msg);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
Channel channel = ctx.channel();
ResponseBO resp = ResponseBO.builder()
.code(HttpStatus.FORBIDDEN.toString())
.success(Boolean.FALSE)
.build();
// 返回错误code
if(cause instanceof IllegalArgumentException){
resp.setMessage(cause.getMessage());
channel.writeAndFlush(JSON.toJSONString(resp));
log.warn("Token 校验未通过,{}", channel.localAddress());
}
}
}
4. Write the final processing logic of the ClientMessageHandler message
package com.whl.mq.handle;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.whl.common.bo.MessageBO;
import com.whl.common.bo.ResponseBO;
import io.netty.channel.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
/**
* @author hl.Wu
* @date 2022/7/14
**/
@Component
@Slf4j
@ChannelHandler.Sharable
public class ClientMessageHandler extends SimpleChannelInboundHandler<String> {
@Override
public void channelRead0(ChannelHandlerContext ctx, String msg) {
log.info("接收到的信息数据={}", msg);
// 返回请求结果
String requestUid = (String)JSONUtil.getByPath(JSONUtil.parse(msg), MessageBO.Fields.requestUid);
ResponseBO resp = ResponseBO
.builder()
.requestUid(requestUid)
.code(HttpStatus.OK.toString())
.success(Boolean.TRUE)
.message("请求成功")
.build();
Channel channel = ctx.channel();
channel.writeAndFlush(JSONUtil.toJsonStr(resp));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
Channel channel = ctx.channel();
ResponseBO resp = ResponseBO.builder()
.code(HttpStatus.INTERNAL_SERVER_ERROR.toString())
.success(Boolean.FALSE)
.build();
// 返回错误code
if(cause instanceof IllegalArgumentException){
resp.setMessage(cause.getMessage());
channel.writeAndFlush(JSON.toJSONString(resp));
log.warn("业务异常请排查, ;{}", cause.getMessage(), cause);
}
log.error("error message {}",cause.getMessage(),cause);
}
}
5. Write the interface control class that provides Token externally
package com.whl.mq.controller;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import com.whl.mq.handle.ClientUserHandler;
import io.netty.channel.Channel;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* @author hl.Wu
* @date 2022/7/14
**/
@RestController
@RequestMapping("/user")
@Api(tags = "用户管理")
public class UserController {
@Autowired
private ClientUserHandler clientUserHandler;
@GetMapping("/token")
@ApiOperation("获取token信息")
public String getToken(){
String token = IdUtil.fastSimpleUUID();
ClientUserHandler.userMap.put(token,token);
return token;
}
@PostMapping("/tips")
@ApiOperation("发送提醒")
public void sendToClient(@RequestParam("tips") String tips, @RequestParam("userId") String userId){
Map<String, Channel> channelMap = clientUserHandler.channelMap;
Channel channel = channelMap.get(userId);
if(ObjectUtil.isNotNull(channel)){
channel.writeAndFlush(tips);
}
}
}
6. Finally, the Netty method is automatically loaded and started in the startup service, and the asynchronous startup is set
package com.whl.mq;
import com.whl.mq.server.NioNettyServer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync
@Slf4j
public class NettyServerApplication implements CommandLineRunner {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(NettyServerApplication.class, args);
// start to netty server
context.getBean(NioNettyServer.class).start();
}
@Override
public void run(String... args) {
log.info("========================server start success========================");
}
}
Client
1. Write client connection Netty service configuration
PostConstruct: This method will be called after the service starts, and cannot contain any parameters.
PreDestroy: This method will be called before the service is closed, and cannot contain any parameters.
package com.whl.client.client;
import com.whl.client.handle.NettyClientChannelInitializer;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.concurrent.TimeUnit;
/**
* @author hl.Wu
* @date 2021/11/9
*
* @description: netty client info
**/
@Slf4j
@Component
public class NioNettyClient {
@Value("${netty.server.host:localhost}")
private String host;
@Value("${netty.server.port:8099}")
private int port;
private SocketChannel socketChannel;
/**
* work 线程组用于数据处理
*/
private EventLoopGroup work = new NioEventLoopGroup();
@Autowired
private NettyClientChannelInitializer nettyClientChannelInitializer;
/**
* 发送消息
*
* @param msg
*/
public void sendMsg(String msg) {
if(!socketChannel.isActive()){
// 如果失去连接,重新创建新的连接
log.info("****************服务失去连接,开始创建新的连接****************");
start();
}
// 发送消息
socketChannel.writeAndFlush(msg);
}
@PostConstruct
public void start() {
work = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(work)
// 设置通道
.channel(NioSocketChannel.class)
// 日志处理格式
.handler(new LoggingHandler(LogLevel.INFO))
// 禁用nagle算法
.option(ChannelOption.TCP_NODELAY, true)
// 保持长连接
.option(ChannelOption.SO_KEEPALIVE, Boolean.TRUE)
// 流程处理
.handler(nettyClientChannelInitializer);
// start to channel
ChannelFuture future = bootstrap.connect(host, port).sync();
future.addListener((ChannelFutureListener) future1 -> {
if (future1.isSuccess()) {
log.info("**********************服务连接成功**********************");
} else {
log.warn("**********************服务连接失败,20s后开始重新连接服务器**********************");
// 20s后重新连接
future1.channel().eventLoop().schedule(() -> this.start(), 20, TimeUnit.SECONDS);
}
});
socketChannel = (SocketChannel) future.channel();
} catch (Exception e) {
log.error("connection error", e.getMessage(), e);
}
}
@PreDestroy
private void close() {
if(socketChannel != null){
socketChannel.close();
}
work.shutdownGracefully();
}
}
2. Write NettyClientChannelInitializer to receive server response message business processing flow
Note:
In this example, I did not use the annotation method, because the heartbeat processing logic will refer to the bean of the reconnection service. If the annotation method is used, a circular dependency error will occur
package com.whl.client.handle;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* @author hl.Wu
* @date 2022/7/14
**/
@Component
@Slf4j
public class NettyClientChannelInitializer extends ChannelInitializer {
private final ResponseChannelHandler responseChannelHandler;
private final ClientHeartbeatHandler clientChannelHandler;
public NettyClientChannelInitializer(ResponseChannelHandler responseChannelHandler,
ClientHeartbeatHandler clientChannelHandler) {
this.responseChannelHandler = responseChannelHandler;
this.clientChannelHandler = clientChannelHandler;
}
@Override
protected void initChannel(Channel channel) {
channel.pipeline().addLast("decoder",new StringDecoder(CharsetUtil.UTF_8));
channel.pipeline().addLast("encoder",new StringEncoder(CharsetUtil.UTF_8));
channel.pipeline().addLast("responseChannelHandler",responseChannelHandler);
channel.pipeline().addLast("clientChannelHandler",clientChannelHandler);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.error("error message {}",cause.getMessage(),cause);
super.exceptionCaught(ctx, cause);
}
}
3. Write heartbeat processing logic ClientHeartbeatHandler
package com.whl.client.handle;
import com.alibaba.fastjson.JSON;
import com.whl.common.bo.MessageBO;
import com.whl.client.client.NioNettyClient;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* 心跳监测
*
* @author hl.Wu
* @date 2022/7/15
**/
@Component
@Slf4j
@ChannelHandler.Sharable
public class ClientHeartbeatHandler extends ChannelInboundHandlerAdapter {
private final NioNettyClient nioNettyClient;
public ClientHeartbeatHandler(NioNettyClient nioNettyClient) {
this.nioNettyClient = nioNettyClient;
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if(evt instanceof IdleStateEvent){
IdleStateEvent idleStateEvent = (IdleStateEvent) evt;
if (idleStateEvent.state() == IdleState.WRITER_IDLE) {
log.info("已经10s没有发送消息给服务端");
//发送心跳消息,并在发送失败时关闭该连接
MessageBO message = new MessageBO();
message.setHeartbeat(Boolean.TRUE);
ctx.writeAndFlush(JSON.toJSONString(message)).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
}
} else {
super.userEventTriggered(ctx, evt);
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
//如果运行过程中服务端挂了,执行重连机制
log.info("start to reconnect netty server");
ctx.channel().eventLoop().schedule(() -> nioNettyClient.start(), 3L, TimeUnit.SECONDS);
super.channelInactive(ctx);
}
}
4. Write the final processing logic ResponseChannelHandler for receiving the server response message
package com.whl.client.handle;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* @author hl.Wu
* @date 2022/7/15
**/
@Component
@Slf4j
@ChannelHandler.Sharable
public class ResponseChannelHandler extends SimpleChannelInboundHandler<String> {
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
log.info("response info is {}", msg);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
}
}
5. Finally write the sending message interface
package com.whl.client.controller;
import com.alibaba.fastjson.JSON;
import com.whl.common.bo.MessageBO;
import com.whl.client.client.NioNettyClient;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author hl.Wu
* @date 2022/7/14
**/
@RestController
@RequestMapping("/client")
@Api(tags = "客户端管理")
@Slf4j
public class ClientController {
@Autowired
private NioNettyClient nioNettyClient;
@PostMapping("/send")
public void send(@RequestBody MessageBO message) {
log.info(JSON.toJSONString(message));
nioNettyClient.sendMsg(JSON.toJSONString(message));
}
}
common
1. Write public entity objects
package com.whl.common.bo;
import java.io.Serializable;
/**
* @author hl.Wu
* @date 2022/7/14
**/
public abstract class BaseBO implements Serializable {
protected String toLog() {
return null;
}
}
package com.whl.common.bo;
import lombok.Data;
import lombok.experimental.FieldNameConstants;
import java.util.List;
/**
* @author hl.Wu
* @date 2022/7/14
**/
@Data
@FieldNameConstants
public class MessageBO extends BaseBO{
/**
* 请求唯一标识id
*/
private String requestUid;
private String token;
private Boolean heartbeat;
private List<VerificationEmsBO> data;
}
package com.whl.common.bo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author hl.Wu
* @date 2022/7/14
**/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class ResponseBO extends BaseBO{
private String requestUid;
/**
* 响应code
*/
private String code;
/**
* 响应提示信息
*/
private String message;
/**
* 是否成功
*/
private Boolean success;
}
package com.whl.common.bo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 验证码短信
*
* @author hl.Wu
* @date 2022/7/14
**/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class VerificationEmsBO extends BaseBO{
/**
* 手机号码
*/
private String mobile;
/**
* 短信内容
*/
private String message;
}
test
1. Start the server service
Access server service, http://localhost:8081/doc.html
2. Start the Client service
Access the client service, http://localhost:8082/doc.html
3. Request server token interface
4. Assemble the returned token information to the client sending interface
5. View the background receiving log
client
server
6. Test failure reconnection mechanism
Operation: Restart the server, and after the service starts successfully, request the client to send the interface again to achieve reconnection
7. The test server actively pushes messages to the client
Operation: Call the server to send reminder interface, userId uses the token when the client access is successful
client log
Summarize
You're done, tested all the features and everything works perfectly.
1. Authentication and heartbeat mechanisms are just simple processing, and Redis can be used for optimization in actual projects.
2. User information storage is too simple, no database is used
Ha ha! A little bit of knowledge every day, have you learned it today?
reference address
Project source code
https://gitee.com/mackjie/whl-netty
Branch: develop-nomq