1. 要求
在Linux环境下,编程实现文件的上传和下载,即客户端可以发送文件给服务器,服务器将文件写到服务器端文件系统中;客户端请求下载文件时服务器读取文件内容,发送给客户端,客户端接收内容并写入本地文件。要求
(1)源代码格式化良好并适当注释;
(2)除上述核心功能外,尽量完善程序,比如使其使用方便等;
(3)提交报告,报告中包括程序源代码和测试效果截图。
2. 基本设计思路
2.1 服务器和客户端的socket连接
服务器通过对socket进行监听listen(),等待客户端的主动连接connect()。
服务器在建立连接后通过pork()函数赋值进程,使子进程关闭服务器的socket监听并向客户端提供服务;父进程则继续监听socket,继续与其他客户端进行连接。
- 为什么客户端connect前不需要进行bind?
操作系统会自动选择一个可用的本地 IP 地址和端口号来作为客户端套接字的地址,并将这个地址作为连接请求的源地址发送给服务器。这个过程称为“自动绑定”
2.2 客户端的服务请求
在建立了与服务端的socket连接后,客户端通过向服务端发送相关的指令来请求相对应的服务。
通过输入指令发送到服务端,使服务端解析指令,并向客户端返回相对应的数据包。客户端接收并解析数据包来获取相关信息。
2.3 服务端的服务提供
服务端在接收到客户端发来的指令后,根据指令的不同,执行不同的操作。
若接收到客户端下载文件的指令,则将相对应的文件封装成数据包,发送到客户端。
若接收到客户端上传文件的指令,则调用recv()函数来接收由客户端发送过来的数据包,并解析内容写入文本中。
3. 源代码
3.1 server.cpp
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<fcntl.h>
#include<dirent.h>
#include<string>
#include<vector>
#define port 8888
#define backlog 5
#define MAXBUFF 4096
#define MAXNAMELEN 100
void process_con_server(int s);
void analyzeCommand(char *command, int s);
void sendMessage(int s);
char** splitString(char* str, int& num);
void sendFile(int s, char *fname);
void recvFile(int s, char *fname);
/*文件信息*/
typedef struct FileMess{
unsigned long fileLen;
char fileName[100];
} FileMess;
/*数据包*/
typedef struct DataPack{
char type; //'D'表示数据,'M'表示文件信息, 'E'表示错误数据包
int packSize; //整个数据包的大小
char content[MAXBUFF]; //数据包携带数据缓冲区
int contentLen; //数据包中数据的长度
unsigned long position; //数据在文件中的字节位置
FileMess fileMess; //文件信息
} DataPack;
/*目录信息数据包*/
typedef struct DirPack{
int flag; //标志位,1表示包含目录文件名,0表示结束目录发送的空数据包
char content[100];
} DirPack;
int main(int argc, char *argv[]){
int ss, sc;
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
int err;
pid_t pid;
/*创建套接字socket*/
ss = socket(AF_INET, SOCK_STREAM, 0);
if(ss < 0){
printf("Create Socket error!!!\n");
return -1;
}
/*设置服务器基本信息*/
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(port);
/*绑定服务器地址到套接字socket*/
err = bind(ss, (struct sockaddr*)&server_addr, sizeof(server_addr));
if(err < 0){
printf("Bind server address error!!!\n");
return -1;
}
/*启动监听*/
err = listen(ss, backlog); //设置最大排队数量为backlog
if(err < 0){
printf("Start listenning failure!!!\n");
return -1;
}
/*服务器建立连接和提供服务*/
while(1){
socklen_t addrlen = sizeof(struct sockaddr);
/*接收来自客户端client的套接字socket*/
sc = accept(ss, (struct sockaddr*)&client_addr, &addrlen);
if(sc < 0){
printf("accept error!\n");
continue; //跳过创建子进程,避免浪费
}
/*创建新进程继续处理排队中的客户端连接*/
pid = fork();
if(pid == 0){ //父进程的pid永远不为0,未初始化时为随机值
close(ss); //在子进程中关闭服务器监听
process_con_server(sc); //向客户端提供服务
}
else{
close(sc);
}
}
return 0;
}
/*服务器处理与客户端的连接服务*/
void process_con_server(int s){
ssize_t size;
char buffer[1024]; //使用1024bytes的缓冲区来接收用户指令
while(1){
memset(buffer, 0, sizeof(buffer));
size = recv(s, buffer, sizeof(buffer), 0); //会读取回车\n
if(size == 0)
return;
write(STDOUT_FILENO, buffer, size);
analyzeCommand(buffer, s); //解析用户指令
}
close(s);
}
/*解析客户端指令*/
void analyzeCommand(char *command, int s){
int substrNum;
char** substr = splitString(command, substrNum);
/*显示可下载的文件列表, 将列表通过socket传输到客户端*/
if(!strcmp(substr[0], "ls\n")){
sendMessage(s);
}
/*下载指定文件*/
else if(!strcmp(substr[0], "download")){
sendFile(s, substr[1]);
}
/*接收来自客户端上传的文件*/
else if(!strcmp(substr[0], "send")){
recvFile(s, substr[1]);
}
else{
char *mes = "Please write down the right command!!!\n";
write(s, mes, strlen(mes));
}
}
/*以一个或多个空格分割字符串*/
/*返回值:指针数组*/
/*一般输入的指令是 指令+文件名的形式 例如"send test.txt"*/
char** splitString(char* str, int& num) {
int len = strlen(str);
char** result = new char*[len];
int count = 0;
int start = 0;
/*遍历分割字符串*/
for (int i = 0; i < len; ++i) {
if (str[i] == ' ') {
if (i - start > 0) {
int size = i - start;
result[count] = new char[size + 1]; //size+1是为了尾部添加'\0'作为截至
strncpy(result[count], &str[start], size);
result[count][size] = '\0';
++count;
}
start = i + 1;
}
}
/*若字符串不以空格结尾,则需要将字符串尾部添加*/
if (len - start > 0) {
int size = len - start;
result[count] = new char[size + 1];
strncpy(result[count], &str[start], size);
result[count][size] = '\0';
++count;
}
num = count;
return result;
}
/*服务器向客户端发送文件,即客户端下载文件*/
void sendFile(int s, char *fname){ //fname含有'\n'回车
DataPack dataPack;
char path[100] = "./resources/";
//去除字符串fname末尾的回车
char *fName = (char*)malloc(strlen(fname)-1);
strncpy(fName, fname, strlen(fname)-1);
//拼接文件名与资源路径得到文件路径
strncat(path, fName, strlen(fname)-1);
//判断用户获取的资源文件是否存在
int st = access(path, F_OK);
if(-1 == st){
/*设置错误信息,并以错误信息类型发送数据包*/
dataPack.type = 'E';
dataPack.packSize = sizeof(DataPack);
char *buffer = "The file isn't exist!!!\n";
strncpy(dataPack.content, buffer, strlen(buffer));
dataPack.contentLen = strlen(buffer);
dataPack.position = 0;
send(s, &dataPack, dataPack.packSize, 0);
return;
}
/*获取并发送文件信息*/
struct stat statbuf;
stat(path, &statbuf);
dataPack.type = 'M';
dataPack.packSize = sizeof(DataPack);
dataPack.fileMess.fileLen = statbuf.st_size;
strncpy(dataPack.fileMess.fileName, fName, strlen(fName));
unsigned long sRe = send(s, &dataPack, dataPack.packSize, 0);
printf("成功发送文件信息数据包!\n");
/*发送文件内容*/
unsigned long sendedCount = 0; //记录已发送的数据大小
int fd = open(path, O_RDONLY); //打开文件
while(sendedCount < statbuf.st_size){
//构造文件内容数据包
DataPack filedata;
memset(&filedata, 0, sizeof(DataPack));
unsigned long readBytes = read(fd, filedata.content, MAXBUFF);
filedata.contentLen = readBytes;
filedata.type = 'D';
filedata.packSize = sizeof(DataPack);
filedata.position = sendedCount;
unsigned long sDa = send(s, &filedata, filedata.packSize, 0);
if(sDa > 0){
sendedCount += filedata.contentLen;
}
printf("成功发送数据:%ld bytes\n", sendedCount);
}
//构造结束标志数据包,标志文件传输完毕
memset(&dataPack, 0, sizeof(DataPack));
dataPack.type = 'E';
send(s, &dataPack, sizeof(DataPack), 0);
close(fd);
return;
}
/*服务器接收文件,即客户端上传文件*/
void recvFile(int s, char *fname){
unsigned long fileSize = 0; //记录接收文件的大小
unsigned long recvedCount = 0; //记录已接收的数据量大小
int fd = 0;
DataPack *dataPack = (DataPack *)malloc(sizeof(DataPack));
while(1){
memset(dataPack, 0, sizeof(DataPack));
unsigned long recvBytes = recv(s, dataPack, sizeof(DataPack), 0);
//'E'类型的数据包为错误数据包
//在文件传输完成后,会发送一个空内容(content)的错误数据包,表示文件传输完成
//也可以选择重新定义一个新的类型的数据包作为结束数据包类型
if(dataPack->type == 'E'){ //该数据包为错误数据包
write(STDOUT_FILENO, dataPack->content, dataPack->contentLen);
break;
}
//'M'类型的数据包为文件信息数据包,根据相应的信息创建文件
else if(dataPack->type == 'M'){
char path[200] = "./upload/";
strcat(path, dataPack->fileMess.fileName);
//无论什么时候都创新创建文件
fd = open(path, O_CREAT | O_TRUNC | O_RDWR, S_IRWXU);
fileSize = dataPack->fileMess.fileLen;
}
//'D'类型数据包为文件内容数据包,解析数据包内容,并写入相应文件的相应位置
else if(dataPack->type = 'D'){
lseek(fd, recvedCount, SEEK_SET);
write(fd, dataPack->content, dataPack->contentLen);
recvedCount += dataPack->contentLen;
}
}
if (fd != 0){
close(fd); //关闭文件描述符
}
}
void sendMessage(int s){
DIR *dirp;
struct dirent *direntp;
int count = 0;
DirPack *dirpack = (DirPack *)malloc(sizeof(DirPack));
memset(dirpack, 0, sizeof(DirPack));
dirpack->flag = 1;
// 打开当前目录
dirp = opendir("./resources/");
// 读取目录内容
while ((direntp = readdir(dirp)) != NULL) {
// 忽略当前目录和父目录
if (strcmp(direntp->d_name, ".") == 0 || strcmp(direntp->d_name, "..") == 0) {
continue;
}
// 将文件名发送到客户端中
char *dirfName = strdup(direntp->d_name); //获取文件名
strncpy(dirpack->content, dirfName, strlen(dirfName));
send(s, dirpack, sizeof(DirPack), 0);
// 清空数据包的内容
memset(dirpack->content, 0, sizeof(dirpack->content));
}
//构造结束数据包
dirpack->flag = 0;
send(s, dirpack, sizeof(DirPack), 0);
// 关闭目录
closedir(dirp);
}
3.2 client.cpp
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/socket.h>
#include<fcntl.h>
#include<unistd.h>
#include<arpa/inet.h>
#define port 8888
#define MAXBUFF 4096
void process_con_client(int s); //处理与服务器的连接服务
char** splitString(char* str, int& num); //以一个或多个空格分割字符串
void analyzeCommand(char *command, int s); //解析用户输入的命令
void recvFile(int s, char *fname); //接收来自服务器的文件数据包
void sendFile(int s, char *fname); //向服务器上传文件
void recvMessage(int s); //接收可下载文件目录信息
/*文件信息*/
typedef struct FileMess{
unsigned long fileLen;
char fileName[100];
} FileMess;
/*文件数据包*/
typedef struct DataPack{
char type; //'D'表示数据,'M'表示文件信息, 'E'表示错误数据包
int packSize; //整个数据包的大小
char content[MAXBUFF]; //数据包携带数据缓冲区
int contentLen; //数据包中数据的长度
unsigned long position; //数据在文件中的字节位置
FileMess fileMess; //文件信息
} DataPack;
/*目录信息数据包*/
typedef struct DirPack{
int flag; //标志位,1表示包含目录文件名,0表示结束目录发送的空数据包
char content[100];
} DirPack;
int main(int argc, char *argv[]){
int s;
struct sockaddr_in server_addr;
int err;
/*创建套接字socket*/
s = socket(AF_INET, SOCK_STREAM, 0);
if(s < 0){
printf("Create socket error!!!\n");
return -1;
}
/*设置连接服务器IP地址和端口号*/
memset(&server_addr, 0, sizeof(struct sockaddr_in));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port);
inet_pton(AF_INET, argv[1], &server_addr.sin_addr);
/*为什么客户端connect前不需要进行bind?*/
/*操作系统会自动选择一个可用的本地 IP 地址和端口号来作为客户端套接字的地址,
并将这个地址作为连接请求的源地址发送给服务器。这个过程称为“自动绑定”*/
/*连接服务器*/
err = connect(s, (struct sockaddr*)&server_addr, sizeof(struct sockaddr));
if(err < 0){
printf("Connect error!!!\n");
return -1;
}
process_con_client(s); //请求服务
close(s); //关闭连接
return 0;
}
/*处理客户端与服务器的连接*/
void process_con_client(int s){
ssize_t size = 0;
char buffer[1024];
//反复处理用户输入的命令
while(1){
memset(buffer, 0, sizeof(buffer));
size = read(STDIN_FILENO, buffer, 1024);
if(size > 0){
write(s, buffer, size);
analyzeCommand(buffer, s);
}
}
close(s);
}
/*以一个或多个空格分割字符串*/
/*返回值:指针数组*/
/*num:传出参数,字符串分割成的子串个数*/
char** splitString(char* str, int& num) {
int len = strlen(str);
char** result = new char*[len];
int count = 0;
int start = 0;
/*遍历分割字符串*/
for (int i = 0; i < len; ++i) {
if (str[i] == ' ') {
if (i - start > 0) {
int size = i - start;
result[count] = new char[size + 1]; //size+1是为了尾部添加'\0'作为截至
strncpy(result[count], &str[start], size);
result[count][size] = '\0';
++count;
}
start = i + 1;
}
}
/*若字符串不以空格结尾,则需要将字符串尾部添加*/
if (len - start > 0) {
int size = len - start;
result[count] = new char[size + 1];
strncpy(result[count], &str[start], size);
result[count][size] = '\0';
++count;
}
num = count;
return result;
}
/*解析客户端用户输入的指令*/
void analyzeCommand(char *command, int s){
int substrNum;
char** substr = splitString(command, substrNum);
/*判断用户输入的指令是否符合格式*/
/*此处仅提供三种指令,ls、download [filename]、send [filename]*/
if(substrNum > 2){
char *buffer = "The format of command is error!!!\n";
write(STDOUT_FILENO, buffer, strlen(buffer));
}
/*显示可下载的文件列表, 接收来自服务器传输过来的信息*/
if(!strcmp(substr[0], "ls\n")){
recvMessage(s);
}
/*下载指定文件*/
if(!strcmp(substr[0], "download")){
recvFile(s, substr[1]);
}
/*上传指定的文件到服务器*/
if(!strcmp(substr[0], "send")){
sendFile(s, substr[1]);
}
}
/*执行文件下载命令,接收服务器发来的数据包*/
void recvFile(int s, char *fname){
unsigned long fileSize = 0; //记录接收文件的大小
unsigned long recvedCount = 0; //记录已接收的数据量大小
int fd = 0;
DataPack *dataPack = (DataPack *)malloc(sizeof(DataPack)); //动态分配内存
while(1){
memset(dataPack, 0, sizeof(DataPack));
unsigned long recvBytes = recv(s, dataPack, sizeof(DataPack), 0);
//'E'类型的数据包为错误数据包
//在文件传输完成后,会发送一个空内容(content)的错误数据包,表示文件传输完成
//也可以选择重新定义一个新的类型的数据包作为结束数据包类型
if(dataPack->type == 'E'){
write(STDOUT_FILENO, dataPack->content, dataPack->contentLen);
break;
}
//'M'类型的数据包为文件信息数据包,根据相应的信息创建文件
else if(dataPack->type == 'M'){
char path[200] = "./download/";
char *fName = (char*)malloc(strlen(fname)-1);
strncpy(fName, fname, strlen(fname)-1);
strncat(path, fName, strlen(fname)-1);
//无论什么时候都创新创建文件
fd = open(path, O_CREAT | O_TRUNC | O_RDWR, S_IRWXU);
fileSize = dataPack->fileMess.fileLen;
}
//'D'类型数据包为文件内容数据包,解析数据包内容,并写入相应文件的相应位置
else if(dataPack->type = 'D'){
lseek(fd, recvedCount, SEEK_SET);
write(fd, dataPack->content, dataPack->contentLen);
recvedCount += dataPack->contentLen;
}
}
if (fd != 0){
close(fd); //关闭文件描述符
}
}
/*接收服务器发来的可下载文件目录数据包*/
void recvMessage(int s){
DirPack *dirpack = (DirPack *)malloc(sizeof(DirPack)); //动态分配内存
memset(dirpack, 0, sizeof(DirPack));
int count = 0;
while(1){
recv(s, dirpack, sizeof(DirPack), 0);
++count;
//接收完毕目录后会发现一个flag标志为0的数据包,表示目录数据包发送完毕
if(dirpack->flag == 0){
memset(dirpack, 0, sizeof(DirPack));
break;
}
write(STDOUT_FILENO, dirpack->content, sizeof(dirpack->content));
//排布显示
if(count%4 == 0){
write(STDOUT_FILENO, "\n", 1);
}
else {
write(STDOUT_FILENO, "\t", 1);
}
memset(dirpack, 0, sizeof(DirPack));
}
free(dirpack);
}
/*执行上传文件命令,向服务器发送文件信息和内容数据包*/
void sendFile(int s, char *fname){ //fname含有'\n'回车
DataPack dataPack;
//去除字符串fname末尾的回车
char *fName = (char*)malloc(strlen(fname)-1);
strncpy(fName, fname, strlen(fname)-1);
//判断是否存在相应名称的文件
int st = access(fName, F_OK);
if(-1 == st){
/*设置错误信息,并以错误信息类型发送数据包*/
dataPack.type = 'E';
dataPack.packSize = sizeof(DataPack);
char *buffer = "The file isn't exist!!!\n";
strncpy(dataPack.content, buffer, strlen(buffer));
dataPack.contentLen = strlen(buffer);
dataPack.position = 0;
send(s, &dataPack, dataPack.packSize, 0);
return;
}
/*获取并发送文件信息*/
struct stat statbuf;
stat(fName, &statbuf);
dataPack.type = 'M';
dataPack.packSize = sizeof(DataPack);
dataPack.fileMess.fileLen = statbuf.st_size;
strncpy(dataPack.fileMess.fileName, fName, strlen(fName));
unsigned long sRe = send(s, &dataPack, dataPack.packSize, 0);
if(sRe > 0){
printf("成功发送文件信息数据包!\n");
}
/*发送文件内容*/
unsigned long sendedCount = 0; //记录已发送的数据大小
int fd = open(fName, O_RDONLY); //打开文件
//当发送的数据量小于文件大小时则继续发送数据
while(sendedCount < statbuf.st_size){
DataPack filedata;
memset(&filedata, 0, sizeof(DataPack));
unsigned long readBytes = read(fd, filedata.content, MAXBUFF);
filedata.contentLen = readBytes;
filedata.type = 'D';
filedata.packSize = sizeof(DataPack);
filedata.position = sendedCount;
unsigned long sDa = send(s, &filedata, filedata.packSize, 0);
if(sDa > 0){
sendedCount += filedata.contentLen;
printf("成功发送数据:%ld bytes\n", sendedCount);
}
}
/*发送传输结束数据包*/
memset(&dataPack, 0, sizeof(DataPack));
dataPack.type = 'E';
send(s, &dataPack, sizeof(DataPack), 0);
close(fd);
return;
}
4. 结果测试
4.1 g++编译源代码
- server.cpp
g++ server.cpp -o server
- client.cpp
g++ client.cpp -o client
4.2 相应文件内容
- 客户端
- 服务端
4.3 运行效果
- 服务端网络配置
- client
- server
运行流程:
-
服务端运行server程序,启动监听;客户端启动client程序,主动与服务端建立连接;
-
客户端向服务端发送‘ls’指令,服务端收到‘ls’指令后,打印指令,并向客户端提供当前目录下的resources文件夹中所包含的文件目录;
客户端收到文件目录后,打印文件目录;
-
客户端向服务端发送‘download test3.txt’指令,服务端收到‘download test3.txt’指令后,打印指令,并打开resource文件夹中中test3.txt文件,读取内容与文件信息,打包成数据包发送到客户端。
客户端收到数据包后,解析数据包内容,并在download文件夹中创建相应类型的文件,将数据包的内容写入文件中。
-
客户端向服务端发送‘download test2.txt’指令,服务端收到‘download test2.txt’指令后,打印指令,并打开resource文件夹中中test2.txt文件,读取文件信息,打包成数据包发送到客户端;读取内容,发现内容为空,故不发送文件内容数据包。
客户端收到文件信息数据包,解析数据包内容,并在download文件夹中创建相应类型的文件。由于没有文件内容数据包,此时边不执行文件内容写入操作;
-
客户端向服务端发送‘send sdtos.txt’指令,服务端收到‘send sdtos.txt’指令后,打印指令。
客户端读取当前目录下的sdtos.txt文件的文件信息和文件内容,将其封装成数据包,发送到服务端;
在收到客户端发送过来的数据包后,在当前目录的upload文件夹中创建相应的文件,并向文件中写入相应的文件内容。
- 客户端
文章来源:https://www.toymoban.com/news/detail-720871.html
- 服务端
文章来源地址https://www.toymoban.com/news/detail-720871.html
到了这里,关于Linux网络编程之TCP文件传输的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!