存档在 ‘网络编程’ 分类

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);
}

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

TCP连接的建立和终止

2011年6月9日

在基于TCP的客户/服务器模型中,服务器和客户程序之间通过一系列的socket接口形成TCP连接。服务器启动后的首要工作就是创建监听套接字,监听套接字是通过socket、bind和listen三个函数完成的。在这之后,监听套接字通过accept函数一直处于阻塞状态,等待客户程序的连接请求。客户程序在连接至服务器之前,必须先创建自己的套接字,再通过connect函数连接到服务器。如果一切顺利,那么服务器和客户之间就形成了一条TCP连接。

上述TCP连接的过程是从编程角度触发,接下来我们将从网络通信的角度分析TCP的建立和终止过程,并且结合相关的socket函数接口分析其建立和终止过程中客户程序和服务器状态的变化。

1.TCP的建立

TCP连接建立过程即通常我们所说的“三路握手”。整个连接过程是由客户端的connect函数发起的,其具体的步骤如下:

1.客户端向服务器发送一个SYN(同步)报文段,这个报文段包含了客户端将要发送数据的初始序列号,假设该序号为J。该SYN段为整个TCP连接过程的报文段1。

2.服务器向客户端发送一个SYN段作为对客户端的应答,该报文段包含服务器在此连接中将要发送数据的起始序列号K。该SYN段为整个TCP连接过程的报文段2;在该报文段中还包含服务器对客户端SYN段的确认(ACK),该确认包含的序号为J+1,表明服务器期望客户端下次发送的数据序号是从J+1开始的。也就是说报文段2包含服务器发送的SYN和服务器对客户SYN的ACK。

3.客户端向服务器发送ACK以对服务器SYN进行确认,该报文段的序号为K+1。这是报文段3。

由于服务器在接收到客户端的TCP连接请求之前调用了accpet函数,所以一直处于阻塞状态。当客户通过connect函数向服务器发送第一个SYN段时,它执行了主动打开TCP连接这个动作。而接受这个SYN的服务器则执行被动打开动作,它向客户发送SYN以及ACK确认。

接下来我们以上文所举例的回射客户/服务器程序为例,说明TCP连接过程中客户端和服务器的状态变化。

1.从后台启动服务器程序。通过netstat命令我们可以看到当前服务器处于监听状态,因此此刻还没有任何客户请求。

在本地地址一栏中,IP地址为*是因为我们在绑定(bind)服务器套接字时使用了INADDR_ANY参数。由于此刻没有任何客户请求,所以外来地址处均为*。

2。运行客户程序,TCP连接建立。第一条信息为主服务器进程,它此刻仍然处于监听状态,等待其他客户程序的连接请求;第二条信息为子服务器进程,它此刻已经建立了TCP连接,本地端口为6666,而外来连接端口为54260。第三条信息为客户客户进程,他此刻也处于已建立连接的状态,本地端口为54260,而外来端口正好为6666。

此刻,我们在客户端并没有输入任何数据,因此此刻主服务器进程、子服务器进程和客户进程均处于阻塞状态,但是其阻塞原因却不同。主服务器进程是因为等待(accept)其他客户请求而阻塞;子服务器进程是因为等待(read())客户进程发送的数据而阻塞;客户进程则是因为等待从标准输入读取(fgets())数据而阻塞。我们可以进一步使用ps命令查看三个进程之间的状态。如下:

对ps使用-t选项可以查看指定伪终端下的进程,而通过-o选项则可以输入指定的进程状态信息,WCHAN参数可以进程睡眠的原因。ps的输出结果和我们上面的分析的结果一致,三个进程确实处于睡眠状态(S)。当进程阻塞于accept或connect时,其睡眠条件为inet_csk_wait_for_connect;当进程阻塞于套接字输入或输出时,其睡眠条件为sk_wait_data;当进程阻塞于终端的读操作时,其睡眠条件为n_tty_read。

2.TCP的终止

由于TCP连接是全双工的,因此必须分别关闭两个方向上的连接。通常一个TCP连接的终止需要4个分节。整个TCP的终止过程通常是由客户端的close函数发起的。

1.客户端进程调用close函数关闭套接字,它执行了主动关闭TCP连接,该端TCP向服务器发送一个FIN报文段(报文段1),表示该端的数据已经发送完毕。假设该报文段的序号为M。

2.服务器接收到这个FIN段后执行被动关闭。为了确认受到这个FIN,服务器向客户端发送一个ACK(报文段2),该确认报文的序号为M+1。服务器接收了客户端发送来的FIN说明服务器在该TCP连接上再无数据可接收。

3.服务器程序调用close关闭套接字,这将导致服务器向客户端发送一个FIN报文(报文段3),假设该报文的序号为N。

4.客户端为了确认服务器发送来的FIN报文,它向服务器发送一个ACK(报文4),该报文的序号为N+1。

上述所提及的关闭套接字有时候并不是只能通过close函数来实现。客户进程不论是正常终止(通过exit或者从主函数中返回)还是通过某个信号而终止时,都会关闭所有已经打开的描述符,这里当然包括套接字描述符。

当用户在标准输入端输入EOF时(Contrl+D),客户正常退出而关闭套接字,紧接着就会发生上述的TCP终止过程。通过netstat命令可以看到当前TCP的连接状态。

当前连接的客户端进入了TIME_WAIT状态,而服务器仍然在等待其他客户的连接。

我们继续使用ps可以查看到当前客户和服务器进程的状态。

可以看到客户进程已经退出,主服务器进程由于等待其他客户的连接而处于睡眠状态,子服务器进程虽然已经停止了服务,但是仍然处于“阴魂不散”的僵死状态。这是由于主服务器进程并未对子服务器进程发出的SIGCHLD信号进行捕获,具体的捕获方法在这里的文章已经说明。

TCP并发服务器程序设计

2011年6月8日

TCP客户/服务器程序设计模型-并发服务器

上文中所说的迭代服务器简单易懂,将它作为学习socket编程的例子再适合不过了。但是在一般的实际应用中我们并不会使用这个简单的迭代服务器,因为这样的服务器每次只能服务一个客户,如果这个客户请求长时间占用服务器,那么其他客户将一直不能被服务。较为理想的情况是服务器可以同时为多个客户服务,接下来将要说明的并发服务器就可以满足这样的要求。

1.并发服务器原理

关于并发服务器的基本原理可以参考本博客之前的文章,这里不在赘述。下面将通过一个具有并发服务器的C/S模型示例来说明并发服务器程序设计的细节。

2客户程序设计

客户程序与上文所述的客户程序类似,只不过在客户端进行connect之后,客户端与服务器之间的所有数据交互都封装在了str_cli函数中。客户程序主函数在调用该函数时不仅要向该函数传递套接字,还要传递标准输入文件指针stdin。具体代码如下:

void str_cli(FILE *fp, int sockfd)
{
	char sendline[DATA_MAX], recvline[DATA_MAX];

	memset(sendline, 0, DATA_MAX);
	memset(recvline, 0, DATA_MAX);

	while(fgets(sendline, DATA_MAX, fp) != NULL) {
		if (write(sockfd, sendline, strlen(sendline)) < 0) {
			printf("write error!\n");
			exit(1);
		}

		if (read(sockfd, recvline, DATA_MAX) == 0) {
			printf("read error!\n");
			exit(1);
		}
		fputs(recvline, stdout);

          	memset(sendline, 0, DATA_MAX);
         	memset(recvline, 0, DATA_MAX);
	}
}

从客户端的数据交互函数str_cli中可以看出客户端进行的操作如下:

1.客户程序通过fgets从标准输入文件读入数据,并通过write将数据发送至服务器端;

2.客户程序通过read函数读取服务器发来的数据,并通过fputs将数据写入标准输出文件;

3.如果fgets读入数据错误时,整个循环结束,客户端也将结束;

3.服务器程序设计

并发服务器的经典模型在本博客之前的文章中有所提到,这里只是特别说明服务器数据处理函数。

void str_echo(int sockfd)
{
	int n;
	char buf[DATA_MAX];

again:
	while((n = read(sockfd, buf, DATA_MAX)) > 0)
		if (write(sockfd, buf, n) < 0) {
			printf("write error!\n");
			exit(1);
		}

	if (n < 0 && errno == EINTR)
		goto again;
	else if (n < 0)
		printf("str_echo error!\n");
}

服务器程序完成的工作如下:

1.通过read读取客户程序发送的数据;

2.通过write将刚刚读到的数据重新发送给客户程序;

3.如果客户端正常退出,那么服务器的read函数将返回0并退出循环;否则将显示错误信息;

通过上述对客户和服务器程序的描述,我们可以得知该C/S模型的工作方式是:客户程序从标准输入读入用户输入的数据,并发送至服务器;服务器从网络输入读入该数据,立即将读入的数据发送给客户程序;客户从网络输入读取服务器发来的数据,并将其显示在标准输出上。

也就是说,每当用户在客户端输入数据,客户端在终端上又会立即回显用户所输入的数据。因此,我们将上述所举例的具有并发服务器的C/S模型称之为回射客户/服务器程序。

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