对端异常状况
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前释放缓冲区