43. TCP套接字之connect非阻塞

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Function_Dou/article/details/89959332

本节来将客户端的connect阻塞式改为非阻塞式. 这样可以同时向服务端发起多个连接并一起进行处理, 非阻塞connect一般用来测试服务端的抗压能力.


connect 非阻塞用途

connect设置为非阻塞之后会立即返回 设置errno为 EINPROGRESS 错误, 表示连接操作正在进行中, 但是仍未完成连接; 同时TCP的三次握手操作继续进行. 之后, 可以调用 select 来检查连接是否成功.

其非阻塞connect有三种用途 :

  1. 可以在三路握手的同时做一些其它的处理. 这段时间可能几毫秒但也可能几百毫秒.
  2. 可以同时建立多个连接. 在Web浏览器中很普遍.
  3. 可以使用 select(或者IO复用其他函数) 来等待连接的完成, 因此可以给 select 设置时间限制, 从而缩短 connect 的超时时间. 毕竟大多数中, connect 的超时时间在75秒到几分钟之间. 我们就可以通过connect 非阻塞来设置更短的超时时间.

connect 连接注意的细节

connect 的细节还有一些需要注意哦, 不然很容易处理错误 :

  1. 即使套接口是非阻塞的, 如果连接的服务器在同一台主机上, 那么调用 connect 建立连接时, 连接通常会立即建立成功.
  2. 源自Berkeley 的实现 (和POSIX) 有两条与 select 和非阻塞 connect 相关的两条规则:
-   当连接建立成功时, 套接口描述符变成可写;

-   当连接出错时, 套接口描述符变成既可读又可写;

注意:当一个套接口出错时,它会被select调用标记为既可读又可写.


connect 超时设置

connect 没有超时设置, 但是我们可以将其设置为非阻塞式, 由select来设置超时即可. 在写代码时要注意上述的细节处理哦.


完整代码 :

客服端 : timeout_client.c

服务端 : service.c

客服端部分代码 :

// connect 超时封装
int timeout_connect(int sockfd, struct sockaddr *addr, socklen_t socklen, int nsec){
	int oldfd;
	int ret;
	int err = 0;	// 通过 getsockopt 获取 connect 是否错误
	struct timeval tval;	// 设置定时时间

	// 设置为非阻塞
	oldfd = setnonblock(sockfd);
	if((ret = connect(sockfd, addr, socklen)) < 0){
		if(errno != EINPROGRESS)
			return -1;
	}
	// 本机连接, 会立即建立连接
	if (ret == 0){
		goto done;
	}
	
	// rset : 用于判断可读; wset : 用于判断可写.
	fd_set rset, wset;
	FD_ZERO(&rset);
	FD_SET(sockfd, &rset);
	wset = rset;

	tval.tv_sec = nsec;
	tval.tv_usec = 0;
	if(select(sockfd+1, &rset, &wset, NULL, nsec ? &tval : 0) == 0){
		// 超时
		close(sockfd);
		errno = ETIMEDOUT;
		return -1;
	}
	// 可读 | 可写
	if(FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset)){
		socklen_t len = sizeof(err);
		// 通过 getsockopt 不返回0, 表示连接出错
		if(getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &err, &len) < 0)
			return -1;
	}
	else{
		fprintf(stderr, "select error");
		exit(1);
	}

done:
	// sockfd 恢复到进函数进前的状态. 因为只有 connect 需要非阻塞
	fcntl(sockfd, F_SETFL, oldfd);
	if(err){
		close(sockfd);
		errno = err;
		return -1;
	}
	return 0;
}

函数中, 注意connect返回0代表本机间能快速连接成功. 连接成功后需要将socket恢复到最初的状态, 在该封装中我们只用关心非阻塞状态.


connect 移植性问题

关于connect 非阻塞还有一个问题, 就是不可移植性. 下面我将移植的问题罗列出来.

  1. 出错的套接口描述符, getsockopt的返回值在 Berkeley 的实现是返回0, 待处理的错误值存储在errno中; 而 Solaris 的实现是返回-1, 待处理的错误存储在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 非阻塞细节
  • connect 非阻塞实现

猜你喜欢

转载自blog.csdn.net/Function_Dou/article/details/89959332