基于Netty的Android长连接实现

使用Netty构建Android长连接实战
本文介绍了基于Netty实现Android长连接的详细过程,包括协议确定、加解密、封装解封装、发包收包对应、心跳机制、网络重连及超时重发策略。重点讲解了协议结构、RSA非对称加密与AES对称加密的运用,以及如何通过序列号sn确保通信的对应性。

0. 传送门

https://github.com/jeanpeng/Netty4Android.git (欢迎star)

1. 协议的确定

  • 协议头——4字节,标识协议体的长度
  • 协议体——包括4字节扩展信息+传参,其中扩展信息里包括协议加密类型,传参为key,value键值对类型的数据,数据格式为4字节key长度+key+4字节value长度+value

2.协议加解密

  • 客户端随机生成16位密钥,通过RSA非对称加密方式将密钥同步给服务器
  • 密钥同步后,使用该密钥通过AES对称加密的方式对协议进行加密,然后进行数据传输

3.协议封装和解封装

package com.**.nettylib.netty.codec;

import com.**.nettylib.constant.NetworkConfig;
import com.**.nettylib.netty.Request;
import com.**.nettylib.netty.util.AESUtils;
import com.**.nettylib.netty.util.ExtendInfo;
import com.**.nettylib.netty.util.GZIPUtils;
import com.**.nettylib.netty.util.KeyManager;
import com.**.nettylib.netty.util.MsgEncryptType;
import com.**.nettylib.netty.util.ProtocolUtil;
import com.**.nettylib.netty.util.RSAUtils;

import java.io.ByteArrayOutputStream;
import java.util.Map;
import java.util.Set;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;

/**
 * Created by james on 2018/7/30.
 * 加密协议组包,支持处理粘包和半包
 */
public class MessageEncryptEncoder extends MessageToByteEncoder<Request> {

    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, Request request, ByteBuf byteBuf) throws Exception {
        byteBuf.writeBytes(encodeResponse(request));
    }

    public byte[] encodeResponse(Request request) {
        try {
            Map<String, String> properties = request.toMap();
            Set<Map.Entry<String, String>> propertiesSet = properties.entrySet();
            ByteArrayOutputStream bodyStream = new ByteArrayOutputStream();
            for (Map.Entry<String, String> property : propertiesSet) {
                String propertyName = property.getKey();
                String propertyValue = property.getValue();
                bodyStream.write(ProtocolUtil.stringToByteArray(propertyName, "utf-8"));
                bodyStream.write(ProtocolUtil.stringToByteArray(propertyValue, "utf-8"));
            }
            byte[] bodyBytes = bodyStream.toByteArray();// 数据部分
            bodyStream.close();

            ByteArrayOutputStream responseStream = new ByteArrayOutputStream();
            ExtendInfo ext = request.getExt();
            byte[] extBytes = ext.toExtBytes();
            int msgEncryptType = ext.getMsgEncryptType();
            if (MsgEncryptType.NONO == msgEncryptType) {
                // do noting
            } else if (MsgEncryptType.RSA == msgEncryptType) {
                bodyBytes = doEncodeOfRSA(bodyBytes);
            } else if (MsgEncryptType.AES == msgEncryptType) {
                bodyBytes = doEncodeOfAES(bodyBytes);
            }
            responseStream.write(ProtocolUtil.intToByteArray(bodyBytes.length + extBytes.length));
            responseStream.write(extBytes);
            responseStream.write(bodyBytes);
            byte[] responseBytes = responseStream.toByteArray();// 整个响应包
            responseStream.close();
            return responseBytes;
        } catch (Exception e) {
            return null;
        }
    }

    private byte[] doEncodeOfRSA(byte[] bodyBytes) {
        // 先压缩,再加密
        return RSAUtils.publicEncrypt(GZIPUtils.compress(bodyBytes), NetworkConfig.RSA_PUBLIC_KEY);
    }

    private byte[] doEncodeOfAES(byte[] bodyBytes) {
        String password = KeyManager.aesKey;
        // 先压缩,再加密
        return AESUtils.encrypt(GZIPUtils.compress(bodyBytes), password);
    }
}

package com.**.nettylib.netty.codec;

import com.**.nettylib.netty.util.AESUtils;
import com.**.nettylib.netty.util.ExtendInfo;
import com.**.nettylib.netty.util.GZIPUtils;
import com.**.nettylib.netty.util.KeyManager;
import com.**.nettylib.netty.util.MsgEncryptType;
import com.**.nettylib.netty.util.ProtocolUtil;

import java.io.ByteArrayInputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;

/**
 * Created by james on 2018/7/30.
 * 加密协议拆包,支持处理粘包和半包
 */
public class MessageEncryptDecoder extends ByteToMessageDecoder {
    public static String skey;

    private byte[] remainingBytes;

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
        ByteBuf currBB = null;
        if(remainingBytes == null) {
            currBB = msg;
        }else {
            byte[] tb = new byte[remainingBytes.length + msg.readableBytes()];
            System.arraycopy(remainingBytes, 0, tb, 0, remainingBytes.length);
            byte[] vb = new byte[msg.readableBytes()];
            msg.readBytes(vb);
            System.arraycopy(vb, 0, tb, remainingBytes.length, vb.length);
            currBB = Unpooled.copiedBuffer(tb);
        }
        while(currBB.readableBytes() > 0) {
            if(!doDecode(ctx, currBB, out)) {
                break;
            }
        }
        if(currBB.readableBytes() > 0) {
            remainingBytes = new byte[currBB.readableBytes()];
            currBB.readBytes(remainingBytes);
        }else {
            remainingBytes = null;
        }
    }

    /**
     * @Title:doDecode
     * @param ctx
     * @param msg
     * @param out
     * @return boolean
     */
    private boolean doDecode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out)  throws Exception{
        if(msg.readableBytes() < 4)
            return false;
        msg.markReaderIndex();
        int len = msg.readInt();
        if(msg.readableBytes() < len) {
            msg.resetReaderIndex();
            return false;
        }
        byte[] extBytes = new byte[4];
        msg.readBytes(extBytes);
        ExtendInfo ext = new ExtendInfo(extBytes);
        int msgEncryptType = ext.getMsgEncryptType();

        byte[] body = new byte[len-4];
        msg.readBytes(body);
        Map<String, String> properties = decodeProperties(body,msgEncryptType);
        out.add(properties);
        if(msg.readableBytes() > 0)
            return true;
        return false;
    }

    private Map<String, String> decodeProperties(byte[] bodyBytes,int msgEncryptType) throws Exception {
        if (MsgEncryptType.NONO == msgEncryptType) {
            // do noting
        } else if (MsgEncryptType.AES == msgEncryptType) {
            bodyBytes = doDecodeOfAES(bodyBytes);
        }

        StringBuilder builder = new StringBuilder();
        for (byte b : bodyBytes) {
            builder.append(b).append(',');
        }
        try {
            Map<String, String> properties = new HashMap<String, String>();
            ByteArrayInputStream in = new ByteArrayInputStream(bodyBytes);
            in.skip(0);
            while (in.available() > 0) {
                String propertyName = ProtocolUtil.readString(in, "utf-8");
                String propertyValue = ProtocolUtil.readString(in, "utf-8");
                properties.put(propertyName, propertyValue);
            }
            return properties;
        } catch (Exception e) {
            throw new Exception("DoDecode exception, Received:byte[" + bodyBytes.length + "]{" + builder + "}");
        }
    }

    private byte[] doDecodeOfAES(byte[] bodyBytes) {
        String password = KeyManager.aesKey;
        return GZIPUtils.uncompress(AESUtils.decrypt(bodyBytes, password));
    }
}

4.发包和收包的对应关系确定

在协议体的传参部分,参数中加入自增长的sn,即sequence num,表示消息的序列号,服务器回包的时候也带上该sn,之后客户端可以通过sn来对应发包和回包

package com.**.nettylib.netty;

import android.content.Context;
import android.content.SharedPreferences;

import com.**.nettylib.netty.codec.MessageDecoder;
import com.**.nettylib.netty.util.SecurityUtil;
import com.**.nettylib.netty.util.ExtendInfo;
import com.**.nettylib.netty.util.KeyManager;
import com.**.nettylib.netty.util.MsgEncryptType;

import java.util.HashMap;
import java.util.Map;
import java.util.Random;

import io.netty.util.internal.StringUtil;

public class Request {
    private String tag = "netty";
    private String serviceName;
    private Map<String, String> reqInfo;
    private String cs;
    private int sn;
    private ExtendInfo ext;
    private ResponseListener callback;

    public Request(String serviceName, Map<String, String> reqInfo, ResponseListener callback, String tag) {
        this(serviceName, reqInfo, callback);
        if (tag != null && !(tag.trim().equals(""))) {
            this.tag = tag;
        }
    }

    public Request(String serviceName, Map<String, String> reqInfo, ResponseListener callback) {
        this.serviceName = serviceName;
        ext = new ExtendInfo();
        if (serviceName.equals("syncAesKey")) {
            this.reqInfo = new HashMap<>();
            String key = createKey(16);
            this.reqInfo.put("aesKey", key);
            KeyManager.aesKey = key;
            ext.setMsgEncryptType(MsgEncryptType.RSA);
        } else {
            this.reqInfo = reqInfo;
            //公共参数
            this.reqInfo.put("version", "1.01");
            this.reqInfo.put("userid", getUserIDFromSDCard());
            this.reqInfo.put("sessionid", getSessionIDFromSDCard());
            ext.setMsgEncryptType(MsgEncryptType.AES);
        }
        this.cs = getSign(serviceName, reqInfo);
        this.sn = NettyClient.getInstance().getSN();
        this.callback = callback;
    }

    public int getSN() {
        return this.sn;
    }

    public String getServiceName() {
        return this.serviceName;
    }

    public String getTag() {
        return this.tag;
    }

    public boolean isDefaultTag(){
        return this.tag.equals("netty");
    }

    public String getReqInfoMd5() {
        return md5(map2String(this.reqInfo));
    }

    public ExtendInfo getExt() {
        return this.ext;
    }

    public ResponseListener getCallback() {
        return this.callback;
    }

    private String getSign(String method, Map<String, String> reqInfo) {
        if (StringUtil.isNullOrEmpty(MessageDecoder.skey)) {
            return "noskey";
        } else {
            String params = map2String(reqInfo);
            String str = method + params + "0x4Plfd8b65O3Lkm" + MessageDecoder.skey;
            if (method.equals("iappay")) {
                str = method + params + "0x4Plfd8b65O3Lkm" + MessageDecoder.skey;
            }
            return md5(str);
        }
    }

    private String map2String(Map<String, String> reqInfo) {
        String params = "";
        for (Map.Entry<String, String> entry : reqInfo.entrySet()) {
            if (!StringUtil.isNullOrEmpty(params)) {
                params += "&";
            }
            params += entry.getKey() + "=" + entry.getValue();
        }
        return params;
    }

    private String md5(String str) {
        if (StringUtil.isNullOrEmpty(str)) {
            return "";
        }

        return SecurityUtil.md5(str);
    }

    //转成Map,用于组包
    public Map<String, String> toMap() {
        Map<String, String> map = new HashMap<String, String>();
        map.put("serviceName", this.serviceName);
        map.put("reqInfo", map2String(this.reqInfo));
        map.put("cs", this.cs);
        map.put("sn", String.valueOf(this.sn));

        return map;
    }

    private static String getUserIDFromSDCard() {
        String result = null;
        SharedPreferences preference = NettyClient.getInstance().getContext().getSharedPreferences("USERINFO", Context.MODE_PRIVATE);
        result = preference.getString("USERID", "");
        return result;
    }

    private static String getSessionIDFromSDCard() {
        String result = null;
        SharedPreferences preference = NettyClient.getInstance().getContext().getSharedPreferences("USERINFO", Context.MODE_PRIVATE);
        result = preference.getString("SESSIONID", "");
        return result;
    }


    /**
     * 随机生成指定位数秘钥
     *
     * @param KeyLength
     * @return
     */
    public String createKey(int KeyLength) {
        String base = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        Random random = new Random();
        StringBuffer Keysb = new StringBuffer();
        for (int i = 0; i < KeyLength; i++)    //生成指定位数的随机秘钥字符串
        {
            int number = random.nextInt(base.length());
            Keysb.append(base.charAt(number));
        }
        return Keysb.toString();
    }
}

5.心跳机制

通过IdleStateHandler设置写空闲时间,间隔一定时间(15s)给服务器发送心跳包来维持连接

package com.**.nettylib.netty;


import com.**.nettylib.constant.NetworkConfig;
import com.**.nettylib.netty.codec.MessageDecoder;
import com.**.nettylib.netty.codec.MessageEncoder;
import com.**.nettylib.netty.codec.MessageEncryptDecoder;
import com.**.nettylib.netty.codec.MessageEncryptEncoder;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.IdleStateHandler;

/**
 * Created by james on 2018/6/20.
 */

public class NettyClientInitializer extends ChannelInitializer<SocketChannel> {

    private NettyListener listener;

    public NettyClientInitializer(NettyListener listener) {
        if(listener == null){
            throw new IllegalArgumentException("listener == null ");
        }
        this.listener = listener;
    }

    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
//        SslContext sslCtx = SSLContext.getDefault()
//                .createSSLEngine(InsecureTrustManagerFactory.INSTANCE).build();

        ChannelPipeline pipeline = ch.pipeline();
//        pipeline.addLast(sslCtx.newHandler(ch.alloc()));    // 开启SSL
        pipeline.addLast(new LoggingHandler(LogLevel.INFO));    // 开启日志,可以设置日志等级
        pipeline.addLast("IdleStateHandler", new IdleStateHandler(NetworkConfig.READ_IDLE_TIME_SECOND, NetworkConfig.HEART_BEAT_TIME, 0));
        pipeline.addLast("StringDecoder", new MessageEncryptDecoder());//解码器 这里要与服务器保持一致
        pipeline.addLast("StringEncoder", new MessageEncryptEncoder());//编码器 这里要与服务器保持一致
        pipeline.addLast(new NettyClientHandler(listener));
    }
}
package com.**.nettylib.netty;

import android.util.Log;

import com.**.nettylib.netty.util.LogUtils;

import java.util.HashMap;
import java.util.Map;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;

/**
 * Created by james on 2018/6/20.
 */

public class NettyClientHandler extends SimpleChannelInboundHandler<Map> {
    private static final String TAG = NettyClientHandler.class.getName();
    private NettyListener listener;

    public NettyClientHandler(NettyListener listener) {
        this.listener = listener;
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        NettyClient.getInstance().setConnectStatus(true);
        listener.onServiceStatusConnectChanged(NettyListener.STATUS_CONNECT_SUCCESS);
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        NettyClient.getInstance().setConnectStatus(false);
        listener.onServiceStatusConnectChanged(NettyListener.STATUS_CONNECT_CLOSED);
        NettyClient.getInstance().reconnect();
    }


    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, Map map) throws Exception {
        LogUtils.logError(TAG, "thread == " + Thread.currentThread().getName());
        listener.onMessageResponse(map);
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            if (event.state() == IdleState.WRITER_IDLE){
                // 发送密钥成功后才开始心跳
                if(NettyClient.getInstance().isSendAesKeyFinish()){
                    try{
                        Request request = new Request("heartbeat", new HashMap<String, String>(), new ResponseListener() {
                            @Override
                            public void onSuccess(String data) {
                            }

                            @Override
                            public void onFail(int errcode) {
                            }

                        });
                        ctx.channel().writeAndFlush(request);
                    } catch (Exception e){
                    }
                }
            }else if(event.state() == IdleState.READER_IDLE){
                // 响应超时进行网络重连
                NettyClient.getInstance().setConnectStatus(false);
                NettyClient.getInstance().reconnect();
            }
        }
    }
}

6.网络重连机制

  • 服务器主动断开后重连
  • 监听网络断开恢复可用后进行重连
  • 服务器响应超时后重连
  • sendMsg时客户端连接状态为断开且手机网络连接正常时重连

7.超时重发/重复消息过滤机制

维护一个请求缓存列表,当发送消息的时候将消息暂存在消息列表中,如果消息发送成功则移除,如果失败则待网络重连成功后再发送

package com.**.nettylib.netty;

import com.**.nettylib.netty.util.LogUtils;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;


/**
 * 管理请求
 */
public class RequestManager {
    private static RequestManager sRequestManager = new RequestManager();
    private CopyOnWriteArrayList<Request> mCachedRequestList = new CopyOnWriteArrayList<>();
    private List<String> mNoCacheServiceList = new ArrayList<>();

    private RequestManager() {
        mNoCacheServiceList.add("syncAesKey");
        mNoCacheServiceList.add("sessionlogin");
    }

    public static RequestManager getInstance() {
        return sRequestManager;
    }

    /**
     * 缓存需要重发的请求
     *
     * @param request
     */
    public void add(Request request) {
        if (request != null) {
            if (isCacheRequest(request)) {
                // 判断sn是否已经存在,过滤重复请求
                for (Request r : mCachedRequestList) {
                    if (r.getSN() == request.getSN()) {
                        return;
                    }
                }
                mCachedRequestList.add(request);
            }
        }
    }

    /**
     * 是否重发
     *
     * @param request
     * @return
     */
    private boolean isCacheRequest(Request request) {
        String serviceName = request.getServiceName();
        return serviceName.startsWith("query") && !mNoCacheServiceList.contains(serviceName);
    }

    public void removeBySN(int sn) {
        Iterator<Request> it = mCachedRequestList.iterator();
        while (it.hasNext()) {
            Request request = it.next();
            if (request.getSN() == sn) {
                LogUtils.logError("NettyClient", "移除缓存请求:" + request.getServiceName());
                mCachedRequestList.remove(request);
                break;
            }
        }
    }


    /**
     * 通过tag移除请求
     */
    public void removeByTag(String tag) {
        Iterator<Request> it = mCachedRequestList.iterator();
        while (it.hasNext()) {
            Request request = it.next();
            if (request.getTag().equals(tag)) {
                LogUtils.logError("NettyClient", "通过Tag:" + tag + "移除缓存请求:" + request.getServiceName());
                mCachedRequestList.remove(request);
            }
        }
    }


    /**
     * 请求重发
     */
    public void resend() {
        for (Request request : mCachedRequestList) {
            LogUtils.logError("NettyClient", "重发缓存请求:" + request.getServiceName());
            NettyClient.getInstance().sendMessage(request);
        }
    }

    /**
     * 是否重复的查询请求
     */
    public boolean isRepeatRequest(Request request) {
        if (request.getServiceName().startsWith("query") && !request.isDefaultTag()) {
            for (Request req : mCachedRequestList) {
                if (req.getTag().equals(request.getTag())
                        && req.getServiceName().equals(request.getServiceName())
                        && req.getReqInfoMd5().equals(request.getReqInfoMd5())) {
                    LogUtils.logError("NettyClient", "重复的查询请求:tag = " + request.getTag() + ",serviceName = " + request.getServiceName());
                    return true;
                }
            }
        }
        return false;
    }
}


评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值