问题:
网络编程中,基于Select对Socket事件的筛选,在连接的地址失效的情况下,可能有不如预期的行为发生。
场景:
这里有一个C++写的SDK,封装了网络通信层,基于Select IO 网络IO模型;同一份代码,移植到到NDK和IOS下,线上反映的问题是,刚建立好TCP连接,然后执行Send操作,立即报EPIPE的错误;
报这个错误的原因解释是:1.尝试在一个未建立连接的socket上send 2.尝试在一个已经断开连接或者关闭写端通道的连接的socket上send;
但是先建立NIO的socket,然后使用Select筛选读写事件来判断TCP连接是否可用,这样的逻辑判断,是连接可用的吗?为什么每次还会Send时报错呢;
奇怪的问题是,这段代码,在Android的NDK下工作是没有问题的;在IOS下与Ubuntu下工作是有问题的;
IOS的测试环境:
Darwin sunwaydeMac-mini.local 15.5.0 Darwin Kernel Version 15.5.0: Tue Apr 19 18:36:36 PDT 2016; root:xnu-3248.50.21~8/RELEASE_X86_64 x86_64
Ubuntu的测试环境:
Linux ubuntu 4.2.0-35-generic #40~14.04.1-Ubuntu SMP Fri Mar 18 16:37:35 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux
代码:
#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/shm.h>
#include <stdlib.h>
#include <time.h>
inline int SetNoblock(int hSocket)
{
int32_t flags = 0 ;
int32_t retval = 0 ;
flags = fcntl( hSocket , F_GETFL ) ;
if ( flags > 0 )
{
flags |= O_NONBLOCK ;
if ( fcntl( hSocket, F_SETFL, flags ) >0 )
{
retval = 1 ;
}
}
return retval ;
}
inline bool isSocketInPending(int nErr)
{
return (nErr == EWOULDBLOCK || nErr == EINPROGRESS);
}
void select_once(int fd)
{
struct timeval tv ;
tv.tv_sec = 0;
tv.tv_usec = 1000*1000 ;
fd_set readset ;
fd_set sendset ;
fd_set exptset;
FD_ZERO( &readset ) ;
FD_ZERO( &sendset ) ;
FD_SET( fd, &readset ) ;
FD_SET( fd, &sendset ) ;
FD_SET( fd, &exptset ) ;
int rc = select( fd+1, &readset, &sendset , &exptset, &tv ) ;
if (rc < 0)
{
printf("select error\n");
return;
}
if (rc == 0){
printf("select timeout\n");
return;
}
if( FD_ISSET( fd, &readset )) {
printf("readable\n");
}
if( FD_ISSET( fd, &sendset)){
printf("writable\n");
char buf[11]="";
int ret=::send( fd,buf,1,0);
printf("send;ret=%d;%d %s\n",ret,errno,strerror(errno));
}
if( FD_ISSET( fd, &exptset )){
printf("exceptional\n");
}
}
int main()
{
int s = socket(AF_INET,SOCK_STREAM, 0);
SetNoblock(s);
const char* ip="172.16.0.41";
int port=6012;
struct sockaddr_in svraddrV4 ;
svraddrV4.sin_family = AF_INET;
svraddrV4.sin_port = htons( port ) ;
svraddrV4.sin_addr.s_addr = inet_addr( ip) ;
socklen_t socklen = sizeof( svraddrV4 ) ;
struct sockaddr* connectAddr = (sockaddr *)&svraddrV4 ;
int nErr;
int rc = ::connect( s, connectAddr, socklen );
if (rc < 0 )
{
printf("connect;ret=%d;%d %s\n",rc,errno,strerror(errno));
nErr = errno ;
if (! isSocketInPending(nErr))
{
printf("connect error at now\n");
return -1;
}
printf("connecting...\n");
}else{
printf("connect ok at now\n");
}
while(1)
{
select_once(s);
sleep(5);
}
return 0;
}
如上代码,是我从SDK中裁剪出来,怀疑有问题的代码逻辑原型;
测试现象是:
如果连接的地址是IP,PORT,
假设IP是非法IP无可达路由,那么上面代码运行结果是,Select在超时预期里没有事件通知,直到消耗掉超时预期被判断为连接失败。这是符合预期的;
假设IP是可达的,而PORT上并没有服务在监听,那么问题来了,Select会发出事件通知,Readable,Writable,并且取SO_ERROR也为0,表示连接是成功了的;这不符合预期。这导致之后,会被认为连接是成功的,而执行Send操作,从而发生EPIPE错误。
这个代码执行过程与结论,我分别在IOS与Ubuntu下都进行了测试,测试结果不符合预期;而在Android NDK下测试,测试结果却是符合预期。
同一份代码在不同平台的执行,可能预期的结果是不同,这再一次证明了“”除非你测试了所有的分支,否则经验极有可能不可靠。“”
解决
解决了这一问题:我采用了判断连接成功之后,预先验证连接是否真正可以发送数据的行为判断;
发送一个空字节之后,检查返回值是否为-1;这样来规避这个问题。
char buf[1]="";
int ret=::send( fd,buf,0,0);
printf("send;ret=%d;%d %s\n",ret,errno,strerror(errno));