C++学习笔记(54)

350、实现文件传输功能
一、demo11.cpp
/*
* 程序名:demo11.cpp,此程序用于演示文件传输的客户端。
*/
#include <iostream>
#include <fstream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
using namespace std;
class ctcpclient // TCP 通讯的客户端类。
{
private:
int m_clientfd; // 客户端的 socket,-1 表示未连接或连接已断开;>=0 表示有效的
socket。
string m_ip; // 服务端的 IP/域名。
unsigned short m_port; // 通讯端口。
public:
ctcpclient():m_clientfd(-1) {}
// 向服务端发起连接请求,成功返回 true,失败返回 false。
bool connect(const string &in_ip,const unsigned short in_port)
{
if (m_clientfd!=-1) return false; // 如果 socket 已连接,直接返回失败。
m_ip=in_ip; m_port=in_port; // 把服务端的 IP 和端口保存到成员变量中。
// 第 1 步:创建客户端的 socket。
if ( (m_clientfd = socket(AF_INET,SOCK_STREAM,0))==-1) return false;
// 第 2 步:向服务器发起连接请求。
struct sockaddr_in servaddr; // 用于存放协议、端口和 IP 地址的结构体。
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET; // ①协议族,固定填 AF_INET。
servaddr.sin_port = htons(m_port); // ②指定服务端的通信端口。
struct hostent* h; // 用于存放服务端 IP 地址(大端序)的结构体的
指针。
if ((h=gethostbyname(m_ip.c_str()))==nullptr ) // 把域名/主机名/字符串格式的 IP转换成结
构体。
{
::close(m_clientfd); m_clientfd=-1; return false;
}
memcpy(&servaddr.sin_addr,h->h_addr,h->h_length); // ③指定服务端的 IP(大端序)。
// 向服务端发起连接清求。
if (::connect(m_clientfd,(struct sockaddr *)&servaddr,sizeof(servaddr))==-1)
{
::close(m_clientfd); m_clientfd=-1; return false;
}
return true;
}
// 向服务端发送报文(字符串),成功返回 true,失败返回 false。
bool send(const string &buffer) // buffer 不要用 const char *
{
if (m_clientfd==-1) return false; // 如果 socket 的状态是未连接,直接返回失败。
if ((::send(m_clientfd,buffer.data(),buffer.size(),0))<=0) return false;
return true;
}
// 向服务端发送报文(二进制数据),成功返回 true,失败返回 false。
bool send(void *buffer,const size_t size)
{
if (m_clientfd==-1) return false; // 如果 socket 的状态是未连接,直接返回失败。
if ((::send(m_clientfd,buffer,size,0))<=0) return false;
return true;
}
// 接收服务端的报文,成功返回 true,失败返回 false。
// buffer-存放接收到的报文的内容,maxlen-本次接收报文的最大长度。
bool recv(string &buffer,const size_t maxlen)
{ // 如果直接操作 string 对象的内存,必须保证:1)不能越界;2)操作后手动设置数据的大小。
buffer.clear(); // 清空容器。
buffer.resize(maxlen); // 设置容器的大小为 maxlen。
int readn=::recv(m_clientfd,&buffer[0],buffer.size(),0); // 直接操作 buffer 的内存。
if (readn<=0) { buffer.clear(); return false; }
buffer.resize(readn); // 重置 buffer 的实际大小。
return true;
}
// 断开与服务端的连接。
bool close()
{
if (m_clientfd==-1) return false; // 如果 socket 的状态是未连接,直接返回失败。
::close(m_clientfd);
m_clientfd=-1;
return true;
}
// 向服务端发送文件内容。
bool sendfile(const string &filename,const size_t filesize)
{
// 以二进制的方式打开文件。
ifstream fin(filename,ios::binary);
if (fin.is_open() == false) { cout << "打开文件" << filename << "失败。\n"; return false; }
int onread=0; // 每次调用 fin.read()时打算读取的字节数。 每次应搬砖头数。
int totalbytes=0; // 从文件中已读取的字节总数。 已搬砖头数。
char buffer[4096]; // 存放读取数据的 buffer。 每次搬七块砖头。
while (true)
{
memset(buffer,0,sizeof(buffer));
// 计算本次应该读取的字节数,如果剩余的数据超过 4096 字节,就读 4096 字节。
if (filesize-totalbytes>4096) onread=4096;
else onread=filesize-totalbytes;
// 从文件中读取数据。
fin.read(buffer,onread);
// 把读取到的数据发送给对端。
if (send(buffer,onread)==false) return false;
// 计算文件已读取的字节总数,如果文件已读完,跳出循环。
totalbytes=totalbytes+onread;
if (totalbytes==filesize) break;
}
return true;
} ~ctcpclient(){ close(); }
};
int main(int argc,char *argv[])
{
if (argc!=5)
{
cout << "Using:./demo11 服务端的 IP 服务端的端口 文件名 文件大小\n";
cout << "Example:./demo11 192.168.101.138 5005 aaa.txt 2424\n\n";
return -1;
}
ctcpclient tcpclient;
if (tcpclient.connect(argv[1],atoi(argv[2]))==false) // 向服务端发起连接请求。
{
perror("connect()"); return -1;
}
// 以下是发送文件的流程。
// 1)把待传输文件名和文件的大小告诉服务端。
// 定义文件信息的结构体。
struct st_fileinfo{
char filename[256]; // 文件名。
int filesize; // 文件大小。
}fileinfo;
memset(&fileinfo,0,sizeof(fileinfo));
strcpy(fileinfo.filename,argv[3]); // 文件名。
fileinfo.filesize=atoi(argv[4]); // 文件大小。
// 把文件信息的结构体发送给服务端。
if (tcpclient.send(&fileinfo,sizeof(fileinfo))==false) { perror("send"); return -1; }
cout << "发送文件信息的结构体" << fileinfo.filename << "(" << fileinfo.filesize <<")。"<<
endl;
// 2)等待服务端的确认报文(文件名和文件的大小的确认)。
string buffer;
if (tcpclient.recv(buffer,2)==false) { perror("recv()"); return -1; }
if (buffer!="ok") { cout << "服务端没有回复 ok。\n"; return -1; }
// 3)发送文件内容。
if (tcpclient.sendfile(fileinfo.filename,fileinfo.filesize)==false)
{
perror("sendfile()"); return -1;
}
// 4)等待服务端的确认报文(服务端已接收完文件)。
if (tcpclient.recv(buffer,2)==false) { perror("recv()"); return -1; }
if (buffer!="ok") { cout << "发送文件内容失败。\n"; return -1; }
cout << "发送文件内容成功。\n";
}
二、demo12.cpp
/*
* 程序名:demo12.cpp,此程序用于演示文件传输的服务端。
*/
#include <iostream>
#include <fstream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <netdb.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
using namespace std;
class ctcpserver // TCP 通讯的服务端类。
{
private:
int m_listenfd; // 监听的 socket,-1 表示未初始化。
int m_clientfd; // 客户端连上来的 socket,-1 表示客户端未连接。
string m_clientip; // 客户端字符串格式的 IP。
unsigned short m_port; // 服务端用于通讯的端口。
public:
ctcpserver():m_listenfd(-1),m_clientfd(-1) {}
// 初始化服务端用于监听的 socket。
bool initserver(const unsigned short in_port)
{
// 第 1 步:创建服务端的 socket。
if ( (m_listenfd=socket(AF_INET,SOCK_STREAM,0))==-1) return false;
m_port=in_port;
// 第 2 步:把服务端用于通信的 IP 和端口绑定到 socket 上。
struct sockaddr_in servaddr; // 用于存放协议、端口和 IP 地址的结构体。
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family=AF_INET; // ①协议族,固定填 AF_INET。
servaddr.sin_port=htons(m_port); // ②指定服务端的通信端口。
servaddr.sin_addr.s_addr=htonl(INADDR_ANY); // ③如果操作系统有多个 IP,全部的 IP 都
可以用于通讯。
// 绑定服务端的 IP 和端口(为 socket 分配 IP 和端口)。
if (bind(m_listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr))==-1)
{
close(m_listenfd); m_listenfd=-1; return false;
}
// 第 3 步:把 socket 设置为可连接(监听)的状态。
if (listen(m_listenfd,5) == -1 )
{
close(m_listenfd); m_listenfd=-1; return false;
}
return true;
}
// 受理客户端的连接(从已连接的客户端中取出一个客户端),
// 如果没有已连接的客户端,accept()函数将阻塞等待。
bool accept()
{
struct sockaddr_in caddr; // 客户端的地址信息。
socklen_t addrlen=sizeof(caddr); // struct sockaddr_in 的大小。
if ((m_clientfd=::accept(m_listenfd,(struct sockaddr *)&caddr,&addrlen))==-1) return
false;
m_clientip=inet_ntoa(caddr.sin_addr); // 把客户端的地址从大端序转换成字符串。
return true;
}
// 获取客户端的 IP(字符串格式)。
const string & clientip() const
{
return m_clientip;
}
// 向对端发送报文,成功返回 true,失败返回 false。
bool send(const string &buffer)
{
if (m_clientfd==-1) return false;
if ( (::send(m_clientfd,buffer.data(),buffer.size(),0))<=0) return false;
return true;
}
// 接收对端的报文(字符串),成功返回 true,失败返回 false。
// buffer-存放接收到的报文的内容,maxlen-本次接收报文的最大长度。
bool recv(string &buffer,const size_t maxlen)
{
buffer.clear(); // 清空容器。
buffer.resize(maxlen); // 设置容器的大小为 maxlen。
int readn=::recv(m_clientfd,&buffer[0],buffer.size(),0); // 直接操作 buffer 的内存。
if (readn<=0) { buffer.clear(); return false; }
buffer.resize(readn); // 重置 buffer 的实际大小。
return true;
}
// 接收客户端的报文(二进制数据),成功返回 true,失败返回 false。
// buffer-存放接收到的报文的内容,size-本次接收报文的最大长度。
bool recv(void *buffer,const size_t size)
{
if (::recv(m_clientfd,buffer,size,0)<=0) return false;
return true;
}
// 关闭监听的 socket。
bool closelisten()
{
if (m_listenfd==-1) return false;
::close(m_listenfd);
m_listenfd=-1;
return true;
}
// 关闭客户端连上来的 socket。
bool closeclient()
{
if (m_clientfd==-1) return false;
::close(m_clientfd);
m_clientfd=-1;
return true;
}
// 接收文件内容。
bool recvfile(const string &filename,const size_t filesize)
{
ofstream fout;
fout.open(filename,ios::binary);
if (fout.is_open() == false) { cout << "打开文件" << filename << "失败。\n"; return
false; }
int totalbytes=0; // 已接收文件的总字节数。
int onread=0; // 本次打算接收的字节数。
char buffer[4096]; // 接收文件内容的缓冲区。
while (true)
{
// 计算本次应该接收的字节数。
if (filesize-totalbytes>4096) onread=4096;
else onread=filesize-totalbytes;
// 接收文件内容。
if (recv(buffer,onread)==false) return false;
// 把接收到的内容写入文件。
fout.write(buffer,onread);
// 计算已接收文件的总字节数,如果文件接收完,跳出循环。
totalbytes=totalbytes+onread;
if (totalbytes==filesize) break;
}
return true;
} ~ctcpserver() { closelisten(); closeclient(); }
};
ctcpserver tcpserver;
void FathEXIT(int sig); // 父进程的信号处理函数。
void ChldEXIT(int sig); // 子进程的信号处理函数。
int main(int argc,char *argv[])
{
if (argc!=3)
{
cout << "Using:./demo12 通讯端口 文件存放的目录\n";
cout << "Example:./demo12 5005 /tmp\n\n";
cout << "注意:运行服务端程序的 Linux 系统的防火墙必须要开通 5005 端口。\n";
cout << " 如果是云服务器,还要开通云平台的访问策略。\n\n";
return -1;
}
// 忽略全部的信号,不希望被打扰。顺便解决了僵尸进程的问题。
for (int ii=1;ii<=64;ii++) signal(ii,SIG_IGN);
// 设置信号,在 shell 状态下可用 "kill 进程号" 或 "Ctrl+c" 正常终止些进程
// 但请不要用 "kill -9 +进程号" 强行终止
signal(SIGTERM,FathEXIT); signal(SIGINT,FathEXIT); // SIGTERM 15 SIGINT 2
if (tcpserver.initserver(atoi(argv[1]))==false) // 初始化服务端用于监听的 socket。
{
perror("initserver()"); return -1;
}
while (true)
{
// 受理客户端的连接(从已连接的客户端中取出一个客户端),
// 如果没有已连接的客户端,accept()函数将阻塞等待。
if (tcpserver.accept()==false)
{
perror("accept()"); return -1;
}
int pid=fork();
if (pid==-1) { perror("fork()"); return -1; } // 系统资源不足。
if (pid> 0)
{ // 父进程。
tcpserver.closeclient(); // 父进程关闭客户端连接的 socket。
continue; // 父进程返回到循环开始的位置,继续受理客户端的连接。
}
tcpserver.closelisten(); // 子进程关闭监听的 socket。
// 子进程需要重新设置信号。
signal(SIGTERM,ChldEXIT); // 子进程的退出函数与父进程不一样。
signal(SIGINT ,SIG_IGN); // 子进程不需要捕获 SIGINT 信号。
// 子进程负责与客户端进行通讯。
cout << "客户端已连接(" << tcpserver.clientip() << ")。\n";
// 以下是接收文件的流程。
// 1)接收文件名和文件大小信息。
// 定义文件信息的结构体。
struct st_fileinfo{
char filename[256]; // 文件名。
int filesize; // 文件大小。
}fileinfo;
memset(&fileinfo,0,sizeof(fileinfo));
// 用结构体存放接收报文的内容。
if (tcpserver.recv(&fileinfo,sizeof(fileinfo))==false) { perror("recv()"); return -1; }
cout << "文件信息结构体" << fileinfo.filename << "(" << fileinfo.filesize <<")。"<< endl;
// 2)给客户端回复确认报文,表示客户端可以发送文件了。
if (tcpserver.send("ok")==false) { perror("send"); break; }
// 3)接收文件内容。 string char * + const char * + char *
if (tcpserver.recvfile(string(argv[2])+"/"+fileinfo.filename,fileinfo.filesize)==false)
{
cout << "接收文件内容失败。\n"; return -1;
}
cout << "接收文件内容成功。\n";
// 4)给客户端回复确认报文,表示文件已接收成功。
tcpserver.send("ok");
return 0; // 子进程一定要退出,否则又会回到 accept()函数的位置。
}
}
// 父进程的信号处理函数。
void FathEXIT(int sig)
{
// 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断。
signal(SIGINT,SIG_IGN); signal(SIGTERM,SIG_IGN);
cout << "父进程退出,sig=" << sig << endl;
kill(0,SIGTERM); // 向全部的子进程发送 15 的信号,通知它们退出。
// 在这里增加释放资源的代码(全局的资源)。
tcpserver.closelisten(); // 父进程关闭监听的 socket。
exit(0);
}
// 子进程的信号处理函数。
void ChldEXIT(int sig)
{
// 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断。
signal(SIGINT,SIG_IGN); signal(SIGTERM,SIG_IGN);
cout << "子进程" << getpid() << "退出,sig=" << sig << endl;
// 在这里增加释放资源的代码(只释放子进程的资源)。
tcpserver.closeclient(); // 子进程关闭客户端连上来的 socket。
exit(0);
}
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值