最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網 會員登陸 & 注冊

IO多路復用是什么?如何設計一個高性能服務器?

2023-07-12 00:25 作者:欞傀-ghost_him  | 我要投稿

筆記同步到了我的個人網站上,b站好像不支持markdown語法,所以為了一個更好的體驗,歡迎前來圍觀(?′?`?):http://www.ghost-him.com/posts/5203630b/


文中代碼采用 c 風格,linux 下的系統(tǒng)調用的名稱。只采用偽代碼的形式編寫,無法運行。


<!-- more -->


## 阻塞 IO 形式


最簡單的服務器形式


偽代碼形式:


```cpp

server_fd = socket(); // 創(chuàng)建一個socket

bind(server_fd, "0.0.0.0", 8080); // 將當前的socket綁定到一個指定的地址和端口

listen(server_fd) // 監(jiān)聽連接請求


while (1) {

client_fd = accept(server_fd); // 接受連接請求

if (read(client_fd, buff)) { // 從client_fd中讀取數據,并將數據存放到buff中

handler(buff) // 處理當前buff中的數據

} else {

close(client_fd) // 如果已經讀完了,則關閉連接

}

}

```


特點:一次只可以處理一個連接,處理完以后才可以接受下一個連接的請求。

原因:這里的 `accept` 函數和 `read` 函數都是阻塞的。


如果要支持多個客戶端的連接請求,那么可以對代碼做一些改進:


```cpp

server_fd = socket(); // 創(chuàng)建一個socket

bind(server_fd, "0.0.0.0", 8080); // 將當前的socket綁定到一個指定的地址和端口

listen(server_fd) // 監(jiān)聽連接請求


while (1) {

client_fd = accept(server_fd); // 接受連接請求

fds.add(client_fd); // 將當前的新連接的fd添加到fds的連接數組中

for (fd : fds) { // 遍歷當前fds中的所有的fd

if (read(fd, buff)) { // 從client_fd中讀取數據,并將數據存放到buff中

handler(buff) // 處理當前buff中的數據

} else {

close(fd) // 如果已經讀完了,則關閉連接

}

}

}

```


代碼存在的問題:

1. 如果在等待新的連接時,無法處理已經連接上的請求。

2. 同理,如果在等待已經連接上的 fd 傳輸數據時,無法連接新的請求。

3. 如果在遍歷 fds 時,如果其中的一個 fd 一直沒有傳輸數據過來,那么整個程序會卡死(一直阻塞在 read 函數)。


產生問題的原因:`read` 函數和 `accept` 函數相互影響導致的。


解決辦法:引入多線程。


## 阻塞 IO+多線程


```cpp

server_fd = socket(); // 創(chuàng)建一個socket

bind(server_fd, "0.0.0.0", 8080); // 將當前的socket綁定到一個指定的地址和端口

listen(server_fd) // 監(jiān)聽連接請求


while (1) {

client_fd = accept(server_fd); // 接受連接請求

pthread_create(client_fd){ // 創(chuàng)建一個新的線程

while(1) {

if (read(client_fd, buff)) { // 從client_fd中讀取數據,并將數據存放到buff中

handler(buff) // 處理當前buff中的數據

} else {

close(client_fd) // 如果已經讀完了,則關閉連接

}

}

}

}

```


特點:

1. 可以實現(xiàn)一個可用的多線程 tcp 服務器,同時支持處理多個客戶端的連接請求。

2. 一個線程處理一個連接


缺點:

1. 無法處理大量連接


原因:每個線程都會占用一定的資源(時,空),所以不但可以創(chuàng)建的線程數是有限的,而且上下文切換的也會占用大量的時間,會影響處理的效率。


解決方法:使用線程池來代替大量的線程


```cpp

server_fd = socket(); // 創(chuàng)建一個socket

bind(server_fd, "0.0.0.0", 8080); // 將當前的socket綁定到一個指定的地址和端口

listen(server_fd) // 監(jiān)聽連接請求

thread_pool_create(num) // 創(chuàng)建指定數量的線程


while (1) {

client_fd = accept(server_fd); // 接受連接請求

thread_pool_get(client_fd) {?// 從線程池中獲取一個線程

while(1) {

if (read(client_fd, buff)) { // 從client_fd中讀取數據,并將數據存放到buff中

handler(buff) // 處理當前buff中的數據

} else {

close(client_fd) // 如果已經讀完了,則關閉連接

break;?// 跳出循環(huán),將線程放回線程池?

}

}

}

}

```


缺點:

1. 獲取線程 `thread_pool_get` 是一個阻塞的函數,所以會影響服務器的處理能力。

2. 在高并發(fā)的環(huán)境下,主線程會由于線程池中的線程的數量受到限制,從而無法處理新的請求(直到有舊的連接關閉,線程釋放)


原因:每個連接都要一個線程處理,而線程池中的線程是有限的,所以線程池的大小就決定了同時在線連接數的數量。


解決辦法:

1. 部署更多的服務器



## 非阻塞 IO


對于一個網絡 IO,共有兩個系統(tǒng)對象,一個是應用進程,一個是系統(tǒng)內核。當一個 read 函數發(fā)生時,會有兩個階段:

1. 等待數據準備

2. 將數據從內核拷貝到用戶空間

在阻塞 IO 模型中,只有當這兩個階段都完成了以后都會返回。


所以這里就是一個可以優(yōu)化的地方。


在非阻塞 IO 模型中,當應用線程發(fā)出 read 系統(tǒng)調用的時候,如果內核中的數據還沒有準備好,他并不會去阻塞應用的線程,而是返回一個錯誤。對于應用線程來說,發(fā)出一個 read 系統(tǒng)調用以后不需要等待,就可以得到一個結果。如果這個結果是一個錯誤,那么就說明當前還沒有準備好,于是,可以再次發(fā)送一個 read 操作。當數據已經準備好了,并且應用線程發(fā)送了一個 read 系統(tǒng)調用的時候,內核會將數據拷貝到應用進程,拷貝完以后再返回成功。


因此,對于阻塞模型與非阻塞模型來說,不同的地方在于第一階段,第二階段下,兩個模型都是一樣的。


```cpp

server_fd = socket(); // 創(chuàng)建一個socket

bind(server_fd, "0.0.0.0", 8080); // 將當前的socket綁定到一個指定的地址和端口

listen(server_fd) // 監(jiān)聽連接請求

set_non_block(server_fd) // 設置成非阻塞


while (1) {

client_fd = accept(server_fd); // 接受連接請求

if (client_fd > 0) {

set_non_block(client_fd); // 設置非阻塞模式

fds.add(client_fd); // 新的fd加入到fds中

}

for (fd : fds) { // 遍歷當前fds中的所有的fd

n = read(fd, buff); // 非阻塞的讀取數據

if (n == -1) {

continue; // 無數據可讀

} else if (n == 0) { // 連接關閉

close(fd); // 斷開連接

} else {

handler(buff) // 讀到數據邏輯處理

}

}

}

```


缺點:`while` 循環(huán)中會不斷的向系統(tǒng)詢問,系統(tǒng)的開銷很大,同時會占用大量的 cpu 資源。


## IO 多路復用


目的:避免應用線程循環(huán)檢查發(fā)起系統(tǒng)調用的開銷


原理:將需要監(jiān)聽的文件描述符,通過一個系統(tǒng) (select, poll, epoll 等)一直傳遞到內核中,由內核來監(jiān)視這些文件描述符。當其中的任意一個文件描述符發(fā)生了 I/O 事件(讀,寫,連接,關閉等),內核就會通知應用程序進行處理。


多路是指需要處理的多個連接的 I/O 事件,復用是指復用一個或少量的線程資源。I/O 多路復用就是用一個或者少量的線程資源去處理多個連接的 I/O 事件。


使用 select 函數來舉例:

```cpp

server_fd = socket();

bind(server_fd, "0,0,0,0", 8080);

listen(server_Fd);

readfds; // 待監(jiān)聽的集合

client_fds; // 連接描述符數組


while (1) {

// 清空集合

FD_ZERO(&readfds);

// 添加server_Fd 到集合中

FD_SET(server_Fd. &readfds);

// 遍歷連接fd集合

for (fd : client_fds) {

//將有效的fd添加到集合中

if (fd) {

FD_SET(fd, &readfds);

}

}


// 阻塞等待fd上的IO事件

select(fd_num, &readfds, NULL, NULL, NULL);

// 如果server_fd有事件,則有新的連接

if (FD_ISSET(server_fd, &readfds)) {

client_fd = accept(server_fd);

// 新的fd加入到數組中

client_fds.add(client_fd);

}


for (fd : client_fds) {

// 如果有IO事件

if (FD_ISSET(fd, &readfds)) {

if (read(fd, buff)) {

handler(buff);

} else {

// 連接關閉?

close(fd);

// 從集合中移除

client_fds.remove(fd);

}

}

}

}

```


優(yōu)點:避免了主動的輪詢,減少了 cpu 的占用。


缺點:

1. 每次在調用 select 函數都需要重新初始化待監(jiān)聽描述符集合

2. 每次都要將描述符集合拷貝到內核中

3. Select 返回后需要遍歷所有文件描述符,依次檢查是否就緒。(即使就一個準備好,也要遍歷全部)

4. 最多只可以監(jiān)聽 1024 個文件描述符


`poll` 對第 1 點和第 4 點做了優(yōu)化。`epoll` 對所有的缺點都進行了優(yōu)化。`epoll` 使用內核空間和用戶空間共享的內存區(qū)來傳遞文件描述符,避免了從用戶態(tài)向內核態(tài)拷貝的開銷。同時,不需要遍歷全部文件描述符,因為它只將發(fā)生變動的文件描述符返回。


**注:看評論區(qū)說 epoll 會將數據從內核拷貝到用戶空間,并不是共享內存實現(xiàn)的**

IO多路復用是什么?如何設計一個高性能服務器?的評論 (共 條)

分享到微博請遵守國家法律
手机| 岳普湖县| 简阳市| 宁强县| 新营市| 红原县| 新津县| 洪湖市| 恩平市| 阳曲县| 池州市| 通辽市| 富顺县| 邯郸县| 商丘市| 云安县| 邻水| 西盟| 凤庆县| 浮山县| 丰城市| 会昌县| 合山市| 永靖县| 犍为县| 全南县| 永登县| 乌什县| 仲巴县| 信丰县| 休宁县| 那曲县| 崇仁县| 海伦市| 诸暨市| 登封市| 南雄市| 韶关市| 尼玛县| 呼和浩特市| 金塔县|