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 { //其餘的處理 } } }