网络编程实战18 增强程序的健壮性

对端异常状况

17讲中接触了一些情况,比如通过read等调用,通过对EOF的判断,防范对方程序崩溃。

int nBytes = recv(connfd, buffer, sizeof(buffer), 0);
if (nBytes == -1) {
    error(1, errno, "error read message");
} else if (nBytes == 0) {
    error(1, 0, "client closed \n");
}

但是如果服务端完全崩溃或者网络中断,如果是阻塞套接字,会一直阻塞在read等调用上,没有办法感知套接字的异常。可以用以下方法来解决。
方法1:给read设置超时

struct timeval tv;
tv.tv_sec = 5;
tv.tv_usec = 0;
//设置套接字的读操作超时
setsockopt(connfd, SOL_SOCKET, SO_RCVTIMEO, (const char *) &tv, sizeof tv);

while (1) {
    int nBytes = recv(connfd, buffer, sizeof(buffer), 0);
    if (nBytes == -1) {
    	//判断超时
        if (errno == EAGAIN || errno == EWOULDBLOCK) {
            printf("read timeout\n");
            onClientTimeout(connfd);
        } else {
            error(1, errno, "error read message");
        }
    } else if (nBytes == 0) {
        error(1, 0, "client closed \n");
    }
    ...
}

方法2:利用12讲中的心跳包判断连接是否正常

方法3:利用多路复用技术自带的超时能力
select函数自带超时的参数

struct timeval tv;
tv.tv_sec = 5;
tv.tv_usec = 0;

FD_ZERO(&allreads);
FD_SET(socket_fd, &allreads);
for (;;) {
    readmask = allreads;
    int rc = select(socket_fd + 1, &readmask, NULL, NULL, &tv);
    if (rc < 0) {
      error(1, errno, "select failed");
    }
    if (rc == 0) {
      printf("read timeout\n");
      //调用函数进行超时处理
      onClientTimeout(socket_fd);
    }
 ...   
}

缓冲区处理

第一个例子

char Response[] = "COMMAND OK";
char buffer[128];

while (1) {
    int nBytes = recv(connfd, buffer, sizeof(buffer), 0);
    if (nBytes == -1) {
        error(1, errno, "error read message");
    } else if (nBytes == 0) {
        error(1, 0, "client closed \n");
    }

    buffer[nBytes] = '\0';
    if (strcmp(buffer, "quit") == 0) {
        printf("client quit\n");
        send(socket, Response, sizeof(Response), 0);
    }
    printf("received %d bytes: %s\n", nBytes, buffer);
}

问题:通过recv读取的字符数为128时,就会buffer[128] = '\0'造成缓冲区溢出
改正:留下buffer里的一个字节,容纳后面的‘\0’

int nBytes = recv(connfd, buffer, sizeof(buffer)-1, 0);

send函数:发送的字符串调用的是sizeof,意味着’\0’是被发送出去的,接收时假设没有’\0’的存在,所以使用strlen,发送中忽略’\0’

send(socket, Response, strlen(Response), 0);

第二个例子

size_t read_message(int fd, char *buffer, size_t length) {
    u_int32_t msg_length;
    u_int32_t msg_type;
    int rc;

    rc = readn(fd, (char *) &msg_length, sizeof(u_int32_t));
    if (rc != sizeof(u_int32_t))
        return rc < 0 ? -1 : 0;
    msg_length = ntohl(msg_length);

    rc = readn(fd, (char *) &msg_type, sizeof(msg_type));
    if (rc != sizeof(u_int32_t))
        return rc < 0 ? -1 : 0;

    if (msg_length > length) {
        return -1;
    }

    /* Retrieve the record itself */
    rc = readn(fd, buffer, msg_length);
    if (rc != msg_length)
        return rc < 0 ? -1 : 0;
    return rc;
}

如果没有if (msg_length > length)这个判断,对方发来的消息体可以构建出一个非常大的msg_length,而实际发送的报文主体长度却没有这么大,这样后面的读取操作就不会成功。

第三个例子
需要开发一个函数,假设报文分界符是换行符\n,一个做法是每次读取一个字符,判断这个字符是不是换行符

版本一:

size_t readline(int fd, char *buffer, size_t length) {
    char *buf_first = buffer;

    char c;
    while (length > 0 && recv(fd, &c, 1, 0) == 1) {
        *buffer++ = c;
        length--;
        if (c == '\n') {
            *buffer = '\0';
            return buffer - buf_first;
        }
    }

    return -1;
}

问题:工作效率太低,每次recv都是一次系统调用,需要从用户空间切换到内核空间,开销较大。用版本二来解决。

版本二: 一次性最多读取512字节到临时缓冲区,之后将临时缓冲区的字符一个个拷贝到应用程序缓冲区,这样做效率很高

size_t readline(int fd, char *buffer, size_t length) {
    char *buf_first = buffer;
    static char *buffer_pointer;
    int nleft = 0;
    static char read_buffer[512];
    char c;
    //需要处理的字符数
    while (length-- > 0) {
    	//临时缓冲区有没有字符要处理
        if (nleft <= 0) {
        	//读nread字符到临时缓冲区
            int nread = recv(fd, read_buffer, sizeof(read_buffer), 0);
            if (nread < 0) {
                if (errno == EINTR) {
                    length++;
                    continue;
                }
                return -1;
            }
            if (nread == 0)
                return 0;
            //读取成功
            buffer_pointer = read_buffer;
            nleft = nread;
        }
        //一个个读到应用程序缓冲区
        c = *buffer_pointer++;
        *buffer++ = c;
        //处理一个就减一个
        nleft--;
        if (c == '\n') {
            *buffer = '\0';
            return buffer - buf_first;
        }
    }
    return -1;
}

问题:如果输入字符为012345678\n

//输入字符为: 012345678\n
char buf[10]
readline(fd, buf, 10)

读到最后一个\n字符时,length为1,读到换行符会增加一个字符串截止符\0,越过了应用程序缓冲区的大小。所以要将length--改为--length

总结

●最终缓冲区的大小应该比预计接收的数据大小大一些,预防缓冲区溢出。
●可以动态分配缓冲区,但是要记得在return前释放缓冲区

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值