第五章 TCP客户服务器程序示例

1. TCP回射示例

服务器代码

View Code
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#define SRV_PORT 8888
#define MAXLINE 4096

void str_echo(int fd);

int main(int argc, char **argv)
{
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if(listenfd < 0)
    {
        perror("create socket error.");
    }

    struct sockaddr_in srvaddr;
    bzero(&srvaddr, sizeof(srvaddr));
    srvaddr.sin_family = AF_INET;
    srvaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    srvaddr.sin_port = htons(SRV_PORT);
    
    if(bind(listenfd, (struct sockaddr*)&srvaddr, sizeof(srvaddr)) < 0)
    {
        perror("bind error.");
    }
    
    if(listen(listenfd, 1023) < 0)
    {
        perror("listen error.");
    }

    struct sockaddr_in cliaddr;

    for(; ;)
    {
        socklen_t clilen = sizeof(cliaddr);
        int connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &clilen);
        if(connfd < 0)
        {
            perror("accept error.");
        }
        pid_t childpid;
        if( (childpid = fork()) == 0 )
        {
            close(listenfd);
            str_echo(connfd);
            exit(0);
        }
        close(connfd);
    }

    return 0;
}

void str_echo(int sockfd)
{
    char line[MAXLINE];

    while(read(sockfd, line, MAXLINE) != 0)
    {
        if(write(sockfd, line, strlen(line)) != strlen(line))
        {
            perror("write error");
        }
    }
}

客户端代码

View Code
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#define SRV_PORT 8888
#define MAXLINE 4096

void str_cli(FILE *fp, int sockfd);

int main(int argc, char **argv)
{
    if(argc != 2)
    {
        printf("usage:tcpcli <ip address>\n");
        exit(0);
    }

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd < 0)
    {
        perror("create socket error.");
    }

    struct sockaddr_in srvaddr;
    bzero(&srvaddr, sizeof(srvaddr));
    srvaddr.sin_family = AF_INET;
    srvaddr.sin_port = htons(SRV_PORT);
    
    if(inet_pton(AF_INET, argv[1], &srvaddr.sin_addr) <= 0)
    {
        printf("address error %s\n", argv[1]);
        exit(0);
    }

    if(connect(sockfd, (struct sockaddr *)&srvaddr, sizeof(srvaddr)) < 0 )
    {
        perror("connect error");
    }

    str_cli(stdin, sockfd);
    exit(0);
}

void str_cli(FILE *fp, int sockfd)
{
    char sendline[MAXLINE];
    char readline[MAXLINE];
    
    while(fgets(sendline, MAXLINE, fp))
    {
        if( write(sockfd, sendline, strlen(sendline)) != strlen(sendline) )
        {
            perror("send data error");
        }
        if( read(sockfd, readline, MAXLINE) == 0)
        {
            perror("recv data error");
        }
        fputs(readline,stdout);
    }
}

2. 示例启动过程

客户端服务器建立连接后发生的动作:

  • 客户端调用str_cli函数,阻塞于fgets,等待输入;
  • 服务器中的accept返回时,服务器调用fork,再由子进程调用str_echo,子进程阻塞于read等待客户端发送的数据;
  • 另一方面服务器的父进程再次调用accept等待下一个客户连接。

所以至此有三个阻塞进程:客户进程,服务器父进程,服务器子进程。

3. 示例正常终止过程

  • 客户端键入EOF,str_cli函数返回main函数, main函数调用exit终止进程;
  • 由于客户端程序没有关闭其描述符,所以其描述符由内核关闭,此时开始发送FIN与服务器进行TCP四次握手关闭连接;
  • 服务器子进程收到FIN,子进程从str_echo返回子进程的main函数,通过调用exit终止子进程,子进程所有描述符关闭;
  • 最终服务器发FIN给客户端,客户端返回ACK,进入TIME_WAIT状态。连接完全终止。
  • 服务器子进程终止时会给父进程发送一个SIGCHLD信号,我们没有捕捉此信号,信号默认被忽略,这导致子进程进入僵死状态,僵死进程占用系统资源,所以我们还需要处理僵死的进程。

4. POSIX信号处理(处理3产生的僵死进程)

    信号由一个进程发给另一个进程(或自身),也可由内核发给某个进程。

1)调用函数sigaction设定一个信号的处理有以下三种选择:

  • 提供一个信号处理函数(signal handler),捕获到特定信号(SIGKILL与SIGSTOP不能被捕获)发生它就被调用,其原型是 void handler(int signo);
  • 可以设置信号为SIG_IGN来忽略它(SIGKILL与SIGSTOP不能被忽略);
  • 可以设置信号为SIG_DFL来启用信号的默认处理。

2)signal函数(使用系统提供的)

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

3)处理SIGCHLD信号(处理僵死进程)

  • 设置僵死进程的目的是为了维护子进程的信息,以便父进程在某个时候获取;
  • 为了防止僵死进程产生无论何时调用fork创建子进程,父进程都得wait它们,为此我们可以建立一个捕获SIGCHLD的信号处理函数

4)例子

  • 在创建子进程之前调用如下函数:

    signal(SIGCHLD, sig_child);

  • 创建信号处理函数sig_child    
void sig_chld(int signo)
{
    int  stat;
    pid_t pid = wait(&stat);
    printf("child %d terminated\n", pid);
    return;
}

5)处理被中断的系统调用

  • 慢系统调用:用于描述永远阻塞的系统调用如accept
  • 适用慢系统调用的规则:当阻塞于某个慢系统调用的一个进程捕获某个信号,且相应信号处理函数返回时该系统调用可能返回一个EINTR错误。编写捕获信号的程序时,应该对慢系统调用返回EINTR有所准备。
  • 如:处理被中断的accept,以下是用于处理被中断的accept调用的代码
for( ; ; )
{
    clilen = sizeof(cliaddr);
    if((connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &clilen)) < 0)
    {
        if(errno == EINTR)
        {
            continue;
        }
        else
        {
            perror("accept error");
        }
    }
}

5. wait和waitpid

#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
status为进程的终止状态:正常终止,信号杀死或由作业控制停止。
waitpid的pid参数允许我们指定想等待的进程,pid值为-1时表示等待第一个终止的进程。
  • wait不足以防止僵死进程的出现,在调用wait之前可能有几个SIGCHLD信号产生,但是wait的调用次数不确定,可能只调用一次。
  • 防止僵死进程正确的解决方法是用waitpid,waitpid必须指定WNOHANG选项,以告知waitpid在有尚未终止的子进程在运行时不要阻塞。可以改造上面sig_chld函数如下:
void sig_chld(int signo)
{
    int  stat;
    pid_t pid;
    while( (pid = waitpid(-1, &stat, WNOHANG)) > 0 )
    {
        printf("child %d terminated\n", pid);
    }
    return;
}

小结:网络编程中可能会遇到的三种情况

  • fork子进程时,必须捕获SIGCHLD信号;
  • 捕获信号时,必须处理被中断的系统调用(EINTR);
  • SIGCHLD的信号处理函数必须编写正确,用waitpid而不是wait,以免留下僵死进程。

6. accept返回前连接终止:服务器在调用accept之前收到RST(此种情况在16章再详细给出)

7.服务器进程终止

       示例程序中,如果服务器终止,当服务器发出的FIN到达客户端时,客户端正阻塞在fgets上等待用户输入。客户端此时要应对两个描述符(套接字和用户输入),它不能单纯的阻塞在其中任何一个源的输入上,这个涉及到select和poll,第六章继续讨论。

8.SIGPIPE信号

  • 场景:例子中服务器终止后,客户端在读数据之前,向服务器执行两次的写操作。
  • 客户端第一次写操作引起服务器的RST回复,而当进程向收到RST的套接字就行写操作时,内核会向该进程发送一个SIGPIPE信号,该信号的默认行为是终止进程。因此进程应该捕获SIGPIPE信号以免它被动的被终止。不论进程忽略SIGPIPE信号还是捕获处理了,写操作都将返回EPIPE错误。
  • 处理SIGPIPE的建议方法:取决于其发生时进程想要做什么,如果没有什么特殊的事情做,一般直接将此信号设置为SIG_IGN,并假设后续的输出操作将捕获EPIPE错误。注意,如果有多个套接字,该信号无法区分哪个套接字出错了,要知道哪个write出差,要么忽略该信号,要么信号处理完后再处理来自write的EPIPE信号。

9.服务器主机崩溃

     主机崩溃可能导致客户端长时间(9分钟左右)对服务器进行重连,这使得我们有时候需要在更短的时间发现服务器已经挂掉,此时可以把read设置一个超时(后面再详说),或者用SO_KEEPALIVE套接字选项和一些心跳技术实现(第七章)。

 10. 服务器主机崩溃后重启

    服务器崩溃后,不向客户端发送任何信息,客户端继续向服务器发送数据。服务器重启后由于失去了崩溃前所有连接信息,所有服务器TCP对所有来自客户的数据分节响应RST,客户收到RST后,阻塞的read调用返回ECONNRESET错误。

11. 服务器主机关机:会先发SIGTERM信号,再发SIGKILL信号。进程终止后与7描述的情况一样。

12. 关于数据格式

  • 在客户和服务器之间传递文本:文本传输不论客户机与服务器的字节序如何,都可以对数据进行正常的传递,不发生错误。
  • 在客户和服务器之间传递二进制结构:传递二进制结构如果客户机和服务器字节序不同将发生错误。

注意三个潜在的问题:

  • 不同的实现以不同格式存储二进制数(如字节序大端小端);
  • 不同实现在存储相同的C数据类型可能存在差异(对于short,int或long等整数类型大小在不同系统间可能不同);
  • 不同的实现给结构打包的方式存在差异,取决于各种数据类型所用的位数及机器对齐限制。

解决以上问题的常用方法:

  • 把所用数据用文本串传递;
  • 显式定义所支持数据类型的二进制格式(位数、大端、小端),并以这样的格式在客户机与服务器之间传递。
原文地址:https://www.cnblogs.com/4tian/p/2623480.html