在accept返回之前,连接终止

情况概述

有一种类似于中断系统调用的情况,会导致accept返回一个非致命错误,这种情况下,我们只需要再次调用accept就行。下图所示的数据包系列,在繁忙的服务器上很常见(特别是繁忙的Web服务器)。


在这里,三次握手完成后,连接建立完成,然后Client TCP发送一个RST(reset)。在服务器方,连接被它的TCP放入等待连接队列,当RST到来时,连接正在等待服务器进程调用accept。一段时间后,服务器进程调用accept。

场景模拟方法

      一种简单的模拟这种场景的方法是:启动server,让它调用socket, bind, listen, 然后在调用accept之前睡眠一端时间。当server进程在休眠时,启动客户端程序并且让它调用socket和connect。一旦connect返回,就设置SO_LINGER套接字选项来产生RST并且终止程序。

不幸的是,如何处理这种已终止的连接是与实现相关的。继承自Berkeley的实现完全由内核来处理已终止连接,服务器进程永远看不到它。然而,大部分的SVR4实现,在accept返回时返回一个错误给进程,而返回的错误又是依赖于实现的。这些SVR4实现返回值为EPROTO的errno(“protocol error”),但是POSIX指定在这种情况必须返回ECONNABORTED(“Software caused connection abort”)。POSIX这样规定是考虑到,当流子系统中一些致命的协议相关的事件发生时,同样会返回EPROTO。为client已建立连接的非致命终止返回相同的错误,可以让server知道是否应该再次调用accept。在出现ECONABORTED错误的情况下,server可以忽略该错误而继续调用accept等待连接。

Berkeley-derived实现

      继承自Berkeley的内核实现不会把该错误传递到用户进程。所涉及的步骤如下:处理RST,导致tcp_close被调用。这个函数先调用in_pcbdetach ,接着调sofree,然后发现这个已被终止的socket仍然在监听套接字连接完成队列中(连接就绪,等待accept返回给进程),就把该socket删除并且释放相应的资源。server抽出时间调用accept时,它重来不知道一个已经就绪的连接被从队列中删除了

模拟场景代码

服务器:

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>

int open_listenfd(int port)
{
	struct sockaddr_in myAddr;
	int listenfd;

	/* Create a socket descriptor */
	if ( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
		return -1;

	/*
	listenfd will be an end point for all requests to port on any IP address for this host
	*/
	bzero((char *)&myAddr, sizeof(myAddr));
	myAddr.sin_family = AF_INET;
	myAddr.sin_port = htons(port);
	myAddr.sin_addr.s_addr = htonl(INADDR_ANY);


	if (bind(listenfd, (struct sockaddr *)&myAddr, sizeof(myAddr)) < 0)
	{
		close(listenfd);
		return -1;
	}

	if (listen(listenfd, 1024) < 0)
	{
		close(listenfd);
		return -1;
	}
	return listenfd;
}


void SetNonblocking(int fd)
{
	int flags = fcntl(fd, F_GETFL, 0); 
	fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}


#define MAXLEN 512
int main(int argc, char **argv)
{
	int port, listenfd, connfd, addrLen = sizeof(struct sockaddr_in);
	struct sockaddr_in clientAddr;

	port = atoi(argv[1]);

	if ( (listenfd = open_listenfd(port)) < 0)
	{
		perror("open_listenfd: ");
		return 0;
	}

	fd_set readfds, readyfds;
	FD_ZERO(&readfds);  
	FD_SET(listenfd, &readfds); 
	int fds;
	//SetNonblocking(listenfd);
	while (1)
	{
		readyfds = readfds;
		fds = select(listenfd+1, &readyfds, NULL, NULL, NULL);
		if (fds <= 0)
		{
			printf("select error e: %s\n", strerror(errno));
			continue;
		}
		printf("listening socket readable\n");
		sleep(10);
		connfd = accept(listenfd, (struct sockaddr *)&clientAddr, &addrLen);
		if (connfd <= 0)
		{
			printf("accept error: %d\n", errno);
		}
		printf("%d connection from %s:%d\n", connfd, inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));
		echo(connfd);
		close(connfd);
		
	}
	close(listenfd);
}


void echo(int fd)
{
	char buff[MAXLEN];
	int n;

	while( (n = recv(fd, buff, MAXLEN, 0)) > 0)
	{
		buff[n] = 0;
		puts(buff);
	}
	printf("recv return: %d errno: %d \n", n, errno);
}
客户端:

#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <unistd.h>
#include <netdb.h>

#define  SERV_PORT 1234

int  main(int argc, char **argv)
  {
      int     sockfd;
      struct linger ling;
      struct sockaddr_in servaddr;

      if (argc != 2)
          printf("usage: tcpcli <IPaddress>");

     sockfd = socket(AF_INET, SOCK_STREAM, 0);

     bzero(&servaddr, sizeof(servaddr));
     servaddr.sin_family = AF_INET;
     servaddr.sin_port = htons(SERV_PORT);
     inet_pton(AF_INET, argv[1], &servaddr.sin_addr);

     connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr));

     ling.l_onoff = 1;          /* cause RST to be sent on close() */
     ling.l_linger = 0;
     setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &ling, sizeof(ling));
    close(sockfd);

     exit(0);
 }

测试结果

在ubuntu系统(Linux ubuntu 2.6.35-22-generic #33-Ubuntu SMP Sun Sep 19 20:34:50 UTC 2010 i686 GNU/Linux)上的测试结果如下:
  1. 当注释掉client中的close(sockfd)时,server端的accept会返回成功,接下来的recv返回errno为ECONNRESET   Connection reset by peer
  2. 如果不注释掉close(sockfd),server端的accept会返回成功,接下来的recv返回0,表示对方关闭连接。
  3. server端的sockfd无论是否是设置非阻塞,accept都会立即返回(这好像《unix网络编程》上说的不一样?)
  4. 启动了多少个client,server端的accept就返回多少次,并且都成功。

带来的问题

这种情况带来的问题是:造成accept不必要的阻塞。不过我测试中没发现这个问题。

解决方法

解决这个问题的方法是:

1  当使用select来指明socket可以被连接时,始终把监听socket设为非阻塞

2 忽略accept返回的EWOULDBLOCK(Berkeley实现,client终止连接) ,ECONNABORTED(POSIX实现,client终止连接) ,EPROTO(SVR4实现,client终止连接) ,EINTR (如果捕捉到信号)


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值