在基于socket API的C/S模型中,有时候需要这样的需求:
1.客户端可以即时获取服务器指定目录下的文件列表及属性;
2.在当前目录下点击某个子目录,可以进入并获取该子目录下的文件列表及其属性;
3.在当前目录下可以返回到上一级目录;
如果C/S模型基于FTP协议,那么这个需求就很容易实现,不过需要事先将服务器端的主机设置为FTP服务器。如果C/S模型是基于TCP/IP协议,那么实现起来就有一些麻烦。
本文所描述的C/S模型即基于TCP/IP协议,服务器程序采用Linux C编写,运行在Linux主机上;而客户端采用Qt以及原生的Socket API编写,以友好的交互界面运行在Windows或LInux主机上。
1.服务器程序实现概述
概括的说,服务器和客户端之间的通信总是“客户端发送请求–服务器接受请求并发送数据–客户端接受数据并解析”这样的循环过程。也就是说,服务器需要一直监听客户端发送的数据,根据发送数据的不同服务器作出相应的操作。客户端发送的数据均为字节流,但是根据其功能不同,我们可以将这些字节流分为两种:第一种为指令型数据,这些数据并不是客户和服务器之间真正交互的数据,而是客户端指定服务器做出相应操作的命令;第二种为请求数据,这类数据才是客户端和服务器用来进行数据交互的。
根据上面的理解,服务器和客户之间的交互模型大致是这样的:服务器和客户连接成功后,服务器首先将指定目录下的文件名及文件属性发送至客户端,客户端进行解析生成文件列表;当用户在客户端点击相应子目录(或上级目录名)后,客户端给服务器发送读取子目录(或上级目录)的指令;服务器收到该命令后,读取相应子目录(或上级目录)下的所有文件并发送至客户端。
在上述过程中,服务器对应的代码模型如下:
int server_dir(int client_fd) { sendDir(client_fd, "/home/edsionte/"); while (1) { memset(recvbuf, 0, CONFIGSTRUCT_LENGTH); if (recv(client_fd, recvbuf, CONFIGSTRUCT_LENGTH, 0) < 0) { my_error("recv", errno, __LINE__); } if (strcmp(recvbuf, "command1") == 0) { do_command1(); } else if (strcmp(recvbuf, "cd") == 0) { sendNextDir(client_fd); } else if (strcmp(recvbuf, "command2") == 0) { do_command2(); } ………… else { do_anotherthing(); } return 1; }
比如客户端请求获得/home/edsionte/目录下的所有文件列表,那么一开始先通过sendDir函数获得该目录下的所有文件。接着服务器通过进入一个死循环而实现无限监听客户端到功能,服务器根据从客户端接受的不同“指令”从而做出相应处理。比如当服务器检测到客户端发送的数据为“cd”时,则进入sendNextDir函数,该函数即获得/home/edsionte/下某个子目录下的所有文件。
2.服务器的具体实现
在实现服务器的过程中主要涉及三个函数,下面将依次对所涉及的函数进行说明。
readDir()读取pathname目录下的所有文件,将文件以其相关属性保存到dirbuf中,将该目录下的文件数保存到num中。该函数具体的实现过程容易理解:先打开pathname目录,再利用循环依次读取该目录下的所有文件及相应的文件属性。并且在读取的过程中,依次将文件名和文件属性按照一定的格式拼接成一个字符流。
int readDir(char *pathname, char *dirbuf) { DIR *mydir; struct dirent *myentry; struct stat mystat; char filepath[DATA_MAX]; int num = 1; if ((mydir = opendir(pathname)) == NULL) { my_error("opendir", errno, __LINE__); } if (pathname[strlen(pathname) - 1] != '/') pathname[strlen(pathname)] = '/'; strcpy(dirbuf, pathname); strcat(dirbuf, "\n"); strcat(dirbuf, "..:1\n"); while ((myentry = readdir(mydir)) != NULL) { if ((strcmp(myentry->d_name, ".") == 0) || (strcmp(myentry->d_name, "..") == 0)) continue; memset(filepath, 0, DATA_MAX); strcpy(filepath, pathname); strcat(dirbuf, myentry->d_name); strcat(filepath, myentry->d_name); if (lstat(filepath, &mystat) == -1) { my_error("stat", errno, __LINE__); } if (S_ISDIR(mystat.st_mode)) { strcat(dirbuf, ":1"); } else { strcat(dirbuf, ":0"); } strcat(dirbuf, "\n"); num++; }//while return num; }
sendDir()先开辟一定大小的内存空间dirbuf,再通过readDir()获得相应字节流;再通过客户端套接字client_fd,将pathname目录下的文件数和该目录下的数据字节流发送至客户端。
void sendDir(int client_fd, char *pathname) { int num; char *dirbuf = NULL; if ((dirbuf = (char *)malloc(DATA_MAX * 10)) == NULL) { my_error("malloc", errno, __LINE__); } memset(dirbuf, 0, DATA_MAX * 10); num = readDir(pathname, dirbuf); if (send(client_fd, &num, sizeof(num), 0) == -1) { my_error("send", errno, __LINE__); } if (send(client_fd, dirbuf, strlen(dirbuf), 0) == -1) { my_error("send", errno, __LINE__); } free(dirbuf); }
当服务器接收到客户端“进入下级目录”的命令(程序中用字节流“cd”表示)时就掉用sendNextDir()。该函数先接收客户端发送来的下级目录的完整路径,再调用sendDir()。
void sendNextDir(int client_fd) { char pathname[DATA_MAX]; memset(pathname, 0, DATA_MAX); if (recv(client_fd, pathname, DATA_MAX, 0) == -1) { my_error("recv", errno, __LINE__); } sendDir(client_fd, pathname); }
通过上述四个函数就可以基本实现本文一开始提出的需求。