【网络】(九)close与shutdown

1、两者的区别

  • close函数会终止数据传送的两个方向,包括套接口的读取和写入;
  • shutdown函数可以有选择的终止某个方向的数据传送,或者终止两个方向;
  • shutdown函数的how参数如果为SHUT_RDWR,表示关闭了套接口的写端,可以保证此时对方会收到一个FIN段,并导致对方read函数读取到EOF并返回0,shutdown函数关闭写端时总会这么做,
    它不理会套接字引用计数;而close函数不会保证,调用close函数时,直到套接字引用计数为0时才会发送FIN段!

2、改进代码

假设:客户端发送数据给服务端,服务端收到数据后,等待一段事件再将数据会送给客户端!

如果客户端将数据全部发送出去后,客户端调用了close函数关闭了套接字,那么服务器在回送数据时,客户端已经无法再接收数据,第一次回送后,服务端会收到RST段,第二次回送后,服务端会引发SIGPIPE信号,从而导致服务端进程终止。

解决方法就是不调用close函数,改用shutdown函数,当客户端将数据全部发送完毕后,只关闭发送端口,接收端口还可正常接收数据,关闭发送端口会导致客户端向服务端发送一个FIN段,服务端收到该段后,可再调用close关闭套接字,此时数据已经回送完毕,等到客户端接收到服务端发送的FIN段,数据也已经接收完成!这时再完全关闭套接字即可。

服务端代码

server.c

编译命令:

gcc -Wall -g -std=gnu99 server.c -o server
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <string.h>
#include <sys/select.h>
#include <signal.h>


#define handle_error(msg)   \
                    do{perror(msg);exit(EXIT_FAILURE);}while(0)


ssize_t readn(int fd, void *buf, size_t count)
{
  if((fd < 0) || (buf == NULL) || (count < 0))
        return -1;
  size_t nleft = count;   //剩余字节数
  ssize_t nread = 0;      //已读字节数
  char *pbuf = (char*)buf;
  while(nleft > 0)
  {
    if((nread = read(fd, pbuf, nleft)) < 0)
    {
        if(errno == EINTR)
            continue;
        return -1;
    }
    else if (nread == 0)
        return count - nleft;
    pbuf += nread;
    nleft -= nread;
  }
  return count;
}

ssize_t writen(int fd, const void *buf, size_t count)
{
    if((fd < 0) || (buf == NULL) || (count < 0))
          return -1;
    size_t nleft = count;   //剩余字节数
    ssize_t nwritten = 0;      //已发送字节数
    char *pbuf = (char*)buf;
    while (nleft > 0)
    {
        if((nwritten = write(fd, pbuf, nleft)) < 0)
        {
            if(errno == EINTR)
                continue;
            return -1;
        }
        else if(nwritten == 0)
            continue;
        pbuf += nwritten;
        nleft -= nwritten;
    }
    return count;
}

//使用recv函数从套接字接收缓冲区中接收数据,但并不从缓冲去中清除数据
ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
    while(true)
    {
        int iret = recv(sockfd, buf, len, MSG_PEEK);
        if(iret == -1 && errno == EINTR)    //如果失败是因为信号中端,那么就重新再试
            continue;
        return iret;
    }
}

//如果读到的数据包含\n,则返回,表示一条完整的消息读取完毕
ssize_t recvline(int sockfd, void *buf, size_t maxlen)
{
    int iret = 0;
    int nread = 0;  //已读数据
    char *pbuf = (char*)buf;
    int nleft = maxlen; //剩余字符
    while(true)
    {
        iret = recv_peek(sockfd, pbuf, nleft);
        if(iret < 0)
            return iret;    //读取失败
        else if(iret == 0)
            return iret;    //对方关闭套接口

        nread = iret;
        if(nread > nleft)       //已经读取到的数据只可能小于或者等于剩余的数据
            exit(EXIT_FAILURE);

        for(int i = 0; i < nread; i++)
        {
            if(pbuf[i] == '\n')
            {
                iret = readn(sockfd, pbuf, i+1);    //从缓冲区中读走包括\n在内的数据
                if(iret != i+1)
                    exit(EXIT_FAILURE);     //没有读取都i+1个数据,说明失败
                return iret;                //读取都\n返回
            }
        }

        //在当前读到的数据中没有发现\n,那么先将这部分数据从缓冲区中读走,然后接着偷窥后面的数据
        nleft -= nread;
        iret = readn(sockfd, pbuf, nread);
        if(iret != nread)
            exit(EXIT_FAILURE);
        pbuf += nread;
    }
    return -1;
}


//为了防止SIGPIPE信号产生而终止了进程,所以捕获此信号
void handle_sigpipe(void)
{
    struct sigaction act;
    act.sa_handler = SIG_IGN;       //忽略SIGPIPE信号
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    if(sigaction(SIGPIPE, &act, NULL) == -1)
        handle_error("sigaction");
}


int main(void)
{
    handle_sigpipe();       //捕获SIGPIPE信号

    int sk_fd = socket(AF_INET, SOCK_STREAM , IPPROTO_TCP);
    if(sk_fd < 0)
        handle_error("socket");


    //使用REUSEADDR,不必等待TIME_WAIT 状态消失,就可以重新使用端口
    int on = 1;
    if(setsockopt(sk_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
    {
        close(sk_fd);
        handle_error("setsockopt");
    }


    struct sockaddr_in sr_addr;
    memset(&sr_addr,0,sizeof(sr_addr));
    sr_addr.sin_family = AF_INET;
    sr_addr.sin_port = htons(5188);
    sr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    //sr_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    //inet_aton("127.0.0.1",&sr_addr.sin_addr);

    if(bind(sk_fd, (struct sockaddr*)&sr_addr, sizeof(sr_addr)) < 0)
    {
        close(sk_fd);
        handle_error("bind");
    }

    //被动套接字
    if(listen(sk_fd, SOMAXCONN) < 0)     //内核为此套接字排队的最大连接数由SOMAXCONN宏指定
    {
        close(sk_fd);
        handle_error("listen");
    }

    //使用select函数实现单进程并发服务器
    fd_set readset;
    fd_set allset;
    FD_ZERO(&readset);
    FD_ZERO(&allset);
    FD_SET(sk_fd, &allset);     //初始状态下,只关心监听套接字

    int maxfd = sk_fd;      //假设最大文件描述符
    int maxfd_last = maxfd; //保存一个maxfd记录,上一次的最大值
    int nready = 0;
    int maxindex = 0;       //client_sk数组最大已用位置
    int maxindex_last = 0;  //maxindex上一次的值
    int client_sk[FD_SETSIZE];      //存储客户端连接信息
    for(int i = 0; i < FD_SETSIZE; i++)
    {
        client_sk[i] = -1;
    }

    while (true)
    {
        readset = allset;
        nready = select(maxfd + 1, &readset, NULL, NULL, NULL);
        if(nready == -1)
        {
            if(errno == EINTR)      //被信号中断
                continue;
            handle_error("select");
        }
        if(nready == 0)     //time out
            continue;
        if(FD_ISSET(sk_fd, &readset))   //检测到监听套接字事件
        {
            //调用accept建立连接
            struct sockaddr_in cl_addr;
            socklen_t cl_length = sizeof(cl_addr);
            memset(&cl_addr,0,sizeof(cl_addr));

            int ac_sk = accept(sk_fd, (struct sockaddr *)&cl_addr, &cl_length);
            if(ac_sk < 0)
            {
                if(errno == EINTR)
                    continue;
                handle_error("accept");
            }
            //加入到监听集合中
            FD_SET(ac_sk, &allset);
            if(ac_sk > maxfd)
            {
                maxfd_last = maxfd; //保存历史记录
                maxfd = ac_sk;      //更新最大文件描述符
            }
            int i;
            for(i = 0; i < FD_SETSIZE; i++)
            {
                if(client_sk[i] < 0)
                {
                    client_sk[i] = ac_sk;
                    if(maxindex < i)
                    {
                        maxindex_last = maxindex;   //保存历史记录
                        maxindex = i;       //更新最大已用位置
                    }
                    break;
                }
            }
            if(i == FD_SETSIZE)     //client_sk数组空间已满,客户端太多
            {
                fprintf(stderr, "Client too many!");
                exit(EXIT_FAILURE);
            }
            printf("Connect ip = %s\tport = %d\n",inet_ntoa(cl_addr.sin_addr),ntohs(cl_addr.sin_port));

            if(--nready <= 0)   //如果除了监听套接字外没有其他套接字发生事件
                continue;
        }
        //客户端连接套接字发生事件
        for(int i = 0; i <= maxindex; i++)
        {
            int conn_sk = client_sk[i];
            if(FD_ISSET(conn_sk, &readset))
            {
                //有数据可以接收
                char recvbuf[1024];
                memset(recvbuf,0,sizeof(recvbuf));
                int iret = recvline(conn_sk,recvbuf,sizeof(recvbuf));    //获取包数据长度
                if(iret == -1)
                    handle_error("read");
                else if(iret == 0)
                {
                    //客户端关闭,删除对应的连接套接字
                    if(conn_sk == maxfd)
                    {
                        maxfd = maxfd_last;     //如果删除的时最大文件描述符
                    }
                    if(i == maxindex)
                    {
                        maxindex = maxindex_last;   //如果删除的描述符的位置是当前最大已用位置
                    }
                    client_sk[i] = -1;
                    FD_CLR(conn_sk, &allset);
                    printf("Client was closed!\n");
                    close(conn_sk);
                }
                fputs(recvbuf,stdout);

                //收到数据后,延迟4秒再发送出去
                sleep(4);

                writen(conn_sk, recvbuf, strlen(recvbuf));    //回传数据

                if(--nready <= 0)       //判断是否处理完
                    break;
            }
        }

    }
    for(int i = 0; i <= maxindex; i++)
    {
        close(client_sk[i]);
    }
    close(sk_fd);
    return 0;
}

客户端代码
client.c

编译命令

gcc -Wall -g -std=gnu99 client.c -o client
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <string.h>
#include <sys/select.h>



#define handle_error(msg)   \
                    do{perror(msg);exit(EXIT_FAILURE);}while(0)

ssize_t readn(int fd, void *buf, size_t count)
{
  if((fd < 0) || (buf == NULL) || (count < 0))
        return -1;
  size_t nleft = count;   //剩余字节数
  ssize_t nread = 0;      //已读字节数
  char *pbuf = (char*)buf;
  while(nleft > 0)
  {
    if((nread = read(fd, pbuf, nleft)) < 0)
    {
        if(errno == EINTR)
            continue;
        return -1;
    }
    else if (nread == 0)
        return count - nleft;
    pbuf += nread;
    nleft -= nread;
  }
  return count;
}

ssize_t writen(int fd, const void *buf, size_t count)
{
    if((fd < 0) || (buf == NULL) || (count < 0))
          return -1;
    size_t nleft = count;   //剩余字节数
    ssize_t nwritten = 0;      //已发送字节数
    char *pbuf = (char*)buf;
    while (nleft > 0)
    {
        if((nwritten = write(fd, pbuf, nleft)) < 0)
        {
            if(errno == EINTR)
                continue;
            return -1;
        }
        else if(nwritten == 0)
            continue;
        pbuf += nwritten;
        nleft -= nwritten;
    }
    return count;
}

//使用recv函数从套接字接收缓冲区中接收数据,但并不从缓冲去中清除数据
ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
    while(true)
    {
        int iret = recv(sockfd, buf, len, MSG_PEEK);
        if(iret == -1 && errno == EINTR)    //如果失败是因为信号中端,那么就重新再试
            continue;
        return iret;
    }
}



ssize_t recvline(int sockfd, void *buf, size_t maxlen)
{
    int iret = 0;
    int nread = 0;  //已读数据
    char *pbuf = (char*)buf;
    int nleft = maxlen; //剩余字符
    while(true)
    {
        iret = recv_peek(sockfd, pbuf, nleft);
        if(iret < 0)
            return iret;    //读取失败
        else if(iret == 0)
            return iret;    //对方关闭套接口

        nread = iret;
        if(nread > nleft)       //已经读取到的数据只可能小于或者等于剩余的数据
            exit(EXIT_FAILURE);

        for(int i = 0; i < nread; i++)
        {
            if(pbuf[i] == '\n')
            {
                iret = readn(sockfd, pbuf, i+1);    //从缓冲区中读走包括\n在内的数据
                if(iret != i+1)
                    exit(EXIT_FAILURE);     //没有读取都i+1个数据,说明失败
                return iret;                //读取都\n返回
            }
        }

        //在当前读到的数据中没有发现\n,那么先将这部分数据从缓冲区中读走,然后接着偷窥后面的数据
        nleft -= nread;
        iret = readn(sockfd, pbuf, nread);
        if(iret != nread)
            exit(EXIT_FAILURE);
        pbuf += nread;
    }
    return -1;
}

//select可看做一个管理者,可用它来管理多个IO,
//一旦其中的一个IO或这多个IO发生了我们感兴趣的事件,select函数就返回,
//返回值为检测到的个数,并且会告诉我们哪些IO发生了事件

//参数:
    //1、读写异常集合中的文件描述父的最大值+1
    //读集合,输入输出参数
    //写集合,输入输出参数
    //异常集合,输入输出参数
    //超时结构体,设置此结构体后,可设定让select函数返回的超时时间
void client_handler(int sk_fd)
{
    fd_set readset;
    FD_ZERO(&readset);
    int nready = 0;
    int fd_stdin = fileno(stdin);   //获得标准输入的描述符,防止输入重定向
    int fd_max = fd_stdin > sk_fd ? fd_stdin : sk_fd;
    bool stdinflag = true;      //表示是否需要select监听stdin
    while(true)
    {
        if(stdinflag)
            FD_SET(fd_stdin, &readset);
        FD_SET(sk_fd, &readset);        //readset集合可能会改变,所以每次都要添加
        nready = select(fd_max + 1, &readset, NULL, NULL, NULL);
        if(nready == -1)
            handle_error("select");
        if(nready == 0)
            continue;
        if(FD_ISSET(sk_fd, &readset))   //套接口产生了读事件
        {
            char recvbuf[1024] = {0};
            int iret = recvline(sk_fd, recvbuf, sizeof(recvbuf)); //接收包数据长度
            if(iret == -1)
                handle_error("read");
            else if(iret == 0)
            {
                printf("Server was closed!\n");
                break;
            }
            fputs(recvbuf, stdout);
            memset(&recvbuf, 0, sizeof(recvbuf));
        }
        if(FD_ISSET(fd_stdin, &readset))    //标准输入产生读事件
        {
            char sendbuf[1024] = {0};
            if(fgets(sendbuf, sizeof(sendbuf), stdin) == NULL)
            {
                //如果从标准输入接收到EOF,表示我们想关闭发送端口
                stdinflag = false;      //让select不再对stdin进行监听
                printf("Ctrl + D\n");
                shutdown(sk_fd, SHUT_WR);   //只关闭写端,对方会收到FIN段
            }
            else
            {
                writen(sk_fd, sendbuf, strlen(sendbuf));   //发送数据
                memset(&sendbuf, 0, sizeof(sendbuf));
            }
        }
    }
}



int main(void)
{
    int sk_fd;
    sk_fd = socket(AF_INET, SOCK_STREAM , IPPROTO_TCP);
    if(sk_fd < 0)
        handle_error("socket");
    struct sockaddr_in sr_addr;
    memset(&sr_addr,0,sizeof(sr_addr));
    sr_addr.sin_family = AF_INET;
    sr_addr.sin_port = htons(5188);
    //sr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    sr_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    //inet_aton("127.0.0.1",&sr_addr.sin_addr);

    if(connect(sk_fd, (struct sockaddr*)&sr_addr, sizeof(sr_addr)) < 0)
    {
        close(sk_fd);
        handle_error("connect");
    }

    client_handler(sk_fd);
    close(sk_fd);
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
第一章概论 ..............1 1.1 网络的历史...1 1.2 OSI 模型.......3 1.3 Internet 体系模型.............4 1.4 客户/服务器模型..............5 1.4 UNIX 的历史 ..................7 1.4.1 Unix 诞生前的故事...7 1.4.2 UNIX 的诞生.........8 1.4.3 1979 – UNIX 第七版........... 10 1.4.4 UNIX 仅仅是历史吗?........... 11 1.5 Linux 的发展................. 11 1.5.1 Linux 的发展历史.... 12 1.5.2 什么叫GNU? ..... 12 1.5.3 Linux 的特色....... 13 1.5.4 硬件需求.............. 14 1.5.5 Linux 可用的软件... 14 1.5.6 为什么选择 Linux ? .......... 15 1.6 Linux 和 Unix 的发展... 15 第二章 UNIX/Linux 模型..............17 2.1 UNIX/Linux 基本结构......17 2.2 输入和输出.....................19 2.2.1 UNIX/Linux 文件系统简介....19 2.2.2 流和标准I/O 库.....20 2.3 进程............21 第三章进程控制 ......22 3.1 进程的建立与运行..........22 3.1.1 进程的概念...........22 3.1.2 进程的建立...........22 3.1.3 进程的运行...........24 3.1.4 数据和文件描述符的继承.....29 3.2 进程的控制操作..............31 3.2.1 进程的终止...........31 3.2.2 进程的同步...........32 3.2.3 进程终止的特殊情况............33 3.2.4 进程控制的实例....33 3.3 进程的属性.....................38 3.3.1 进程标识符...........38 3.3.2 进程的组标识符....39 3.3.3 进程环境...............40 3.3.4 进程的当前目录....42 3.3.5 进程的有效标识符....43 3.3.6 进程的资源...........44 3.3.7 进程的优先级........45 3.4 守护进程.....46 3.4.1 简介...46 3.4.2 守护进程的启动...........46 3.4.3 守护进程的错误输出............46 3.4.4 守护进程的建立....48 3.5 本章小结.....49 第四章进程间通信...50 4.1 进程间通信的一些基本概念...........50 4.2 信号............50 4.2.1 信号的处理...........52 4.2.2 信号与系统调用的关系.........54 4.2.3 信号的复位...........55 4.2.4 在进程间发送信号....56 4.2.5 系统调用alarm()和pause()....58 4.2.6 系统调用setjmp()和longjmp()...........62 4.3 管道............63 4.3.1 用C 来建立、使用管道........65 4.3.2 需要注意的问题....72 4.4 有名管道.....72 4.4.1 有名管道的创建....72 4.4.2 有名管道的I/O 使用.............73 4.4.3 未提到的关于有名管道的一些注意...75 4.5 文件和记录锁定..............75 4.5.1 实例程序及其说明....75 4.5.2 锁定中的几个概念....78 4.5.3 System V 的咨询锁定............78 4.5.4 BSD 的咨询式锁定...79 4.5.5 前面两种锁定方式的比较.....81 4.5.6 Linux 的其它上锁技术..........81 4.6 System V IPC ..................84 4.6.1 ipcs 命令...............85 4.6.2 ipcrm 命令.............86 4.7 消息队列(Message Queues).........86 4.7.1 有关的数据结构....86 4.7.2 有关的函数.....

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值