目录
socket编程(七)
TCP 11种状态
连接建立三次握手、连接终止四次握手
TIME_WAIT与SO_REUSEADDR
RST标志
SIGPIPE
socket编程(八)
五种I/O模型
1. 阻塞I/O模型
2. 非阻塞I/O模型
3. I/O复用模型(最常用)
4.信号驱动I/O模型
5. 异步IO模型(效率最高)
select
socket编程(九)
select读、写、异常事件发生条件
socket编程(十)
close与shutdown的区别:
图上只包含10种状态,还有一种特殊的状态:CLOSING(产生原因是双方同时关闭)
(同时调用close,此时两端同时给对端发送FIN包),将产生closing状态,最后双方都进入TIME_WAIT状态(如下图)。
TIME_WAIT状态(由发起colse的一端产生)
//TCP状态转换图【11种状态】 是 针对“一个TCP连接【一个socket连接】”来说的;
//客户端: CLOSED ->SYN_SENT->ESTABLISHED【连接建立,可以进行数据收发】
//服务端: CLOSED ->LISTEN->【客户端来握手】SYN_RCVD->ESTABLISHED【连接建立,可以进行数据收发】
//谁主动close连接,谁就会给对方发送一个FIN标志置位的一个数据包给对方;【服务器端发送FIN包给客户端】
//服务器主动关闭连接:ESTABLISHED->FIN_WAIT1->FIN_WAIT2->TIME_WAIT
//客户端被动关闭:ESTABLISHED->CLOSE_WAIT->LAST_ACK
TIME_WAIT 状态也称为 2MSL 等待状态。每个具体 TCP 实现必须选择一个报文段最大生存时间 MSL(Maximum Segment Lifetime)。
(四次挥手主动关闭的一方就会产生这个状态)
//setsockopt(SO_REUSEADDR)用在服务器端,socket()创建之后,bind()之前
SO_REUSEADDR的能力:
//(1)SO_REUSEADDR允许启动一个监听服务器并捆绑其端口,即使以前建立的将端口用作他们的本地端口的连接仍旧存在;
//【即便TIME_WAIT状态存在,服务器bind()也能成功】
//(2)允许同一个端口上启动同一个服务器的多个实例,只要每个实例捆绑一个不同的本地IP地址即可;
//(3)SO_REUSEADDR允许单个进程捆绑同一个端口到多个套接字,只要每次捆绑指定不同的本地IP地址即可;
//(4)SO_REUSEADDR允许完全重复的绑定:当一个IP地址和端口已经绑定到某个套接字上时,如果传输协议支持
//对于每一个TCP连接,操作系统是要开辟出来一个收缓冲区,和一个发送缓冲区 来处理数据的收和发;
//当我们close一个TCP连接时,如果我们这个发送缓冲区有数据,那么操作系统会很优雅的把发送缓冲区里的数据发送完毕,然后再发fin包表示连接关闭;
//FIN【四次挥手】,是个优雅的关闭标志,表示正常的TCP连接关闭;
//反观RST标志:出现这个标志的包一般都表示 异常关闭;如果发生了异常,一般都会导致丢失一些数据包;
//如果将来用setsockopt(SO_LINGER)选项要是开启;发送的就是RST包,此时发送缓冲区的数据会被丢弃;
//RST是异常关闭,是粗暴关闭,不是正常的四次挥手关闭,所以如果你这么关闭tcp连接,那么主动关闭一方也不会进入TIME_WAIT;
如果对方socket已关闭,对等方再发写数据,则会产生SIGPIPE信号
往一个已经接收FIN的套接中写是允许的,接收到FIN仅仅代表对方不再发送数据;但是在收到RST段之后,如果还继续写,调用write就会产生SIGPIPE信号,对于这个信号的处理我们通常忽略即可
signal(SIGPIPE, SIG_IGN);
Client端测试代码:(请注意观察 27-39 , 44,53 行代码)
//client端完整代码实现及解析
#include "commen.h"
//return a socket that have connected to server.
int mkATCPClient(int serverPort, string serverIPAddr)
{
//first. create a socket
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if (sockfd == -1)
{
err_exit("socket error");
}
//second. connect to a server
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(serverPort);
serverAddr.sin_addr.s_addr = inet_addr(serverIPAddr.c_str());
if (connect(sockfd,(struct sockaddr *)&serverAddr,sizeof(serverAddr)) == -1)
{
err_exit("connect error");
}
return sockfd;
}
//捕获SIGPIPE信号的测试代码
void onSignalCatch(int signalNumber)
{
switch(signalNumber)
{
case SIGPIPE:
cout << "receive SIGPIPE = " << SIGPIPE << endl;
_exit(0);
default:
cout << "UnKnow signal" << endl;
break;
}
}
int main()
{
//安装捕获SIGPIPE信号的处理服务
signal(SIGPIPE,onSignalCatch);
int serverSocket = mkATCPClient(8002,"127.0.0.1");
char sendBuf[BUFSIZ];
//从键盘输入数据
while (fgets(sendBuf,sizeof(sendBuf),stdin) != NULL)
{
//向server发送数据(注意在server端关闭时也要发送数据,注意观察现象)
if (writen(serverSocket,sendBuf,strlen(sendBuf)) == -1)
{
err_exit("write socket error");
}
}
close(serverSocket);
return 0;
}
说明:在server端关闭(或者是处理该客户端的子进程关闭)后,仍继续往socket中写数据,则会观察到如下现象:
五种I/O模型分别是阻塞式I/O,非阻塞式I/O,信号驱动,I/O复用(这四种是同步I/O),异步I/O。
最常用的模型就是阻塞I/O模型,默认情况下,所有文件的操作都是阻塞的。应用进程调用recvfrom获取数据,系统调用直到数据包被复制到用户进程的缓冲区或发生错误时返回。从调用recvfrom到系统返回的整段时间内,应用进程都是被阻塞的,因此被称为阻塞I/O模型。
用户进程只有在第二个阶段被阻塞了,而第一个阶段没有阻塞,但是在第一个阶段中,用户进程需要盲等,不停的去轮询内核,看数据是否准备好了,因此该模型是比较消耗CPU的。
Linux系统提供select/poll,进程将fd传给select/poll系统调用,阻塞在select操作上。select/poll顺序扫描fd是否就绪,如果数据变为可读状态,应用进程调用recvfrom获取数据。
需要开启套接口信号驱动I/O功能,应用进程通过系统调用sigaction后立即返回继续工作(非阻塞)。当数据准备就绪后,系统为该应用进程生产一个SIGIO信号,通过信号回调通知应用程序调用recvfrom获取数据。
应用进程告知内核执行某个动作,并让内核在操作完成之后(包括数据从内核空间复制到用户空间)通知该进程。
异步与信号驱动的区别:
信号驱动:内核通知进程,数据包已经准备好了,可以调用recvfrom来获取数据了
异步:内核通知进程,数据准备好了,并且也已经从内核拷贝到了用户空间
select管理者
用select管理多个IO
一旦其中的一个IO或者多个IO检测到我们所感兴趣的事件 slect函数返回 返回值为检测到事件的个数
函数原形:
int select(int maxfdp, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);
函数功能:
实现多路复用,也就是说可以同时监听多个文件描述符;
所属头文件:
#include
#include #include #include 返回值
超时返回0;失败返回-1;成功返回大于0的整数,这个整数表示就绪描述符的数目。
参数说明:
maxfdp:被监听的文件描述符的总数,它比所有文件描述符集合中的文件描述符的最大值大1,因为文件描述符是从0开始计数的; readfds、writefds、exceptset:分别指向可读、可写和异常等事件对应的描述符集合。 timeout:用于设置select函数的超时时间,即告诉内核select等待多长时间之后就放弃等待。timeout == NULL 表示等待无限长的时间 struct timeval { long tv_sec; /*秒 */ long tv_usec; /*微秒 */ };
跟select一起使用的函数的作用
void FD_ZERO(fd_set *set);//清空一个文件描述符的集合 void FD_SET(int fd, fd_set *set);//将一个文件描述符添加到一个指定的文件描述符集合中 void FD_CLR(int fd, fd_set *set);//将一个指定的文件描述符从集合中清除; int FD_ISSET(int fd, fd_set *set);//检查集合中指定的文件描述符是否可以读写
例子
#include
#include #include #include #include int main(int argc, cahr **argv) { struct timeval timeout; int ret = 0; fd_set rfds; FD_ZERO(&rfds);//清空描述符集合 FD_SET(0, &rfds);//将标准输入(stdin)添加到集合中 FD_SET(sockfd, &rfds);//将我们的套接字描述符添加到集合中 /*设置超时时间*/ timeout.tv_sec = 1; timeout.tv_usec = 0; /*监听套接字是否为可读*/ ret = select(sockfd+1, &rfds, NULL, NULL, &timeout); if(ret == -1) {//select 连接失败 perror("select failure\n"); return 1; } else if(ret == 0) {//超时(timeout) return 1; } if(FD_ISSET(pesockfd, &rfds)) {//如果可读 //recv or recvfrom .................. } return 0; }
1.可读:
套接口缓冲区有数据可读。
连接的读一半关闭,即接收到FIN段,读操作将返回0。
如果是监听套接口,已完成连接队列不为空时。
套接口上发生了一个错误待处理,错误可以通过getsockopt指定SO_ERROR选项来获取。
2.可写:
套接口发送缓冲区有空间可容纳数据。
连接的写一半关闭。即收到RST段之后,再次调用write操作。
套接口上发生了一个错误待处理,错误可以通过getsockopt指定SO_ERROR选项来获取。
3.异常:
套接口存在带外数据。
close()函数:
close一个套接字的默认行为是把套接字标记为已关闭,然后立即返回到调用进程,该套接字描述符不能再由调用进程使用,然而TCP将尝试发送已经排队等待发送到对端的任何数据,发送完毕后发生的是正常的TCP连接终止序列。
在多进程并发服务器中,父子进程共享着套接字,套接字描述符引用计数记录着共享着的进程个数,当父进程或某一子进程close掉套接字时,描述符引用技术会相应的减一,当引用计数仍大于0时,这个close调用就不会引发TCP的四路握手断连过程。
shutdown()函数:
int shutdown(int sockfd,int how)
该函数行为依赖how的值。
1.SHUT_RD:值为0,关闭连接的读这一半,套接字中不再有数据接收,且套接字接收缓冲区中的现有数据全都被丢弃,该套接字描述符不能再被进程调用,对端发送的数据会被确认,然后丢弃。
2.SHUT_WR:值为1,关闭连接的写这一半。这称为半关闭,当前在套接字发送缓冲区数据被发送,然后连接终止序列。不论套接字描述符引用技术是否等于0,写半部都会被关闭。
3.SHUT_RDWR:值为2,连接的读和写都关闭。相当于先调用SHUT_RD,再调用SHUT_WR。
close与shutdown的区别:
①:close函数函数会关闭套接字,如果由其他进程共享着这个套接字,那么它仍然是打开的,这个连接仍然可以用来读和写。
②:shutdown会切断进程共享的套接字的所有连接,不管引用计数是否为0,由第二个参数选择断连的方式