第四章:基于TCP/IP的文件传输子系统
4.1基础知识学习
计算机网络基本知识。
掌握scoket通信流程,熟悉常用函数。
4.2封装socket常用函数
当使用TCP协议传输数据时,数据被划分为多个TCP报文段进行传输,而TCP协议并没有记录每个报文段的边界。因此,在发送端和接收端之间,可能会出现多个报文段合并为一个数据包(粘包)或一个报文段被拆分成多个数据包(分包)的情况。例如:
粘包:发送方发送了两个字符串,例如"hello"和"world",接收方却一次性接收到了"helloword"。
分包:发送方发送了一个字符串"helloworld",接收方却接收到了两个字符串"hello"和"world"。
需要注意的是:粘包和分包不会影响报文的顺序,发送hello,接受到的不会是olleh。也不会在分割的包中插入其他的数据。发送helloworld,不会收到helloabcdworld。
详细信息参考:TCP粘包、拆包与通信协议详解 - 腾讯云开发者社区-腾讯云 (tencent.com)
在开发框架中,封装了原本的recv,send函数。一定程度上解决了粘包,分包问题。下面函数的实现思路是,在接收数据时循环调用recv()函数,每次接收固定长度的数据,直到接收到需要读取的字节数为止。这样做可以将每个数据包都单独处理,避免将多个数据包合并为一个导致粘包,也可以避免一个数据包被分割成多个导致分包。发送同理。
但需要注意的是,如果在发送端发送的多个数据包之间时间间隔很短,这个函数也不能避免粘包的情况。此外,如果数据包的长度不是固定的,仍然可能出现粘包和分包的情况。因此可以在应用层上设计一种数据传输协议,用于在数据包之间进行分隔和标识。这些协议可以使用特殊的标记符、长度字段或其他方式来确保数据包的完整性和独立性。例如,可以在数据包中添加包头和包尾,或者在数据包前面添加长度信息等。接收方根据协议解析和处理数据包,以确保每个数据包都能够正确地被处理。
封装后的读取数据函数:
// 接收socket的对端发送过来的数据。
// sockfd:可用的socket连接。
// buffer:接收数据缓冲区的地址。
// ibuflen:本次成功接收数据的字节数。
// itimeout:接收等待超时的时间,单位:秒,-1-不等待;0-无限等待;>0-等待的秒数。
// 返回值:true-成功;false-失败,失败有两种情况:1)等待超时;2)socket连接已不可用。
bool TcpRead(const int sockfd, char* buffer, int* ibuflen, const int itimeout)
{
if (sockfd == -1) return false;
// 如果itimeout>0,表示需要等待itimeout秒,如果itimeout秒后还没有数据到达,返回false。
if (itimeout > 0)
{
struct pollfd fds;
fds.fd = sockfd;
fds.events = POLLIN;
if (poll(&fds, 1, itimeout * 1000) <= 0) return false;
}
//首先创建一个pollfd结构体fds,并将sockfd赋值给它的fd字段,表示要查询的文件描述符为sockfd。
//然后将POLLIN赋值给events字段,表示要查询的事件为可读事件。
//接下来调用poll()函数查询sockfd是否有可读事件发生,如果查询超时或没有事件发生,则返回false。
// 如果itimeout==-1,表示不等待,立即判断socket的缓冲区中是否有数据,如果没有,返回false。
if (itimeout == -1)
{
struct pollfd fds;
fds.fd = sockfd;
fds.events = POLLIN;
if (poll(&fds, 1, 0) <= 0) return false;
}
(*ibuflen) = 0; // 报文长度变量初始化为0。
// 先读取报文长度,4个字节。
if (Readn(sockfd, (char*)ibuflen, 4) == false) return false;
(*ibuflen) = ntohl(*ibuflen); // 把报文长度由网络字节序转换为主机字节序。
// 再读取报文内容。
if (Readn(sockfd, buffer, (*ibuflen)) == false) return false;
return true;
}
// 从已经准备好的socket中读取数据。
// sockfd:已经准备好的socket连接。
// buffer:接收数据缓冲区的地址。
// n:本次接收数据的字节数。
// 返回值:成功接收到n字节的数据后返回true,socket连接不可用返回false。
bool Readn(const int sockfd, char* buffer, const size_t n)
{
int nLeft = n; // 剩余需要读取的字节数。
int idx = 0; // 已成功读取的字节数。
int nread; // 每次调用recv()函数读到的字节数。
while (nLeft > 0)
{
if ((nread = recv(sockfd, buffer + idx, nLeft, 0)) <= 0) return false;
idx = idx + nread;
nLeft = nLeft - nread;
}
return true;
}
封装后发送数据函数:
// 向socket的对端发送数据。
// sockfd:可用的socket连接。
// buffer:待发送数据缓冲区的地址。
// ibuflen:待发送数据的字节数,如果发送的是ascii字符串,ibuflen填0或字符串的长度,
// 如果是二进制流数据,ibuflen为二进制数据块的大小。
// 返回值:true-成功;false-失败,如果失败,表示socket连接已不可用。
bool TcpWrite(const int sockfd, const char* buffer, const int ibuflen)
{
if (sockfd == -1) return false;
int ilen = 0; // 报文长度。
// 如果ibuflen==0,就认为需要发送的是字符串,报文长度为字符串的长度。
if (ibuflen == 0) ilen = strlen(buffer);
else ilen = ibuflen;
int ilenn = htonl(ilen); // 把报文长度转换为网络字节序。
char TBuffer[ilen + 4]; // 发送缓冲区。
memset(TBuffer, 0, sizeof(TBuffer)); // 清区发送缓冲区。
memcpy(TBuffer, &ilenn, 4); // 把报文长度拷贝到缓冲区。
memcpy(TBuffer + 4, buffer, ilen); // 把报文内容拷贝到缓冲区。
// 发送缓冲区中的数据。
if (Writen(sockfd, TBuffer, ilen + 4) == false) return false;
return true;
}
// 向已经准备好的socket中写入数据。
// sockfd:已经准备好的socket连接。
// buffer:待发送数据缓冲区的地址。
// n:待发送数据的字节数。
// 返回值:成功发送完n字节的数据后返回true,socket连接不可用返回false。
bool Writen(const int sockfd, const char* buffer, const size_t n)
{
int nLeft = n; // 剩余需要写入的字节数。
int idx = 0; // 已成功写入的字节数。
int nwritten; // 每次调用send()函数写入的字节数。
while (nLeft > 0)
{
if ((nwritten = send(sockfd, buffer + idx, nLeft, 0)) <= 0) return false;
nLeft = nLeft - nwritten;
idx = idx + nwritten;
}
return true;
}
解决粘包和分包问题后,使用封装后的函数进行简单的试验学习。服务端的程序如下:
#include "_public.h"
CTcpServer TcpServer; //封装了server端常用函数
CLogFile logfile; //日志类
void FathEXIT(int sig); //父进程退出的处理函数
void ChldEXIT(int sig); //子进程退出的处理函数
int main(int argc,char *argv[])
{
//打印帮助信息
if(argc!=3)
{
printf("Using: ./server port logfile\nExample: ./server 5005 /tmp/server.log\n\n");
return -1;
}
//关闭所有IO和信号的目的是为了确保后台程序不会受到终端的影响,以及避免与其他进程产生冲突。然后父进程重新启用退出的信号。
CloseIOAndSignal(); signal(SIGINT,FathEXIT); signal(SIGTERM,FathEXIT);
//创建日志文件
if(logfile.Open(argv[2],"a+")==false) { printf("logfile.Open(%s) failed.\n",argv[2]); return -1; }
//服务端初始化
if(TcpServer.InitServer(atoi(argv[1]))==false)
{
logfile.Write("TcpServer.InitServer(%s) failed \n",argv[1]); return -1;
}
while(1)
{
//等待客户端的连接请求
if(TcpServer.Accept()==false)
{
logfile.Write("TcpServer.Accept() failed.\n"); FathEXIT(-1);
}
logfile.Write("客户端(%s) 已连接.\n",TcpServer.GetIP());
//当成功建立一个连接时,创建出一个子进程。
if(fork()>0) { TcpServer.CloseClient(); continue; } //父进程关闭用于通信的文件描述符,接着回到Accept继续等待新的连接请求。
//以下为子进程执行的语句。
signal(SIGINT,ChldEXIT); signal(SIGTERM,ChldEXIT); //子进程重新启用退出的信号
//子进程处理通信
TcpServer.CloseListen(); //子进程可以关闭用于监听的文件描述符
char buffer[102400];
while(1)
{
memset(buffer,0,sizeof(buffer));
if(TcpServer.Read(buffer)==false) break;
logfile.Write("接收: %s\n",buffer);
strcpy(buffer,"ok");
if(TcpServer.Write(buffer)==false)
{ perror("send"); break; }
logfile.Write("发送: %s\n",buffer);
}
ChldEXIT(-1);
}
return 0;
}
void FathEXIT(int sig)
{
//防止在信号处理函数在执行的过程中被信号中断
signal(SIGINT,SIG_IGN); signal(SIGTERM,SIG_IGN);
logfile.Write("父进程退出.sig= %d\n",sig);
TcpServer.CloseListen();//关闭监听
kill(0,15); //通知全部的子进程退出
exit(0);
}
void ChldEXIT(int sig)
{
//防止在信号处理函数在执行的过程中被信号中断
signal(SIGINT,SIG_IGN); signal(SIGTERM,SIG_IGN);
logfile.Write("子进程退出.sig= %d",sig);
TcpServer.CloseClient();//关闭通信
exit(0);
}
客户端的程序如下:
#include "_public.h"
int main(int argc,char *argv[])
{
//帮助信息
if(argc!=3)
{
printf("Using: ./client ip port\n");
printf("Example: ./client 127.0.0.1 5005");
return -1;
}
//向服务器发起连接请求
CTcpClient TcpClient;
if(TcpClient.ConnectToServer(argv[1],atoi(argv[2]))==false)
{
printf("TcpClient.ConnectToServer(%s,%s) failed",argv[1],argv[2]);
return -1;
}
//与服务器进行通讯,发送一个报文后等待回复,然后再发送下一个报文。
char buffer[102400];
for(int ii=0;ii<1000;ii++)
{
SPRINTF(buffer,sizeof(buffer),"这是第%d条信息,编号%03d。\n",ii+1,ii+1);
if(TcpClient.Write(buffer)==false) break; //向服务端发送请求报文
printf("发送: %s\n",buffer);
memset(buffer,0,sizeof(buffer));
if(TcpClient.Read(buffer)==false) break; //接收服务端的报文
printf("接收: %s",buffer);
sleep(1);
}
return 0;
}
测试结果:
//先运行服务端
[sxixia@localhost c_project]$ ./server 5005 /tmp/server.log
//然后运行客户端
[sxixia@localhost c_project]$ ./client 127.0.0.1 5005
发送: 这是第1条信息,编号001。
接收: ok发送: 这是第2条信息,编号002。
接收: ok发送: 这是第3条信息,编号003。
接收: ok发送: 这是第4条信息,编号004。
//查看日志内容
[sxixia@localhost c_project]$ tail -f /tmp/server.log
2023-04-23 16:37:57 客户端(127.0.0.1) 已连接.
2023-04-23 16:37:57 接收: 这是第1个超女,编号001。
2023-04-23 16:37:57 发送: ok
2023-04-23 16:37:58 接收: 这是第2条信息,编号002。
...
//使用ctrl+c或者kill命令杀死进程,可以看到日志中记录了进程退出的信息。
[sxixia@localhost c_project]$ killall server
...
2023-04-23 16:38:18 发送: ok
2023-04-23 16:38:19 接收: 这是第23条信息,编号023。
2023-04-23 16:38:19 发送: ok
2023-04-23 16:38:19 父进程退出.sig= 15
2023-04-23 16:38:19 子进程退出.sig= 15
在本测试程序中,使用封装好的类,搭建了一个多进程的socket网络编程模型,并且一定程度上解决了粘包和分包的问题。经过测试,客户端程序中不进行延时1秒,一次性发送接收1000条信息,也没有发生粘包,分包问题。同时,在服务端的函数中,我们关闭了父进程的用于通信的文件描述符,关闭了子进程用于监听的文件描述符。在一定程度上减少了资源的消耗。最后,处理了进程的退出函数。
4.3TCP的短/长连接和心跳机制
TCP短连接和长连接分别是什么?TCP短连接和长连接是两种不同的连接方式,主要区别在于连接建立的生命周期和连接的复用方式。
短连接是指每次通信时建立一次连接,数据传输完成后立即断开连接。这种连接方式的优点是简单,不需要考虑长时间保持连接的问题,但是建立和断开连接的过程都需要消耗资源和时间,如果频繁建立和断开连接会造成较大的开销。
长连接是指在一定时间内保持连接不断开,可以多次使用同一个连接进行数据传输。这种连接方式的优点是可以避免频繁建立和断开连接的开销,提高了传输效率,但是需要考虑连接保持的时间和容错机制,避免连接因为网络故障或其他原因导致中断。
选择TCP短连接或长连接需要根据具体的业务场景和需求进行评估和选择,一般来说,对于频繁交互的短消息或数据,可以采用短连接;对于大量数据传输或需要保持长时间连接的应用,可以采用长连接。
那么如何管理TCP长连接呢?我们可以采用心跳机制来进行管理。
心跳机制:为了保证连接的有效性,可以在长连接中实现心跳机制,定期发送心跳消息以保持连接。当接收方在一定时间内未收到心跳消息时,可以认为连接已经断开,并及时进行重连
例如服务端与客户端约定一个超时时间为30秒,如果在30秒后服务端没有收到客户端的任何报文,则认为客户端发生异常,需要进行处理。如果在30内没有发生任何动作,客户端应该向服务端发送自己的心跳报文。
在实际生活中,在同一网段内部,网络设备不会断开空闲的TCP连接。在不同的网段之间,网络设备会断开空闲的TCP连接,超时时间一般为1-5分钟。
4.4开发基于TCP协议的文件传输系统
本节有三个模块需要完成。
1.文件传输的服务端模块(支持下载和上传)
2.文件下载的客户端模块
3.文件上传的客户端模块
为什么要将客户端的下载和上传功能拆分为两个模块呢?是为了程序设计的方便。以FTP的下载和上传客户端程序为例,两者的运行参数非常多并且不相同。如果将两个功能写在一起,程序是十分复杂的。第二就是下载和上传可能略有不同。对于下载,由于客户端通常只需要向服务器发送一个请求,然后等待服务器返回响应,因此使用TCP短连接比较常见。在下载完成后,客户端关闭连接即可。对于上传,由于需要不断地向服务器发送数据,因此使用TCP长连接比较常见。客户端可以将数据分批次发送,每次发送完成后不关闭连接,保持连接状态。这样可以避免重复建立和关闭连接,提高传输效率。当所有数据传输完成后,客户端再关闭连接。
下面的代码是使用tcp的客户端下载模块,客户端上传模块。服务端收发模块的完整实现。代码较长,我主要给出核心代码。
对于客户端的上传模块:
先看主流程。如果运行参数数量不正确,会打印出帮助信息,然后结束程序。帮助信息写在了_help()函数里。帮助信息给出了运行示例。第一个是文件上传以后删除本地文件的示例,第二个是文件上传以后备份本地文件的示例。_help()函数中给出了运行参数的含义。当程序正确运行后,第一时间关闭信号和输入输出,接着打开日志文件,并且解析运行参数的第三项,即argv[2]。并将解析结果填入程序运行参数结构体中。然后将程序的心跳信息写入共享内容,那么准备工作就告一段落。
/*
* 程序名:tcpputfiles.cpp,采用tcp协议,实现文件上传的客户端。
* 作者:sxixia
*/
#include "_public.h"
// 程序运行的参数结构体。
struct st_arg
{
int clienttype; // 客户端类型,1-上传文件;2-下载文件。
char ip[31]; // 服务端的IP地址。
int port; // 服务端的端口。
int ptype; // 文件上传成功后本地文件的处理方式:1-删除文件;2-移动到备份目录。
char clientpath[301]; // 本地文件存放的根目录。
char clientpathbak[301]; // 文件成功上传后,本地文件备份的根目录,当ptype==2时有效。
bool andchild; // 是否上传clientpath目录下各级子目录的文件,true-是;false-否。
char matchname[301]; // 待上传文件名的匹配规则,如"*.TXT,*.XML"。
char srvpath[301]; // 服务端文件存放的根目录。
int timetvl; // 扫描本地目录文件的时间间隔,单位:秒。
int timeout; // 进程心跳的超时时间。
char pname[51]; // 进程名,建议用"tcpputfiles_后缀"的方式。
} starg;
CLogFile logfile; //日志类
// 程序退出和信号2、15的处理函数。
void EXIT(int sig);
void _help();
// 把xml解析到参数starg结构中。
bool _xmltoarg(char *strxmlbuffer);
CTcpClient TcpClient;
bool Login(const char *argv); // 登录业务。
bool ActiveTest(); // 心跳。
char strrecvbuffer[1024]; // 发送报文的buffer。
char strsendbuffer[1024]; // 接收报文的buffer。
// 文件上传的主函数,执行一次文件上传的任务。
bool _tcpputfiles();
bool bcontinue=true; // 如果调用_tcpputfiles发送了文件,bcontinue为true,初始化为true。
// 把文件的内容发送给对端。
bool SendFile(const int sockfd,const char *filename,const int filesize);
// 删除或者转存本地的文件。
bool AckMessage(const char *strrecvbuffer);
CPActive PActive; // 进程心跳。
int main(int argc,char *argv[])
{
if (argc!=3) { _help(); return -1; }
// 关闭全部的信号和输入输出。
// 设置信号,在shell状态下可用 "kill + 进程号" 正常终止些进程。
// 但请不要用 "kill -9 +进程号" 强行终止。
CloseIOAndSignal(); signal(SIGINT,EXIT); signal(SIGTERM,EXIT);
// 打开日志文件。
if (logfile.Open(argv[1],"a+")==false)
{
printf("打开日志文件失败(%s)。\n",argv[1]); return -1;
}
// 解析xml,得到程序运行的参数。
if (_xmltoarg(argv[2])==false) return -1;
PActive.AddPInfo(starg.timeout,starg.pname); // 把进程的心跳信息写入共享内存。
// 向服务端发起连接请求。
if (TcpClient.ConnectToServer(starg.ip,starg.port)==false)
{
logfile.Write("TcpClient.ConnectToServer(%s,%d) failed.\n",starg.ip,starg.port); EXIT(-1);
}
// 登录业务。
if (Login(argv[2])==false) { logfile.Write("Login() failed.\n"); EXIT(-1); }
while (true)
{
// 调用文件上传的主函数,执行一次文件上传的任务。
if (_tcpputfiles()==false) { logfile.Write("_tcpputfiles() failed.\n"); EXIT(-1); }
if (bcontinue==false)
{
sleep(starg.timetvl);
if (ActiveTest()==false) break;
}
PActive.UptATime();
}
EXIT(0);
}
// 心跳。
bool ActiveTest()
{
memset(strsendbuffer,0,sizeof(strsendbuffer));
memset(strrecvbuffer,0,sizeof(strrecvbuffer));
SPRINTF(strsendbuffer,sizeof(strsendbuffer),"<activetest>ok</activetest>");
// logfile.Write("发送:%s\n",strsendbuffer);
if (TcpClient.Write(strsendbuffer)==false) return false; // 向服务端发送请求报文。
if (TcpClient.Read(strrecvbuffer,20)==false) return false; // 接收服务端的回应报文。
// logfile.Write("接收:%s\n",strrecvbuffer);
return true;
}
// 登录业务。
bool Login(const char *argv)
{
memset(strsendbuffer,0,sizeof(strsendbuffer));
memset(strrecvbuffer,0,sizeof(strrecvbuffer));
SPRINTF(strsendbuffer,sizeof(strsendbuffer),"%s<clienttype>1</clienttype>",argv);
logfile.Write("发送:%s\n",strsendbuffer);
if (TcpClient.Write(strsendbuffer)==false) return false; // 向服务端发送请求报文。
if (TcpClient.Read(strrecvbuffer,20)==false) return false; // 接收服务端的回应报文。
logfile.Write("接收:%s\n",strrecvbuffer);
logfile.Write("登录(%s:%d)成功。\n",starg.ip,starg.port);
return true;
}
//退出函数
void EXIT(int sig)
{
logfile.Write("程序退出,sig=%d\n\n",sig);
exit(0);
}
//打印帮助信息
void _help()
{
printf("\n");
printf("Using:/project/tools1/bin/tcpputfiles logfilename xmlbuffer\n\n");
printf("Sample:/project/tools1/bin/procctl 20 /project/tools1/bin/tcpputfiles /log/idc/tcpputfiles_surfdata.log \"<ip>192.168.211.128</ip><port>5005</port><ptype>1</ptype><clientpath>/tmp/tcp/surfdata1</clientpath><andchild>true</andchild><matchname>*.XML,*.CSV,*.JSON</matchname><srvpath>/tmp/tcp/surfdata2</srvpath><timetvl>10</timetvl><timeout>50</timeout><pname>tcpputfiles_surfdata</pname>\"\n");
printf(" /project/tools1/bin/procctl 20 /project/tools1/bin/tcpputfiles /log/idc/tcpputfiles_surfdata.log \"<ip>192.168.211.128</ip><port>5005</port><ptype>2</ptype><clientpath>/tmp/tcp/surfdata1</clientpath><clientpathbak>/tmp/tcp/surfdata1bak</clientpathbak><andchild>true</andchild><matchname>*.XML,*.CSV,*.JSON</matchname><srvpath>/tmp/tcp/surfdata2</srvpath><timetvl>10</timetvl><timeout>50</timeout><pname>tcpputfiles_surfdata</pname>\"\n\n\n");
printf("本程序是数据中心的公共功能模块,采用tcp协议把文件上传给服务端。\n");
printf("logfilename 本程序运行的日志文件。\n");
printf("xmlbuffer 本程序运行的参数,如下:\n");
printf("ip 服务端的IP地址。\n");
printf("port 服务端的端口。\n");
printf("ptype 文件上传成功后的处理方式:1-删除文件;2-移动到备份目录。\n");
printf("clientpath 本地文件存放的根目录。\n");
printf("clientpathbak 文件成功上传后,本地文件备份的根目录,当ptype==2时有效。\n");
printf("andchild 是否上传clientpath目录下各级子目录的文件,true-是;false-否,缺省为false。\n");
printf("matchname 待上传文件名的匹配规则,如\"*.TXT,*.XML\"\n");
printf("srvpath 服务端文件存放的根目录。\n");
printf("timetvl 扫描本地目录文件的时间间隔,单位:秒,取值在1-30之间。\n");
printf("timeout 本程序的超时时间,单位:秒,视文件大小和网络带宽而定,建议设置50以上。\n");
printf("pname 进程名,尽可能采用易懂的、与其它进程不同的名称,方便故障排查。\n\n");
}
// 把xml解析到参数starg结构
bool _xmltoarg(char *strxmlbuffer)
{
memset(&starg,0,sizeof(struct st_arg));
GetXMLBuffer(strxmlbuffer,"ip",starg.ip);
if (strlen(starg.ip)==0) { logfile.Write("ip is null.\n"); return false; }
GetXMLBuffer(strxmlbuffer,"port",&starg.port);
if ( starg.port==0) { logfile.Write("port is null.\n"); return false; }
GetXMLBuffer(strxmlbuffer,"ptype",&starg.ptype);
if ((starg.ptype!=1)&&(starg.ptype!=2)) { logfile.Write("ptype not in (1,2).\n"); return false; }
GetXMLBuffer(strxmlbuffer,"clientpath",starg.clientpath);
if (strlen(starg.clientpath)==0) { logfile.Write("clientpath is null.\n"); return false; }
GetXMLBuffer(strxmlbuffer,"clientpathbak",starg.clientpathbak);
if ((starg.ptype==2)&&(strlen(starg.clientpathbak)==0)) { logfile.Write("clientpathbak is null.\n"); return false; }
GetXMLBuffer(strxmlbuffer,"andchild",&starg.andchild);
GetXMLBuffer(strxmlbuffer,"matchname",starg.matchname);
if (strlen(starg.matchname)==0) { logfile.Write("matchname is null.\n"); return false; }
GetXMLBuffer(strxmlbuffer,"srvpath",starg.srvpath);
if (strlen(starg.srvpath)==0) { logfile.Write("srvpath is null.\n"); return false; }
GetXMLBuffer(strxmlbuffer,"timetvl",&starg.timetvl);
if (starg.timetvl==0) { logfile.Write("timetvl is null.\n"); return false; }
// 扫描本地目录文件的时间间隔,单位:秒。
// starg.timetvl没有必要超过30秒。
if (starg.timetvl>30) starg.timetvl=30;
// 进程心跳的超时时间,一定要大于starg.timetvl,没有必要小于50秒。
GetXMLBuffer(strxmlbuffer,"timeout",&starg.timeout);
if (starg.timeout==0) { logfile.Write("timeout is null.\n"); return false; }
if (starg.timeout<50) starg.timeout=50;
GetXMLBuffer(strxmlbuffer,"pname",starg.pname,50);
if (strlen(starg.pname)==0) { logfile.Write("pname is null.\n"); return false; }
return true;
}
接着程序会向服务器发起连接请求。当成功建立连接以后,会进入到Login()函数中去。这个函数实际上就是在运行参数最后面拼接一个<clienttype>1</clienttype>。这是在告诉服务端现在连接你的客户端类型是1,即上传文件的客户端。服务端判断完客户端的类型后会启用对应的处理函数。
// 登录业务。
bool Login(const char *argv)
{
memset(strsendbuffer,0,sizeof(strsendbuffer));
memset(strrecvbuffer,0,sizeof(strrecvbuffer));
SPRINTF(strsendbuffer,sizeof(strsendbuffer),"%s<clienttype>1</clienttype>",argv);
logfile.Write("发送:%s\n",strsendbuffer);
if (TcpClient.Write(strsendbuffer)==false) return false; // 向服务端发送请求报文。
if (TcpClient.Read(strrecvbuffer,20)==false) return false; // 接收服务端的回应报文。
logfile.Write("接收:%s\n",strrecvbuffer);
logfile.Write("登录(%s:%d)成功。\n",starg.ip,starg.port);
return true;
}
来到上传文件的主流程,进入一个死循环里,在循环中,先进行一次上传操作,然后判断变量bcontinue是否为false,如果是,则程序休眠一段时间再判断连接是否还在。最后更新一次心跳时间。程序退出。在我们传输文件的过程中,如果每传输一次就休眠一段时间,那么新进来的文件需要等到本次上传完成才能够被上传。这无疑是不合理的。所以设置了bcontinue这个变量。在上传文件的函数中首先将这个变量设置为false,如果没有文件需要上传,函数退出,我们认为系统此时比较空闲,该变量的值会是false,可以进行休眠。如果扫描到有程序需要上传,则设置为true,上传一次完成后将会不进行休眠迅速进入下一次上传。
来到文件上传的主函数_tcpputfiles()。首先调用文件操作类的函数打开存放待上传文件的目录。定义变量delayed为未收到对端确认报文的文件数量。bcontinue先设置为false。接着我们调用文件类的ReadDir()函数,获得一个文件名。然后把文件名、修改时间、文件大小组成报文,发送给对端。接着调用SendFile()函数将文件内容发送给对端。每发送一次,delayed++。每收到一次确认报文,delayed--。这是因为在数据量够大的情况下,确认报文有可能会延迟。发送文件内容是一项可能会耗时很长的操作,所以需要在完成后即时更新自己的心跳信息。上传完成后开始接收确认报文。一直到delayed为0,也就是收到全部的确认报文为止。在这之后会根据运行参数的不同调用AckMessage()函数选择删除或者备份原文件。
// 文件上传的主函数,执行一次文件上传的任务。
bool _tcpputfiles()
{
CDir Dir;
// 调用OpenDir()打开starg.clientpath目录。
if (Dir.OpenDir(starg.clientpath,starg.matchname,10000,starg.andchild)==false)
{
logfile.Write("Dir.OpenDir(%s) 失败。\n",starg.clientpath); return false;
}
int delayed=0; // 未收到对端确认报文的文件数量。
int buflen=0; // 用于存放strrecvbuffer的长度。
bcontinue=false;
while (true)
{
memset(strsendbuffer,0,sizeof(strsendbuffer));
memset(strrecvbuffer,0,sizeof(strrecvbuffer));
// 遍历目录中的每个文件,调用ReadDir()获取一个文件名。
if (Dir.ReadDir()==false) break;
bcontinue=true;
// 把文件名、修改时间、文件大小组成报文,发送给对端。
SNPRINTF(strsendbuffer,sizeof(strsendbuffer),1000,"<filename>%s</filename><mtime>%s</mtime><size>%d</size>",Dir.m_FullFileName,Dir.m_ModifyTime,Dir.m_FileSize);
// logfile.Write("strsendbuffer=%s\n",strsendbuffer);
if (TcpClient.Write(strsendbuffer)==false)
{
logfile.Write("TcpClient.Write() failed.\n"); return false;
}
// 把文件的内容发送给对端。
logfile.Write("send %s(%d) ...",Dir.m_FullFileName,Dir.m_FileSize);
if (SendFile(TcpClient.m_connfd,Dir.m_FullFileName,Dir.m_FileSize)==true)
{
logfile.WriteEx("ok.\n"); delayed++;
}
else
{
logfile.WriteEx("failed.\n"); TcpClient.Close(); return false;
}
PActive.UptATime();
// 接收对端的确认报文。
while (delayed>0)
{
memset(strrecvbuffer,0,sizeof(strrecvbuffer));
if (TcpRead(TcpClient.m_connfd,strrecvbuffer,&buflen,-1)==false) break;
// logfile.Write("strrecvbuffer=%s\n",strrecvbuffer);
// 删除或者转存本地的文件。
delayed--;
AckMessage(strrecvbuffer);
}
}
// 继续接收对端的确认报文。
while (delayed>0)
{
memset(strrecvbuffer,0,sizeof(strrecvbuffer));
if (TcpRead(TcpClient.m_connfd,strrecvbuffer,&buflen,10)==false) break;
// logfile.Write("strrecvbuffer=%s\n",strrecvbuffer);
// 删除或者转存本地的文件。
delayed--;
AckMessage(strrecvbuffer);
}
return true;
}
在发送文件内容的函数中,我们需要做判断,如果文件的内容太大,则每次只读取1000字节。然后将读取的内容发送给对端。接着计算总字节数与读取字节数的差,继续判断。
// 把文件的内容发送给对端。
bool SendFile(const int sockfd,const char *filename,const int filesize)
{
int onread=0; // 每次调用fread时打算读取的字节数。
int bytes=0; // 调用一次fread从文件中读取的字节数。
char buffer[1000]; // 存放读取数据的buffer。
int totalbytes=0; // 从文件中已读取的字节总数。
FILE *fp=NULL; // 文件指针
// 以"rb"的模式打开文件。
if ( (fp=fopen(filename,"rb"))==NULL ) return false;
while (true)
{
memset(buffer,0,sizeof(buffer));
// 计算本次应该读取的字节数,如果剩余的数据超过1000字节,就打算读1000字节。
if (filesize-totalbytes>1000) onread=1000;
else onread=filesize-totalbytes;
// 从文件中读取数据。
bytes=fread(buffer,1,onread,fp);
// 把读取到的数据发送给对端。
if (bytes>0)
{
if (Writen(sockfd,buffer,bytes)==false) { fclose(fp); return false; }
}
// 计算文件已读取的字节总数,如果文件已读完,跳出循环。
totalbytes=totalbytes+bytes;
if (totalbytes==filesize) break;
}
fclose(fp);
return true;
}
在删除或者备份原文件的函数AckMessage()中,服务端在接收完上传的一个文件后,会将文件名和接收结果返回给客户端。所以首先判断返回结果是否为ok,如果接收文件失败了,函数直接返回。如果接收文件成功了。将判断操作类型。对于删除文件,只需要调用REMOVE()函数,里面封装了库函数remove()。对于备份源文件则复杂一点。需要重新更改文件名为备份文件名。然后再进行移动。
// 删除或者转存本地的文件。
bool AckMessage(const char *strrecvbuffer)
{
char filename[301];
char result[11];
memset(filename,0,sizeof(filename));
memset(result,0,sizeof(result));
GetXMLBuffer(strrecvbuffer,"filename",filename,300);
GetXMLBuffer(strrecvbuffer,"result",result,10);
// 如果服务端接收文件不成功,直接返回。
if (strcmp(result,"ok")!=0) return true;
// ptype==1,删除文件。
if (starg.ptype==1)
{
if (REMOVE(filename)==false) { logfile.Write("REMOVE(%s) failed.\n",filename); return false; }
}
// ptype==2,移动到备份目录。
if (starg.ptype==2)
{
// 生成转存后的备份目录文件名。
char bakfilename[301];
STRCPY(bakfilename,sizeof(bakfilename),filename);
UpdateStr(bakfilename,starg.clientpath,starg.clientpathbak,false);
if (RENAME(filename,bakfilename)==false)
{ logfile.Write("RENAME(%s,%s) failed.\n",filename,bakfilename); return false; }
}
return true;
}
接下来是下载文件的客户端代码。前期的准备流程与上传文件的程序基本一致。在此不作过多赘述。在编写上传客户端时,采用的是客户端向服务端发起上传文件请求和测试连接信息。在编写下载文件的客户端时,可以将服务端看做是客户端。由服务端向下载文件的客户端发起上传文件请求和测试连接信息。直接看最核心的下载主函数。
接下来是服务端的代码。与之前的测试程序流程类似。总体流程是程序参数不正确时给出帮助信息,给出示例运行命令。关闭信号与输入输出,打开日志文件。接着服务端打开端口进行初始化。如果这时端口被占用或者其他原因导致失败会记录日志。接着程序在一个死循环里开始监听连接。当有客户端进行连接时,创造一个新的进程。父进程关闭用于通信的套接字,通continue返回继续监听。子进程首先重新设置进程的退出信号处理函数。这样可以确保子进程在接收到这些信号时有机会进行必要的处理,而不是立即被终止。然后子进程关闭用于监听的套接字以节约资源。接着调用登入处理函数ClientLogin(),该函数实际上就是将客户端发送过来运行参数进行解析传人结构体内。最后判断客户端的类型来进行相应的处理。
#include "_public.h"
// 程序运行的参数结构体。
struct st_arg
{
int clienttype; // 客户端类型,1-上传文件;2-下载文件。
char ip[31]; // 服务端的IP地址。
int port; // 服务端的端口。
int ptype; // 文件成功传输后的处理方式:1-删除文件;2-移动到备份目录。
char clientpath[301]; // 客户端文件存放的根目录。
bool andchild; // 是否传输各级子目录的文件,true-是;false-否。
char matchname[301]; // 待传输文件名的匹配规则,如"*.TXT,*.XML"。
char srvpath[301]; // 服务端文件存放的根目录。
char srvpathbak[301]; // 服务端文件存放的根目录。
int timetvl; // 扫描目录文件的时间间隔,单位:秒。
int timeout; // 进程心跳的超时时间。
char pname[51]; // 进程名,建议用"tcpgetfiles_后缀"的方式。
} starg;
// 把 xml解析到参数starg结构中。
bool _xmltoarg(char *strxmlbuffer);
CLogFile logfile; // 服务程序的运行日志。
CTcpServer TcpServer; // 创建服务端对象。
void FathEXIT(int sig); // 父进程退出函数。
void ChldEXIT(int sig); // 子进程退出函数。
bool ActiveTest(); // 心跳。
char strrecvbuffer[1024]; // 发送报文的buffer。
char strsendbuffer[1024]; // 接收报文的buffer。
// 文件下载的主函数,执行一次文件下载的任务。
bool _tcpputfiles();
bool bcontinue=true; // 如果调用_tcpputfiles发送了文件,bcontinue为true,初始化为true。
// 把文件的内容发送给对端。
bool SendFile(const int sockfd,const char *filename,const int filesize);
// 删除或者转存本地的文件。
bool AckMessage(const char *strrecvbuffer);
// 登录业务处理函数。
bool ClientLogin();
// 处理上传文件的主函数。
void RecvFilesMain();
// 处理下载文件的主函数。
void SendFilesMain();
// 接收文件的内容。
bool RecvFile(const int sockfd,const char *filename,const char *mtime,int filesize);
CPActive PActive; // 进程心跳。
int main(int argc,char *argv[])
{
if (argc!=3)
{
printf("Using:./fileserver port logfile\n");
printf("Example:./fileserver 5005 /log/idc/fileserver.log\n");
printf(" /project/tools1/bin/procctl 10 /project/tools1/bin/fileserver 5005 /log/idc/fileserver.log\n\n\n");
return -1;
}
// 关闭全部的信号和输入输出。
// 设置信号,在shell状态下可用 "kill + 进程号" 正常终止些进程
// 但请不要用 "kill -9 +进程号" 强行终止
CloseIOAndSignal(); signal(SIGINT,FathEXIT); signal(SIGTERM,FathEXIT);
if (logfile.Open(argv[2],"a+")==false) { printf("logfile.Open(%s) failed.\n",argv[2]); return -1; }
// 服务端初始化。
if (TcpServer.InitServer(atoi(argv[1]))==false)
{
logfile.Write("TcpServer.InitServer(%s) failed.\n",argv[1]); return -1;
}
while (true)
{
// 等待客户端的连接请求。
if (TcpServer.Accept()==false)
{
logfile.Write("TcpServer.Accept() failed.\n"); FathEXIT(-1);
}
logfile.Write("客户端(%s)已连接。\n",TcpServer.GetIP());
if (fork()>0) { TcpServer.CloseClient(); continue; } // 父进程继续回到Accept()。
// 子进程重新设置退出信号。
signal(SIGINT,ChldEXIT); signal(SIGTERM,ChldEXIT);
TcpServer.CloseListen();
// 子进程与客户端进行通讯,处理业务。
// 处理登录客户端的登录报文。
if (ClientLogin()==false) ChldEXIT(-1);
// 如果clienttype==1,调用处理上传文件的主函数。
if (starg.clienttype==1) RecvFilesMain();
// 如果clienttype==2,调用处理下载文件的主函数。
if (starg.clienttype==2) SendFilesMain();
ChldEXIT(0);
}
}
// 父进程退出函数。
void FathEXIT(int sig)
{
// 以下代码是为了防止信号处理函数在执行的过程中被信号中断。
signal(SIGINT,SIG_IGN); signal(SIGTERM,SIG_IGN);
logfile.Write("父进程退出,sig=%d。\n",sig);
TcpServer.CloseListen(); // 关闭监听的socket。
kill(0,15); // 通知全部的子进程退出。
exit(0);
}
// 子进程退出函数。
void ChldEXIT(int sig)
{
// 以下代码是为了防止信号处理函数在执行的过程中被信号中断。
signal(SIGINT,SIG_IGN); signal(SIGTERM,SIG_IGN);
logfile.Write("子进程退出,sig=%d。\n",sig);
TcpServer.CloseClient(); // 关闭客户端的socket。
exit(0);
}
// 登录。
bool ClientLogin()
{
memset(strrecvbuffer,0,sizeof(strrecvbuffer));
memset(strsendbuffer,0,sizeof(strsendbuffer));
if (TcpServer.Read(strrecvbuffer,20)==false)
{
logfile.Write("TcpServer.Read() failed.\n"); return false;
}
logfile.Write("strrecvbuffer=%s\n",strrecvbuffer);
// 解析客户端登录报文。
_xmltoarg(strrecvbuffer);
// 如果出现运行参数非法填入的情况,返回failed
if ( (starg.clienttype!=1) && (starg.clienttype!=2) )
strcpy(strsendbuffer,"failed");
else
strcpy(strsendbuffer,"ok");
if (TcpServer.Write(strsendbuffer)==false)
{
logfile.Write("TcpServer.Write() failed.\n"); return false;
}
// 登入成功,记录客户端的ip。
logfile.Write("%s login %s.\n",TcpServer.GetIP(),strsendbuffer);
return true;
}