eclipse 網(wǎng)站開發(fā)教程臺州關鍵詞優(yōu)化平臺
目錄
- 目標
- 網(wǎng)絡編程關注的問題
- 連接的建立
- 連接的斷開
- 消息的到達
- 消息發(fā)送完畢
- 網(wǎng)絡 IO 職責
- 檢測 IO
- 檢測 io剖析
- 操作 IO
- 阻塞IO 和 非阻塞IO
- IO 多路復用
- epoll
- 結(jié)構(gòu)以及接口
- reactor編程
- 連接建立
- 連接斷開
- 數(shù)據(jù)到達
- 數(shù)據(jù)發(fā)送完畢
- reactor 應用:后續(xù)補充源碼解析
- 單 reacrtor
- 多 reactor(one eventloop per thread)
- 多線程
- 多進程
目標
- 明白網(wǎng)絡模塊要處理那些事情
- reactor 是怎么處理這些事情的
- reactor 如何封裝的
- 網(wǎng)絡模塊與業(yè)務邏輯的關系
- 如何優(yōu)化 reactor
網(wǎng)絡編程關注的問題
連接的建立
分為兩種:
??服務端處理接收客戶端的連接;
??服務端作為客戶端連接第三方服務(如數(shù)據(jù)庫)
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 == EINPROGRESS 正在建立連接
// ret == -1 && errno = EISCONN 連接建立成功
連接的斷開
分為兩種:
??主動斷開
??被動斷開
// 主動關閉
close(fd);
shutdown(fd, SHUT_RDWR);
// 主動關閉本地讀端,對端寫段關閉
shutdown(fd, SHUT_RD);
// 主動關閉本地寫端,對端讀段關閉
shutdown(fd, SHUT_WR);// 被動:讀端關閉
// 有的網(wǎng)絡編程需要支持半關閉狀態(tài)
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);
}
消息的到達
從緩沖區(qū)中讀取數(shù)據(jù)
int n = read(fd, buf, sz);
if (n < 0) { // n == -1if (errno == EINTR || errno == EWOULDBLOCK)break;close(fd);
} else if (n == 0) {close(fd);
} else {// 處理 buf
}
消息發(fā)送完畢
往緩沖區(qū)中寫數(shù)據(jù)
int n = write(fd, buf, dz);
if (n == -1) {if (errno == EINTR || errno == EWOULDBLOCK) {return;}close(fd);
}
網(wǎng)絡 IO 職責
檢測 IO
??io 函數(shù)本身可以檢測 io的狀態(tài);但是只能檢測一個 fd對應的狀態(tài);
??io 多路復用可以同時檢測多個 io的狀態(tài);
區(qū)別:
??io 函數(shù)可以檢測具體的狀態(tài),io 多路復用只能檢測出可讀、可寫、錯誤、斷開等籠統(tǒng)的事件
檢測 io剖析
??io 函數(shù)和系統(tǒng)調(diào)用中都有用到 檢測 io。主要功能就是檢測 io 是否就緒,如果對應到 socket 網(wǎng)絡通信來說每個函數(shù)檢測的部分如下:
acccept();//檢測全連接隊列是否有數(shù)據(jù)://第 1 次握手:將數(shù)據(jù)放到半連接隊列//第 3 次握手:將數(shù)據(jù)放入全連接隊列connect();//檢測是否收到 ACK,收到 ACK 就代表 IO 就緒,連接成功//第 2 次握手成功,就表示 client 連接成功read = 0; //檢測 buf 是否含有 EOF 標記//關閉連接時,會往對應的緩沖區(qū)寫入 EOF,讀到 EOF 就會返回 0write //就是把數(shù)據(jù)寫到 send_buf 緩沖區(qū)中,至于數(shù)據(jù)什么時候?qū)?#xff0c;以什么形式寫,何時到達對端,都是根絕協(xié)議棧來決定的
操作 IO
只能使用 io 函數(shù)來進行操作;分為兩種操作方式:
??阻塞 io
??非阻塞 io
阻塞IO 和 非阻塞IO
- 阻塞在網(wǎng)絡線程
- 連接的 fd阻塞屬性決定了 io函數(shù)是否阻塞
- 具體差異在:io 函數(shù)在數(shù)據(jù)未到達時是否立刻返回
// 默認情況下,fd 是阻塞的,設置非阻塞的方法如下;
int flag = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flag | O_NONBLOCK);
詳細分析可以看I/O詳解與五種網(wǎng)絡I/O模型
IO 多路復用
io 多路復用只負責檢測io,不負責操作 io
int n = epoll_wait(epfd, evs, sz, timeout);
??timeout = -1 一直阻塞直到網(wǎng)絡事件到達;
??imeout = 0 不管是否有事件就緒立刻返回;
??timeout = 1000 最多等待 1 s,如果1 s內(nèi)沒有事件觸發(fā)則返回;
詳細分析可以看I/O詳解與五種網(wǎng)絡I/O模型
epoll
結(jié)構(gòu)以及接口
struct eventpoll {// ...struct rb_root rbr; // 管理 epoll 監(jiān)聽的事件struct list_head rdllist; // 保存著 epoll_wait
返回滿?條件的事件// ...
};
struct epitem {// ...struct rb_node rbn; // 紅?樹節(jié)點struct list_head rdllist; // 雙向鏈表節(jié)點struct epoll_filefd ffd; // 事件句柄信息struct eventpoll *ep; // 指向所屬的eventpoll對
象struct epoll_event event; // 注冊的事件類型// ...
};
struct epoll_event {__uint32_t events; // epollin epollout
epollel(邊緣觸發(fā))epoll_data_t data; // 保存 關聯(lián)數(shù)據(jù)
};typedef union epoll_data {void *ptr;int fd;uint32_t u32;uint64_t u64;
}epoll_data_t;int epoll_create(int size);/**
op:
EPOLL_CTL_ADD
EPOLL_CTL_MOD
EPOLL_CTL_DELevent.events:
EPOLLIN 注冊讀事件
EPOLLOUT 注冊寫事件
EPOLLET 注冊邊緣觸發(fā)模式,默認是水平觸發(fā)
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);/**
events[i].events:
EPOLLIN 觸發(fā)讀事件
EPOLLOUT 觸發(fā)寫事件
EPOLLERR 連接發(fā)生錯誤
EPOLLRDHUP 連接讀端關閉
EPOLLHUP 連接雙端關閉
*/
int epoll_wait(int epfd, struct epoll_event*
events, int maxevents, int timeout);
??調(diào)用 epoll_create 會創(chuàng)建一個 epoll對象;
??調(diào)用 epoll_ctl 添加到 epoll 中的事件都會與網(wǎng)卡驅(qū)動程序建立回調(diào)關系,相應事件觸發(fā)是會調(diào)用回調(diào)函數(shù)(ep_poll_callback),將觸發(fā)的事件拷貝到 rdlist 雙向鏈表中;
??調(diào)用 epoll_wait 將會把 rdlist 中就緒事件拷貝到用戶態(tài)中;
reactor編程
reactor為什么要引入 IO多路復用?
Q: 什么是 IO 復用,IO 多路復用是否具有操作 具體連接的 IO功能?
A: IO 多路復用只有檢測 IO 的功能,能檢測多條連接是否 IO 就緒,但是不具備 IO 操作的功能,無法操作 IO 數(shù)據(jù)
Q: 為什么要把 IO 檢測的功能丟給 IO 多路復用去做,而不是 IO 函數(shù)自己來做?
A: 主要是為了提升性能,因為在大部分情況下,大會部分連接是沒有交互的。
?提升性能的原因如下,就 IO 是否阻塞的情況進行分析:
- 阻塞 IO :若 IO 有自己檢測,那么就代表每條 連接需要一條線程來處理
- 非阻塞 IO :每個 IO 都需要調(diào)用 while 循環(huán)在應用層檢測
reactor 把對 IO 的處理轉(zhuǎn)換成對事件的處理:
- 注冊 IO 就緒事件,注冊到 IO 多路復用之中。注冊具體事件時,會綁定一個回調(diào)函數(shù),當事件發(fā)生時調(diào)用該回調(diào)函數(shù),并在回調(diào)函數(shù)中操作具體的 IO
- epoll_wait 收集事件,處理事件(通常是封裝為事件循環(huán))
reactor中用到了 IO 多路復用 和 非阻塞 IO,他們分別用到了 IO的哪種功能?
- IO 多路復用 :檢測 IO
- 非阻塞 IO:操作 IO
reactor 為什么要搭配非阻塞 IO?
- 多線程環(huán)境:將一個 listen放到多個 epoll中處理,如果此時有三個縣城響應了,但是只會有一個線程搶到執(zhí)行權(quán),其余的線程就會一直被阻塞
- 邊緣觸發(fā):讀事件出發(fā)時,如果 read 在一次事件中把 read_buf 讀空后再 read,就會阻塞線程
- 用select產(chǎn)生的bug:當一個數(shù)據(jù)到達時,select會報告讀事件,但是數(shù)據(jù)可能沒有通過校驗和檢測——所以該事件會被丟棄。但此時 select 已經(jīng)上報讀事件了,此時如果用的是阻塞 IO 去讀,就會造成阻塞線程
Q: 是不是 IO 多路復用一定要搭配 非阻塞 IO?
A: 不一定:例如 MySQL
連接建立
// 一、處理客戶端的連接
// 1. 注冊監(jiān)聽 listenfd 的讀事件
struct epoll_event ev;
ev.events |= EPOLLIN;
epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &ev);
// 2. 當觸發(fā) listenfd 的讀事件,調(diào)用 accept 接收新的連
接
int clientfd = accept(listenfd, addr, sz);
struct epoll_event ev;
ev.events |= EPOLLIN;
epoll_ctl(efd, EPOLL_CTL_ADD, clientfd, &ev);
// 二、處理連接第三方服務
// 1. 創(chuàng)建 socket 建立連接
int connectfd = socket(AF_INET, SOCK_STREAM, 0);
connect(connectfd, (struct sockaddr *)&addr,
sizeof(addr));
// 2. 注冊監(jiān)聽 connectfd 的寫事件
struct epoll_event ev;
ev.events |= EPOLLOUT;
epoll_ctl(efd, EPOLL_CTL_ADD, connectfd, &ev);
// 3. 當 connectfd 寫事件被觸發(fā),連接建立成功
if (status == e_connecting && e->events &
EPOLLOUT) {status == e_connected;// 這里需要把寫事件關閉epoll_ctl(epfd, EPOLL_CTL_DEL, connectfd,
NULL);
}
連接斷開
if (e->events & EPOLLRDHUP) {// 讀端關閉close_read(fd);close(fd);
}
if (e->events & EPOLLHUP) {// 讀寫端都關閉close(fd);
}
數(shù)據(jù)到達
// reactor 要用非阻塞io
// select
if (e->events & EPOLLIN) {while (1) {int n = read(fd, buf, sz);if (n < 0) {if (errno == EINTR)continue;if (errno == EWOULDBLOCK)break;close(fd);} else if (n == 0) {close_read(fd);// close(fd);}// 業(yè)務邏輯了}
}
數(shù)據(jù)發(fā)送完畢
int n = write(fd, buf, dz);
if (n == -1) {if (errno == EINTR)continue;if (errno == EWOULDBLOCK) {struct epoll_event ev;ev.events = EPOLLOUT;epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);return;}close(fd);
}
// ...
if (e->events & EPOLLOUT) {int n = write(fd, buf, sz);//...if (n == sz) {epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);}
}
reactor 應用:后續(xù)補充源碼解析
??The reactor design pattern is an event handling pattern
(事件處理模式)for handling service requests delivered
??concurrently to a service handler by one or more inputs
(處理一個或多個并發(fā)傳遞到服務端的服務請求). The service
??handler then demultiplexes the incoming requests and
dispatches them synchronously (同步)to the associated
request handlers.