移动终端网络接入

1.1 终端接入网络

1.1.1 移动终端接入网络有如下的几种情况

  • 终端设备在属地,终端设备通过基站,接入属地的网络
  • 终端设备在国内漫游时,移动设备会漫游连接到当地的网络,联通和电信则需要漫游回属地的网络
  • 终端设备在国外漫游时,需要漫游回属地的网络

1.1.2 基站接入分析

移动终端通过基站接入移动运营商网络,终端与基站之间是数据链路层,不涉及网络层(IP)和传输层(TCP),终端设备的IP地址是由运营商分配的,在切换基站时一般不会引起IP地址的变化

有以下几种情况:

  • 当设备进行重启、飞行模式切换等时,设备会重新发起接入,这时IP地址会发生改变
  • 设备在同一区域内切换基站的过程中,如果没有发生断网情况下,即没有重新接入,IP地址是不会变化的
  • 设备在区域间切换基站,比如联通设备从北京到河北,接入由北京联通变成河北联通,IP地址会发生变化

终端设备切换基站一般情况下可在50ms~200ms完成,TCP是基于连接的协议,连接状态由状态机来维护,连接完毕后,双方都会处于established状态,它们之间的连接由各自的IP和TCP的端口唯一标识,即使这个连接没有任何数据,但仍是保持连接状态。TCP的KeepAlive机制用于检测连接死活,一般时间为 7200 s,失败后重试 10 次,每次超时时间 75 s,以释放无效链接。这个时间比切换基站时间要大的多,因此TCP通道在切换基站时,其IP地址一般没有变化,所以基于IP和端口的已建立的TCP连接不会失效。

1.1.3 DNS解析

当前移动 DNS 的现状:

  • 运营商 LocalDNS 出口根据权威 DNS 目标 IP 地址进行 NAT,或将解析请求转发到其他DNS 服务器,导致权威 DNS 无法正确识别运营商的 LocalDNS IP,引发域名解析错误、流量跨网。
  • 域名被劫持的后果:网站无法访问(无法连接服务器)、访问到钓鱼网站等。
  • 解析结果跨域、跨省、跨运营商、国家的后果:网站访问缓慢甚至无法访问。

为了解决这些问题,通常TCP网关的地址可以通过HttpDNS技术获取,以避免DNS解析异常、域名劫持的问题。客户端直接访问HTTPDNS接口,获取服务最优IP,返回给客户端,客户拿到IP地址后,直接使用此IP地址进行连接。

1.2 接入层

接入层最靠近客户端,接入层一般使用LVS(DR模式)+VIP+HAProxy来实现,如果使用公有云也可以使用云服务提供的负载均衡服务,如使用腾讯云的CLB,阿里云的ALB,配置按TCP转发;有矿的话可以使用F5硬件来做接入层;保留这一层有如下好处:

  • 负载均衡:均衡客户端连接,尽量保证连接在连接服务器上均衡
  • 真实服务不需要公网IP,因为它不需要对外暴露IP地址,更安全
  • 会话保持

1.3 长连接服务器

长连接服务部署的机器关注以下几个配置项

nf_conntrack_max
nf_conntrack_max 决定连接跟踪表的大小,当nf_conntrack模块被装置且服务器上连接超过这个设定的值时,系统会主动丢掉新连接包,直到连接小于此设置值才会恢复。
Backlognet.core.somaxconn排队等待接受的最大连接数

net.core.netdev_max_backlog数据包在发送给cpu之ueej被网卡缓冲的速率,增加可以提高有高带宽机器的性能
文件描述符sys.fs.file-max允许的最大文件描述符 /proc/sys/fs/file-max

nofile应用层面允许的最大文件描述数 /etc/security/limits.conf
portsnet.ipv4.ip_local_port_range端口范围

net.ipv4.tcp_tw_reuse端口复用,允许time wait的socket重新用于新的连接,默认为0,关闭短连接设置为1

net.ipv4.tcp_tw_recycletcp连接中的time wait的sockets快速回收,默认为0,表示关闭

1.4 服务实现

1.4.1 认证:

验证终端身份,确保只有合法的终端才能够使用服务,流程如下

  1. 服务端生成设备私钥、公钥;私钥执久化到设备上,公钥保存在服务端
  2. 握手:客户端发起TCP连接,TCP连接建立成功后,服务端生成256字符随机字串 randomMsg,返回客户端
  3. 客户端登录:客户端拿出token+randomMsg,使用其私钥签名得到 secretChap,并把token、secretChap 通过TCP通道上报服务端
  4. 服务端验证:服务端使用设备公钥验证签名,并调用 Passport 服务验证 token,拿到用户信息;服务端配置用户、设备的路由信息
  5. 协商对称密钥:服务端验证后,生成并返回对称密钥 secureKey,返回的对称密钥 secureKey 使用终端的公钥加密,只有使用设备的私钥才可以解密;客户端解密对称密钥 secureKey,至此服务端和终端完成密钥协商,之后可以愉快并安全的通信了。

模拟代码:

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.RandomStringUtils;

import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Map;

public class AuthClientTest {
    private static final byte[] PARAM_IV;

    static {
        String PARAM_IV_CONFIG = Base64.encodeBase64String(new SecureRandom().generateSeed(32));
        PARAM_IV = Base64.decodeBase64(PARAM_IV_CONFIG);
    }

    private static class ClientStore {
        private static final byte[] PARAM_IV = AuthClientTest.PARAM_IV;
        String privateKey;
        String token;// 用户标识信息

        String randomMsg;
        // 对称密钥
        byte[] secretKey;
    }

    private static class ServerStore {
        private static final String SECRET_KEY = RandomStringUtils.random(32);
        private static final byte[] PARAM_IV = AuthClientTest.PARAM_IV;

        // 对称密钥
        byte[] secretKey = new SecureRandom().generateSeed(32);

        String publicKey;
        String randomMsg;
    }

    public static void main(String[] args) throws Exception {
        // s0 初始化,生成设备的公私钥
        SHA256SignUtil.RsaKeys rsaKeys = SHA256SignUtil.generateKeyBytes();
        ClientStore clientStore = new ClientStore();
        ServerStore serverStore = new ServerStore();
        // 发送私钥到客户端
        clientStore.privateKey = Base64.encodeBase64String(rsaKeys.getPrivateKey());
        // 公钥保存在服务器端
        serverStore.publicKey = Base64.encodeBase64String(rsaKeys.getPublicKey());
        // s1: 握手
        // s1.1 客户端连接服务端 app -> server
        serverStore.randomMsg = RandomStringUtils.randomAlphanumeric(256);
        // s1.2 TCP建立成功后 server -(randomMsg)-> app
        clientStore.randomMsg = serverStore.randomMsg;
        // s2 端侧签名登录 app -(token,secretChap:privateKey签名)-> server
        clientStore.token = genToken(1000L);
        Map<String, Object> data = new HashMap<>();
        data.put("token", clientStore.token);
        PrivateKey privateKey = SHA256SignUtil.restorePrivateKey(Base64.decodeBase64(clientStore.privateKey));
        byte[] secretChap = SecretChapUtils.createSecretChap(data, clientStore.randomMsg, privateKey);
        // s3 服务端验证登录
        PublicKey publicKey = SHA256SignUtil.restorePublicKey(Base64.decodeBase64(serverStore.publicKey));
        boolean verify = SecretChapUtils.verifySecretChap(data, serverStore.randomMsg, secretChap, publicKey);
        System.out.println(verify);
        // s4 服务端下发对称密钥 server -(secretKey:publicKey加密)-> app
        byte[] secureKey = SHA256SignUtil.encryptByPublicKey(serverStore.secretKey, publicKey.getEncoded());
        // s5 端侧解密对称密钥并存储
        clientStore.secretKey = SHA256SignUtil.decryptByPrivateKey(secureKey, privateKey.getEncoded());
        // s6 正常加密传输数据
        byte[] encrypt = AesUtil.encrypt("田加国是好人".getBytes(StandardCharsets.UTF_8), clientStore.secretKey, ClientStore.PARAM_IV);
        byte[] decrypt = AesUtil.decrypt(encrypt, serverStore.secretKey, ServerStore.PARAM_IV);
        String sourceData = new String(decrypt, StandardCharsets.UTF_8);
        System.out.println(sourceData);
    }

    private static String genToken(long uid) {
        HashMap<String, Object> claims = new HashMap<>();
        claims.put("iss", "user.tianjiaguo.com");
        claims.put("expire", System.currentTimeMillis() + 24 * 60 * 60 * 1000L * 30);
        claims.put("uid", uid);
        claims.put("type", 2);
        return Jwts.builder().setClaims(claims)
                .signWith(SignatureAlgorithm.HS256, ServerStore.SECRET_KEY.getBytes())
                .compact();
    }
}

1.4.2 连接保持

心跳机制+自适应心跳:

  1. 端侧定时发送心跳包,服务端重置心跳检查点
  2. 端侧会根据业务数据,决定是否、何时上报心跳包
  3. 心跳包频率可控

附录:

示例架构图

未完

This entry was posted in 未分类. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.