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

在我的上一篇文章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网络编程之二(回显程序实例))