日志标签 ‘socket’

Netlink编程-用户主动发起会话

2012年8月19日

Netlink是一种在内核态和用户态可以进行双向通信的机制,也就是说,用户进程既可以作为服务器端又可以作为客户端,内核也是如此。用户进程和内核谁是服务器端谁是客户端,这个问题与谁先主动发起数据交互会话有关。

用户进程主动向内核发起会话在Linux内核中很常见,比如系统调用、对/proc的操作等。本文通过详解一个简单的实例程序来说明用户进程通过netlink机制如何主动向内核发起会话。在该程序中,用户进程向内核发送一段字符串,内核接收到后再将该字符串后再重新发给用户进程。

用户态程序

netlink是一种特殊的套接字,在用户态除了一些参数的传递对其使用的方法与一般套接字无较大差异,。

1.宏与数据结构的定义

在使用netlink进行用户进程和内核的数据交互时,最重要的是定义好通信协议。协议一词直白的说就是用户进程和内核应该以什么样的形式发送数据,以什么样的形式接收数据。而这个“形式”通常对应程序中的一个特定数据结构。

本文所演示的程序并没有使用netlink已有的通信协议,因此我们自定义一种协议类型NETLINK_TEST。

#define NETLINK_TEST 18
#define MAX_PAYLOAD 1024

struct req {
	struct nlmsghdr nlh;
	char buf[MAX_PAYLOAD];
};

除此之外,我们应该再自定义一个数据报类型req,该结构包含了netlink数据包头结构的变量nlh和一个MAX_PAYLOAD大小的缓冲区。这里我们为了演示简单,并没有像上文中描述的那样将一个特定数据结构与nlmsghdr封装起来。

2.创建netlink套接字

要使用netlink,必须先创建一个netlink套接字。创建方法同样采用socket(),只是这里需要注意传递的参数:

	int sock_fd;
	sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_TEST);
	if (sock_fd < 0) {
		eprint(errno, "socket", __LINE__);
		return errno;
	}

第一个参数必须指定为PF_NETLINK或AF_NETLINK。第二个参数必须指定为SOCK_RAW或SOCK_DGRAM,因为netlink提供的是一种无连接的数据报服务。第三个参数则指定具体的协议类型,我们这里使用自定义的协议类型NETLINK_TEST。

另外,eprint()是一个自定义的出错处理函数,实现如下:

void eprint(int err_no, char *str, int line)
{
	printf("Error %d in line %d:%s() with %s\n", err_no, line, str, strerror(errno));
}

3.将本地套接字与源地址绑定

将本地的套接字与源地址进行绑定通过bind()完成。在绑定之前,需要将源地址进行初始化,nl_pid字段指明发送消息一方的pid,nl_groups表示多播组的掩码,这里我们并没有涉及多播,因此默认为0。

	struct sockaddr_nl src_addr;
	memset(&src_addr, 0, sizeof(src_addr));
	src_addr.nl_family = AF_NETLINK;
	src_addr.nl_pid = getpid();
	src_addr.nl_groups = 0;

	if (bind(sock_fd, (struct sockaddr *)&src_addr, sizeof(src_addr)) < 0) {
		eprint(errno, "bind", __LINE__);
		return errno;
	}

4.初始化msghdr结构

用户进程最终发送的是msghdr结构的消息,因此必须对这个结构进行初始化。而此结构又与sockaddr_nl,iovec和nlmsghdr三个结构相关,因此必须依次对这些数据结构进行初始化。

首先初始化目的套接字的地址结构,该结构与源套接字地址结构初始化的方法稍有不同,即nl_pid必须为0,表示接收方为内核。

	struct sockaddr_nl dest_addr;
	memset(&dest_addr, 0, sizeof(dest_addr));
	dest_addr.nl_family = AF_NETLINK;
	dest_addr.nl_pid = 0;
	dest_addr.nl_groups = 0;

接下来对req类型的数据报进行初始化,即依次对其封装的两个数据结构初始化:

	struct req r;
	r.nlh.nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
	r.nlh.nlmsg_pid = getpid();
	r.nlh.nlmsg_flags = 0;
	memset(r.buf, 0, MAX_PAYLOAD);
	strcpy(NLMSG_DATA(&(r.nlh)), "hello, I am edsionte!");

这里的nlmsg_len为为sizeof(struct nlmsghdr)+MAX_PAYLOAD的总和。宏NLMSG_SPACE会自动将两者的长度相加。接下来对缓冲区向量iov进行初始化,让iov_base字段指向数据报结构,而iov_len为数据报长度。

	struct iovec iov;
	iov.iov_base = (void *)&r;
	iov.iov_len = sizeof(r);

一切就绪后,将目的套接字地址与当前要发送的消息msg绑定,即将目的套接字地址复制给msg_name。再将要发送的数据iov与msg_iov绑定,如果一次性要发送多个数据包,则创建一个iovec类型的数组。

	struct msghdr msg;
	msg.msg_name = (void *)&dest_addr;
	msg.msg_namelen = sizeof(dest_addr);
	msg.msg_iov = &iov;
	msg.msg_iovlen = 1;

5.向内核发送消息

发送消息则很简单,通过sendmsg函数即可完成,前提是正确的创建netlink套接字和要发送的消息。

	if (sendmsg(sock_fd, &msg, 0) < 0) {
		eprint(errno, "sendmsg", __LINE__);
		return errno;
	}

6.接受内核发来的消息

如果用户进程需要接收内核发送的消息,则需要通过recvmsg完成,只不过在接收之前需要将数据报r重新初始化,因为发送和接收时传递的数据结构可能是不同的。

为了简单演示netlink的用法,本文所述的用户进程发送的是一段字符串,这一点从数据报结构req的定义可以看出。而内核向用户进程发送的也是一段字符串,具体情况下面将会具体说明。

        memset(&r.nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
        if (recvmsg(sock_fd, &msg, 0) < 0) {
                eprint(errno, "recvmsg", __LINE__);
                return errno;
        }

        printf("Received message payload:%s\n", (char *)NLMSG_DATA(&r.nlh));
        close(sock_fd);

接收完毕后,通过专门的宏NLMSG_DATA对数据报进行操作。 netlink对数据报的的访问和操作都是通过一系列标准的宏NLMSG_XXX来完成的,具体的说明可以通过man netlink查看。这里的NLMSG_DATA传递进去的是nlh,但它获取的是紧邻nlh的真正数据。本程序中传递的是字符串,所以取数据时候用char *强制类型转换,如果传递的是其他数据结构,则相应转换数据类型即可。

内核模块

netlink既然是一种用户态和内核态之间的双向通信机制,那么除了编写用户程序还要编写内核模块,也就是说用户进程和内核模块之间对数据的处理要彼此对应起来。

1.内核模块加载和卸载函数

内核模块加载函数主要通过netlink_kernel_create函数申请服务器端的套接字nl_sk,内核中对套接字表示为sock结构。另外,在创建套接字时还需要传递和用户进程相同的netlink协议类型NETLINK_TEST。创建套接字函数的第一个参数默认为init_net,第三个参数为多播时使用,我们这里不使用多播因此默认值为0。nl_data_handler是一个钩子函数,每当内核接收到一个消息时,这个钩子函数就被回调对用户数据进行处理。

#define NETLINK_TEST 17
struct sock *nl_sk = NULL;
static int __init hello_init(void)
{
	printk("hello_init is starting..\n");
	nl_sk = netlink_kernel_create(&init_net, NETLINK_TEST, 0, nl_data_ready, NULL, THIS_MODULE);
	if (nl_sk == 0)
	{
		printk("can not create netlink socket.\n");
		return -1;
	}
	return 0;
}

内核模块卸载函数所做的工作与加载函数相反,通过sock_release函数释放一开始申请的套接字。

static void __exit hello_exit(void)
{
	sock_release(nl_sk->sk_socket);
	printk("hello_exit is leaving..\n");
}

2.钩子函数的实现

在内核创建netlink套接字时,必须绑定一个钩子函数,该钩子函数原型为:

 void (*input)(struct sk_buff *skb);

钩子函数的实现主要是先接收用户进程发送的消息,接收以后内核再发送一条消息到用户进程。

在钩子函数中,先通过skb_get函数对套接字缓冲区增加一次引用值,再通过nlmsg_hdr函数获取netlink消息头指针nlh。接着使用NLMSG_DATA宏获取用户进程发送过来的数据str。除此之外,再打印发送者的pid。

void nl_data_handler(struct sk_buff *__skb)
{
	struct sk_buff *skb;
	struct nlmsghdr *nlh;
	u32 pid;
	int rc;
	char str[100];
	int len = NLMSG_SPACE(MAX_PAYLOAD);

	printk("read data..\n");
	skb = skb_get(__skb);

	if (skb->len >= NLMSG_SPACE(0)) {
		nlh = nlmsg_hdr(skb);
		printk("Recv: %s\n", (char *)NLMSG_DATA(nlh));
		memcpy(str, NLMSG_DATA(nlh), sizeof(str));
		pid = nlh->nlmsg_pid;
		printk("pid is %d\n", pid);
		kfree_skb(skb);

接下来重新申请一个套接字缓冲区,为内核发送消息到用户进程做准备,nlmsg_put函数将填充netlink数据报头。接下来将用户进程发送的字符串复制到nlh紧邻的数据缓冲区中,等待内核发送。netlink_unicast函数将以非阻塞的方式发送数据包到用户进程,pid具体指明了接收消息的进程。

		skb = alloc_skb(len, GFP_ATOMIC);
		if (!skb){
			printk(KERN_ERR "net_link: allocate failed.\n");
			return;
		}
		nlh = nlmsg_put(skb, 0, 0, 0, MAX_PAYLOAD, 0);
		NETLINK_CB(skb).pid = 0;

		memcpy(NLMSG_DATA(nlh), str, sizeof(str));
		printk("net_link: going to send.\n");
		rc = netlink_unicast(nl_sk, skb, pid, MSG_DONTWAIT);
		if (rc < 0) {
			printk(KERN_ERR "net_link: can not unicast skb (%d)\n", rc);
		}
		printk("net_link: send is ok.\n");
	}
}

这样就完成了内核模块的编写,它与用户进程通信共同完成数据交互。

Netlink编程-数据结构

2012年8月18日

内核态与用户态进行数据交互的方法很多,分为用户程序主动发起的消息交交互和内核主动发起的信息交互。通常我们所熟知的系统调用、对/proc进行读写操作、mmap和编写驱动程序等都属于前者,而由内核主动发起的信息交互方法比较少,最典型的方法为内核发送信号给当前进程。上述这些方法都是单向通信的,也就是说要么是用户态进程主动发起数据交互会话,要么是内核主动发起会话。有没有一种可以在用户态和内核态进行双向数据交互的方法呢?

Netlink是一种在内核态和用户态可以进行双向数据交互的通信机制。在用户态,我们可以将netlink看作是一种特殊的socket,因此通过socket接口来就可以直接使用netlink;在内核态,则需要通过一组特殊的内核接口编写内核模块。

如果初次编写netlink程序,可能会对它所涉及的数据结构感到困惑,本文将简单介绍一下netlink编程中遇到的基本数据结构。

struct msghdr

如果使用sendmsg()发送数据,那么必须使用msghdr结构,该结构内部封装了本次发送数据的一些参数:

           struct msghdr {
               void         *msg_name;       /* optional address */
               socklen_t     msg_namelen;    /* size of address */
               struct iovec *msg_iov;        /* scatter/gather array */
               size_t        msg_iovlen;     /* # elements in msg_iov */
               void         *msg_control;    /* ancillary data, see below */
               size_t        msg_controllen; /* ancillary data buffer len */
               int           msg_flags;      /* flags on received message */
           };

netlink提供的是一种基于数据报的通信服务,因此与UDP通信协议类似,必须通过msg_name指明目的套接字地址结构,而msg_namelen则指明该地址结构的长度。msg_iov指定数据缓冲区数组,而msg_iovlen指明了该数组的元素个数。

struct iovec

iovec结构表示一个向量元素,它定义了一个标准的数据缓冲区格式。其包含两个字段:指向数据的指针和数据长度。

struct iovec
{
    void *iov_base;     /* Pointer to data.  */
    size_t iov_len;     /* Length of data.  */
};

当使用iovec结构类型的数组传递数据时,可以将多个消息通过一次系统调用进行发送。

struct nlmsghdr

如果使用sendmsg函数来发送netlink数据报,那么iovec结构中iov_base字段应该根据具体协议指向一个自定义的数据结构。一般这个自定义的数据结构如下所示:

struct req {
        struct nlmsghdr *nlh;
        struct special_struct *r;
};

其中nlmsghdr结构为必须所有的,它用来描述netlink消息头。

           struct nlmsghdr {
               __u32 nlmsg_len;    /* Length of message including header. */
               __u16 nlmsg_type;   /* Type of message content. */
               __u16 nlmsg_flags;  /* Additional flags. */
               __u32 nlmsg_seq;    /* Sequence number. */
               __u32 nlmsg_pid;    /* PID of the sending process. */
           };

对于special_struct结构则要根据具体需求而定。如果使用netlink现有的协议,比如NETLINK_INET_DIAG,那么special_struct结构在用户程序向内核发送消息时应该为inet_diag_req结构,如果当内核发送消息给用户进程那么special_struct结构又必须定义为inet_diag_msg。这些结构体都是事先定义好的,我们直接拿来用,按需获取这些结构中的某些字段的值即可。

如果是自己定义的通信协议,那么则根据具体需求自定义数据结构即可。比如用户进程想通过pid在内核中获取该进程亲属的pid,那么用户进程在发送消息和接受消息时的special_struct分别可以按照如下定义:

struct get_pid_req {
        int pid;
};

struct get_pid_msg {
        int parent_pid;
        int sibling_pid;
        int child_pid;
}

为netlink消息确定好了结构以后,则将该结构赋值给iovec结构的iov_base字段。

struct sockaddr_nl

netlink是一种特殊的无连接套接字,因此必须在msghdr结构的msg_name中指明目的netlink套接字的地址。套接字地址的通用结构为struct sockaddr,但对于具体类型的套接字则有不同的地址结构,比如TCP/IP协议族为sockaddr_in,对于netlink则为sockaddr_nl结构:

           struct sockaddr_nl {
               sa_family_t     nl_family;  /* AF_NETLINK */
               unsigned short  nl_pad;     /* Zero. */
               pid_t           nl_pid;     /* Process ID. */
               __u32           nl_groups;  /* Multicast groups mask. */
           };

其中该结构中的nl_family字段填充为AF_NETLINK。

以上这些结构在netlink编程中都会涉及到,但是通过上面描述的关系来看,它们最终都是与msghdr结构有关的。而msghdr结构作为参数通过sendmsg函数就可以netlink数据包发送至内核。

recv()中的flags参数

2011年10月28日

从昨天上午到10分钟之前都在调试程序中的一个BUG,问题的精简描述是这样的:

一个经典的C/S模型,客户端异步的向服务器端发送一条指令,服务器端接收到该指令后向客户端发送一个int型数据len。这个过程重复的次数是随机的,并且有时候操作频繁。

对于上述过程的实现并不困难,在本地测试C/S端也并无异常。但是进行远程测试就出现异常:服务器端发送的len正常,但是客户端接收的数据有时候会为0。

经历了长时间的调试和分析后,终将问题锁定在recv()。该函数的原型如下:

       #include < sys/types.h >
       #include < sys/socket.h >
       ssize_t recv(int sockfd, void *buf, size_t len, int flags);

这个函数前三个参数的含义与read()大致相同,只不过recv()中的sockfd代表套接字描述符。除此之外,还有flags参数,一般被指定为0。而恰恰正是这种默认赋值才导致上述问题的产生,该标志可取多个值,其中一个值为MSG_WAITALL。这个标志告知内核在未读入指定字节len大小的数据之前不要将recv()返回。当为这个flags指定了该值后,结果正常。不过,该函数还是会在下述三种情况下提前返回:

1.连接被终止或套接字发生异常

2.接收了一个与之前类型不同的数据

3.捕获了一个信号

这三种情况都属于异常情况,跟上述问题无关。

基于Socket API的C/S通信:以树形结构显示服务器端目录(2)

2011年8月11日

3.客户端的实现

由于客户端采用面向对象的Qt来编码,因此客户端比较容易实现。客户端的主要功能是:以列表形式显示指定目录下的文件以及相关属性;当用户点击文件列表中的某个目录时进入该子目录,并显示子目录下的文件列表;可以返回到当前目录的父目录。

根据上述需求,客户端的界面设计可以参考下图,即通过一个QTableWidget来显示某个目录下的所有文件,该部件的使用方法可以参考Qt Assitant。

接下来重点说明通信部分的功能实现。在服务器程序中,服务器向客户端发送的数据有两种,一种即为文件数,另一种则为所有文件信息形成的字节流。那么在客户端中则相应的有接受函数recvDirInfo()。

void configShowUI::recvDirInfo()
{
    //1.先接受指定目录下文件的数目
    int fileNum = 0;
    if (cli.recvData(&fileNum, sizeof(int)) == 0) {
        ui->statusLabel->setText("ERROR:receive fileNum error!");
    }

    //2.接收指定目录下的文件名序列
    char *dirbuf = NULL;
    if ((dirbuf = (char *)malloc(DATA_MAX * 10)) == NULL) {
        ui->statusLabel->setText("ERROR:malloc error!");
    }
    memset(dirbuf, 0, DATA_MAX * 10);
    if (cli.recvData(dirbuf, DATA_MAX * 10) == 0) {
        ui->statusLabel->setText("ERROR:receive dir info error!");
    } else {
        ui->statusLabel->setText("receive dir info success!");
        this->printCurDir(QString(dirbuf), fileNum);
        free(dirbuf);
    }
}

该函数有两种使用场景,其一为客户端与服务器连接成功时,此时服务器要向客户端发送指定目录下的文件列表(服务器主动发送),因此应该调用该函数。其二为当用户在客户端的文件列表中点击某个子目录时,客户端此时主动要求服务器发送指定子目录下的文件列表(服务器被动发送),此时应该调用该函数。

第二种情景可以采用Qt中的信号–槽机制(可以参考Qt Assitant了解相关概念),当用户点击了文件列表中的某个子目录时,该列表部件(fileWidget)就发送cdNextDir(QString)信号(该信号在),包含列表部件的当前部件(this)接收到这个信号后就做出相应操作,即执行槽函数cdNextDirOperation(QString)。这对信号和槽之间还传递了pathname这个变量,该变量存储了子目录的完整路径。下面的连接函数应该在当前部件的构造函数中完成。

    connect(this->fileWidget, SIGNAL(cdNextDir(QString)), this, SLOT(cdNextDirOperation(QString)));

槽函数cdNextDirOperation(QString)指定的动作很简单,即先向服务器发送“读取下级目录”的指令,再将该子目录的完整路径发送到服务器端,接着再调用recvDirInfo()。可以看到客户端和服务器之间的数据交互是同步发生的。

void configShowUI::cdNextDirOperation(QString pathname)
{
    char cd[] = "cd";
    if (cli.sendData(cd, strlen(cd)) == 0) {
        ui->statusLabel->setText("ERROR: send cd command error!");
        exit(1);
    }

    if (cli.sendData(pathname.toUtf8().data(), pathname.size()) == 0) {
        ui->statusLabel->setText("ERROR: send pathname error!");
        exit(1);
    }

    this->recvDirInfo();
}

在上述的recvDirInfo()中,如果客户端成功接收了服务器发送的字节流数据,那么就调用printCurDir()对字节流进行解析,然后以列表形式显示在客户端。这里的解析过程由具体的需求而定,再次不再赘述。

基于Socket API的C/S通信:以树形结构显示服务器端目录(1)

2011年8月10日

在基于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);
}

通过上述四个函数就可以基本实现本文一开始提出的需求。

windows 7 ultimate product key

windows 7 ultimate product key

winrar download free

winrar download free

winzip registration code

winzip registration code

winzip free download

winzip free download

winzip activation code

winzip activation code

windows 7 key generator

windows 7 key generator

winzip freeware

winzip freeware

winzip free download full version

winzip free download full version

free winrar download

free winrar download

free winrar

free winrar

windows 7 crack

windows 7 crack

windows xp product key

windows xp product key

windows 7 activation crack

windows7 activation crack

free winzip

free winzip

winrar free download

winrar free download

winrar free

winrar free

download winrar free

download winrar free

windows 7 product key

windows 7 product key