开发环境:linux环境
项目功能:实现客户端对服务端文件的远程访问,远程传输功能。类似于无界面的FTP服务器。
对于本地客户端功能有:
1)lls显示当前目录有哪些文件
2)lpwd打印当前工作目录
3)lcd xx 进入xx文件夹
4) quit退出
对于服务端功能有:
1)put xxx上传客户端的xxx文件到服务端
2)get xxx从服务端下载xxx文件到客户端
3)pwd打印服务端的当前工作目录
4)ls显示服务端当前目录有哪些文件
5)cd xx进入xx文件夹
开发步骤:
1.对于服务端会使用socket函数创建套接字,bind函数添加信息(IP地址和端口号),listen函数监听网络连接,accept函数接收客户端的连接。对于客户端会使用socket函数创建套接字,connect()函数连接服务器。
函数int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen)中第二个参数对应的结构体如下:
struct sockaddr {
sa_family_t sa_family; /*协议族*/
char sa_data[14]; /*端口号*/
}
同等替换:
struct sockaddr_in {
__kernel_sa_family_t sin_family; /* 协议族 */
__be16 sin_port; /* 端口号 */
struct in_addr sin_addr; /* IP地址结构体 */
/* 填充 没有实际意义,只是为跟sockaddr结构在内存中对齐,这样两者才能相互转换 */
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)];
};
如何找到这个结构体?
1.切换路径至 /usr/include/
2.使用指令 grep "struct sockaddr_in {" -nir 查找
3.vi 编辑进入该头文件
字节序转换api
uint32_t htonl(uint32_t hostlong);//返回网络字节序的值
uint16_t htons(uint16_t hostshort);//返回网络字节序的值
uint32_t ntohl(uint32_t netlong);//返回主机字节序的值uint21_t
uint16_t ntohs(uint16_t netshort);//返回主机字节序的值
h代表host,n代表net,s代表short(两个字节),l代表long(4个字节),通过上面的4个函数可以实现主机字节序和网络字节序之间的转换。
地址转换API
int inet_aton(const char *cp, struct in_addr *inp);
把字符串形式的“192.168.1.123”转为网络能识别的格式
char *inet_ntoa(struct in_addr in);
把网络格式的ip地址转为字符串形式
服务端代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <errno.h>
#include <arpa/inet.h>
int main (int argc,char *argv[])
{
int s_fd;
int bind_ret;
int listen_ret;
int c_fd;
struct sockaddr_in c_sockaddr;
/*socket创建套接字*/
s_fd = socket(AF_INET,SOCK_STREAM,0);
if(s_fd == -1)
{
printf("socket error!\n");
perror("error is:");
exit(-1);
}
/*bind添加信息:IP地址和端口号*/
struct sockaddr_in sockaddr;
sockaddr.sin_family = AF_INET;
sockaddr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1],&sockaddr.sin_addr);
/*命令行参数判断*/
if(argc != 3)
{
printf("param error!\n");
exit(-1);
}
bind_ret = bind(s_fd,(struct sockaddr *)&sockaddr,sizeof(struct sockaddr_in));
if(bind_ret == -1)
{
printf("bind error\n");
perror("error is:");
exit(-1);
}
/*listen函数监听网络连接*/
listen_ret = listen(s_fd,10);/*最多接入10个客户端*/
/*accept函数接入客户端*/
int size = sizeof(struct sockaddr_in);
c_fd = accept(s_fd,(struct sockaddr *)&c_sockaddr,&size);
if(c_fd == -1)
{
printf("accept error\n");
perror("error is:");
exit(-1);
}
printf("connect successfully!the IP address is %s\n",inet_ntoa(c_sockaddr.sin_addr));
return 0;
}
客户端代码:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
int main (int argc,char** argv)
{
int c_fd;
int connect_ret;
/*命令行参数判断*/
if(argc != 3)
{
printf("param error!\n");
exit(-1);
}
/*socket创建套接字*/
c_fd = socket(AF_INET,SOCK_STREAM,0);
if(c_fd == -1)
{
printf("socket error!\n");
exit(-1);
}
/*connect连接客户端*/
struct sockaddr_in sockaddr;
sockaddr.sin_family = AF_INET;
sockaddr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1],&(sockaddr.sin_addr));
connect_ret = connect(c_fd,(struct sockaddr*)&sockaddr,sizeof(struct sockaddr_in));
if( connect_ret == -1)
{
printf("connect error!\n");
exit(-1);
}
return 0;
}
运行结果如下:
说明服务端和客户端通过socket套接字连接成功了。
2.当有客户端接入的时候,服务端通过fork()函数创建子进程,让子进程和客户端进行数据的交互。父进程则继续等待下一个客户端的连接。
服务端创建子进程代码如下:
/*循环等待客户端的接入*/
while(1)
{
/*accept函数接入客户端,如果没有客户端的可入将会阻塞等待客户端的接入*/
int size = sizeof(struct sockaddr_in);
c_fd = accept(s_fd,(struct sockaddr *)&c_sockaddr,&size);
if(c_fd == -1)
{
printf("accept error\n");
perror("error is:");
exit(-1);
}
/*打印接入客户端的地址*/
printf("connect successfully!the IP address is %s\n",inet_ntoa(c_sockaddr.sin_addr));
/*创建子进程进行数据的交互*/
pid = fork();
if(pid == -1)
{
printf("no child process is created!\n");
eixt(-1);
}
else(pid == 0)/*child process*/
{
}
}
3.在进行数据交互的时候,会使用一个结构体作为数据交互的载体,这个结构体里面有两个数组,一个数组用于存放指令,一个数组用于存放数据。这个结构体和一些指令的宏定义会存放在一个配置文件里面。
config.h文件内容如下:
#define LLS 1
#define LPWD 2
#define PWD 3
#define LS 4
#define GET 5
#define QUIT 6
#define PUT 7
#define CD 8
#define LCD 9
struct message
{
char cmd[128];
char data[1024];
};
4.客户端使用gets()函数获取用户的输入,构造一个指令处理函数cmd_handler,在这个指令处理函数里面又会构造一个解析指令函数switch_cmd,在这个函数里面会通过字符串比较函数strcmp,查找子串函数strstr对指令进行解析。解析完之后会返回一个指令给指令处理函数cmd_handler。而指令处理函数会根据这个指令,会使用swtichcase走不同的分支进行指令处理。服务端的操作也是类似的。
5.对于不同的指令功能从简到难分步实现的。
1)实现本地的LLS查看客户端本地文件,LPWD查看客户端工作路径。使用system()函数调用系统指令ls,pwd来实现。
2)实现本地的LCD进入客户端某个文件夹,需要构造一个获取文件名函数getDir,在这个函数中会使用字符串切割函数获取文件名.获取文件名之后,使用chdir()函数切换目录。
3)对于输入的指令有误,则打印错误信息。
4)实现LS查看服务端有哪些文件,和PWD查看服务端工作路径。将指令写入socket套接字传到服务端, 服务端解析之后调用popen()函数,将运行结果写入socket套接字传回到客户端。
5)实现put上传文件到服务端,先使用access()函数判断本地是否有该文件,如果有,将文件写入管道。服务端读取管道中的数据,创建该文件,将数据写入该文件。
6)实现get下载服务端的文件,将指令传到服务端,服务端使用access()函数判断是否存在该文件,如果不存在,就将“nonexist”写入管道,如果存在就将文件数据写入管道。 而客户端通过判断是否有“nonexit”来确定是否能下载,如果有“nonexit”字符就提示客户端没有该文件,否则,就将创建该文件,将管道的数据写入该文件。
7)实现CD进入服务端某个文件夹,将指令写入管道,客户端读取指令后调用chdir()函数实现切换文件夹。
8)实现quit指令退出,关闭客户端套接字,服务端退出子进程。
服务端代码完整版:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <errno.h>
#include <arpa/inet.h>
#include "config.h"
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
/*全局变量结构体用于数据的交互*/
struct message mesg;
/*解析指令函数*/
int switch_cmd(char *cmd)
{
if(strcmp(cmd,"ls") == 0) return LS;
if(strcmp(cmd,"pwd") == 0) return PWD;
if(strstr(cmd,"put") != NULL) return PUT;
if(strstr(cmd,"get") != NULL) return GET;
if(strstr(cmd,"cd") != NULL) return CD;
if(strcmp(cmd,"quit") == 0) return QUIT;
}
/*获取文件名函数*/
char *getDir(char *temp)
{
char *dir;
dir = strtok(temp," ");
dir = strtok(NULL," ");
return dir;
}
/*指令处理函数*/
void cmd_handler(int fd,char *cmd)
{
printf("%s\n",cmd);/*打印指令方便调试*/
int ret;
int dirFd;
FILE *pipe;
char *temp = (char *)malloc(strlen(cmd));
strcpy(temp,cmd);
/*获取文件名*/
char *dir = getDir(temp);
ret =switch_cmd(cmd);
switch(ret)
{
case PWD:
case LS:
pipe = popen(cmd,"r");
fread(mesg.data,sizeof(mesg.data),1,pipe);
pclose(pipe);/*关闭管道*/
write(fd,&mesg,sizeof(struct message));
break;
case PUT:
dirFd = open(dir,O_RDWR|O_TRUNC|O_CREAT,0777);
/*写入大小应该为实际的大小使用strlen而不是sizeof*/
write(dirFd,&mesg.data,strlen(mesg.data));
close(dirFd);
break;
case GET:
/*判断客户端是否客户端所需要下载的文件*/
if(access(dir,F_OK) == -1)
{
memset(mesg.cmd,'\0',sizeof(mesg.cmd));
strcpy(mesg.cmd,"nonexist");
write(fd,&mesg,sizeof(struct message));
}
else
{
memset(mesg.data,'\0',sizeof(mesg.data));
dirFd = open(dir,O_RDWR);
read(dirFd,&mesg.data,sizeof(mesg.data));
close(dirFd);
write(fd,&mesg,sizeof(struct message));
}
break;
case CD:
if(chdir(dir) == -1)
{
printf("we failed to change directory!\n");
perror("chdir error:");
}
break;
case QUIT:
printf("the client out!\n");
exit(-1);/*退出子进程*/
break;
}
}
int main (int argc,char *argv[])
{
int s_fd;
int bind_ret;
int listen_ret;
int c_fd;
struct sockaddr_in c_sockaddr;
int pid;
int n_read;
/*socket创建套接字*/
s_fd = socket(AF_INET,SOCK_STREAM,0);
if(s_fd == -1)
{
printf("socket error!\n");
perror("error is:");
exit(-1);
}
/*bind添加信息:IP地址和端口号*/
struct sockaddr_in sockaddr;
sockaddr.sin_family = AF_INET;
sockaddr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1],&sockaddr.sin_addr);
/*命令行参数判断*/
if(argc != 3)
{
printf("param error!\n");
exit(-1);
}
bind_ret = bind(s_fd,(struct sockaddr *)&sockaddr,sizeof(struct sockaddr_in));
if(bind_ret == -1)
{
printf("bind error\n");
perror("error is:");
exit(-1);
}
/*listen函数监听网络连接*/
listen_ret = listen(s_fd,10);/*最多接入10个客户端*/
/*循环等待客户端的接入*/
while(1)
{
/*accept函数接入客户端,如果没有客户端的可入将会阻塞等待客户端的接入*/
int size = sizeof(struct sockaddr_in);
c_fd = accept(s_fd,(struct sockaddr *)&c_sockaddr,&size);
if(c_fd == -1)
{
printf("accept error\n");
perror("error is:");
exit(-1);
}
/*打印接入客户端的地址*/
printf("connect successfully!the IP address is %s\n",inet_ntoa(c_sockaddr.sin_addr));
/*创建子进程进行数据的交互*/
pid = fork();
if(pid == -1)
{
printf("no child process is created!\n");
exit(-1);
}
else if (pid == 0)
{
while(1)
{
memset(mesg.cmd,'\0',sizeof(mesg.cmd));
memset(mesg.data,'\0',sizeof(mesg.data));
n_read = read(c_fd,&mesg,sizeof(struct message));//读取管道中的数据
if(n_read == 0)
{
printf("we failed to recevie message form client!\n");
exit(-1);/*退出子进程*/
}
/*指令处理函数*/
cmd_handler(c_fd,mesg.cmd);
}
}
}
return 0;
}
客户端的代码完整版:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include "config.h"
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
/*全局变量结构体用于数据的交互*/
struct message mesg;
/*指令解析函数*/
int switch_cmd(char *cmd)
{
if( strcmp(cmd,"lls") == 0) return LLS;
if( strcmp(cmd,"lpwd") == 0) return LPWD;
if( strstr(cmd,"lcd") != NULL && strcmp(cmd,"lcd") != 0) return LCD;
if( strcmp(cmd,"ls") == 0) return LS;
if( strcmp(cmd,"pwd") == 0) return PWD;
if(strstr(cmd,"put") != NULL && strcmp(cmd,"put") != 0) return PUT;
if(strstr(cmd,"get") != NULL && strcmp(cmd,"get") != 0) return GET;
if(strstr(cmd,"cd") != NULL && strcmp(cmd,"cd") != 0 && strstr(cmd,"lcd") == NULL) return CD;
if(strcmp(cmd,"quit") == 0 ) return QUIT;
return -1;
}
/*获取文件名函数*/
char *getDir(char *temp)
{
char *dir;
dir = strtok(temp," ");
dir = strtok(NULL," ");
return dir;
}
/*指令处理函数*/
void cmd_handler(int fd,char *cmd)
{
int ret;
int desFd;
char *temp = (char *)malloc(sizeof(strlen(cmd)));
ret = switch_cmd(cmd);
strcpy(temp,cmd);/*temp用于字符串切割*/
/*获取文件名*/
char *dir = getDir(temp);
switch(ret){
case PWD:
case LS:
write(fd,&mesg,sizeof(struct message));
memset(mesg.cmd,'\0',sizeof(mesg.cmd));
memset(mesg.data,'\0',sizeof(mesg.data));
read(fd,&mesg,sizeof(struct message));
printf("----------------------------\n");
printf("%s",mesg.data);
printf("----------------------------\n");
break;
case PUT:
/*判断本地是否有该文件*/
if(access(dir,F_OK) == -1)
{
printf("%s doesnt exist!\n",dir);
}
else
{
desFd = open(dir,O_RDWR);
read(desFd,&mesg.data,sizeof(mesg.data));
close(desFd);
write(fd,&mesg,sizeof(struct message));
}
break;
case GET:
write(fd,&mesg,sizeof(struct message));
memset(mesg.cmd,'\0',sizeof(mesg.cmd));
memset(mesg.data,'\0',sizeof(mesg.data));
read(fd,&mesg,sizeof(struct message));
if(strstr(mesg.cmd,"nonexist") != NULL)
{
printf("%s doesnt exist!\n",dir);
}
else
{
desFd = open(dir,O_RDWR|O_TRUNC|O_CREAT,0777);
/*写入的大小需要使用strlen写入实际大小*/
write(desFd,&mesg.data,strlen(mesg.data));
close(desFd);
}
break;
case CD:
write(fd,&mesg,sizeof(struct message));
break;
case LLS:
printf("----------------------------\n");
system("ls");
printf("----------------------------\n");
break;
case LPWD:
printf("----------------------------\n");
system("pwd");
printf("----------------------------\n");
break;
case LCD:
chdir(dir);
break;
case QUIT:
write(fd,&mesg,sizeof(struct message));
close(fd);/*关闭套接字*/
exit(-1);/*退出进程*/
break;
case -1:
printf("command not found!\n");
break;
}
}
int main (int argc,char** argv)
{
int c_fd;
int connect_ret;
/*命令行参数判断*/
if(argc != 3)
{
printf("param error!\n");
exit(-1);
}
/*socket创建套接字*/
c_fd = socket(AF_INET,SOCK_STREAM,0);
if(c_fd == -1)
{
printf("socket error!\n");
exit(-1);
}
/*connect连接客户端*/
struct sockaddr_in sockaddr;
sockaddr.sin_family = AF_INET;
sockaddr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1],&(sockaddr.sin_addr));
connect_ret = connect(c_fd,(struct sockaddr*)&sockaddr,sizeof(struct sockaddr_in));
if( connect_ret == -1)
{
printf("we failed to connect the server!\n");
exit(-1);
}
while(1)
{
memset(mesg.cmd,'\0',sizeof(mesg.cmd));
memset(mesg.data,'\0',sizeof(mesg.cmd));
printf("input cmd:\n");
/*获取用户指令*/
gets(mesg.cmd);
/*指令处理函数*/
cmd_handler(c_fd,mesg.cmd);
}
return 0;
}
问题总结:
1.上传的文件或者下载的文件可能回会出现乱码,大概率是因为写入文件的大小有误。sizeof和strlen使用混淆导致。
2.每次使用完之后都需要使用memset清空数组,不然会导致上一轮指令产生的数据残留下来。