存档在 ‘网络编程’ 分类

TCP迭代服务器程序设计

2011年6月7日

TCP客户/服务器程序设计模型-迭代服务器

本博客将从本文开始持续更新基于TCP的C/S程序设计基本模型系列文章。一开始我们以一个简单的C/S模型为例进行学习,在后续的文章中我们将给出不同的客户/服务器模型。

下面将要示例的程序具有这样的功能:客户程序与服务器建立一个TCP连接后,服务器以友好的方式将当前的时间和日期发送给客户程序。我们接下来重点对示例程序的设计模式和一些设计细节作以说明,程序中所涉及的socket接口具体可以参考这里的文章。

1.客户程序设计

在基于socket的C/S模型中,客户程序往往比服务器程序简单的多。客户程序通常完成的工作是:通过socket函数创建一个套接字;connect到服务器;如果连接成功,则与服务器进行数据交互,具体为从服务器读取数据或向服务器写入数据。客户程序的核心代码如下:

int main(int argc, char **argv)
{
	int sockfd, n;
	char recvline[MAXLINE + 1];
	struct sockaddr_in serv_addr;

	if (argc != 2) {
		printf("Usage:a.out \n");
		exit(1);
	}

	if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
		my_error("socket", errno, __LINE__);

	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(4555);

	if (inet_pton(AF_INET, argv[1], &serv_addr.sin_addr) <= 0)
 		my_error("inet_pton", errno, __LINE__);
 	if (connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr)) == -1)
  		my_error("connect", errno, __LINE__);
 	while ((n = read(sockfd, recvline, MAXLINE)) > 0) {
		recvline[n] = 0;
		if (fputs(recvline, stdout) == EOF)
			my_error("fputs", errno, __LINE__);
	}

	if (n < 0)
		printf("Error:read error\n");

	exit(0);
}

关于客户端程序的设计,说明如下:

1.客户端程序每次启动时,以命令行参数形式将服务器IP传递给客户端。因此程序一开始必须检测命令行参数的合理性;

2.由于我们的C/S模型是基于TCP的字节流套接字,因此相应的对socket函数使用AF_INET和SOCK_STREAM参数。socket函数返回一个整形的套接字描述符,在客户程序中要使用的connet和read等函数将依靠此描述符来对套接字进行操作;

3.在客户连接至服务器之前前,必须设置网际套接字地址结构,即依次对sockaddr_in结构中的字段填入正确的值。htons函数将主机字节序列转换成网络字节序列;inet_pton函数则是将字符形式的IP地址转换成二进制数值形式;

4.connect将与其第二个参数所指定的服务器建立一个TCP连接。connect函数的第二个参数为通用套接字地址结构sockaddr,因此必须对网际套接字地址结构进行强制类型转换;

5.通过套接字描述符sockfd从服务器端进行读数据,并通过fputs函数将读到的数据显示在标准输出设备;

2.服务器程序设计

服务器程序按照以下步骤进行设计:socket一个套接字;将服务器端口bind到服务器套接字上;通过listen函数将服务器套接字转换成监听套接字;accept客户端的连接;如果连接成功,则与客户端进行数据交互;终止连接。服务器程序核心程序如下所示。

int main(int argc, char **argv)
{
	int listenfd, connfd;
	struct sockaddr_in serv_addr, client_addr;
	char buf[MAXLINE];
	time_t ticks;
	int addr_size;

	if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
		my_error("socket", errno, __LINE__);

	memset(&serv_addr, 0, sizeof(struct sockaddr_in));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	serv_addr.sin_port = htons(4555);

	if (bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr)) == -1)
		my_error("bind", errno, __LINE__);

	if (listen(listenfd, LISTENQ) == -1)
		my_error("listen", errno, __LINE__);

	for (;;) {
		addr_size = sizeof(struct sockaddr);
		if ((connfd = accept(listenfd, (struct sockaddr*)&client_addr, &addr_size)) == -1)
			my_error("accept", errno, __LINE__);

		printf("%s has connectd to server..\n", inet_ntoa(client_addr.sin_addr));
		ticks = time(NULL);
		snprintf(buf, sizeof(buf), "%.24s\r\n", ctime(&ticks));
		if (write(connfd, buf, strlen(buf)) < 0)
			my_error("write", errno, __LINE__);

		close(connfd);
	}
}

关于服务器程序的相关细节,说明如下:

1.在设置网际套接字地址时,将IP地址指定为INADDR_ANY。如果服务器主机有多个网络接口,那么服务器进程就可以在任意网络接口上接受客户连接;

2.listen函数将套接字转换成监听套接字,这样内核就可以通过该套接字接受客户端的外来连接请求;

3.socket、bind和listen这三步是TCP服务器获得监听套接字描述符的通用步骤;

4.服务器的连续运转是通过死循环来实现的,并通过accept函数使得服务器程序不会过于占据系统内存。通常服务器进程通过accept函数进入阻塞状态,直到某个客户连接到达时才会被唤醒。客户和服务器之间的TCP连接是通过三次握手来建立的,accept函数建立连接成功后会返回另一个套接字描述符——已连接套接字。该套接字用于和刚刚连接的客户进行通信。

5.time函数将返回1970年1月1日至现在的秒数,而ctime函数将这些秒数转换成友好的时间格式;

6.wirte函数通过已连接套接字connfd将时间信息发送至客户端;

通过服务器程序我们可以看到,如果服务器没有处理客户端的请求,那么它将一直处于阻塞状态;如果服务器正在处理来自客户端的连接请求,那么其他请求连接至服务器的客户进程则一直处于阻塞状态。只有当服务器处理完当前客户的连接请求时,其他客户的连接请求才有机会被服务器处理。我们将此服务器称之为迭代式服务器,也就是说服务器对于每个客户都将迭代执行一次。

上述所示例的迭代服务器适用性很差,因为在服务器处理当前的客户请求之前,其余等待的客户程序都不能被处理。但它作为一个基本的C/S模型,对于初学者以及理解后续C/S模型都有极大帮助。

C/S模型的健壮性-为僵死进程收尸

2011年4月21日

并发服务器可以同时处理多个客户端的请求,服务器对客户端网络请求的处理是通过fork子服务进程来完成的。当有多个客户请求时,主服务器进程就会产生多个子服务器进程。当这些子服务器进程处理完客户请求时,就终止运行了。由于主服务器进程(也就是这些子服务器进程的父亲)仍然在运行,因此这些终止的子服务器进程在主服务器退出之前就成为了僵死进程。通常服务器进程都长时间处于运行状态,所以这些僵死进程就会一直存在于系统内。如果不及时清除这些僵死进程,他们将占用内核的空间,最终可能会耗尽系统资源。本文将描述如何清理这些僵死进程。

1. 僵死进程的产生

下面将通过一个简单的C/S模型回射程序来演示僵死进程的产生。我们首先将服务器置于后台运行,然后再查看当前终端(pts/3)下的进程状态:

edsionte@edsionte-desktop:~/echoCS$ ps -o pid,ppid,args,stat,wchan
  PID  PPID COMMAND                     STAT WCHAN
 5071  2305 bash                        Ss   wait
 6146  5071 ./server                    S    inet_csk_wait_for_connect
 6149  5071 ps -o pid,ppid,args,stat,wc R+   -

由上可以看出,服务器进程处于等待连接的状态,此刻它正在监听客户请求。接着运行客户端程序,连接成功后输入任意的字符串,可以看到服务器和客户端通信正常。

edsionte@edsionte-desktop:~/echoCS$ ./client 127.0.0.1
received a connection from:127.0.0.1
ps
ps

我们在另一个终端下,查看位于终端pts/3下客户端和服务器进程的状态。

edsionte@edsionte-desktop:~/echoCS$ ps -t pts/3 -o pid,ppid,args,stat,wchan
  PID  PPID COMMAND                     STAT WCHAN
 5071  2305 bash                        Ss   wait
 6146  5071 ./server                    S    inet_csk_wait_for_connect
 6150  5071 ./client 127.0.0.1          S+   n_tty_read
 6151  6146 ./server                    S    sk_wait_data

由于客户进程发来连接请求,因此服务器进程fork了一个子服务器进程,从pid和ppid可以看到两个服务器进程之间的父子关系。父子进程此刻都处于等待状态,不过他们等待的目标不同:父进程监听客户请求,子进程则阻塞于read函数等待套接字数据的来临。

接下来通过发信号SIGINT(通过键盘上的ctrl+c可发出此信号)使客户进程终止。

edsionte@edsionte-desktop:~/echoCS$ ./client 127.0.0.1
received a connection from:127.0.0.1
ps
ps
^C

我们在另一个终端查看pty/3终端当前进程的状态:

edsionte@edsionte-desktop:~/echoCS$ ps -t pts/3 -o pid,ppid,args,stat,wchan
  PID  PPID COMMAND                     STAT WCHAN
 5071  2305 bash                        Ss+  n_tty_read
 6146  5071 ./server                    S    inet_csk_wait_for_connect
 6151  6146 [server]           Z    exit

可以看到,子服务器进程现在处于僵死状态。目前我们只有运行了一次客户端程序,如果一个服务器的网络请求繁忙,则会产生很多僵死进程。

2.处理僵死进程

当子进程终止时会给父进程发送SIGCHLD信号,因此我们可以利用信号处理函数捕获这个信号并对僵死进程进行处理。我们知道在父进程中调用wait函数可以防止先于父进程终止的子进程编程僵死进程,因此我们的信号捕获函数如下所示:

void sig_zchild(int signo)
{
	pid_t pid;
	int stat;

	pid = wait(&stat);
        printf("child %d terminated\n", pid);
	return;
}

并且我们需要适当的修改服务器程序,在accept函数调用之前调用signal函数:

	if(listen(sockfd, BACKLOG) == -1) {

		printf("listen error!\n");
		exit(1);
	}

	if (signal(SIGCHLD, sig_zchild) == SIG_ERR) {
		printf("signal error!\n");
		exit(1);
	}

	while (1) {

		sin_size = sizeof(struct sockaddr_in);
		if ((client_fd = accept(sockfd, (struct sockaddr *)&remote_addr,
&sin_size)) == -1) {

			printf("accept error!\n");
			continue;
		}
		…… ……
	}//while

我们下面进行测试,我们首先在终端后台运行服务器程序,然后同时在其他多个终端分别运行客户端程序。当前终端的进程状态如下:

edsionte@edsionte-desktop:~/echoCS$ ps
  PID TTY          TIME CMD
 6699 pts/0    00:00:00 bash
 6926 pts/0    00:00:00 server
 6947 pts/0    00:00:00 server
 6966 pts/0    00:00:00 server
 6985 pts/0    00:00:00 server
 6986 pts/0    00:00:00 ps

然后我们分别在运行客户程序的终端杀死客户端进程。再次查看当前终端下的进程状态,可以发现并没有僵死的子服务进程。因为每个子进程在终止时发送SIGCHLD信号都会被父进程中的信号处理函数捕获。

3.僵死进程处理的加强版

上述的客户端进程每次向服务器端只发出一个连接请求,当客户端终止时子服务器进程将终止;如果一个客户端向服务器发出多个连接请求,当该客户端终止时多个子服务器进程将同时终止,也就是说父服务器进程将面临同时处理多个SIGCHLD信号的情况。

按照上述的特殊客户端,我们将回射C/S模型中的客户端适当修改,使运行一次客户端就连接服务器5次,这样就会产生相应的5个子服务器进程。

	int i;
	for (i = 0; i < 5; i++) {
		if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
              		printf("socket error!\n");
         		exit(1);
         	}
	
		serv_addr.sin_family = AF_INET;
          	serv_addr.sin_port = htons(SERV_PORT);
         	serv_addr.sin_addr = *((struct in_addr *)host->h_addr);

            	if(connect(sockfd, (struct sockaddr *)&serv_addr,
					sizeof(struct sockaddr)) == -1) {
			printf("connect error!\n");
	          	exit(1);
            	}
	}

	str_cli(stdin, sockfd);

运行服务器程序和刚刚修改过的客户端程序,我们作如下测试:

edsionte@edsionte-desktop:~/echoCS$ ps -t pts/0 -o pid,ppid,args,stat
  PID  PPID COMMAND                     STAT
 2651  2649 bash                        Ss
 2861  2651 ./server                    S
 2865  2651 ./mclient 127.0.0.1         S+
 2866  2861 ./server                    S
 2867  2861 ./server                    S
 2868  2861 ./server                    S
 2869  2861 ./server                    S
 2870  2861 ./server                    S
edsionte@edsionte-desktop:~/echoCS$ kill 2865
edsionte@edsionte-desktop:~/echoCS$ ps -t pts/0 -o pid,ppid,args,stat
  PID  PPID COMMAND                     STAT
 2651  2649 bash                        Ss+
 2861  2651 ./server                    S
 2867  2861 [server]           Z
 2868  2861 [server]           Z
 2869  2861 [server]           Z
 2870  2861 [server]           Z

当杀死客户端进程后,只有一个子服务器进程被SIGCHLD信号处理函数捕获处理,其他四个子服务器进程仍然处于僵死状态。出现这种现象的原因是,主服务器进程中的信号处理函数使用了wait函数,该函数会一直阻塞到出现第一个终止的子进程。也就是说,该函数只等待第一个终止的子进程并返回。

本客户端程序终止后,将同时产生5个SIGCHLD信号,而wait函数只处理其中一个,解决这个问题的办法是使用waitpid函数。该函数将周期性的检查是否有子进程终止,也就是说可以等待所有的终止子进程。修改后的服务器信号处理函数如下:

void sig_zchild(int signo)
{
	pid_t pid;
	int stat;

	while ((pid = waitpid(-1, &stat, WNOHANG)) > 0)
		printf("child %d terminated\n", pid);

	return;
}

按此修改后SIGCHLD信号处理函数后,就可以避免留下僵死进程。

C/S模型中的并发服务器

2011年4月20日

网络通信程序中最经典的模型就是客户端/服务器(C/S)模型。该模型中的服务器程序通常处于长时间运行的状态,当客户端程序主动对服务器程序发出网络请求时,服务器程序才做出具体的回应。也就是说,服务器大多数处于等待状态,只有收到客户端的网络请求才被唤醒,当处理完客户端的请求时候,又将处于等待状态。

上述同时只能处理一个客户端请求的服务器被称为迭代服务器(iterative server),这样的C/S模型只能应对那些简单的应用。大多数情况下我们希望服务器可以同时服务多个客户端程序,即服务器程序既能处理客户端发送来的请求同时又能接收其他客户端发送的请求。这样的服务器称为并发服务器(concurrent server)。

Linux中实现并发服务器的方法很简单:每当客户端对服务器有网络请求时,服务器程序就fork一个子服务器进程来处理这个客户的请求,而主服务器程序仍处于等待其他客户端网络请求的状态。基于这个原理,并发服务器的基本模型如下:

	if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
		my_error("socket", errno, __LINE__);
	}

	if (bind(sockfd, (struct sockaddr *)&my_addr,
				sizeof(struct sockaddr_in)) == -1) {
		my_error("bind", errno, __LINE__);
	}

	if (listen(sockfd, BACKLOG) == -1) {
		my_error("listen", errno, __LINE__);
	}

	while (1) {
		sin_size = sizeof(struct sockaddr_in);
		if ((client_fd = accept(sockfd, (struct sockaddr *)&remote_addr, &sin_size)) == -1) {
			my_error("accept", errno, __LINE__);
			continue;
		}

		if ((pid = fork()) == 0) {
			close(sockfd);
			process_client_request(client_fd);
			close(client_fd);
			exit(0);
		} else if (pid > 0)
			close(client_fd);
		else
			my_error("fork", errno, __LINE__);
	}

每当accept()接收到一个TCP连接时,主服务器进程就fork一个子服务器进程。子服务器进程调用相应的函数,通过client_fd(连接套接字)对客户端发来的网络请求进程处理;由于客户端的请求已被子服务进程处理,那么主服务器进程就什么也不做,通过sockfd(监听套接字)继续循环等待新的网络请求。

这里我们需要特别强调的是,在父子进程中需要关闭相应的套接字描述符。从上述代码中可以看到,子服务进程在处理客户端的网络请求之前关闭了监听套接字,主服务进程则关闭了连接套接字,子服务进程在处理完客户端请求后关闭了连接套接字。最后一种关闭套接字的情形比较合乎常理,这里我们重点关注前两种关闭套接字的情况。

我们的问题是:子服务进程关闭了监听套接字,服务器是否还能监听其他连接请求;父服务进程关闭了连接套接字,服务器是否还能够处理客户端请求。

如果理解了文件的的引用计数,这个问题就会迎刃而解。每个文件都有一个引用计数,该引用计数表示当前系统内的所有进程打开该文件描述符的个数。套接字是一种特殊的文件,当然也有引用计数。

在并发服务器这个例子当中,在accept函数之前,sockfd的引用计数为1;在fork函数执行之前,sockfd和client_fd的引用计数分别为1;当fork执行后,由于子进程复制了父进程的资源,所以子进程也拥有这两个套接字描述符,则此时sockfd和client_fd的引用计数都为2。只有当子进程处理完客户请求时,client_fd的引用计数才由于close函数而变为0。由于父服务器进程的主要任务是监听客户请求,则它关闭了连接套接字client_fd;而子进程的主要任务是处理客户请求,它不必监听其他客户请求,因此子进程关闭了sockfd。由上可知,父子进程关闭相应的套接字并不会影响其所负责的通信功能。

我们可以通过下面的两幅示意图更进一步了解fork函数执行前后的客户端和服务器之间的状态。

父子进程关闭相应套接字后客户端和服务器端的状态如下:

这就是监听套接字和连接套接字两者的最终状态,子进程处理客户请求,父进程监听其他客户请求。这样的并发服务器看似完美,但是会产生许多僵死进程,这是为什么?下文将会分析。

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