Android 简单的SFTP服务端+客户端通信传文件

#这是一个简单的使用第三方jar通信例子#

1.使用sshd https://mvnrepository.com/artifact/org.apache.sshd

2.下载或导入对应的jar

3.服务端代码

A设备 启动服务端代码

Intent intent = new Intent(this, SFTPServerService.class);

intent.putExtra("port", 2222);

intent.putExtra("userName", "root");

intent.putExtra("passWord", "123456");

startService(intent);

java 复制代码
import android.app.Service;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import android.util.Log;
import org.apache.sshd.common.util.io.PathUtils;
import org.apache.sshd.server.SshServer;
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
import org.apache.sshd.sftp.server.SftpSubsystemFactory;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Set;

public class SFTPServerService extends Service {
    private static final String TAG = "SFTPServerService";
    private SshServer sshd;

    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "创建SFTP服务器服务");
        setupUserHomeFolder();
    }

    private void setupUserHomeFolder() {
        // 设置用户主目录解析器
        PathUtils.setUserHomeFolderResolver(() -> {
            File homeDir = getExternalFilesDir(null);
            if (homeDir == null) {
                homeDir = getFilesDir();
            }
            Log.d(TAG, "User home folder set to: " + homeDir.getAbsolutePath());
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                return homeDir.toPath();
            }
            return null;
        });
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        int port = intent.getIntExtra("port", 2222);
        String userName = intent.getStringExtra("userName");
        String passWord = intent.getStringExtra("passWord");
        startSFTPSServer(port, userName, passWord);
        return START_STICKY;
    }

    private void startSFTPSServer(int port,String userName, String passWord) {
        new Thread(() -> {
            try {
                sshd = SshServer.setUpDefaultServer();
                sshd.setPort(port);

                // 设置主机密钥
                File hostKeyFile = new File(getFilesDir(), "hostkey.ser");
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    sshd.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(hostKeyFile.toPath()));
                }

                // 设置密码验证
                sshd.setPasswordAuthenticator((username, password, session) ->
                        userName.equals(username) &&
                                passWord.equals(password));

                // 设置SFTP子系统
                sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));

                // 设置文件系统工厂 - 使用Android的外部存储
                sshd.setFileSystemFactory(new FileSystemFactory(this));

                sshd.start();

                Log.d(TAG, "SSH服务启动成功");
            } catch (Exception e) {
                Log.e(TAG, "SSH服务启动失败", e);
            }
        }).start();
    }

    /**
     * 设置目录权限
     */
    private void setDirectoryPermissions(File directory) {
        try {
            Path path = null;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                path = directory.toPath();

                // 尝试设置POSIX权限(在支持的系统上)
                try {
                    Set<PosixFilePermission> perms = EnumSet.of(
                            PosixFilePermission.OWNER_READ,
                            PosixFilePermission.OWNER_WRITE,
                            PosixFilePermission.OWNER_EXECUTE,
                            PosixFilePermission.GROUP_READ,
                            PosixFilePermission.GROUP_WRITE,
                            PosixFilePermission.GROUP_EXECUTE,
                            PosixFilePermission.OTHERS_READ,
                            PosixFilePermission.OTHERS_WRITE,
                            PosixFilePermission.OTHERS_EXECUTE
                    );
                    Files.setPosixFilePermissions(path, perms);
                    System.out.println("设置POSIX权限: " + directory.getAbsolutePath());
                } catch (UnsupportedOperationException e) {
                    // Android可能不支持POSIX权限,使用Java权限
                    System.out.println("系统不支持POSIX权限,使用Java权限系统");
                }
            }

            // 设置可读可写
            directory.setReadable(true, false);
            directory.setWritable(true, false);
            directory.setExecutable(true, false);

            System.out.println("设置目录权限: " + directory.getAbsolutePath() +
                    " [可读:" + directory.canRead() +
                    ", 可写:" + directory.canWrite() +
                    ", 可执行:" + directory.canExecute() + "]");

        } catch (Exception e) {
            System.err.println("设置目录权限时出错: " + e.getMessage());
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        stopSFTPSServer();
    }

    private void stopSFTPSServer() {
        if (sshd != null) {
            try {
                sshd.stop();
                Log.i(TAG, "SFTP Server stopped");
            } catch (IOException e) {
                Log.e(TAG, "Error stopping SFTP server", e);
            }
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}

4.客户端

B设备 打开客户端界面开始连接服务

java 复制代码
import android.content.Context;
import org.apache.sshd.common.session.SessionContext;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.Path;
import java.nio.file.Paths;

public class FileSystemFactory implements org.apache.sshd.common.file.FileSystemFactory {
    private final Context context;

    public FileSystemFactory(Context context) {
        this.context = context;
    }

    @Override
    public Path getUserHomeDir(SessionContext session) throws IOException {
        File rootDir = context.getFilesDir();
        return Paths.get(rootDir.getAbsolutePath()).getParent();
    }

    @Override
    public FileSystem createFileSystem(SessionContext session) throws IOException {
        // 使用Android的外部存储目录作为根目录
        File rootDir = context.getExternalFilesDir(null);
        if (rootDir == null) {
            rootDir = context.getFilesDir();
        }

        return Paths.get(rootDir.getAbsolutePath()).getFileSystem();
    }
}
java 复制代码
import android.content.Context;
import android.util.Log;
import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.common.util.io.PathUtils;
import org.apache.sshd.sftp.client.SftpClient;
import org.apache.sshd.sftp.client.SftpClientFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.List;

public class SftpClientHelper {
    private SshClient client;
    private ClientSession session;
    private SftpClient sftpClient;
    private static boolean isInitialized = false;
    //单列
    private static SftpClientHelper instance;

    public static SftpClientHelper getInstance() {
        if (instance == null) {
            instance = new SftpClientHelper();
        }
        return instance;
    }

    /**
     * 初始化SSHD配置(必须在连接前调用)
     * 这个方法是修复"No user home folder available"错误的关键
     */
    private synchronized void initialize(Context context) {
        if (!isInitialized) {
            try {
                // 设置用户主目录解析器 - 修复Android环境问题
                PathUtils.setUserHomeFolderResolver(() -> {
                    // 使用应用的缓存目录作为临时主目录
                    File cacheDir = new File("/data/data/" + getAppPackageName(context) + "/cache");
                    if (!cacheDir.exists()) {
                        cacheDir.mkdirs();
                    }
                    return Paths.get(cacheDir.getPath());
                });

                isInitialized = true;
                Log.d("SFTPClient", "SSHD initialization completed");
            } catch (Exception e) {
                Log.e("SFTPClient", "SSHD initialization failed", e);
            }
        }
    }

    /**
     * 获取应用包名
     */
    private String getAppPackageName(Context context) {
        // 这里返回您的应用包名,或者从Context获取
        return context.getPackageName(); // 请替换为您的实际包名
    }

    /**
     * 连接到SFTP服务器
     */
    public boolean connect(Context context, String host, int port, String username, String password) {
        try {
            // 确保先初始化
            initialize(context);

            // 创建SSH客户端
            client = SshClient.setUpDefaultClient();

            // 配置服务器密钥验证(接受所有服务器密钥 - 仅用于测试)
            // 在生产环境中应该实现正确的服务器密钥验证
            client.setServerKeyVerifier((clientSession, socketAddress, publicKey) -> {
                Log.d("SFTPClient", "Accepting server key: " + publicKey.getAlgorithm());
                return true; // 接受所有服务器密钥
            });

            client.start();

            // 连接到服务器
            session = client.connect(username, host, port).verify(10000).getSession();
            session.addPasswordIdentity(password);

            // 认证
            if (!session.auth().verify(15000).isSuccess()) {
                throw new IOException("Authentication failed");
            }

            // 创建SFTP客户端
            sftpClient = SftpClientFactory.instance().createSftpClient(session);

            Log.i("TAG", "SFTP连接成功: ");
            return true;

        } catch (Exception e) {
            Log.e("TAG", "SFTP连接失败: " + e.getMessage(), e);
            disconnect();
            return false;
        }
    }

    /**
     * 断开连接
     */
    public void disconnect() {
        try {
            if (sftpClient != null) {
                sftpClient.close();
                sftpClient = null;
            }
        } catch (Exception e) {
            Log.e("SFTPClient", "Error closing SFTP client", e);
        }

        try {
            if (session != null) {
                session.close();
                session = null;
            }
        } catch (Exception e) {
            Log.e("SFTPClient", "Error closing session", e);
        }

        try {
            if (client != null) {
                client.stop();
                client.close();
                client = null;
            }
        } catch (Exception e) {
            Log.e("SFTPClient", "Error closing client", e);
        }

        Log.i("SFTPClient", "Disconnected from SFTP server");
    }

    /**
     * 检查是否已连接
     */
    public boolean isConnected() {
        return session != null && session.isOpen() &&
                sftpClient != null && sftpClient.isOpen();
    }

    /**
     * 列出远程目录下的文件
     */
    public List<String> listFiles(String remoteDir) {
        List<String> fileList = new ArrayList<>();
        try {
            Iterable<SftpClient.DirEntry> entries = sftpClient.readDir(remoteDir);
            for (SftpClient.DirEntry entry : entries) {
                String filename = entry.getFilename();
                if (!filename.equals(".") && !filename.equals("..")) {
                    fileList.add(filename);
                }
            }
        } catch (Exception e) {
            System.err.println("列出文件失败: " + e.getMessage());
            e.printStackTrace();
        }
        return fileList;
    }

    /**
     * 从SFTP服务器下载文件
     * remoteFilePath:远程文件路径
     * localFilePath:本地文件路径
     */
    public boolean downloadFile(String remoteFilePath, String localFilePath) {
        try (InputStream inputStream = sftpClient.read(remoteFilePath);
             OutputStream outputStream = Files.newOutputStream(Paths.get(localFilePath))) {

            byte[] buffer = new byte[8192];
            int bytesRead;
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
            }

            System.out.println("文件下载成功: " + remoteFilePath + " -> " + localFilePath);
            return true;

        } catch (Exception e) {
            System.err.println("文件下载失败: " + e.getMessage());
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 上传文件到SFTP服务器
     * localFilePath:本地文件路径
     * remoteFilePath:远程文件路径
     */
    public boolean uploadFile(String localFilePath, String remoteFilePath) {
        try (InputStream inputStream = Files.newInputStream(Paths.get(localFilePath));
             OutputStream outputStream = sftpClient.write(remoteFilePath)) {

            byte[] buffer = new byte[8192];
            int bytesRead;
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
            }

            //生成校验码
            String sha256 = calculateSHA256(localFilePath);
            System.out.println("校验码: " + sha256);
            //e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
            System.out.println("文件上传成功: " + localFilePath + " -> " + remoteFilePath);
//            IUpgradeImpl.setSHA256(sha256, remoteFilePath);
            return true;

        } catch (Exception e) {
            System.err.println("文件上传失败: " + e.getMessage());
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除远程文件
     */
    public boolean deleteFile(String remoteFilePath) {
        try {
            sftpClient.remove(remoteFilePath);
            System.out.println("文件删除成功: " + remoteFilePath);
            return true;
        } catch (Exception e) {
            System.err.println("文件删除失败: " + e.getMessage());
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 创建远程目录
     */
    public void createDirectory(String remoteDir) throws IOException {
        boolean directory = fileExists(remoteDir);
        if (!directory) {
            try {
                sftpClient.mkdir(remoteDir);
                System.out.println("远程目录创建成功: " + remoteDir);
            } catch (Exception e) {
                System.err.println("远程目录创建失败: " + e.getMessage());
                e.printStackTrace();
            }
        } else {
            // 目录已存在,可选择删除后重新创建或直接使用
            System.out.println("远程目录已存在");
        }
    }

    /**
     * 检查文件是否存在
     */
    public boolean fileExists(String remoteFilePath) throws IOException {
        if (!isConnected()) {
            throw new IOException("Not connected to SFTP server");
        }

        try {
            sftpClient.stat(remoteFilePath);
            return true;
        } catch (IOException e) {
            return false;
        }
    }

    /**
     * 获取当前工作目录
     */
    public String getCurrentDirectory() throws IOException {
        if (!isConnected()) {
            throw new IOException("Not connected to SFTP server");
        }

        try {
            return sftpClient.canonicalPath(".");
        } catch (IOException e) {
            throw new IOException("Failed to get current directory", e);
        }
    }

    //生成唯一指纹
    public String calculateSHA256(String apkPath) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            File apkFile = new File(apkPath);
            FileInputStream fis = new FileInputStream(apkFile);

            byte[] buffer = new byte[1024];
            int read;
            while ((read = fis.read(buffer)) != -1) {
                digest.update(buffer, 0, read);
            }
            fis.close();

            StringBuilder sb = new StringBuilder();
            for (byte b : digest.digest()) {
                sb.append(String.format("%02x", b));
            }
            return sb.toString();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

xml布局代码

html 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">

    <Button
        android:id="@+id/connectButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="连接SFTP服务器" />

    <Button
        android:id="@+id/btnCreate"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="创建远程目录"
        android:layout_marginTop="8dp" />
    <Button
        android:id="@+id/btnUpload"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="上传文件"
        android:layout_marginTop="8dp" />
    <Button
        android:id="@+id/listButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="查看上传的文件"
        android:layout_marginTop="8dp" />

    <Button
        android:id="@+id/disconnectButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="断开连接"
        android:layout_marginTop="8dp" />

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginTop="16dp">

        <TextView
            android:id="@+id/logTextView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#f0f0f0"
            android:padding="8dp"
            android:textColor="@color/black"
            android:text="日志输出将显示在这里..."
            android:textSize="12sp" />

    </ScrollView>

</LinearLayout>

注意:A设备 B设备需要连接在同一个wifi下,客服端代码里host

复制代码
192.168.49.76需要换成对应设备的
相关推荐
fatiaozhang95277 小时前
中兴B860AV5.2-U_原机安卓4.4.2系统专用_晶晨S905L3SB处理器_线刷固件包
android·电视盒子·刷机固件·机顶盒刷机·中兴b860av5.2-u
儿歌八万首7 小时前
Android 自定义 View 实战:打造一个跟随滑动的丝滑指示器
android·kotlin
我有与与症7 小时前
Kuikly 实战:手把手撸一个跨平台 AI 聊天助手 (ChatDemo)
android
恋猫de小郭7 小时前
Flutter UI 设计库解耦重构进度,官方解答未来如何适配
android·前端·flutter
apihz8 小时前
全球IP归属地查询免费API详细指南
android·服务器·网络·网络协议·tcp/ip
hgz07109 小时前
Linux环境下MySQL 5.7安装与配置完全指南
android·adb
Just_Paranoid9 小时前
【Android UI】Android 添加圆角背景和点击效果
android·ui·shape·button·textview·ripple
梁同学与Android9 小时前
Android ---【经验篇】阿里云 CentOS 服务器环境搭建 + SpringBoot项目部署(二)
android·spring boot·后端