Unix网络编程学习笔记之第5章 TCP客户端/服务器程序示例

一、 一个简单TCP回射服务端程序

#include "unp.h"
#define MAXLINE 1024
#define PORT 13
#define CONMAX 5
void err_sys(const char* s)
{
    fprintf(stderr, "%s\n",s);
    exit(1);
}
void str_echo(int connfd)
{
    int nbyte;
    char buff[MAXLINE+1];
again:
    while(nbyte=read(connfd,buff, MAXLINE)>0)
        write(connfd, buff, nbyte);
    if(nbyte<0&& errno=EINTR)//被中断,重启。后面详见
        goto again;
    else if(nbyte<0)
        err_sys("read error");
}
int main(int argc, char** argv)
{
    int listenfd, connfd;
    struct sockaddr_in servaddr;
    listenfd=socket(AF_INET, SOCK_STREAM, 0);
   
    bzero(&servaddr,sizeof(servaddr));
    servaddr.sin_family=AF_INET;
    servaddr.port=htona(PORT);
    servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
   
    bind(listenfd,(struct sockaddr*) &servaddr, sizeof(servaddr));
   
    listen(listenfd, CONMAX);
   
    pid_t childpid;
    while(true)
    {
        connfd=accept(listenfd, (struct sockaddr*) NULL, NULL);
        if((childpid=fork())==0){
            close(listenfd);
            str_echo(connfd);
            exit(0);
        }
        close(connfd);
    }
}
// socket bind listen connect accept涉及socket的这些函数正常情况下都返回0或者具体描述符,在错误情况下都返回-1,错误被记录在errno。
//这里就不做错误检查了。

1. 这个回射程序很简单,就是客户端给服务器字符串,然后服务端接受并返回给客户端。和前面的程序相比较,不同的是这里是多进程程序,调用fork来创建子进程。子进程处理函数为str_echo

二、一个简单TCP回射客户端程序

#include "unp.h"
#define MAXCON 50
#define MAXLINE 1024
#define PORT 13
void err_sys(const char* s)
{
    fprintf(stderr, "%s\n",s);
    exit(1);
}
void str_cli(int sockfd, FILE* fd)
{
    char recvmsg[MAXLINE], sendmsg[MAXLINE];
    int n;
    while(fgets(sendmsg,MAXLINE,fd)!=NULL)//fgets函数会自动在尾部加'\0'
    {
        write(sockfd, sendmsg,strlen(sendmsg));
        if(read(sockfd,recvmsg, MAXLINE)<=0) //注意此时read返回0,不是正常终止,原因后面说
            err_sys("readerror");
        fputs(recvmsg, stdout);
    }
}
int main(int argc, char** argv)
{
    int sockfd;
    struct sockaddr_in servaddr;
    char buff[MAXLINE+1];
    if(argc!=2)
        err_sys("input error");
    if((sockfd=sock(AF_INET,SOCK_STREAM, 0))<0)
        err_sys("socket error");
    servaddr.sin_fimly=AF_INET;
    servaddr.sin_port=htons(PORT);
    if(inet_pton(AF_INET,argv[1], &servaddr.sin_addr)<=0)
        err_sys("ipaddress error");
    if(connect(sockfd,(struct sockaddr *)servaddr, sizeof(servaddr))<0)
        err_sys("connect error");
    str_cli(sockfd,stdin);
    exit(0);
}

1. 这个客户端程序很简单,就是从标准输入读入一行数据,然后发送给服务器。然后从服务器读取一行数据,显示在标准输出上。

 

三、正常启动

1. 首先服务器端启动,然后阻塞在accept函数。

2. 客户端启动,connect连接。然后客户端调用str_cli函数,阻塞在fgets函数上,等待用户输入数据。

3. 服务器端accept接受连接,fork一个子进程,调用str_echo函数,然后子进程阻塞在read函数上,而服务器端的主进程又会阻塞在accept函数,等待其他用户连接。

至此我们三个进程都在阻塞。

 

四、正常终止

1. 当客户端输入EOF(Ctrl-D)时,我们来终止程序,此时客户端fgets返回NULL,然后函数str_cli返回,则客户端exit退出,于是客户端会关闭打开的所有描述符,于是向服务器端发送一个FIN。

2. 当服务器端接受到这个FIN时,read函数返回0,向客户端发送ACK确认。然后 str_echo函数终止,然后子进程exit终止,这会关闭其打开的描述符,所以会向客户端发送一个FIN。

3. 然后客户端收到FIN并确认ACK,则至此客户端进入TIME_WAIT状态,服务器端继续监听其他客户。连接终止。

 

注意:Unix子进程终止时,会给父进程发送一个SIGCHLD信号,在上例中我们没有处理该信号,默认是忽略信号。既然父进程未加处理,则子进程进入僵死状态。

注意僵死状态说明该进程并没有完全地终止,没有释放所有资源。下面我们要说一下如何让子进程完全终止消失。

 

五、 信号处理

1. 信号:就是告知某个进程发生了某个事件的通知。一般信号都是异步的,即进程事先不知道信号在何时发生。

 

2. 信号可以是:

(1) 一个进程给另一个进程发送信号。

(2) 内核给一个进程发送信号。

上面提到的SIGCHLD信号就是内核在一个进程终止时,给它的父进程发送的信号。

 

3. 我们对一个信号的处理有三种方式:设置如何处理信号使用sigaction函数来设置。

(1) 我们提供一个信号处理函数,只要特定的信号发生,它就会被调用。这种行为称为捕获信号。有两种信号不能被捕获:SIGKILL和SIGSTOP

这个信号处理函数的原型:

void handler (int signo); 即无返回值,且参数是一个整型。

(2) 我们可以把某个信号的处理设定为SIG_IGN忽略它。SIG_KILL和SIG_STOP这两种信号不能被忽略。注意此时我们调用sigaction函数来设置忽略,则此进程就会被交给系统init去回收,不会存在僵死状态。

子进程进入僵死状态的原因是父进程未加任何处理。我们可以调用sigaction函数设置为SIG_IGN,则就不会产生僵死状态了。

(3) 我们也可以把某个信号的处理设定为SIG_DFL,来启动它的默认处置。有的信号默认处置是忽略,有的信号默认是终止进程。

 

4. sigaction函数

我们调用sigaction函数来设置对某个信号的处理。但是sigaction函数比较复杂,而signal函数比较简单:

void (* Sigfunc)(int);
Sigfunc signal(int sig, Sigfunc fun);

Sig为信号类型,如:SIGCHLD,fun为自定义的信号处理函数或SIG_IGN/SIG_DFL。然后函数返回该信号以前的信号处理函数指针。

但是POSIX规定要使用sigaction函数来设置信号。因为sigaction规定了一些有关信号处理的其他操作,而且signal函数在不同的系统之间的行为可能有所差别。有关如何使用sigaction详见P104,这里就不多说了。

 

5. POSIX保证某个信号在被处理函数处理期间,该信号是阻塞的。即如果此时,其他进程产生该信号,则该信号不会被提交。

注意:POSIX默认的是该信号在被处理函数处理处理期间,其他信号是不会被阻塞的,我们可以调用sigaction函数设置这个默认项。

且Unix信号是不排队的。即如果某个信号正在被处理,此时其他进程产生了多个该信号,则当这个信号被处理结束后,只有会产生一次信号提交。

 

六、 处理SIGCHLD信号

1. Unix设置僵死状态是为了维护子进程的信息,以便父进程在以后某个时候获取。这些信息包括进程pid,终止状态,资源利用信息等。

如果一个进程终止,如果该进程有僵死子进程,则僵死子进程的父进程的id都会被设为1,则交给init处理,来回收所有的僵死子进程。

注意只有fork子进程时,才有可能产生僵死进程。所以当我们fork子进程时,一定要处理SIGCHLD信号。

 

2. 在上述程序中,我们显然不需要保留僵死进程,则我们想要为SIGCHLD信号设置一个我们自定义的信号处理函数。则我们在主进程中加入代码:

signal(SIGCHLD, handler);   --注意这段代码一定要在第一个fork之前,且只要执行一次,所以在while循环之前加入此代码。

且必须在处理函数中调用wait/waitpid函数。

例如我们写一个处理函数:

void hander(int signo)
{
    pid_t pid;
    int stat;
    pid=wait(&stat);//此时的pid就是子进程的id
    return ;
}

 

3. 注意此时,看看程序,当一个子进程终止,向父进程递交SIGCHLD信号,此时父进程还正在阻塞accept。注意:此时父进程的accept会被中断,转而执行信号处理函数。即accept函数的优先级比较低。此时accept会发生错误,并且errno被设为EINTR。我们可以使用sigaction函数来设置处理该信号之后,会重启被中断的函数。

所以我们可以把上述的服务器端程序中的accept函数改为如下:

    while(true)
    {
        if((connfd=accept(listenfd,(struct sockaddr*) NULL, NULL))<0)
        {
            if(errno==EINTR)
                continue;
            else
                err_sys("accept error");
        }

注意此时accept可以重启的,read,write都是可以重启的。但是connect是不可以重启的,所以如果connect被中断,我们将调用select来等待连接完成。

 

七、 wait和waitpid函数

#include<sys/wait.h>
pid_t wait(int* statloc);
pid_t waitpid(pid_t pid, int* statloc, int option);

1. 可以看到waitpid提供了更多的选项,可以指定进程id以处理特定的子进程。-1表示处理第一个终止子进程。statloc和wait一样。option提供一些附加选项,其中最常用的就是WNOHANG,它告知waitpid在尚有未终止的子进程时,不要阻塞。

2. 我们建议使用waitpid,而不使用wait。原因:

考虑如下场景,当多个客户端连接上服务器端时,如果某个时刻,这多个客户端几乎同时终止。这样也就是多个子进程几乎同时终止,则同时给父进程发送多个SIGCHLD信号,而上文提到,Unix信号是不排队的,所以造成了不能完全处理多个子进程,导致存在多个僵死进程。

解决办法:使用waitpid

void hander(int signo)
{
       pid_t pid;
       int stat;
       while((pid=waitpid(-1,&stat, WNOHANG))>0);
       return;
}

此时我们使用WNOHANG来告知waitpid在尚有未终止的子进程时,不阻塞。而wait是阻塞的。

所以这样即使多个SIGCHLD信号几乎同时来到,waitpid不会阻塞,这样就能保证多个进程被正确处理,避免僵死进程的存在。

 

八、服务器进程终止

当我们正常的启动上述客户端和服务器端进程后,当某个时刻,服务器端的进程终止/崩溃了(可能是服务器端执行关机了,注意不是服务器主机崩溃)。

1. 这时,服务器端关闭打开的描述符,向客户端发送FIN。

2. 客户端响应ACK(注意一般响应ACK是内核自动完成的),但是此时我们看看客户端程序,其还阻塞在fgets上面,这就有问题了。客户端等待用户输入数据,当客户端输入数据后,阻塞在read上面,而服务器端接收到这个数据后发送一个RST,由于套接字接收缓冲区中已有一个第1步服务器发送FIN,所以read会去读这个FIN,然后导致read返回0,然后输出错误。导致程序终止。

3. 我们不希望出现这种情况,而是希望当服务器端程序终止,客户端这边应该立即知道,并且终止客户端。

4. 出现上述错误的原因在于,当FIN分组到达后,客户端阻塞在fgets上。客户此时有两个描述符,套接字和用户输入。它不能单纯的阻塞在某一个描述符上,所以后面我们提到使用select,poll等IO复用技术来解决这个问题。

 

插入的知识:

1. 发送/响应SYN,ACK,FIN,RST等分组是系统内核自动完成的,无需我们关心,即即使某个套接字描述符已经关闭,内核也可以发送RST,ACK等分组。

2. 套接字接收缓冲区的作用:当内核接收到对端发送的消息时,就会把这些消息放入套接字接收缓冲区中,如果此时进程正在处理其他程序,而不是阻塞在read上,等到进程处理完其他的程序后,调用read,这时read就会读出接收缓冲区内的消息。

 

九、 SIGPIPE信号

当一个进程向某个已收到RST分组的套接字描述符进行写操作时,内核就会向该进程发送SIGPIPE信号。

情景模拟:假如某个时刻服务器进程崩溃,客户端并不是知道,连续进行两次写操作,则第一次写操作,服务器端发送RST,第二次写操作就会发生SIGPIPE信号。

不管进程对该信号是处理还是忽略,第二个写操作都会返回EPIPE错误。

 

十、 服务器主机崩溃

注意这里不是主机关机。

则服务端不会发送任何东西,而客户端不知情,所以当客户端继续发送数据时,得不到任何东西,则它就会重传以希望得到ACK,大约过了9分钟左右,客户端才会发送重传。Read函数返回一个超时的错误。

 

十一、 服务器主机崩溃后重启

当服务器端崩溃后,9分钟内重启,此时服务器端接收到消息,而然服务器此时丢失了TCP连接的信息,所以它会向客户端发送RST分组。

客户端接收RST,然后read函数返回错误。

 

十二、服务器主机关机和服务器进程崩溃的效果是一样的。

 

十三、客户端、服务器端传送数据类型

我们可以在write函数中,发送二进制结构,如:结构体对象。

struct
{
       int i;
       long k;
}msg;
write(sockfd,&msg, sizeof(msg));

但是很不提倡在socket传输中,有这样的二进制结构,因为如果客户端和服务器端某一方不支持这种数据结构,或字节序不一样,支持的long的字节不一样,这都会引起异常的现象。

所以在传输socket时,应该把所有的二进制结构转换为字符型文本串,即char数组的形式。再进行传输是明智的。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值