一.FTP介绍
FTP服务器(File Transfer Protocol Server)是在互联网上提供文件存储和访问服务的计算机,它们依照FTP协议提供服务。 FTP是File Transfer Protocol(文件传输协议)。 程序运行,服务端不断接收客户端指令,服务端可同时处理多个客户端接入并对指令作出解析,并把执行结果返回给客户端,客户端根据服务端对指令的解析并把由服务端传递过来的处理信息通过客户端呈现给客户,实现文件的各种操作。
二.项目介绍
Linux网络编程实现的FTP服务器,服务器由服务端和客户端组成,具有浏览远程服务端的文件和浏览客户端本地文件,同时支持对远程服务端文件的删除,存储,归档操作处理,以及客户端对远程服务端文件的上传和下载。
利用socket实现云盘的:
ls———查看服务端文件
lls———查看客户端自己的文件
cd———切换服务端目录
lcd———切换客户端自己的目录
put———上传文件
get———下载文件
三.客户端服务器的搭建
我们首先mkdir一个FTP录目,在此目录下创建一个client.c文件
之后写main函数,我们的main函数是带参数的,之后我们客户端需要用输入地址和端口号与服务端连接,这里需要两个参数,加上命令一共三个参数。进入函数需要先判断输入的参数是否正确,不正确直接退出。
perror函数用于输出错误信息,列子如下
经过上面的准备工作,现在我们正式进入客观端的实现
1.socket 创建客户端的套接字,构建客户端和服务端发送和接收信息的桥梁
客户端与服务端之间的信息交流是通过套接字实现的
关于socket想要具体学习的同学可以看这篇文章一文带你了解socket网络编程以及详解过程和原理-CSDN博客
我接下来只讲用到内容
下图是socket通信的流程
图一
int socket(int domain,int type,int protocol);
参数
domain
AF_INET (IPV4)
AF_INET6 (IPV6)
AF_UNIX,AF_LOCAL
AF_NETLINK
AF_PACKET
type
SOCK_STREAM: 流式套接字,唯一对应于TCP (可靠)
SOCK_DGRAM:数据报套接字,唯一对应着UDP (不可靠)
SOCK_RAW:原始套接字
protocol
一般填0,原始套接字编程时需填充
返回值
成功返回文件描述符
出错返回-1
socket函数参数的选择我们大多数使用IPV4 流式套接字,记住就行。这里重要的是使用该函数成功之后返回的文件描述符,这是我们后续需要使用的,我们可以使用一个int c_fd去接收这个文件描述符。
int c_fd;
c_fd = socket(AF_INET,SOCK_STREAM,0);
if(c_fd ==-1)
{
perror("socket");
exit(1);
}
我们使用socket成功创建套接字之后,需要去填充基于Internel通信结构体
这个结构体有四个成员我们需要填写前三个
struct sockaddr_in{
as_family_t sin_family; //网络协议,IP4 or IP6
in_port_t sin_port; //端口号
struct in_addr sin_addr; //IP地址,如192.168.128
sin_zero ,
}
填写端口号的时候需要转换字节序,我们是从主机输入的,需要转换成网络字节序以便通信,
主机字节序转到网络字节序的函数又如下两个
u_long htonl(u_long hostlong); //把uint32_t类型从主机序转换到网络序
u_short htons(u_short short); // 把uint16_t类型从主机序转换到网络序
从网络序转换到主机序
uint32_t ntohl(uint32_t netlong); //把uint32_t类型从网络序转换到主机序
uint16_t ntohs(uint16_t netshort); //把uint16_t类型从网络序转换到主机序
我们的IP地址也是需要转换的,使用inet_aton()函数
inet_aton() 转换网络主机地址ip(如192.168.1.10)为二进制数值,并存储在struct in_addr结构中,即第二个参数*inp,函数返回非0表示cp主机有地有效,返回0表示主机地址无效。(这个转换完后不能用于网络传输,还需要调用htons或htonl函数才能将主机字节顺序转化为网络字节顺序)
int inet_aton(const char *cp, struct in_addr *inp);
struct sockaddr_in c_addr;//先创建 struct sockaddr_in结构体
memset(&c_addr,0,sizeof(struct sockaddr_in));
c_addr.sin_family =AF_INET; //IPV4
c_addr.sin_port = htons(atoi(argv[2])); //端口号
inet_aton(argv[1],&c_addr.sin_addr); //IP地址
到此我们socket的创建才算完成,整体代码如下
2.使用客户端连接函数与服务端进行连接
按照图一的流程,接下来我们需要去使用connect()函数与服务端进行连接
客户端连接函数connect()
int connect (int sockfd, struct sockaddr * serv_addr, int addrlen)
参数:
sockfd:通过socket()函数拿到的fd
addr:struct sockaddr的结构体变量地址
addrlen:地址长度
返回值:
成功,返回0
失败,返回-1
之前定义的结构体c_addr类型与此函数的不同,所以需要强制转换成struct sockaddr
clent = sizeof(struct sockaddr_in);
connect(c_fd,(struct sockaddr *)&c_addr,clent)
到这一步建立通信的工作就完成了,如果连接成功就可以进行收发数据了
3.客户端收发数据
连接成功之后,客户端是要能够一直收发数据的,直到退出服务器。因此这个过程是在while中实现。
首先我们服务器需要知道我们从键盘输入的内容,这就需要受用gets函数从标准输入获取输入的内容,获取内容后再输出到屏幕让用户知道自己输入了什么内容。除了受用gets函数还可以使用fgets函数,只是用法不同。
gets(Writebuf);
//fgets(Writebuf,sizeof(Writebuf),stdin);
//Writebuf[strcspn(Writebuf, "\n")] = '\0';
printf("cmd:%s\n",Writebuf);
获取内容之后我们需要将内容写到套接字的文件描述符c_fd中用于与服务器的通信,于此同时需要一个函数去解析我们输入的指令内容,去执行相关的操作。到此客户端的基本框架就完成了,之后我们需要根据功能写相关的函数。
客户端完整框架代码如下,具体功能实现的代码我们再讲完服务端之后讲解,先做到对整体框架的掌握再进行具体功能的实现。使用strtok函数进行字符串分割-CSDN博客
int change(char cmd[128]) //用于判断是哪个一个指令
{
if(!strcmp("lls",cmd))
{
return 1;
}
else if(!strcmp("ls",cmd))
{
return 2;
}
else if(!strcmp("g",cmd))
{
return 3;
}
else if(strstr(cmd,"cd") !=NULL)
{
return 4;
}
else if(strstr(cmd,"lcd") !=NULL)
{
return 5;
}
else if(strstr(cmd,"get") !=NULL)
{
return 6;
}
else if(strstr(cmd,"put") !=NULL)
{
return 7;
}
}
void choosecmd(char cmd[128],int c_fd)//根据指令执行相关的操作,以实现指令功能
{
int ret =change(cmd);
printf("cmd = %s,ret =%d\n",cmd,ret);
char *p =(char *)malloc(8000);
switch(ret)
{
case 1:
system("ls");
break;
case 2:
read(c_fd,p,1024);
printf("%s\n",p);
memset(p,0,1024);
break;
case 3:
printf("unconnection\n");
write(c_fd,"away host ",128);
close(c_fd);
exit(1);
break;
case 4:
printf("hello world4\n");
break;
case 5:
p = getbind(cmd);
chdir(p);
memset(p,0,8000);
break;
case 6:
getmessage(cmd,c_fd);
break;
case 7:
putmessage(cmd,c_fd);
break;
}
}
int main(int argc,char **argv)
{
char Writebuf[128];
char Readbuf[1024];
int c_fd;
struct sockaddr_in c_addr;
int clent;
if(argc !=3)
{
perror("argc");
exit(1);
}
c_fd = socket(AF_INET,SOCK_STREAM,0);
if(c_fd ==-1)
{
perror("socket");
exit(1);
}
memset(&c_addr,0,sizeof(struct sockaddr_in));
c_addr.sin_family =AF_INET;
c_addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1],&c_addr.sin_addr);
//connect
printf("hello world\n");
clent = sizeof(struct sockaddr_in);
if(connect(c_fd,(struct sockaddr *)&c_addr,clent) ) //connect successe return 0,fail return-1
{
perror("connect");
exit(1);
}
//waite send
printf("coneect .....\n");
while(1)
{
gets(Writebuf);
//fgets(Writebuf,sizeof(Writebuf),stdin);
//Writebuf[strcspn(Writebuf, "\n")] = '\0';
printf("cmd:%s\n",Writebuf);
write(c_fd,Writebuf,strlen(Writebuf));
choosecmd(Writebuf,c_fd);
printf("------------------cmd----------------\n>");
memset(Writebuf,0,strlen(Writebuf));
}
return 0;
}
四.服务端服务器的搭建
1.socket 创建用户端的套接字,构建客户端和服务端发送和接收信息的桥梁
这里的步骤与客户端的一致,直接上代码。需要说明的的是服务端不仅需要创建创建服务端的网络通信结构体,还需要创建一个客户端的,用于将服务端的信息传到客户端。
2.bind绑定服务器端口号与IP地址
bind()的作用及原理:
-
作用:绑定服务器的IP地址和端口号。
-
原理:通过bind()函数,服务器指定一个本地地址(IP和端口),以便客户端可以连接到这个地址、
int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen)
第一个参数:socket()函数返回的文件描述符fd
第二个参数:通信结构体
第三个参数:通信结构体的大小
3.listen监听
-
isten()的作用及原理:
-
作用:监听传入的连接请求。
-
原理:listen()将socket置于被动模式,允许它接受客户端的连接请求;它设置了一个队列来管理待处理的连接。
-
引用支持:“监听指定的socket地址”,“服务器socket监听端口号请求,随时准备接收客户端发来的连接”
-
listen()函数
int listen(int sockfd,int backlog);
参数:
sockfd: 通过socket()函数拿到的fd;
backLog:同时允许几路客户端和服务器进行正在连接的过程(正在三次握手),一般填5。
内核中服务器的套接字fd会维护2个链表
1.正在三次握手的客户端链表(数量=2*backlog+1)
2.已经建立好连接的客户端链表(已经完成三次握手分配好了的newfd)
返回值:
成功返回0
3.accept堵塞等待客户端连接请求
我们accept需要在while死循环中,当接收到请求之后去fork一个子进程,使用read()用于接收客户端发送的指令,使用choosecmd()去解析指令内容之后执行相关的操作。服务端的框架就是这些步骤。到这里我们服务端、客户端的框架都实现了,之后就可以写相关函数去执行命令了。
accept()函数
阻塞等待客户端连接请求
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数
sockfd:经过前面socket()创建并通过bind(),listen()设置过的fd
addr:指向存放地址信息的结构体的首地址
获取客户端IP地址和端口号
addrlen:存放地址信息的结构体的大小
返回值
成功,返回返回已经建立连接的新的newfd
出错,返回-1
五.功能实现
要实现各种指令,首先我们要知道用户输入了什么指令,确认指令之后才能去执行相对的操作。这里我们客户端与服务端都使用一个choosecmd()与change()函数去共同完成。
change()函数我们去字符串比对函数strcmp去识别lls、ls、g等,使用strstr去识别cd、put等指令,识别之后返回数值,在choosemcd函数中调用change函数,使用switch函数去根据返回的数值执行对应的操作。
int change(char cmd[128])
{
if(!strcmp("ls",cmd))
{
return 1;
}
.........
}
void choosecmd(char cmd[128],int c_fd)
{
ret = change(cmd);
switch(ret)
{
case 1:
fdb =popen("ls","r");//only read
fread(freadbuf,sizeof(freadbuf),1,fdb);
write(c_fd,freadbuf,sizeof(freadbuf));
memset(freadbuf,0,sizeof(freadbuf));
printf("ok\n");
break;
............
1.lls指令
这个指令是查看客户端本地的目录与文件,我们直接使用system("ls"),就可以查看,不需要服务端那边进行操作
2.ls指令
我们输入ls之后,服务端那边会接收到,之后也是和客户端这边一样去解析指令,解析到之后去执行ls这个指令操作。
FILE *fdb;
使用popen打开“ls”文件