TCP客户/服务器程序实例——回射服务器


目录

客户/服务器程序源码

POSIX信号处理

POSIX信号语义

处理SIGCHLD信号

处理僵死进程

处理被中断的系统调用

wait和waitpid函数

wait和waitpid函数的区别

网络编程可能会遇到的三种情况

TCP程序小结

数据格式

 

回射输入行这样一个客户/服务器程序是一个虽然简单然而却很有效的网络应用程序的例子。实现任何客户/服务器网络应用所需的所有基本步骤可通过本例子阐明。若想把本例子扩充成你自己的应用程序,你只需修改服务器对于来自客户的输入的处理过程。

<sys/socket.h><strings.h><sys/types.h><netinet/.h><stdio.h><stdlib.h><unistd.h><errno.h> argc, **((listenfd = socket(AF_INET, SOCK_STREAM, )) < &servaddr, === htons((bind(listenfd, ( sockaddr *)&servaddr, (servaddr)) < (listen(listenfd, ) < = ((connfd = accept(listenfd, ( sockaddr *)&cliaddr, &clilen)) < ((childpid = fork()) < (childpid == ) (close(listenfd) < ) (close(connfd) < )

TCP回射服务器程序:str_echo函数

<stdio.h><stdlib.h><errno.h>

           buf[((n = read(sockfd, buf, )) > (n <  && errno == (n < 

TCP回射客户程序:main函数

<stdio.h><strings.h><.h><stdlib.h><errno.h><unistd.h><sys/types.h><netinet/.h><sys/socket.h>

 argc,  **(argc != ((sockfd = socket(AF_INET, SOCK_STREAM, )) < &servaddr, == htons((inet_pton(AF_INET, argv[], &servaddr.sin_addr) < (connect(sockfd, ( sockaddr *)&servaddr, (servaddr)) < 

TCP回射客户程序:str_cli函数

<stdio.h><stdlib.h><.h>

*fp,     sendline[], recvline[(fgets(sendline, , fp) !=(readline(sockfd, recvline, ) == 

服务器和客户都要调用的自定义函数:

#include <stdio.h><stdlib.h><errno.h><unistd.h><sys/types.h> fd,  *       *==(nleft > ((nread = read(fd, ptr, nleft)) < (errno === ;    
            
                (- (nread == ;            -=+=(n - nleft);             fd,   *     *==(nleft > ((nwritten = write(fd, ptr, nleft)) <= (nwritten <  && errno === ;    
            
                (-);    -=+=(n - fd,  *           c, *=(n = ; n < maxlen; n++((rc = read(fd, &c, )) == *ptr++ =(c == ;     (rc == *ptr = (n - );    (errno ==(-);    *ptr = ;    
    

正常启动:

首先,我们在主机Linux上后台启动服务器。

handler( signo );

对于大多数信号来说,调用sigaction函数并指定信号发生时所调用的函数就是捕获信号所要做的全部工作。不过,SIGIO、SIGPOLL和SIGURG这些个别信号还要求捕获它们的进程做些额外工作。

(2)我们可以把某个信号的处置设定为SIG_IGN来忽略(ignore)它。SIGKILL和SIGSTOP这两个信号不能被忽略。

(3)我们可以把某个信号的处置设定为SIG_DFL来启用它的缺省(default)处置。缺省处置通常是在收到信号后终止进程,其中某些信号还在当前工作目录产生一个进程的核心映像(core image,也称为内存映像)。另有个别的缺省处理是忽略:SIGCHLD和SIGURG(带外数据到达时发送)就是缺省处置为忽略的两个信号。

POSIX信号语义

我们把符合POSIX的系统上的信号处理总结如下:

(1)一旦安装了信号处理函数,它便一直安装着(较早期的系统是每执行一次就将其拆除)。

(2)在一个信号处理函数运行期间,正被递交的信号是阻塞的。而且,安装处理函数时在传递给sigaction函数的sa_mask信号集中指定的任何额外信号也被阻塞。

(3)如果一个信号在被阻塞期间产生了一次或多次,那么该信号被解阻塞之后通常只递交一次,也就是说UNIX信号缺省是不排队的。

(4)利用siagprocmask函数(http://www.cnblogs.com/nufangrensheng/p/3515257.html)选择性地阻塞或解阻塞一组信号是可能的。这使得我们可以做到在一段临界区代码执行期间,防止捕获某些信号,以此保护这段代码。

处理SIGCHILD信号

设置僵死(zombie)状态的目的是维护子进程的信息,以便父进程在以后某个时候获取。这些信息包括子进程的进程ID、终止状态以及资源利用信息(CPU时间、内存使用量等等)。如果一个进程终止,而该进程有子进程处于僵死状态,那么它的所有僵死子进程的父进程ID将被重置为1(init进程)。继承这些子进程的init进程将清理它们(也就是说init进程将wait它们,从而去除它们的僵死状态)。有些UNIX系统在ps命令输出的COMMAND栏以<defunct>指明僵死进程。

处理僵死进程

我们显然不愿意留存僵死进程。它们占用内核中的空间,最终可能导致我们耗尽进程资源。无论何时我们fork子进程都得wait它们,以防它们变成僵死进程。为此我们建立一个俘获SIGCHLD信号的信号处理函数,在函数体中我们调用wait。通过在TCP回射服务器程序:main函数中的listen调用之后增加如下函数调用:

signal(SIGCHLD, sig_chld);

这样我们就建立了该信号处理函数。(这必须在fork第一个子进程之前完成,并且只做一次。) 我们接着定义名为sig_chld的这个信号处理函数,如下:

= wait(&

处理僵死进程的可移植方法就是捕获SIGCHLD,并调用wait或waitpid。

新的问题是:在某些系统上(这些 系统标准C函数库中提供的signal函数不会致使内核自动重启被中断的系统调用),SIGCHLD信号被捕获并处理后,慢系统调用accept会返回一个EINTR错误(被中断的系统调用).

处理被中断的系统调用

慢系统调用(slow system call)是指那些可能永远阻塞的系统调用(调用有可能永远无法返回)。多数网络支持函数都属于这一类。

适用于慢系统调用的基本规则是:当阻塞于某个慢系统调用的一个进程捕获某个信号且相应信号处理函数返回时,该系统调用可能返回一个EINTR错误。有些内核自动重启某些被中断的系统调用。

为了处理被中断的accept,我们把对accept的调用从for循环开始修改如下:

= ((connfd = accept(listenfd, ( sockaddr *)&cliaddr, &clilen)) < (errno ==;    
        

这段代码所做的事情就是自己重启被中断的系统调用。对于accept以及诸如read、write、select和open之类函数来说,这是合适的。不过有一个函数我们不能重启:connect。如果该函数返回EINTR,我们就不能再次调用它,否则将立即返回一个错误。当connect被一个捕获的信号中断而且不自动重启时,我们必须调用select来等待连接完成。

wait和waitpid函数

#include <sys/wait.h> * *statloc, -

函数wait和waitpid均返回两个值:函数返回值是已终止子进程ID号,子进程的终止状态(一个整数)则通过statloc指针返回。我们可以调用三个宏来检查终止状态,并辨别子进程是正常终止、由某个信号杀死还是仅仅由作业控制停止而已(http://www.cnblogs.com/nufangrensheng/p/3510101.html)。

如果调用wait的进程没有已终止的子进程,不过有一个或多个子进程仍在执行,那么wait将阻塞到现有子进程第一个终止为止。

waitpid函数对于等待哪个进程以及是否阻塞给了我们更多的控制。首先,pid参数允许我们指定想等待的进程ID,值-1表示等待第一个终止的子进程。其次,options参数允许我们指定附加选项。最常用的选项是WNOHANG,它告知内核在没有已终止子进程时不要阻塞。

函数wait和waipid的区别

为了说明wait和waitpid的区别,我们试想如下情况:

((pid = waitpid(-, &stat, WNOHANG)) >

服务器的最终版本

<sys/socket.h><strings.h><sys/types.h><netinet/.h><stdio.h><stdlib.h><unistd.h><errno.h>

 argc,  **                   sig_chld(((listenfd = socket(AF_INET, SOCK_STREAM, )) < &servaddr, === htons((bind(listenfd, ( sockaddr *)&servaddr, (servaddr)) < (listen(listenfd, ) < 
    = ((connfd = accept(listenfd, ( sockaddr *)&cliaddr, &clilen)) < (errno ==;                
            ((childpid = fork()) <  (childpid == )    (close(listenfd) < ) (close(connfd) < ) 

在网络编程时可能会遇到的三种情况:

(1)当fork子进程时,必须捕获SIGCHLD信号。

(2)当捕获信号时,必须处理被中断的系统调用。

(3)SIGCHLD的信号处理函数必须正确编写,应使用waitpid函数以免留下僵死进程。

TCP程序实例小结:

在客户和服务器可以彼此通信之前,每一端都得指定连接的套接口对:本地IP地址、本地端口、远地IP地址、远地端口。在下图中我们以粗体圆点标出了这四个值。该图处于客户端的角度。远地IP地址和远地端口必须在客户端调用connect时指定,而两个本地值通常就由内核作为connect的一部分来选定。客户也可在调用connect之前,通过调用bind来指定其中一个或全部两个本地值,不过这么做不常见。

<sys/types.h><stdlib.h><unistd.h><.h> line[((n = readline(sockfd, line, )) == ; (sscanf(line, , &arg1, &arg2) == (line), , arg1 +(line), =

我们调用sscanf把文本串中的两个参数转换为长整数,然后调用snprintf把结果转换为文本串。

不论客户和服务器主机的字节序如何,这个新的客户和服务器对都工作的很好。

例子:在客户与服务器之间传递二进制结构

现在我们 把客户和服务器程序修改为穿越套接口传递二进制值而不是文本串。

我们的客户和服务器程序的main函数无需改动。另外我们给两个参数定义了一个结构,给结果定义了另一个结构。

 _COMMON_H

<stdio.h><stdlib.h><.h>

*fp,     sendline[], recvline[(fgets(sendline, , fp) !=(sscanf(sendline, , &args.arg1, &args.arg2) != &args, (readn(sockfd, &result, (result)) == 
<stdio.h><stdlib.h><errno.h>

((n = readn(sockfd, &args, (args))) == = args.arg1 +&result, 

如果我们在具有相同体系结构的两个主机上运行我们的客户和服务器程序,那么什么问题都没有。但是如果在具有不同体系结构的两个主机上运行同样的客户和服务器程序(例如服务器运行在大端系统,而客户运行在小端系统上),那就无法工作了。

本例子实际上存在三个潜在的问题:

(1)不同的实现以不同的格式存储二进制数。(大端和小端)

(2)不同的实现在存储相同的C数据类型上可能存在差异。(32位系统和64位系统)

(3)不同的实现给结构打包的方式存在差异,这取决于各种数据类型所用的位数以及机器的对齐限制。

因此,穿越套接口传送二进制结构绝不是明智的选择。

解决这种数据格式问题有两个常用的方法:

(1)把所有的数值数据作为文本串来传递。当然这里假设客户和服务器主机具有相同的字符集。

(2)显示定义所支持数据类型的二进制格式(位数、大端或小端),并以这样的格式在客户和服务器之间传递所有数据。

相关内容