文件传输是计算机系统最早最基本的应用之一。因特网上使用的最广泛的文件传送协议(File Transfer Protocol,FTP)就是一个典型的例子,它以客户/服务器模式工作,专门提供客户到服务器的文件上传和文件下载等服务,它的实现使用了TCP协议。后来出现的许多网络应用都具有文件传输功能,专门用于下载的迅雷、网际快车等自不必说,就连QQ这样的聊听工具都提供文件传输功能。由此可见,实现文件传输是网络编程的一项最基本的工作。
文件是一种数据存储的形式,因此文件传输的实质就是数据的传输。在文件传输程序中,发送方首先打开文件将文件数据读入应用程序的发送缓冲区,然后调用send()函数将数据发送给接收端,接收方则调用recv()函数接收数据,并将收到的数据写入文件。
程序要求
利用流式套接字编写一个简单的文件传输程序:
(1).服务器程序和客户程序均为控制台应用程序;
(2).服务器程序是文件的发送方,服务器程序启动以后要求从键盘输入要发送的文件的存放位置及文件名,然后等待客户端下载该文件;
(3).客户程序是文件的接收方,客户程序启动后要求输入服务器的IP地址及所使用的TCP端口号,然后与服务器建立连接并下载服务器提供的文件,文件保存在C盘根目录下。
下面分析程序要完成的功能及相应的实现技术。
文件位置的表示
服务器程序要完成的第一件事情是输入文件位置及文件名,Windows系统使用“文件路径”的概念来指定文件在磁盘上的存放位置,文件路径通常是一个由盘符、文件夹名、子文件夹名和文件名组成的字符串,各项用反斜线“\”隔开。例如存储在D盘“C++程序”文件夹的LX1子文件中的文件readme.txt的路径是:
D:\C++程序\LX1\readme.txt
在C\C++程序中,反斜线“\”由于是转意字符的标志,因此反斜线本身需要用“\\”表示,因此程序代码中文件路径应该表示为:
D:\\C++程序\\LX1\\readme.txt
C++中文件的基本操作
服务器端和客户端程序都使用文件操作,有MFC的文件操作语句,也有控制台应用程序使用的C\C++文件操作语句。C++中的文件处理功能是由输入文件流ifstream和输出文件流ofstream提供,这两个流在头文件fstream中定义。C++中的文件操作包含以下5个步骤:
1.在程序中包含头文件fstream;
2.定义文件流变量:
ifstream inFile; //定义输入文件流对象
ofstream outFile; //定义输出文件流对象
3.打开文件:
inFile.open(filename,inmode);
outFile.open(filename,outmode);
inFile和outFile是步骤(2)中定义的流对象,filename是要打开的文件名,可以包含文件路径,inmode和outmode则是打开或建立文件的方式,该参数有默认值。对于inmode和outmode可以取如下值:
ios::in---打开输入文件,是ifstream流的默认方式
ios::out---打开输入文件,是ofstream流的默认方式,若文件不存在,则将创建文件
ios::app---已追加方式打开,文件存在则在文件尾追加数据,不存在则创建文件
ios::ate---文件打开以后,位置指针在文件最后,常和in、out联合使用
ios::trunk---文件存在则文件已有内容被清除
ios::binary---打开二进制文件
注意:(2)和(3)步骤也可合并为一个语句:
ifstream inFile(filename,inmode);
ofstream outFile(filename,inmode);
//以读写方式打开二进制文件
fstream f("d:\\12.dat",ios::in | ios::out | ios::binary);
流对象提供is_open()方法来判断文件是否成功打开,如果打开成功,该方法就返回true,打开失败则返回false。
4.读写文件
对文本文件可适用“«”或“»”连接文件流变量进行读写,输入文件流变量与cin用法完全相同,输出流变量与cout用法完全相同。
对二进制文件使用get()方法可以从输入流中读取一个字符,put()方法则用于向输出流中写入一个字节。比如:
char ch1,ch2='A'; //定义两个字符变量
inFile.get(ch1); //从输入文件流中读取一个字符存入变量ch1中
outFile.put(ch2); //将变量ch2中的一个字符写入输出文件流
如果要一次读写多个字节的数据,则需要使用read()方法和write()方法,这两个方法格式如下:
read(char * buffer,streamsize size);
write(char * buffer,streamsize size);
其中,参数buffer指向的存储区用来存储读出的或要写入流中的数据,参数size是一个整数数值,表示要从缓存buffer中读出或写入的字节数。
使用read()方法读取文件数据时,应使用eof()方法检测文件是否读结束,使用gcount()方法获得实际读取的字节数。
注意:对以二进制方式打开的文件不能使用“«”或“»”进行输入输出操作。
5.关闭文件
文件操作完成后必须关闭文件,关闭文件时系统会立即将文件缓冲区的数据存入磁盘并且断开文件与流变量之间的关联,关闭文件方法如下:
inFile.close();
outFile.close();
文件名字和文件内容的传输
传输一个文件需要传输两部分内容:一是文件的名字,二是文件的内容,收发双方必须约定何时发送文件以及何时发送文件内容。一般的做法是,发送方发送文件名给接收方,接收方收到文件名以后以输出方式(ios::out)打开文件,然后通知发送方“可以发送文件内容了”,发送方收到允许发送文件内容的通知后,就开始从文件中读取文件内容并发送给接收方。其中发送方已事先打开要发送的文件,并且收发双方约定用字符串“OK”表示接收方允许发送方发送文件内容的通知。
应用程序发送缓冲区和接收缓冲区大小
通常在编写程序时,被传输的文件大小是不可预知的,可能不足1KB,也可能达到几个GB,因此发送端不可能事先定义一个较大的数据缓冲区一次性将文件中的所有数据都读出来再发送,而只能是定义一个大小适当的缓冲区,利用循环进行多次读取多次发送,直到文件结束。接收方也不可能事先定义一个较大的接收缓冲区来容纳整个文件,也必须定义一个适当大小的缓冲区进行多次接收、多次写入文件直到文件传输结束。
接收方判断文件传输结束的方法
发送方将文件中的所有数据读出(读到文件尾)发送完成,就可以停止文件内容的读取与发送工作,转而进行其他处理。但当文件传输结束时,接收方如何知道传输已经结束进而停止接收并保存数据?
一种方法是在文件数据传送之前,先由发送方将文件的大小发送给接收方,数据传输开始后,接收方对收到的数据字节数进行累计,当累计到的数据量等于文件的大小便可停止接收。
还有一种常见、比较简单的方法利用recv()函数返回值,如果用于传送数据的套接字已关闭,recv()函数将返回0,其他情况要么返回实际接收到的数据量,要么返回SOCKET_ERROR。因此,文件发送方在文件数据传输完成后将所用的套接字直接关闭,接收方接收完数据后在此调用recv(),函数的返回值将为0,此时就可以知道文件传输已结束。
获取文件长度的方法
接收端判断文件传输是否结束需要使用文件长度,但C/C++并不提供直接获取文件长度的函数。当文件的读写位置指针位于文件结束时,其值就等于文件长度。如下面的getfilesize()函数:
long getfilesize(const char * filename){
ifstream inFile(filename); //定义流变量并打开文件
inFile.seekg(0,ios::end); //设置文件指针到文件流的尾部
streampos ps=inFile.tellg(); //读取文件指针的位置
inFile.close(); //关闭文件流
return ps;
}
函数中出现的streampos是文件流中专门用于表示文件读写指针位置的一种类型,一可把它看作是long int型或long long int类型,当以二进制方式打开文件时,其单位为字节。
seekg()是文件流提供的移动文件的读位置指针的方法,tellg()则是读取文件的读位置指针的方法。C++的文件流维持有两个文件位置指针:一个是读位置指针,一个是写位置指针。seekg()和tellg()只可用来控制读位置指针,而专用于控制写位置指针的方法是seekp()和tellp(),teelg()和teelp()无参数,seekg()和seekp()的使用方法完全相同。
seekg(off_type offset,seekdir direction);
seekp(off_type offset,seekdir direction);
第一个参数时偏移量,通常是以字节为单位的整数,可以是正负数值,正的表示向后偏移,负表示向前偏移。第二个参数时基地址,可以是:
ios::beg---表示输入流的开始位置
ios::cur---表示输入流的当前位置
ios::end---表示输入流的结束位置
例如,inFile.seekg(-100,ios::end);表示从文件末尾向前移动100字节。
文件信息结构定义
为了一次性将文件名和文件长度发送给接收端,可定义如下的结构:
struct fileMessage{
char fileName[256];
long int fileSize;
}
文件传输的完整流程
如图所示为文件传输程序发送端和接收端的流程图:
服务器初始化Winsock库后,创建套接字并绑定服务器地址,然后调用listen()函数设置套接字为监听套接字,并调用accept函数等待客户端的连接。客户端软件则初始化Winsock库,创建套接字、填写客户地址和服务器地址的地址结构,调用connect()函数与服务器进行连接,连接成功后双方就可以进行文件传输了。
文件传输前,首先要打开文件,获取文件长度并将文件名和文件长度发送给客户端,客户端收到后则打开文件为写入数据做好准备,并向服务器端发送“准备好接收文件内容”的确认信息。服务器端在收到客户端发来的确认信息后,就通过循环多次读取文件内容并发送给客户端,直到读取文件结束。接收方则循环调用recv函数接收数据并将数据写入文件,直到文件传输完成。文件传输完成后客户端和服务器端各自关闭打开的文件和套接字并卸载套接字库。
服务器端代码:
#include <iostream>
#include <fstream>
#include "winsock2.h"
#define PORT 65432
#pragma comment(lib,"ws2_32.lib")
using namespace std;
struct fileMessage{
char fileName[256];
long int fileSize;
};
bool InitSocket(SOCKET &sock_server,int addr_len){
/************初始化winsock2.DLL************/
WSADATA wsaData;
WORD wVerionRequested=MAKEWORD(2,2);
if(WSAStartup(wVerionRequested,&wsaData)!=0){
cout<<"加载winsock.dll失败!";
return 0;
}
/*************创建监听套接字****************/
if((sock_server=socket(AF_INET,SOCK_STREAM,0))==SOCKET_ERROR){
cout<<"创建套接字失败!错误代码:"<<WSAGetLastError()<<endl;
WSACleanup();
return 0;
}
/***********填写绑定服务器地址**************/
struct sockaddr_in addr; //服务器地址
memset((void *)&addr,0,addr_len); //把地址结构体清0
addr.sin_family=AF_INET;
addr.sin_port=htons(PORT);
addr.sin_addr.s_addr=htonl(INADDR_ANY); //使用本机IP地址作为服务器地址
/**********监听套接字绑定服务器地址*********/
if(bind(sock_server,(struct sockaddr *)&addr,sizeof(addr))!=0){
cout<<"地址绑定失败!错误代码:"<<WSAGetLastError()<<endl;
closesocket(sock_server);
WSACleanup();
return 0;
}
/**********将套接字设置为监听状态***********/
if(listen(sock_server,0)!=0){
cout<<"listen 函数调用失败!错误代码:"<<WSAGetLastError()<<endl;
closesocket(sock_server);
WSACleanup();
return 0;
}else
cout<<"listenning.....\n";
return 1;
}
int main(){
/*************定义相关变量******************/
SOCKET sock_server,newsock; //监听套接字 已连接套接字
struct sockaddr_in client_addr; //客户端地址
int addr_len=sizeof(struct sockaddr_in);
/*****************初始化socket*************/
InitSocket(sock_server,addr_len);
/**********输入要传输的文件路径************/
char filename[500]="D:\\cc.txt"; //用于存储要传输的文件的文件路径
/********循环接受连接请求并收发数据*********/
if((newsock=accept(sock_server,(struct sockaddr *)&client_addr,&addr_len))==INVALID_SOCKET){
cout<<"accept函数调用失败!错误代码:"<<WSAGetLastError()<<endl;
closesocket(sock_server);
WSACleanup();
return 0;
}else{
cout<<"connect from:"<<inet_ntoa(client_addr.sin_addr)<<endl;
}
/*********定义文件传输所需变量**************/
char OK[3],fileBuffer[1000]; //接收"OK"的缓冲区和发送缓冲区
struct fileMessage fileMsg; //定义保存文件名及文件长度的结构变量
/*从文件路径中提取文件名保存到结构变量fileMsg中*/
int size=strlen(filename);
while(filename[size]!='\\'&&size>0)
size--;
strcpy(fileMsg.fileName,filename+size);
/*************打开要传输的文件**************/
ifstream inFile(filename,ios::in|ios::binary);
if(!inFile.is_open()){
cout<<"Cannot open"<<filename<<endl;
closesocket(newsock); //关闭socket 对方等待的recive()函数將返回
closesocket(sock_server);
WSACleanup();
return 0;
}
/***************获取文件长度****************/
inFile.seekg(0,ios::end); //將文件的位置指针移到文件末尾
size=inFile.tellg(); //获取当前文件位置指针值 该值即为文件长度
inFile.seekg(0,ios::beg); //将文件的位置指针返回到文件头
/************发送文件名及文件长度*************/
fileMsg.fileSize=htonl(size); //将文件长度存入结构变量fileMsg中
send(newsock,(char*)&fileMsg,sizeof(fileMsg),0); //发送fileMsg
/**********接收对方发送来的OK信息************/
if(recv(newsock,OK,sizeof(OK),0)<=0){
cout<<"接收OK失败,程序退出!"<<endl;
closesocket(newsock);
closesocket(sock_server);
WSACleanup();
return 0;
}
/***************发送文件内容*****************/
if(strcmp(OK,"OK")==0){
while(!inFile.eof()){ //判断文件是否完全读取完成
inFile.read(fileBuffer,sizeof(fileBuffer));
size=inFile.gcount(); //获取实际读取的字节数
send(newsock,fileBuffer,size,0);
}
cout<<"file transfer finished"<<endl; //显示传输完成
inFile.close(); //关闭文件
}else{
cout<<"对方无法接收文件!"<<endl;
}
closesocket(newsock);
closesocket(sock_server);
WSACleanup();
system("pause");
}
客户端:
#include <iostream>
#include <fstream>
#include <direct.h>
#include "winsock2.h"
#define PORT 65432
#pragma comment(lib,"ws2_32.lib")
using namespace std;
struct fileMessage{
char fileName[256];
long int fileSize;
};
struct Student{ //学生信息结构
char sName[20];
char sTtID[10];
int nEnglist;
int nComputerNetwork;
int nCProg;
};
bool InitSocket(SOCKET &sock_client,int addr_len){
/********初始化winsock2.DLL**********/
WSADATA wsaData;
WORD wVerionRequested=MAKEWORD(2,2);
if(WSAStartup(wVerionRequested,&wsaData)!=0){
cout<<"加载winsock.dll失败!";
return 0;
}
/************创建套接字**************/
if((sock_client=socket(AF_INET,SOCK_STREAM,0))<0){
cout<<"创建套接字失败!错误代码:"<<WSAGetLastError()<<endl;
WSACleanup();
return 0;
}
/***********填写服务器地址************/
struct sockaddr_in server_addr; //存放服务器地址
char IP[20];
cout<<"请输入服务器IP地址:";
cin>>IP;
memset((void *)&server_addr,0,addr_len); //把地址结构体清0
server_addr.sin_family=AF_INET;
server_addr.sin_port=htons(PORT);
server_addr.sin_addr.s_addr=inet_addr(IP);
/*********套接字与服务器连接***********/
if(connect(sock_client,(struct sockaddr *)&server_addr,addr_len)!=0){
cout<<"地址绑定失败!错误代码:"<<WSAGetLastError()<<endl;
closesocket(sock_client);
WSACleanup();
return 0;
}
return 1;
}
int main(){
/************定义相关变量*************/
SOCKET sock_client; //客户端套接字
int addr_len=sizeof(struct sockaddr_in);
/************初始化socket*************/
InitSocket(sock_client,addr_len);
/********定义文件传输所需变量*********/
struct fileMessage fileMsg;
long int filelen;
char filename[500]="G:\\"; //指定接收到的文件保存目录
char ok[3]="OK";
char fileBuffer[1000]; //接收文件数据的缓冲区
/*********创建文件的保存目录**********/
_mkdir(filename); //_mkdir()用于创建文件夹 其声明包含在direct.h中
/********接收文件名及文件长度信息******/
if(filelen=recv(sock_client,(char*)&fileMsg,sizeof(fileMsg),0)<=0){
cout<<"未接收到文件名字及文件长度"<<endl;
closesocket(sock_client);
WSACleanup();
return 0;
}
filelen=ntohl(filelen);
strcat(filename,fileMsg.fileName);
/********创建文件准备接收文件内容******/
ofstream outFile(filename,ios::out|ios::binary);
if(!outFile.is_open()){ //文件打开失败则退出
cout<<"Cannot open"<<filename<<endl;
closesocket(sock_client);
WSACleanup();
return 0;
}
send(sock_client,ok,sizeof(ok),0); //发送接收文件数据的确认信息
/********接收文件数据并写入文件*********/
int size=0;
do{
size=recv(sock_client,fileBuffer,sizeof(fileBuffer),0);
outFile.write(fileBuffer,size);
filelen-=size;
}while(size!=0&&filelen>0); //判断接收结束
/*********文件传输完成结束程序**********/
cout<<"Transfer finished"<<endl;
outFile.close();
closesocket(sock_client);
WSACleanup();
system("pause");
}
程序运行结果:
服务器:
客户: