面向连接的socket数据处理过程以及非阻塞connect问题

来源:互联网 发布:mac版word删除空白页 编辑:程序博客网 时间:2024/05/17 22:58

对于面向连接的socket类型(SOCK_STREAM,SOCK_SEQPACKET)在读数据之前必须建立连接,首先服务器端socket必须在一个客户端知道的地址进行监听,也就是创建socket之后必须调用bind绑定到一个指定的地址,然后调用int listen(int sockfd, int backlog);进行监听。此时服务器socket允许客户端进行连接,backlog提示没被accept的客户连接请求队列的大小,系统决定实际的值,最大值定义为SOMAXCONN在头文件<sys/socket.h>里面。如果某种原因导致服务器端进程未及时accpet客户连接而导致此队列满了的话则新的客户端连接请求被拒绝(在工作中遇到过此情况,IONA ORBIX(CORBA中间件)由于没有配置超时时间结果在WIFI网络中传输数据出现异常情况一直阻塞而无机会调用accept接受新的客户请求,于是最终队列满导致新的客户连接被拒绝)。

  调用listen之后当有客户端连接到达的时候调用int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict len);接受客户端连接建立起连接返回用于连接数据传送的socket描述符,进行监听的socket可以用于继续监听客户端的连接请求,返回的socket描述符跟监听的socket类型一致。如果addr不为NULL,则客户端发起连接请求的socket地址信息会通过addr进行返回。如果监听的socket描述符为阻塞模式则accept一直会阻塞直到有客户发起连接请求,如果监听的socket描述符为非阻塞模式则如果当前没有可用的客户连接请求,则返回-1(errno设置为EAGAIN)。可以使用select函数对监听的socket描述符进行多路分离,如果有客户连接请求则select将监听的socket描述符设置为可读(注意,如果监听的socket为阻塞模式而使用select进行多路分离则可能造成select返回可读但是调用accept会被阻塞住的情况,原因是在调用accept之前客户端可能主动关闭连接或者发送RST异常关闭连接,因此select最好跟非阻塞socket搭配使用)

  
客户端调用int connect(int sockfd, const struct sockaddr *addr, socklen_t len);发起对服务器的socket的连接请求,如果客户端socket描述符为阻塞模式则会一直阻塞到连接建立或者连接失败(注意阻塞模式的超时时间可能为75秒到几分钟之间),而如果为非阻塞模式,则调用connect之后如果连接不能马上建立则返回-1(errno设置为EINPROGRESS,注意连接也可能马上建立成功比如连接本机的服务器进程),如果没有马上建立返回,此时TCP的三路握手动作在背后继续,而程序可以做其他的东西,然后调用select检测非阻塞connect是否完成(此时可以指定select的超时时间,这个超时时间可以设置为比connect的超时时间短),如果select超时则关闭socket,然后可以尝试创建新的socket重新连接,如果select返回非阻塞socket描述符可写则表明连接建立成功,如果select返回非阻塞socket描述符既可读又可写则表明连接出错(注意:这儿必须跟另外一种连接正常的情况区分开来,就是连接建立好了之后,服务器端发送了数据给客户端,此时select同样会返回非阻塞socket描述符既可读又可写,这时可以通过以下方法区分:
  1.调用getpeername获取对端的socket地址.如果getpeername返回ENOTCONN,表示连接建立失败,然后用SO_ERROR调用getsockopt得到套接口描述符上的待处理错误;
  2.调用read,读取长度为0字节的数据.如果read调用失败,则表示连接建立失败,而且read返回的errno指明了连接失败的原因.如果连接建立成功,read应该返回0;
  3.再调用一次connect.它应该失败,如果错误errno是EISCONN,就表示套接口已经建立,而且第一次连接是成功的;否则,连接就是失败的;
  对于无连接的socket类型(SOCK_DGRAM),客户端也可以调用connect进行连接,此连接实际上并不建立类似SOCK_STREAM的连接,而仅仅是在本地保存了对端的地址,这样后续的读写操作可以默认以连接的对端为操作对象。

  当对端机器crash或者网络连接被断开(比如路由器不工作,网线断开等),此时发送数据给对端然后取本端socket会返回ETIMEDOUT或者EHOSTUNREACH 或者ENETUNREACH(后两个是中间路由器判断服务器主机不可达的情况)。

  当对端机器crash之后重新启动,然后客户端再向原来的连接发送数据,因为服务器端已经没有原来的连接信息,此时服务器端回送RST给客户端,此时客户端本地端口返回ECONNRESET错误。

  当服务器所在的进程正常或者异常关闭时,会对所有打开的文件描述符进行close,因此对于连接的socket描述符则会向对端发送FIN分节进行正常关闭流程。对端在收到FIN之后端口变得可读,此时取端口会返回0表示到了文件结尾(对端不会再发送数据)
 

  当一端收到RST导致socket返回ECONNRESET,此时如果再次调用write发送数据给对端则触发SIGPIPE信号,信号默认终止进程,如果忽略此信号或者从SIGPIPE的信号处理程序返回则write出错返回EPIPE。


  可以看出只有当本地端口主动发送消息给对端才能检测出连接异常中断的情况,搭配select进行多路分离的时候,socket收到RST或者FIN时候,select返回可读(心跳消息就是用于检测连接的状态)。也可以使用socket的KEEPLIVE选项,依赖socket本身侦测socket连接异常中断的情况。



  发送socket数据有以下方法:

  调用ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags);,只能用于建立好了连接的socket(面向连接的SOCK_STREAM或者调用了connect的SOCK_DGRAM)。flags取值如下:

  MSG_DONTROUTE 对数据不进行路由

  MSG_DONTWAIT 不等待数据发送完成

  MSG_EOR 数据包结尾

  MSG_OOB 带外数据

  注意send函数成功返回并不代表对端一定收到了发送的消息,另外对于数据报协议如果发送的数据大于一个数据报长度则发送失败(errno设置为EMSGSIZE)。

linux 客户端 Socket 非阻塞connect编程(正文)linux 客户端 Socket 非阻塞connect编程(正文)/*开发过程与源码解析

  开发测试环境:虚拟机CentOS,windows网络调试助手
  非阻塞模式有3种用途

  1.三次握手同时做其他的处理。connect要花一个往返时间完成,从几毫秒的局域网到几百毫秒或几秒的广域网。这段时间可能有一些其他的处理要执行,比如数据准备,预处理等。
  2.用这种技术建立多个连接。这在web浏览器中很普遍.
  3.由于程序用select等待连接完成,可以设置一个select等待时间限制,从而缩短connect超时时间。多数实现中,connect的超时时间在75秒到几分钟之间。有时程序希望在等待一定时间内结束,使用非阻塞connect可以防止阻塞75秒,在多线程网络编程中,尤其必要。 例如有一个通过建立线程与其他主机进行socket通信的应用程序,如果建立的线程使用阻塞connect与远程通信,当有几百个线程并发的时候,由于网络延迟而全部阻塞,阻塞的线程不会释放系统的资源,同一时刻阻塞线程超过一定数量时候,系统就不再允许建立新的线程(每个进程由于进程空间的原因能产生的线程有限),如果使用非阻塞的connect,连接失败使用select等待很短时间,如果还没有连接后,线程立刻结束释放资源,防止大量线程阻塞而使程序崩溃。

  目前connect非阻塞编程的普遍思路是:
  在一个TCP套接口设置为非阻塞后,调用connect,connect会在系统提供的errno变量中返回一个EINRPOCESS错误,此时TCP的三路握手继续进行。之后可以用select函数检查这个连接是否建立成功
。以下实验基于unix网络编程和网络上给出的普遍示例,在经过大量测试之后,发现其中有很多方法,在linux中,并不适用。

  我先给出了重要源码的逐步分析,在最后给出完整的connect非阻塞源码。
  1.首先填写套接字结构,包括远程的ip,通信端口如下: */
  struct sockaddr_in serv_addr;
  serv_addr.sin_family=AF_INET;
  serv_addr.sin_port=htons(9999);
  serv_addr.sin_addr.s_addr = inet_addr("58.31.231.255"); //inet_addr转换为网络字节序
  bzero(&(serv_addr.sin_zero),8);

  // 2.建立socket套接字:
  if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
  {
  perror("socket creat error");
  return 1;
  }

  // 3.将socket建立为非阻塞,此时socket被设置为非阻塞模式
  flags = fcntl(sockfd,F_GETFL,0);//获取建立的sockfd的当前状态(非阻塞)
  fcntl(sockfd,F_SETFL,flags|O_NONBLOCK);//将当前sockfd设置为非阻塞
  /*4. 建立connect连接,此时socket设置为非阻塞,connect调用后,无论连接是否建立立即返回-1,同时将errno(包含errno.h就可以直接使用)设置为EINPROGRESS, 表示此时tcp三次握手仍旧进行,如果errno不是EINPROGRESS,则说明连接错误,程序结束。
  当客户端和服务器端在同一台主机上的时候,connect回马上结束,并返回0;无需等待,所以使用goto函数跳过select等待函数,直接进入连接后的处理部分。*/

  if ( ( n = connect( sockfd, ( struct sockaddr *)&serv_addr , sizeof(struct sockaddr)) ) < 0 )
  {
  if(errno != EINPROGRESS) return 1;
  }

  if(n==0)
  {
  printf("connect completed immediately");
  goto done;
  }

  /* 5.设置等待时间,使用select函数等待正在后台连接的connect函数,这里需要说明的是使用select监听socket描述符是否可读或者可写,如果只可写,说明连接成功,可以进行下面的操作。如果描述符既可读又可写,分为两种情况,第一种情况是socket连接出现错误(不要问为什么,这是系统规定的,可读可写时候有可能是connect连接成功后远程主机断开了连接close(socket)),第二种情况是connect连接成功,socket读缓冲区得到了远程主机发送的数据。需要通过connect连接后返回给errno的值来进行判定,或者通过调用 getsockopt(sockfd,SOL_SOCKET,SO_ERROR,&error,&len); 函数返回值来判断是否发生错误,这里存在一个可移植性问题,在solaris中发生错误返回-1,但在其他系统中可能返回0.我首先按unix网络编程的源码进行实现。如下:*/

  FD_ZERO(&rset);
  FD_SET(sockfd,&rset);
  wset = rset;
  tval.tv_sec = 0;
  tval.tv_usec = 300000;
  int error;
  socklen_t len;

  if(( n = select(sockfd+1, &rset, &wset, NULL,&tval)) <= 0)
  {
  printf("time out connect error");
  close(sockfd);
  return -1;
  }

  If ( FD_ISSET(sockfd,&rset) || FD_ISSET(sockfd,&west) )
  {
  len = sizeof(error);
  if( getsockopt(sockfd,SOL_SOCKET,SO_ERROR,&error,&len) <0)
  return 1;
  }

  /* 这里我测试了一下,按照unix网络编程的描述,当网络发生错误的时候,getsockopt返回-1,return -1,程序结束。网络正常时候返回0,程序继续执行。
  可是我在linux下,无论网络是否发生错误,getsockopt始终返回0,不返回-1,说明linux与unix网络编程还是有些细微的差别。就是说当socket描述符可读可写的时候,这段代码不起作用。不能检测出网络是否出现故障。
  我测试的方法是,当调用connect后,sleep(2)休眠2秒,借助这两秒时间将网络助手断开连接,这时候select返回2,说明套接口可读又可写,应该是网络连接的出错情况。
  此时,getsockopt返回0,不起作用。获取errno的值,指示为EINPROGRESS,没有返回unix网络编程中说的ENOTCONN,EINPROGRESS表示正在试图连接,不能表示网络已经连接失败。
针对这种情况,unix网络编程中提出了另外3种方法,这3种方法,也是网络上给出的常用的非阻塞connect示例:
  a.再调用connect一次。失败返回errno是EISCONN说明连接成功,表示刚才的connect成功,否则返回失败。 代码如下:*/

  int connect_ok;

  connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr) );
  switch (errno)
  {
  case EISCONN: //connect ok
  printf("connect OK \n");
  connect_ok = 1;
  break;
  case EALREADY:
  connect_0k = -1
  break;
  case EINPROGRESS: // is connecting, need to check again
  connect_ok = -1
  break;
  default: 
  printf("connect fail err=%d \n",errno);
  connect_ok = -1;
  break;
  }

  /*如程序所示,根据再次调用的errno返回值将connect_ok的值,来进行下面的处理,connect_ok为1继续执行其他操作,否则程序结束。
  但这种方法我在linux下测试了,当发生错误的时候,socket描述符(我的程序里是sockfd)变成可读且可写,但第二次调用connect 后,errno并没有返回EISCONN,,也没有返回连接失败的错误,仍旧是EINPROGRESS,而当网络不发生故障的时候,第二次使用 connect连接也返回EINPROGRESS,因此也无法通过再次connect来判断连接是否成功。
  b.unix网络编程中说使用read函数,如果失败,表示connect失败,返回的errno指明了失败原因,但这种方法在linux上行不通,linux在socket描述符为可读可写的时候,read返回0,并不会置errno为错误。
   c.unix网络编程中说使用getpeername函数,如果连接失败,调用该函数后,通过errno来判断第一次连接是否成功,但我试过了,无论网络连接是否成功,errno都没变化,都为EINPROGRESS,无法判断。
  悲哀啊,即使调用getpeername函数,getsockopt函数仍旧不行。
  综上方法,既然都不能确切知道非阻塞connect是否成功,所以我直接当描述符可读可写的情况下进行发送,通过能否获取服务器的返回值来判断是否成功。(如果服务器端的设计不发送数据,那就悲哀了。)
  程序的书写形式出于可移植性考虑,按照unix网络编程推荐写法,使用getsocketopt进行判断,但不通过返回值来判断,而通过函数的返回参数来判断。
  6. 用select查看接收描述符,如果可读,就读出数据,程序结束。在接收数据的时候注意要先对先前的rset重新赋值为描述符,因为select会对 rset清零,当调用select后,如果socket没有变为可读,则rset在select会被置零。所以如果在程序中使用了rset,最好在使用时候重新对rset赋值。

  程序如下:*/

  FD_ZERO(&rset);
  FD_SET(sockfd,&rset);//如果前面select使用了rset,最好重新赋值

  if( ( n = select(sockfd+1,&rset,NULL, NULL,&tval)) <= 0 )
  {
  close(sockfd);
  return -1;
  } 

  if ((recvbytes=recv(sockfd, buf, 1024, 0)) ==-1)
  {
  perror("recv error!");
  close(sockfd);
  return 1;

  }
  printf("receive num %d\n",recvbytes);

  printf("%s\n",buf);

  */

非阻塞connect

在一个TCP套接口被设置为非阻塞之后调用connect,connect会立即返回EINPROGRESS错误,表示连接操作正在进行中,但是仍未完成;同时TCP的三路握手操作继续进行;在这之后,我们可以调用select来检查这个链接是否建立成功;非阻塞connect有三种用途:
1.我们可以在三路握手的同时做一些其它的处理.connect操作要花一个往返时间完成,而且可以是在任何地方,从几个毫秒的局域网到几百毫秒或几秒的广域网.在这段时间内我们可能有一些其他的处理想要执行;
2.可以用这种技术同时建立多个连接.在Web浏览器中很普遍;
3.由于我们使用select来等待连接的完成,因此我们可以给select设置一个时间限制,从而缩短connect的超时时间.在大多数实现中,connect的超时时间在75秒到几分钟之间.有时候应用程序想要一个更短的超时时间,使用非阻塞connect就是一种方法;
非阻塞connect听起来虽然简单,但是仍然有一些细节问题要处理:
1.即使套接口是非阻塞的,如果连接的服务器在同一台主机上,那么在调用connect建立连接时,连接通常会立即建立成功.我们必须处理这种情况;
2.源自Berkeley的实现(和Posix.1g)有两条与select和非阻塞IO相关的规则:
  A:当连接建立成功时,套接口描述符变成可写;
  B:当连接出错时,套接口描述符变成既可读又可写;
  注意:当一个套接口出错时,它会被select调用标记为既可读又可写;

非阻塞connect有这么多好处,但是处理非阻塞connect时会遇到很多可移植性问题;

处理非阻塞connect的步骤:
第一步:创建socket,返回套接口描述符;
第二步:调用fcntl把套接口描述符设置成非阻塞;
第三步:调用connect开始建立连接;
第四步:判断连接是否成功建立;
       A:如果connect返回0,表示连接简称成功(服务器可客户端在同一台机器上时就有可能发生这种情况);
       B:调用select来等待连接建立成功完成;
         如果select返回0,则表示建立连接超时;我们返回超时错误给用户,同时关闭连接,以防止三路握手操作继续进行下去;
         如果select返回大于0的值,则需要检查套接口描述符是否可读或可写;如果套接口描述符可读或可写,则我们可以通过调用getsockopt来得到套接口上待处理的错误(SO_ERROR),如果连接建立成功,这个错误值将是0,如果建立连接时遇到错误,则这个值是连接错误所对应的errno值(比如:ECONNREFUSED,ETIMEDOUT等).
"读取套接口上的错误"是遇到的第一个可移植性问题;如果出现问题,getsockopt源自Berkeley的实现是返回0,等待处理的错误在变量errno中返回;但是Solaris会让getsockopt返回-1,errno置为待处理的错误;我们对这两种情况都要处理;

这样,在处理非阻塞connect时,在不同的套接口实现的平台中存在的移植性问题,首先,有可能在调用select之前,连接就已经建立成功,而且对方的数据已经到来.在这种情况下,连接成功时套接口将既可读又可写.这和连接失败时是一样的.这个时候我们还得通过getsockopt来读取错误值;这是第二个可移植性问题;
移植性问题总结:
1.对于出错的套接口描述符,getsockopt的返回值源自Berkeley的实现是返回0,待处理的错误值存储在errno中;而源自Solaris的实现是返回0,待处理的错误存储在errno中;(套接口描述符出错时调用getsockopt的返回值不可移植)
2.有可能在调用select之前,连接就已经建立成功,而且对方的数据已经到来,在这种情况下,套接口描述符是既可读又可写;这与套接口描述符出错时是一样的;(怎样判断连接是否建立成功的条件不可移植)

这样的话,在我们判断连接是否建立成功的条件不唯一时,我们可以有以下的方法来解决这个问题:
1.调用getpeername代替getsockopt.如果调用getpeername失败,getpeername返回ENOTCONN,表示连接建立失败,我们必须以SO_ERROR调用getsockopt得到套接口描述符上的待处理错误;
2.调用read,读取长度为0字节的数据.如果read调用失败,则表示连接建立失败,而且read返回的errno指明了连接失败的原因.如果连接建立成功,read应该返回0;
3.再调用一次connect.它应该失败,如果错误errno是EISCONN,就表示套接口已经建立,而且第一次连接是成功的;否则,连接就是失败的;

被中断的connect:
如果在一个阻塞式套接口上调用connect,在TCP的三路握手操作完成之前被中断了,比如说,被捕获的信号中断,将会发生什么呢?假定connect不会自动重启,它将返回EINTR.那么,这个时候,我们就不能再调用connect等待连接建立完成了,如果再次调用connect来等待连接建立完成的话,connect将会返回错误值EADDRINUSE.在这种情况下,应该做的是调用select,就像在非阻塞式connect中所做的一样.然后,select在连接建立成功(使套接口描述符可写)或连接建立失败(使套接口描述符既可读又可写)时返回;

=========================================另一篇博文=========================================

 socket  non-blocking mode connect


        对于面向连接的socket类型(SOCK_STREAM,SOCK_SEQPACKET),在读写数据之前必须建立连接,connect()函数用于完成面向连接的socket的建链过程,对于TCP,也就是三次握手过程。

connect()函数

头文件:

    #include<sys/types.h>

    #include<sys/socket.h>

声明:

    int connect (int sockfd,struct sockaddr * serv_addr,int addrlen);
功能:

    使用套接字sockfd建立到指定网络地址serv_addr的socket连接,参数addrlen为serv_addr指向的内存空间大小,即sizeof(struct sockaddr_in)。

返回值:

   1)成功返回0,表示连接建立成功(如服务器和客户端是同一台机器上的两个进程时,会发生这种情况)

   2)失败返回SOCKET_ERROR,相应的设置errno,通过errno获取错误信息。常见的错误有对方主机不可达或者超时错误,也可能是对方主机没有进程监听对应的端口。

非阻塞connect(non-block mode connect)

套接字执行I/O操作有阻塞和非阻塞两种模式。在阻塞模式下,在I/O操作完成前,执行操作的函数一直等候而不会立即返回,该函数所在的线程会阻塞在这里。相反,在非阻塞模式下,套接字函数会立即返回,而不管I/O是否完成,该函数所在的线程会继续运行。

客户端调用connect()发起对服务端的socket连接,如果客户端的socket描述符为阻塞模式,则connect()会阻塞到连接建立成功或连接建立超时(Linux内核中对connect的超时时间限制是75s, Soliris 9是几分钟,因此通常认为是75s到几分钟不等)。如果为非阻塞模式,则调用connect()后函数立即返回,如果连接不能马上建立成功(返回-1),则errno设置为EINPROGRESS,此时TCP三次握手仍在继续。此时可以调用select()检测非阻塞connect是否完成。select指定的超时时间可以比connect的超时时间短,因此可以防止连接线程长时间阻塞在connect处。

select判断规则:

   1)如果select()返回0,表示在select()超时,超时时间内未能成功建立连接,也可以再次执行select()进行检测,如若多次超时,需返回超时错误给用户。

   2)如果select()返回大于0的值,则说明检测到可读或可写的套接字描述符。源自 Berkeley 的实现有两条与 select 和非阻塞 I/O 相关的规则:

        A) 当连接建立成功时,套接口描述符变成 可写(连接建立时,写缓冲区空闲,所以可写)

        B) 当连接建立出错时,套接口描述符变成 既可读又可写(由于有未决的错误,从而可读又可写)

        因此,当发现套接口描述符可读或可写时,可进一步判断是连接成功还是出错。这里必须将B)和另外一种连接正常的情况区分开,就是连接建立好了之后,服务器端发送了数据给客户端,此时select同样会返回非阻塞socket描述符既可读又可写。

        □对于Unix环境,可通过调用getsockopt来检测描述符集合是连接成功还是出错(此为《Unix Network Programming》一书中提供的方法,该方法在linux环境上测试,发现是无效的):

               A)如果连接建立是成功的,则通过getsockopt(sockfd,SOL_SOCKET,SO_ERROR,(char *)&error,&len) 获取的error 值将是0

               B)如果建立连接时遇到错误,则errno 的值是连接错误所对应的errno值,比如ECONNREFUSED,ETIMEDOUT 等

        □一种更有效的判断方法,经测试验证,在Linux环境下是有效的

        再次调用connect,相应返回失败,如果错误errno是EISCONN,表示socket连接已经建立,否则认为连接失败。

 

综上所述,这里总结一下非阻塞connect的实现过程。

非阻塞connect的实现过程

1. 创建套接字sockfd

[cpp] view plain copy
  1. /* 1. obtain a socket */  
  2. int sock_fd;  
  3. sock_fd = socket(AF_INET, SOCK_STREAM, 0);  

2. 设置套接字为非阻塞模式

[cpp] view plain copy
  1. /* 2. set non-blocking mode no socket */  
  2. #if 1  
  3. int flags = fcntl(sock_fd, F_GETFL, 0);  
  4. fcntl(sock_fd, F_SETFL, flags|O_NONBLOCK);  
  5. #else  
  6. int imode = 1;  
  7. ioctl(sock_fd, FIONBIO, &imode);  

3. 调用connect进行连接

[cpp] view plain copy
  1. struct sockaddr_in addr;  
  2. addr.sin_family = AF_INET;  
  3. addr.sin_port   = htons(PEER_PORT);  
  4. addr.sin_addr.s_addr = inet_addr(PEER_IP);  
  5. int ret = connect(sock_fd, (struct sockaddr *)&addr, sizeof(struct sockaddr));  
  6. if (0 == res)  
  7. {  
  8.   printf("socket connect succeed immediately.\n");  
  9.   ret = 0;  
  10. }  
  11. else  
  12. {  
  13.   printf("get the connect result by select().\n");  
  14.   if (errno == EINPROGRESS)  
  15.   {  
  16.     ....  
  17.   }  
  18. }  

connect会立即返回,可能返回成功,也可能返回失败。如果连接的服务器在同一台主机上,那么在调用connect 建立连接时,连接通常会立即建立成功(我们必须处理这种情况)。
4.调用select(),通过FD_ISSET()检查套接口是否可写,确定连接请求是否完成

[cpp] view plain copy
  1. fd_set rfds, wfds;  
  2. struct timeval tv;  
  3.   
  4. FD_ZERO(&rfds);FD_ZERO(&wfds);  
  5. FD_SET(sock_fd, &rfds);  
  6. FD_SET(sock_fd, &wfds);  
  7. /* set select() time out */  
  8. tv.tv_sec = 10;   
  9. tv.tv_usec = 0;  
  10. int selres = select(sock_fd + 1, &rfds, &wfds, NULL, &tv);  
  11. switch (selres)  
  12. {  
  13.     case -1:  
  14.         printf("select error\n");  
  15.         ret = -1;  
  16.         break;  
  17. case 0:  
  18.        printf("select time out\n");  
  19.        ret = -1;  
  20.        break;  
  21. default:  
  22.        if (FD_ISSET(sock_fd, &rfds) || FD_ISSET(sock_fd, &wfds))  
  23.        {  
  24.            .....  
  25.        }  
  26. }  

对于无连接的socket类型(SOCK_DGRAM),客户端也可以调用connect进行连接,此连接实际上并不建立类似SOCK_STREAM的连接,而仅仅是在本地保存了对端的地址,这样后续的读写操作可以默认以连接的对端为操作对象。

Linux下常见的socket错误码:

EACCES, EPERM:用户试图在套接字广播标志没有设置的情况下连接广播地址或由于防火墙策略导致连接失败。

EADDRINUSE 98:Address already in use(本地地址处于使用状态)

EAFNOSUPPORT 97:Address family not supported by protocol(参数serv_add中的地址非合法地址)

EAGAIN:没有足够空闲的本地端口。

EALREADY 114:Operation already in progress(套接字为非阻塞套接字,并且原来的连接请求还未完成)

EBADF 77:File descriptor in bad state(非法的文件描述符)

ECONNREFUSED 111:Connection refused(远程地址并没有处于监听状态)

EFAULT:指向套接字结构体的地址非法。

EINPROGRESS 115:Operation now in progress(套接字为非阻塞套接字,且连接请求没有立即完成)

EINTR:系统调用的执行由于捕获中断而中止。

EISCONN 106:Transport endpoint is already connected(已经连接到该套接字)

ENETUNREACH 101:Network is unreachable(网络不可到达)

ENOTSOCK 88:Socket operation on non-socket(文件描述符不与套接字相关)

ETIMEDOUT 110:Connection timed out(连接超时)

 

测试代码:

[cpp] view plain copy
  1. #include <stdio.h>  
  2. #include <string.h>  
  3. #include <stdlib.h>  
  4. #include <sys/types.h>  
  5. #include <errno.h>  
  6. #include <unistd.h>  
  7. #include <fcntl.h>  
  8. #include <sys/select.h>  
  9. #include<sys/ioctl.h>  
  10.   
  11.   
  12. //inet_addr()  
  13. #include <sys/socket.h>  
  14. #include <netinet/in.h>  
  15. #include <arpa/inet.h>  
  16.   
  17.   
  18. #define PEER_IP     "192.254.1.1"  
  19. #define PEER_PORT   7008  
  20. int main(int argc, char **argv)  
  21. {  
  22.  int ret = 0;  
  23.  int sock_fd;  
  24.  int flags;  
  25.  struct sockaddr_in addr;  
  26.    
  27.  /* obtain a socket */  
  28.  sock_fd = socket(AF_INET, SOCK_STREAM, 0);  
  29.    
  30.  /* set non-blocking mode on socket*/  
  31. #if 1  
  32.  flags = fcntl(sock_fd, F_GETFL, 0);  
  33.  fcntl(sock_fd, F_SETFL, flags|O_NONBLOCK);  
  34. #else  
  35.  int imode = 1;  
  36.  ioctl(sock_fd, FIONBIO, &imode);  
  37. #endif  
  38.   
  39.   
  40.  /* connect to server */  
  41.  addr.sin_family  = AF_INET;  
  42.  addr.sin_port  = htons(PEER_PORT);  
  43.  addr.sin_addr.s_addr = inet_addr(PEER_IP);  
  44.  int res = connect(sock_fd, (struct sockaddr *)&addr, sizeof(struct sockaddr_in));  
  45.  if (0 == res)  
  46.  {  
  47.   printf("socket connect succeed immediately.\n");  
  48.         ret = 0;  
  49.  }  
  50.  else  
  51.  {  
  52.   printf("get the connect result by select().\n");  
  53.    if (errno == EINPROGRESS)  
  54.    {  
  55.             int times = 0;  
  56.             while (times++ < 5)  
  57.             {  
  58.                 fd_set rfds, wfds;  
  59.                 struct timeval tv;  
  60.                   
  61.                 printf("errno = %d\n", errno);  
  62.                 FD_ZERO(&rfds);  
  63.                 FD_ZERO(&wfds);  
  64.                 FD_SET(sock_fd, &rfds);  
  65.                 FD_SET(sock_fd, &wfds);  
  66.                   
  67.                 /* set select() time out */  
  68.                 tv.tv_sec = 10;   
  69.                 tv.tv_usec = 0;  
  70.                 int selres = select(sock_fd + 1, &rfds, &wfds, NULL, &tv);  
  71.                 switch (selres)  
  72.                 {  
  73.                     case -1:  
  74.                         printf("select error\n");  
  75.                         ret = -1;  
  76.                         break;  
  77.                     case 0:  
  78.                         printf("select time out\n");  
  79.                         ret = -1;  
  80.                         break;  
  81.                     default:  
  82.                         if (FD_ISSET(sock_fd, &rfds) || FD_ISSET(sock_fd, &wfds))  
  83.                         {  
  84.                         #if 0 // not useable in linux environment, suggested in <<Unix network programming>>  
  85.                             int errinfo, errlen;  
  86.                             if (-1 == getsockopt(sock_fd, SOL_SOCKET, SO_ERROR, &errinfo, &errlen))  
  87.                             {  
  88.                                 printf("getsockopt return -1.\n");  
  89.                                 ret = -1;  
  90.                                 break;  
  91.                             }  
  92.                             else if (0 != errinfo)  
  93.                             {  
  94.                                 printf("getsockopt return errinfo = %d.\n", errinfo);  
  95.                                 ret = -1;  
  96.                                 break;  
  97.                             }  
  98.                               
  99.                             ret = 0;  
  100.                             printf("connect ok?\n");  
  101.                         #else  
  102.                         #if 1  
  103.                             connect(sock_fd, (struct sockaddr *)&addr, sizeof(struct sockaddr_in));  
  104.                             int err = errno;  
  105.                             if  (err == EISCONN)  
  106.                             {  
  107.                                 printf("connect finished 111.\n");  
  108.                                 ret = 0;  
  109.                             }  
  110.                             else  
  111.                             {  
  112.                                 printf("connect failed. errno = %d\n", errno);  
  113.                                 printf("FD_ISSET(sock_fd, &rfds): %d\n FD_ISSET(sock_fd, &wfds): %d\n", FD_ISSET(sock_fd, &rfds) , FD_ISSET(sock_fd, &wfds));  
  114.                                 ret = errno;  
  115.                             }  
  116.                         #else  
  117.                         char buff[2];  
  118.                         if (read(sock_fd, buff, 0) < 0)  
  119.                         {  
  120.                             printf("connect failed. errno = %d\n", errno);  
  121.                             ret = errno;  
  122.                         }  
  123.                         else  
  124.                         {  
  125.                             printf("connect finished.\n");  
  126.                             ret = 0;  
  127.                         }  
  128.                         #endif  
  129.                         #endif  
  130.                         }  
  131.                         else  
  132.                         {  
  133.                             printf("haha\n");  
  134.                         }  
  135.                 }  
  136.                   
  137.                 if (-1 != selres && (ret != 0))  
  138.                 {  
  139.                     printf("check connect result again... %d\n", times);  
  140.                     continue;  
  141.                 }  
  142.                 else  
  143.                 {  
  144.                     break;  
  145.                 }  
  146.             }  
  147.   }  
  148.   else  
  149.   {  
  150.    printf("connect to host %s:%d failed.\n", PEER_IP, PEER_PORT);  
  151.    ret = errno;  
  152.   }  
  153.  }  
  154.  if (0 == ret)  
  155.     {  
  156.         send(sock_fd, "12345"sizeof("12345"), 0);  
  157.     }  
  158.     else  
  159.     {  
  160.         printf("connect to host %s:%d failed.\n", PEER_IP, PEER_PORT);  
  161.     }  
  162.       
  163.     close(sock_fd);  
  164.  return ret;  
  165. }  





1. 非阻塞socket可调用fcntl或ioctl设置

2.connect的返回值

    非阻塞connect返回-1,并不一定是连接失败,可能是连接过程未完成,此时errno为EINPROGRESS,可通过select检查连接何时完成

3. select的返回值

    1)-1,表示select出错,可以关闭socket,重新发起连接过程

    2)0,表示select超时,此时可能connect还在进行中,可再次进行select,反复数次仍超时,可认为连接失败,需返回失败错误

    3)1,表示存在套接口描述字可读或可写,需根据规则进一步判断是连接成功还是出错

4. 通过socket可读/可写能否判断连接状态

    源自 Berkeley 的实现有两条与 select 和非阻塞 I/O 相关的规则:

        A) 当连接建立成功时,套接口描述符变成 可写(连接建立时,写缓冲区空闲,所以可写)

        B) 当连接建立出错时,套接口描述符变成 既可读又可写(由于有未决的错误,从而可读又可写)

        当发现套接口描述符可读或可写时,可进一步判断是连接成功还是出错。这里必须将B)和另外一种连接正常的情况区分开,就是连接建立好了之后,服务器端发送了数据给客户端,此时select同样会返回非阻塞socket描述符既可读又可写。

    因此,仅从socket可读或可写无法判断socket连接的状态。

5. 如何有效判断连接状态:

    1)getsockopt方法在Linux环境下无效

    2)再次执行connect,检查errno值,该方法在linux环境下有效。测试中发现的问题:

    一次select之后,发现此时套接口描述字可读或可写,再次执行connect,此时errno始终不变,仍未EINPROGRESS,增加select的超时时间结果也一样。

    之后尝试在select返回值为0,或返回值为1,且connect后errno仍为EINPROGRESS(115)时,再次执行select+connect,即再次检测连接状态。此时errno被置为EISCONN(106),connect成功。

    还没有搞清楚原因。

7. socket连接成功后是否重置为阻塞模式

    这要看连接需要什么样的效果。

    如果连接建立成功后,重置socket为阻塞模式。在给服务器发送信息等待接收数据时,如果服务器很忙,而服务器也没设计好,每到来一个客户端就服务,那么后来的要排队,客户端多的话,会导致后来的请求长时间得不到应答,线程一直被阻塞。
    然而,在非阻塞模式下,send和recv也会立即返回。测试发现,非阻塞模式下,recv常常未能接收到数据,返回错误。而在建链之后将socket重置为阻塞,recv的接收正常。

8. socket使用完成后要进行释放

    close(sock_fd)

9. 具有非阻塞属性的接口

==============================另一篇文章关于移植问题=======================================

非阻塞 connect:

在一个 TCP 套接字被设置为非阻塞之后调用 connect ,connect 会立即返回 EINPROGRESS 错误,表示连接操作正在进行中,但是仍未完成,与此同时 TCP 三次握手操作会同时进行。在这之后,我们可以通过调用 select 来检查这个链接是否建立成功。

非阻塞 connect 有三种用途:
1.我们可以在 TCP 三次握手的同时做一些其它的处理。connect 操作需要一个往返时间才能完成,从几个毫秒(局域网)到几百毫秒或几秒(广域网)。在这段时间内我们可能有一些其他的处理想要同时执行;
2.可以用这种技术同时建立多个连接。在 Web 浏览器中很普遍;
3.由于我们使用 select 来等待连接的完成,因此我们可以给 select 设置一个时间限制,从而缩短 connect 的超时时间。在大多数实现中,connect 的超时时间在 75 秒到几分钟之间。有时候应用程序想要一个更短的超时时间,使用非阻塞 connect 就是一种方法。

非阻塞 connect 听起来虽然简单,但是仍然有一些细节问题要处理:
1.即使套接字是非阻塞的,如果连接的服务器在同一台主机上,那么在调用 connect 建立连接时,连接通常会立即建立成功。我们必须处理这种情况;
2.源自 Berkeley 的实现有两条与 select 和非阻塞 I/O 相关的规则:
  A)当连接建立成功时,套接口描述符变成 可写
  B)当连接建立出错时,套接口描述符变成 既可读又可写

注意:当一个套接口出错时,它会被 select 调用标记为既可读又可写。
非阻塞 connect 有这么多好处,但是处理非阻塞 connect 时会遇到很多【可移植性问题】。

处理非阻塞 connect 的步骤:
第一步,创建 socket,返回套接字描述符;
第二步,调用 fcntl 或 ioctlsocket 把套接口描述符设置成非阻塞;
第三步,调用 connect 开始建立连接;
第四步,判断连接是否成功建立:
  A)如果 connect 返回 0 ,表示连接成功(服务器和客户端在同一台机器上时就有可能发生这种情况);
  B)调用 select 来判定连接建立的是否成功;
    如果 select 返回 0 ,则表示在 select 的超时时间内未能成功建立连接;我们需要返回超时错误给用户,同时关闭连接,以防止TCP三次握手继续进行下去;
    如果 select 返回大于 0 的值,则说明检测到可读或可写或异常的套接字描述符存在;此时我们可以通过调用 getsockopt 来检测集合中的套接口上是否存在待处理的错误,如果连接建立是成功的,则通过 getsockopt(sockfd,SOL_SOCKET,SO_ERROR,(char *)&error,&len) 获取的 error 值将是 0 ,如果建立连接时遇到错误,则 error 的值是连接错误所对应的 errno 值,比如ECONNREFUSED,ETIMEDOUT等。

=============
“读取套接口上的错误”是遇到的【第一个可移植性问题】:如果出现问题,getsockopt 源自 Berkeley 的实现是返回 0 ,等待处理的错误在变量 errno 中返回;但是 Solaris 会让 getsockopt 返回 -1 ,errno 置为待处理的错误。我们对这两种情况都要处理。

这样,在处理非阻塞 connect 时,在不同的套接口实现的平台中存在的移植性问题。首先,有可能在调用 select 之前,连接就已经建立成功,而且对方的数据已经到来。在这种情况下,连接成功时套接口将既可读又可写,这和连接失败时是一样的。这个时候我们还得通过 getsockopt 来读取错误值。这是【第二个可移植性问题】。
=============

移植性问题总结 :
1.对于出错的套接口描述符,getsockopt 的返回值源自 Berkeley 的实现是返回 0 ,待处理的错误值存储在 errno 中;而源自 Solaris 的实现是返回 -1 ,待处理的错误存储在 errno 中。(套接口描述符出错时调用 getsockopt 的返回值不可移植)
2.有可能在调用 select 之前,连接就已经建立成功,而且对方的数据已经到来,在这种情况下,套接口描述符是既可读又可写,这与套接口描述符出错时是一样的。(怎样判断连接是否建立成功的条件不可移植)

这样的话,在我们判断连接是否建立成功的条件不唯一时,我们可以有以下的方法来解决这个问题:
1.调用获取对端socket地址的 getpeername 代替 getsockopt 。如果调用 getpeername 失败,getpeername 返回 ENOTCONN ,表示连接建立失败,之后我们必须再以 SO_ERROR 调用 getsockopt 得到套接口描述符上的待处理错误;
2.调用 read ,读取长度为 0 字节的数据。如果连接建立失败,则 read 会返回 -1 ,且相应的 errno 指明了连接失败的原因;如果连接建立成功,read 应该返回 0 。
3.再调用一次 connect 。它应该失败,如果错误 errno 是 EISCONN ,就表示套接口已经建立,而且第一次连接是成功的;否则,连接就是失败的。

被中断的 connect :
如果在一个阻塞式套接口上调用 connect ,在 TCP 的三次握手操作完成之前被中断了,比如说被捕获的信号中断,将会发生什么呢?假定 connect 不会自动重启,它将返回 EINTR 。那么这个时候,我们就不能再调用 connect 等待连接建立完成了,如果再次调用 connect 来等待连接建立完成的话,connect 将会返回错误值 EADDRINUSE 。在这种情况下,应该做的是调用 select ,就像在非阻塞式 connect 中所做的一样。然后 select 在连接建立成功(使套接口描述符可写)或连接建立失败(使套接口描述符既可读又可写)时返回。

socket api 存在一批核心接口,而这一批核心接口就是几个看似简单的函数,尽管实际上这些函数没有一个是简单。connect 函数就是这些核心接口中的一个函数,它完成主动连接的过程。

connect 函数的功能对于TCP来说就是完成面向连接的协议的连接过程,它的函数原型:

Linux下

#include

#include

int connect(int sockfd, const struct sockaddr* server_addr, socklen_t addrlen)

windows下

int connect(

SOCKET s, // 没绑定套接口描述字

const struct sockaddr FAR *name, // 目标地址指针,目标地址中必须包含IP和端口信息。

int namelen // name的长度

);


面向连接的协议,在建立连接的时候总会有一方先发送数据(SYN),那么谁调用了 connect 谁就是先发送数据的一方。如此理解 connect 三个参数就容易了。我必需指定数据发送的目的地址,同时也必需指定数据从哪里发送,这正好是 connect 的前两个参数,而第三个参数是为第二个参数服务的。

参数 sockfd 
指定数据发送的套接字,解决从哪里发送的问题。内核需要维护大量 I/O 通道,所以用户必需通过这个参数告诉内核从哪个 I/O 通道(此处就是从哪个 socket 接口)中发送数据。

参数 server_addr 
指定数据发送的目的地,也就是服务器端的地址。这里服务器是针对 connect 说的,因为 connect 是主动连接的一方调用的,所以相应的要存在一个被连接的一方,被动连接的一方需要调用 listen 以接受 connect 的连接请求,如此被动连接的一方就是服务器了。 

参数 addrlen 
指定 server_addr 结构体的长度。我们知道系统中存在大量的地址结构,但 socket 接口只是通过一个统一的结构来指定参数类型,所以需要指定一个长度,以使内核在进行参数复制的时候有个界限。

返回值:没有错误发生,返回0;否则返回 SOCKET_ERROR(-1) 。

=============================================

=============================================

与所有的 socket 网络接口一样,connect 总会在某个时候可能失败,此时它会返回 -1 ,相应的 errno 会被设置,用户可能通过这个值确定是哪个错误。常见的错误有对方主机不可达或者超时错误,也可以是对方主机没有相应的进程在对应端口等待。

connect 函数可用于 面向连接套接字 也可用于 无连接套接字 。

无连接套接字:对于无连接的套接字 (SOCK_DGRAM) ,该套接字与目标地址之间建立默认的对应关系,且在本地保存了对端的地址,这样后续的读写操作可以默认以连接的对端为操作对象,网络数据交互发生时可以直接使用 send ,而不是用 sendto 来向该地址发送数据;内核会丢弃所有发送给该套接字的源地址不是 connect 地址的报文。再次调用 connect 函数,若此时 name 和 namelen 两个参数均为空指针,就会将该套接字恢复为未连接状态,再调用 send 函数,系统会提示 WSAENOTCONN 错误码。

面向连接套接字:面向连接的套接字 (SOCK_STREAM) ,函数 connect 会引起调用端主动进行 TCP 的三次握手过程。结果通常是成功连接、WSAETIMEDOUT (多次发送SYN报文,始终未收到回复)、WSAECONNREFUSED (目标主机返回 RST) 等。

  当对端机器 crash 或者网络连接被断开(比如路由器不工作,网线断开等),此时发送数据给对端然后读取本端 socket 会返回 ETIMEDOUT 或者 EHOSTUNREACH 或者 ENETUNREACH (后两个是中间路由器判断服务器主机不可达的情况)。

  当对端机器 crash 之后又重新启动,然后客户端再向原来的连接发送数据,因为服务器端已经没有原来的连接信息,此时服务器端回送 RST 给客户端,此时客户端读本地端口返回 ECONNRESET 错误。

  当服务器所在的进程正常或者异常关闭时,会对所有打开的文件描述符进行 close ,因此对于连接的 socket 描述符则会向对端发送 FIN 分节进行正常关闭流程。对端在收到 FIN 之后端口变得可读,此时读取端口会返回 0 表示到了文件结尾(对端不会再发送数据)。 

  当一端收到 RST 导致读取 socket 返回 ECONNRESET ,此时如果再次调用 write 发送数据给对端则触发 SIGPIPE 信号,信号默认终止进程,如果忽略此信号或者从 SIGPIPE 的信号处理程序返回则 write 出错返回 EPIPE 。

================================= =====
1) Broken PIPE 的字面意思是“管道破裂”。Broken PIPE 产生的原因是该管道的读端被关闭。
2) Broken PIPE 经常发生在 Client 端通过 Socket 发送信息到 Server 端后,就关闭当前 Socket , 之后 Server 端回复信息给 Client 端时。
3) 发生 Broken PIPE 错误时,调用写的进程会收到 SIGPIPE 信号,默认动作是导致当前进程终止。
4) Broken PIPE 最直接的意思是:写入端出现的时候,PIPE 的另一端却休息或退出了,因此造成没有及时取走管道中的数据,从而系统异常退出。

Client 端通过 PIPE 发送信息到 Server 端后,就关闭 Client 端 Socket , 这时 Server 端返回信息给 Client 端时就会产生 Broken PIPE 信号。

======================================

  可以看出只有当本地端口主动发送消息给对端才能检测出连接异常中断的情况,搭配 select 进行多路分离的时候,socket 收到 RST 或者 FIN 时候,select 返回可读(心跳消息就是用于检测连接的状态)。也可以使用 socket 的 KEEPLIVE 选项,依赖 socket 本身侦测 socket 连接异常中断的情况。


错误信息:

EACCES, EPERM:用户试图在套接字广播标志没有设置的情况下连接广播地址或由于防火墙策略导致连接失败。
EADDRINUSE:本地地址处于使用状态。
EAFNOSUPPORT:参数 serv_add 中的地址非法。
EAGAIN:没有足够空闲的本地端口。
EALREADY:套接字为非阻塞套接字,并且原来的连接请求还未完成。
EBADF:非法的文件描述符。
ECONNREFUSED:远程地址并没有处于监听状态。
EFAULT:指向套接字结构体的地址非法。
EINPROGRESS:套接字为非阻塞套接字,且连接请求没有立即完成。
EINTR:系统调用的执行由于捕获中断而中止。
EISCONN:已经连接到该套接字。
ENETUNREACH:网络不可到达。
ENOTSOCK:文件描述符不与套接字相关。

ETIMEDOUT:连接超时。





原创粉丝点击