linux socket recv函数如何判断收完一包_linux 下经典 IO 复用模型 epoll 的使用
1. 概述
epoll?是?linux?內核為處理大批量文件描述符而對?poll?進行的改進版本,是?linux?下多路復用?IO?接口?select/poll?的增強版本,顯著提高了程序在大量并發連接中只有少量活躍的情況下的CPU利用率。
在獲取事件時,它無需遍歷整個被偵聽描述符集,只要遍歷被內核?IO?事件異步喚醒而加入?ready?隊列的描述符集合就行了。
epoll?除了提供?select/poll?所提供的?IO?事件的電平觸發,還提供了邊沿觸發,,這樣做可以使得用戶空間程序有可能緩存?IO?狀態,減少?epoll_wait?或?epoll_pwait?的調用,提高程序效率。
2. 實現原理
當某個進程調用?epoll_create?函數創建?epoll?專用的文件描述符時,Linux?內核會創建一個?eventpoll?結構體變量:
struct eventpoll{
...
struct rb_root rbr; // 紅黑樹根節點,存儲 epoll 中所有事件
struct list_head rdllist; // 雙向鏈表,保存需要 epoll_wait 返回的事件
...
}
每一個?epoll?對象都擁有一個獨立的?eventpoll?結構體,這個結構體會在內核空間中分配獨立的內存,用于存儲使用?epoll_ctl?函數向?epoll?對象中添加進來的事件。
每一個事件都會掛到紅黑樹?rbr?上,這樣重復添加的時間就可以通過紅黑樹結構快速識別并避免加入,保證了?epoll_ctl?函數的效率。
所有添加到?epoll?中的時間都會與設備驅動程序建立回調關系,一旦某個事件發生,則設備驅動程序會調用相應的回調函數,這個回調函數就是?ep_poll_callback,它會把相應事件放到?rdllist?這個雙向鏈表中。
這個雙向鏈表的元素是?epitem?結構體類型的:
{
// 紅黑樹節點
struct rb_node rbn;
// 雙向鏈表節點
struct list_head rdllink;
// 事件 fd 等信息
struct epoll_filefd ffd;
// 指向所屬的 eventpoll 對象的指針
struct eventpoll *ep;
// 期待時間類型
struct epoll_event event;
}
3. 函數原型(全部定義于?sys/epoll.h?中)
3.1. epoll?的創建
int epoll_create(int size);創建一個?epoll?專用的文件描述符,調用成功返回描述符,否則返回?-1。
需要注意的是,該描述符使用完畢后同樣需要?close?操作。
size?參數用來告訴內核監聽的數目,自從?linux?2.6.8?開始,size?參數被忽略,但是依然必須大于?0。
3.2. 事件注冊函數
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);調用成功返回0,否則返回?-1。
參數說明
epfd?—?epoll_create?返回的?epoll?專用的文件描述符
op?—?表示參數,有以下取值:
epoll_ctl?動作參數取值
| EPOLL_CTL_ADD | 注冊新的?fd?到?epfd?中 |
| EPOLL_CTL_MOD | 修改已經注冊的?fd?的監聽事件 |
| EPOLL_CTL_DEL | 從?epfd?中刪除一個?fd |
fd?—?需要監聽的fd
event?—?監聽事件類型
{
__uint32_t events;
epoll_data_t data;
}
可選以下幾個宏的組合:
epoll_ctl?event?參數可選宏
| EPOLLIN | 文件描述符可讀(或對端?socket?正常關閉) |
| EPOLLOUT | 文件描述符可寫 |
| EPOLLPRI | 文件描述符有帶外數據可讀 |
| EPOLLERR | 文件描述符發生錯誤 |
| EPOLLHUP | 文件描述符被掛斷 |
| EPOLLET | 將?epoll?設為邊緣觸發 |
| EPOLLONESHOT | 只監聽一次 |
3.3. 等待事件產生
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
返回需要處理的事件數目,如返回?0?表示超時,調用失敗返回?-1。
類似于?select?函數。
參數說明
epfd?—?epoll?專用文件描述符
events?—?事件集合
maxevents?—?每次能處理的最大事件數,不能大于?epoll_create?的?size?參數
timeout?—?超時時間,以毫秒為單位,0?表示立即返回,-1?表示永遠阻塞
4. 優勢
4.1. 支持同時打開大量的文件描述符
select?函數對一個進程所打開的FD是有一定限制的,由FD_SETSIZE設置,默認值是1024,這對于一個服務器來說顯然是太少了,雖然修改這個宏之后重新編譯系統可以解決這個問題,但是隨著?FD_SETSIZE?值的上升,select?函數的性能會顯著下降。
傳統?Apache?服務器對此的解決方案是使用多進程的方式來打開大于?FD_SETSIEZE?的文件描述符,但是開辟進程的效率和資源都有一定的消耗,同時進程間數據同步也遠沒有線程間數據同步來的高效。
epoll?能夠打開的?FD?與系統能夠持有的?FD?數目是一致的,只受限于系統的內存。
4.2. IO效率不隨?FD?數目增加而線性下降
傳統的?select、poll?具有一個致命弱點,每當有數據可讀或可寫,都需要對整個描述符集合進行掃描,這樣如果文件描述符集合很大,而同時又有大量空閑連接,則效率下降會非常明顯。
4.3. 使用mmap加速內核與用戶空間的消息傳遞
epoll是通過內核與用戶空間mmap同一塊內存實現的,這樣就可以避免從內核空間通知用戶空間的時候不必要的拷貝了。
4.4. 內核微調
內核的?TCP/IP?協議棧使用內存池管理?sk_buff?結構,通過在運行時改變?/proc/sys/net/core/hot_list_length?的值,即可動態調整整個內存池的大小,如?listen?函數所指示的3次握手數據包隊列長度也可以根據平臺內存動態調整。
5. 邊緣觸發與水平觸發
5.1. 邊緣觸發(Edge?Triggered)
邊緣觸發模式是高速工作方式,只支持?no-block?socket。
在這種模式下,當描述符從未就緒變為就緒時,內核通過?epoll?告知調用者。
然后它會假設你知道文件描述符已經就緒,并且不會再為那個文件描述符發送更多的就緒通知,直到你做了某些操作導致那個文件描述符不再為就緒狀態了(比如,你在發送,接收或者接收請求,或者發送接收的數據少于一定量時導致了一個EWOULDBLOCK?錯誤)
需要注意的是,如果一直不對這個?fd?作?IO?操作(從而導致它再次變成未就緒),內核不會發送更多的通知(only?once),不過在TCP協議中,ET?模式的加速效用仍需要更多的?benchmark?確認(這句話不理解)。
由于邊緣觸發模式下,epoll?產生一個EPOLLIN事件后,不會再次發出更多的通知,所以調用者需要判斷?recv?返回的大小是否等于請求的大小,如果?recv?返回的大小小于請求的大小,則說明有可能數據仍然存在于緩沖區中,需要再次進行讀取。
{
buflen = recv(activeevents.data.fd, buf, sizeof(buf), 0);
if(buflen < 0)
{
// 由于是非阻塞的模式,所以當errno為EAGAIN時,表示當前緩沖區已無數據可讀
// 在這里就當作是該次事件已處理處.
if(errno == EAGAIN)
break;
else
return;
}
else if(buflen == 0)
{
// 這里表示對端的socket已正常關閉.
}
if(buflen == sizeof(buf)
rs = 1; // 需要再次讀取
else
rs = 0;
}
5.2. 水平觸發(Level?Triggered)
epoll?的默認工作模式。
在水平觸發模式下,epoll?相當于一個較快的?poll。
6. 示例
6.1. SERVER
/*** date: 2015-01-02
* file: main.c
* author: 龍泉居士
*/
#include "function/function.h"
int main(int argc, char **argv)
{
int listenfd, connfd, epollfd, nfds, n, curfds, accept_count = 0;
struct sockaddr_in cliaddr;
socklen_t socklen = sizeof(struct sockaddr_in);
struct epoll_event ev;
struct epoll_event events[MAXEPOLLSIZE];
char buf[MAXLINE];
listenfd = get_listenfd();
epollfd = get_epollfd(listenfd, &ev);
curfds = 1;
printf("epollserver startup, port %d, max connection is %d, backlog is %d\n",
SERV_PORT, MAXEPOLLSIZE, LISTENQ);
while (1)
{
/* 等待有事件發生 */
nfds = epoll_wait(epollfd, events, curfds, -1);
if (nfds == -1)
{
perror("epoll_wait");
continue;
}
/* 處理所有事件 */
for (n = 0; n < nfds; ++n)
{
if (events[n].data.fd == listenfd)
{
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &socklen);
if (connfd < 0)
{
perror("accept");
continue;
}
sprintf(buf, "accept form %s:%d\n",
inet_ntoa(cliaddr.sin_addr),
cliaddr.sin_port);
printf("%d:%s", ++accept_count, buf);
if (curfds >= MAXEPOLLSIZE)
{
fprintf(stderr, "too many connection, more than %d\n",
MAXEPOLLSIZE);
close(connfd);
continue;
}
if (setnonblocking(connfd) < 0) {
perror("setnonblocking error");
return -1;
}
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = connfd;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &ev) < 0)
{
fprintf(stderr, "add socket '%d' to epoll failed: %s\n",
connfd,
strerror(errno));
return -1;
}
curfds++;
continue;
}
// 處理客戶端請求
if (handle(events[n].data.fd) < 0)
{
epoll_ctl(epollfd, EPOLL_CTL_DEL, events[n].data.fd,&ev);
curfds--;
}
}
}
close(listenfd);
return 0;
}/**
* date: 2015-01-02
* file: function.h
* author: 龍泉居士
*/
#ifndef EPOLLSERV_FUNCTION_20150102
#define EPOLLSERV_FUNCTION_20150102
#include
#include /* basic system data types */
#include /* basic socket definitions */
#include /* sockaddr_in{} and other Internet defns */
#include /* inet(3) functions */
#include /* epoll function */
#include /* nonblocking */
#include /* setrlimit */
#include
#include
#include
#include
#define LISTENQ 1024
#define MAXLINE 10240
#define SERV_PORT 8888
#define MAXEPOLLSIZE 10000 int handle(int); int setnonblocking(int); int get_listenfd(); int get_epollfd(int, struct epoll_event *);
#endif/**
* date: 2015-01-02
* file: function.c
* author: 龍泉居士
*/
#include "function.h"
int setnonblocking(int sockfd)
{
if (fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0)|O_NONBLOCK) < 0)
{
perror("fcntl");
exit(-1);
}
return 0;
}
int get_listenfd()
{
int opt = 1;
int listenfd;
struct rlimit rt;
struct sockaddr_in servaddr;
/* 設置每個進程允許打開的最大文件數 */
rt.rlim_max = rt.rlim_cur = MAXEPOLLSIZE;
if (setrlimit(RLIMIT_NOFILE, &rt) == -1)
{
perror("setrlimit");
exit(-1);
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl (INADDR_ANY);
servaddr.sin_port = htons (SERV_PORT);
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd < 0)
{
perror("can't create socket file");
exit(-1);
}
// 允許綁定已經被使用的端口
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 設置連接為非阻塞狀態
if (setnonblocking(listenfd) < 0)
{
perror("setnonblock");
exit(-1);
}
if (bind(listenfd, (struct sockaddr *) &servaddr, sizeof(struct sockaddr))
< 0)
{
perror("bind");
exit(-1);
}
if (listen(listenfd, LISTENQ) < 0)
{
perror("listen");
exit(-1);
}
return listenfd;
}
int get_epollfd(int listenfd, struct epoll_event *pev)
{
/* 創建 epoll 句柄,把監聽 socket 加入到 epoll 集合里 */
int epollfd = epoll_create(MAXEPOLLSIZE);
pev->events = EPOLLIN | EPOLLET;
pev->data.fd = listenfd;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, pev) < 0)
{
perror("epoll_ctl");
exit(-1);
}
return epollfd;
}
int handle(int connfd) {
int nread;
char buf[MAXLINE];
nread = read(connfd, buf, MAXLINE);//讀取客戶端socket流
if (nread == 0) {
printf("client close the connection\n");
close(connfd);
return -1;
}
if (nread < 0) {
perror("read error");
close(connfd);
return -1;
}
write(connfd, buf, nread);//響應客戶端
return 0;
}
6.2. CLIENT
/*** date: 2015-01-02
* file: main.c
* author: 龍泉居士
*/
#include "function/function.h"
int main(int argc, char **argv)
{
int sockfd = connect_serv();
printf("welcome to echoclient\n");
handle(sockfd); /* do it all */
close(sockfd);
printf("exit\n");
return 0;
}/**
* date: 2015-01-02
* file: function.h
* author: 龍泉居士
*/
#ifndef EPOLLCLI_FUNCTION_20150102
#define EPOLLCLI_FUNCTION_20150102
#include /*gethostbyname function */
#include
#include
#include
#include
#include
#include /* inet(3) functions */
#include /* basic system data types */
#include /* basic socket definitions */
#include /* sockaddr_in{} and other Internet defns */
#define MAXLINE 1024
#define IPADDR "127.0.0.1"
#define SERV_PORT 8888 void handle(int connfd); int connect_serv();
#endif/**
* date: 2015-01-02
* file: function.c
* author: 龍泉居士
*/
#include "function.h"
void handle(int sockfd)
{
char sendline[MAXLINE], recvline[MAXLINE];
int n;
for (;;) {
if (fgets(sendline, MAXLINE, stdin) == NULL)
{
break;//read eof
}
n = write(sockfd, sendline, strlen(sendline));
n = read(sockfd, recvline, MAXLINE);
if (n == 0) {
printf("echoclient: server terminated prematurely\n");
break;
}
write(STDOUT_FILENO, recvline, n);
}
}
int connect_serv()
{
int sockfd;
struct sockaddr_in servaddr;
if ((sockfd = socket (AF_INET, SOCK_STREAM, 0)) <= 0)
{
perror ("socket error");
exit(-1);
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, IPADDR, &servaddr.sin_addr.s_addr);
if (connect (sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)))
{
perror ("connect error");
exit(-1);
}
return sockfd;
}
總結
以上是生活随笔為你收集整理的linux socket recv函数如何判断收完一包_linux 下经典 IO 复用模型 epoll 的使用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 妈妈爱宝宝的简短说说 表达妈妈爱宝宝的简
- 下一篇: lan口配置 petalinux_Pet