說到網絡編程,就要把下面四個方面處理好。
一、網絡連接分為兩種:服務端處理接收客戶端的連接,服務端作為客戶端連接第三方服務
來自客戶端的連接,監聽accept有收到epollIN事件,或者當前服務器連接上遊服務器,進行connect時返回-1,errno為EINPROGRESS,此時再收到EPOLLOUT事件就代表連接上了,因為三次握手最後是需要回複ack給上遊服務器。(Connect非阻塞 ,在圖中箭頭出表示建立成功(需要注冊寫事件:最終客戶端還要給服務器發送個ack确認請求才能建立成功))
int clientfd=accept(listenfd, addr, sz);
//舉例為非阻塞io, 阻塞io成功直接返回0
int connectfd=socket(AF_INET,SOCK_STREAM,0);
int ret=connect(connectfd,(struct sockaddr *)&addr,sizeof(addr));
// ret==-1 && errno==EINPROGERESS 正在建立連接
// ret==-1 && errno==EISCONN 連接建立成功
當客戶端斷開時,服務端read返回0,或者收到EPOLLRDHUP事件,如果服務端要支持半關閉狀态,就關閉讀端shutdown(SHUT_RD),如果不需要支持,直接close即可,大部分都是直接close,一般close前也會進行類似釋放資源的操作,如果這步操作比較耗時,可以異步處理,否則導緻服務端出現大量的close_wait狀态。
如果是服務端主動斷開連接時,通過shutdown(SHUT_WR)發送FIN包給客戶端,**此時再調用write會返回-1,errno為EPIPE,代表寫通道已經關閉。**這裡要注意close和shutdown的區别,close時如果fd的引用不為0,是不會真正的釋放資源的,比如fd1=dup(fd2),close(fd2)不會對fd1造成影響,而shutdown則跳過了前面的引用計數檢查,直接對網絡進行操作,多線程多進程下用close比較好。斷開連接時,如果發現接收緩沖區還有數據,直接丢棄,并回複RST包,如果是發送緩沖區還有數據,則會取消nagle進行發送,末尾加上FIN包,如果開啟了SO_LINGER,則會在linger_time内等待FIN包的ack,這樣保證發送緩沖區的數據被對端接收到。
//主動關閉
close(fd);
shutdown(fd,SHUT_RDWR);
//主動關閉本地讀端,對端寫段關閉
shutdown(fd,SHUT_RD);
//主動關閉本地寫端,對端讀端關閉
shutdown(fd,SHUT_WR);
//被動:讀端關閉
//有的網絡編程需要支持半關閉狀态
int n=read(fd,buf,sz);
if(n==0)
{
close_read(fd);
//write();
//close(fd);
}
//被動: 寫端關閉
int n=write(fd,buf,sz);
if(n==-1 && errno==EPIPE)
{
close_write(fd);
//close(fd);
}
如果read大于0,接收數據正常,處理對應的業務邏輯即可,如果read等于0,說明對端發送了FIN包,如果read小于0,此時要根據errno進行下一步的判斷處理,如果是EWOULDBLOCK或者EAGAIN,說明接收緩沖區還沒有數據,直接重試即可,如果是EINTR,說明被信号中斷了,因為信号中斷的優先級比系統調用高,此時也是重試read即可(read從内核态到用戶态:正向錯誤(被信号打斷),如EWOULDBLOCK EINRT 還可以正常的讀下一次,其他錯誤直接close),如果是ETIMEDOUT,說明探活超時了,每個socket都有一個tcp_keepalive_timer,當超過tcp_keepalive_time沒有進行數據交換時,開始發送探活包,如果探測失敗,間隔tcp_keepalive_intvl時間發送下一次探活包,最多連續發送tcp_keepalive_probes次,如果都失敗了,則關閉連接,返回ETIMEDOUT錯誤。
這些探活都是在傳輸層進行的,如果應用層的進程有死鎖或者阻塞,它是檢測不到的,這種情況需要在應用層自行加入心跳包機制來進行檢測。一般客戶端與數據庫之間,反向代理與服務器直接直接用探活機制就行,但數據庫之間主從複制以及客戶端與服務器之間需要加入心跳機制,以防進程有阻塞。
int n=read(fd,buf,sz);
if(n<0)
{
//n==-1
if(errno==EINTR || errno==EWOULDBLOCK)
break;
close(fd);
}
else if(n==0)
{
close(fd);
}
else
{
//處理buf
}
第四個是消息發送,write大于0,消息放入了發送緩沖區,write小于0,同樣要分errno的情況處理,如果錯誤碼為EWOULDBLOCK,說明發送緩沖區還裝不下你要發送的數據,需要重試,如果是EINTR,說明write系統調用被信号中斷了,同樣進行重試處理,如果是EPIPE,說明寫通道已經關閉了。
int n=write(fd,buf,sz);
if(n==-1)
{
if(errno==EINTR || errno==EWOULDBLOCK)
return;
close(fd);
}
常見網絡io模型
阻塞io和非阻塞io指的是内核數據準備階段要不要阻塞等待,如果内核數據準備好了,将數據從内核拷貝至用戶空間還是阻塞的,所以它們都為同步io
阻塞io和非阻塞io:
(1)阻塞在網絡線程(2)連接的fd阻塞屬性決定了io函數是否阻塞(3)具體差異在:io函數在數據未到達時是否立刻返回;
//默認情況下,fd是阻塞的,設置非阻塞的方法如下:
int flag=fcntl(fd,F_GETFL,0);
fcntl(fd,F_SETFL,flag | O_NONBLOCK);
相關視頻推薦
6種epoll的做法,從redis,memcached到nginx的網絡模型實現
linux多線程之epoll原理剖析與reactor原理及應用
學習地址:C/C Linux服務器開發/後台架構師【零聲教育】-學習視頻教程-騰訊課堂
需要C/C Linux服務器架構師學習資料加qun812855908獲取(資料包括C/C ,Linux,golang技術,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK,ffmpeg等),免費分享
目前大部分高性能網絡中間件都是采用io多路複用加事件處理的機制,也就是reactor模型
一、redis(單reactor)redis是單reactor模型,隻有一個epoll對象,主線程就是一個循環,不斷的處理epoll事件, 首先處理accept事件,将接入的連接綁定讀事件處理函數後加入epoll中,等epoll檢測到連接有讀事件到來時,觸發讀事件處理函數,但這個函數并沒有真正的去讀數據,而是将該有讀事件到來的連接放入clients_pending_read任務隊列中,主線程循環到下一次epoll_wait前,再将這些任務隊列中的連接分配給各個io線程本地的任務隊列io_threads_list處理,在io線程裡,不斷對io_threads_pending[i]原子變量進行判斷,看有沒有值,有就代表主線程給派了任務,然後根據io_threads_op任務類型對任務進行讀或寫處理,先說讀處理部分,主要讀取客戶端數據并進行解析命令處理,将命令讀到該連接對應的querybuf中,主線程忙輪詢,等待所有io線程解析命令完成,再主線程開始執行命令,因為這些都是内存操作,所以單線程就可以,如果用多線程的話還有鎖的問題,執行完命令後将相應結果寫入連接對應的buf數組中,如果放不下就放入reply鍊表中,然後再将各個連接的響應客戶端的任務放入clients_pending_write任務隊列中,主線程再分配給各個io線程進行寫處理,将數據響應給客戶端,主線程此時也是忙輪詢,等待所有io線程完成,如果最後發現還有數據沒發送完,就注冊epollout寫事件sendReplyToClient,等客戶端可寫時再把數據發送完。可以看出網絡io相關的操作使用了多線程處理,但是命令執行等純内存操作都是單線程完成的,全程隻有一個eventLoop即相當于epollfd,這種就是單reactor模型,每個io線程都有自己的任務隊列io_threads_list,所以也沒有多線程競争的問題。
nginx采用的是單reactor多進程模型,因為每個連接都是處理的無狀态數據,故可以通過多進程實現,多進程之間共享epollfd,在内核2.6以前,accept還存在驚群問題,即如果有連接到來,多個進程的epoll_wait都能監測到,這樣多個進程都處理了該相同的連接,這是有問題的,所以nginx采用了文件鎖的方式,在ngx_process_events_and_timers函數中可以看出,哪個進程先獲得了這把鎖ngx-accept_mutex,就開始監聽EPOLLIN事件,并進行epoll_wait接收對應的事件,接收到的事件先不處理,先放入一個隊列中,如果是accept事件,就放入accept對應的ngx_posted_accept_events隊列中,其它事件放入另外一個ngx_posted_events隊列,然後再處理accept隊列的事件回調函數,到這裡才能釋放文件鎖,再去處理非accept隊列裡面的事件回調函數handler。可以看出nginx其實也是隻有一個epollfd,隻是被多個進程共享了,這樣多個進程可以并行處理事件。
memcached是基于libevent來實現網絡模型的,它是多線程多reactor模型,相比前面兩種,它是有多個reactot模型的,也就是說有多個epoll進行事件循環,它也是将網絡接入和網絡讀寫io單獨分離的方式,主線程主要處理網絡的接入,主要看server_sockets函數,有accept事件後會調用event_handler回調函數,該事件是通過調用conn_new函數注冊在主線程的event_base類型變量main_base上,所以主線程陷入事件循環後會監聽accept事件。該event_handler回調函數裡面做的事情就是調用drive_machine,看這個名字就知道是個狀态機處理,所以accept事件來時就處理conn_listening下的事情,主要是進行accept得到客戶端的sfd,然後通過round_robin算法選擇一個工作線程,将該fd信息打包成CQ_ITEM放在工作線程的連接事件隊列ev_queue中,最後通過pipe通知那個工作隊列有連接事件過來了,其實就是往pipe中發一個”c”字符,工作線程此時處理事件回調thread_libevent_process,該回調主要是從連接事件隊列ev_queue中取出主線程傳過來的那個item,再調用conn_new處理該item,conn_new之前主線程也是調用的這個,主要是用來綁定fd的讀寫事件到event_base上去,工作線程則綁定到該線程對應的那個event_base上,這個event_base對應一個epoll,所以memcached是有多個epoll,因為每個工作線程都有自己的event_base,綁定完後,後續的讀寫事件回調也是event_handler函數,裡面再調用drive_machine函數,隻是連接的狀态變成了conn_read和conn_write,這就是狀态機的好處,代碼邏輯很清晰。總的來說,主線程處理accept,工作線程處理後續的通信read和write,思路很清晰。
更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!