zoukankan      html  css  js  c++  java
  • 第五章 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等整数类型大小在不同系统间可能不同);
    • 不同的实现给结构打包的方式存在差异,取决于各种数据类型所用的位数及机器对齐限制。

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

    • 把所用数据用文本串传递;
    • 显式定义所支持数据类型的二进制格式(位数、大端、小端),并以这样的格式在客户机与服务器之间传递。
  • 相关阅读:
    oracle热备份与冷备份的对比
    oracle数据库备份
    shell 去除空行
    已有实例 Oracle 下新增实例(2)通过dbca克隆实例
    oracle启动,提示“LRM-00109: could not open parameter file”
    linux——使用fidsk对linux硬盘进行操作【转】
    前端——知识点
    hdu 3996 Gold Mine 最大权闭合子图
    hdu 3917 Road constructions 最大权闭合子图
    poj 2987 Firing 最大权闭合子图
  • 原文地址:https://www.cnblogs.com/4tian/p/2623480.html
Copyright © 2011-2022 走看看