Netty第一次使用
远程控制pdu系统
第一次使用 Netty 是在开发一个远程控制pdu系统的时候,刚开始是使用Java原生的socket api,使用的是BIO模式,系统作为一个服务端接收和发送pdu的数据,同时用户访问该系统操作pdu。采用定时任务接收数据,处理数据,保存到数据库中供用户访问。由于每次发送数据到pdu都会返回数据,其他时间只接收固定时间差的心跳检测的数据,所以开始有了使用Netty的想法。
使用的框架:springBoot、mybatisPlus、Netty、shiro、druid连接池
数据库:mysql
前端:jq、layui、bootstrap
Netty服务器
boss线程处理连接请求,将任务发送给work线程进行处理,
配置端口,其他参数等等
package com.pdu.device.netty;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
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.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.Map;
@Component
@Slf4j
public class NettyServer {
/**
* boss 线程组用于处理连接工作
*/
private EventLoopGroup boss = new NioEventLoopGroup();
/**
* work 线程组用于数据处理
*/
private EventLoopGroup work = new NioEventLoopGroup();
private Integer port=4600;
/**
* 启动Netty Server
*
* @throws InterruptedException
*/
@PostConstruct
public void start() throws InterruptedException {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(boss, work)
// 指定Channel
.channel(NioServerSocketChannel.class)
//使用指定的端口设置套接字地址
.localAddress(new InetSocketAddress(port))
//服务端可连接队列数,对应TCP/IP协议listen函数中backlog参数
.option(ChannelOption.SO_BACKLOG, 1024)
//设置TCP长连接,一般如果两个小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文
.childOption(ChannelOption.SO_KEEPALIVE, true)
//将小的数据包包装成更大的帧进行传送,提高网络的负载
.childOption(ChannelOption.TCP_NODELAY, true)
.childHandler(new ServerChannelInitializer());
ChannelFuture future = bootstrap.bind().sync();
if (future.isSuccess()) {
log.info("启动 Netty Server");
}
}
@PreDestroy
public void destory() throws InterruptedException {
boss.shutdownGracefully().sync();
work.shutdownGracefully().sync();
log.info("关闭Netty");
}
}
ServerChannel初始化
Netty解码、编码配置
package com.pdu.device.netty;
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.util.CharsetUtil;
import org.apache.commons.lang3.CharSet;
import java.nio.charset.Charset;
public class ServerChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//添加编解码
socketChannel.pipeline().addLast("decoder", new StringDecoder(Charset.forName("GBK")));
socketChannel.pipeline().addLast("encoder", new StringEncoder(Charset.forName("GBK")));
socketChannel.pipeline().addLast(new NettyServerHandler());
}
}
消息处理
连接,数据接收,连接断开消息处理及持久化,
保存连接的ChannelHandlerContext,便于发送数据,由于数据有时返回多行,所以将channelRead的Object o转化为inputStream按行遍历
package com.pdu.device.netty;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.pdu.device.MessageHandle.*;
import com.pdu.device.entity.TblDevice;
import com.pdu.device.service.ITblDeviceService;
import com.pdu.sys.common.SpringUtil;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelId;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.extern.slf4j.Slf4j;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
private ITblDeviceService tblDeviceService = SpringUtil.getBean(ITblDeviceService.class);//线程获取service
/**
* 管理一个全局map,保存连接进服务端的通道数量
*/
public static ConcurrentHashMap<ChannelId, ChannelHandlerContext> CHANNEL_MAP = new ConcurrentHashMap<>();
/**
* 管理一个全局map,保存连接进服务端的通道ChannelId
*/
public static ConcurrentHashMap< String,ChannelId> DEVICE_ID_MAP = new ConcurrentHashMap<>();
/**
* 客户端连接会触发
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("Channel active......");
InetSocketAddress insocket = (InetSocketAddress) ctx.channel().remoteAddress();
String clientIp = insocket.getAddress().getHostAddress();
int clientPort = insocket.getPort();
//获取连接通道唯一标识
ChannelId channelId = ctx.channel().id();
System.out.println();
//如果map中不包含此连接,就保存连接
if (CHANNEL_MAP.containsKey(channelId)) {
log.info("客户端【" + channelId + "】是连接状态,连接通道数量: " + CHANNEL_MAP.size());
} else {
//保存连接
CHANNEL_MAP.put(channelId, ctx);
log.info("客户端【" + channelId + "】连接netty服务器[IP:" + clientIp + "--->PORT:" + clientPort + "]");
log.info("连接通道数量: " + CHANNEL_MAP.size());
}
}
/**
* 客户端发消息会触发
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object o) throws Exception {
log.info("服务器收到消息: {}", o.toString().trim());
ChannelId channelId = ctx.channel().id();
InetSocketAddress insocket = (InetSocketAddress) ctx.channel().remoteAddress();
String clientIp = insocket.getAddress().getHostAddress();
try {
InputStream inputStream = new ByteArrayInputStream(o.toString().getBytes());
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
String msg;
while ((msg = br.readLine()) != null) {
if ((msg.length() > 20) && msg.substring(0, 11).equals("START login")) {//登录
log.info("login消息处理开始");
LoginHandle loginHandle = new LoginHandle();
String deviceId = loginHandle.Handle(msg, clientIp, tblDeviceService);
DEVICE_ID_MAP.put(deviceId, channelId);
ctx.write("Login Successful");
ctx.flush();
} else if (msg.trim().equals("S")) {//10秒一次心跳检测
log.info("S消息处理开始");
ConnectHandle connectHandle = new ConnectHandle();
connectHandle.handle(clientIp, tblDeviceService);
} else {
log.info("other消息处理开始");
List<TblDevice> list = new ArrayList<>();
QueryWrapper<TblDevice> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("public_network_ip", clientIp);//内网Ip
list = tblDeviceService.list(queryWrapper);
if (list.size() == 1) {
HandleMessage handleMessage = new HandleMessage();
handleMessage.handle(msg, list.get(0).getDeviceId(), tblDeviceService);
}
}
}
log.info("服务器处理消息完成");
} catch (Exception e) {
log.error("服务器处理消息失败");
}
}
/**
* @param ctx
* @author xiongchuan on 2019/4/28 16:10
* @DESCRIPTION: 有客户端终止连接服务器会触发此函数
* @return: void
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) {
InetSocketAddress insocket = (InetSocketAddress) ctx.channel().remoteAddress();
String clientIp = insocket.getAddress().getHostAddress();
ChannelId channelId = ctx.channel().id();
//包含此客户端才去删除
if (CHANNEL_MAP.containsKey(channelId)) {
//删除连接
CHANNEL_MAP.remove(channelId);
System.out.println();
log.info("客户端【" + channelId + "】退出netty服务器[IP:" + clientIp + "--->PORT:" + insocket.getPort() + "]");
log.info("连接通道数量: " + CHANNEL_MAP.size());
}
}
/**
* 发生异常触发
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}