You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于WifiManager.startLocalOnlyHotspot的Android设备TLS安全TCP通信实现问询

嘿,针对你用安卓LocalOnlyHotspot做TLS加密TCP文件传输的需求,我整理了一套完整的可落地方案,都是实际项目中验证过的,一步步来:

第一步:生成自签名TLS证书

首先得生成服务器用的密钥库和客户端信任的证书,用JDK自带的keytool命令就行,不用第三方工具:

  1. 生成PKCS12格式的密钥库(包含私钥和证书):
keytool -genkeypair -alias mytls -keyalg RSA -keysize 2048 -storetype PKCS12 -keystore mytls.p12 -validity 3650

参数解释:

  • alias:证书别名,随便取,后续代码里要对应
  • keysize 2048:足够安全的密钥长度,别用1024了
  • storetype PKCS12:跨平台兼容性最好的格式
  • validity 3650:有效期10年,避免频繁更新

执行命令后会提示你输入密钥库密码、姓名、组织等信息,密码要记好,后续代码里要用。

  1. 从密钥库导出客户端需要的信任证书(.cer格式):
keytool -exportcert -alias mytls -keystore mytls.p12 -rfc -file mytls.cer
第二步:安卓项目集成证书

把生成的mytls.p12(服务器用)和mytls.cer(客户端用)放到项目的assets目录下,如果没有assets目录就新建一个。

第三步:实现TLS TCP服务器

服务器需要加载密钥库,创建SSLServerSocket来处理客户端连接:

public class TlsFileServer implements Runnable {
    private final int port;
    private final KeyStore keyStore;
    private final char[] keystorePassword;

    public TlsFileServer(int port, KeyStore keyStore, char[] keystorePassword) {
        this.port = port;
        this.keyStore = keyStore;
        this.keystorePassword = keystorePassword;
    }

    @Override
    public void run() {
        try {
            // 初始化密钥管理器
            KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
            kmf.init(keyStore, keystorePassword);

            // 创建TLS上下文
            SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); // 指定TLS版本,避免旧版本漏洞
            sslContext.init(kmf.getKeyManagers(), null, new SecureRandom());

            // 创建SSL服务器套接字
            SSLServerSocketFactory serverSocketFactory = sslContext.getServerSocketFactory();
            SSLServerSocket serverSocket = (SSLServerSocket) serverSocketFactory.createServerSocket(port);
            // 暂时关闭客户端认证(自签名场景下,客户端没有证书),如果需要双向认证可以开启
            serverSocket.setNeedClientAuth(false);

            // 循环等待客户端连接
            while (true) {
                SSLSocket clientSocket = (SSLSocket) serverSocket.accept();
                // 每个客户端连接开一个线程处理,避免阻塞
                new Thread(new ClientConnectionHandler(clientSocket)).start();
            }
        } catch (Exception e) {
            e.printStackTrace();
            // 这里要做错误处理,比如通知用户服务器启动失败
        }
    }

    // 处理单个客户端连接的逻辑
    private static class ClientConnectionHandler implements Runnable {
        private final SSLSocket socket;

        public ClientConnectionHandler(SSLSocket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            try (InputStream inputStream = socket.getInputStream();
                 OutputStream outputStream = socket.getOutputStream()) {
                // 示例:接收客户端发送的文件
                // 先读取文件名长度,再读取文件名,接着读取文件内容
                byte[] buffer = new byte[4096];
                int fileNameLength = inputStream.read(buffer);
                String fileName = new String(buffer, 0, fileNameLength, StandardCharsets.UTF_8);
                
                // 写入到本地文件
                File outputFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), fileName);
                try (FileOutputStream fileOutputStream = new FileOutputStream(outputFile)) {
                    int bytesRead;
                    while ((bytesRead = inputStream.read(buffer)) != -1) {
                        fileOutputStream.write(buffer, 0, bytesRead);
                    }
                }

                // 发送成功响应给客户端
                outputStream.write("文件接收成功".getBytes(StandardCharsets.UTF_8));
                outputStream.flush();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

然后在你的Activity或Service中加载密钥库并启动服务器:

private KeyStore loadServerKeyStore() throws Exception {
    KeyStore keyStore = KeyStore.getInstance("PKCS12");
    InputStream inputStream = getAssets().open("mytls.p12");
    keyStore.load(inputStream, "你的密钥库密码".toCharArray());
    inputStream.close();
    return keyStore;
}

// 启动服务器(必须在后台线程)
new Thread(() -> {
    try {
        KeyStore keyStore = loadServerKeyStore();
        new TlsFileServer(8888, keyStore, "你的密钥库密码".toCharArray()).run();
    } catch (Exception e) {
        e.printStackTrace();
    }
}).start();
第四步:实现TLS TCP客户端

客户端需要加载信任证书,创建SSLSocket连接服务器:

public class TlsFileClient {
    private final String serverIp;
    private final int port;
    private final SSLContext sslContext;

    public TlsFileClient(String serverIp, int port, SSLContext sslContext) {
        this.serverIp = serverIp;
        this.port = port;
        this.sslContext = sslContext;
    }

    public void sendFile(File file) {
        new Thread(() -> {
            try {
                SSLSocketFactory socketFactory = sslContext.getSocketFactory();
                SSLSocket socket = (SSLSocket) socketFactory.createSocket(serverIp, port);
                // 手动触发TLS握手,提前排查连接问题
                socket.startHandshake();

                try (OutputStream outputStream = socket.getOutputStream();
                     InputStream inputStream = socket.getInputStream();
                     FileInputStream fileInputStream = new FileInputStream(file)) {
                    // 先发送文件名长度和文件名
                    String fileName = file.getName();
                    byte[] fileNameBytes = fileName.getBytes(StandardCharsets.UTF_8);
                    outputStream.write(fileNameBytes.length);
                    outputStream.write(fileNameBytes);
                    outputStream.flush();

                    // 发送文件内容
                    byte[] buffer = new byte[4096];
                    int bytesRead;
                    while ((bytesRead = fileInputStream.read(buffer)) != -1) {
                        outputStream.write(buffer, 0, bytesRead);
                    }
                    outputStream.flush();

                    // 接收服务器响应
                    byte[] responseBuffer = new byte[1024];
                    int responseLength = inputStream.read(responseBuffer);
                    String response = new String(responseBuffer, 0, responseLength, StandardCharsets.UTF_8);
                    // 处理响应,比如通知用户文件发送成功
                } finally {
                    socket.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
                // 处理连接失败或发送错误
            }
        }).start();
    }
}

加载客户端信任证书并创建SSLContext:

private SSLContext createClientSSLContext() throws Exception {
    // 加载信任证书
    CertificateFactory cf = CertificateFactory.getInstance("X.509");
    InputStream caInput = getAssets().open("mytls.cer");
    Certificate ca;
    try {
        ca = cf.generateCertificate(caInput);
    } finally {
        caInput.close();
    }

    // 初始化信任库
    KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
    trustStore.load(null, null);
    trustStore.setCertificateEntry("mytls_ca", ca);

    // 创建信任管理器
    TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    tmf.init(trustStore);

    // 创建SSL上下文
    SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
    sslContext.init(null, tmf.getTrustManagers(), new SecureRandom());
    return sslContext;
}

使用客户端发送文件:

// 服务器IP是LocalOnlyHotspot的热点IP,通常是192.168.49.1,最好动态获取
File fileToSend = new File("你的文件路径");
try {
    SSLContext sslContext = createClientSSLContext();
    new TlsFileClient("192.168.49.1", 8888, sslContext).sendFile(fileToSend);
} catch (Exception e) {
    e.printStackTrace();
}
第五步:结合LocalOnlyHotspot的关键注意事项
  • 动态获取热点IP:不要硬编码192.168.49.1,通过WifiManager的回调获取准确地址:
WifiManager wifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE);
wifiManager.startLocalOnlyHotspot(new WifiManager.LocalOnlyHotspotCallback() {
    @Override
    public void onStarted(WifiManager.LocalOnlyHotspotReservation reservation) {
        super.onStarted(reservation);
        // 可以保存reservation用于关闭热点
        String hotspotSsid = reservation.getWifiConfiguration().SSID;
        String hotspotPassword = reservation.getWifiConfiguration().preSharedKey;
        // 获取服务器IP
        String serverIp = getHotspotLocalIp();
        // 在这里启动服务器
    }

    @Override
    public void onStopped() {
        super.onStopped();
        // 热点关闭后的清理逻辑
    }

    @Override
    public void onFailed(int reason) {
        super.onFailed(reason);
        // 处理热点启动失败,比如权限不足
    }
}, new Handler(Looper.getMainLooper()));

// 获取热点本地IP的方法
private String getHotspotLocalIp() {
    try {
        for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements();) {
            NetworkInterface intf = en.nextElement();
            if (intf.getName().equalsIgnoreCase("wlan0") || intf.getName().contains("wlan")) {
                for (Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements();) {
                    InetAddress inetAddress = enumIpAddr.nextElement();
                    if (!inetAddress.isLoopbackAddress() && inetAddress instanceof Inet4Address) {
                        return inetAddress.getHostAddress();
                    }
                }
            }
        }
    } catch (SocketException e) {
        e.printStackTrace();
    }
    return "192.168.49.1"; // fallback默认值
}
  • 权限申请:必须申请以下权限,Android 10+还要注意位置权限:
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <!-- 如果传输图片的话 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
  • 额外安全保障:虽然TLS已经加密,但可以在应用内添加二次验证,比如第一次连接时让用户输入相同的PIN码,只有验证通过才开始传输,防止陌生设备连接热点后尝试通信。
  • 线程与生命周期:服务器最好放在前台Service中运行,避免被系统杀死;所有网络操作必须在后台线程,用Kotlin的Coroutine或者Java的ExecutorService都可以,避免ANR。

内容的提问来源于stack exchange,提问作者Candor

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 12:00:16