TCP实现 FTP功能
模拟 FTP 核心原理:客户端连接服务器后,向服务器发送一个文件。文件名可以通过参数指定,服务器端接收客户端传来的文件(文件名随意)。如果文件不存在,则自动创建文件;如果文件存在,那么清空文件,然后写入。
功能介绍
所有服务器和客户端代码,基于 TCP 编写。
在同一路径下,客户端可执行代码、服务器端可执行代码 位于不同子路径下,在不同的路径下运行服务器和客户端。模拟另外一台电脑在访问服务器。
客户端和服务器链接成功后出现以下提示:四个功能
*list // 列出服务器所在目录下的文件名(除目录不显示)
*put filename // 上传一个文件
get filename // 从服务器所在路径下载文件
quit // 退出(可只退出客户端,服务器等待下一客户端连接)
注意 fgets / read 的区别:
read() 在读取数据时会将最后的回车 ‘\n’ 同时读入到 buf[] 中,但是 不会在后面加上字符串结束符 ‘\0’。 (记得手动补入 ‘\n’ )
fgets() 在读取数据时会将最后的回车 ‘\n’ 同时读入到 buf[] 中,并且 会在后面加上字符串结束符 ‘\0’。
stat 获取当前路径下文件的属性
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *filename, struct stat *buf);
// 用的时候定义一个结构体变量,将变量地址传给 struct stat * buf
功能: 通过文件名 filename 获取文件信息,并保存在 buf 所指向的结构体 stat 中
参数: pathname:文件名
buf: 存放文件属性信息(地址)
返回值:成功 0
失败:-1,更新 errno
struct stat {
dev_t st_dev; // device 文件的设备编号
ino_t st_ino; // inode 文件的i-node
mode_t st_mode; // protection 文件的类型和存取的权限(见下面内容)
nlink_t st_nlink; // number of hard links 连到该文件的硬连接数目, 刚建立的文件值为1.
uid_t st_uid; // user ID of owner 文件所有者的用户识别码(用户ID)
gid_t st_gid; // group ID of owner 文件所有者的组识别码 (组ID)
dev_t st_rdev; // device type 若此文件为装置设备文件, 则为其设备编号
off_t st_size; // total size, in bytes 文件大小, 以字节计算
unsigned long st_blksize; // blocksize for filesystem I/O 文件系统的I/O 缓冲区大小.
unsigned long st_blocks;
// number of blocks allocated 占用文件区块的个数, 每一区块大小为512 个字节.
time_t st_atime;
// time of lastaccess 文件最近一次被存取或被执行的时间, 一般只有在用 mknod, utime, read
time_t st_mtime; // 与 localtime 配合转换为 月、日、时间
// time of last modification 文件最后一次被修改的时间, 一般只有在用mknod、utime, write
time_t st_ctime;
// time of last change i-node 最近一次被更改的时间, 此参数会在文件所有者, 组, 权限被更新
};
使用:
struct stat st;
stat(argv[1], &st); // argv[1] 写路径/文件
printf(“%ld\n”, st.st_ino); // 获取文件的 inode 号
// ……
// 判断文件类型:
// 方法一:
// 以下宏需要 st_mode 参数传入
1. S_IFMT 0170000 文件类型的位遮罩(先 st_mode & S_IFMT,再得到 以下 2-11 的内容)
2、S_IFSOCK 0140000 套接字 s
3、S_IFLNK 0120000 符号连接 l
4、S_IFREG 0100000 普通文件 - if((st.st_mode & S_IFMT) == S_IFREG)
5、S_IFBLK 0060000 区块设备 b
6、S_IFDIR 0040000 目录 d
7、S_IFCHR 0020000 字符设备 c
8、S_IFIFO 0010000 管道 p
// 方法二:
#define S_ISREG(mt_mode) mt_mode & S_IFMT == S_IFREG
21、S_ISLNK (st_mode) 是否是一个连接 l if(S_ISLNK(st_mode))
22、S_ISREG (st_mode) 是否是一个普通文件 - if(S_ISREG(st_mode))
23、S_ISDIR (st_mode) 是否是一个目录 d if(S_IFDIR(st_mode))
24、S_ISCHR (st_mode) 是否是一个字符设备 c if(S_ISCHR(st_mode))
25、S_ISBLK (st_mode) 是否是一个块设备 b if(S_ISBLK(st_mode))
26、S_ISFIFO (st_mode) 是否是个管道文件 p if(S_ISIFO(st_mode))
27、S_ISSOCK (st_mode) 是否是个套接字文件 s if(S_ISSOCK(st_mode))
// 文件权限:
用户:
1. S_IRUSR (S_IREAD) 00400 文件所有者(用户)具可读取权限 r
if(st_mode & S_IRUSR);
13、S_IWUSR (S_IWRITE) 00200 文件所有者(用户)具可写入权限 w
if(st_mode & S_IWUSR);
14、S_IXUSR (S_IEXEC) 00100 文件所有者(用户)具可执行权限 x
if(st_mode & S_IXUSR);
组:
15、S_IRGRP 00040 用户组具可读取权限 r if(st_mode & S_IRGRP);
16、S_IWGRP 00020 用户组具可写入权限 w if(st_mode & S_IWGRP);
17、S_IXGRP 00010 用户组具可执行权限 x if(st_mode & S_IXGRP);
其他用户:
18、S_IROTH 00004 其他用户具可读取权限 r if(st_mode & S_IROTH);
19、S_IWOTH 00002 其他用户具可写入权限 w if(st_mode & S_IWOTH);
20、S_IXOTH 00001 其他用户具可执行权限 x if(st_mode & S_IXOTH);
ser_ftp.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <pwd.h>
#include <grp.h>
#include <time.h>
#include <fcntl.h>
#include <dirent.h>
void ls_stat(int accfd, char *buf, int size);
void get_file(int accfd, char *buf, int size);
void put_file(int accfd, char *buf, int size);
int main(int argc, char const *argv[])
{
if (argc != 2){
printf("Please input %s <port>. \n", argv[0]);
return -1;
}
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0){
perror("Failed to create a socket");
return -1;
}
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
saddr.sin_port = htons(atoi(argv[1]));
if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){
perror("Failed to bind");
return -1;
}
if (listen(sockfd, 6) < 0){
perror("Failed to listen");
return -1;
}
struct sockaddr_in caddr;
socklen_t length = sizeof(caddr);
while (1){
int accfd = accept(sockfd, (struct sockaddr *)&caddr, &length);
if (accfd < 0){
perror("Failed to accept");
return -1;
}
printf("Client IPv4: %s\tport: %d\n",
inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
char buf[256] = {};
while (1){
int receiver = recv(accfd, buf, sizeof(buf), 0);
if (receiver < 0){
perror("Failed to receive (server)");
return -1;
} else if (receiver == 0){
printf("Client exited.\n");
break;
} else {
if (!(strncmp(buf, "list", 4) && strncmp(buf, "List", 4)))
ls_stat(accfd, buf, sizeof(buf));
if (!(strncmp(buf, "put ", 4) && strncmp(buf, "Put ", 4)))
get_file(accfd, buf, sizeof(buf));
if (!(strncmp(buf, "get ", 4) && strncmp(buf, "Get ", 4)))
put_file(accfd, buf, sizeof(buf));
// if (!(strncmp(buf, "quit", 4) && strncmp(buf, "Quit", 4)))
// break;
}
}
close(accfd);
}
close(sockfd);
return 0;
}
void ls_stat(int accfd, char *buf, int size){
DIR * dir = opendir("./");
if(dir == NULL){
perror("Failed to open a directory");
return;
}
struct dirent *reader = NULL;
struct stat st;
while((reader = readdir(dir)) != NULL){
stat(reader->d_name, &st); // reader->d_name 为拿到的文件名
if(S_ISREG(st.st_mode)){ // 判断是否为普通文件
strcpy(buf, reader->d_name);
send(accfd, buf, size, 0);
}
}
strcpy(buf, "It's done.");
send(accfd, buf, size, 0);
closedir(dir);
}
void get_file(int accfd, char *buf, int size){
int fd = open(buf + 4, O_TRUNC | O_CREAT | O_WRONLY, 0666);
if(fd < 0){
perror("Failed to open this file (get_file)");
return;
}
while(1){
int receiver = recv(accfd, buf, size, 0);
if(receiver < 0){
perror("Failed to receive (get_file)");
return;
} else if (receiver == 0){
printf("Client exited. \n");
break;
} else {
if(strncmp(buf, "Upload done.", 12) == 0)
break;
write(fd, buf, strlen(buf));
}
}
close(fd);
}
void put_file(int accfd, char *buf, int size){
int fd = open(buf + 4, O_RDONLY);
if(fd < 0){
perror("Failed to open this file (put_file");
return;
}
int ret;
while((ret = read(fd, buf, size-1)) != 0){
buf[ret] = '\0';
send(accfd, buf, size, 0);
}
strcpy(buf, "Download successfully.");
send(accfd, buf, size, 0);
close(fd);
}
cli_ftp.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
void show();
void list(int sockfd, char *buf, int size);
void up_load(int sockfd, char *buf, int size);
void down_load(int sockfd, char *buf, int size);
int main(int argc, char const *argv[])
{
if (argc != 3){
printf("Please input %s <ip> <port>. \n", argv[0]);
return -1;
}
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0){
perror("Failed to create a socket");
return -1;
}
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = inet_addr(argv[1]);
saddr.sin_port = htons(atoi(argv[2]));
if (connect(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){
perror("Failed to connect");
return -1;
}
char buf[256] = {};
while (1){
show();
fgets(buf, sizeof(buf), stdin);
if (buf[strlen(buf) - 1] == '\n')
buf[strlen(buf) - 1] = '\0';
send(sockfd, buf, sizeof(buf), 0);
if (!(strncmp(buf, "list", 4) && strncmp(buf, "List", 4)))
list(sockfd, buf, sizeof(buf));
if (!(strncmp(buf, "put ", 4) && strncmp(buf, "Put ", 4)))
up_load(sockfd, buf, sizeof(buf));
if (!(strncmp(buf, "get ", 4) && strncmp(buf, "Get ", 4)))
down_load(sockfd, buf, sizeof(buf));
if (!(strncmp(buf, "quit", 4) && strncmp(buf, "Quit", 4))){
printf("Client exited. \n");
break;
}
}
close(sockfd);
return 0;
}
void show(){
putchar(10);
printf("**************** list ****************\n");
printf("*********** put <filename> ***********\n");
printf("*********** get <filename> ***********\n");
printf("**************** quit ****************\n\n");
}
void list(int sockfd, char *buf, int size){
printf("----------------------\n");
while(1){
// 接收服务器发来的目录文件名
int receiver = recv(sockfd, buf, size, 0);
if(receiver < 0){
perror("Failed to receive (client)");
return;
} else {
// 服务器发送完毕,须告知客户端,以"It's done. ......"为信号
if(strncmp(buf, "It's done.", 10) == 0)
break;
printf("%s\n", buf);
}
}
// putchar(10);
}
void up_load(int sockfd, char *buf, int size){
// 打开文件, 读
// 应当跳过 “put <filename>” 中的 "put "
int fd = open(buf + 4, O_RDONLY);
if(fd < 0){
perror("Failed to open this file (upload)");
return;
}
int ret;
while((ret = read(fd, buf, size-1))!= 0)
{
// read不会自动补入'\0',需要手动添加, 否则会与下次读取的内容合并(粘包)
buf[ret] = '\0';
send(sockfd, buf, size, 0);
}
// 读取完毕后,不能让服务器一致阻塞recv
strcpy(buf, "Upload done.");
send(sockfd, buf, size, 0);
printf("Upload successfully! \n");
close(fd);
}
void down_load(int sockfd, char *buf, int size){
// buf 中应去除 "get "
int fd = open(buf + 4, O_TRUNC | O_CREAT | O_WRONLY, 0666);
if(fd < 0){
perror("Failed to open this file (download)");
return;
}
while(1){
if((recv(sockfd, buf, size, 0)) < 0){
perror("Failed to receive (download)");
return;
}
// 如果服务器发送来 发送完毕的指令, 则客户端应该停止 recv
if(strncmp(buf, "Download successfully.", 22) == 0)
break;
write(fd, buf, strlen(buf));
}
printf("Download successfully! \n");
close(fd);
}
实现效果
UDP 编程
流程图
实现步骤
服务器端:
1)创建数据报套接字(socket(AF_INET, SOCK_DGRAM, 0));
2)绑定网络信息(bind());
3)接收信息(recvfrom()); 必须先接收信息,才能获取到客户端的 IP 和端口号!!
// 4)发送信息(sendto());
5)关闭套接字(close())。
客户端:
1)创建数据报套接字(socket());
2)指定服务器的网络信息;
3)发送信息(sendto()); 必须先发送信息!!
// 4)接收信息(recvfrom());
5)关闭套接字(close())。
函数
recvfrom 接收
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
功能:接收数据
参数:
sockfd: 套接字描述符
buf: 接收缓存区的首地址
len: 接收缓存区的大小
flags:0 接收数据 并 阻塞
MSG_DONTWAIT: 设置非阻塞
src_addr: 发送端的网络信息结构体的指针(对方的 caddr)
addrlen: 发送端的网络信息结构体的大小的指针(对方的 caddr)
返回值:
成功接收的字节个数(若接收到的数据为0: 返回0)
失败:-1
sendto 发送
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
功能:发送数据
参数:
sockfd: 套接字描述符
buf: 发送缓存区的首地址
len: 发送缓存区的大小
flags:0 发送消息并阻塞
src_addr: 接收端的网络信息结构体的指针
addrlen: 接收端的网络信息结构体的大小
返回值:
成功发送的字节个数
失败:-1
udp_ser.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netinet/ip.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
if (argc != 2){
printf("Please input %s <port>. \n", argv[0]);
return -1;
}
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0){
perror("Failed to create a socket");
return -1;
}
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1]));
saddr.sin_addr.s_addr = INADDR_ANY;
if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){
perror("Failed to bind");
return -1;
}
char buf[256] = {};
struct sockaddr_in caddr;
socklen_t length = sizeof(caddr);
while (1){
int receiver = recvfrom(sockfd, buf, sizeof(buf), 0,
(struct sockaddr *)&caddr, &length);
if (receiver < 0){
perror("Failed to receive");
return -1;
} else {
printf("Client IPv4: %s\t\tport: %d\n%s\n",
inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port), buf);
}
}
close(sockfd);
return 0;
}
udp_cli.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netinet/ip.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
if (argc != 3){
printf("Please input %s <ip> <port>. \n", argv[0]);
return -1;
}
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0){
perror("Failed to create a socket");
return -1;
}
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = inet_addr(argv[1]);
saddr.sin_port = htons(atoi(argv[2]));
char buf[256] = {};
while (1){
fgets(buf, sizeof(buf), stdin);
if (buf[strlen(buf) - 1] == '\n')
buf[strlen(buf) - 1] = '\0';
sendto(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&saddr, sizeof(saddr));
}
close(sockfd);
return 0;
}
实现效果
注意:
1、对于 TCP ,先运行服务器,客户端才能运行。
2、对于 UDP,服务器和客户端运行顺序没有先后(因为无连接),但客户端应先发送消息给服务器。
3、一个服务器可以同时连接多个客户端(可以在服务器代码里面打印 IP 和 端口号)。
4、对于 UDP,当客户端使用 send 时,需要提前 connect(这个 connect 不是代表连接的作用,而是指定客户端即将要给谁发送数据)。这样就不需要使用 sendto,用 send 就可以。
5、对于 TCP ,也可以使用 recvfrom 和 sendto,将后面的两个参数都写为 NULL 即可。
(点击跳转至 TCP 编程)