课程设计题目二
实验项目名称:文件传输协议的简单设计与实现
实验项目性质:综合性
所属课程名称:计算机网络
一、 实验目的
文件传送是各种计算机网络都实现的基本功能,文件传送协议是一种最基本的应用层协议按照客户/服务器的模式进行工作,提供交互式的访问,是INTERNET使用最广泛的协议之一。
本实验的目的是,学会利用已有网络环境设计并实现简单应用层协议,掌握TCP/IP 网络应用程序基本的设计方法和实现技巧。
二、 实验内容和要求
1、实验内容
我们的计算机网络实验环境建立在TCP/IP 网络体系结构之上。各计算机除了安装TCP/IP 软件外,还安装了TCP/IP 开发系统。实验室各计算机具备Windows环境中套接字socket 的编程接口功能,可为用户提供全网范围的进程通信功能。本实验要求学生利用这些功能,设计和实现一个简单的文件传送协议。
2、具体要求
用socket 编程接口编写两个程序,分别为客户程序(client.c)和服务器程序(server.c),该程序应能实现下述命令功能:
get:取远方的一个文件
put:传给远方一个文件
pwd:显示远主当前目录
dir:列出远方当前目录
cd :改变远方当前目录
? :显示你提供的命令
quit :退出返回
这此命令的具体工作方式(指给出结果的形式)可以参照FTP 的相应命令,有余力的同学可以多实现几个命令。
最后,写出实验报告。
三、 实验主要仪器设备和材料
联网计算机。
四、 实验方法、步骤及结构测试
1) 关于端口号(假设用SERV_PORT 来表示)的设定,原则上2000 至5000都可用,为避免冲突,建议取你学号后三位数加上2000,比如学号为971234,则可定义:
#define SERV_PORT 2234
2) 客户和服务程序中要有相应的include 文件(参考所给例子程序)
3) 有些同学的server 方程序支持多连接,为了不占用更多的系统资源,并发连接数限制在3 个以内。
4) 最后提交源程序,撰写实验报告,在实验报告中说明设计的思路。
五、 程序设计方案
设计主要函数分析
本程序的步骤包括客户端和服务器建立socket连接、客户端登录服务器、服务器进入被动(主动)模式、客户端下载(上传)文件等。当中调用多个函数从而实现客户端和都完全的连接。
client端函数
(1)DWORD StartSock()函数//启动winsock
(2)DWORD CreateSocket()//创建套接字
(3)DWORD CallServer() //发送连接请求
通过此函数创建与指定外都端口的连接
(4)DWORD TCPSend(char data[]) //发送命令
通过此函数发送客户端的命令
(5) void list(SOCKET sockfd)
通过此函数列出指定目录下的所有文件
(6)int SendFile(SOCKET datatcps, FILE* file)
通过此函数传输目标文件
server端函数
(1)DWORD StartSock() //初始化winsock
初始化winsock以便client端连接
(2)DWORD CreateSocket()//创建套接字
(3)int SendFileRecord(SOCKET datatcps, WIN32_FIND_DATA* pfd) //用来发送当前文件记录
(4)int SendFileList(SOCKET datatcps)//发送文件列表
通过此函数创建一个线程用于发送文件列表信息。使用FindFirstFile函数根据当前的文件存放路径查找目标文件来把待操作文件的相关属性读取到WIN32_FIND_DATA结构以初始化线程。
(5)int SendFile(SOCKET datatcps, FILE* file)//发送文件
(6)DWORD ConnectProcess()//连接
通过此函数创建服务器监听、创建连接、根据客户端命令完成应答。
六、 实验结果
客户端连接服务器
查看客户端文件路径、查看服务器文件列表
传输文件、获取文件
七、 思考题
- 本题目采用的是C/S模式下实现文件传输协议,考虑当前应用广泛的B/S模式,这两种编程模式优缺点如何?
C/S:即客户端和服务器结构。客户端和服务器分别承担不同的任务。Client将用户的需求提交给Server,再将Server返回的结果以一定的形式提交给用户。Server的任务是按接收Client提出的服务请求,进行相应的处理,并将结果返回给Client。
在CS结构下,服务器程序通常在一个固定的地址监听客户端的请求。服务器进程通常下处于“休眠”状态,直到客户端对该服务发出连接请求,将其“唤醒”。此时,服务进程“醒来”并对客户端的请求作出适当的反应。
BS( Browser/Server),即浏览器与服务器结构。客户端运行浏览器,浏览器以超文本形式向web服务器提出访问数据库请求。Web服务器接受客户端请求后,将该请求转换为SQL语法,并对数据库进行访问,然后将结果返回给Web服务器。Web服务器再将该结果转换为HTML文档,返回客户端浏览器,以网页的形式显示出来。BS结构中,Web浏览器是客户端最主要的软件,系统功能实现的核心部分集中到服务器上。
1.1、B/S模式的优点和缺点
B/S结构的优点
(1)、具有分布性特点,可以随时随地进行查询、浏览等业务处理。
(2)、业务扩展简单方便,通过增加网页即可增加服务器功能。
(3)、维护简单方便,只需要改变网页,即可实现所有用户的同步更新。
(4)、开发简单,共享性强
B/S 模式的缺点
(1)、个性化特点明显降低,无法实现具有个性化的功能要求。
(2)、操作是以鼠标为最基本的操作方式,无法满足快速操作的要求。
(3)、页面动态刷新,响应速度明显降低。
(4)、无法实现分页显示,给数据库访问造成较大的压力。
(5)、功能弱化,难以实现传统模式下的特殊功能要求。
1.2、C/S 模式的优点和缺点
C/S 模式的优点
(1).由于客户端实现与服务器的直接相连,没有中间环节,因此响应速度快。
(2).操作界面漂亮、形式多样,可以充分满足客户自身的个性化要求。
(3).C/S结构的管理信息系统具有较强的事务处理能力,能实现复杂的业务流程。
C/S 模式的缺点
(1).需要专门的客户端安装程序,分布功能弱,针对点多面广且不具备网络条件的用户群体,不能够实现快速部署安装和配置。
(2).兼容性差,对于不同的开发工具,具有较大的局限性。若采用不同工具,需要重新改写程序。
(1)3.开发成本较高,需要具有一定专业水准的技术人员才能完成。
- 查找资料,如何在本题目中添加“断点续传”功能?以及该功能的实现基本原理如何?
2.1原理
由于FTP是顺序接收文件的,所以,只要计算已接收的数据大小,就可以知道断点在文件的偏移量,续传时,客户端将已接收的数据量传递给服务器,服务器使用这个作为偏移量继续读取文件,将剩余的数据发送至客户端,客户端仅需将接收到的数据追加至原文件尾部即可
2.2实现
以下载文件为例:
1)向服务器发送“REST + 本地文件长度”命令,告诉服务器,客户端要断点下载了,服务器必须支持REST命令;
2)向服务器发送“RETR + 文件名”命令,通知服务器要下载的文件名,这时服务器开始定位文件指针读文件并发送数据;
3)客户端定位本地文件指针(文件末尾);
4)两端的准备工作都做完了以后,客户端创建socket,以被动或非被动方式建立数据通道,循环调用recv接收数据并追加入本地文件。
- 我们已经有了FTP后,为何在邮件服务器之间传输邮件(邮件也是一种文件)时,还需要SMTP协议?以及为何需要HTTP协议?
SMTP(Simple Mail Transfer Protocol)是简单邮件传输协议,是一种提供可靠且有效电子邮件传输的协议。SMTP是建立在FTP文件传输服务上的一种邮件服务,主要用于传输系统之间的邮件信息并提供与来信有关的通知。
超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议。所有的WWW文件都必须遵守这个标准。设计HTTP最初的目的是为了提供一种发布和接收HTML页面的方法。在Internet上的Web服务器上存放的都是超文本信息,客户机需要通过HTTP协议传输所要访问的超文本信息。HTTP包含命令和传输信息,不仅可用于Web访问,也可以用于其他因特网/内联网应用系统之间的通信,从而实现各类应用资源超媒体访问的集成。
- 考虑如何用多线程实现FTP?
每个线程创建一个 TCP 连接,对应一个文件,采用循环接收,应答等机制来传送文件数据。N个这样的线程就对应而来正在下载N个文件
client:
#pragma warning(disable:4996)
#include "Winsock.h"
#include "windows.h"
#include "stdio.h"
#include "time.h"
#include <iostream>
using namespace std;
#define RECV_PORT 3312
#define SEND_PORT 4302
#pragma comment(lib, "wsock32.lib")
SOCKET sockclient;
char filename[20]; //文件名
sockaddr_in ServerAddr; //服务器地址
char rbuff[1024]; //接收缓冲区
char sbuff[1024]; //发送缓冲区
char InputIP[20]; //存储输入的服务器IP
void help()//处理help
{
cout << " 欢迎进入迷你FTP帮助菜单 " << endl
<< " * * * * * * * * * * * * * * * * * * * * * " << endl
<< " 1.get....................下载(接受)文件 " << endl
<< " get的用法: get 文件名 " << endl << endl
<< " 2.put.................上传(发送)文件 " << endl
<< " put的用法:put 文件名 " << endl
<< " 3.pwd..........显示当前文件夹的绝对路径 " << endl
<< " 4.dir............显示远方当前目录的文件 " << endl << endl
<< " 5.cd.............改变远方当前目录和路径 " << endl
<< " cd的用法(进入下级目录): cd 路径名 " << endl
<< " cd的用法(进入上级目录): cd .. " << endl << endl
<< " 6.?或者help................进入帮助菜单 " << endl
<< " 7.quit..........................退出FTP " << endl
<< " * * * * * * * * * * * * * * * * * * * * * " << endl;
}
void list(SOCKET sockfd)
{
int nRead;
while (true)
{
nRead = recv(sockclient, rbuff, 1024, 0);
//recv函数通过sockclient套接口接受数据存入rbuff缓冲区,返回接受到的字节数
if (nRead == SOCKET_ERROR)
{
printf("read response error!\n");
exit(1);
}
if (nRead == 0)//数据读取结束
break;
//显示数据
rbuff[nRead] = '\0';
printf("%s", rbuff);
}
}
/*********************** put:传给远方一个文件***************************/
int SendFile(SOCKET datatcps, FILE * file)
{
printf(" sending file data..");
for (;;) //从文件中循环读取数据并发送客户端
{
int r = fread(sbuff, 1, 1024, file);//fread函数从file文件读取1个1024长度的数据到sbuff,返回成功读取的元素个数
if (send(datatcps, sbuff, r, 0) == SOCKET_ERROR)
{
printf("lost the connection to client!\n");
closesocket(datatcps);
return 0;
}
if (r < 1024) //文件传送结束
break;
}
closesocket(datatcps);
printf("done\n");
return 1;
}
DWORD StartSock()//启动winsock
{
WSADATA WSAData;
char a[20];
memset(a, 0, 20);
if (WSAStartup(MAKEWORD(2, 2), &WSAData) != 0)//加载winsock版本
{
printf("sock init fail!\n");
return (-1);
}
if (strncmp(InputIP, a, 20) == 0)
{
printf("请输入连接的主机IP:");
scanf("%s", &InputIP);
}
//设置地址结构
ServerAddr.sin_family = AF_INET;//AF_INET表示使用IP地址族
ServerAddr.sin_addr.s_addr = inet_addr(InputIP);//指定服务器IP
ServerAddr.sin_port = htons(RECV_PORT);//设置端口号
return(1);
}
//创建套接字
DWORD CreateSocket()
{
sockclient = socket(AF_INET, SOCK_STREAM, 0);//当socket函数成功调用时返回一个新的SOCKET(Socket Descriptor)
if (sockclient == SOCKET_ERROR)
{
printf("sockclient create fail! \n");
WSACleanup();
return(-1);
}
return(1);
}
DWORD CallServer() //发送连接请求
{
CreateSocket();
if (connect(sockclient, (struct sockaddr*) & ServerAddr, sizeof(ServerAddr)) == SOCKET_ERROR)
{//connect函数创建与指定外部端口的连接
printf("Connect fail \n");
memset(InputIP, 0, 20);
return(-1);
}
return(1);
}
DWORD TCPSend(char data[]) //发送命令
{
int length;
length = send(sockclient, data, strlen(data), 0);
//send函数通过sockclient接口发送data里面的数据,发送成功返回发送的字节数
if (length <= 0)
{
printf("send data error ! \n");
closesocket(sockclient);
WSACleanup();
return(-1);
}
return(1);
}
int main()
{
char messge1[10]; //定义输入要处理的文件名
char messge2[20]; //定义输入要处理的文件名
char order[30]; //输入的命令
order[0] = '\0';
char buff[80]; //用以存储经过字串格式化的order
FILE* fd; //File协议主要用于访问本地计算机中的文件,fd指针指向要访问的目标文件
FILE* fd2;
int count;
int sin_size = sizeof(ServerAddr);
StartSock();
if (CallServer() == -1)
return main(); //发送连接请求失败,返回主函数
printf("\n请输入命令(输入?或help进入帮助菜单):\n");
memset(buff, 0, 80); //清空数组
memset(messge2, 0, 20);
memset(order, 0, 30);
memset(messge1, 0, 10);
memset(rbuff, 0, 1024);
memset(sbuff, 0, 1024);
scanf("%s", &messge1);//s%输入字符串
if (strncmp(messge1, "get", 3) == 0)
scanf("%s", &messge2);
if (strncmp(messge1, "put", 3) == 0)
scanf("%s", &messge2);
if (strncmp(messge1, "cd", 2) == 0)
scanf("%s", &messge2);
strcat(order, messge1); //把messge1加在order的末尾
strcat(order, " "); //命令中间的空格
strcat(order, messge2); //把messge2加在order的末尾
sprintf(buff, order); //把调整格式的order存入buff
//help和?
if (strncmp(messge1, "help", 4) == 0) {
help();
}
if (strncmp(messge1, "?", 1) == 0) {
help();
}
if (strncmp(messge1, "quit", 4) == 0)
{
printf(" 欢迎再次进入迷你FTP,谢谢使用!\n");
closesocket(sockclient);
WSACleanup();
return 0;
}
TCPSend(buff);//发送buff里面的数据
recv(sockclient, rbuff, 1024, 0);
printf(rbuff);
if (strncmp(rbuff, "get", 3) == 0) //get
{
fd = fopen(messge2, "wb");//使用二进制方式,打开文件,wb只写打开或新 建一个二进制文件;只允许写数据。
if (fd == NULL)
{
printf("open file %s for weite failed!\n", messge2);
return 0;
}
while ((count = recv(sockclient, rbuff, 1024, 0)) > 0)
{
fwrite(rbuff, sizeof(rbuff), count, fd);
}
//把count个数据长度为size0f()的数据从 rbuff输入到fd指向的目标文件
fclose(fd); //关闭文件
}
if (strncmp(rbuff, "put", 3) == 0) //put
{
strcpy(filename, rbuff + 9);
fd2 = fopen(filename, "rb");//rb读写打开一个二进制文件,只允许读写数据。
if (fd2)
{
if (!SendFile(sockclient, fd2)) {
printf("send failed!");
return 0;
}
fclose(fd2);
}//关闭文件
else//打开文件失败
{
strcpy(sbuff, "can't open file!\n");
if (send(sockclient, sbuff, 1024, 0))
return 0;
}
}
if (strncmp(rbuff, "dir", 3) == 0) //dir
{
printf("\n");
list(sockclient); //列出接受到的列表内容
}
if (strncmp(rbuff, "pwd", 3) == 0)
{
list(sockclient); //列出接受到的内容--绝对路径
}
if (strncmp(rbuff, "cd", 2) == 0) {} //cd
closesocket(sockclient); //关闭连接
WSACleanup(); //释放Winsock
return main();
}
server:
#pragma warning(disable:4996)
#include "Winsock.h"
#include "windows.h"
#include "stdio.h"
#define RECV_PORT 3312
#define SEND_PORT 4302
#pragma comment(lib, "wsock32.lib")
SOCKET sockclient, sockserver;
struct sockaddr_in ServerAddr;//服务器地址
struct sockaddr_in ClientAddr;//客户端地址
/***********************全局变量***********************/
int Addrlen;//地址长度
char filename[20];//文件名
char order[10];//命令
char rbuff[1024];//接收缓冲区
char sbuff[1024];//发送缓冲区
DWORD StartSock() //初始化winsock
{
WSADATA WSAData;
if (WSAStartup(MAKEWORD(2, 2), &WSAData) != 0)
{
printf("socket init fail!\n");
return (-1);
}
return(1);
}
DWORD CreateSocket()
{
sockclient = socket(AF_INET, SOCK_STREAM, 0);
if (sockclient == SOCKET_ERROR)
{
printf("sockclient create fail ! \n");
WSACleanup();
return(-1);
}
ServerAddr.sin_family = AF_INET;
ServerAddr.sin_addr.s_addr = htonl(INADDR_ANY);
ServerAddr.sin_port = htons(RECV_PORT);
if (bind(sockclient, (struct sockaddr FAR*) & ServerAddr, sizeof(ServerAddr)) == SOCKET_ERROR)
{ //bind函数将套接字和地址结构绑定
printf("bind is the error");
return(-1);
}
return (1);
}
int SendFileRecord(SOCKET datatcps, WIN32_FIND_DATA* pfd) //用来发送当前文件记录
{
char filerecord[MAX_PATH + 32];
FILETIME ft; //文件建立时间
FileTimeToLocalFileTime(&pfd->ftLastWriteTime, &ft);
SYSTEMTIME lastwtime; //SYSTEMTIME系统时间数据结构
FileTimeToSystemTime(&ft, &lastwtime);
char* dir = pfd->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY ? "<DIR>" : " ";
sprintf(filerecord, "%04d-%02d-%02d %02d:%02d %5s %10d %-20s\n",
lastwtime.wYear,
lastwtime.wMonth,
lastwtime.wDay,
lastwtime.wHour,
lastwtime.wMinute,
dir,
pfd->nFileSizeLow,
pfd->cFileName);
if (send(datatcps, filerecord, strlen(filerecord), 0) == SOCKET_ERROR)
{ //通过datatcps接口发送filerecord数据,成功返回发送的字节数
printf("Error occurs when sending file list!\n");
return 0;
}
return 1;
}
int SendFileList(SOCKET datatcps)
{
HANDLE hff;//建立一个线程
char buffer[MAX_PATH] = { 0, };
WIN32_FIND_DATA fd; //搜索文件
hff = FindFirstFile(buffer, &fd);
//可以通过FindFirstFile()函数根据当前的文件存放路径查找该文件来把待操作文件的相关属性读取到WIN32_FIND_DATA结构中去
if (hff == INVALID_HANDLE_VALUE)//发生错误
{
const char* errstr = "can't list files!\n";
printf("list file error!\n");
if (send(datatcps, errstr, strlen(errstr), 0) == SOCKET_ERROR)
{
printf("error occurs when senging file list!\n");
}
closesocket(datatcps);
return 0;
}
BOOL fMoreFiles = TRUE;
while (fMoreFiles)
{//发送此项文件信息
if (!SendFileRecord(datatcps, &fd))
{
closesocket(datatcps);
return 0;
}
//搜索下一个文件
fMoreFiles = FindNextFile(hff, &fd);
}
closesocket(datatcps);
return 1;
}
int SendFile(SOCKET datatcps, FILE* file)
{
printf(" sending file data..");
for (;;) //从文件中循环读取数据并发送客户端
{
int r = fread(sbuff, 1, 1024, file);//把file里面的内容读到sbuff缓冲区
if (send(datatcps, sbuff, r, 0) == SOCKET_ERROR)
{
printf("lost the connection to client!\n");
closesocket(datatcps);
return 0;
}
if (r < 1024)//文件传送结束
break;
}
closesocket(datatcps);
printf("done\n");
return 1;
}
//连接
DWORD ConnectProcess()
{
Addrlen = sizeof(ClientAddr);
if (listen(sockclient, 5) < 0)
{
printf("Listen error");
return(-1);
}
printf("服务器监听中...\n");
for (;;)
{
sockserver = accept(sockclient, (struct sockaddr FAR*) & ClientAddr, &Addrlen);
//accept函数取出连接队列的第一个连接请求,sockclient是处于监听的套接字ClientAddr 是监听的对象地址,
//Addrlen是对象地址的长度
for (;;)
{
memset(rbuff, 0, 1024);
memset(sbuff, 0, 1024);
if (recv(sockserver, rbuff, 1024, 0) <= 0)
{
break;
}
printf("\n");
printf("获取并执行的命令为:");
printf(rbuff);
if (strncmp(rbuff, "get", 3) == 0)
{
strcpy(filename, rbuff + 4); printf(filename);
FILE* file; //定义一个文件访问指针
//处理下载文件请求
file = fopen(filename, "rb");//打开下载的文件,只允许读写
if (file)
{
sprintf(sbuff, "get file %s\n", filename);
if (!send(sockserver, sbuff, 1024, 0))
{
fclose(file); return 0;
}
else
{//创建额外数据连接传送数据
if (!SendFile(sockserver, file))
return 0;
fclose(file);
}
}//file
else//打开文件失败
{
strcpy(sbuff, "can't open file!\n");
if (send(sockserver, sbuff, 1024, 0))
return 0;
} //lost
}//get
if (strncmp(rbuff, "put", 3) == 0)
{
FILE* fd;
int count;
strcpy(filename, rbuff + 4);
fd = fopen(filename, "wb");
if (fd == NULL)
{
printf("open file %s for weite failed!\n", filename);
return 0;
}
sprintf(sbuff, "put file %s", filename);
if (!send(sockserver, sbuff, 1024, 0))
{
fclose(fd);
return 0;
}
while ((count = recv(sockserver, rbuff, 1024, 0)) > 0)//recv函数返回接受的字节数赋给count
fwrite(rbuff, sizeof(char), count, fd);
//把count个数据长度为size0f()的数据从rbuff输入到fd指向的目标文件
printf(" get %s succed!\n", filename);
fclose(fd);
}//put
if (strncmp(rbuff, "pwd", 3) == 0) {
char path[1000];
GetCurrentDirectory(1000, path);//找到当前进程的当前目录
strcpy(sbuff, path);
send(sockserver, sbuff, 1024, 0);
}//pwd
if (strncmp(rbuff, "dir", 3) == 0) {
strcpy(sbuff, rbuff);
send(sockserver, sbuff, 1024, 0);
SendFileList(sockserver);//发送当前列表
}//dir
if (strncmp(rbuff, "cd", 2) == 0)
{
strcpy(filename, rbuff + 3);
strcpy(sbuff, rbuff);
send(sockserver, sbuff, 1024, 0);
SetCurrentDirectory(filename);//设置当前目录
}//cd
closesocket(sockserver);
}//for 2
}//for 1
}
int main()
{
if (StartSock() == -1)
return(-1);
if (CreateSocket() == -1)
return(-1);
if (ConnectProcess() == -1)
return(-1);
return(1);
}