close函数:
终止数据传送的两个方向。。。。
1,客户端A调用close函数,也就是说:socket(A)不能向socket(B)发送数据,也不能接收从B发送来的据。。。
当客户端A调用了close函数时(一般情况下:会发送一个FIN分节),对端read返回为0,但是这并不意味着对端
socket(B)不能向A发送数据,,,这也是允许的,,,(因为B收到FIN只能说明:A->B的数据不能传输了,但
是B->A还是可以传输数据的,,,)
不用质疑的是:A不接受B发送来的数据,完全不影响B向A发送数据,只是A不接受罢了(关闭了),A关闭了,TCP
会向B响应一个RST分节,表示连接重置。。。。如果B再次调用write函数,那么就会产生一个
SIGPIPE信号,,,
这里我们说明一个问题:调用close函数(对于一个客户端和一个服务端而言:会发送一个FIN分节;
对于多个客户端而言(多进程并发,select的I/O复用的并发机制),close会把描述符的引用
计数减1,仅在该计数变为0时才关闭套接字)
接下来,我们可以在看一个问题,我们可以把TCP看成是全双工的管道:
我们以简单理解一下这个图:首先TCP报文端A,发送过去,然后B,C等,紧接着在还未收到A的确认的时候,
我们客户端发送了FIN分节,导致我们客户端不能在次发送数据,严重的是我们的确认也就收不到了,,,,
这个情况的产生是很正常的,,,,
为了避免这个尴尬的问题,,,,我们引入了shutdown,,,
#include <sys/socket.h>
int shutdown(int sockfd, int how); //若成功则为0,若出错则为-1
该函数的行为依赖于how的值:
SHUT_RD(0):关闭连接的读这一半——套接字中不能在从管道中读取数据
SHUT_WR(1):不能再向管道当中写入数据,,,
SHUT_RDWR(2):相当于前面两者的组合。。。。。
如果how = 1,就可以保证发送一个FIN分节,保证对端接收到一个EOF字符,不用管套接字的引用计数是不是等于0,
1,shutdown可以有选择的终止某个方向上的数据传送,,,或者终止数据传送的两个方向,,,,我们既可以终止
数据的发送方向,也可以终止数据的接收方向。。。。。。
2,close要发送FIN分节,必须等到引用计数等于0,而shutdown不用,,,,
看一下代码:
客户端处理代码,,,
while(1) //需要检测标准输入/套接口输入是不是产生了可读事件,
{
FD_SET(fd_stdin, &rset);
FD_SET(sock, &rset);
nready = select(maxfd + 1, &rset, NULL , NULL, NULL);
if(nready == -1)
ERR_EXIT("select");
if(nready == 0)
continue;
if(FD_ISSET(sock, &rset))
{
int ret = readline(sock, recvbuf, sizeof(recvbuf));
if(ret == -1)
ERR_EXIT("readline");
else if(ret == 0)
{
printf("server close\n");
break;
}
fputs(recvbuf, stdout);
memset(recvbuf, 0, sizeof(recvbuf));
}
if(FD_ISSET(fd_stdin, &rset))
{
if(fgets( sendbuf, sizeof(sendbuf), stdin) == NULL)
{
close(sock); //这里是重点
}
else
{ writen(sock, sendbuf, strlen(sendbuf));
memset(sendbuf, 0, sizeof(sendbuf));
}
}
}
最后一个if是select检测缓冲区,如果缓冲区中有数据,调用else中的writen()函数,如果出现信号中断(或者其他
方式使得fgets返回为-1,好像ctrl + b可以),然后就会调用close(sock)函数,,,,
服务端程序:
//这里是对已完成的套接字进行继续检测
for(i = 0; i< FD_SETSIZE; i++)
{
conn = client[i];
if(conn == -1)
continue;
if(FD_ISSET(conn, &rset))
{
char recvbuf[1024] = {0};
int ret = readline(conn, recvbuf, 1024);
if(ret == -1)
ERR_EXIT("readline");
if(ret == 0)
{
printf("client close\n");
FD_CLR(conn, &allset);
}
fputs(recvbuf, stdout);
sleep(4); //同样,这里也是重点,,,,
writen(conn, recvbuf, strlen(recvbuf));
if(--nready <= 0)
break;
}
}
这里所加的延时,目的很简单,就是向客户端在发送完数据后,还没有来得及收到回射(响应)就调用close函数,,
调用close函数,就意味着不能向管道里面写入数据,也不能从管道中读取数据。。。。
这里如果,客户端,快速的输入(“aaa”,“bbb”),即使的按下ctrl+b,那么服务端是会收到aaa,bbb,但是我们
在客户端看到到回射会来的数据,因为,刚才按下ctrl+b,就已经导致程序执行到了close,close使得套接口不能
接收数据,也不能发送数据,所以很正常的收不到回射回来的数据,,,,
这里,可能会产生一个RST错误,然后接着产生sigpipe,所以我们应该对这个信号进行处理。。。。。。。。
那么,如果,我们调用shutdown呢????
if(FD_ISSET(fd_stdin, &rset))
{
if(fgets( sendbuf, sizeof(sendbuf), stdin) == NULL)
{
// close(sock); //这里是重点
shutdown(sock, SHUT_WR); //表示关闭了写
}
else
{ writen(sock, sendbuf, strlen(sendbuf));
memset(sendbuf, 0, sizeof(sendbuf));
}
}
如此,我们的程序还是可以回射回来的,因为,此刻表示套接口可以接受数据,只是不能发送而已。。。。。。。。
套接字超时:
1,调用alarm,它在指定超时期满时产生SIGALRM信号
void handler(int sig)
{
return 0; //不做多余的操作,仅仅只是打断read
}
signal(SIGALRM, handler); //对下面信号的处理
alarm(5); //我们设置一个闹钟,5秒钟
int ret = read(fd, buf, sizeof(buf)); //再一个I/O上读操作
//由于上面设置了alarm,那么如果5秒的时间到来了,还没有产生读操作,那么
//就会产生一个SIGALRM信号。。。。将read函数打断,不再阻塞
if(ret == -1 && errno == EINTR)
{
errno = ETIMEDOUT;
}
else if(ret >= 0)
{
alarm(0); //5秒之前就已经读操作了,必须关闭闹钟
}
但是,我们一般不会通过这种方式来实现超时,因为闹钟在程序里经常另作它用,而如果我们通过这种方式来实现
超时,闹钟直接会产生冲突,但是,冲突的解决比较麻烦,,,,
2,使用较新的套接字选项SO_RCVTIMEO和SO_SNDTIMEO
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, 5);
//设置读的超时时间
int ret = read(sock, buf, sizeof(buf));
if(ret == -1 && errno == EWOULDBLOCK)
{
//超时之后,直接打断read,read返回-1,同时errno等于EWOULDBLOCK
errno = ETIMEDOUT; //更改错误
}
我们也不经常用这两个套接字选项来处理超时问题,很简单,因为并非所有的TCP实现都支持这两个套接字。。。
3,select实现超时,在select中阻塞等待I/O(select有内置的时间限制),以此代替直接阻塞在read或write调用上。。
实现简单的read_timeout()(下面是实现的一些伪代码,,,)
//成功(未超时,返回0),失败返回-1,超时返回-1并且errno = ETIMEDOUT
read_timeout(int fd, unsigned int wait_seconds)//wait_seconds要等待的秒数
{
int ret = 0;
if(wait_seconds > 0) //要等待的秒数大于0,执行如下的操作
{
fd_set read_fdset;//定义一个读的集合
struct timeval timeout; //定义超时时间
FD_ZERO(&read_fdset); //清空该集合
FD_SET(fd, &read_fdset); //将该套接口加入读集合
timeout.tv_sec = wait_seconds; //初始化秒数
timeout.tv_usec = 0; //毫秒
do
{
//select超时检测
ret = select(fd + 1, &read_fdset, NULL,NULL, &time_out);
}while(ret < 0 && errno == EINTR);
if(ret == 0) //超时返回
{
ret = -1;
errno = ETIMEDOUT;
}
else if(ret == 1) //产生可读事件
{
ret = 0;
}
}
return ret;
}
int ret;
ret = read_timeout(fd, 5);
if(ret == 0)
{
read(fd, ,,,,,); //没有超时,可以进行读操作
}
else if(ret == -1 && errno == ETIMEDOUT)
{
timeout = ETIMEDOUT; //已经超时,重新设置超传时间
}
else
{
ERR_EXIT("read_timeout");//read_timeout函数内部执行出错
}
实现简单的write_timeout(),这里跟上面唯一不一样的地方就在于:
将fd加入到写的集合中去了,,,
do
{
//select超时检测
ret = select(fd + 1,NULL, &write_fdset,NULL, &time_out);
}while(ret < 0 && errno == EINTR);
这里要强调的一点就是,我们这里的timeout函数,都只是简单的一个检测,并没有读操作/写操作。。。。。。。。。。
//fd表示相应的套接字,addr表示返回的对端的地址,wait_seconds设置秒数
int accept(int fd, struct sockaddr_in *addr, unsigned int wait_seconds)
{
int ret= 0;
socklen_t addrlen = sizeof(struct sockaddr_in);
if(wait_seconds > 0)
{
fd_set accept_fdset;
struct timeval timeout;
FD_ZERO(&accept_fdset);
FD_SET(fd, &accept_fdset);
timeout.tv_sec = wait_seconds;
timeout.tv_usec = 0;
do
{
ret = select(fd + 1, &accept_fdset, NULL, NULL, &timeout);
//此刻如果fd可读事件,说明已经有三次握手握手了
//如果在timeout时间内,那么会返回1,如果超时会返回0
//如果函数执行出错,那么返回-1
//如果返回-1,并且errno 等于EINTR,那么重复执行
}while(ret < 0 && errno == EINTR);
if(ret == -1)
return -1;
else if(ret == 0)
{
errno = ETIMEDOUT;
return -1;
}
}
//此刻说明已经不再阻塞了
if(addr != NULL)
ret = accept(fd, (struct sockaddr*)addr, &addrlen);
else
ret = accept(fd, NULL, NULL); //如果地址为空,那么长度也要为空
if(ret == -1)
ERR_EXIT("accept");
return ret;
}
下面就是connect_timeout()函数,那么为什么要对连接进行超时检测呢???????????
大家肯定还记得我们当时的三次握手吧,,,,,由客户端发起的连接请求,然后等待服务端的响应,
然后这个过程就是一个RTT(数据的往返时间),而对于服务端而言,就是1.5RTT,在一般的情况下,
请求的响应时间都是极短的,但是在广域网而言,传输的过程网络中极有可能发生网络拥塞,在拥塞
的情况下,这个响应时间是很大的,一般socket,内核默认是75s,这对于我们而言,是不能容忍的
,仅仅光一个请求的确认就要花费这么长时间,这个时间足够我们去喝杯咖啡了,,,,,
所以,我们这里要设置超时,????
对于connect超时时间:我们不能直接调用connect,如果直接调用那么就会产生阻塞,,,为了不阻塞,我们
首先应该将socket(fd)设置为非阻塞的,,,,
void activate nonblock(int fd)
{
int ret;
int flags = fcntl(fd, F_GETFL);
if(flags == -1)
ERR_EXIT("fcntl");
flags |= O_NONBLOCK;
ret = fcntl(fd, F_SETFL, flags);
if(ret == -1)
ERR_EXIT(fcntl);
}
//设置相应的I/O为非阻塞模式,,,
//同样的,设置为阻塞模式,
//仅仅是相对于上面的 flags |= O_NONBLOCK; 改为flag &= ~O_NONBLOCK;
void deactivate nonblock(int fd)
{
int ret;
int flags = fcntl(fd, F_GETFL);
if(flags == -1)
ERR_EXIT("fcntl");
flags &= ~O_NONBLOCK; //只有这里不一样
ret = fcntl(fd, F_SETFL, flags);
if(ret == -1)
ERR_EXIT(fcntl);
}
int connect_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds)
fd表示I/O或套接口,,,addr表示对方的地址,,,wait_seconds表示设置的时间
int connect_timeout(int fd, struct sockaddr_in *addr , unsigned int wait_seconds)
{
int ret;
socklen_t addrlen = sizeof(struct sockaddr_in);
if(wait_seconds > 0)
activate_nonblock(fd); //设置为阻塞模式
//再设置为非阻塞模式之后,我们就可以调用accept了
ret = connect(fd, (struct sockaddr *)addr, addrlen);
//当然此刻,极有可能返回失败,也有可能返回成功
if(ret < 0 && errno == EINPROGRESS) //小于0,表示失败,表示正在处理当中,,,,
{
fd_set connect_fdset;
struct tineval timeout;
FD_ZERO(&connect_fdset);
FD_ZET(fd, &connect_fdset);
timeout.tv_sec = wait_seconds;
timeout.tv_usec = 0;
do
{
//一旦连接建立完成,套接字就可以写,说明此刻三次握手就已经完成,已经有已完成的套接字了
ret = select(fd + 1, NULL, &connect_fdset, NULL, &timeout);
}while(ret < 0 && errno == EINTR)
if(ret == 0)
{
ret == -1;
errno = ETIMEDOUT;
} //表示已然超时
else if(ret < 0)
{
return -1;
}
else if(ret == 1) //返回1,可能有两种情况,一种是连接建立成功,一种是产生错,,,,
//此时错误信息不会保存至errno变量中,因此,需要调用getsockopt来获取。。。
{
int err;
socklen_t socklen = sizeof(err);
int sockoptret = getsockopt(fd, SOL_SOCKET, SO_ERROR, err, &socklen);
if(sockoptret == -1)
ret = -1;
if(err = 0)
ret = 0; //说明没有错误,属于连接建立成功的那一种,,,,
else
{
errno = err; //产生错误,我们赋值错误给errno
ret = -1;
}
}
if(wait_seconds > 0)
{
deactivate_nonblock(fd);
}
return ret;
}
}
这里面可能会产生错误情况下的1,