UNP——第五章,TCP客户/服务程序

tcpser

void
str_echo(int sockfd)
{
        long            arg1, arg2;             
        ssize_t         n;              
        char            line[MAXLINE];  
        
        for ( ; ; ) {
                if ( (n = Readline(sockfd, line, MAXLINE)) == 0)        // 当读到EOF时(即对端close),函数返回,
                        return;         /* connection closed by other end */
        
                if (sscanf(line, "%ld%ld", &arg1, &arg2) == 2)
                        snprintf(line, sizeof(line), "%ld\n", arg1 + arg2);
                else
                        snprintf(line, sizeof(line), "input error\n");
        
                n = strlen(line);
                Writen(sockfd, line, n);
        }
}    

int
main(int argc, char **argv)
{
        int                                     listenfd, connfd;
        pid_t                           childpid;
        socklen_t                       clilen;
        struct sockaddr_in      cliaddr, servaddr;
        
        listenfd = Socket(AF_INET, SOCK_STREAM, 0);
                        
        bzero(&servaddr, sizeof(servaddr));
        servaddr.sin_family      = AF_INET;
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
        servaddr.sin_port        = htons(SERV_PORT);
                        
        Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
                
        Listen(listenfd, LISTENQ);
        
        for ( ; ; ) {
                clilen = sizeof(cliaddr);
                connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);

                if ( (childpid = Fork()) == 0) {        /* child process */
                        Close(listenfd);        /* close listening socket */
                        str_echo(connfd);       /* process the request */
                        exit(0);
                }
                Close(connfd);                  /* parent closes connected socket */
        }
}

 

tcpcli

void
str_cli(FILE *fp, int sockfd)
{
        char    sendline[MAXLINE], recvline[MAXLINE];
        
        while (Fgets(sendline, MAXLINE, fp) != NULL) {  // 行输入,当键入 <C-D>即输入EOF时,返回NULL,函数返回
        
                Writen(sockfd, sendline, strlen(sendline));

                if (Readline(sockfd, recvline, MAXLINE) == 0)
                        err_quit("str_cli: server terminated prematurely");
        
                Fputs(recvline, stdout);
        }
} 

int
main(int argc, char **argv)
{
        int                                     sockfd;
        struct sockaddr_in      servaddr;

        if (argc != 2)
                err_quit("usage: tcpcli <IPaddress>");

        sockfd = Socket(AF_INET, SOCK_STREAM, 0);

        bzero(&servaddr, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(SERV_PORT);
        Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);

        Connect(sockfd, (SA *) &servaddr, sizeof(servaddr));

        str_cli(stdin, sockfd);         /* do it all */

        exit(0);
}

以上面程序为基础,进行修改

1. 多进程服务器,对僵死进程的处理

void    
sig_chld(int signo)                     
{       
        pid_t   pid;
        int             stat;           
        
        while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0) {
                printf("child %d terminated\n", pid);
        }
        return;
} 
Signal(SIGCHLD, sig_chld);

捕捉处理SIGCHLD,sig_child 中使用 waitpid 非阻塞方式,理由是:

  虽然 信号处理函数执行时,SIGCHILD被阻塞,但是SIGCHILD是不排队的,即若有多个SIGCHILD发出,则 sig_child 返回后,只会再被调用一次。

  所以使用 waitpid 非阻塞方式,在一次sig_child调用时处理完所有剩余的 僵死进程。

 

1.2 信号处理的副作用

  信号处理会打断慢系统调用。解决方法有:

    (1)设置 sigaction 设置 flags 时,添加 SA_RESTART,交给内核重启。

    (2)处理慢系统调用 EINTR。

  后者更推荐,因为不是所有系统调用都支持 SA_RESTART 重启。

 

2. accept 返回前,连接中止

场景:

  服务器过于繁忙,来不及处理已建立连接的套接字。

  客户端等待太久,向服务端发了RST

  服务端应用层调用accpet,报错,返回 ECONNABORTED ,服务器可以忽略该报错,重新执行accept

 

3. 服务器进程终止

  启动服务器/客户端,连接后,杀死服务器子进程。

  子进程收到kill信号,进程退出,并关闭所有打开的文件描述符,服务端发送FIN。

  客户端TCP服务接受FIN,向套接字接受缓冲区写入EOF,并进入CLOSE_WAIT状态,而应用程序阻塞在fgets。

  客户端键入值后,会继续 write 给服务器端(TCP服务认为此时处于半关闭)

  服务端对应进程已经关闭,所以TCP服务收到数据后,会返回RST。

  客户端read获得EOF,知道对端已经关闭。

 

  本例子的问题:

    服务端进程关闭后,发送的FIN不能及时通知给客户端进程, 原因是:

    客户端在其他文件描述符处阻塞了。

    可以使用 select poll 解决。

 

3.1 固执的向已关闭的对端写数据

  当客户端的TCP服务接受到RST后,知道了对端已经关闭。

  客户端的应用程序虽然收到了EOF,但不理会,并向对端写更多数据,

  内核会对,向收到RST套接字执行写操作的进程,发送SIGPIPE信号。

  客户端应用程序收到SIGPIPE,默认动作是exit,并且write会返回EPIPE错误。

  

  通常 对于 SIGPIPE 选择 SIG_IGN

 

4. 服务器崩溃

  服务器/客户端正常连接后,直接断开服务器的网线,并重启

  由于服务器崩溃,所以未能发出FIN

  客户端应用执行 write,将数据交给TCP,后执行readline

  客户端TCP会重传数据,并期待对方ACK,大约9分钟后,向应用层报错。

  客户端应用由于阻塞在read ,所以在read处获得TCP报错, ETIMEOUT 或 EHOSTUNREACH 或ENETUNREACH

 

  我们希望客户端能更快的检查出对端已崩溃,可以选择

    (1)readline 设置超时

    (2)SO_KEEPALIVE 套接字选项

    (3)客户端心跳

 

4.1 服务崩溃后重启

  客户端正在不停的重发报,并期待ACK

  服务端重启完成。

  服务端TCP会返回RST

  由于客户端应用程序阻塞在readline,所以返回ECONNRESET错误

 

5. 服务器关机

  客户端/服务器正常通信后

  服务器关机,init进程向所有进程发送 SIGTERM,几秒后再发送SIGKILL,所以服务端会关闭所有套接字,TCP发送FIN。

  客户端应用会阻塞在readline,而不能即使知道服务端已经死掉。

 

  使用 select , poll 能解决这问题

 

6.地址和fd

  有时只知道fd,不知道地址对,可以使用 getsockname 和 getpeername 获得地址对

 

7.数据格式

  通信时,如果通信双方需要以某种格式理解接受的数据,而不是向上面的程序一样,单纯的收发。

  则需要注意数据格式。

  比如,A,B的大小端不同,A给B发数据,A是大端,发送 0x0102 的二进制串,B接受后虽然内存中二进制串的排列没变,但是理解方式是安装小端,于是A,B通信出错。

  另外系统位数也会造成问题。

  所以,通常选择合适的编码方式进行发送,比如文本方式,而非二进制方式。

  或显示定义 二进制格式(位数,大小端)

 

posted on 2020-02-20 22:13  开心种树  阅读(212)  评论(0编辑  收藏  举报