[关闭]
@lishuhuakai 2016-11-15T13:38:25.000000Z 字数 4533 阅读 1768

一起来写ftp server 01 -- 一个实现了基本功能的ftp server


一些坑,踩了就好了.

写这个ftp server只是为了练一下手,写这种文章只是想记录一下我在编写这个ftp server的过程中的一些想法和收获.虽然编写这个玩意有点耗时,但是有一些坑,你不踩,你永远都不知道.

我们应该用什么样的方式来实现我们的程序?

这是一个老生长谈的问题,使用多线程,多进程还是别的什么方式?

鉴于我们已经有了一个web server的经验,我们自然会想,可不可以采取多线程的方式来处理连接,我的回答是,可以,但是太费劲,你的第一个版本应该是能够将ftp跑起来,高不高效,并发度高不高不应该在你的考虑范围之内.

不同于web server,ftp连接不是一次处理之后立马就会断开,它会一直连接着.这从另外一个角度也说明了服务器可以同时处理的ftp连接数是远远达不到web server可以同时处理的连接数的.

如果我们用多线程的方式来实现处理的话,还会遭遇到一个很严重的问题,那就是应用程序的当前目录的切换,一般而言,每个客户端处在的目录是不同的,举个栗子,a客户处在服务器的/home目录,b客户处在服务器的/home/tmp目录,使用多进程方式同时处理这么a,b客户的连接的话,目录的切换会是一个很大的问题,a线程处理a用户,要将目录且换到/home目录,b线程处理b用户,要将目录切换到/home/tmp目录,而多线程之间的工作目录是共享的,这样以来,我们确实很难控制目录的切换.

当然,并非没有办法,我们可以采取记录下用户所处目录的方式来进行处理,那是后话,这里暂时不谈论.

为了实现简单,我们的第一个版本对客户端的连接采用了多进程的方式来处理.

主函数

和之前的一些一起来写系列的文章一样,我这里也采用了包裹函数的方式.
我们先来看一下主函数:

  1. int main(int argc, char *argv[])
  2. {
  3. int listenfd = Open_listenfd(1024); /* 监听套接字 */
  4. struct sockaddr_in clnaddr;
  5. socklen_t len = sizeof(clnaddr);
  6. int connfd = Accept(listenfd, (SA*)&clnaddr, &len);
  7. Close(listenfd); /* 关闭监听套接字 */
  8. HandleFtp handle(connfd);
  9. handle.Handle(); /* 开始处理连接 */
  10. return 0;
  11. }

我这里这么写是为了方便,所以暂时只处理了一个连接.这样以来可以方便调试,以后代码接近完工的时候,会将某些东西一一补全.

HandleFtp

在main函数中主要是调用了HandleFtp来处理连接,HandleFtp这个类异常简单:

  1. class HandleFtp : boost::noncopyable
  2. {
  3. public:
  4. HandleFtp(int cmdfd);
  5. void Handle();
  6. private:
  7. int cmdfd_; /* 这个文件描述符用于传递命令 */
  8. int commufd_[2]; /* 这个管道用于父子进程通信 */
  9. };

我并不打算在这个类里面干多少事情,这个类只是起到了一个过渡的作用.

  1. void HandleFtp::Handle() {
  2. /* 首先要调用socketpair函数构建管道 */
  3. Socketpair(AF_UNIX, SOCK_STREAM, 0, commufd_);
  4. /* 然后调用Fork函数 */
  5. if (Fork() == 0) { /* 子进程 */
  6. Close(cmdfd_);
  7. Close(commufd_[0]); /* 关闭掉另外一端 */
  8. DataHandle data(commufd_[1]);
  9. data.Handle();
  10. }
  11. else { /* 父进程 */
  12. Close(commufd_[1]); /* 关闭掉一端 */
  13. CmdHandle cmd(cmdfd_, commufd_[0]); /* cmdfd_ */
  14. cmd.Handle(); /* 处理连接 */
  15. }
  16. }

这里的话,代码分成了两个部分,首先创建管道,用于进程间的通信,然后调用Fork函数,将进程一分为二,子进程进行数据连接的处理,父进程进行命令行的处理.

如果你要问为什么要分成两个进程来分别进行命令和数据连接的处理,我只能告诉你,我是参考了别人的实现方式,我们其实也可以换成多线程的方式,这是后话.

CmdHandle

我的设计当中,CmdHandle类用于处理对方发过来的命令,所以这个类里面有一堆的命令处理函数,我挺讨厌将一个类设计得如此繁杂.

  1. class CmdHandle : boost::noncopyable /* 这个类主要用于处理ftp的命令 */
  2. {
  3. public:
  4. typedef boost::function<void()> Handler;
  5. typedef std::map<std::string, Handler> Router;
  6. typedef boost::shared_ptr<Conn> Connection;
  7. CmdHandle(int cmdfd, int commufd);
  8. ~CmdHandle() {}
  9. public:
  10. void Handle();
  11. private:
  12. enum Mode {
  13. ascii, /* ascii文本格式 */
  14. binary, /* 二进制格式 */
  15. };
  16. Mode mode_;
  17. enum State {
  18. error, /* 出错 */
  19. success, /* 成功 */
  20. readerror,
  21. writeerror,
  22. };
  23. private:
  24. void FEAT();
  25. void SYST(); /* 操作系统类型 */
  26. void PWD(); /* 当前目录 */
  27. void OPTS(); /* 用于调整一些选项 */
  28. void CWD();
  29. void PASV();
  30. void LIST(); /* 获得文件信息 */
  31. void PORT();
  32. void CDUP(); /* 进入上一个文件夹 */
  33. void SIZE(); /* 获取文件的大小 */
  34. void RETR(); /* 获取某个文件 */
  35. void TYPE(); /* 获取啥? */
  36. void STOR(); /* 上传文件 */
  37. void MKD(); /* 新建文件夹 */
  38. void DELE(); /* 删除某个文件 */
  39. void QUIT(); /* 关闭 */
  40. private:
  41. void Reply(const char *format, ...); /* 发送回复信息 */
  42. void GetLine(char* buf, size_t len);
  43. void Login(); /* 处理登陆信息 */
  44. State SendList(bool detail);
  45. Connection GetConnect();
  46. void Parsing(char* cmdLine);
  47. private:
  48. int cmdfd_; /* 此外,我们还需要一条管道 */
  49. int commufd_; /* 用于和另外一个进程进行交流 */
  50. sockaddr_in clnaddr_;
  51. bool passive_;
  52. Router router_; /* 实现命令到函数的映射 */
  53. std::string cmd_; /* 记录命令 */
  54. std::string argv_; /* 用来记录参数 */
  55. };

在构造函数中,实现了将命令和函数的一一绑定.

  1. CmdHandle::CmdHandle(int cmdfd, int commufd)
  2. : cmdfd_(cmdfd) /* 用于和客户端交流 */
  3. , commufd_(commufd) /* 用于和进程交流 */
  4. , passive_(true)
  5. {
  6. router_["FEAT"] = boost::bind(&CmdHandle::FEAT, this);
  7. .... /* 将命令和函数一一映射 */
  8. }

接下来的处理函数就没有什么好说的,如果大家想看怎么样来处理这些命令,可以翻看我的代码文件夹中带的rfc文档:

  1. void CmdHandle::Handle() { /* 全局唯一一个入口函数 */
  2. char buf[1024];
  3. Reply("220 Tiny Ftp Server v0.1\r\n");
  4. Login(); /* 首先要解决的是login的问题 */
  5. for (; ; ) {
  6. GetLine(buf, sizeof buf); /* 读取一行 */
  7. Parsing(buf); /* 解析命令 */
  8. boost::function<void()> &func = router_.at(cmd_); /* 找到函数 */
  9. if (func) {
  10. func();
  11. }
  12. else
  13. Reply("%d Unknown command.\r\n", FTP_BADCMD); /* 没有找到相对应的命令处理函数 */
  14. }
  15. }

接下来不过是对于命令的处理而已.具体的可以查看代码,因为确实没有什么好说的,非常简单.

处理连接

DataHandle这个类主要用来处理连接:

  1. #ifndef _DATA_HANDLE_H_
  2. #define _DATA_HANDLE_H_
  3. #include <boost/noncopyable.hpp>
  4. #include "csapp.h"
  5. class DataHandle : boost::noncopyable
  6. {
  7. public:
  8. DataHandle(int commufd)
  9. : sockfd_(-1)
  10. , commufd_(commufd)
  11. {}
  12. ~DataHandle() {
  13. if (sockfd_ != -1)
  14. utility::Close(sockfd_);
  15. }
  16. public:
  17. void Handle();
  18. private:
  19. void Accept();
  20. void PasvListen();
  21. void PosiListen();
  22. private:
  23. int sockfd_; /* 这个用于和客户端数据的交互 */
  24. int commufd_; /* 一条管道用于进程间的交流 */
  25. };
  26. #endif

这个类主要就是在循环中等待,等待命令的到来,然后执行命令,将结果发送给命令的发送者,直到永远:

  1. void DataHandle::Handle() {
  2. for (; ; ) { /* 暂时什么事情也不干 */
  3. CMD cmd;
  4. readn(commufd_, &cmd, sizeof(cmd));
  5. switch (cmd) {
  6. case kExpectPort:
  7. printf("recv kExpectPort!\n");
  8. PasvListen(); /* 被动监听 */
  9. break;
  10. case kExpectFd:
  11. printf("recv kExpectFd!\n");
  12. Accept();
  13. break;
  14. case kExpectConn:
  15. printf("recv kExpectConn!\n"); /* 期待值连接的到来 */
  16. PosiListen(); /* 主动监听 */
  17. default:
  18. break;
  19. }
  20. }
  21. }

当然,这个命令的发送者正是CmdHandle.

ftp协议

ftp只是一个很简单的协议,具体的协议细节大家可以去翻看我的代码文件夹中提供的rfc.我这里稍微讲一下吧.

ftp的命令都是以\r\n来结尾的.客户端和服务端的通信过程大抵是这样的,客户端每发送一个请求,服务端都要回复一个状态,要么成功(2xx),要么失败(4xx),这样客户端才能进行下一个操作.

这些东西的处理其实和我要练手的东西没有多么大的关系.

最后附上项目地址: https://github.com/lishuhuakai/miniftp

项目里大量使用了cpp11的新特性,也是练手而已.

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注