TCP一共定义了11种状态,这些状态可使用 netstat 命令查看html
TCP包头有4个很是重要的东西:编程
(1) Sequence Number:包的序列号,用来解决 网路包乱序的问题浏览器
(2) Acknowledge Number:ACK确认号,用来实现 超时重传机制(不丢包)缓存
(3) Window:滑动窗口,用来解决 拥塞控制的服务器
(4) TCP flag:包的类型,主要是用来 操控 TCP状态机的网络
主要是 初始化 Sequence Number的初始值异步
通讯的双方要互相通知对方本身的初始化的Sequence Number,这个号做为之后的数据通讯的序号,以保证应用层接收到的数据不会由于网络上的传输的问题而乱序socket
SYN超时:server端接收到client发的SYN后 发送了 SYN-ACK以后,此时 client掉线,server端没有收到client回送的ACK,这时链接处于中间状态,既没成功,也没失败,这时,server端每隔一段时间会重发SYN-ACK,Linux下默认重发次数为5,时间间隔依次位1s、2s、4s、8s、16s,第5次发送以后须要等 32s才知道 第5次也超时了,因此,总共须要 1 + 2 + 4 + 8 + 16 + 32 = 2 ^ 6 - 1 = 63s,这时TCP才会完全断开链接tcp
SYN Flood攻击:人为发起多个链接,在client 收到 server发送的 SYN-ACK以后 恶意掉线,这时服务器默认须要等待63s才断开链接,攻击者就能够把服务器的syn链接的队列耗尽,让正常的链接请求得不处处理。 解决办法:调整TCP参数,减小默认的重试次数、增大SYN队列链接数
TCP是全双工的,发送发和接收方都须要FIN和ACK
若是server和client同时断开链接,就会进入CLOSING状态,而后到达TIME_WAIT状态
为何有TIME_WAIT状态?
这个状态是主动执行关闭的话会经历的状态,在这个状态停留时间 是最长分节生命期(maximum segment liftime,MSL)的两倍,咱们称为2MSL.MSL意思是任何一个IP数据报可能停留在网络中存活 的最长时间,这个时间是一个有限值,不一样系统设置不一样。RFC建议值是2min,而BSD的传统实现是30s.
TIME_WAIT状态存在有两个理由:
(1) 可靠地实现TCP全双工链接终止
TIME_WAIT确保有足够的时间让 对端接收到了ACK,若是被动关闭的一方 没有收到ACK,就会触发 被动端重发FIN,一来一去正好2个MSL
(2) 容许老的重复分组在网络中消失
假设A->B发送一个分节,中途因为路由器出现故障而缓存在路由器中,A超时重发以后,链接关闭。如今AB又同时使用相同的IP和端口而且 分节序列号也正好匹配的话,那么之前链接丢失的分组就会出如今新的链接而被处理,TIME_WAIT状态 不容许2MSL以内使用相同的端口链接,就不会出现老分组出如今新链接上了
关于TIME_WAIT数量太多?
若是服务器是HTTP服务器,那么设置一个HTTP的 KeepAlive(浏览器会重用一个TCP链接来处理多个HTTP请求)
/* rio_readn -robustly read n bytes (unbuffered) */ int rio_readn(int fd, void* usrbuf, size_t n) { size_t nleft = n; int nread = 0; char* bufp = (char*)usrbuf; while(nleft > 0) { nread = read(fd, bufp, nleft); if (nread < 0) { if(errno == EINTR) { /* interrupted by sig handler return */ nread = 0; /* and call read() again */ } else { return -1; /* errno set by read() */ } } else if (nread == 0) { break; /* EOF */ } nleft -= nread; bufp += nread; } return (n - nleft); /* return >= 0 */ }
/* rio_writen -robustly write n bytes (unbuffered) */ int rio_writen(int fd, void* usrbuf, size_t n) { size_t nleft = n; int nwrite = 0; char* bufp = (char*)usrbuf; while(nleft > 0) { nwrite = write(fd, bufp, nleft); if (nwrite <= 0) { if(errno == EINTR) { /* interrupted by sig handler return */ nwrite = 0; /* and call write() again */ } else { return -1; /* errno set by write() */ } } nleft -= nwrite; bufp += nwrite; } return (n - nleft); }
Unix一共有5中I/O模型
(1) 阻塞式I/O:进程read系统调用,一直阻塞到 内核将数据准备好并成功返回。默认状况下,全部套接字调用都是阻塞的(read、write、connect、accept)
(2) 非阻塞式I/O:进程反复调用read(轮询),若是没有数据准备好,当即返回一个EWOULDBLOCK错误。fcntl函数将默认套接字转换为 non-blocking
(3) I/O多路复用:进程阻塞于select调用,等待可能多个套接字中的任一个变为可读
(4) 信号驱动式I/O:SIGIO
(5) 异步I/O:(POSIX aio_系列函数)
套接字默认状态是阻塞的,可能阻塞的套接字调用可分为如下4类:
(1) 读操做:read、readv、recv、recvfrom、recvmsg
这些函数若是对 阻塞的套接字进行调用,若是该套接字的接收缓冲区中没有数据可读,该进程将投入睡眠(即阻塞),直到有数据可读时才唤醒
这些函数若是对 非阻塞套接字进行调用,若是该套接字的接收缓冲区中没有数据可读,相应调用当即返回一个 EWOULDBLOCK错误
(2) 写操做:write、writev、send、sendto、sendmsg
这些函数若是对 阻塞的套接字进行调用,若是该套接字的发送缓冲区中没有剩余空间可写,该进程将投入睡眠(即阻塞),直到有剩余空间可写时才唤醒
这些函数若是对 非阻塞套接字进行调用,若是该套接字的发送缓冲区中没有剩余空间可写,相应调用当即返回一个 EWOULDBLOCK错误
(3) accept函数
若是对 阻塞套接字进行调用,而且尚无新的链接到达,调用进程将投入睡眠
若是对 非阻塞套接字进行调用,而且尚无新的链接到达,accept调用会当即返回一个 EWOULDBLOCK错误
(4) connect函数
若是对 非阻塞套接字调用 connect函数,而且链接不能当即创建,那么链接的创建能照常发起,不过会返回一个 EINPROGRESS错误
int connect_nonb(int sockfd, const struct sockaddr* addr, socklen_t addrlen, int nsec) { int flags = fcntl(sockfd, F_GETFL, 0); fcntl(sockfd, F_SETFL, flags | O_NONBLOCK); // fcntl设置非阻塞 int error = 0; int ret = connect(sockfd, addr, addrlen); if (ret < 0) { if (errno != EINPROGRESS) { // 指望的错误是EINPROGRESS,表示链接创建已经启动可是 还没有完成,其余错误一概直接返回-1 return -1; } } /* Do whatever we want while the connect is taking place. */ if (ret == 0) { // 非阻塞connect返回0,表示链接创建完成 fcntl(sockfd, F_SETFL, flags); // 恢复套接字的文件标志并返回 if (error != 0) { // 若是getsockopt返回的error变量非0,表示链接创建发生错误 close(sockfd); errno = error; return -1; } return 0; } else { // 非阻塞connect,链接创建已经启动可是还没有完成,调用select等待套接字变为可读或可写 fd_set read_set; fd_set write_set; FD_ZERO(&read_set); FD_SET(sockfd, &read_set); write_set = read_set; struct timeval tval; // 设置select超时时间 tval.tv_sec = nsec; tval.tv_usec = 0; ret = select(sockfd + 1, &read_set, &write_set, NULL, nsec ? &tval : NULL); if (ret == 0) { // select返回0,超时,关闭套接字 close(sockfd); errno = ETIMEDOUT; return -1; } // 若是描述符变为可读或可写,调用getsockopt获取套接字的待处理错误(SO_ERROR选项),若是链接成功创建,error值为0,若是链接创建发生错误,error = errno if (FD_ISSET(sockfd, &read_set) || FD_ISSET(sockfd, &write_set)) { len = sizeof(error); if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0) { return -1; } } else { err_quit("select error:sockfd not set"); } } }
当咱们执行epoll_ctl时,除了把socket放到epoll文件系统里file对象对应的红黑树上以外,还会给内核中断处理程序注册一个回调函数,告诉内核,若是这个句柄的中断到了,就把它放到准备就绪list链表里
(1) 进程打开的最大描述符数目
select中 一个进程打开的最大描述符数目由FD_SETSIZE设置,32位默认为1024个(硬编码)
epoll没有FD_SETSIZE的限制,它所支持的FD上限是最大能够打开文件的数目,1GB内存大约10W
(2) FD集合扫描
select/poll每次调用都会线性扫描所有的集合,致使效率呈现线性降低
epoll每次调用只扫描 "活跃"的socket(通常状况下,任一时间只有部分的socket是"活跃"的),这是由于在内核实现中epoll是根据每一个fd上面的callback函数实现的
(3) 内核与用户空间的消息传递
不管是select,poll仍是epoll都须要内核把FD消息通知给用户空间
select和poll直接采用 内存拷贝
epoll使用mmap内存共享,避免内存拷贝
for( ; ; ) { nfds = epoll_wait(epfd, events, 20, 500); for(i = 0;i < nfds; ++i) { if(events[i].data.fd == listenfd) //有新的链接 { connfd = accept(listenfd, (sockaddr *)&clientaddr, &clilen); //accept这个链接 ev.data.fd = connfd; ev.events = EPOLLIN | EPOLLET; epoll_ctl(epfd,EPOLL_CTL_ADD, connfd, &ev); //将新的fd添加到epoll的监听队列中 } else if( events[i].events & EPOLLIN ) //接收到数据,读socket { n = read(sockfd, line, MAXLINE)) < 0 //读 ev.data.ptr = md; //md为自定义类型,添加数据 ev.events = EPOLLOUT | EPOLLET; epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev);//修改标识符,等待下一个循环时发送数据,异步处理的精髓 } else if(events[i].events & EPOLLOUT) //有数据待发送,写socket { struct myepoll_data* md = (myepoll_data*)events[i].data.ptr; //取数据 sockfd = md->fd; send(sockfd, md->ptr, strlen((char*)md->ptr), 0 ); //发送数据 ev.data.fd = sockfd; ev.events = EPOLLIN | EPOLLET; epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev); //修改标识符,等待下一个循环时接收数据 } else { //其余的处理 } } }