我要好offer之 網絡大總結

1. TCP協議的狀態機

TCP一共定義了11種狀態,這些狀態可使用 netstat 命令查看html

 

@左耳朵耗子 tcp系列教程: 上篇  下篇shell

2. TCP創建鏈接3次握手、釋放鏈接4次握手

TCP包頭有4個很是重要的東西:編程

(1) Sequence Number:包的序列號,用來解決 網路包亂序的問題瀏覽器

(2) Acknowledge Number:ACK確認號,用來實現 超時重傳機制(不丟包)緩存

(3) Window:滑動窗口,用來解決 擁塞控制的服務器

(4) TCP flag:包的類型,主要是用來 操控 TCP狀態機的網絡

 

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釋放鏈接:四次握手

TCP是全雙工的,發送發和接收方都須要FIN和ACK

若是server和client同時斷開鏈接,就會進入CLOSING狀態,而後到達TIME_WAIT狀態

 

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請求)

 

3. 套接字socket編程

 

/* 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);
}

 

4. 瀏覽器輸入網址的背後

當你輸入一個網址的時候,實際會發生什麼?

5. Unix I/O模型 阻塞I/O和非阻塞I/O

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_系列函數)

知乎關於 阻塞/非阻塞、異步同步的討論

Linux 網絡I/O模型

   

  

 

 

 

 

6. 非阻塞I/O

套接字默認狀態是阻塞的,可能阻塞的套接字調用可分爲如下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");
        }
    }
}

 

7. I/O多路複用 select vs. epoll 

 select vs. epoll

當咱們執行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
            {
                //其餘的處理
            }
        }
    }
相關文章
相關標籤/搜索