项目中需要客户端APP连接多个服务端IP和端口,需要与服务端保持长链接,说到长链接的技术首先想到Netty框架,也是目前使用最广泛的长链接框架;Netty适合用来开发高性能长连接的客户端或服务器,其次Netty基于NIO,并做了封装与优化以及一些高级算法,使得使用起来相比原生NIO更加容易,性能更好。
1、配置build.gradle文件
build.gradle文件的dependencies标签下添加Netty引用
implementation 'io.netty:netty-all:4.1.23.Final'
2、主要代码
2.1、调用连接
private BootNettyClientThread bc;
private BootNettyClientThread bc1;
private BootNettyClientThread bc2;
private BootNettyClientThread bc3;
//创建连接线程
private void connectSever() {
bc = new BootNettyClientThread(Constants.PORT_1, Constants.IP_2, this);
bc.start();
bc1 = new BootNettyClientThread(Constants.PORT_2, Constants.IP, this);
bc1.start();
bc2 = new BootNettyClientThread(Constants.PORT_3, Constants.IP, this);
bc2.start();
bc3 = new BootNettyClientThread(Constants.PORT_4, Constants.IP, this);
bc3.start();
}
//数据发送
public void sendMsg(String string, int sendPort, String desc) {
int channelSize = BootNettyClientChannelCache.channelMapCache.size();
if (channelSize > 0) {
BootNettyClientChannelCache.removeDuplicated2();
for (Map.Entry<String, BootNettyClientChannel> entry : BootNettyClientChannelCache.channelMapCache.entrySet()) {
BootNettyClientChannel bootNettyChannel = entry.getValue();
int port = bootNettyChannel.getPort();
if (port == sendPort) {
ByteBuf buf = Unpooled.buffer().writeBytes(HexUtils.hexStringToByte(string));
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
bootNettyChannel.getChannel().writeAndFlush(buf);
LogUtils.i("send port:" + port + ",desc:" + desc + ",data:" + string);
}
}
}
}
@Override
public void onChannelRead(int port, String data) {
//接收数据
switch (port) {
case Constants.PORT_1:
break;
case Constants.PORT_2:
break;
case Constants.PORT_3:
break;
case Constants.PORT_4:
break;
default:
break;
}
}
@Override
public void onConnectState(int state, int port) {
//连接成功或断开
}
@Override
public void onExDisConnect(int state) {
//异常断开
}
@Override
public void onConnectFail(String string) {
//连接失败
}
//销毁时中断线程、断开连接、关闭服务等操作
private void releaseThread() {
if (bc != null) {
bc.releaseNetty();
bc.interrupt();
bc = null;
}
if (bc1 != null) {
bc1.releaseNetty();
bc1.interrupt();
bc1 = null;
}
if (bc2 != null) {
bc2.releaseNetty();
bc2.interrupt();
bc2 = null;
}
if (bc3 != null) {
bc3.releaseNetty();
bc3.interrupt();
bc3 = null;
}
}
2.2、连接线程
public class BootNettyClientThread extends Thread {
private final int port;
private final String address;
private IMessageListener mIMessageListener;
private BootNettyClient mBootNettyClient;
public BootNettyClientThread(int port, String address,
IMessageListener iMessageListener) {
this.port = port;
this.address = address;
this.mIMessageListener = iMessageListener;
mBootNettyClient = new BootNettyClient(mIMessageListener);
}
public void run() {
try {
mBootNettyClient.connect(address, port);
} catch (Exception e) {
Thread.currentThread().interrupt();
}
}
public void releaseNetty() {
if (mBootNettyClient != null) {
mBootNettyClient.stop();
}
}
2.3、建立连接
class BootNettyClient {
private Bootstrap b = new Bootstrap();
private EventLoopGroup group;
private IMessageListener mIMessageListener;
BootNettyClient(IMessageListener iMessageListener) {
mIMessageListener = iMessageListener;
group = new NioEventLoopGroup();
b.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.SO_KEEPALIVE, true)
.handler(new BootNettyChannelInitializer(mIMessageListener));
}
void connect(String host, int port) {
LogUtils.i("connect " + host + " " + Thread.currentThread());
b.connect(host, port).addListener((ChannelFutureListener) future -> {
/*
* 这里就不是主线程了,这里是 netty 线程中执行
*/
if (future.isSuccess()) {
BootNettyClientChannel bootNettyClientChannel = new BootNettyClientChannel();
Channel channel = future.channel();
String id = channel.id().toString();
bootNettyClientChannel.setPort(port);
bootNettyClientChannel.setChannel(channel);
bootNettyClientChannel.setCode("clientId:" + id);
LogUtils.i("channel:" + channel.isActive());
BootNettyClientChannelCache.save("clientId:" + id, bootNettyClientChannel);
LogUtils.i("netty client start success=" + id);
LogUtils.i("connect success " + host + " " + Thread.currentThread());
} else {
mIMessageListener.onConnectFail("WIFI连接失败");
LogUtils.i("connect failed " + host + " " + Thread.currentThread());
// 连接不成功,5秒后重新连接
future.channel().eventLoop().schedule(() -> {
LogUtils.i("reconnect " + host + " " + Thread.currentThread());
connect(host, port);
}, 5, TimeUnit.SECONDS);
}
});
}
void stop() {
//关闭服务线程
if (group != null) {
group.shutdownGracefully();
group = null;
}
//循环channel,然后断开连接,如果不断开则会创建多个新连接
for (Map.Entry<String, BootNettyClientChannel> entry : BootNettyClientChannelCache.channelMapCache.entrySet()) {
BootNettyClientChannel bootNettyChannel = entry.getValue();
bootNettyChannel.getChannel().disconnect();
}
BootNettyClientChannelCache.clear();
}
2.4、数据接收、连接状态、重连操作
public class BootNettyChannelInboundHandlerAdapter extends ChannelInboundHandlerAdapter {
private IMessageListener messageListener;
BootNettyChannelInboundHandlerAdapter(IMessageListener messageListener) {
this.messageListener = messageListener;
}
/**
* 从服务端收到新的数据时,这个方法会在收到消息时被调用
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
if (msg == null) {
return;
}
//BootNettyClientChannel bootNettyClientChannel = BootNettyClientChannelCache.get("clientId:" + ctx.channel().id().toString());
//if (bootNettyClientChannel != null) {
//bootNettyClientChannel.setLastData((String) msg);
InetSocketAddress inSocket = (InetSocketAddress) ctx.channel().remoteAddress();
messageListener.onChannelRead(inSocket.getPort(), (String) msg);
//}
//回应服务端
//ctx.write("I got server message thanks server!");
}
/**
* 从服务端收到新的数据、读取完成时调用
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
LogUtils.i("channelReadComplete");
ctx.flush();
}
/**
* 当出现 Throwable 对象才会被调用,即当 Netty 由于 IO 错误或者处理器在处理事件时抛出的异常时
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();//抛出异常,断开与客户端的连接
messageListener.onExDisConnect(1);
BootNettyClientChannelCache.clear();
}
/**
* 客户端与服务端第一次建立连接时 执行
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
super.channelActive(ctx);
InetSocketAddress inSocket = (InetSocketAddress) ctx.channel().remoteAddress();
String clientIp = inSocket.getAddress().getHostAddress();
LogUtils.i("连接成功:" + clientIp + ":" + inSocket.getPort());
messageListener.onConnectState(0, inSocket.getPort());
}
/**
* 客户端与服务端 断连时 执行
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
super.channelInactive(ctx);
InetSocketAddress inSocket = (InetSocketAddress) ctx.channel().remoteAddress();
String clientIp = inSocket.getAddress().getHostAddress();
int port = inSocket.getPort();
ctx.close(); //断开连接时,必须关闭,否则造成资源浪费
LogUtils.i("channelInactive异常断开,重新连接:" + clientIp + ":" + port);
messageListener.onConnectState(1, port);
BootNettyClientChannelCache.remove("clientId:" + ctx.channel().id().toString());
SystemClock.sleep(1000);
//如果退出界面不走重连操作,可设置Application中定义全局标志位
if (!MyApplication.isFinish) {
BootNettyClientThread thread = new BootNettyClientThread(port, clientIp, messageListener);
thread.start();
}
}
2.5、定义收发编解码方式
public class BootNettyChannelInitializer extends ChannelInitializer<Channel> {
private IMessageListener messageListener;
BootNettyChannelInitializer(IMessageListener messageListener) {
this.messageListener = messageListener;
}
@Override
protected void initChannel(Channel ch) {
ch.pipeline().addLast("encoder", new StringEncoder(CharsetUtil.UTF_8));
ch.pipeline().addLast("decoder", new MyDecoder());
/**
* 自定义ChannelInboundHandlerAdapter
*/
ch.pipeline().addLast(new BootNettyChannelInboundHandlerAdapter(messageListener));
}
}
public class MyDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out) throws Exception {
//创建字节数组,buffer.readableBytes可读字节长度
byte[] b = new byte[buffer.readableBytes()];
//复制内容到字节数组b
buffer.readBytes(b);
out.add(HexUtils.byte2HexStrNoSpace(b));
}
}
2.6、集合缓存channel
public class BootNettyClientChannel {
private transient volatile Channel channel;
//连接客户端唯一的code
private String code;
//客户端最新发送的消息内容
private String lastData;
private int port;
public Channel getChannel() {
return channel;
}
public void setChannel(Channel channel) {
this.channel = channel;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getLastData() {
return lastData;
}
public void setLastData(String lastData) {
this.lastData = lastData;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
}
public class BootNettyClientChannelCache {
public static volatile Map<String, BootNettyClientChannel> channelMapCache = new ConcurrentHashMap<String, BootNettyClientChannel>();
public static void add(String code, BootNettyClientChannel channel) {
channelMapCache.put(code, channel);
}
public static BootNettyClientChannel get(String code) {
return channelMapCache.get(code);
}
public static void remove(String code) {
channelMapCache.remove(code);
}
public static void clear() {
channelMapCache.clear();
}
public static void save(String code, BootNettyClientChannel channel) {
if (channelMapCache.get(code) == null) {
add(code, channel);
}
}
public static void removeDuplicated2() {
Set<Integer> set = new HashSet<>();
Iterator<Map.Entry<String, BootNettyClientChannel>> iterator = channelMapCache.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, BootNettyClientChannel> entry = iterator.next();
if (!set.add(entry.getValue().getPort())) {
iterator.remove();
}
}
LogUtils.i("channelMapCache.size:" + channelMapCache.size());
}
}
以上便可完成客户端连接多个服务端IP和端口的需求,实现在不同端口接收和发送数据,目前在项目中使用不高,并没有做太深的优化,后续可以结合实际的业务需求深度code。