文章目录

  • 前言
  • 一、FTP是什么?
  • 二、使用步骤
  • 1 服务端
  • 1.1 服务端的代码实现
  • 2 客户端
  • 2.1 客户端的代码实现
  • 附件



前言

最近在公司的项目中,使用到了 局域网通信,不同的设备直接传递消息以及发送文件。普通文本消息的发送使用websocket 封装了一套可以使用,但是对于文件类型的消息,尤其是大文件的发送,使用websocket 发送字节数组的形式出现了很多的问题,所以就取消了使用websocket 自己封装发送的方式。采用局域网


一、FTP是什么?

FTP(File Transfer Protocol,文件传输协议) 是 TCP/IP 协议组中的协议之一。FTP协议包括两个组成部分,其一为FTP服务器,其二为FTP客户端。
其中FTP服务器用来存储文件,用户可以使用FTP客户端通过FTP协议访问位于FTP服务器上的资源。在开发网站的时候,通常利用FTP协议把网页或程序传到Web服务器上。此外,由于FTP传输效率非常高,在网络上传输大的文件时,一般也采用该协议

二、使用步骤

1 服务端

引入库:

//ftp server
    implementation files('libs/ftpserver-core-1.0.6.jar')
    implementation files('libs/mina-core-2.0.16.jar')
    implementation 'commons-net:commons-net:3.8.0'

1.1 服务端的代码实现

FtpServerlet:

import androidx.annotation.Keep
import org.apache.ftpserver.FtpServer
import java.io.File
import org.apache.ftpserver.listener.ListenerFactory

import org.apache.ftpserver.FtpServerFactory
import org.apache.ftpserver.ftplet.*
import org.apache.ftpserver.usermanager.SaltedPasswordEncryptor

import org.apache.ftpserver.usermanager.PropertiesUserManagerFactory
import org.apache.ftpserver.usermanager.impl.WritePermission
import org.apache.ftpserver.usermanager.impl.BaseUser

@Keep
class FtpServerlet(
    private val sharedDirectory: String,
    private val ftpPort: Int = 2121,
    private val log: ((tag: String, content: String?) -> Unit)?=null
) :
    DefaultFtplet() {
    private val TAG = "FtpServerlet"
    private var mFtpServer: FtpServer? = null
    private val mUser = "myUser"
    private val mPassword = "myPassword"

    fun startFtp() {
        if (null != mFtpServer && !mFtpServer!!.isStopped) {
            return
        }
        val file = File(sharedDirectory)
        if (!file.exists()) {
            file.mkdirs()
        }

        val serverFactory = FtpServerFactory()
        val listenerFactory = ListenerFactory()

        // 设定端末番号
        listenerFactory.port = ftpPort

        // 通过PropertiesUserManagerFactory创建UserManager然后向配置文件添加用户
        val userManagerFactory = PropertiesUserManagerFactory()
        userManagerFactory.passwordEncryptor = SaltedPasswordEncryptor()

        val userManager: UserManager = userManagerFactory.createUserManager()
        val auths = mutableListOf<Authority>()
        val auth: Authority = WritePermission()
        auths.add(auth)
        //添加用户
        val user = BaseUser()
        user.name = mUser
        user.password = mPassword
        user.homeDirectory = sharedDirectory
        user.authorities = auths
        userManager.save(user)

        // 设定Ftplet
        val ftpletMap = mutableMapOf<String, Ftplet>()
        ftpletMap["Ftplet"] = this
        serverFactory.userManager = userManager;
        serverFactory.addListener("default", listenerFactory.createListener());
//        serverFactory.ftplets = ftpletMap
        mFtpServer = serverFactory.createServer();
        mFtpServer?.start()

        log?.invoke(TAG, "start ftp server , sharedDirectory = $sharedDirectory")
    }

    fun stopFtp() {
        // FtpServer不存在和FtpServer正在运行中
        if (null != mFtpServer && !mFtpServer!!.isStopped) {
            mFtpServer?.stop()
            log?.invoke(TAG, "stop ftp server")
        }
    }

    override fun onAppendStart(session: FtpSession?, request: FtpRequest?): FtpletResult {
        log?.invoke(
            TAG,
            "onAppendStart  argument = ${request?.argument} , requestLine = ${request?.requestLine} , command = ${request?.command}"
        )
        return super.onAppendStart(session, request)
    }

    override fun onAppendEnd(session: FtpSession?, request: FtpRequest?): FtpletResult {
        log?.invoke(
            TAG,
            "onAppendEnd  argument = ${request?.argument} , requestLine = ${request?.requestLine} , command = ${request?.command}"
        )
        return super.onAppendEnd(session, request)
    }

    override fun onLogin(session: FtpSession?, request: FtpRequest?): FtpletResult {
        log?.invoke(TAG, "onLogin  argument = ${request?.argument} , requestLine = ${request?.requestLine} , command = ${request?.command}")
        return super.onLogin(session, request)
    }

    override fun onConnect(session: FtpSession?): FtpletResult {
        log?.invoke(TAG, "onConnect ")
        return super.onConnect(session)
    }

    override fun onDisconnect(session: FtpSession?): FtpletResult {
        log?.invoke(TAG, "onDisconnect ")
        return super.onDisconnect(session)
    }

    override fun onUploadStart(session: FtpSession?, request: FtpRequest?): FtpletResult {
        log?.invoke(
            TAG,
            "onUploadStart  argument = ${request?.argument} , requestLine = ${request?.requestLine} , command = ${request?.command}"
        )
        return super.onUploadStart(session, request)
    }

    override fun onUploadEnd(session: FtpSession?, request: FtpRequest?): FtpletResult {
        log?.invoke(
            TAG,
            "onUploadEnd  argument = ${request?.argument} , requestLine = ${request?.requestLine} , command = ${request?.command}"
        )
        return super.onUploadEnd(session, request)
    }

    override fun onDownloadStart(session: FtpSession?, request: FtpRequest?): FtpletResult {
        log?.invoke(
            TAG,
            "onDownloadStart  argument = ${request?.argument} , requestLine = ${request?.requestLine} , command = ${request?.command}"
        )
        return super.onDownloadStart(session, request)
    }

    override fun onDownloadEnd(session: FtpSession?, request: FtpRequest?): FtpletResult {
        log?.invoke(
            TAG,
            "onDownloadEnd  argument = ${request?.argument} , requestLine = ${request?.requestLine} , command = ${request?.command}"
        )
        return super.onDownloadEnd(session, request)
    }
}

2 客户端

引入库:

//ftp
    implementation 'commons-net:commons-net:3.8.0'
    implementation files('libs/ftpserver-core-1.0.6.jar')

2.1 客户端的代码实现

import android.net.Uri
import android.util.Log
import com.demo.client.model.ClientConfig
import kotlinx.coroutines.*
import org.apache.commons.net.ftp.FTPClient
import org.apache.commons.net.ftp.FTPReply
import java.io.*
import java.net.SocketException

class FtpClientManager {

    companion object {
        private const val TAG = "FtpClient"
        private const val BUFFER_SIZE = 2 * 1024 * 1024
        val instance by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
            FtpClientManager()
        }
    }

    private val job = SupervisorJob()
    private val scope = CoroutineScope(Dispatchers.IO + job)
    var ilog: ((tag: String, content: String?) -> Unit)? = null
    private var ftpClient: FTPClient? = null
    private var config: ClientConfig? = null

    fun connect2Server(config: ClientConfig, callback: ((result: Boolean) -> Unit)? = null) {
        this.config = config
        ilog?.invoke(TAG, config.toString())

        val ftpUri = Uri.parse(config.ftpUrl)
        ftpClient = FTPClient()
        //设定连接超时时间
        ftpClient?.connectTimeout = config.connectTimeout.toInt()
        ftpClient?.setDataTimeout(config.connectTimeout.toInt())

        ilog?.invoke(TAG, "ftpUri = ${ftpUri.host} , ${ftpUri.port}")

        scope.launch {

            try {

                // 要连接的FTP服务器Url,Port
                ftpClient?.connect(ftpUri.host, ftpUri.port)

                // 看返回的值是不是230,如果是,表示登陆成功
                val reply = ftpClient?.replyCode
                ilog?.invoke(TAG, "reply = $reply")

                if (!FTPReply.isPositiveCompletion(reply!!)) {
                    // 断开
                    ftpClient?.disconnect()

                    withContext(Dispatchers.Main) {
                        callback?.invoke(false)
                    }

                    return@launch
                }

                // 登陆FTP服务器
                val login = ftpClient?.login(config.ftpUserName ?: "anonymous", config.ftpUserPwd)
                ilog?.invoke(TAG, "login = $login")
                if (!login!!) {
                    // 断开
                    ftpClient?.disconnect()

                    withContext(Dispatchers.Main) {
                        callback?.invoke(false)
                    }
                    return@launch
                }

                ftpClient?.setFileType(FTPClient.BINARY_FILE_TYPE)
                ftpClient?.enterLocalPassiveMode()

                withContext(Dispatchers.Main) {
                    callback?.invoke(true)
                }

            } catch (e: SocketException) {
                e.printStackTrace()
                withContext(Dispatchers.Main) {
                    callback?.invoke(false)
                }
            } catch (e: IOException) {
                e.printStackTrace()
                withContext(Dispatchers.Main) {
                    callback?.invoke(false)
                }
            }
        }
    }

    fun disconnectFTP(): Boolean {
        return try {
            ftpClient?.disconnect()
            true
        } catch (e: IOException) {
            e.printStackTrace()
            false
        }
    }


    /**
     * 下载文件
     *
     * @param saveFilePath   要存放的文件的路径
     * @param remoteFileName  远程FTP服务器上的那个文件的名字
     * @param callback true为成功,false为失败
     */
    fun downloadFile(saveFilePath: String, remoteFileName: String, callback: ((result: Boolean) -> Unit)? = null) {
        if (null == ftpClient) {
            callback?.invoke(false)
            return
        }

        if (!ftpClient!!.isConnected) {

            connect2Server(config!!) { result ->
                if (result) {
                    realDownload(saveFilePath, remoteFileName, callback)
                }
            }
        } else {
            realDownload(saveFilePath, remoteFileName, callback)
        }
    }

    private fun realDownload(saveFilePath: String, remoteFileName: String, callback: ((result: Boolean) -> Unit)? = null) {
        scope.launch {
            var out: BufferedOutputStream? = null
            try {
                val workPath = ftpClient?.printWorkingDirectory()
                ilog?.invoke(TAG, "realDownload workPath = $workPath")
                if ("/" == workPath) {
                     //工作目录切换,按需看自己是否需要切换
                    ftpClient?.changeWorkingDirectory("/resources/")
                }

                ftpClient?.bufferSize = BUFFER_SIZE
                val files = ftpClient!!.listFiles(remoteFileName)
                if (null == files || files.isEmpty()) {
                    throw FileNotFoundException("not found remoteFileName")
                }

                val fileInfo = files[0]
                val localFile = File(saveFilePath)
                out = BufferedOutputStream(FileOutputStream(localFile))
                val downFlag = ftpClient!!.retrieveFile(fileInfo.name, out)

                out.flush()

                withContext(Dispatchers.Main) {
                    callback?.invoke(downFlag)
                }
            } catch (e: Exception) {
                e.printStackTrace()
                withContext(Dispatchers.Main) {
                    callback?.invoke(false)
                }
            } finally {
                out?.close()
                // 退出登陆FTP,关闭ftpCLient的连接
                ftpClient?.logout()
                ftpClient?.disconnect()
            }
        }
    }

    /**
     * 上传文件
     *
     * @param uploadFilePath 要上传文件所在SDCard的路径
     * @param callback true为成功,false为失败
     */
    fun uploadFile(uploadFilePath: String, callback: ((result: Boolean) -> Unit)? = null) {
        if (null == ftpClient) {
            callback?.invoke(false)
            return
        }

        if (!ftpClient!!.isConnected) {

            connect2Server(config!!) { result ->

                if (result) {
                    realUpload(uploadFilePath, callback)
                }
            }
        } else {
            realUpload(uploadFilePath, callback)
        }
    }

    private fun realUpload(uploadFilePath: String, callback: ((result: Boolean) -> Unit)? = null) {
        val uoloadFile = File(uploadFilePath)

        if (!uoloadFile.exists()) {
            callback?.invoke(false)
            return
        }

        scope.launch {

            val workPath = ftpClient?.printWorkingDirectory()
            ilog?.invoke(TAG, "realUpload workPath = $workPath")
            if ("/" == workPath) {
            	//工作目录切换,按需看自己是否需要切换
                ftpClient?.changeWorkingDirectory("/resources/")
            }

            // 设置上传文件需要的一些基本信息
            ftpClient?.bufferSize = BUFFER_SIZE
            ftpClient?.controlEncoding = "UTF-8"
            ftpClient?.enterLocalPassiveMode()

            var fileInputStream: FileInputStream? = null
            try {

                fileInputStream = FileInputStream(uploadFilePath)
                val uploadFlag = ftpClient!!.storeFile(uoloadFile.name, fileInputStream)
                withContext(Dispatchers.Main) {
                    callback?.invoke(uploadFlag)
                }
            } catch (e: Exception) {
                e.printStackTrace()
                withContext(Dispatchers.Main) {
                    callback?.invoke(false)
                }
            } finally {
                fileInputStream?.close()
                // 退出登陆FTP,关闭ftpCLient的连接
                ftpClient?.logout()
                ftpClient?.disconnect()
            }
        }
    }
}

附件

依赖库下载地址:

https://mina.apache.org/downloads-mina_2_0.html

https://mina.apache.org/ftpserver-project/old-downloads.html