美文网首页程序员IOS
socket网络编程之二(回显程序实例)

socket网络编程之二(回显程序实例)

作者: 无心雨眸 | 来源:发表于2017-09-28 22:35 被阅读99次

    在我的上一篇文章socket网络编程之一(TCP套接字API)中,介绍了ipv4的套接字数据结构和tcp套接字相关API,本篇文章,将利用上一篇介绍的API写一个服务器回显程序,加深对TCP套接字API的理解.源码地址

    注意!github上的源码是我在学习UNIX网络编程的过程中,对书中的源码的实现,是一系列的源码,还在完善中,echoProgram文件夹中的代码和本篇博文是对应的,下载下来之后请先查看, README文件。代码中的一些公共函数,比如错误处理和包裹函数我都是放在public文件夹下面的。包裹函数就是一些基本函数包含了错误处理操作,大家一看就能明白。

    注意!!!所有源码,我使用xcodeIDE实现的,没办法IOS程序猿一枚,大家要在其它开发环境上面跑,请自行移植!所有源码,我使用xcodeIDE实现的,没办法IOS程序猿一枚,大家要在其它开发环境上面跑,请自行移植!所有源码,我使用xcodeIDE实现的,没办法IOS程序猿一枚,大家要在其它开发环境上面跑,请自行移植!

    客户端代码

     int sockfd;
     struct sockaddr_in sockaddr;
    
     sockfd = Socket(AF_INET, SOCK_STREAM, 0);
     bzero(&sockaddr, sizeof(sockaddr));
     sockaddr.sin_port = htons(9999);
     sockaddr.sin_family = AF_INET;
     inet_pton(AF_INET,"127.0.0.1",&sockaddr.sin_addr);
        
     Connect(sockfd, (struct sockaddr *)&sockaddr, sizeof(sockaddr));
     str_cli(stdin, socked);
    
    • 上述代码,首先声明了一个套接字描述符和一个IPV4的套接字结构,然后调用Socket函数,赋值给sockfd.
    • bzero表示将sockaddr的值设置为0,在使用sockaddr之前,必须要调用bzero.
    • 设置服务端的端口号为9999,htons表示将主机子节序转换为网络子节序。设置sin_family的协议族为AF_INET.将一个点分十进制的地址,转换为sockaddr.sin_addr结构的地址。这样我们就完成了要连接到服务端的套接字的配置.
    • 调用connection函数,连接服务器,如果connect函数,成功返回,则表示TCP三次握手完成。完成之后,就可以进行通信了.connect函数,如果不返回,程序则会阻塞在connect调用上.
    • 最后我们调用,str_cli函数,和服务端进行通信,下面讲解str_cli函数.
    void str_cli(FILE *fd,int sockfd){
        char sendline[MAXLINE],recvline[MAXLINE];
        ssize_t status;
        while ( Fgets(sendline, MAXLINE, fd)!=NULL  ) {
            Writen(sockfd, sendline, strlen(sendline));
            status = read(sockfd, recvline, MAXLINE);
            if (  status< 0  ) {
                err_sys("read error");
            }
            puts(recvline);
        }
    }
    
    • 我们从while循环讲起,在while中首先调用fgets函数,等待用户从标准输入,如果用户一直没有输入,程序会是一只阻塞的,如果用户输入了一行,以回车键结束,fget函数解除阻塞,然后返回。
    • 如果fget函数接收到了输入,输入的数据存在sendline数组中,调用write函数会将数据发送出去,此时write函数阻塞,当发送完成之后,则解除阻塞,函数返回.
    • 如果数据发送成功,则调用read函数,用于接收服务端发送来的数据,此时程序依然阻塞,如果接收完数据了,就将数据打印出来。

    自此,str_cli函数完成了,在正常情况下,该程序没有任何问题,但是有一种例外情况,就是,当程序阻塞在fgets函数期间,服务端程序崩溃了,服务端程序会发送关闭连接的请求,而此时,客户端程序是不知道的。然后用户输入文本,fgets解除阻塞,再调用write函数,由于服务端已经关闭了连接,write函数肯定会写入不成功,这样照成的问题是客户端由于阻塞在fgets上,不能及时知道服务端的状况,这样写出来的程序就有问题.因此我们考虑用selece函数,来解决该问题,下面是str_cli的select版本.

     void str_cli(FILE *fd,int sockfd){
        int maxfdp1;
        fd_set rset;
        char sendline[MAXLINE],recvline[MAXLINE];
        ssize_t readlen;
        
        //将fd_set全部设置为0
        FD_ZERO(&rset);
        
        for (; ; ) {
            //FD_SET表示我们关心的文件描述符
            FD_SET(fileno(fd),&rset);
            FD_SET(sockfd,&rset);
            //将maxfdp1设置为描述符+1是因为文件描述符是从0开始的
            maxfdp1 = ((int)fmaxf(fileno(fd), sockfd)) + 1;
            Select(maxfdp1, &rset, NULL, NULL, NULL);
            
            //FD_ISSET如果返回真,表示sockfd有数据了
            if ( FD_ISSET(sockfd,&rset) ) {
                readlen = read(sockfd, recvline, MAXLINE);
                //如果读取到的数据为0表示服务端的子进程被杀死了
                if ( readlen==0 ) {
                    err_quit("server terminal");
                }
                if ( readlen < 0 ) {
                    err_sys("read error");
                }
                
                //将输出的文件打印出来
                puts(recvline);
            }
            
            if ( FD_ISSET(fileno(fd),&rset) ) {
                if ( Fgets(sendline, MAXLINE, fd)==NULL ) {
                    return;
                }
                
                Writen(sockfd, sendline, MAXLINE);
            }
        }
    }
         
    
    • 首先调用FD_ZERO函数,将rset设置为0,我们在用fd_set结构的时候,必须要调用该函数.
    • 在for循环中,我们调用FD_SET函数,设置我们要关心的描述符.
    • 然后调用select函数,进行I\O复用,注意select函数,传递描述符的时候,一定是最大描述符+1,select的最后一个参数,是等待的时间,在等待的时间内,程序是阻塞的。传入NULL表示无限等待.
    • FD_ISSET用于判断到底是哪一个描述符的被激活了,如果是sockfd则调用读,若服务端发送了关闭连接,也能马上监测到了。

    我们用select 函数可以解决,当fgets函数出于阻塞状态时,服务端关闭了连接,客户程序不能立马知道的情况。

    服务端代码

        int listenfd,connfd;
        pid_t childPid;
        socklen_t clilen;
        struct sockaddr_in cliaddr,seraddr;
        
        listenfd = socket(AF_INET, SOCK_STREAM, 0);
        
        bzero(&seraddr, sizeof(seraddr));
        seraddr.sin_port = htons(9999);
        seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
        seraddr.sin_family = AF_INET;
        
        Bind(listenfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
        
        Listen(listenfd,LISTENQ);
        
        Sigal(SIGCHLD, sig_child);
        
        for ( ; ; ) {
            clilen = sizeof(cliaddr);
            connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
          
            //等于0表示子进程
            if ( ( childPid = Fork() ) == 0 ) {
                printf("子进程号为:%d",getpid());
                Close( listenfd );
                str_echo(connfd);
                exit(0);
            }
            
            Close(connfd); /* 父进程应当关闭连接 */
        }
    
    • 服务端程序,首先调用bind函数设置服务端的端口号,ip地址设置为INADDR_ANY表示通配IP地址。
    • 然后调用listen函数用于监听,listen函数在上一篇文章中有介绍,不多说.
    • 调用signal函数,处理信号,要用该函数的原因是,当子进程结束的时候,内核会给程序发送一个中断信号,告诉程序,他的子进程已经终止,我们要捕捉该信号,将子进程的资源回收,不然会照成资源的浪费.
    • 在for循环中,accept函数,在返回之前,是一直阻塞的,该函数属于慢系统调用,慢系统调用的意思是有可能永远阻塞下去,此时如果子进程终止,如果没有信号处理函数的话,会返回EINTR的错误。accept返回之后,表明三次握手完成。
    • 我们的每一个连接,都用一个子进程来处理,由于子进程是父进程的一份拷贝,因此我们要关闭一个套接字描述符,然后调用str_echo函数,用于处理客户端发来的数据,下面是str_echo函数.
    void str_echo(int sockfd)
    {
        ssize_t n;
        char buf[MAXLINE];
        
    again:
        while ( (n = read(sockfd, buf, MAXLINE)) > 0 ) {
            Writen(sockfd, buf, n);
        }
        
        if ( n < 0 && errno == EINTR )
            goto again;
        else if (n < 0)
            err_sys("str_echo: read error");
    }
    
    • 当三次握手完成后,我们首先调用read函数,读取从客户端发来的数据,如果读取到了数据,则将数据原封不动的发回去。这便是程序的回显功能。在signal函数汇总,还有一个sig_child没有讲到.
    void sig_child(int signo)
    {
        pid_t pid;
        int stat;
        
        printf("当前进程号为:%d",getpid());
        
        printf("signal num = %d",signo);
        
        //pid = wait(&stat);
        //waitpid处理
        //printf("child %d terminated\n",pid);
        while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0 ) {
            printf("child %d terminated\n",pid);
        }
    }
    
    • 如果内核给进程发送信号,sig_child函数便是我们的信号处理函数的回调函数,调用waitpid函数,回收子进程。

    相关文章

      网友评论

        本文标题:socket网络编程之二(回显程序实例)

        本文链接:https://www.haomeiwen.com/subject/szivextx.html