云计算百科
云计算领域专业知识百科平台

网络编程练手-轻量化FTP文件传输服务器

前言

最近在学习网络编程,此项目适合新手小白练手,熟悉tcp/ip,套接字编程。(基于C语言

一、项目概述

        本项目实现了一个轻量级的 FTP(File Transfer Protocol)服务器与客户端系统,借助自定义协议达成了文件列表查看、文件获取和文件上传等基础文件传输功能。通过多线程技术,服务器能够同时处理多个客户端请求,确保了系统的并发处理能力。

二、项目功能

服务器端

1.多客户端处理:采用多线程技术,可同时处理多个客户端的连接和请求

2.指令处理:支持 LIST、GET、PUSH QUIT 指令,能完成文件列表查看、文件下载、文件上传和断开连接操作。

3.错误处理:对文件操作(如打开、创建、读取等)失败情况进行处理,记录错误信息并向客户端返回相应状态。

客户端

1.用户交互:提供命令行界面,用户可输入 list、get、push、quit 和 cls 指令。

2.指令发送与响应处理:能发送指令到服务器,并处理服务器返回的响应,如显示文件列表、保存下载文件等。

3.错误处理:在文件操作(如打开、保存等)失败或服务器返回push错误消息时,显示相应错误信息

三、自定义协议

指令类型

指令类型 说明
LIST 列出服务器指定目录下的文件列表
GET 从服务器获取指定的文件
PUSH 向服务器上传指定文件
QUIT        关闭于服务器的连接

数据包结构

分为请求数据包 和 响应数据包 两个大类。

每个数据包中包含数据包头

        RequestHeader:包含 `cmd`(请求指令)和 `body_size`(请求包体大小)两个字段。

        ResponseHeader:包含 status(响应状态)和 body_size(响应包体大小)两个字段。

//自定义数据最大长度
#define MSG_SIZE 1024

// 请求包头结构体,包含请求的指令和包体大小
typedef struct RequestHeader
{
Command cmd; // 请求指令,使用前面定义的Command枚举类型
uint16 body_size; // 请求包体大小,使用无符号短整型
} RequestHeader;

// 响应包头结构体,包含响应的状态和包体大小
typedef struct ResponseHeader
{
Status status; // 响应状态,使用前面定义的Status枚举类型
uint16 body_size; // 响应包体大小,使用无符号短整型
} ResponseHeader;

// 请求数据包结构体,包含请求包头和请求包体
typedef struct RequestPacket
{
RequestHeader header; // 请求包头
char body[MSG_SIZE]; // 请求包体,最大长度为MSG_SIZE
} RequestPacket;

// 响应数据包结构体,包含响应包头和响应包体
typedef struct ResponsePacket
{
ResponseHeader header; // 响应包头
char body[MSG_SIZE]; // 响应包体,最大长度为MSG_SIZE
} ResponsePacket;

四、代码实现

服务器端

        采用多线程的方式实现,根据客户端发送的请求数据包中的cmd指令,从而执行相关的操作

myftp_server.c

#include "myftp_server.h"

static int sockfd=-1;
static int confd=-1;

int main(int argc, char const *argv[])
{
//提示输入参数
if (argc != 2)
{
fprintf(stderr, "Usage: %s <server-port> \\n", argv[0]);
return 1;
}

init_server(atoi(argv[1]));

server_run();

close(sockfd);
close(confd);
return 0;
}

//初始化服务器
void init_server(uint16 port)
{
//创建通信节点,流式套接字
sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1)
error_exit("socket error");

//设置套接字的选项
int opt_val = 1;//表示开启地址重用功能
int r = setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&opt_val,4);
if(r == -1)
error_exit("set socketopt error");

//printf("socketopt val:%d\\n",opt_val);

//获取套接字选项的值
int sockval = -1;
int len = sizeof(int);
r = getsockopt(sockfd,SOL_SOCKET,SO_RCVBUF,&sockval,&len);
if(r == -1)
error_exit("getsockopt error");
//printf("socketopt len:%d\\n",sockval);

//绑定通信地址
struct sockaddr_in addr;
bzero(&addr,sizeof(addr)); //把结构清零
addr.sin_family = AF_INET; //表示使用IPV4
addr.sin_port = htons(port); //设置端口号,网络字节序 atoi(argv[1])
addr.sin_addr.s_addr = INADDR_ANY;

r = bind(sockfd,(struct sockaddr*)&addr,sizeof(addr));
if(r == -1)
error_exit("bind error");

//监听,监听完成后,sockfd称为监听套接字,只能用来接收连接,不能用来通信
r = listen(sockfd,10);
if(r == -1)
error_exit("listen error");

printf("Server Init Success\\n");
}

//服务器连接操作
void server_run(void)
{
//初始化客户机信息
struct sockaddr_in caddr;
bzero(&caddr,sizeof(caddr));
socklen_t len = sizeof(caddr);

while (1)
{
//接受连接,如果没有连接,该函数会阻塞,如果有,则返回一个连接套接字,后续的数据通信使用这个连接套接字
confd = accept(sockfd,(struct sockaddr *)&caddr,&len);
if(confd == -1)
error_exit("connect error");
//上线提示
printf("client:%s on line\\n",inet_ntoa(caddr.sin_addr));

//创建线程进行处理
pthread_t tid;
int r = pthread_create(&tid,NULL,handle_connection,(void *)&confd);
if(r != 0)
error_exit("create pthread error");
}
}

//处理连接 判定命令
void *handle_connection(void * arg)
{
//设置线程分离属性
pthread_detach(pthread_self());

int confd = *(int*)arg;

//接收请求数据包
RequestPacket packet;

//建立连接 持续运行
while (1)
{
int r = myread(confd,(char *)&packet,sizeof(packet));
if(r == 0)
{
//下线提示
struct sockaddr_in caddr;
bzero(&caddr,sizeof(caddr));
socklen_t len = sizeof(caddr);
getpeername(confd,(struct sockaddr *)&caddr,&len);
printf("client:%s off line\\n",inet_ntoa(caddr.sin_addr));
close(confd);
pthread_exit(NULL);
}
printf("r = %d\\n",r);
printf("cmd = %d\\n",packet.header.cmd);
printf("body %s\\n",packet.body);
printf("body size %d \\n",packet.header.body_size);

switch (packet.header.cmd)
{
case LIST:
handle_LIST(packet);
break;
case GET:
handle_GET(packet);
break;
case PUSH:
handle_PUSH();
break;
case QUIT:
handle_QUIT(packet);
break;
default:
break;
}
bzero(&packet,sizeof(packet));
}
}

void handle_LIST(RequestPacket packet)
{
char path[100] ={};
memcpy(path,packet.body,ntohs(packet.header.body_size));
// 处理ls指令:打开服务目录,读目录,把读到的文件名发送给客户端
DIR *pdir = opendir((const char *)packet.body);
if (pdir == NULL)
error_exit("open service dir error");

struct dirent *pent;
// 构建消息包
ResponsePacket res_packet;
res_packet.header.status = SUCCESS;
while (1)
{
errno = 0;
pent = readdir(pdir);
if (pent == NULL)
{
if (errno != 0)
perror("readdir");
break;
}
if (strcmp(pent->d_name, ".") == 0 || strcmp(pent->d_name, "..") == 0)
continue;

res_packet.header.body_size = htons(strlen(pent->d_name));
strcpy(res_packet.body, pent->d_name);
write(confd, &res_packet, sizeof(res_packet.header) + ntohs(res_packet.header.body_size));
}
res_packet.header.body_size = 0;
write(confd, &res_packet, sizeof(res_packet.header) + res_packet.header.body_size);

closedir(pdir);
}

void handle_GET(RequestPacket packet)
{
char path[100] = {};
memcpy(path, packet.body, ntohs(packet.header.body_size));

// 打开文件
int fd = open(path, O_RDONLY);
if (fd == -1) {
error_exit("server open file error");
// 发送错误响应给客户端
ResponsePacket error_packet;
error_packet.header.status = ERROR;
error_packet.header.body_size = htons(0);
write(confd, &error_packet, sizeof(error_packet.header));
return;
}

while (1)
{
ResponsePacket res_packet;
memset(&res_packet, 0, sizeof(res_packet));
int r = read(fd, res_packet.body, sizeof(res_packet.body));
if (r == -1) {
error_exit("server read file error");
break;
}
if (r == 0)
break;

res_packet.header.body_size = htons(r);
res_packet.header.status = SUCCESS;

// 发送数据包
int bytes_sent = write(confd, &res_packet, sizeof(res_packet.header) + r); //头部+读到的文件长度
if (bytes_sent == -1) {
error_exit("write error");
break;
}
}

// 发送结束标志(空包)
ResponsePacket end_packet;
end_packet.header.status = SUCCESS;
end_packet.header.body_size = htons(0);
write(confd, &end_packet, sizeof(end_packet.header));

close(fd);
printf("server send file success\\n");
}

void handle_PUSH()
{
// 接收客户端保存的文件名
RequestPacket filename;
int r = myread(confd, (char *)&filename, sizeof(filename));
if (r <= 0) {
error_exit("Failed to receive filename");
return;
}

// 清除换行符
filename.body[strcspn(filename.body, "\\n")] = 0;

// 打开文件
int fd = open((const char *)filename.body, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
error_exit("server open file error");
return;
}

while (1)
{
ResponsePacket revc_packet;
memset(&revc_packet, 0, sizeof(revc_packet));

// 先读取头部
r = myread(confd, (char *)&revc_packet.header, sizeof(revc_packet.header));
if (r <= 0)
break;

uint16_t body_size = ntohs(revc_packet.header.body_size);

// 检查是否为结束标志
if (body_size == 0) {
printf("Received end of file marker\\n");
break;
}

// 读取包体
r = myread(confd, revc_packet.body, body_size);
if (r != body_size) {
error_exit("Error receiving file data");
break;
}

// 写入文件
r = write(fd, revc_packet.body, body_size);
if (r == -1) {
error_exit("Error writing to file");
break;
}
}

close(fd);
printf("File upload success\\n");

// 发送上传成功响应
ResponsePacket success_packet;
success_packet.header.status = SUCCESS;
success_packet.header.body_size = htons(0);
write(confd, &success_packet, sizeof(success_packet.header));
}

void handle_QUIT(RequestPacket packet)
{
//下线提示
struct sockaddr_in caddr;
bzero(&caddr,sizeof(caddr));
socklen_t len = sizeof(caddr);
getpeername(confd,(struct sockaddr *)&caddr,&len);
printf("client:%s off line\\n",inet_ntoa(caddr.sin_addr));
close(confd);
pthread_exit(NULL);
}

myftp_server.h

#ifndef _MYFTP_SERVER_H
#define _MYFTP_SERVER_H

#include "mydatabase.h"
#include "stdio.h"
#include "stdlib.h"
#include <sys/types.h>
#include <sys/socket.h>
#include "netinet/in.h"
#include "arpa/inet.h"
#include "strings.h"
#include "string.h"
#include "unistd.h"
#include "fcntl.h"
#include "pthread.h"
#include <dirent.h>
#include "errno.h"

void init_server(uint16 port);
void server_run(void);
void *handle_connection(void * arg);
void handle_LIST(RequestPacket packet);
void handle_GET(RequestPacket packet);
void handle_PUSH();
void handle_QUIT(RequestPacket packet);

#endif

客户端

myftp_client.c

#include "myftp_client.h"

static int confd = -1;

int main(int argc, char const *argv[])
{
//输入参数提示
if (argc != 3)
{
fprintf(stderr, "%s <server-ip> <server-port> \\n", argv[0]);
return 1;
}
//初始化
init_client(argv[1],atoi(argv[2]));
//运行
client_run();
//关闭连接
client_close();

return 0;
}

void init_client(const char * ip,uint16 port)
{
//创建套接字
confd = socket(AF_INET,SOCK_STREAM,0);
if(confd == -1)
error_exit("socket error");

//连接服务器
//先准备服务器端的通信地址
struct sockaddr_in addr;
bzero(&addr,sizeof(addr)); //把结构清零
addr.sin_family = AF_INET; //表示使用IPV4
addr.sin_port = htons(port); //设置端口号,网络字节序
addr.sin_addr.s_addr = inet_addr(ip);
//再连接
int r = connect(confd,(struct sockaddr*)&addr,sizeof(addr));
if(r == -1)
error_exit("client connect error");
}

void client_run(void)
{
while(1)
{
char buf[256] = {0};
fgets(buf,sizeof(buf),stdin);
//截取命令 和 参数
char cmd[10] = {0};
char path[256] = {0};
sscanf(buf,"%s%s",cmd,path);

RequestPacket packet;

memset(&packet,0,sizeof(packet));
memcpy(packet.body,path,strlen(path));
packet.header.body_size = htons(strlen(path));

if (strcmp(cmd, "list") == 0)
{
packet.header.cmd = LIST;

int r = write(confd, &packet, sizeof(packet));

handle_list();
}
else if (strcmp(cmd, "get") == 0)
{
packet.header.cmd = GET;

int r = write(confd, &packet, sizeof(packet));

handle_get();
}
else if (strcmp(cmd, "push") == 0)
{
packet.header.cmd = PUSH;

int r = write(confd, &packet, sizeof(packet));

handle_push(packet);
}
else if (strcmp(cmd, "quit") == 0)
{
handle_quit();
}
else if (strcmp(cmd, "cls") == 0)
{
system("clear");
}
}
}

void client_close(void)
{
close(confd);
}

void handle_list(void)
{
printf("PRINT LIST\\n");
printf("———————————-\\n");
while(1)
{
ResponsePacket revc_packet;
memset(&revc_packet,0,sizeof(revc_packet));
myread(confd,(char *)&revc_packet.header,sizeof(revc_packet.header));
revc_packet.header.body_size = ntohs(revc_packet.header.body_size);
if(revc_packet.header.body_size == 0) //如果包头的数据长度为0则退出
break;
myread(confd,(char *)&revc_packet.body,revc_packet.header.body_size);
printf("%s\\n", revc_packet.body);
}
printf("———————————-\\n");
}

void handle_get()
{
printf("Start get file\\n");
printf("———————————-\\n");

printf("save file as\\n");
char filename[20] = {0};
fgets(filename, sizeof(filename), stdin);
filename[strcspn(filename, "\\n")] = 0; // 清除换行符

int fd = open((const char *)filename, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
error_exit("client open file error");
return;
}

while (1)
{
ResponsePacket revc_packet;
memset(&revc_packet, 0, sizeof(revc_packet));

// 先读取头部
int r = myread(confd, (char *)&revc_packet.header, sizeof(revc_packet.header));
if (r <= 0)
break;

uint16_t body_size = ntohs(revc_packet.header.body_size);

// 如果是结束标志(空包),则退出循环
if (body_size == 0) {
printf("Received end of file marker\\n");
break;
}

// 再读包体
r = myread(confd, revc_packet.body, body_size);
if (r != body_size) {
error_exit("client read error");
break;
}

// 写入文件
int bytes_written = write(fd, revc_packet.body, body_size);
if (bytes_written == -1) {
error_exit("client write error");
break;
}
}

close(fd);
printf("———————————-\\n");
printf("Get file Success\\n");
}

void handle_push(RequestPacket packet)
{
printf("Start Push\\n");
printf("———————————-\\n");
char path[100] = {0};
memcpy(path, packet.body, ntohs(packet.header.body_size));

printf("Save file as\\n");

// 发送保存文件名
RequestPacket filenamepacket;
memset(&filenamepacket, 0, sizeof(filenamepacket));
filenamepacket.header.cmd = PUSH;
fgets(filenamepacket.body, sizeof(filenamepacket.body), stdin);
filenamepacket.header.body_size = htons(strlen(filenamepacket.body));

int r = write(confd, &filenamepacket, sizeof(filenamepacket));
if (r == -1) {
error_exit("Failed to send filename");
return;
}

// 打开文件
int fd = open(path, O_RDONLY);
if (fd == -1) {
error_exit("client open file error");
return;
}

// 分块发送文件内容
while (1)
{
ResponsePacket res_packet;
memset(&res_packet, 0, sizeof(res_packet));

int r = read(fd, res_packet.body, sizeof(res_packet.body));
if (r == -1) {
error_exit("client read file error");
break;
}

// 发送文件内容块
res_packet.header.body_size = htons(r);
res_packet.header.status = SUCCESS;

if (r > 0) {
r = write(confd, &res_packet, sizeof(res_packet.header) + r);
if (r == -1) {
error_exit("client write error");
break;
}
} else {
// 文件读取完毕,发送结束标志
res_packet.header.body_size = htons(0);
write(confd, &res_packet, sizeof(res_packet.header));
break;
}
}

close(fd);

// 等待服务器确认
ResponsePacket confirm_packet;
memset(&confirm_packet, 0, sizeof(confirm_packet));
r = myread(confd, (char *)&confirm_packet.header, sizeof(confirm_packet.header));
if (r > 0 && ntohs(confirm_packet.header.body_size) == 0) {
printf("———————————-\\n");
printf("Push file Success\\n");
} else {
printf("Push file failed\\n");
}
}

void handle_quit(void)
{
printf("Bye Bye~\\n");
exit(0);
}

myftp_client.h

#ifndef __MYFTP_CLIENT_H
#define __MYFTP_CLIENT_H

#include "mydatabase.h"
#include "stdio.h"
#include "stdlib.h"
#include <sys/types.h>
#include <sys/socket.h>
#include "netinet/in.h"
#include "arpa/inet.h"
#include "strings.h"
#include "string.h"
#include "unistd.h"
#include "fcntl.h"
#include "signal.h"

void init_client(const char * ip,uint16 port);
void client_run(void);
void client_close(void);

void handle_list(void);
void handle_get();
void handle_push(RequestPacket packet);
void handle_quit(void);

#endif

数据包定义

        定义了请求数据包、响应数据包、命令枚举、和状态枚举

mydatabase.h

#ifndef __MYDATABESE_H
#define __MYDATABESE_H

#include "stdio.h"
#include "stdlib.h"
#include "unistd.h"
#include "string.h"
#include "arpa/inet.h"
#include "sys/socket.h"
#include "pthread.h"
#include "malloc.h"

//自定义数据最大长度
#define MSG_SIZE 1024

typedef unsigned int uint32;
typedef unsigned short uint16;

//定义命令枚举
typedef enum Command
{
LIST, //列出当前文件
GET, //下载文件
PUSH, //上传文件
QUIT //退出指令
} Command;

//定义响应状态
typedef enum Status{
SUCCESS, //操作成功
ERROR //操作失败
} Status;

// 告诉编译器按1字节对齐结构体,避免字节对齐带来的内存填充问题,确保结构体在不同平台上的内存布局一致
#pragma pack(1)

// 请求包头结构体,包含请求的指令和包体大小
typedef struct RequestHeader
{
Command cmd; // 请求指令,使用前面定义的Command枚举类型
uint16 body_size; // 请求包体大小,使用无符号短整型
} RequestHeader;

// 响应包头结构体,包含响应的状态和包体大小
typedef struct ResponseHeader
{
Status status; // 响应状态,使用前面定义的Status枚举类型
uint16 body_size; // 响应包体大小,使用无符号短整型
} ResponseHeader;

// 请求数据包结构体,包含请求包头和请求包体
typedef struct RequestPacket
{
RequestHeader header; // 请求包头
char body[MSG_SIZE]; // 请求包体,最大长度为MSG_SIZE
} RequestPacket;

// 响应数据包结构体,包含响应包头和响应包体
typedef struct ResponsePacket
{
ResponseHeader header; // 响应包头
char body[MSG_SIZE]; // 响应包体,最大长度为MSG_SIZE
} ResponsePacket;

// 恢复默认的字节对齐方式
#pragma pack()

// 错误处理函数,用于输出错误信息并终止程序
static void error_exit(const char *msg)
{
perror(msg); // 输出错误信息
exit(1); // 终止程序,返回错误码1
}

// 从文件描述符中读取指定大小的数据的函数,确保读取到足够的数据
static int myread(int fd, char *buf, int size)
{
int readed_bytes = 0; // 已读取的字节数
int ret; // 每次read函数的返回值
while (1)
{
// 从文件描述符fd中读取数据到buf + readed_bytes位置,最多读取size – readed_bytes字节
ret = read(fd, buf + readed_bytes, size – readed_bytes);
if (ret == -1)
exit(1); // 读取出错,终止程序
if (ret == 0)
break; // 读取到文件末尾,退出循环

readed_bytes += ret; // 更新已读取的字节数
if (readed_bytes == size)
break; // 已读取到指定大小的数据,退出循环
}
return readed_bytes; // 返回实际读取的字节数
}

#endif

五、编译与运行

编译项目

在项目根目录下,使用 Makefile 进行编译:

make server
make client

此命令将生成 server 和 client 可执行文件。

运行服务器

./server <server-port>

  • server-port 是服务器监听的端口号,例如 8888。

运行客户端

./client <server-ip> <server-port>

  • server-ip 是服务器的 IP 地址。

  • server-port 是服务器监听的端口号,需与启动服务器时指定的端口号一致。

Makefile

server: myftp_server.o
gcc $^ -o $@ -pthread

client: myftp_client.o
gcc $^ -o $@ -pthread

clean:
rm *.o -rf
rm server client -rf

六、运行示例

list指令

结果如图

get指令

输入 要获取的文件./example.bmp(路径+文件名)

再次输入将此文件重命名信息  ./new.bmp (路径+文件名)

push指令

输入 要发送的文件./example.bmp(路径+文件名)

再次输入将此文件在客户端重命名信息  ./new1.bmp (路径+文件名)

quit指令

客户端退出提示

服务端提示下线

七、注意事项

1.代码中使用了 pthread 库进行多线程编程,编译时需链接该库。

2.请确保服务器和客户端在同一网络环境中,且防火墙允许相应端口的通信。

3.在传输大文件的时候,如果觉得传输的速度较慢,可以将宏定义的MSG_SIZE调大

结尾

如果你发现项目存在问题或有改进建议,欢迎留言

赞(0)
未经允许不得转载:网硕互联帮助中心 » 网络编程练手-轻量化FTP文件传输服务器
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!