美文网首页
高级C与网络编程复习(5)—— TCP C/S程序示例(TCP

高级C与网络编程复习(5)—— TCP C/S程序示例(TCP

作者: SunnyQjm | 来源:发表于2020-04-26 23:12 被阅读0次

简单回射程序概述

  • 客户从标准输入读入一行文本,并写给服务器
  • 服务器从网络输入读入这行文本,并回射给客户
  • 客户从网络输入读入这行回射文本,并显示在标准输出上


    简单的回射客户 / 服务器

TCP 回射服务程序

tcpserv01.c

#include <unp.h>

void str_echo(int);

int main(int argc, char** argv){
    int listenfd, connfd;
    pid_t pid;
    socklen_t clen;
    struct sockaddr_in cliaddr, servaddr;

    listenfd = Socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(9748);

    //指定服务端socket的地址为通配地址
    //表示接收来自本机各个网络接口的连接请求
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

    Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
    Listen(listenfd, LISTENQ);

    for( ; ; ){
        clen = sizeof(cliaddr);
        connfd = Accept(listenfd, (SA *) &cliaddr, &clen);
        //接受到来自客户端的请求之后,fork一个进程,在子进程中为客户提供服务
        //父进程则关闭本进程内该已连接描述符(引用计数减1)
        //然后再返回继续accept,可以达到并发的效果
        if( (pid = Fork()) == 0 ){      //子进程执行
            Close(listenfd);
            str_echo(connfd);
            Close(connfd);
            exit(0);
        }
        Close(connfd);                  //父进程执行
    }
}


/**
 * 为客户端提供服务
 * 从客户端接收一个字符串,并将字符串回射回客户端
 */
void str_echo(int connfd){
    ssize_t n;
    char buf[MAXLINE];

again:
    while((n = read(connfd, buf, MAXLINE)) > 0)
        Writen(connfd, buf, n);
    if(n < 0 && errno == EINTR)
        goto again;
    else if(n < 0)
        err_sys("str_echo: read error");
}
  • 50 ~ 55: 这里用到的是系统的 read 函数,没有对信号中断错误处理,需要自己处理。(收到客户的 FIN 或 EOF 将导致 read 返回)

TCP 回射客户端程序

#include <unp.h>

void str_cli(FILE*, int);

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

    if(argc != 2)
        err_quit("usage: ./tcpserv01 <IPAddress>");
    sockfd = Socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_port = htons(9748);
    servaddr.sin_family = AF_INET;

    //调用inet_pton函数将用户输入的点分十进制串转化成网络字节序的32为IPv4地址
    Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);

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

    str_cli(stdin, sockfd);
    Close(sockfd);
}

/**
 * 用fgets读取用户的一行输入,发送给服务器,再从服务器接收一行回复并打印到控制台
 */
void str_cli(FILE* fp, int sockfd){
    char sendbuf[MAXLINE], recvline[MAXLINE];
    while(Fgets(sendbuf, MAXLINE, fp) != NULL){
        Writen(sockfd, sendbuf, strlen(sendbuf));
        if(Readline(sockfd, recvline, MAXLINE) == 0)
            err_quit("str_cli: server terminated prematurely");
        Fputs(recvline, stdout);
    }
}

正常启动

  • 先启动服务器

    //后台启动服务器
    ./tcpserv01 &
    [1] 10186
    
    //我们查看一下端口的状态,用管道过滤,只显示9748端口的信息
    netstat -a | grep 9748
    
    Proto Recv-Q Send-Q Local Address           Foreign Address         State
    tcp        0      0 0.0.0.0:9748            0.0.0.0:*               LISTEN
    
  • 接着直接在本机连接服务器

    //连接到本机的回环地址
    ./tcpcli01 127.0.0.1
    
    //再开一个shell,查看当前的端口状态状态
    netstat -a | grep 9748
    Proto Recv-Q Send-Q Local Address           Foreign Address         State
    tcp        0      0 0.0.0.0:9748            0.0.0.0:*               LISTEN
    tcp        0      0 localhost:9748          localhost:32890         ESTABLISHED
    tcp        0      0 localhost:32890         localhost:9748          ESTABLISHED
    
  • 0.0.0.0 ==> 代表通配地址

    • ==> 代表通配端口
  • 客户端收到三路握手的第二个分节的时候,connect 函数就返回了。而服务器收到三次握手的第三个分节的时候 accept 才返回(之前分析过,服务器在收到第二个分节的时候还处于 SYN_RECV 状态,只有在收到第三个分节的时候才进入 ESTABLISHED 状态,此时相应的 socket 才被扔进监听套接字的已完成队列。而 accept 只在已完成队列中取 socket,所以 accept 必定是在收到第三个分节之后才返回)

正常终止

  • 状态转换图:


    TCP 状态转换图
  • 进程终止:

    • 关闭本进程打开的所有的描述符
    • 向父进程发送一个 SIGCHLD 信号
./tcpcli01 127.0.0.1
hello
hello         //服务器回射回来的
good bye
good bye      //服务器回射回来的
^D            //Ctrl + D 相当于输入EOF

//在客户端程序退出后马上查看端口状态(由于是本机测试,一定要快,要不然抓不到)
netstat -a | grep 9748
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 0.0.0.0:9748            0.0.0.0:*               LISTEN
tcp        0      0 localhost:33146         localhost:9748          TIME_WAIT
  • 键入 EOF 后,客户端的 fgets 返回 NULL,导致 str_cli 结束,最终导致 main 函数执行到 exit 而终止

  • 进程终止的部分任务是关闭进程打开的所有的描述符,因此客户端打开的套接字由内核关闭,这导致客户 TCP 向服务器发送一个 FIN,服务器回一个 ACK。至此,四路挥手的前两步完成。服务器处于 CLOSE_WAIT 状态,而客户端处于 FIN_WAIT_2 状态

  • 服务器 TCP 收到 FIN 后,readline 函数返回 0,导致 str_echo 退出,接着 main 函数执行到 exit,进而导致服务端子进程退出。

  • 同样的,服务端子进程所打开的所有描述符随之关闭。这导致服务器向客户发送一个 FIN,客户回一个 ACK,至此,四路挥手结束,连接完全终止。客户套接字进入 TIME_WAIT 状态

  • 进程终止的另一部分内容是:在服务器子进程终止时,给父进程发送一个 SIGCHLD 信号。由于我们在代码中没有捕获该信号,而该信号的默认处理为忽略,所以就导致子进程进入僵死状态

    //我们调用ps命令来验证一下
    //因为笔者在测试的时候打开了客户端两次,所以有两个僵死的子进程
    ps -a
      PID TTY          TIME CMD
     2720 pts/0    00:00:21 hexo
    10186 pts/1    00:00:00 tcpserv01
    11146 pts/1    00:00:00 tcpserv01 <defunct>         //僵死进程
    13389 pts/1    00:00:00 tcpserv01 <defunct>         //僵死进程
    18996 pts/1    00:00:00 ps
    

POSIX 信号处理

信号(signal)就是告知某个进程发生了某个事件的通知,有时也称为软件中断(software interrupt)。通常是异步的

  • 每个信号关联一个处置(deposition),或称行为(action)。在信号发生时执行
  • 类型

    • 一个进程发给另一个进程(可以是自身)
    • 由内核发给进程
  • 三种处置

    • 自定义信号处理函数,然后用 sigaction 设置给信号

      • SIGKILL 和 SIGSTOP 不能被捕获

        //信号处理函数原型
        void handler(int signo);
        
    • SIG_IGN ==> 忽略信号

    • SIG_DEF ==> 默认处理

  • signal 函数

    • 原型:

      void (*signal(int signo, void (*func)(int)))(int);
      
      //定义新类型,来化简上面的原型
      typedef void Sigfunc(int)
      
      /**
      * 为一个信号设置处理函数
      * @param signo      信号
      * @param func       信号处理函数
      * @return           指向信号处理函数
      */
      Sigfunc *signal(int signo, Sigfunc *func);
      
    • POSIX 规定设置信号的处置必须调用 sigaction,上面的 signal 是对 signation 的封装,更容易使用

处理 SIGCHLD 信号

  • 僵死状态

    • 僵死(zombie)状态的目的是维护子进程的信息,以便父进程在以后某个时候获取。这些信息包括子进程的进程 ID、终止状态以及资源利用信息。如果一个进程终止,而该进程有子进程处于僵死状态,那么它的所有僵死子进程的父进程 ID 将被重置为 1(init 进程)。继承这些子进程的 init 进程将清理它们
  • 处理僵死进程

    //首先定义如下信号处理函数
    void sig_chld(int signo){
      pid_t pid;
      int stat;
      pid = wait(&stat);
      printf("child %d terminated\n", pid);
      return;
    }
    
    //在上面的server端程序的Listen之后添加下面这行
    Signal(SIGCHLD, sig_child);
    
    • 点我查看源码
    • 在执行了上面的处理之后,再测试,就观测不到僵死进程了
  • 处理被中断的慢系统调用

    • 适用于慢系统调用的基本规则:当阻塞于某个慢系统调用的一个进程捕获某个信号且相应信号处理函数返回时,该系统调用可能返回一个 EINTR 错误

    • 有些系统上会发生,有些系统做了处理,不会发生。但是为了便于移植,还是建议用类似于下面的方法处理这种错误

      for( ; ; ){
        clilen = sizeof(cliaddr);
        if( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0){
          if(errno == EINTR)
            continue;
          else
            err_sys("accept error");
        }
      }
      

wait 和 waitpid 函数

#include <sys/wait.h>

/**
* 用于在父进程中清理已终止子进程(解除子进程的僵死状态)
*
* @param  wait函数通过这个参数返回子进程的终止状态
* @return 成功则返回被清理子进程的ID,错误则返回0或-1
*/
pid_t wait(int *statloc);

/**
* 用于在父进程中清理已终止子进程(解除子进程的僵死状态)
* @param pid      用于指定清理那个子进程,如果传入-1则表示等待第一个终止的子进程
* @param statloc  函数通过这个参数返回子进程的终止状态
* @param options  可选项
* @return 成功则返回被清理子进程的ID,错误则返回0或-1
*/
pid_t waitpid(pid_t pid, int *statloc, int options);
  • 调用 wait 函数的时候如果没有已经终止的子进程,不过仍然有一个或多个子进程在执行,那么 wait 函数将阻塞到其中任意一个子进程终止为止

  • waitpid 的 options 可选项如果制定为 WNOHANG,则告知内核在没有已终止子进程时不要阻塞

  • wait 函数和 waitpid 的区别

    • waitpid 可以指定终止哪个子进程,而 wait 不能
    • waitpid 可以实现在没有已终止子进程时不要阻塞,而 wait 不能
  • 根据上述区别的第二点我们改进之前的信号处理函数

    //由于wait不能实现在没有已终止子进程时不要阻塞,所以在下面的循环中不能调用wait,否则可能会阻塞主线程
    //经过下面的修改之后,就可以支持一次调用清理多个进程的要求
    
    void sig_chld(int signo){
      pid_t   pid;
      int     stat;
      while( (pid = waitpid(-1, &stat, WNOHANG)) > 0)
        printf("chihld %d terminated\n", pid);
      return;
    }
    
  • 具体可以参考课本 5.10 介绍的同时开五个连接请求的情况

相关文章

网友评论

      本文标题:高级C与网络编程复习(5)—— TCP C/S程序示例(TCP

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