總結下網絡編程中關鍵的細節問題,包含鏈接創建、鏈接斷開、消息到達、發送消息等等;html
包括服務端接受 (accept) 新鏈接和客戶端成功發起 (connect) 鏈接。
accept接受鏈接的問題在本文最後會聊到,這裏談談connect的關鍵點;
使用非阻塞鏈接創建須要注意:
connect/select返回後,可能沒有鏈接上;須要再次確認是否成功鏈接;git
步驟爲:github
Fcntl(sockfd, F_SETFL, flags | O_NONBLOCK); error = 0; if ( (n = connect(sockfd, saptr, salen)) < 0) if (errno != EINPROGRESS) return(-1); /* Do whatever we want while the connect is taking place. */ if (n == 0) goto done; /* connect completed immediately */ if ( (n = Select(sockfd+1, &rset, &wset, NULL, nsec ? &tval : NULL)) == 0) { close(sockfd); /* timeout */ errno = ETIMEDOUT; return(-1); } if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset)) { len = sizeof(error); //二次確認是否真的鏈接成功 if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0) return(-1); /* Solaris pending error */ } else err_quit("select error: sockfd not set");
包括主動斷開 (close 或 shutdown) 和被動斷開 (read 返回 0)。編程
當打算關閉網絡鏈接時,如何能知道對方已經發送了數據本身尚未收到?
在TCP層面解決:主動關閉的時候只使用半關閉shutdown(), 這樣,服務端這邊之時關閉了寫端,還能夠正常讀;客戶端收到關閉的信號後(read返回0),會再調用shutdown關閉整個鏈接;
在應用層面解決:雙方經過某個標記協商,在標記以後再也不讀寫數據,這樣就能夠徹底的關閉鏈接了;json
關閉鏈接時須要注意的:
是否還有未發送的數據,須要保證應用緩衝區中的數據都發送完畢以後再關閉緩衝區;
TCP緩存區不用咱們考慮,由於在調用shutdown或close的時候,TCP的實現是會將TCP的發送緩衝區中的數據都發送出去,而後再發送FIN報文(也多是組合成一個報文發送);緩存
消息到達是最重要的事件;對它的處理決定了網絡編程的風格:是阻塞仍是非阻塞、分包的處理、應用層的緩衝如何設計等等;服務器
所謂分包,就是在一個個字節流消息中如何區分出一個個消息來;
常見的分包方法有:網絡
若是傳輸的是二進制類型,在字節流的緩存區中直接強轉可能core dump;由於有的系統訪問地址須要字節對齊,不能在任意地址上訪問二進制類型(如整形),合理的方式是將其copy到一個本地變量中,而後再作字節序的轉換:數據結構
int32_t peekInt32() const { assert(readableBytes() >= sizeof(int32_t)); int32_t be32 = 0; ::memcpy(&be32,readerIndex_, sizeof(be32) ); return be32toh(be32); }
數據到達時處理須要注意:
socket讀事件來到,必須一次將全部的數據都讀完,不然會形成一直有可讀事件,形成busy-loop;讀到的數據固然就須要有個應用層的緩衝區來存放;
由於應用的緩存區是有限的,能夠默認設置一個大小,好比2kb,或者根本就不設置初始大小,用多少分配多少;muduo中使用的是vector 來做爲緩存區,能夠動態增加;多線程
muduo buffer使用的技巧:
buffe採用了vector自動增加的數據結構;
從系統內核中調用的時候,在應用層須要有足夠大的緩衝區,最好能一次將系統recv到的緩衝區給讀空,一次系統調用就搞定一切事情;
而應用緩衝區考慮到有不少個併發的可能,針對每一個鏈接一次都分配較大的緩衝區浪費嚴重,陳碩推薦使用readv一次讀入到兩個地址中,首先將第一個地址填滿,若是還有更多數據,就寫入到臨時緩衝區中,而後append到應用緩衝區;
讀的時候使用readv,局部使用一個足夠大的額外空間(64KB),這樣,一次讀取就足以將socket中的緩存區讀空(通常不會超過64K,tcp buffer若是確實要設置大的緩存區,須要調整系統參數);若是數據很少,可能內部buffer就裝下了,沒有額外操做,不然,多的數據讀到了外部的緩存區,再append到內部緩存區:
ssize_t Buffer::readFd(int fd, int* savedErrno) { // saved an ioctl()/FIONREAD call to tell how much to read char extrabuf[65536]; struct iovec vec[2]; const size_t writable = writableBytes(); vec[0].iov_base = begin()+writerIndex_; vec[0].iov_len = writable; vec[1].iov_base = extrabuf; vec[1].iov_len = sizeof extrabuf; // when there is enough space in this buffer, don't read into extrabuf. // when extrabuf is used, we read 128k-1 bytes at most. const int iovcnt = (writable < sizeof extrabuf) ? 2 : 1; //只有一次系統調用:這裏的實現比較巧妙 const ssize_t n = sockets::readv(fd, vec, iovcnt); if (n < 0) { *savedErrno = errno; } else if (implicit_cast<size_t>(n) <= writable) { writerIndex_ += n; } else { writerIndex_ = buffer_.size(); append(extrabuf, n - writable); } // if (n == writable + sizeof extrabuf) // { // goto line_30; // } return n; }
網絡編程中數據發送比數據接受要難處理;
數據的接收,只須要peek足夠的數據後,就能夠從應用緩衝區接收出來,而後處理;而數據的發送,還須要考慮對方接受緩慢的狀況,致使tcp發送緩衝區累積,最終致使應用緩衝區累積;
舉個例子:某客戶端對echo服務器只發送,但故意不接收;
客戶端若是隻是發送,但從不接收的話,那麼這邊發送過去的報文,首先會致使客戶端的tcp接收緩衝區滿,而後經過ack報文告訴服務器端,這邊的滑動窗口爲0了,不能再發了;後續客戶端發送的報文就把服務器端TCP發送緩衝區積滿,而後累積應用層的發送緩衝區(由於是非阻塞),最終致使服務端的應用緩存區滿或者內存撐爆;
須要發送數據的時候,優先直接調用write()發送,若是發送不成功,或沒有所有發送完畢,才加入到發送緩存區,等待可寫事件到來後發送;
直接調用write()發送數據時,須要先將本次須要發送的數據添加到緩存區,而後發送緩存區,不可直接發送本次數據(由於緩存區中可能有遺留的數據未發送完)
void TcpConnection::handleWrite() { loop_->assertInLoopThread(); if (channel_->isWriting()) { //注意,這裏只調用了一次write,而沒有反覆調用write直到出現EAGAIN錯誤, //緣由是若是第一次調用沒有發送徹底部的數據,第二次調用幾乎確定是EAGAIN錯誤, //所以這裏減小了一次系統調用,這麼作不影響正確性,卻可以下降系統時延 ssize_t n = sockets::write(channel_->fd(), outputBuffer_.peek(), outputBuffer_.readableBytes()); if (n > 0) { outputBuffer_.retrieve(n); if (outputBuffer_.readableBytes() == 0) { //若是發送緩存區爲空,再也不關注寫事件,避免 busy loop channel_->disableWriting(); //若是還有寫完成以後的回調,加入待執行回調隊列 if (writeCompleteCallback_) { loop_->queueInLoop(boost::bind(writeCompleteCallback_, shared_from_this())); } //若是此時正在關閉,調用shutdownInLoop 繼續執行關閉過程 if (state_ == kDisconnecting) { shutdownInLoop(); } } } else { LOG_SYSERR << "TcpConnection::handleWrite"; // if (state_ == kDisconnecting) // { // shutdownInLoop(); // } } } else { LOG_TRACE << "Connection fd = " << channel_->fd() << " is down, no more writing"; } }
對於低流量的服務,能夠沒必要關心這個事件;另外,這裏「發送完畢」是指將數據寫入操做系統的緩衝區,後續由 TCP 協議棧負責數據的發送與重傳,不表明對方已經收到數據。
通常都配合非阻塞socket使用,若是使用阻塞IO,可能在讀寫事件上阻塞當前線程,形成沒法繼續處理已經就緒的事件;
初學網絡編程可能都會有這個想法,select返回後,若是是讀事件,那麼這時候tcp讀緩衝區確定是有數據,這時即便使用阻塞套接字來read,應該也不會阻塞;但這樣忽略了一個點,緩衝區確實是有數據,可是極可能到達的數據並不知足你要求讀的數據大小,這樣read調用仍是會阻塞,直到有足夠的數據才返回;
那麼,對於數據讀不能夠,對accept()總能夠吧,鏈接事件返回,通常都是有新用戶接入,這時候阻塞的accept()應該老是可以返回;但在某些狀況下,可能對方剛鏈接上就斷開了,並給服務端發送了一個RST請求,形成服務端這邊將已經就緒的鏈接請求又移除了,這樣的場景下,select返回,可是accept卻沒法獲取新的鏈接,形成阻塞,直到下一個鏈接請求到來;(這方面的例子詳見《UNIX網絡編程卷1:套接字聯網API》16.6節非阻塞accept() )
因此任什麼時候候,IO multiplexing都須要配合非阻塞IO使用;
對於內核層的實現,底層調用的是系統調用sendFile()方法;
zerocopy技術省去了將操做系統的read buffer拷貝到程序的buffer, 以及從程序buffer拷貝到socket buffer的步驟, 直接將 read buffer 拷貝到 socket buffer;
詳見:http://www.cnblogs.com/zemliu/p/3695549.html
應用層上的實現,對於自定義的結構,通常是交換內部指針(使用C++11,可使用move操做來實現高效交換結構體)
若是是vector等結構,使用其成員函數swap()就能達到高效的交換(相似C++11中的move操做);
例如muduo中buffer實現:經過swap實現了緩存區的指針交換,從而達到數據交換的目的,而不用拷貝緩衝區;
void swap(Buffer& rhs) { buffer_.swap(rhs.buffer_); // std::vector<char> buffer_; std::swap(readerIndex_, rhs.readerIndex_); std::swap(writerIndex_, rhs.writerIndex_); }
epoll使用是LT而非ET,緣由以下:
通常認爲 edge-trigger 模式的優點在於可以減小 epoll 相關係統調用,這話不假,但網絡服務程序裏可不是隻有 epoll 相關係統調用,爲了繞過餓死問題,edge-trigger 模式下用戶要自行進行 read/write 循環處理,這其中增長的系統調用和減小的 epoll 系統調用加起來,整體性能收益究竟如何?只有實際測量才知道,沒法一律而論。爲了下降處理邏輯複雜度,經常使用的事件處理庫大部分都選擇了 level-trigger 模式(如 libevent、boost::asio、muduo等)
《UNIX網絡編程卷1:套接字聯網API》
《Linux多線程服務端編程:使用muduo網絡庫》
Posted by: 大CC | 31DEC,2015
博客:blog.me115.com [訂閱]
Github:大CC