文件传输协议FTP是基于TCP协议实现的,为了体验这个过程,本文自定义一个简单的文件传输协议,并基于TCP协议编程实现,经过本机和局域网的测试,代码初步可用。
定义文件传输协议
自定义文件传输协议:
下载文件过程:
客户端发送:download:filename:
服务端反馈:
文件存在,反馈文件大小size
文件不存在,反馈“error”,断开连接
客户端发送:
缓冲区开辟完成,反馈“ok”
缓冲区开辟失败,反馈“error”,断开连接
服务端反馈:
二进制文件信息
上传文件过程:
客户端发送:download:filename:file_size
服务端反馈:
缓冲区开辟完成,反馈“ok”
缓冲区开辟失败,反馈“error”,断开连接
客户端发送:
二进制文件信息
注意:
1. 在使用new读取、发送或接受文件时,考虑到new申请内存失败,因此需要用到异常处理机制 try catch;
2. 文件使用IOS::binary打开,这样可以读取所有格式的文件;
3. 文件需要完整传输,如果接收到的文件内容不完整,即长度不符合,需要将接收的文件删除;
4. 获取文件的大小使用以下代码:
file.seekg(0, ios::end);
int length = infile.tellg();
file.seekg(0, ios::beg);
具体参考:http://www.cplusplus.com/reference/istream/istream/seekg/?kw=seekg
5. 每次发送数据\接收数据都需要判断数据是否发送\接收成功,如果失败,则表示TCP断开连接,程序结束;
本文代码的缺陷:
(1)因为需要将文件读取到数组中再发送,所以文件太大时这种方式不好,应该将文件分段发送;
(2)没有“优雅的断开TCP连接”,即没有用到半关闭TCP连接,文件发送后成功与否没有反馈以及进一步处理;
参考资料
1、C++中TCP通信实现文件传输
2、c++网络编程——用TCP实现Echo服务端与客户端(windows下基于VS2017)
3、《TCP IP网络编程》(韩)尹圣雨
4、TCP/IP协议详解
服务端代码
服务端创建日志文本存储日志,并于while(true)中死循环等待客户端连接。
#include "pch.h"
#include <iostream>
#include <fstream>
#include <cstdlib>
#include<winsock.h>
#include <string>
#include "time.h"
#include<windows.h>
#include <exception>
//使用localtime
#pragma warning(disable:4996)
//使用套接字库
#pragma comment(lib,"ws2_32.lib")
using namespace std;
string timetoStr() {
char tmp[64];
time_t t = time(NULL);
tm *_tm = localtime(&t);
int year = _tm->tm_year + 1900;
int month = _tm->tm_mon + 1;
int date = _tm->tm_mday;
int hh = _tm->tm_hour;
int mm = _tm->tm_min;
int ss = _tm->tm_sec;
sprintf(tmp, "%04d%02d%02d %02d:%02d:%02d ", year, month, date, hh, mm, ss);
return string(tmp);
}
string GetLogNameByDate() {
char tmp[30];
time_t t = time(NULL);
tm *_tm = localtime(&t);
int year = _tm->tm_year + 1900;
int month = _tm->tm_mon + 1;
int date = _tm->tm_mday;
sprintf(tmp, "%04d%02d%02d.txt", year, month, date);
return string(tmp);
}
void ErrorHandling(const string &s)
{
cerr << timetoStr() << s << endl;
exit(1);
}
//************************************
// Method: ClientDownloadFile
// FullName: ClientDownloadFile
// Access: public
// Returns: void
// Qualifier: 客户端下载文件处理函数
// Parameter: SOCKET s_accept 服务端接收套接字
// Parameter: string file_name 文件名
// Parameter: string clinet_IP 客户端IP
// Parameter: ostream & out_stream 输出流 这里主要为日志文件输出流/cout
//************************************
void ClientDownloadFile(SOCKET s_accept, string file_name, string clinet_IP,ostream &out_stream)
{
int send_len = 0, recv_len = 0;
//发送、接收缓冲区
char send_buf[100], recv_buf[100];
memset(send_buf, '\0', 100);
memset(recv_buf, '\0', 100);
string send_info;
ifstream infile;
infile.open(file_name, ios::in | ios::binary);
if (!infile.good())
{
//文件不存在,回复客户端
send_len = send(s_accept, "error", 100, 0);
out_stream << timetoStr() << "客户端为:" << clinet_IP << " 发送给客户端的信息:" << "error" << endl;
if (send_len < 0)
{
out_stream << timetoStr() << "客户端为:" << clinet_IP << " 发送客户端信息失败!断开连接" << endl;
closesocket(s_accept);
return;
}
out_stream << timetoStr() << "客户端为:" << clinet_IP << " 请求文件不存在而断开连接" << endl;
closesocket(s_accept);
return;
}
//文件存在,返回客户端文件大小
else
{
infile.seekg(0, ios::end);
int length = infile.tellg();
infile.seekg(0, ios::beg);
send_info = to_string(length);
send_len = send(s_accept, send_info.c_str(), send_info.length(), 0);
//Sleep(1000);
if (send_len < send_info.length())
{
out_stream << timetoStr() << "客户端为:" << clinet_IP << " 发送客户端信息失败!断开连接" << endl;
closesocket(s_accept);
return;
}
else
out_stream << timetoStr() << "客户端为:" << clinet_IP << " 发送客户端信息:" << send_info << endl;
//等待客户端开辟缓冲区,接收信息后再发送文件
memset(recv_buf, '\0', 100);
recv_len = recv(s_accept, recv_buf, 100, 0);
if (recv_len < 0)
{
out_stream << timetoStr() << "客户端为:" << clinet_IP << " 接收数据失败!断开连接" << endl;
closesocket(s_accept);
return;
}
else
out_stream << timetoStr() << "客户端为:" << clinet_IP << " 接收来自客户端的信息:" << recv_buf << endl;
if (string(recv_buf) == "ok")
{
//发送整个文件
char* data;
//此处没有考虑到new失败的情况!!!!
data = new char[length];
infile.read(data, length);
send_len = send(s_accept, data, length, 0);
if (send_len < length)
{
out_stream << timetoStr() << "客户端为:" << clinet_IP << " 发送客户端文件信息失败!断开连接" << endl;
closesocket(s_accept);
return;
}
delete data;
out_stream << timetoStr() << "客户端为:" << clinet_IP << " 文件发送成功!断开连接" << endl;
closesocket(s_accept);
return;
}
out_stream << timetoStr() << "客户端为:" << clinet_IP << " 客户端缓冲区开辟失败!断开连接" << endl;
closesocket(s_accept);
}
}
//************************************
// Method: ClientUploadFile
// FullName: ClientUploadFile
// Access: public
// Returns: void
// Qualifier:客户端上传文件处理函数
// Parameter: SOCKET s_accept 服务端接收套接字
// Parameter: string file_name 文件名
// Parameter: int file_size 文件大小
// Parameter: string clinet_IP 客户端IP
// Parameter: ostream & out_stream 输出流 这里主要为日志文件输出流/cout
//************************************
void ClientUploadFile(SOCKET s_accept, string file_name,int file_size, string clinet_IP, ostream &out_stream)
{
int send_len = 0, recv_len = 0;
//发送、接收缓冲区
char send_buf[100], recv_buf[100];
memset(send_buf, '\0', 100);
memset(recv_buf, '\0', 100);
string send_info;
//根据文件大小,开辟文件缓冲区
ofstream outfile;
outfile.open(file_name, ios::out | ios::binary | ios::trunc);
if (!outfile.good())
{
//文件无法打开
send_len = send(s_accept, "error", 100, 0);
out_stream << timetoStr() << "客户端为:" << clinet_IP << " 发送给客户端的信息:" << "error" << endl;
if (send_len < 0)
{
out_stream << timetoStr() << "客户端为:" << clinet_IP << " 发送客户端信息失败!断开连接" << endl;
closesocket(s_accept);
return;
}
out_stream << timetoStr() << "客户端为:" << clinet_IP << " 上传的文件无法建立而断开连接" << endl;
closesocket(s_accept);
return;
}
//文件打开成功,开辟缓冲区,并返回客户端结果
else
{
char* data=nullptr;
try
{
data = new char[file_size];
}
catch (const std::exception&)
{
//失败,发送error
memset(send_buf, '\0', 100);
strcpy(send_buf, "error");
send_len = send(s_accept, send_buf, 100, 0);
out_stream << timetoStr() << "客户端为:" << clinet_IP << " 发送给客户端的信息:" << send_buf << endl;
if (send_len < 0)
{
out_stream << timetoStr() << "客户端为:" << clinet_IP << " 发送客户端信息失败!断开连接" << endl;
closesocket(s_accept);
return;
}
out_stream << timetoStr() << "客户端为:" << clinet_IP << " 服务端开辟缓冲区失败!断开连接" << endl;
closesocket(s_accept);
return;
}
//成功,发送ok
memset(send_buf, '\0', 100);
strcpy(send_buf, "ok");
send_len = send(s_accept, send_buf, 100, 0);
out_stream << timetoStr() << "客户端为:" << clinet_IP << " 发送给客户端的信息:" << send_buf << endl;
if (send_len < 0)
{
out_stream << timetoStr() << "客户端为:" << clinet_IP << " 发送客户端信息失败!断开连接" << endl;
closesocket(s_accept);
return;
}
recv_len = recv(s_accept, data, file_size, 0);
outfile.write(data, file_size);
outfile.close();
if (recv_len < file_size)
{
//接收的信息不全,需要删除文件
remove(file_name.c_str());
out_stream << timetoStr() << "客户端为:" << clinet_IP << " 接收客户端文件信息失败!断开连接" << endl;
closesocket(s_accept);
return;
}
else
out_stream << timetoStr() << "客户端为:" << clinet_IP << " 接收客户端文件成功!断开连接" << endl;
delete data;
closesocket(s_accept);
}
}
void InitiallizationWSA()
{
WSADATA wsaData;
//初始化套接字库
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
ErrorHandling("初始化套接字库失败!");
//检查版本号
if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wHighVersion) != 2)
ErrorHandling("套接字库版本号不符!");
}
int main()
{
//创建日志文件,存储日志
string log_file_name = GetLogNameByDate();
ofstream Log;
try
{
Log.open(log_file_name, std::ofstream::out | std::ofstream::app);
}
catch (ios_base::failure &e)
{
cout << "创建日志文件失败!退出程序" << endl;
exit(1);
}
Log << timetoStr() << "启动:文件传输TCP实现——服务端。。。" << endl;
//初始化套接字库
InitiallizationWSA();
//发送、接收数据的大小
int send_len = 0, recv_len = 0;
int len = 0;
//发送、接收缓冲区
char send_buf[100], recv_buf[100];
memset(send_buf,'\0',100);
memset(recv_buf, '\0', 100);
string send_info;
//定义服务端套接字,接受请求套接字
SOCKET s_server, s_accept;
//客户端IP
string clinet_IP;
//定义服务端地址,客户端地址
SOCKADDR_IN server_addr, clnt_addr;
//创建服务端套接字
s_server = socket(AF_INET, SOCK_STREAM, 0);
//填充服务端信息
server_addr.sin_family = AF_INET;
server_addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(5010);
//绑定套接字
if (bind(s_server, (SOCKADDR *)&server_addr, sizeof(SOCKADDR)) == SOCKET_ERROR)
ErrorHandling("套接字绑定失败!");
//监听套接字
if (listen(s_server, SOMAXCONN) < 0)
ErrorHandling("监听套接字失败!");
//接受连接
while (true)
{
len = sizeof(SOCKADDR);
s_accept = accept(s_server, (SOCKADDR *)&clnt_addr, &len);
if (s_accept == SOCKET_ERROR)
ErrorHandling("连接失败!");
//获取客户端IP
clinet_IP = inet_ntoa(clnt_addr.sin_addr);
Log << timetoStr() << "连接" << clinet_IP << "成功!" << endl;
//获取客户端发送的文件基本信息:命令:文件名:文件大小
memset(recv_buf, '\0', 100);
recv_len = recv(s_accept, recv_buf, 100, 0);
if (recv_len < 0)
{
Log << timetoStr() << "客户端为:" << clinet_IP << " 接收数据失败!断开连接" << endl;
closesocket(s_accept);
continue;
}
else
{
Log << timetoStr() << "客户端为:" << clinet_IP << " 接收来自客户端的信息:" << recv_buf << endl;
}
//根据协议解析命令数据,格式为命令:文件名:文件大小
string order;//文件操作命令
string file_name;//文件名
int size_of_file;//文件大小
string file_info(recv_buf);
auto id_addr1 = file_info.find_first_of(':');
order = file_info.substr(0, id_addr1);
auto id_addr2 = file_info.find_first_of(':', id_addr1+1);
file_name = file_info.substr(id_addr1 + 1, id_addr2- id_addr1-1);
auto Size= file_info.substr(id_addr2 + 1, file_info.length() - id_addr2 - 1);
//下载文件
if (order == "download")
{
ClientDownloadFile(s_accept, file_name,clinet_IP, Log);
}
if (order == "upload")
{
ClientUploadFile(s_accept, file_name,atoi(Size.c_str()), clinet_IP, Log);
}
}
Log.close();
//关闭套接字
closesocket(s_server);
//释放DLL资源
WSACleanup();
return 0;
}
客户端代码
客户端每次执行一次上传/下载命令,执行完毕后连接断开,程序退出。
#include "pch.h"
#include <iostream>
#include <cstdlib>
#include<winsock.h>
#include <string>
#include <fstream>
#include <cstdio>
#pragma warning( disable : 4996)
#pragma comment(lib,"ws2_32.lib")
using namespace std;
void ErrorHandling(const string &s)
{
cerr << s << endl;
exit(1);
}
void InitiallizationWSA()
{
WSADATA wsaData;
//初始化套接字库
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
ErrorHandling("初始化套接字库失败!");
//检查版本号
if(LOBYTE(wsaData.wVersion) !=2 || HIBYTE(wsaData.wHighVersion)!=2)
ErrorHandling("套接字库版本号不符!");
}
int main()
{
cout << "欢迎使用:文件传输TCP实现——客户端" << endl;
//初始化套接字库
InitiallizationWSA();
string server_ip;
int port = 0;
//发送、接收数据的大小
int send_len = 0, recv_len = 0;
int len = 0;
//发送、接收缓冲区
char send_buf[100];
char recv_buf[100];
string send_info;
memset(recv_buf,'\0',100);
memset(send_buf, '\0', 100);
//定义服务端套接字
SOCKET s_server;
//定义服务端地址
SOCKADDR_IN server_addr;
//创建套接字
s_server = socket(AF_INET, SOCK_STREAM, 0);
//填充服务端信息
cout << "请输入服务端IP地址(点分十进制):";
cin >> server_ip;
cout << "请输入服务端端口号:";
cin >> port;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.S_un.S_addr = inet_addr(server_ip.c_str());
server_addr.sin_port = htons(port);//5010
//连接服务端
if (connect(s_server, (SOCKADDR *)&server_addr, sizeof(SOCKADDR)) == SOCKET_ERROR)
ErrorHandling("服务端连接失败!");
cout << "连接服务端成功!" << endl;
//发送/接收数据
cout << "请输入发送信息的内容:(格式为 命令:文件名:)";
cin >> send_buf;
//根据协议解析命令数据,格式为命令:文件名:文件大小
string order;//文件操作命令
string file_name;//文件名
int size_of_file;//文件大小
string file_info(send_buf);
auto id_addr1 = file_info.find_first_of(':');
order = file_info.substr(0, id_addr1);
auto id_addr2 = file_info.find_first_of(':', id_addr1 + 1);
file_name = file_info.substr(id_addr1 + 1, id_addr2 - id_addr1 - 1);
//auto Size = file_info.substr(id_addr2 + 1, file_info.length() - id_addr2 - 1);
if (order == "download")
{
send_len = send(s_server, send_buf, 100, 0);
if (send_len < 0)
{
ErrorHandling("发送失败!断开连接");
}
//获取反馈的命令
memset(recv_buf, '\0', 100);
recv_len = recv(s_server, recv_buf, 100, 0);
if (recv_len < 0)
{
ErrorHandling("接收失败!断开连接");
}
else
cout << "接收服务端信息:" << recv_buf << endl;
string recv_info(recv_buf);
if (recv_info != "error")
{
//接收文件大小,开辟文件缓冲区
ofstream outfile;
outfile.open(file_name, ios::out | ios::binary | ios::trunc);
if (!outfile.good())
{
//文件无法打开
cerr << "文件无法建立" << endl;
}
else
{
//开辟缓冲区
auto size_of_file = atoi(recv_info.c_str());
char* data=nullptr;
try
{
data = new char[size_of_file];
}
catch (const std::exception&)
{
//失败,发送error
memset(send_buf, '\0', 100);
strcpy(send_buf, "error");
send_len = send(s_server, send_buf, 100, 0);
if (send_len < 0)
{
ErrorHandling("发送失败!断开连接");
}
ErrorHandling("开辟缓冲区失败!");
}
//成功,发送ok
memset(send_buf, '\0', 100);
strcpy(send_buf, "ok");
send_len = send(s_server, send_buf, 100, 0);
if (send_len < 0)
{
ErrorHandling("发送失败!断开连接");
}
recv_len = recv(s_server, data, size_of_file, 0);
outfile.write(data, size_of_file);
outfile.close();
//cout << data << endl;
cout << "接收文件大小为:"<<recv_len << endl;
if (recv_len < size_of_file)
{
cout << "接收失败!" << endl;
//接收的信息不全,需要删除文件
remove(file_name.c_str());
}
else
cout << "接收成功!" << endl;
delete data;
}
}
cout << "断开连接" << endl;
//关闭套接字
closesocket(s_server);
}
if (order == "upload")
{
//获取文件长度,并发送给服务端
ifstream infile;
infile.open(file_name, ios::in | ios::binary);
if (!infile.good())
//文件不存在,提示
ErrorHandling("文件不存在!");
else
{
infile.seekg(0, ios::end);
int length = infile.tellg();
infile.seekg(0, ios::beg);
send_info=string(send_buf)+to_string(length);
//发送 命令:文件名:文件大小
//等待服务端开辟缓冲区
send_len = send(s_server, send_info.c_str(), 100, 0);
if (send_len < 0)
{
ErrorHandling("发送失败!断开连接");
}
memset(recv_buf, '\0', 100);
recv_len = recv(s_server, recv_buf, 100, 0);
if (recv_len < 0)
{
ErrorHandling("接收失败!断开连接");
}
else
cout << "接收服务端信息:" << recv_buf << endl;
string recv_info(recv_buf);
if (recv_info == "ok")
{
//服务端开辟缓冲区成功,上传文件数据
//发送整个文件
char* data;
data = new char[length];
infile.read(data, length);
send_len = send(s_server, data, length, 0);
if (send_len < length)
ErrorHandling("发送文件失败!断开连接");
delete data;
cout << "文件上传成功!" << endl;
}
else
cout << "服务端开辟缓冲区失败!" << endl;
infile.close();
cout << "断开连接" << endl;
//关闭套接字
closesocket(s_server);
}
}
while (true)
{
string in;
cout << "输入任意字符退出程序:" << endl;
if(cin>>in)
break;
}
//释放DLL资源
WSACleanup();
return 0;
}
测试
本机测试:
客户端控制台:
1、客户端上传、下载文件测试:
2、客户端错误输入/命令测试:
服务端日志:
局域网通信测试:
1、客户端上传、下载文件测试:
2、客户端错误输入/命令测试:
服务端日志: