使用apache ftpserver实现自定义ftp服务器

maven依赖

<dependency>
    <groupId>org.apache.ftpserver</groupId>
    <artifactId>ftpserver-core</artifactId>
    <version>1.1.1</version>
</dependency>
<dependency>
    <groupId>org.apache.ftpserver</groupId>
    <artifactId>ftplet-api</artifactId>
    <version>1.1.1</version>
</dependency>
<dependency>
    <groupId>org.apache.mina</groupId>
    <artifactId>mina-core</artifactId>
    <version>2.0.16</version>
</dependency>

java bean 配置结构

核心FtpServerFactory
——需要addListener(“default”, CustomListenerFactory.createListener) 可以定义被动模式的端口范围、外网地址
——需要setFtplets( new HashMap() { “ftpService”: ? extends DefaultFtplet})

class DefaultFtplet {
    .... 一些onXXStart | End 方法
public FtpletResult onUploadStart(FtpSession session, FtpRequest request)
            throws FtpException, IOException {}
public FtpletResult onUploadEnd(FtpSession session, FtpRequest request)
            throws FtpException, IOException {}

——需要setUserManager(? extends AbstractUserManager)

  class AbstractUserManager {
      ...
    User getUserByName(String username) throws FtpException;

      返回一个BaseUser实例即可,
          需要对其设置setHomeDirectory,setAuthorities(new WritePermission() | new ConcurrentLoginPermission(9999,9999)),setUserId、Name、Password

最后调用createServer获得FtpServer,再调用 server.start()开启服务。

总结:创建FtpServer的3要素——监听器(监听器工厂创建)、登录逻辑(UserManager)、事件监听器(Ftplet)

重要源码

底层用mina进行通信

public class FtpHandlerAdapter extends IoHandlerAdapter {
    ...

    public void messageReceived(IoSession session, Object message)
            throws Exception {
        FtpIoSession ftpSession = new FtpIoSession(session, context);
        FtpRequest request = new DefaultFtpRequest(message.toString());

        ftpHandler.messageReceived(ftpSession, request);
    }


public class DefaultFtpHandler implements FtpHandler {
    ...
    public void messageReceived(final FtpIoSession session,
            final FtpRequest request) throws Exception {
        try {
            session.updateLastAccessTime();

            //获取请求中的命令信息
             String commandName = request.getCommand();
            CommandFactory commandFactory = context.getCommandFactory();
            Command command = commandFactory.getCommand(commandName);

            FtpletContainer ftplets = context.getFtpletContainer();

            //使用我们自定义的ftplet进行预处理判断命令类型
            //有DEFAULT、NO_FTPLET、SKIP、DISCONNECT
            FtpletResult ftpletRet = ftplets.beforeCommand(session.getFtpletSession(),
                        request);


            //判断预处理结果
            if (ftpletRet == FtpletResult.DISCONNECT) {
                session.close(false).awaitUninterruptibly(10000);
                return;
            }
            else if (ftpletRet != FtpletResult.SKIP) {
                 synchronized (session) {
                     //真正的命令处理
                        command.execute(session, context, request);
                    }


                 ftpletRet = ftplets.afterCommand(
                            session.getFtpletSession(), request, session
                                    .getLastReply());
            }

代码看起来很多总结起来就是——实现了mina的IoHandlerAdapter,处理消息接收的逻辑是调用FtpHandler间接调用Ftplet逻辑。

真正的命令实现

命令执行是这行

command.execute(session, context, request);

它的定义
void execute(FtpIoSession session, FtpServerContext context,
            FtpRequest request) throws IOException, FtpException;

查看实现类
image-20211106221312039
发现是FTP协议中的命令,
选择协议中网络通信的第一条命令USER进去看一看——

public class USER extends AbstractCommand {
     public void execute(final FtpIoSession session,
            final FtpServerContext context, final FtpRequest request)
            throws IOException, FtpException {
         ...
         // already logged-in
            User user = session.getUser();//从Mina的session中获取用户信息,判断是否登录

         boolean anonymous = userName.equals("anonymous");
         //看是否开启了匿名登录

          // anonymous login limit check
            int currAnonLogin = stat.getCurrentAnonymousLoginNumber();
            int maxAnonLogin = context.getConnectionConfig()
                    .getMaxAnonymousLogins();

           // login limit check
            int currLogin = stat.getCurrentLoginNumber();
            int maxLogin = context.getConnectionConfig().getMaxLogins();

         //重头戏 运行我们自定义的用户登录逻辑
          User configUser = context.getUserManager().getUserByName(userName);
            if (configUser != null) {
                ConcurrentLoginRequest loginRequest = new ConcurrentLoginRequest(
                        stat.getCurrentUserLoginNumber(configUser) + 1,
                        stat.getCurrentUserLoginNumber(configUser, address) + 1);

                 if (configUser.authorize(loginRequest) == null) {
                     因为没获取到授权,属于登录失败逻辑
                    return;
                 }

                success = true;
            session.setUserArgument(userName);

                 session.write(LocalizedFtpReply.translate(session, request, context,
                        FtpReply.REPLY_331_USER_NAME_OKAY_NEED_PASSWORD,
                        "USER", userName));

                 /**
     * 331 User name okay, need password.
     */
    public static final int REPLY_331_USER_NAME_OKAY_NEED_PASSWORD = 331;
            }


命令执行的钩子

在DefaltFtplet(我们做实现通常要继承的那个)逻辑中,
image-20211106220814620

image-20211106220846438

可以看到,实际在命令执行前后会执行很多onXXStart、onXXEnd,这是我们可以做业务逻辑的地方,也就是扩展点。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注