netty,io与nio的区别,先上代码在分析:
依赖包:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.12.Final</version>
</dependency>
服务端:
import com.gs.util.ReadUtils;
import com.gs.util.YamlUtils;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Component
@EnableScheduling
public class NettyServer {
public static Map<String, ChannelHandlerContext> map = new HashMap<String, ChannelHandlerContext>();
public static EventLoopGroup bossGroup;
public static EventLoopGroup workerGroup;
private static final int port = 9092;
@Async
@Scheduled(cron = "*/3 * * * * ?")
public void run() throws Exception{
// 记录netty 服务重启时间
new YamlUtils().getRestar();
//NioEventLoopGroup是用来处理IO操作的多线程事件循环器 用来接收进来的连接
bossGroup = new NioEventLoopGroup();
// 用来处理已经被接收的连接
workerGroup = new NioEventLoopGroup();
try{
//是一个启动NIO服务的辅助启动类
ServerBootstrap server =new ServerBootstrap();
server.group(bossGroup,workerGroup )
.channel(NioServerSocketChannel.class)
// 这里告诉Channel如何接收新的连接
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 自定义处理类
ch.pipeline().addLast(new NettyServerFilter());
}
})
;
// 具体参数介绍 https://blog.youkuaiyun.com/zhousenshan/article/details/72859923
server.option(ChannelOption.SO_BACKLOG,128);
// 保持长连接
server.childOption(ChannelOption.SO_KEEPALIVE, true);
//绑定端口,同步等待成功
ChannelFuture future = server.bind(port).sync();
ReadUtils.write("服务端启动成功...");
// 监听服务器关闭监听
future.channel().closeFuture().sync();
}finally {
//关闭EventLoopGroup,释放掉所有资源包括创建的线程
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception{
new NettyServer().run();
}
public static Map<String, ChannelHandlerContext> map = new
HashMap<String, ChannelHandlerContext>();
- 这个很重要,是用来记录客户端的ID,也是通过它来像客户端发送信息。需要注意的是在心跳结束或者网络异常等导致连接断开时,需要异常 map 里对应的端口。 (代码中注释几乎都解释了…)
public static EventLoopGroup bossGroup;
public static EventLoopGroup workerGroup;
这两个这样写是为了在修改配置文件的时候方便关闭服务端。可能这样写有些不妥吧。
*代码中ReadUtils类是我用来记录netty 服务端的日志,等结尾会给大家都贴出来。
服务端的:心跳设置
import com.gs.util.YamlUtils;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.timeout.IdleStateHandler;
import java.util.concurrent.TimeUnit;
public class NettyServerFilter extends ChannelInitializer<SocketChannel>{
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 配置文件
Integer time = new YamlUtils().getCode("code2");
// 心跳机制
// readerIdleTime:读超时时间
// writerIdleTime:写超时时间
// allIdleTime:所有类型的超时时间
ch.pipeline().addLast(new IdleStateHandler(0, time +1,0, TimeUnit.SECONDS));
// 解码和编码,应和客户端一致
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new StringEncoder());
//处理服务端的业务逻辑
ch.pipeline().addLast(new HeartNettyServerHandler());
}
*这边心跳设置有3个参数,服务端这边我永远是比客服端多出一秒的。也可以服务端和客户端直接都采用第三个参数,具体看注释吧。
服务端的业务逻辑
import com.gs.entity.result.ImeiResult;
import com.gs.service.BusinessService;
import com.gs.service.impl.GsNettyServiceImpl;
import com.gs.util.HttpUtils;
import com.gs.util.ReadUtils;
import com.gs.util.YamlUtils;
import io.netty.channel.*;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Resource;
import java.net.InetAddress;
import java.util.List;
@Slf4j
public class HeartNettyServerHandler extends ChannelInboundHandlerAdapter {
/**
* 空闲次数
*/
private int readIdleTimes = 1;
/**
* 发送次数
*/
private int count = 1;
/**
* 超时处理,如果5秒没有收到客户端的心跳,就触发; 如果超过两次,则直接关闭;
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object obj) throws Exception {
if (obj instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) obj;
if (IdleState.READER_IDLE.equals(event.state())) { // 如果读通道处于空闲状态,说明没有接收到心跳命令
if (readIdleTimes > 2) {
ReadUtils.write("超过两次无客户端请求,关闭该channel");
ctx.channel().close();
}
ReadUtils.write("已等待5秒还没收到客户端发来的消息");
readIdleTimes ++;
}
if(readIdleTimes > 2){
ReadUtils.write(" server : 读空闲超过2次,关闭连接 client:"+ ctx.channel().id());
ctx.channel().writeAndFlush("Close the connection");
ctx.channel().close();
}
} else {
super.userEventTriggered(ctx, obj);
}
}
/**
* 业务逻辑处理
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ReadUtils.write("第" + count + "次" + ",服务端收到的消息:" + msg);
String message = (String) msg;
// 如果是心跳命令,服务端收到命令后回复一个相同的命令给客户端
if ("hb_request".equals(message)) {
ctx.channel().writeAndFlush("copy that the heartbeat");
ctx.flush();
}else{
// 业务数据
ReadUtils.write("开始处理业务");
ReadUtils.write("报文总长: " + message);
String channelId = ctx.channel().id().toString();
if (NettyServer.map.get(channelId) != null && NettyServer.map.get(channelId).equals(ctx)) {
ReadUtils.write("加入map通道");
}else{
ReadUtils.write("加入map通道");
NettyServer.map.put(channelId,ctx);
}
}
count++;
}
/**
* 异常处理
* @param ctx
* @param cause
*/
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
try{
cause.printStackTrace();
}catch (Exception e){
e.printStackTrace();
ReadUtils.write("netty 异常.."+e.getMessage());
}finally {
ctx.close();
}
}
/**
* 建立连接时,返回消息
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ReadUtils.write("连接的客户端地址:" + ctx.channel().remoteAddress());
ReadUtils.write("连接的客户端ID:" + ctx.channel().id());
ctx.writeAndFlush("client: "+ InetAddress.getLocalHost().getHostName() + " success connected!");
ReadUtils.write("connection in .. success");
super.channelActive(ctx);
}
/**
* 客户端主动断开服务端的链接,关闭流
* */
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
ReadUtils.write(ctx.channel().localAddress().toString() + " 通道不活跃!");
removeChannnelMap(ctx);
// 关闭流
ctx.close();
}
/**
* 删除map...
* @param ctx
*/
public void removeChannnelMap(ChannelHandlerContext ctx){
String channelId = ctx.channel().id().toString();
ChannelHandlerContext context = NettyServer.map.get(channelId);
if( NettyServer.map.get(channelId) !=null && context.equals(ctx)){
NettyServer.map.remove(channelId);
ReadUtils.write("remove: "+channelId);
}
}
服务端结束。
客户端开始:
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
@EnableScheduling
public class NettyClient {
private static NioEventLoopGroup workGroup = new NioEventLoopGroup(4);
private static Channel channel;
private static Bootstrap bootstrap;
public static void main(String[] args) {
try {
bootstrap = new Bootstrap();
bootstrap
.group(workGroup)
.channel(NioSocketChannel.class)
.handler(new NettyClientFilter());
doConnect();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
//@Async
//@Scheduled(cron = "*/4 * * * * ?")
public void run(){
try {
bootstrap = new Bootstrap();
bootstrap
.group(workGroup)
.channel(NioSocketChannel.class)
.handler(new NettyClientFilter());
doConnect();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
protected static void doConnect() {
if (channel != null && channel.isActive()) {
return;
}
ChannelFuture future = bootstrap.connect("localhost",9091);
future.addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture futureListener) throws Exception {
if (futureListener.isSuccess()) {
channel = futureListener.channel();
Channel channel = futureListener.sync().channel();
channel.writeAndFlush("Connect to server successfully!");
} else {
log.info("Failed to connect to server, try connect after 10s");
futureListener.channel().eventLoop().schedule(new Runnable() {
@Override
public void run() {
doConnect();
}
}, 10, TimeUnit.SECONDS);
}
}
});
}
客户端这边也是比较全了,有网络异常,重新连接。博主的这种配置,都写在了配置文件里。最后都给大家贴出来。
客户端心跳设置
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 配置文件
Integer time = new YamlUtils().getCode("code2");
ChannelPipeline ph = ch.pipeline();
//因为服务端设置的超时时间是5秒,所以客户端设置4秒
ph.addLast(new IdleStateHandler(0, time, 0, TimeUnit.SECONDS));
// 解码和编码,应和服务端一致
ph.addLast(new StringDecoder());
ph.addLast(new StringEncoder());
//处理客户端的业务逻辑
ph.addLast(new HeartNettyClientHandler());
}
客户端业务:
import com.gs.util.DateUtils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.CharsetUtil;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class HeartNettyClientHandler extends ChannelInboundHandlerAdapter {
/**
* 客户端请求的心跳命令
*/
private static final ByteBuf HEARTBEAT_SEQUENCE =
Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("hb_request", CharsetUtil.UTF_8));
/**
* 空闲次数
*/
private int idle_count = 1;
/**
* 发送次数
*/
private int count = 1;
/**
* 循环次数
*/
private int cycle_count = 1;
/**
* 建立连接时
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("建立连接时:" + DateUtils.getTime());
ctx.fireChannelActive();
}
/**
* 关闭连接时
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
log.info("关闭连接时:" + DateUtils.getTime());
}
/**
* 心跳请求处理,每4秒发送一次心跳请求
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object obj) throws Exception {
log.info("\r\n循环请求的时间:" + DateUtils.getTime() + ",次数" + cycle_count);
if (obj instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) obj;
if (IdleState.WRITER_IDLE.equals(event.state())) { // 如果写通道处于空闲状态就发送心跳命令
// 设置发送次数,允许发送3次心跳包
if (idle_count <= 3) {
idle_count++;
ctx.channel().writeAndFlush(HEARTBEAT_SEQUENCE.duplicate());
} else {
log.info("心跳包发送结束,不再发送心跳请求!!!");
ctx.close();
}
}
}
cycle_count ++;
}
/**
* 业务逻辑处理
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 根据命令码执行......
log.info("第" + count + "次" + ",客户端收到的消息:" + msg);
count ++;
}
}
贴入ReadUtils 工具类
import com.gs.common.DefContants;
import lombok.Cleanup;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.util.StringUtils;
import java.io.*;
@Slf4j
public class ReadUtils {
public static void read(){
Resource resource = new ClassPathResource(DefContants.LOG_PATH);
try{
@Cleanup InputStream is = resource.getInputStream();
@Cleanup InputStreamReader isr = new InputStreamReader(is);
@Cleanup BufferedReader br = new BufferedReader(isr);
String data = null;
while((data = br.readLine()) != null) {
System.out.println(data);
}
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 日志
* @param str
*/
public static void write(String str) {
String path = new ReadUtils().getClass().getClassLoader().getResource(DefContants.LOG_PATH).getPath();
String key ="\n"+DateUtils.getTime()+":"+str;
try {
FileOutputStream fos = new FileOutputStream(new File(path),true);
fos.write(key.getBytes());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 原始透传数据只保留 20 条
*/
public static void clear(){
Resource resource = new ClassPathResource(DefContants.PASSTHROUTH_PATH);
try{
@Cleanup InputStream is = resource.getInputStream();
@Cleanup InputStreamReader isr = new InputStreamReader(is);
@Cleanup BufferedReader br = new BufferedReader(isr);
String data = null;
StringBuffer buffer = new StringBuffer();
int i = 0;
while((data = br.readLine()) != null) {
if(i != 0){
if(data != null){
buffer.append(data+",");
}
}
i ++;
}
String toString = buffer.toString();
if(!StringUtils.isEmpty(toString)){
String str = buffer.deleteCharAt(buffer.length() - 1).toString();
String[] split = str.split(",");
int length = split.length;
if(length > 20){
String path = new ReadUtils().getClass().getClassLoader().getResource(DefContants.PASSTHROUTH_PATH).getPath();
String key ="原始数据透传 - 只保留 最新 20 条";
try {
FileOutputStream fos = new FileOutputStream(new File(path));
fos.write(key.getBytes());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 原始数据透传
* @param str
*/
public static void writeDate(String str) {
clear();
String path = new ReadUtils().getClass().getClassLoader().getResource(DefContants.PASSTHROUTH_PATH).getPath();
String key ="\n"+DateUtils.getTime()+":"+str;
try {
FileOutputStream fos = new FileOutputStream(new File(path),true);
fos.write(key.getBytes());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
此外在贴一个获取配置文件的公具类,可以修改,但需要注意的是修改后的文件并不会在原有基础上改变,而是在target/ 下
import com.alibaba.fastjson.JSONObject;
import com.gs.common.DefContants;
import com.sun.istack.Nullable;
import lombok.Cleanup;
import lombok.extern.slf4j.Slf4j;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;
import java.io.FileWriter;
import java.io.InputStream;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
@Slf4j
public class YamlUtils {
private final static DumperOptions OPTIONS = new DumperOptions();
static{
//设置yaml读取方式为块读取
OPTIONS.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
OPTIONS.setDefaultScalarStyle(DumperOptions.ScalarStyle.PLAIN);
OPTIONS.setPrettyFlow(false);
}
/**
* 将yaml配置文件转化成map
* fileName 默认是resources目录下的yaml文件, 如果yaml文件在resources子目录下,需要加上子目录 比如:conf/config.yaml
* @param fileName
* @return
*/
public static Map<String,Object> getYamlToMap(String fileName){
LinkedHashMap<String, Object> yamls = new LinkedHashMap<>();
Yaml yaml = new Yaml();
try {
@Cleanup InputStream in = YamlUtils.class.getClassLoader().getResourceAsStream(fileName);
yamls = yaml.loadAs(in,LinkedHashMap.class);
}catch (Exception e){
log.error("{} load failed !!!" , fileName);
}
return yamls;
}
/**
* key格式:aaa.bbb.ccc
* 通过properties的方式获取yaml中的属性值
* @param key
* @param yamlMap
* @return
*/
public static Object getValue(String key, Map<String,Object> yamlMap){
String[] keys = key.split("[.]");
Object o = yamlMap.get(keys[0]);
if(key.contains(".")){
if(o instanceof Map){
return getValue(key.substring(key.indexOf(".")+1),(Map<String,Object>)o);
}else {
return null;
}
}else {
return o;
}
}
/**
* 使用递归的方式设置map中的值,仅适合单一属性
* key的格式: "server.port"
* server.port=111
*
**/
public Map<String,Object> setValue(String key,Object value) {
Map<String, Object> result = new LinkedHashMap<>();
String[] keys = key.split("[.]");
int i = keys.length - 1;
result.put(keys[i], value);
if (i > 0) {
return setValue(key.substring(0, key.lastIndexOf(".")), result);
}
return result;
}
public static Map<String,Object> setValue(Map<String,Object> map, String key, Object value){
String[] keys = key.split("\\.");
int len = keys.length;
Map temp = map;
for(int i = 0; i< len-1; i++){
if(temp.containsKey(keys[i])){
temp = (Map)temp.get(keys[i]);
}else {
return null;
}
if(i == len-2){
temp.put(keys[i+1],value);
}
}
for(int j = 0; j < len - 1; j++){
if(j == len -1){
map.put(keys[j],temp);
}
}
return map;
}
/**
* 修改yaml中属性的值
* @param key key是properties的方式: aaa.bbb.ccc (key不存在不修改)
* @param value 新的属性值 (新属性值和旧属性值一样,不修改)
* @param yamlName
* @return true 修改成功,false 修改失败。
*/
public static boolean updateYaml(String key, @Nullable Object value, String yamlName){
Map<String, Object> yamlToMap = getYamlToMap(yamlName);
if(null == yamlToMap) {
return false;
}
Object oldVal = getValue(key, yamlToMap);
//未找到key 不修改
if(null == oldVal){
log.error("{} key is not found",key);
return false;
}
//不是最小节点值,不修改
if(oldVal instanceof Map){
log.error("input key is not last node {}",key);
return false;
}
//新旧值一样 不修改
if(value.equals(oldVal)){
log.info("newVal equals oldVal, newVal: {} , oldVal: {}",value,oldVal);
return false;
}
Yaml yaml = new Yaml(OPTIONS);
YamlUtils utils = new YamlUtils();
String path = utils.getClass().getClassLoader().getResource(yamlName).getPath();
try {
Map<String, Object> resultMap = setValue(yamlToMap, key, value);
if(resultMap != null){
yaml.dump(setValue(yamlToMap,key,value),new FileWriter(path));
return true;
}else {
return false;
}
}catch (Exception e){
log.error("yaml file update failed !");
log.error("msg : {} ",e.getMessage());
log.error("cause : {} ",e.getCause());
}
return false;
}
/**
* 参数配置获取
* @param str
* @return
*/
public Integer getCode(String str){
Map<String, Object> yamlToMap = this.getYamlToMap(DefContants.PC);
String param = yamlToMap.get("param").toString().replace("=",":");
JSONObject parseObject = JSONObject.parseObject(param);
String code = parseObject.get(str).toString().replace(".",",");
String time = code.split(",")[0];
return Integer.valueOf(time);
}
/**
* 服务重启时间记录
*/
public static void getRestar(){
boolean b = updateYaml("netty.start", DateUtils.getTime(), DefContants.PC);
log.info("netty 服务重启:"+b);
}
/**
* 获取aes 加密密钥
* @return
*/
public static byte[] getAes(){
Map<String, Object> yamlToMap = getYamlToMap(DefContants.PC);
byte[] aes = yamlToMap.get("aes").toString().getBytes();
return aes;
}
/**
* 配置参数修改次数
*/
public static Integer getCount(){
Map<String, Object> yamlToMap = getYamlToMap(DefContants.PC);
String count = yamlToMap.get("count").toString();
return Integer.valueOf(count);
}
public static void main(String[] args) {
Date date = new Date();
YamlUtils configs = new YamlUtils();
boolean b = new YamlUtils().updateYaml("netty.start", date.toString(), DefContants.PC);
System.out.println(b);
Map<String, Object> yamlToMap = configs.getYamlToMap(DefContants.PC);
Object restart = yamlToMap.get("netty.start");
System.out.println(restart);
}
}