揭秘網絡編程:同步與異步IO模型的實戰演練

摘要

? 在網絡編程領域,同步(Synchronous)、異步(Asynchronous)、阻塞(Blocking)與非阻塞(Non-blocking)IO模型是核心概念。盡管這些概念在多篇文章中被廣泛討論,它們的抽象性使得徹底理解并非易事。本文旨在通過具體的實驗案例,將這些抽象概念具體化,以助于讀者構建清晰的理解框架。

概念
IO 復用到底復用了什么

? IO 的類型有網絡 IO、磁盤 IO。我們可以把標準輸入、套接字等都看做 I/O 的一路,多路復用的意思,就是在任何一路 I/O 有“事件”發生的情況下,通知應用程序去處理相應的 I/O 事件,這樣我們的程序就變成了“多面手”,在同一時刻仿佛可以處理多個 I/O 事件。

? IO 事件類型

  1. 標準輸入文件描述符準備好可以讀。
  2. 監聽套接字準備好,新的連接已經建立成功。
  3. 已連接套接字準備好可以寫。
  4. 如果一個 I/O 事件等待超過了 10 秒,發生了超時事件。
IO 模型

? 阻塞 IO

在這里插入圖片描述

? 阻塞 IO 模型,當我們調用 recvfrom 讀取數據時,只用等數據完全準備好,然后應用程序把數據從內核態拷貝到應用空間,程序才會返回,否則從調用方視角來看程序將會一直阻塞在 recvfrom 上。

? 非阻塞 IO

在這里插入圖片描述

? 非阻塞IO場景發起 recvfrom 后,在內核數據沒準備好的情況下會返回 EWOULDBLOCK,EAGAIN 錯誤,所以調用方需要不斷的輪訓獲取數據結果。非阻塞 I/O 可以使用在 read、write、accept、connect 等多種不同的場景,在非阻塞 I/O 下,使用輪詢的方式引起 CPU 占用率高,所以一般將非阻塞 I/O 和 I/O 多路復用技術 select、poll 等搭配使用,在非阻塞 I/O 事件發生時,再調用對應事件的處理函數。這種方式,極大地提高了程序的健壯性和穩定性,是 Linux 下高性能網絡編程的首選。

非阻塞 IO Write 流程
/* 向文件描述符 fd 寫入 n 字節數 */
ssize_t writen(int fd, const void * data, size_t n)
{size_t      nleft;ssize_t     nwritten;const char  *ptr;ptr = data;nleft = n;// 如果還有數據沒被拷貝完成,就一直循環while (nleft > 0) {if ( (nwritten = write(fd, ptr, nleft)) <= 0) {/* 這里 EINTR 是非阻塞 non-blocking 情況下,通知我們再次調用 write() */if (nwritten < 0 && errno == EINTR)nwritten = 0;      elsereturn -1;         /* 出錯退出 */}/* 指針增大,剩下字節數變小 */nleft -= nwritten;ptr   += nwritten;}return n;
}
  1. nleft 標記剩余寫入數據
  2. while 循環一直寫入直到 nleft == 0
  3. write 失敗后,如果是非阻塞將會返回 EINTR 錯誤,說明數據還未準備好,這是我們將 nwritten 置為0
  4. write 成功則說明內核 socket 緩沖有空間了,不斷寫入值直到 nleft == 0

? IO 復用

? IO 復用不同于非阻塞IO的地方在于,IO 復用是在內核態實現了輪訓,相比應用層實現少了很多系統調用(系統調用成本很高)

Read 和 Write 非阻塞模式對比

在這里插入圖片描述

? 圖源-網絡編程實戰

案例
使用Select與非阻塞IO實現高效網絡通信
#define MAX_LINE 1024
#define FD_INIT_SIZE 128char rot13_char(char c) {if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))return c + 13;else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))return c - 13;elsereturn c;
}// 數據緩沖區
struct Buffer {int connect_fd;  // 連接字char buffer[MAX_LINE];  // 實際緩沖size_t writeIndex;      // 緩沖寫入位置size_t readIndex;       // 緩沖讀取位置int readable;           // 是否可以讀
};struct Buffer *alloc_Buffer() {struct Buffer *buffer = malloc(sizeof(struct Buffer));if (!buffer)return NULL;buffer->connect_fd = 0;buffer->writeIndex = buffer->readIndex = buffer->readable = 0;return buffer;
}void free_Buffer(struct Buffer *buffer) {free(buffer);
}int onSocketRead(int fd, struct Buffer *buffer) {char buf[1024];int i;ssize_t result;// 循環讀取數據直到讀完while (1) {result = recv(fd, buf, sizeof(buf), 0);if (result <= 0)break;for (i = 0; i < result; ++i) {if (buffer->writeIndex < sizeof(buffer->buffer))buffer->buffer[buffer->writeIndex++] = rot13_char(buf[i]);if (buf[i] == '\n') {buffer->readable = 1;  // 緩沖區可以讀}}}if (result == 0) {return 1;} else if (result < 0) {if (errno == EAGAIN)return 0;return -1;}return 0;
}int onSocketWrite(int fd, struct Buffer *buffer) {while (buffer->readIndex < buffer->writeIndex) {ssize_t result = send(fd, buffer->buffer + buffer->readIndex, buffer->writeIndex - buffer->readIndex, 0);if (result < 0) {if (errno == EAGAIN)return 0;return -1;}buffer->readIndex += result;}if (buffer->readIndex == buffer->writeIndex)buffer->readIndex = buffer->writeIndex = 0;buffer->readable = 0;return 0;
}int main(int argc, char **argv) {int listen_fd;int i, maxfd;struct Buffer *buffer[FD_INIT_SIZE];for (i = 0; i < FD_INIT_SIZE; ++i) {buffer[i] = alloc_Buffer();}// 設置 非 阻塞監聽listen_fd = tcp_nonblocking_server_listen(SERV_PORT);fd_set readset, writeset, exset;FD_ZERO(&readset);FD_ZERO(&writeset);FD_ZERO(&exset);while (1) {maxfd = listen_fd;FD_ZERO(&readset);FD_ZERO(&writeset);FD_ZERO(&exset);// listener 加入 readsetFD_SET(listen_fd, &readset);for (i = 0; i < FD_INIT_SIZE; ++i) {if (buffer[i]->connect_fd > 0) {if (buffer[i]->connect_fd > maxfd)maxfd = buffer[i]->connect_fd;FD_SET(buffer[i]->connect_fd, &readset);if (buffer[i]->readable) {FD_SET(buffer[i]->connect_fd, &writeset);}}}if (select(maxfd + 1, &readset, &writeset, &exset, NULL) < 0) {error(1, errno, "select error");}if (FD_ISSET(listen_fd, &readset)) {printf("listening socket readable\n");// sleep 模擬處理延時sleep(5);struct sockaddr_storage ss;socklen_t slen = sizeof(ss);// 如果是阻塞 IO 由于超時原因客戶端斷開連接,此時服務端的連接也失效,加入一直沒有請求進來// 將會一直阻塞在 accept 這里。如果是異步IO accept 將會立刻返回,但我們要處理好 accept 的// 異常情況int fd = accept(listen_fd, (struct sockaddr *) &ss, &slen);if (fd < 0) {error(1, errno, "accept failed");} else if (fd > FD_INIT_SIZE) {error(1, 0, "too many connections");close(fd);} else {// 把連接套接字設置為非阻塞make_nonblocking(fd);if (buffer[fd]->connect_fd == 0) {buffer[fd]->connect_fd = fd;} else {error(1, 0, "too many connections");}}}for (i = 0; i < maxfd + 1; ++i) {int r = 0;if (i == listen_fd)continue;if (FD_ISSET(i, &readset)) {r = onSocketRead(i, buffer[i]);}if (r == 0 && FD_ISSET(i, &writeset)) {r = onSocketWrite(i, buffer[i]);}if (r) {buffer[i]->connect_fd = 0;close(i);}}}
}
  1. 調用 fcntl 將監聽套接字設置為非阻塞。
  2. 行調用 select 進行 I/O 事件分發處理
  3. 把accept的連接套接字設置為非阻塞的
  4. 處理連接套接字上的 I/O 讀寫事件,抽象了一個 Buffer 對象,Buffer 對象使用了 readIndex 和 writeIndex 分別表示當前緩沖的讀寫位置。
結尾

文章總結了同步、異步、阻塞與非阻塞IO模型的關鍵概念,并通過對Select與非阻塞IO的案例分析,展示了這些概念在實際編程中的應用。希望讀者通過本文能夠獲得對網絡編程中IO模型的深入理解,并指導實踐中的應用

Reference
  1. http://www.pandademo.com/2016/11/linux-kernel-select-source-dissect/
  2. https://www.jianshu.com/p/95b50b026895
  3. https://www.zhihu.com/question/19732473
  4. https://tubetrue01.github.io/articles/2021/08/16/c_unix/Socket(%E4%BA%8C)recv%E4%B8%8Esend%E5%87%BD%E6%95%B0/
  5. https://time.geekbang.org/column/intro/100032701
  6. https://github.com/froghui/yolanda

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/17377.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/17377.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/17377.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

在React中使用Sass實現Css樣式管理-10

0. 什么是Sass Sass(Syntactically Awesome Stylesheets)是一個 CSS 預處理器&#xff0c;是 CSS 擴展語言&#xff0c;可以幫助我們減少 CSS 重復的代碼&#xff0c;節省開發時間&#xff1a; Sass 引入合理的樣式復用機制&#xff0c;可以節約很多時間來重復。支持變量和函…

【HM】簡單說明白:裝飾器@State、@Prop、@Link、@Provide、@Consume修飾變量,@Watch監聽變量狀態發生變化

首先要明白什么是“狀態變量”&#xff1f;即被狀態裝飾器&#xff08;State、Prop、Link、Provide、Consume&#xff09;修飾的變量&#xff0c;比如 State str : string; str就是狀態變量。狀態變量值的改變會引起UI界面重新渲染。 State State裝飾的變量&#xff0c;是私…

C++之“流”-第2課-C++和C標準輸入輸出同步

為什么C和C的標準輸入輸出不同步時&#xff0c;數據會混亂&#xff1f;同步會帶來多大性能損失&#xff1f;為什么說這個損失通常不用太在乎&#xff1f; 0. 課堂視頻 C之“流”-第2課&#xff1a;和C輸入輸出的同步 1. 理解cin和cout的類型與創建過程 std::cout 是std::ostre…

Ubuntu系統Discover軟件中心簡介

Discover軟件中心是Ubuntu操作系統中默認的軟件管理工具&#xff0c;它提供了一個圖形用戶界面(GUI)來幫助用戶瀏覽、搜索、安裝和卸載軟件包。Discover軟件中心是Ubuntu軟件中心(Ubuntu Software Center)的繼承者&#xff0c;它在Ubuntu 16.04 LTS版本中首次被引入&#xff0c…

添加、修改和刪除字典元素

自學python如何成為大佬(目錄):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 由于字典是可變序列&#xff0c;所以可以隨時在字典中添加“鍵-值對”。向字典中添加元素的語法格式如下&#xff1a; dictionary[key] value 參數…

You don‘t have enough free space或者no space left on device異常

1.磁盤空間不足 Linux安裝軟件顯示 You dont have enough free space 或者docker拉鏡像時&#xff0c;出現磁盤空間不足的情況 no space left on device 如果你是ubuntu系統。查看磁盤空間 df -h 多半是這個目錄滿了/dev/mapper/ubuntu--vg-ubuntu--lv 大多情況我們只希望擴…

學習編程對英語要求高嗎?

學習編程并不一定需要高深的英語水平。我這里有一套編程入門教程&#xff0c;不僅包含了詳細的視頻講解&#xff0c;項目實戰。如果你渴望學習編程&#xff0c;不妨點個關注&#xff0c;給個評論222&#xff0c;私信22&#xff0c;我在后臺發給你。 雖然一些編程資源和文檔可能…

typora自動生成標題序號(修改V1.0)

目錄 帶序號效果圖 解決方法 帶序號效果圖 解決方法 1.進入文件夾&#xff1a;文件–>偏好設置–>外觀–>主題–>打開主題文件夾 2.如果沒有base.user.css文件&#xff0c;新建一個。如果有直接用記事本打開&#xff0c;把下面代碼拷貝進去保存。 /** initiali…

【JUC編程】-多線程和CompletableFuture的使用

多線程編程 文章目錄 多線程編程[toc]引言創建多線程的方式繼承Thread類實現Runnable接口實現Callable接口Callable和Runnable的區別 Lambda表達式 線程的實現原理Future&FutureTask具體使用submit方法Future到FutureTask類Future注意事項局限性 CompletionService引言使用…

第八大奇跡

目錄 題目描述 輸入描述 輸出描述 輸入輸出樣例 示例 輸入 輸出 運行限制 原題鏈接 代碼思路 題目描述 在一條 R 河流域&#xff0c;繁衍著一個古老的名族 Z。他們世代沿河而居&#xff0c;也在河邊發展出了璀璨的文明。 Z 族在 R 河沿岸修建了很多建筑&#xff0c…

java如何向數組中插入元素

java的數組是不可改變的&#xff0c;因此如果要向數組中插入新的元素&#xff0c;需要新建一個數組&#xff0c;新的數組元素個數減去老數組元素個數的差大于等于要插入新的元素數量。 假如說要插入一個數組元素&#xff0c;需要把新元素插入到中間&#xff0c;把新的數組分為…

Vue組件通訊?組件中通過 provide 來提供變量,然后在?組件中通過 inject 來注?變量例子

在Vue中&#xff0c;provide 和 inject 主要用于依賴注入&#xff0c;允許祖先組件向其所有子孫組件提供一個依賴&#xff0c;而不論組件層次有多深。這在開發高階插件/組件庫時特別有用。 以下是一個簡單的例子&#xff0c;演示了如何在父組件中使用 provide 提供變量&#x…

軟件測試面試題(八)

一&#xff1a;TestDirector有哪些功能&#xff0c;如何對軟件測試過程進行管理&#xff1f; 需求管理 定義測試范圍 定義需求樹 描述需求樹的功能點 測試計劃 定義測試目標和測試策略 分解應用程序&#xff0c;建立測試計劃樹 確定每個功能點的測試方法 將每個功能點連接…

Ps 濾鏡:消失點

Ps菜單&#xff1a;濾鏡/消失點 Filter/Vanishing Point 快捷鍵&#xff1a;Ctrl Alt V 兩條平行的鐵軌或兩排樹木連線相交于很遠很遠的某一點&#xff0c;這點在透視圖中叫做“消失點”&#xff0c;也稱為“滅點”。 消失點 Vanishing Point濾鏡主要用于在圖像中處理具有透視…

C++入門3——類與對象(2)

1.類的6個默認成員函數 如果一個類中什么成員都沒有&#xff0c;簡稱為空類。可是空類中真的什么都沒有嗎&#xff1f; 其實并不是的&#xff0c;任何類在什么都不寫時&#xff0c;編譯器會自動生成以下6個默認成員函數。 默認成員函數&#xff1a;用戶沒有顯式實現&#xf…

libmodbus開發庫介紹

目錄 功能概要源碼獲取源碼內容結構源碼與移植 功能概要 libmodbus是一個免費的跨平臺支持RTU和TCP的Modbus庫&#xff0c;遵循LGPL V2.1協議。libmodbus支持Linux、Mac Os X、FreeBSD、QNX和Windows等操作系統。libmodbus可以向符合Modbus協議的設備發送和接收數據&#xff0…

vector的reverse和resize區別

一 代碼 #include "stdafx.h" #include <iostream> #include <vector> using namespace std;class TEST{ public:TEST(){std::cout << "construct t" << std::endl;} };int main() {std::cout << "hello,world" …

《Python偵探手冊:用正則表達式破譯文本密碼》

在這個信息爆炸的時代&#xff0c;每個人都需要一本偵探手冊。阿佑今天將帶你深入Python的正則表達式世界&#xff0c;教你如何像偵探一樣&#xff0c;用代碼破解文本中的每一個謎題。從基礎的字符匹配到復雜的數據清洗&#xff0c;每一個技巧都足以讓你在文本處理的領域中成為…

【一站式學會Kotlin】第十三節:kotlin語言中的解構

作者介紹: 百度資深Android工程師T6,在百度任職7年半。 目前:成立趙小灰代碼工作室,歡迎大家找我交流Android、微信小程序、鴻蒙項目。= 一:通俗易懂的人工智能教程:https://www.captainbed.cn/nefu/ 點一下,打開新世界的大門。 二:【一站式學會Kotlin】免費領取:作者…

SQLSyntaxErrorException: FUNCTION dbname.to_timestamp does not exist

由于MySQL數據庫高版本&#xff08;如8.x&#xff09;中有to_timestamp(&#xff09;函數&#xff0c;低版本中&#xff08;如5.7.x&#xff09;沒有這個函數&#xff0c;服務運行報錯。 自己創建函數實現功能&#xff0c;創建語句如下&#xff1b; DELIMITER // CREATE FUN…