android 长连接库

概述

一直都想写些什么,刚好这两年一直都在做聊天app,想了很久决定把底层网络库开源出来。两年来这个库经历了好几个阶段:

  • java实现,BIO连接
  • java实现,NIO连接
  • c/c++实现,NIO连接

本次要开源出来的是c/c++实现的版本,取名fastsocks,github地址:

  • https://github.com/zaki-kous/Fastsocks-simple

fastsock参考了Telegram的部分实现,如果对大家对Telegram还不熟悉,可以去它的官网 逛逛。

长连接必然要涉及到序列化/反序列化、数据包收发、重连、动态ip/port切换、推送等内容,接下来都将逐一介绍。

开发环境

新版本的android studio已经支持ndk开发,并且加入了cmake 的支持,如果大家对cmake还不熟悉,请查看官网。下面是我的开发环境:

android studio版本 gradle版本 ndk版本
2.3.1 2.2.3 14.1

fastscoks使用cmake编译c/c++代码,开发之前先安装好cmake插件,当然了为了方便调试c/c++代码,最好是把LLDB也安装好。

使用

和大多数sdk一样,fastsocks启动起来需要一些配置,可以参考fastsocks-simple的相关代码,所有对fastsocks的操作都在一个java类里面:

com.me.fastsocks.tcp.ConnectionsManager

初始化

主要是设置应用层和底层库的监听PacketDispatcherDelegate,并且调用注册相关jni函数。有时候我们会遇到c/c++的crash,底层库如果崩溃会将崩溃文件保存至crashPath。

void init(PacketDispatcherDelegate dispatcherDelegate, 
                 Context context,String crashPath);

breakpad

安卓c/c++崩溃定位比较困难,借助breakpad相关工具定位崩溃会简单很多。breakpad本身就跨平台并且支持android移植,项目里已经有编译好的.a库,fastsocks使用关联静态库的方式使用breakpad
定位崩溃需要的工具也需要自己手动编译,github地址:

  • https://chromium.googlesource.com/breakpad/breakpad/

这里需要注意:必须在Linux环境下才能编译和使用,如果大家没有Linux环境可以装个VMware,添加个ubuntu虚拟机,至于怎么定位崩溃这里就不赘述了,网上很多这样的教程。

启动和销毁

一般都在登录成功之后启动服务,注销登录的时候销毁服务,参考:

com.me.fastsocks.utils.StartUtils

启动服务

启动服务的时候需要将userId设置到底层库,这里我们使用long类型标示一个用户,当然了如果大家的userId不是long类型的,可以修改相应的实现。

void startSdkService(Context context);

销毁服务

void stopSdkService(Context context);

添加长连接ip

一般长连接都是一组ip和端口,并且通过服务器下发ip列表,大家可以从服务器获取到列表之后顺序添加服务器地址:

void addSvrAddr(String address, short port);

应用启动时会随机从数组中选取一个ip先连上,Datacenter.cpp对应:

static const char *adds[] = {
        "192.168.1.100",
        "192.168.1.110",
        "192.168.1.112",
        "192.168.1.113",
        "192.168.1.114"
};

设置长连接ip

有时候我们需要固定连接一个ip地址,设置完长连接ip之后,底层库会只连接这个ip地址。比如说我们在测试的时候一般都会连测试环境,设置长连接ip:

void setSvrAddr(String address, short port);

自定义协议

协议一般都会包含包头和包体两个部分,包头一般不加密,包体都会选择加密。底层库默认有一个包体结构:

#pragma pack(push, 1)
//一字节对齐,方便拷贝
typedef struct PacketHeader{
    uint16_t pkgLen; // 包大小(unsigned short)

    uint8_t headLen; // 头部大小

    uint8_t version; // 协议版本,当前为1

    int32_t cmd; // 协议命令字

    int64_t uin; // 用户账户uin(唯一的一个标识)

    int32_t seq; // 包的seq
} PacketHeader;
#pragma pack(pop)

如果要自定义自己的协议,需要修改要修改两处:

  • PacketHeader改为相应的协议包头
  • NetworkMessage.cppserializeToBuffer方法写入包头部分改为相应包头

心跳和握手命令码

底层默认的心跳和握手命令码对应Datacenter.cpp

static const int HANDSHAKE_CMD = 257;//握手命令码
static const int PING_CMD = 259;//心跳命令码

心跳和握手都是底层自动发送的,其中心跳没有包体。

握手

socket连接成功之后一般都需要握手,握手成功之后才能发送和接收数据包,底层连接成功之后会回调onHandshakeConnected方法,需要在这个方法里面返回握手数据和检验握手结果,实例:

int onHandshakeConnected(int buffer){
        if(buffer == 0){
            // 返回握手信息
            return requestHandShake();
        } else {
            // 校验握手信息
            byte[] data = NativeByteBuffer.with(buffer);
            //TODO 判断data数据包里面的握手返回是否成功
            // 如果返回0表示握手成功,否则握手失败
            // 握手失败会重连
            return -1;
        }
    }

发送

发送请求始于命令码,用命令码来标示客户端数据包,实例:

ConnectionsManager.getInstance().with(1280)
.buffer(new byte[128])//要发送的数据放到这里
.timeout(8000)//自定义超时时间
.flags(RequestFlag.FlagReSend)//请求类型
.loadSendListener(newOnSendRequestListener() {//请求回调
    @Override
    public void onSendComplete(byte[] cmpleteBuffer,   
      ConnectionsManager.ERROR error) {
         //如果error为null,表示请求成功,否则失败

    }
    @Override
    public void onSendCancel() {
        //请求被取消
    }
})
.start();

RequestFlag用来标示请求类型,比如:请求是否有回包(默认所有数据包都有回包)、是否需要重发(默认不重发)、是否可以在握手成功之前发送(默认都是只能在握手成功之后发送)。

public enum RequestFlag{
        UNKNOWN(0),          //未知
        FlagWithoutAck(1),   //没有回包
        FlagReSend(2),       //数据包可以重发
        FlagWithoutLogin(4); //数据包可以在握手之前发送
}

数据分发

如何判断是发送的回包和收到新数据包至关重要,试想一下如果发送回包和收到新数据包错乱了后果会有多严重。这里介绍底层库的分发策略,参考ConnectionsManager.cpp对应:

void onConnectionRecviedData() {
    Request *request = getRequestWithSeq(seq);
    if(request != nullptr && request->cmdId == cmd - 1) {
        //回调发送完成
        sendRequest->onComplete(buffer, 0, "");
    } else {
        //回调收到新消息
        delegate->onRecvMessages(cmd, buffer);
    }
}

客户端将seq放到包头发送给服务器,服务器返回相同的seq。客户端收到数据包时匹配本地seq和命令码,匹配成功回调发送完成,否则回调收到新消息。

接收数据包

初始化的时候需要设置PacketDispatcherDelegate,收到新消息、长连接状态发生改变会回调相应的方法。
收到消息时,底层库会把命令码和包体数据抛到应用层,对应:

// 新消息
void msgRecv(int cmd, byte[] datas);

连接状态改变

底层连接状态如果发生改变时,会回调相应的连接状态到应用层,此时一般都会去更新UI,对应:

//连接状态改变
void onConnectionStateChanged(int status);

连接状态定义:

//连接状态改变
enum ConnectionState{
     UNKNOWN(-1),//未知
     ConnectionStateConnecting(1),//连接中
     ConnectionStateWaitingForNetwork(2),//没有网络
     ConnectionStateConnected(3);//连接成功
}

策略

由于代码已经开源出来了,策略的具体实现大家可以参考相应代码。这里只是简单介绍下几个模块。

心跳

做过长连接的人都知道,心跳的重要性。而心跳间隔也会影响发现长连接出问题的灵敏度,fastsocks的心跳策略是分为前台和后台两种,前台:20s一次,后台:1min一次。并且有回包的数据包会代替心跳,注意:有回包的数据包

ip和端口选择

ip的选择首先要符合随机性,这样才能降低服务器的荷载,如果所有客户端都连接同一台服务器,服务器很容易宕机。其次是可用性,收到过数据包的ip要比没有收到过数据包的ip连接次数更多,TcpConnection.cpp对应:

void onDisconnected(int reason) {
    failedConnectionCount++;
    if(failedConnectionCount == 1){
       if(currentConnectionRecvData){
            willRetryConnectCount = 5;
       } else {
            willRetryConnectCount = 1;
       }
    }
}

经验表明非http连接的80端口经常被封,如果连接超时或者握手超时
首先切换端口,当端口切换了10次之后继续切换ip。
Datacenter.cpp对应:

void addAddressAndPort(std::string address, uint16_t port) {
    std::random_shuffle();//随机打乱svr顺序
}
//切换ip或者端口
void Datacenter::switchAddressOrPort() {
    if (strlen(nioServer.nioIp)) {
        return;
    }
    //先增加端口的索引,再增加地址的索引
    if(currentPortIndex < 10){
        currentPortIndex++;
    } else {
        if(currentAddressIndex < addresses.size() - 1){
            currentAddressIndex++;
        } else {
            currentAddressIndex = 0;
        }
        currentPortIndex = 0;
    }
}

重连

除非应用层主动调用断开连接,否则底层必须重连,来保证长连接的可用性,默认重连间隔为1s,TcpConnection.cpp对应:

reconnectTimer = new Timer([&]{
        DEBUG_D("reconnectTimer execute , begin connect.");
        reconnectTimer->stop();
        connect();
    });

超时

超时分为连接超时和读写超时,也是动态改变的。

连接超时

当切换ip和端口时连接的超时为8s,否则为15秒,TcpConnection.cpp对应:

void connect() {
    if(isTryingNextPort){
        //切换端口连接超时为8秒
        setTimeout(8);
    } else {
        //切换ip连接超时为15秒
        setTimeout(15);
    }
}

读写超时

当应用处于前台时,读写超时设置为30s;应用处于后台时,读写超时设置为70s。

总结

水平有限,难免做到尽善尽美,欢迎大家指正和学习!

你可能感兴趣的:(android)