[UNP]:TCP客户服务器_迭代式

编写一个完整的 TCP 客户端/服务器程序:

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

1. 函数准备

1. TCP 回射服务器程序

原型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include “unp.h”

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");
}

2. TCP 回射客户端程序

原型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "unp.h"

void str_cli(FILE *fp, int sockfd)
{
char sendline[MAXLINE], recvline[MAXLINE];

while(Fgets(sendline, NAXLINE, fp) != NULL)
{
Writen(sockfd, sendline, strlen(sendline));

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

}

}

2. 代码实现

1. TCP客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include "unp.h"

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);
}

2. TCP服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include "unp.h"

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

void sig_chld(int);

listenfd = Socket(AF_INET, SOCK_STREAN, 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);

Signal(SIGCHLD, sig_chld); /*必须调用 waitpid() 回收僵尸进程*/

for( ; ; )
{
chilen = sizeof(chiaddr);
if((connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0)
{
if(errno == EINTR) // 被中断重新调用
continue;
else
err_sys("accept error");
}
if((childpid = Fork()) == 0)/* 子进程*/
{
Close(lisenfd); /*关闭监听套接字*/ // 关闭后面没用的套接字
str_echo(connfd);
exit(0);
}
Close(connfd); /*必须关闭 不然一直占用描述符*/
}

}

/**********************************************************************************/
// 回收子进程 避免产生僵尸进程
void sig_chld(int signo)
{
pid_t pid;
int stat;

while( (pid = waitpid(-1, &stat, WNOHANG)) > 0) // WNOHANG 不阻塞
printf("child %d terminated\n", pid);

return;
}

3. 问题分析

僵尸进程与处理

  1. 产生:

    • 子进程终止时发送给父进程一个 SIGCHLD 信号,若父进程没有捕获改信号处理,则子进程变为僵尸进程。
  2. POSIX 信号处理知识:

    1. 父进程通过调用 sigaction() 函数捕获信号,捕获到信号有三种处理方式:

      • 提供一个函数,只要特定信号发生,就调用该函数。SIGKILLSIGSTOP 不能被捕获

        信号处理函数原型:

        1
        void handler(int signo);// 无返回值, 形参为 信号
      • 把信号的处置设置为 SIG_NGN 忽略信号。SIGKILLSIGSTOP 不能忽略

      • 把信号处置设置为 SIG_DEF 启动默认处置

    2. POSIX 信号处理总结

      • 一旦安装了信号处理函数,便一直有效
      • 在一个信号处理函数运行期间,正被递交的函数是阻塞的
      • 如果一个信号在被阻塞期间产生了一次或多次,那么该信号被解除阻塞后只递交一次,Unix 信号默认是不排队的
  3. 使用 waitpid() 函数代替 wait() 函数,因为 Unix 信号是不排队的,当同时出现多个子进程的 SIGCHLD 信号时,wait() 可能不能全部处理所有信号

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #include "unp.h"

    void sig_child(int signo) {
    pid_t pid;
    int stat;

    while ((pid = waitpid(-1, &stat, WHOHANG)) > 0) {
    printf("child %d terminated\n", pid);
    }
    }
  4. accept() 中必须处理中断

    • SIGCHLD 信号递交时,父进程阻塞于 accept() 系统调用,内核会使 accept() 返回一个 EINTR 错误( 被中断的系统调用 ),所以必须在程序 accept() 中处理该错误,重新启动 accept() 系统调用
    • 对于 accept() 以及诸如read()write()select()open() 之类的函数来说,重启被中断的系统调用时合适的,但是 connect() 除外,重启 connect() 将返回错误,因为原来的套接字已经无效
  • 总结

网络编程时可能会遇到的三种情况:

  1. 当 fork 子进程时,必须捕获 SIGCHLD 信号
  2. 当捕获信号时,必须处理被中断的系统调用
  3. SIGCHLD 的信号处理函数必须正确编写,应使用 waitpid 函数以免留下僵尸进程

服务器进程终止

  1. 服务器进程终止,发送FIN 给客户端

  2. 客户端阻塞在 fget() 上不能立即响应该 FIN

    这就是引入 select()poll() 的原因之一,客户端不能单纯阻塞在某个特定套接字描述符上,而应该阻塞在任意输入套接字描述

  3. 等待用户输入文本后, str_cli() 函数调用 writen() 把数据发送给服务器

  4. 服务器接收到数据,响应 RST

  5. 客户端此时阻塞在 readline() 上,看不到这个 RST,并且由于第 1 步中的 FINreadline() 立即返回 0 ,所以 str_cli() 第 9 行,打印 “str_cli:server terminated prematurely”

  6. 客户端终止,关闭所有打开的描述符

SIGPIPE 信号

  1. 产生:
    如上第 5 步,客户端内核收到 RST ,而客户端进程并未及时处理,假如此时进程继续向对端服务器发送数据时(调用 write() ), 函数客户端内核将向该进程发送 SIGPIPE 信号
  2. 处理
    SIGPIPE 信号默认行为是终止进程,因此进程必须捕获它以免不情愿的被终止
  3. 无论进程有没有捕获SIGPIPE信号,write()返回 EPIPE 错误
    1. 写一个接收了 FIN 的套接字正确( CLOSE_WAIT 状态);
    2. 写一个接收了 RST 的套接字 EPIPE 错误

服务器主机崩溃

  1. 过程
    1. 服务器崩溃时,客户端不会收到任何通知
    2. 客户端调用 wtrten() 时,客户端 TCP 持续重传数据,试图从服务器接收 ACK
    3. 重传过程结束还是没收到服务器 ACK ,此时客户端阻塞在readline()上,返回错误 ETIMEDOUT
  2. 处理
    1. readline() 调用设置超时,提前得知服务器崩溃信息,不必等待重传机制完成
    2. 设置 SO_KEEPALIVE 心跳保活选项

服务器主机崩溃后重启

  1. 服务器崩溃重启后丢失之前的所有 TCP 连接,因此服务器收到客户端的消息直接返回 RST
  2. 客户端收到 RST 返回 ECONNRESET 错误

服务器主机关机

  1. Unix 系统关机时,init进程给所有进程发送 SIGTERM 信号
  2. 内核等待(5-20 秒),留给程序小段时间来清除和终止,然后给所有仍在运行的进程发送 SIGKILL 杀死进程
  3. 若进程不捕获 SIGTERM ,服务器进程由 SIGKILL 终止,随后发生的步骤如上: <服务器进程终止>
  • 总结

    必须在客户端程序中使用 select 或 poll 函数,使服务器进程终止一经发生,立刻检测到


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!