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模块被装置且服务器上连接超过这个设定的值时,系统会主动丢掉新连接包,直到连接小于此设置值才会恢复。 | ||
Backlog | net.core.somaxconn | 排队等待接受的最大连接数 | |
net.core.netdev_max_backlog | 数据包在发送给cpu之ueej被网卡缓冲的速率,增加可以提高有高带宽机器的性能 | ||
文件描述符 | sys.fs.file-max | 允许的最大文件描述符 /proc/sys/fs/file-max | |
nofile | 应用层面允许的最大文件描述数 /etc/security/limits.conf | ||
ports | net.ipv4.ip_local_port_range | 端口范围 | |
net.ipv4.tcp_tw_reuse | 端口复用,允许time wait的socket重新用于新的连接,默认为0,关闭 | 短连接设置为1 | |
net.ipv4.tcp_tw_recycle | tcp连接中的time wait的sockets快速回收,默认为0,表示关闭 |
1.4 服务实现
1.4.1 认证:
验证终端身份,确保只有合法的终端才能够使用服务,流程如下
- 服务端生成设备私钥、公钥;私钥执久化到设备上,公钥保存在服务端
- 握手:客户端发起TCP连接,TCP连接建立成功后,服务端生成256字符随机字串 randomMsg,返回客户端
- 客户端登录:客户端拿出token+randomMsg,使用其私钥签名得到 secretChap,并把token、secretChap 通过TCP通道上报服务端
- 服务端验证:服务端使用设备公钥验证签名,并调用 Passport 服务验证 token,拿到用户信息;服务端配置用户、设备的路由信息
- 协商对称密钥:服务端验证后,生成并返回对称密钥 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 连接保持
心跳机制+自适应心跳:
- 端侧定时发送心跳包,服务端重置心跳检查点
- 端侧会根据业务数据,决定是否、何时上报心跳包
- 心跳包频率可控
附录:
示例架构图
未完