參考
Linux Epoll使用詳解
利用Socket網(wǎng)絡(luò)編程實現(xiàn)TCP時遇到的無法立刻建立第二次連接傳輸數(shù)據(jù)的問題
TCP面試常見題:time_wait狀態(tài)產(chǎn)生的原因,危害,如何避免
計算機網(wǎng)絡(luò) | C++實現(xiàn)TCP/UDP的socket通信
愛編程的大丙
前言
在linux的網(wǎng)絡(luò)編程中,很長的時間都在使用select來做事件觸發(fā)。在linux新的內(nèi)核中,有了一種替換它的機制,就是epoll。
相比于select,epoll最大的好處在于它不會隨著監(jiān)聽fd數(shù)目的增長而降低效率。因為在內(nèi)核中的select實現(xiàn)中,它是采用輪詢來處理的,輪詢的fd數(shù)目越多,自然耗時越多。并且,在linux/posix_types.h頭文件有這樣的聲明:
#define __FD_SETSIZE 1024
表示select最多同時監(jiān)聽1024個fd,當(dāng)然,可以通過修改頭文件再重編譯內(nèi)核來擴大這個數(shù)目,但這似乎并不治本。
案例
-
單線程epoll:redis
純粹內(nèi)容操作,只有一個epoll,沒有多線程加鎖以及切換 - 多線程epoll:nettyserver
- 多核epoll:ntyco
- 多進程epoll:nginx
一、epoll的基本使用
epoll的接口非常簡單,一共就三個函數(shù):
- epoll_create:創(chuàng)建一個epoll的句柄 ,相當(dāng)于聘請了一個快讀員
- epoll_ctl:epoll的事件注冊函數(shù),相當(dāng)于快遞員對住戶的增刪改查
- epoll_wait:等待事件的產(chǎn)生,相當(dāng)于快讀員等待不停的收發(fā)快遞
首先是epoll_create函數(shù):
int epoll_create (int __size)
它只有一個參數(shù),用來告訴系統(tǒng)這個監(jiān)聽的數(shù)目一共有多大,但這個參數(shù)不同于select中的第一個參數(shù)。而是我們真實要監(jiān)聽的文件數(shù)量(事實上,在新版的Linux內(nèi)核中,該值已經(jīng)被忽略,只要大于0即可)
需要注意的是,當(dāng)創(chuàng)建好epoll句柄后,它就是會占用一個fd值,所以在使用完epoll后,必須調(diào)用close()關(guān)閉
然后是epoll_ctl函數(shù):
int epoll_ctl (int __epfd, int __op, int __fd,epoll_event *__event)
它不同與select是在監(jiān)聽事件時告訴內(nèi)核要監(jiān)聽什么類型的事件,而是在這里先注冊要監(jiān)聽的事件類型。
- 第一個參數(shù)是
epoll_create
的返回值 - 第二個參數(shù)表示要執(zhí)行的動作,有以下幾種參數(shù)可填:
EPOLL_CTL_ADD
:注冊新的文件標識符到epfd中EPOLL_CTL_MOD
:修改已經(jīng)注冊的fd的監(jiān)聽事件EPOLL_CTL_DEL
:從epfd中刪除一個文件標識符 - 第三個參數(shù)是需要監(jiān)聽的文件標識符
- 第四個參數(shù)是告訴內(nèi)核需要監(jiān)聽什么事,
結(jié)構(gòu)體epoll_event結(jié)構(gòu)如下:
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event
{
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
} __EPOLL_PACKED;
其中events參數(shù)可以是以下幾個宏的集合:
-
EPOLLIN
:表示對應(yīng)的文件描述符可以讀(包括對端SOCKET正常關(guān)閉); -
EPOLLOUT
:表示對應(yīng)的文件描述符可以寫; -
EPOLLPRI
:表示對應(yīng)的文件描述符有緊急的數(shù)據(jù)可讀(這里應(yīng)該表示有帶外數(shù)據(jù)到來); -
EPOLLERR
:表示對應(yīng)的文件描述符發(fā)生錯誤; -
EPOLLHUP
:表示對應(yīng)的文件描述符被掛斷; -
EPOLLET
: 將EPOLL設(shè)為邊緣觸發(fā)(Edge Triggered)模式,這是相對于水平觸發(fā)(Level Triggered)來說的。 -
EPOLLONESHOT
:只監(jiān)聽一次事件,當(dāng)監(jiān)聽完這次事件之后,如果還需要繼續(xù)監(jiān)聽這個socket的話,需要再次把這個socket加入到EPOLL隊列里
而data則一般用于攜帶附加數(shù)據(jù),比如網(wǎng)絡(luò)編程中的套接字文件的fd
最后是epoll_wait函數(shù):
TCP接收到事件時會回調(diào)觸發(fā)epoll_wait 中事件
int epoll_wait (int __epfd, struct epoll_event *__events,int __maxevents, int __timeout)
- 第一個參數(shù)就是前面
epoll_create
函數(shù)的返回值 - 參數(shù)
events
用來從系統(tǒng)內(nèi)核得到事件的集合 -
maxevents
告知內(nèi)核這個events有多大,不能大于創(chuàng)建epoll_create()時的size - 參數(shù)
timeout
是超時時間(毫秒,0會立即返回,-1將永久阻塞(直到有事件發(fā)生) - 該函數(shù)返回需要處理的事件數(shù)目,如返回0表示已超時。
關(guān)于ET(邊沿觸發(fā))、LT(水平觸發(fā))兩種工作模式可以得出這樣的結(jié)論:
ET模式僅當(dāng)狀態(tài)發(fā)生變化的時候才獲得通知,這里所謂的狀態(tài)的變化并不包括緩沖區(qū)中還有未處理的數(shù)據(jù),也就是說,如果要采用ET模式,需要一直read/write直到出錯為止,很多人反映為什么采用ET模式只接收了一部分數(shù)據(jù)就再也得不到通知了,大多因為這樣;而LT模式是只要有數(shù)據(jù)沒有處理就會一直通知下去的.
二、使用
前提先包含一個頭文件:
#include <sys/epoll.h>
然后通過create_epoll
來創(chuàng)建一個epoll,其參數(shù)為你epoll所支持的最大數(shù)量(已忽略)。
這個函數(shù)會返回一個新的epoll句柄
,之后的所有操作將通過這個句柄來進行操作。在用完之后,記得用close
來關(guān)閉這個創(chuàng)建出來的epoll句柄
。
之后在你的網(wǎng)絡(luò)主循環(huán)里面,每次調(diào)用epoll_wait來查詢所有的網(wǎng)絡(luò)接口,看哪一個可以讀,哪一個可以寫了。基本的語法為:
nfds = epoll_wait(kdpfd, events, maxevents, -1);
其中kdpfd為
用epoll_create創(chuàng)建之后的句柄events
是一個epoll_event*
的指針,當(dāng)epoll_wait
這個函數(shù)操作成功之后,epoll_events
里面將儲存所有的讀寫事件。max_events
是當(dāng)前需要監(jiān)聽的所有socket句柄數(shù)。
最后一個timeout是 epoll_wait的超時,為0的時候表示馬上返回,為-1的時候表示一直等下去,直到有事件發(fā)生,為任意正整數(shù)的時候表示等這么長的時間,如果一直沒有事件,則等待。
一般如果網(wǎng)絡(luò)主循環(huán)是單獨的線程的話,可以用-1來等,這樣可以保證一些效率,如果是和主邏輯在同一個線程的話,則可以用0來保證主循環(huán)的效率。
epoll_wait函數(shù)之后應(yīng)該是一個循環(huán),遍利所有的事件。
代碼簡易實現(xiàn)
#include<sys/socket.h> // socket依賴
#include<arpa/inet.h> // socket依賴
#include <unistd.h> // close依賴
#include<sys/epoll.h>
#include<cstring>
#include<iostream>
#include "log.h"
using namespace std;
#define MAX_LISTEN_SOCKET 10
#define SOCKET_PORT 5000
int main(){
int sockfd = 0;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
LOG_INFO("監(jiān)聽套接字文件描述符:%d\n", sockfd);
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(SOCKET_PORT);
// addr.sin_addr.s_addr = inet_addr("127.0.0.1");
addr.sin_addr.s_addr = htons(INADDR_ANY);
// 一般在一個端口釋放后需要等一段時間才能重新啟用,因此需要借助SO_REUSEADDR來使端口重新啟用。解決服務(wù)端異常退出之后,再次啟動服務(wù)端,客戶端無法使用同一個端口連接socket的問題
int out = 1;
int ret = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &out, sizeof(out));
if (ret < 0) {
LOG_ERROR("setsockopt");
return -1;
}
ret = bind(sockfd, (sockaddr*)&addr, sizeof(addr));
if (ret == -1) {
LOG_ERROR("綁定失?。n");
return -1;
}
ret = listen(sockfd, 5);
if (ret == -1) {
LOG_ERROR("監(jiān)聽失??!\n");
return -1;
}
sockaddr_in cliAddr;
socklen_t len = sizeof(cliAddr);
// 創(chuàng)建epoll ,int epoll_create(int size); size參數(shù) 相當(dāng)于提供給內(nèi)核一個提示,當(dāng)前需要監(jiān)聽的fd個數(shù)
int fdEp = epoll_create(MAX_LISTEN_SOCKET);
epoll_event eve;
eve.data.fd = sockfd;
eve.events = EPOLLIN ;
// 注冊事件
ret = epoll_ctl(fdEp, EPOLL_CTL_ADD, sockfd, &eve);
if(ret == -1) {
LOG_ERROR("epoll注冊失敗\n");
close(sockfd);
return -1;
}
LOG_INFO("開始監(jiān)聽!");
epoll_event events[MAX_LISTEN_SOCKET];
while (1) {
/* timeout 500 epoll_wait (int __epfd, struct epoll_event *__events, int __maxevents, int __timeout);
* __epfd:由epoll_create 生成的epoll專用的文件描述符
__events:分配好的epoll_event結(jié)構(gòu)體數(shù)組 用于接收fd對象的緩沖區(qū)
__maxevents:每次能夠處理的事件數(shù) 這一次調(diào)用可以接收多少準備好的fd對象,通常設(shè)置為events參數(shù)的長度
__timeout:如果沒有準備好的事件對象,那么等待多久返回,-1阻塞,0非阻塞
返回值:返回events緩沖區(qū)中有效的fd個數(shù),也即準備好事件的fd個數(shù)
*/
int eventCount = epoll_wait(fdEp, events, MAX_LISTEN_SOCKET, 500);
if (eventCount == -1) {
LOG_ERROR("select 出錯!\n");
break;
}else if (eventCount == 0) {
continue;
}
LOG_INFO("監(jiān)聽到事件數(shù)量:%d\n",eventCount);
for (int i = 0; i < eventCount; i++) {
// 如果是服務(wù)器fd并且是讀事件,則接收連接
if (events[i].data.fd == sockfd) {
// 是否讀事件
if(events[i].events & EPOLLIN){
int clisock = accept(sockfd, (sockaddr*)&cliAddr, &len);
if (clisock == -1) {
LOG_ERROR("接收客戶端錯誤\n");
continue;
}else{
LOG_INFO("接收到客戶端連接");
}
eve.data.fd = clisock;
eve.events = EPOLLIN;
epoll_ctl(fdEp, EPOLL_CTL_ADD, clisock, &eve);
}else{
LOG_INFO("服務(wù)器其他事件");
}
}
else {
// 對非服務(wù)器socket進行處理
if (events[i].events & EPOLLIN) {
char buf[0xFF]{};
size_t len = recv(events[i].data.fd, buf, 0xFF, 0);
if (len < 0) {
LOG_ERROR("客戶端異常!");
epoll_ctl(fdEp, EPOLL_CTL_DEL, events[i].data.fd, NULL);
close(events[i].data.fd);
break;
}else if (len == 0) {
LOG_INFO("客戶端已經(jīng)斷開連接!");
epoll_ctl(fdEp, EPOLL_CTL_DEL, events[i].data.fd, NULL);
close(events[i].data.fd);
break;
}else{
LOG_INFO("客戶端接收到數(shù)據(jù):%s",buf);
}
send(events[i].data.fd, buf, len, 0);
}
else if(events[i].events & EPOLLOUT) {
LOG_INFO("客戶端 EPOLLOUT");
}
else {
LOG_INFO("客戶端其他事件");
}
}
}
}
close(fdEp);
close(sockfd);
return 0;
}
三、注意問題
因TIME_WAIT導(dǎo)致的客戶端斷開之后無法立馬重連
問題描述
在測試時發(fā)現(xiàn),當(dāng)客戶端和服務(wù)器端建立第一次連接并成功發(fā)送數(shù)據(jù)后,關(guān)閉連接,想要立刻建立第二次連接發(fā)送數(shù)據(jù)時,客戶端會無法再次連接。大約2分鐘(MSL)之后才可以重新連接
time_wait狀態(tài)產(chǎn)生的原因
-
1)為實現(xiàn)TCP全雙工連接的可靠釋放
由TCP狀態(tài)變遷圖可知,假設(shè)發(fā)起主動關(guān)閉的一方(client)最后發(fā)送的ACK在網(wǎng)絡(luò)中丟失,由于TCP協(xié)議的重傳機制,執(zhí)行被動關(guān)閉的一方(server)將會重發(fā)其FIN,在該FIN到達client之前,client必須維護這條連接狀態(tài),也就說這條TCP連接所對應(yīng)的資源(client方的local_ip,local_port)不能被立即釋放或重新分配,直到另一方重發(fā)的FIN達到之后,client重發(fā)ACK后,經(jīng)過2MSL時間周期沒有再收到另一方的FIN之后,該TCP連接才能恢復(fù)初始的CLOSED狀態(tài)。如果主動關(guān)閉一方不維護這樣一個TIME_WAIT狀態(tài),那么當(dāng)被動關(guān)閉一方重發(fā)的FIN到達時,主動關(guān)閉一方的TCP傳輸層會用RST包響應(yīng)對方,這會被對方認為是有錯誤發(fā)生,然而這事實上只是正常的關(guān)閉連接過程,并非異常。 -
2)為使舊的數(shù)據(jù)包在網(wǎng)絡(luò)因過期而消失
為說明這個問題,我們先假設(shè)TCP協(xié)議中不存在TIME_WAIT狀態(tài)的限制,再假設(shè)當(dāng)前有一條TCP連接:(local_ip, local_port, remote_ip,remote_port),因某些原因,我們先關(guān)閉,接著很快以相同的四元組建立一條新連接。本文前面介紹過,TCP連接由四元組唯一標識,因此,在我們假設(shè)的情況中,TCP協(xié)議棧是無法區(qū)分前后兩條TCP連接的不同的
,在它看來,這根本就是同一條連接,中間先釋放再建立的過程對其來說是“感知”不到的。這樣就可能發(fā)生這樣的情況:前一條TCP連接由local peer發(fā)送的數(shù)據(jù)到達remote peer后,會被該remot peer的TCP傳輸層當(dāng)做當(dāng)前TCP連接的正常數(shù)據(jù)接收并向上傳遞至應(yīng)用層(而事實上,在我們假設(shè)的場景下,這些舊數(shù)據(jù)到達remote peer前,舊連接已斷開且一條由相同四元組構(gòu)成的新TCP連接已建立,因此,這些舊數(shù)據(jù)是不應(yīng)該被向上傳遞至應(yīng)用層的),從而引起數(shù)據(jù)錯亂進而導(dǎo)致各種無法預(yù)知的詭異現(xiàn)象。作為一種可靠的傳輸協(xié)議,TCP必須在協(xié)議層面考慮并避免這種情況的發(fā)生,這正是TIME_WAIT狀態(tài)存在的第2個原因。 -
3)總結(jié)
具體而言,local peer主動調(diào)用close后,此時的TCP連接進入TIME_WAIT狀態(tài),處于該狀態(tài)下的TCP連接不能立即以同樣的四元組建立新連接,即發(fā)起active close的那方占用的local port在TIME_WAIT期間不能再被重新分配。由于TIME_WAIT狀態(tài)持續(xù)時間為2MSL,這樣保證了舊TCP連接雙工鏈路中的舊數(shù)據(jù)包均因過期(超過MSL)而消失,此后,就可以用相同的四元組建立一條新連接而不會發(fā)生前后兩次連接數(shù)據(jù)錯亂的情況。
原因分析
-
1、
TCP的可靠連接
眾所周知,TCP是一種可靠的連接,而斷開連接需要“四次揮手”,為保證傳輸?shù)目煽啃?,需要有一個階段來保證能夠?qū)Τ鲥e或丟失的數(shù)據(jù)包進行重發(fā),那么這個階段就是TIME_WAIT狀態(tài)。 -
2、
允許老的重復(fù)分解在網(wǎng)絡(luò)中消失
關(guān)于這句話,看起來可能沒那么容易理解,按通俗的話來講就是:在第一次使用bind()函數(shù)綁定端口后,再次啟動程序,導(dǎo)致地址復(fù)用,從而bind()函數(shù)error。 -
MSL(Maximum Segment Lifetime)最大報文生存時間
每個TCP實現(xiàn)必須選擇一個MSL。它是任何報文段被丟棄前在網(wǎng)絡(luò)內(nèi)的最長時間。這個時間是有限的,因為TCP報文段以IP數(shù)據(jù)報在網(wǎng)絡(luò)內(nèi)傳輸,而IP數(shù)據(jù)報則有限制其生存時間的TTL時間。RFC 793指出MSL為2分鐘,現(xiàn)實中常用30秒或1分鐘。 -
2MSL
當(dāng)TCP執(zhí)行主動關(guān)閉,并發(fā)出最后一個ACK,該鏈接必須在TIME_WAIT狀態(tài)下停留的時間為2MSL。這樣可以(1)讓TCP再次發(fā)送最后的ACK以防這個ACK丟失(被動關(guān)閉的一方超時并重發(fā)最后的FIN);保證TCP的可靠的全雙工連接的終止。(2)允許老的重復(fù)分節(jié)在網(wǎng)絡(luò)中消失。參考文章《unix網(wǎng)絡(luò)編程》(3)TCP連接的建立和終止 在TIME_WAIT狀態(tài) 時兩端的端口不能使用,要等到2MSL時間結(jié)束才可繼續(xù)使用。當(dāng)連接處于2MSL等待階段時任何遲到的報文段都將被丟棄。不過在實際應(yīng)用中可以通過設(shè)置 SO_REUSEADDR選項達到不必等待2MSL時間結(jié)束再使用此端口。
有了這個思路,那么如果能夠讓一個端口在短時間內(nèi)連續(xù)使用,我們的問題不就可以解決了嗎?
SO_REUSEADDR,用于對TCP套接字處于TIME_WAIT狀態(tài)下的socket,當(dāng)你想要在端口被釋放后立即可以使用,那么應(yīng)該在bind之前調(diào)用SO_REUSEADDR。
int ret = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &out, sizeof(out));
總結(jié):一般在一個端口釋放后需要等一段時間才能重新啟用,因此需要借助SO_REUSEADDR來使端口重新啟用。
四、socket
緩沖區(qū)大小
建立一個socket,通過getsockopt獲取緩沖區(qū)的值如下:
發(fā)送緩沖區(qū)大?。?code>SNDBufSize = 16384
接收緩沖區(qū)大?。?code>RCVBufSize = 87380
通過代碼設(shè)置緩存區(qū)文章來源:http://www.zghlxwxcb.cn/news/detail-444965.html
bool Socket::set_send_buffer(int size)
{
int buff_size = size;
if (setsockopt(m_sockfd, SOL_SOCKET, SO_SNDBUF, &buff_size, sizeof(buff_size)) < 0)
{
LOG_ERROR("socket set send buffer error: errno=%d errstr=%s", errno, strerror(errno));
return false;
}
return true;
}
bool Socket::set_recv_buffer(int size)
{
int buff_size = size;
if (setsockopt(m_sockfd, SOL_SOCKET, SO_RCVBUF, &buff_size, sizeof(buff_size)) < 0)
{
LOG_ERROR("socket set recv buffer error: errno=%d errstr=%s", errno, strerror(errno));
return false;
}
return true;
}
如果接收緩存區(qū)設(shè)置過小,比如設(shè)置10,會導(dǎo)致recv循環(huán)讀取返回值為-1,此時socket并沒有斷開,循環(huán)繼續(xù)是可以讀取到數(shù)據(jù)的,但是會導(dǎo)致比如上傳文件時速度變慢,所以緩存區(qū)大小建議默認或者設(shè)置大一些文章來源地址http://www.zghlxwxcb.cn/news/detail-444965.html
set_keep_alive
set_linger
/* 優(yōu)雅關(guān)閉: 直到所剩數(shù)據(jù)發(fā)送完畢或超時 */
到了這里,關(guān)于linux中epoll+socket實戰(zhàn)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!