在实现了HTTPserver之后。本人打算再实现一个FTPserver。

因为FTP协议与HTTP一样都位于应用层,所以实现原理也类似。

在这里把实现的原理和源代码分享给大家。

首先须要明白的是FTP协议中涉及命令port和数据port,即每一个client通过命令port向server发送命令(切换文件夹、删除文件等),通过数据port从server接收数据(文件夹列表、下载上传文件等)。这就要求对每一个连接都必须同一时候维护两个port,假设使用类似于上一篇文章中的多路IO就会复杂非常多,因此本文採用了类似Apache的多进程机制,即对每一个连接创建一个单独的进程进行管理。

接下来简要说明一下FTP协议的通信流程。Ftpserver向client发送的消息主要由两部分组成。第一部分是状态码(与HTTP类似),第二部分是详细内容(能够为空)。两部分之间以空格分隔,如“220 TS FTP Server ready”就告诉了client已经连接上了server;client向server发送的命令也由两部分组成。第一部分是命令字符串。第二部分是详细内容(能够为空),两部分之间也以空格分隔。如“USER anonymous”就指定了登录FTPserver的username。以一个登录FTPserver并获取文件夹列表的流程为例:

220 TS FTP Server ready...
USER anonymous
331 Password required for anonymous
PASS chrome@example.com
530 Not logged in,password error.
QUIT
221 Goodbye
USER zhaoxy
331 Password required for zhaoxy
PASS 123
230 User zhaoxy logged in
SYST
215 UNIX Type: L8
PWD
257 "/" is current directory.
TYPE I
200 Type set to I
PASV
227 Entering Passive Mode (127,0,0,1,212,54)
SIZE /
550 File not found
PASV
227 Entering Passive Mode (127,0,0,1,212,56)
CWD /
250 CWD command successful. "/" is current directory.
LIST -l
150 Opening data channel for directory list.
16877 8 501 20 272 4 8 114 .
16877 29 501 20 986 4 8 114 ..
33188 1 501 20 6148 3 28 114 .DS_Store
16877 4 501 20 136 2 27 114 css
33279 1 501 20 129639543 6 14 113 haha.pdf
16877 11 501 20 374 2 27 114 images
33261 1 501 20 11930 3 9 114 index.html
16877 6 501 20 204 2 28 114 js
226 Transfer ok.
QUIT
221 Goodbye

在一个client连接到server后,首先server要向client发送欢迎信息220,client依此向server发送username和password,server校验之后假设失败则返回530,成功则返回230。一般全部的client第一次连接server都会尝试用匿名用户进行登录。登录失败再向用户询问username和password。接下来,client会与server确认文件系统的类型,查询当前文件夹以及设定传输的数据格式。

FTP协议中主要有两种格式,二进制和ASCII码,两种格式的主要差别在于换行。二进制格式不会对数据进行不论什么处理,而ASCII码格式会将回车换行转换为本机的回车字符。比方Unix下是\n,Windows下是\r\n,Mac下是\r。一般图片和运行文件必须用二进制格式。CGI脚本和普通HTML文件必须用ASCII码格式。

在确定了传输格式之后,client会设定传输模式,Passive被动模式或Active主动模式。在被动模式下,server会再创建一个套接字绑定到一个空暇port上并開始监听,同一时候将本机ip和port号(h1,h2,h3,h4,p1,p2,当中p1*256+p2等于port号)发送到client。

当之后须要数据传输的时候。server会通过150状态码通知client。client收到之后会连接到之前指定的port并等待数据。

传输完毕之后,server会发送226状态码告诉client传输成功。

假设client不须要保持长连接的话,此时能够向server发送QUIT命令断开连接。在主动模式下。流程与被动模式类似。仅仅是套接字由client创建并监听,server连接到client的port上进行数据传输。

下面是main函数中的代码:

#include <iostream>
#include "define.h"
#include "CFtpHandler.h"
#include <sys/types.h>
#include <sys/socket.h> int main(int argc, const char * argv[])
{
int port = 2100;
int listenFd = startup(port);
//ignore SIGCHLD signal, which created by child process when exit, to avoid zombie process
signal(SIGCHLD,SIG_IGN);
while (1) {
int newFd = accept(listenFd, (struct sockaddr *)NULL, NULL);
if (newFd == -1) {
//when child process exit, it'll generate a signal which will cause the parent process accept failed.
//If happens, continue.
if (errno == EINTR) continue;
printf("accept error: %s(errno: %d)\n",strerror(errno),errno);
}
//timeout of recv
struct timeval timeout = {3,0};
setsockopt(newFd, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout,sizeof(struct timeval));
int pid = fork();
//fork error
if (pid < 0) {
printf("fork error: %s(errno: %d)\n",strerror(errno),errno);
}
//child process
else if (pid == 0) {
//close useless socket
close(listenFd);
send(newFd, TS_FTP_STATUS_READY, strlen(TS_FTP_STATUS_READY), 0);
CFtpHandler handler(newFd);
int freeTime = 0;
while (1) {
char buff[256];
int len = (int)recv(newFd, buff, sizeof(buff), 0);
//connection interruption
if (len == 0) break;
//recv timeout return -1
if (len < 0) {
freeTime += 3;
//max waiting time exceed
if (freeTime >= 30) {
break;
}else {
continue;
}
}
buff[len] = '\0';
//reset free time
freeTime = 0;
if (handler.handleRequest(buff)) {
break;
}
}
close(newFd);
std::cout<<"exit"<<std::endl;
exit(0);
}
//parent process
else {
//close useless socket
close(newFd);
}
}
close(listenFd);
return 0;
}

代码中先创建了套接字并绑定到指定port上,然后进入循环開始监听port。

每监听到一个新的连接就fork出一个子进程。子进程向client发送欢迎信息后进入循环处理client发送过来的命令,直到收到QUIT命令或者连接超时退出循环。以上代码中须要注意三个地方。一是子进程在退出之后会向父进程发送SIGCHLD信号。假设父进程不进行处理(调用wait或忽略)就会导致子进程变为僵尸进程,本文中採用的是忽略的方式;二是accept函数在父进程收到信号时会直接返回。因此须要推断假设返回是因为信号则继续循环,不fork,否则会无限创建子进程;三是在fork之后须要将不使用的套接字关闭,比方父进程须要关闭新的连接套接字,而子进程须要关闭监听套接字。避免套接字无法全然关闭。

最后通过CFtpHandler类中的handleRequest方法处理client的命令,部分代码例如以下:

//handle client request
bool CFtpHandler::handleRequest(char *buff) {
stringstream recvStream;
recvStream<<buff; cout<<buff;
string command;
recvStream>>command; bool isClose = false;
string msg;
//username
if (command == COMMAND_USER) {
recvStream>>username;
msg = TS_FTP_STATUS_PWD_REQ(username);
}
//password
else if (command == COMMAND_PASS) {
recvStream>>password;
if (username == "zhaoxy" && password == "123") {
msg = TS_FTP_STATUS_LOG_IN(username);
}else {
msg = TS_FTP_STATUS_PWD_ERROR;
}
}
//quit
else if (command == COMMAND_QUIT) {
msg = TS_FTP_STATUS_BYE;
isClose = true;
}
//system type
else if (command == COMMAND_SYST) {
msg = TS_FTP_STATUS_SYSTEM_TYPE;
}
//current directory
else if (command == COMMAND_PWD) {
msg = TS_FTP_STATUS_CUR_DIR(currentPath);
}
//transmit type
else if (command == COMMAND_TYPE) {
recvStream>>type;
msg = TS_FTP_STATUS_TRAN_TYPE(type);
}
//passive mode
else if (command == COMMAND_PASSIVE) {
int port = 0;
if (m_dataFd) {
close(m_dataFd);
}
m_dataFd = startup(port); stringstream stream;
stream<<TS_FTP_STATUS_PASV<<port/256<<","<<port%256<<")";
msg = stream.str(); //active passive mode
m_isPassive = true;
}
//active mode
else if (command == COMMAND_PORT) {
string ipStr;
recvStream>>ipStr; char ipC[32];
strcpy(ipC, ipStr.c_str());
char *ext = strtok(ipC, ",");
m_clientPort = 0; m_clientIp = 0;
m_clientIp = atoi(ext);
int count = 0;
//convert string to ip address and port number
//be careful, the ip should be network endianness
while (1) {
if ((ext = strtok(NULL, ","))==NULL) {
break;
}
switch (++count) {
case 1:
case 2:
case 3:
m_clientIp |= atoi(ext)<<(count*8);
break;
case 4:
m_clientPort += atoi(ext)*256;
break;
case 5:
m_clientPort += atoi(ext);
break;
default:
break;
}
}
msg = TS_FTP_STATUS_PORT_SUCCESS;
}
//file size
else if (command == COMMAND_SIZE) {
recvStream>>fileName;
string filePath = ROOT_PATH+currentPath+fileName;
long fileSize = filesize(filePath.c_str());
if (fileSize) {
stringstream stream;
stream<<TS_FTP_STATUS_FILE_SIZE<<fileSize;
msg = stream.str();
}else {
msg = TS_FTP_STATUS_FILE_NOT_FOUND;
}
}
//change directory
else if (command == COMMAND_CWD) {
string tmpPath;
recvStream>>tmpPath;
string dirPath = ROOT_PATH+tmpPath;
if (isDirectory(dirPath.c_str())) {
currentPath = tmpPath;
msg = TS_FTP_STATUS_CWD_SUCCESS(currentPath);
}else {
msg = TS_FTP_STATUS_CWD_FAILED(currentPath);
}
}
//show file list
else if (command == COMMAND_LIST || command == COMMAND_MLSD) {
string param;
recvStream>>param; msg = TS_FTP_STATUS_OPEN_DATA_CHANNEL;
sendResponse(m_connFd, msg);
int newFd = getDataSocket();
//get files in directory
string dirPath = ROOT_PATH+currentPath;
DIR *dir = opendir(dirPath.c_str());
struct dirent *ent;
struct stat s;
stringstream stream;
while ((ent = readdir(dir))!=NULL) {
string filePath = dirPath + ent->d_name;
stat(filePath.c_str(), &s);
struct tm tm = *gmtime(&s.st_mtime);
//list with -l param
if (param == "-l") {
stream<<s.st_mode<<" "<<s.st_nlink<<" "<<s.st_uid<<" "<<s.st_gid<<" "<<setw(10)<<s.st_size<<" "<<tm.tm_mon<<" "<<tm.tm_mday<<" "<<tm.tm_year<<" "<<ent->d_name<<endl;
}else {
stream<<ent->d_name<<endl;
}
}
closedir(dir);
//send file info
string fileInfo = stream.str();
cout<<fileInfo;
send(newFd, fileInfo.c_str(), fileInfo.size(), 0);
//close client
close(newFd);
//send transfer ok
msg = TS_FTP_STATUS_TRANSFER_OK;
}
//send file
else if (command == COMMAND_RETRIEVE) {
recvStream>>fileName;
msg = TS_FTP_STATUS_TRANSFER_START(fileName);
sendResponse(m_connFd, msg);
int newFd = getDataSocket();
//send file
std::ifstream file(ROOT_PATH+currentPath+fileName);
file.seekg(0, std::ifstream::beg);
while(file.tellg() != -1)
{
char *p = new char[1024];
bzero(p, 1024);
file.read(p, 1024);
int n = (int)send(newFd, p, 1024, 0);
if (n < 0) {
cout<<"ERROR writing to socket"<<endl;
break;
}
delete p;
}
file.close();
//close client
close(newFd);
//send transfer ok
msg = TS_FTP_STATUS_FILE_SENT;
}
//receive file
else if (command == COMMAND_STORE) {
recvStream>>fileName;
msg = TS_FTP_STATUS_UPLOAD_START;
sendResponse(m_connFd, msg);
int newFd = getDataSocket();
//receive file
ofstream file;
file.open(ROOT_PATH+currentPath+fileName, ios::out | ios::binary);
char buff[1024];
while (1) {
int n = (int)recv(newFd, buff, sizeof(buff), 0);
if (n<=0) break;
file.write(buff, n);
}
file.close();
//close client
close(newFd);
//send transfer ok
msg = TS_FTP_STATUS_FILE_RECEIVE;
}
//get support command
else if (command == COMMAND_FEAT) {
stringstream stream;
stream<<"211-Extension supported"<<endl;
stream<<COMMAND_SIZE<<endl;
stream<<"211 End"<<endl;;
msg = stream.str();
}
//get parent directory
else if (command == COMMAND_CDUP) {
if (currentPath != "/") {
char path[256];
strcpy(path, currentPath.c_str());
char *ext = strtok(path, "/");
char *lastExt = ext;
while (ext!=NULL) {
ext = strtok(NULL, "/");
if (ext) lastExt = ext;
}
currentPath = currentPath.substr(0, currentPath.length()-strlen(lastExt)-1);
}
msg = TS_FTP_STATUS_CDUP(currentPath);
}
//delete file
else if (command == COMMAND_DELETE) {
recvStream>>fileName;
//delete file
if (remove((ROOT_PATH+currentPath+fileName).c_str()) == 0) {
msg = TS_FTP_STATUS_DELETE;
}else {
printf("delete error: %s(errno: %d)\n",strerror(errno),errno);
msg = TS_FTP_STATUS_DELETE_FAILED;
}
}
//other
else if (command == COMMAND_NOOP || command == COMMAND_OPTS){
msg = TS_FTP_STATUS_OK;
} sendResponse(m_connFd, msg);
return isClose;
}

以上代码针对每种命令进行了不同的处理,在这里不具体说明。须要注意的是。文中採用的if-else方法推断命令效率是非常低的。时间复杂度为O(n)(n为命令总数),有两种方法能够进行优化。一是因为FTP命令都是4个字母组成的。能够将4个字母的ascii码拼接成一个整数,使用switch进行推断。时间复杂度为O(1);二是类似Httpserver中的方法,将每一个命令以及对应的处理函数存到hashmap中,收到一个命令时能够通过hash直接调用对应的函数,时间复杂度相同为O(1)。

另外,以上代码中的PORT命令处理时涉及对ip地址的解析。须要注意本机字节顺序和网络字节顺序的差别。如127.0.0.1转换成整数应逆序转换,以网络字节顺序存到s_addr变量中。

以上源代码已经上传到GitHub中,感兴趣的朋友能够前往下载

假设大家认为对自己有帮助的话,还希望能帮顶一下,谢谢:)
转载请注明出处。谢谢!

应用层协议实现系列(三)——FTPserver之设计与实现的更多相关文章

  1. 应用层协议实现系列(一)——HTTPserver之仿nginx多进程和多路IO的实现

    近期在尝试自己写一个Httpserver,在粗略研究了nginx的代码之后,决定仿照nginx中的部分设计自己实现一个高并发的HTTPserver,在这里分享给大家. 眼下使用的较多的Httpserv ...

  2. C#进阶系列——DDD领域驱动设计初探(三):仓储Repository(下)

    前言:上篇介绍了下仓储的代码架构示例以及简单分析了仓储了使用优势.本章还是继续来完善下仓储的设计.上章说了,仓储的最主要作用的分离领域层和具体的技术架构,使得领域层更加专注领域逻辑.那么涉及到具体的实 ...

  3. 应用层协议系列(两)——HTTPserver之http协议分析

    上一篇文章<抄nginx Httpserver设计与实现(一)--多进程和多通道IO现>中实现了一个仿照nginx的支持高并发的server.但仅仅是实现了port监听和数据接收.并没有实 ...

  4. C#进阶系列——DDD领域驱动设计初探(四):WCF搭建

    前言:前面三篇分享了下DDD里面的两个主要特性:聚合和仓储.领域层的搭建基本完成,当然还涉及到领域事件和领域服务的部分,后面再项目搭建的过程中慢慢引入,博主的思路是先将整个架构走通,然后一步一步来添加 ...

  5. RTSP RTSP(Real Time Streaming Protocol),RFC2326,实时流传输协议,是TCP/IP协议体系中的一个应用层协议

    RTSP 编辑 RTSP(Real Time Streaming Protocol),RFC2326,实时流传输协议,是TCP/IP协议体系中的一个应用层协议,由哥伦比亚大学.网景和RealNetwo ...

  6. http协议学习系列

    深入理解HTTP协议(转)  http://www.blogjava.net/zjusuyong/articles/304788.html http协议学习系列   1. 基础概念篇 1.1 介绍 H ...

  7. http协议学习系列(一个博文链接)

    深入理解HTTP协议(转) http协议学习系列(转自:http://www.blogjava.net/zjusuyong/articles/304788.html) 1. 基础概念篇 1.1 介绍 ...

  8. 应用层协议:HTTPS

    1. HTTPS定义 Hyper Text Transfer Protocol over Secure Socket Layer,安全的超文本传输协议,网景公式设计了SSL(Secure Socket ...

  9. C#进阶系列——DDD领域驱动设计初探(一):聚合

    前言:又有差不多半个月没写点什么了,感觉这样很对不起自己似的.今天看到一篇博文里面写道:越是忙人越有时间写博客.呵呵,似乎有点道理,博主为了证明自己也是忙人,这不就来学习下DDD这么一个听上去高大上的 ...

随机推荐

  1. Cannot use isset() on the result of an expression (you can use "null !== expression" instead)

    if (isset($array[2])){ 抛出错误  Cannot use isset() on the result of an expression (you can use "nu ...

  2. [NOI.AC#30]candy 贪心

    链接 一个直观的想法是,枚举最小的是谁,然后二分找到另外一个序列对应位置更新答案,复杂度 \(O(NlogN)\) 实际上不需要二分,因为每次当最大的变大之后,原来不行的最小值现在也一定不行,指针移动 ...

  3. jqXHR对象

    //$.ajax()返回的对象就是jqXHR对象 var jqXHR = $.ajax({ type:'post', url:'test.php', data:$('form').serialize( ...

  4. iPad之Linux平台实践

    updata.... 本文出自 "李晨光原创技术博客" 博客,谢绝转载!

  5. CSS min-height不能解决垂直外边距合并问题

    垂直外边距合并有一种情况是嵌套元素的垂直外边距合并,当父级元素没有设定外边距时,在顶部或者底部边缘的子元素的垂直外边距就会和父级的合并,导致父级也有了“隐形”的垂直外边距. 当父级元素的min-hei ...

  6. virtualtemplate 接口

    虚拟接口的配置.建立.与实际接口的关联 VPN在会话连接建立之后.须要创建一个虚拟接口用于和对端之间数据传输.此时,将依照用户配置,选择一个虚拟接口模板,动态地创建一个虚拟接口. 该接口将在会话结束时 ...

  7. [Node] Stateful Session Management for login, logout and signup

    Stateful session management: Store session which associate with user, and store in the menory on ser ...

  8. CentOS搭建xfce桌面+VNC教程

    CentOS搭建xfce桌面+VNC教程 Linux的安全与性能向来为开发者所称道,你可以轻松地在搜索引擎中找到各种Linux优越性的说辞,其中不乏Linux的激进者.特别是当你步入VPS领域,更多地 ...

  9. 不安装谷歌市场,下载谷歌市场中的APK

    不安装谷歌市场,下载谷歌市场中的APK GooglePlayStore 是谷歌官方的的应用市场,有的时候还是需要从谷歌市场下载APK文件.国内的安卓手机厂商都不自带GooglePlay,甚至一些手机& ...

  10. amazeui学习笔记一(开始使用3)--兼容性列表compatibility

    amazeui学习笔记一(开始使用3)--兼容性列表compatibility 一.总结 1.不要用ie做前端测试,不要碰ie,尽量用google 浏览器: 按照微软官方的说法,IE 开发者工具中的浏 ...