FTP协议详解和在Android上的应用

概述

FTP 是File Transfer Protocol(文件传输协议)的英文简称,用于Internet上文件的双向传输。FTP的主要作用就是让客户端连接上一个远程计算机(这些计算机上运行着FTP服务器程序)从而察看远程计算机上的文件,然后把文件从远程计算机上拷到本地计算机,或把本地的文件上传到远程计算机。

FTP是仅基于TCP的服务,不支持UDP。FTP协议使用2个连接,一个数据连接和一个控制连接(用来传输客户端向FTP服务器发送的命令、传输FTP服务器向客户端命令的响应)。通常来说控制连接的端口号是21,数据连接的端口号是20,但由于FTP协议工作方式的不同,数据连接的端口号并不总是20,这也是FTP的主动与被动模式的最大不同之处。

FTP协议的两种工作模式

1. 主动模式(Port模式)

1> 客户端发起从一个任意的非特权端口N(N大于等于1024)连接到FTP服务器的控制端口21,从而建立控制连接。
2> 客户端开始监听客户端的端口N+1,并发送FTP命令“port N+1”到FTP服务器。
3> 服务器从自己的数据端口20连接到客户端指定的数据端口N+1,从而建立数据连接。

2. 被动模式(Pasv模式)

1> 客户端发起从一个任意的非特权端口N(N大于等于1024)连接到FTP服务器的控制端口21,从而建立控制连接。
2> 与主动模式不同,客户端不会发送PORT命令要求服务器来回连它的数据端口,而是发送PASV命令。
3> 服务器接收到会PASV命令后会开启一个任意的非特权端口P(大于等于1024),并发送PORT P命令给客户端。
4> 客户端接收到PORT P命令后会发起从非特权端口N+1连接到服务器的端口P,从而建立数据连接。

注意:
1> 由于控制连接和数据连接都由客户端发起,可以解决从FTP服务器到客户端的数据端口的入方连接被防火墙过滤掉的问题。

FTP协议命令与响应信息

在FTP服务的执行过程中,FTP客户端与FTP服务器之间需要传输控制信息,这些信息用于完成某个具体的FTP操作,它们可以分为两种类型:FTP命令与FTP响应信息。其中,FTP命令是FTP客户端向FTP服务器发送的操作请求,FTP响应信息是FTP服务器根据操作结果向FTP客户端返回的响应信息。FTP协议详细规定了每种协议命令的顺序--首先需要顺序发送USER与PASS命令,最后需要发送QUIT命令,其他命令的顺序没有特殊要求。

1. FTP协议命令

FTP 每个命令都有 3 到 4 个字母组成,命令后面跟参数,用空格分开。每个命令都以 "\r\n"结束。FTP命令的标准格式为:

命令名 <参数>

常用的FTP命令如下表所示:

命令 描述
USER <username> 参数是标记用户的Telnet串。用户标记是访问服务器必须的,此命令通常是控制连接后第一个发出的命令,有些主机还会要求口令和帐户。服务器可以在任何时间接收新的USER命令以改变访问控制和(或)帐户信息。这可以重新开始登录过程,所以传输参数不变,在进行中的文件传输在过去的访问控制参数下完成。
PASS <password> 参数是标记用户口令的Telnet串。此命令紧跟USER命令,在某些站点它是完成访问控制不可缺少的一步。因此口令是个重要的东西,因此不能显示出来,服务器方没有办法隐藏口令,所以这一任务得由用户FTP进程完成。
ACCT <account> 参数是标记用户帐户的Telnet串。此命令不需要与USER相关,一些站点可能需要帐户用于登录,另一些可以限制帐户的权限,在后一种情况下,此命令可在任何时候发送。应答的不同可以区别不同的情况:当登录需要帐户信息时,对PASS命令的响应是332。另外,如果不需要帐户信息,对PASS的响应是230,如果需要帐户信息在以后需要,服务器会返回332或532,这要看它是保存此命令还是拒绝此命令了。
CWD <dir path> 此命令使用户可以在不同的目录或数据集下工作而不用改变它的登录或帐户信息。传输参数也不变。参数一般是目录名或与系统相关的文件集合。
CDUP 该命令要求系统回到上一级目录
SMNT <pathname> 此命令使用户在不改变登录或帐户信息的情况下加载另一个文件系统数据结构。传输参数也不变。参数是文件目录或与系统相关的文件集合。
REIN 此命令终止USER,将所有I/O和帐户信息写入,但不许进行中的数据传输完成。重置所有参数,控制连接打开,可以再次开始USER命令。
OUIT 此命令终止USER,如果没有数据传输,服务器关闭控制连接;如果有数据传输,在得到传输响应后服务器关闭控制连接。如果用户进程正在向不同的USER传输数据,不希望对每个USER关闭然后再打开,可以使用REIN。对控制连接的意外关闭,可以导致服务器运行中止(ABOR)和退出登录(QUIT)。
PORT <address> 参数是要使用的数据连接端口,通常情况下对此不需要命令响应。如果使用此命令时,要发送32位的IP地址和16位的TCP端口号。上面的信息以8位为一组,逗号间隔十进制传输,如下例: PORT h1,h2,h3,h4,p1,p2 其中h1是IP地址的最高8位。
PASV 此命令要求服务器DTP在指定的数据端口侦听,进入被动接收请求的状态,参数是主机和端口地址。
TYPE <data type> 该命令定义文件类型以及打印格式
STRU <type> 参数是一个Telnet字符代码指定文件结构。下面是代码及其意义: F - 文件(非记录结构),它是默认值 ;R - 记录结构; P - 页结构
MODE <mode> 参数是一个Telnet字符代码指定传输模式。下面是代码及其意义: S - 流(默认值); B - 块; C - 压缩
RETR <filename> 此命令使服务器DTP传送指定路径内的文件复本到服务器或用户DTP。这边服务器上文件的状态和内容不受影响。
STOR <filename> 此命令使服务器DTP接收数据连接上传送过来的数据,并将数据保存在服务器的文件中。如果文件已存在,原文件将被覆盖。如果文件不存在,则新建文件。
STOU <filename> 此命令和STOR差不多,此命令要求在此目录下的文件名是唯一的,对此命令的响应必须包括产生的用户名。
APPE <filename> 它和STOR的功能差不多,但是如果文件在指定路径内已存在,则把数据附加到原文件尾部,如果不存在则新建文件。
ALLO <bytes> 此命令用于在一些主机上为新传送的文件分配足够的存储空间。参数是十进制的逻辑字节数。如果是记录或页结构,页或记录的最大大小也需要,这在第二个参数内以十进制指定。第二个参数是可选的,如果有它,它和第一个参数以Telnet字符<SP> R <SP>分隔。此命令在STOR或APPE命令后,对于不需要分配存储空间的机器,它的作用等于NOOP。
REST <offset> 参数域代表服务器要重新开始的那一点,此命令并不传送文件,而是略过指定点后的数据,此命令后应该跟其它要求文件传输的FTP命令。
RNFR <old path> 这个命令和我们在其它操作系统中使用的一样,只不过后面要跟"rename to"指定新的文件名。
RNTO <new path> 此命令和上面的命令共同完成对文件的重命名。
ABOR 此命令通知服务中止以前的FTP命令和与之相关的数据传送。如果先前的操作已经完成,则没有动作,返回226。如果没有完成,返回426,然后再返回226。关闭控制连接,数据连接不关闭。
DELE <filename> 此命令删除指定路径下的文件。用户进程负责对删除的提示。
RMD <directory> 此命令删除目录。
MKD <directory> 此命令在指定路径下创建新目录。
PWD 在响应时返回当前工作目录。
LIST <name> 服务器传送列表到被动DTP,如果路径指定一个目录或许多文件,返回指定路径下的文件列表。如果路径名指定一个文件,服务器返回文件的当前信息,参数为空表示用户当前的工作目录或默认目录。数据传输在ASCII或EBCDIC下进行,用户必须确认这一点。因为文件信息因系统不同而不同,所以不可能被程序自动利用,但是人类用户却很需要。
NLST <directory> 服务器传送目录表名到用户,路径名应指定目录或其它系统指定的文件群描述子;空参数指当前目录。服务器返回文件名数据流,以ASCII或EBCDIC形式传送,并以<CRLF>或<NL>分隔。这里返回的信息有时可以供程序进行进一步处理。
SITE <params> 服务器用来提供服务器系统信息,信息因系统不同而不同,格式在HELP SITE命令应答中给出。
SYST 用于确定服务器上运行的操作系统。
STAT <directory> 此命令返回控制连接状态,它可以在文件传送过程中发送,服务器返回操作进行的状态。也可以在文件传送之间发送,这时命令有参数,参数是路径名,此命令的功能除了数据在控制连接上传送以外和列表命令相似。如果指定部分路径,服务器以文件名或与说明相关的属性返回;如没有参数,服务器返回服务器FTP进程的状态信息,包括传输参数的当前值和连接状态。
HELP <command> 这条命令我们在平常系统中得到的帮助没有什么区别,响应类型是211或214。建议在使用USER命令前使用此命令。
NOOP 此命令不产生什么实际动作,它仅使服务器返回OK。

2. FTP协议响应信息

FTP响应信息由两部分组成:响应码与描述信息(中间以空格隔开)。其中,响应码是由3位数字组成的字符串,它是对响应信息的数字标识,例如200表示用户登录成功;描述信息是对响应码的文字描述,例如200的描述信息是"Command okay."。FTP响应的标准格式为:

响应码  描述信息

常见的FTP响应如下表所示:

响 应 码 含 义
110 重新启动标记应答
120 服务器准备就绪的时间(分钟数)
125 打开数据连接,开始传输
150 文件状态良好,打开数据连接
200 命令成功
202 命令未执行
211 系统状态
212 目录状态
213 文件状态
214 帮助信息
215 系统类型
220 服务就绪
221 服务关闭控制连接,可以退出登录
225 打开数据连接
226 关闭数据连接,请求的文件操作成功
227 进入被动模式(IP地址、ID端口)
230 登录因特网
250 请求的文件操作完成
257 路径名建立
331 用户名正确,需要密码
332 登录时需要账户信息
350 请求的文件操作需要进一步命令
421 不能提供服务,关闭控制连接
425 无法打开数据连接
426 关闭连接,中止传输
450 请求的文件操作未执行
451 遇到本地错误
452 磁盘空间不足
500 格式错误,无效命令
501 参数语法错误
502 命令未执行
503 命令顺序错误
504 此参数下的命令功能未执行
530 未登录网络
532 存储文件需要账户信息
550 未执行请求的操作
551 不知道的页类型
552 超过存储分配
553 文件名不合法

在Android上的应用(实现一个FTP服务器)

  1. 首先通过启动一个Service来实现在后台守护ServerScoket模拟的FTP服务器线程(下面称为服务器线程),由于Service是在主线程中执行的,所以守护服务器线程的工作是在新建的线程中(下面称为守护线程)完成的,代码如下所示:
@Override
public void run() {
    Log.d(TAG, "Server thread running");

    if (isConnectedToLocalNetwork() == false) {
        Log.w(TAG, "run: There is no local network, bailing out");
        stopSelf();
        sendBroadcast(new Intent(ACTION_FAILEDTOSTART));
        return;
    }

    // Initialization of wifi, set up the socket
    try {
        setupListener();
    } catch (IOException e) {
        Log.w(TAG, "run: Unable to open port, bailing out.");
        stopSelf();
        sendBroadcast(new Intent(ACTION_FAILEDTOSTART));
        return;
    }

    // @TODO: when using ethernet, is it needed to take wifi lock?
    WifiUtil.takeWifiLock(getApplicationContext(), wifiLock);
    PowerUtil.takeWakeLock(getApplicationContext(), wakeLock);

    // A socket is open now, so the FTP server is started, notify rest of world
    Log.i(TAG, "Ftp Server up and running, broadcasting ACTION_STARTED");
    sendBroadcast(new Intent(ACTION_STARTED));

    while (!shouldExit) {
        if (wifiListener != null) {
            if (!wifiListener.isAlive()) {
                Log.d(TAG, "Joining crashed wifiListener thread");
                try {
                    wifiListener.join();
                } catch (InterruptedException e) {
                }
                wifiListener = null;
            }
        }
        if (wifiListener == null) {
            // Either our wifi listener hasn't been created yet, or has crashed,
            // so spawn it
            wifiListener = new TcpListener(serverSocket, this);
            wifiListener.start();
        }
        try {
            // TODO: think about using ServerSocket, and just closing
            // the main socket to send an exit signal
            Thread.sleep(WAKE_INTERVAL_MS);
        } catch (InterruptedException e) {
            Log.d(TAG, "Thread interrupted");
        }
    }

    terminateAllSessions();

    if (wifiListener != null) {
        wifiListener.quit();
        wifiListener = null;
    }
    shouldExit = false; // we handled the exit flag, so reset it to acknowledge
    Log.d(TAG, "Exiting cleanly, returning from run()");

    stopSelf();
    sendBroadcast(new Intent(ACTION_STOPPED));
}
// This opens a listening socket on all interfaces.
void setupListener() throws IOException {
    serverSocket = new ServerSocket();
    serverSocket.setReuseAddress(true);
    serverSocket.bind(new InetSocketAddress(FtpServerSettings.getPortNumber()));
}

上面setupListener方法就是用来为serverScoket绑定端口用的,由于FtpServerSettings.getPortNumber()得到的返回值就是2121,因此serverScoket绑定了2121端口;由于shouldExit的值是false,因此守护线程会每间隔WAKE_INTERVAL_MS时长就会检查wifiListener线程是否存活,如果不存活就会重新建立wifiListener线程,从而完成了守护wifiListener线程的工作。
wifiListener线程就是serverScoket监听2121端口的工作线程,即上面提到的ServerScoket模拟的FTP服务器线程。

  1. wifiListener对应的TcpListener类(坚持Thread类)中实现监听2121端口的过程如下:
@Override
public void run() {
    try {
        while (true) {
            Socket clientSocket = listenSocket.accept();
            Log.i(TAG, "New connection, spawned thread");
            SessionThread newSession = new SessionThread(clientSocket,
                    new LocalDataSocket());
            newSession.start();
            ftpServerService.registerSessionThread(newSession);
        }
    } catch (Exception e) {
        Log.d(TAG, "Exception in TcpListener");
    }
}

可以看到是通过调用serverSocket的accept方法实现的,当执行accept方法后,FTP服务器线程就会进入等待的状态,只要有客户端发起连接FTP服务器线程,FTP服务器线程才会继续向下执行,现在在windows操作系统的“计算机”上的地址栏上输入上输入ftp://192.168.10.52:2121/,然后回车,连接FTP服务器线程成功后的效果图如下所示:



然后你就可以像在本地操作文件夹一样操作android中的文件目录。
在mac系统中通过finder或者通过浏览器的地址栏中输入ftp://192.168.10.52:2121/也可以连接FTP服务器线程,效果图如下:




在mac系统中通过finder或者通过浏览器连接虽然是成功的,但是只能浏览和下载文件,修改和上传是不行的,所以如果想在mac上修改和上传,就需要下载一个ftp客户端。

注意:
我现在实现的是在同一个局域网上才可以,上面的IP地址192.168.10.52就是手机的wifi的IP。

  1. 当第2步中发起连接(就是上面FTP协议中提到的控制连接)后,就会创建SessionThread类(继承自Thread类)的实例sessionThread,就相当为这个控制连接创建一个会话线程,该会话线程用来专门处理客户端向ServerScoket模拟的FTP服务器发送的命令并且对客户端的命令做出响应。代码如下所示:
@Override
public void run() {
    Log.i(TAG, "SessionThread started");

    if (sendWelcomeBanner) {
        writeString("220 SwiFTP " + MyApplication.getVersion() + " ready\r\n");
    }
    // Main loop: read an incoming line and process it
    try {
        BufferedReader in = new BufferedReader(new InputStreamReader(
                cmdSocket.getInputStream()), 8192); // use 8k buffer
        while (true) {
            String line;
            line = in.readLine(); // will accept \r\n or \n for terminator
            if (line != null) {
                FTPServerService.writeMonitor(true, line);
                Log.d(TAG, "Received line from client: " + line);
                FtpCmd.dispatchCommand(this, line);
            } else {
                Log.i(TAG, "readLine gave null, quitting");
                break;
            }
        }
    } catch (IOException e) {
        Log.i(TAG, "Connection was dropped");
    }
    closeSocket();
}

可以看到,会话线程会先解析出FTP客户端请求的命令,然后通过FtpCmd.dispatchCommand方法处理命令,处理命令的代码如下:

protected static CmdMap[] cmdClasses = { new CmdMap("SYST", CmdSYST.class),
      new CmdMap("USER", CmdUSER.class), new CmdMap("PASS", CmdPASS.class),
      new CmdMap("TYPE", CmdTYPE.class), new CmdMap("CWD", CmdCWD.class),
      new CmdMap("PWD", CmdPWD.class), new CmdMap("LIST", CmdLIST.class),
      new CmdMap("PASV", CmdPASV.class), new CmdMap("RETR", CmdRETR.class),
      new CmdMap("NLST", CmdNLST.class), new CmdMap("NOOP", CmdNOOP.class),
      new CmdMap("STOR", CmdSTOR.class), new CmdMap("DELE", CmdDELE.class),
      new CmdMap("RNFR", CmdRNFR.class), new CmdMap("RNTO", CmdRNTO.class),
      new CmdMap("RMD", CmdRMD.class), new CmdMap("MKD", CmdMKD.class),
      new CmdMap("OPTS", CmdOPTS.class), new CmdMap("PORT", CmdPORT.class),
      new CmdMap("QUIT", CmdQUIT.class), new CmdMap("FEAT", CmdFEAT.class),
      new CmdMap("SIZE", CmdSIZE.class), new CmdMap("CDUP", CmdCDUP.class),
      new CmdMap("APPE", CmdAPPE.class), new CmdMap("XCUP", CmdCDUP.class), // synonym
      new CmdMap("XPWD", CmdPWD.class), // synonym
      new CmdMap("XMKD", CmdMKD.class), // synonym
      new CmdMap("XRMD", CmdRMD.class), // synonym
      new CmdMap("MDTM", CmdMDTM.class), //
      new CmdMap("MFMT", CmdMFMT.class), //
      new CmdMap("REST", CmdREST.class), //
      new CmdMap("SITE", CmdSITE.class), //
};
protected static void dispatchCommand(SessionThread session, String inputString) {
    String[] strings = inputString.split(" ");
    String unrecognizedCmdMsg = "502 Command not recognized\r\n";
    if (strings == null) {
        // There was some egregious sort of parsing error
        String errString = "502 Command parse error\r\n";
        Log.d(TAG, errString);
        session.writeString(errString);
        return;
    }
    if (strings.length < 1) {
        Log.d(TAG, "No strings parsed");
        session.writeString(unrecognizedCmdMsg);
        return;
    }
    String verb = strings[0];
    if (verb.length() < 1) {
        Log.i(TAG, "Invalid command verb");
        session.writeString(unrecognizedCmdMsg);
        return;
    }
    FtpCmd cmdInstance = null;
    verb = verb.trim();
    verb = verb.toUpperCase();
    for (int i = 0; i < cmdClasses.length; i++) {

        if (cmdClasses[i].getName().equals(verb)) {
            // We found the correct command. We retrieve the corresponding
            // Class object, get the Constructor object for that Class, and
            // and use that Constructor to instantiate the correct FtpCmd
            // subclass. Yes, I'm serious.
            Constructor<? extends FtpCmd> constructor;
            try {
                constructor = cmdClasses[i].getCommand().getConstructor(
                        new Class[] { SessionThread.class, String.class });
            } catch (NoSuchMethodException e) {
                Log.e(TAG, "FtpCmd subclass lacks expected " + "constructor ");
                return;
            }
            try {
                cmdInstance = constructor.newInstance(new Object[] { session,
                        inputString });
            } catch (Exception e) {
                Log.e(TAG, "Instance creation error on FtpCmd");
                return;
            }
        }
    }
    if (cmdInstance == null) {
        // If we couldn't find a matching command,
        Log.d(TAG, "Ignoring unrecognized FTP verb: " + verb);
        session.writeString(unrecognizedCmdMsg);
        return;
    }

    if (session.isUserLoggedIn()) {
        cmdInstance.run();
    } else if (session.isAnonymouslyLoggedIn() == true) {
        boolean validCmd = false;
        for (Class<?> cl : allowedCmdsWhileAnonymous) {
            if (cmdInstance.getClass().equals(cl)) {
                validCmd = true;
                break;
            }
        }
        if (validCmd == true) {
            cmdInstance.run();
        } else {
            session.writeString("530 Guest user is not allowed to use that command\r\n");
        }
    } else if (cmdInstance.getClass().equals(CmdUSER.class)
            || cmdInstance.getClass().equals(CmdPASS.class)
            || cmdInstance.getClass().equals(CmdQUIT.class)) {
        cmdInstance.run();
    } else {
        session.writeString("530 Login first with USER and PASS, or QUIT\r\n");
    }
}

上面的代码很简单,首先是对格式不正确的命令做出响应(响应码事502,表示命令未执行);接着根据命令名称找到对应的处理这个命令的类(例如CmdUSER.class就是用来处理USER命令),然后通过反射创建创建该类的实例,然后执行实例的run方法,继而完成命令的处理,处理完成后,一般都会返回给FTP客户端响应信息,具体可以参考上面 常见的FTP响应表。

上面的源码是参考SwiFTP开源软件,源码地址:
FTP Server (swiftp)
有兴趣的同学可以自己研究一下。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,392评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,258评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,417评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,992评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,930评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,199评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,652评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,327评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,463评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,382评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,432评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,118评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,704评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,787评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,999评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,476评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,057评论 2 341

推荐阅读更多精彩内容

  • 运行操作 CMD命令:开始->运行->键入cmd或command(在命令行里可以看到系统版本、文件系统版本) CM...
    小明yz阅读 2,742评论 0 8
  • 1、第八章 Samba服务器2、第八章 NFS服务器3、第十章 Linux下DNS服务器配站点,域名解析概念命令:...
    哈熝少主阅读 3,693评论 0 10
  • 命令简介 cmd是command的缩写.即命令行 。 虽然随着计算机产业的发展,Windows 操作系统的应用越来...
    Littleston阅读 3,308评论 0 12
  • 运行操作 CMD命令:开始->运行->键入cmd或command(在命令行里可以看到系统版本、文件系统版本) CM...
    小沐子_IT阅读 2,014评论 0 4
  • win7 cmd管理员权限设置 net localgroup administrators 用户名 /add 把“...
    f675b1a02698阅读 5,121评论 0 11