簡介
Socket(套接字)是計算機網(wǎng)絡中的一套編程接口,是網(wǎng)絡編程的核心,它將復雜的網(wǎng)絡協(xié)議封裝為簡單的API,是應用層(HTTP)與傳輸層(TCP)之間的橋梁。
應用程序通過調(diào)用Socket API,比如connect、send、recv,無需處理IP包封裝,路由選擇等復雜網(wǎng)絡操作,屏蔽底層細節(jié)
將網(wǎng)絡通信簡化為建立連接-數(shù)據(jù)接收-數(shù)據(jù)發(fā)送-連接斷開
,降低了開發(fā)復雜度。

FD&Handle
- FD
文件描述符,在linux系統(tǒng)中,一切皆文件
,它是內(nèi)核為了管理已打開的文件,而給每個進程維護的一個文件描述符表,而FD就是一個文件的索引。 - Handle
而在windows平臺下,這個概念被稱為Handle(句柄)
,都為應用程序提供了一種統(tǒng)一的方式來訪問和操作資源,隱藏了底層資源管理的復雜性。
FD主要用于標識文件、套接字、管道等輸入輸出資源;而Handle的應用范圍更廣,除了文件和網(wǎng)絡資源外,還可以用于標識窗口、進程、線程、設備對象等各種系統(tǒng)資源。
Socket 網(wǎng)絡模型
BIO,Blocking I/O
BIO 是最傳統(tǒng)的 I/O 模型,其核心特征是一個連接一個線程
,線程在讀取/寫入時會阻塞,直到I/O操作完成。
private static Socket _server;
private static byte[] _buffer = new byte[1024 * 4];
static void Main(string[] args)
{
_server=new Socket(AddressFamily.InterNetwork,SocketType.Stream, ProtocolType.Tcp);
_server.Bind(new IPEndPoint(IPAddress.Any, 6666));
_server.Listen();
while (true)
{
var client = _server.Accept();
Console.WriteLine($"Client {client.RemoteEndPoint} connect. ");
var messageCount = client.Receive(_buffer);
var message = Encoding.UTF8.GetString(_buffer, 0, messageCount);
Console.WriteLine($"Client {client.RemoteEndPoint} Say:{message}");
}
}
從代碼中可以看出,有兩個地方阻塞,一是Accept(),二是Receive(),如果客戶端一直不發(fā)送數(shù)據(jù),那么線程會一直阻塞在Receive()上,也不會接受其它客戶端的連接。

C10K問題
有聰明的小伙伴會想到,我可以利用多線程來處理Receive(),這樣就服務端就可以接受其它客戶端的連接了。
internal class Program
{
private static Socket _server;
private static byte[] _buffer = new byte[1024 * 4];
static void Main(string[] args)
{
_server=new Socket(AddressFamily.InterNetwork,SocketType.Stream, ProtocolType.Tcp);
_server.Bind(new IPEndPoint(IPAddress.Any, 6666));
_server.Listen();
while (true)
{
var client = _server.Accept();
Console.WriteLine($"Client {client.RemoteEndPoint} connect. ");
Task.Run(() => HandleClient(client));
}
}
static void HandleClient(Socket client)
{
while (true)
{
var messageCount = client.Receive(_buffer);
var message = Encoding.UTF8.GetString(_buffer, 0, messageCount);
Console.WriteLine($"Client {client.RemoteEndPoint} Say:{message}");
}
}
}
當給客戶端建立好連接后,會啟用一個新的線程來單獨處理Receive(),避免了主線程阻塞。
但有一個嚴重的缺陷,就是當一萬個客戶端同時連接,服務端要創(chuàng)建一萬個線程來接。一萬個線程帶來的CPU上下文切換與內(nèi)存成本,非常容易會拖垮服務器。這就是C10K問題來由來。
因此,BIO的痛點在于:
- 高并發(fā)下資源耗盡
當連接數(shù)激增時,線程數(shù)量呈線性增長(如 10000 個連接對應 10000 個線程),導致內(nèi)存占用過高、上下文切換頻繁,系統(tǒng)性能急劇下降。 - 阻塞導致效率低下
線程在等待 IO 時無法做其他事情,CPU 利用率低。
NIO,Non-Blocking I/O
為了解決此問題,需要跪舔操作系統(tǒng),為用戶態(tài)程序提供一個真正非阻塞的Accept/Receive的函數(shù)
。
該函數(shù)的效果應該是,當沒有新連接/新數(shù)據(jù)到達時,不阻塞線程。而是返回一個特殊標識
,來告訴線程沒有活干。
Java 1.4 引入 NIO,C# 通過Begin/End異步方法或SocketAsyncEventArgs實現(xiàn)類似邏輯。
internal class Program
{
private static Socket _server;
private static byte[] _buffer = new byte[1024 * 4];
private static readonly List<Socket> _clients = new List<Socket>();
static void Main(string[] args)
{
_server=new Socket(AddressFamily.InterNetwork,SocketType.Stream, ProtocolType.Tcp);
_server.Bind(new IPEndPoint(IPAddress.Any, 6666));
_server.Listen();
_server.Blocking = false;
while (true)
{
try
{
var client = _server.Accept();
_clients.Add(client);
Console.WriteLine($"Client {client.RemoteEndPoint} connect. ");
}
catch (SocketException ex) when(ex.SocketErrorCode==SocketError.WouldBlock)
{
}
HandleClient();
}
}
static void HandleClient()
{
foreach (var client in _clients.ToList())
{
try
{
var messageCount = client.Receive(_buffer, SocketFlags.None);
var message = Encoding.UTF8.GetString(_buffer, 0, messageCount);
Console.WriteLine($"Client {client.RemoteEndPoint} Say:{message}");
}
catch (SocketException ex) when (ex.SocketErrorCode == SocketError.WouldBlock)
{
}
}
}
}
通過NIO,我們可以非常驚喜的發(fā)現(xiàn)。我們僅用了一個線程就完成對客戶端的連接與監(jiān)聽
,相對BIO有了質(zhì)的變化。
當數(shù)據(jù)未就緒時(內(nèi)核緩沖區(qū)無數(shù)據(jù)),非阻塞模式下的Accept/Receive會立即返回WouldBlock異常(或-1);當數(shù)據(jù)就緒時,調(diào)用會立即返回讀取的字節(jié)數(shù)(>0),不會阻塞線程。數(shù)據(jù)從內(nèi)核緩沖區(qū)到用戶緩沖區(qū)的拷貝由 CPU 同步完成,屬于正常 IO 操作流程,不涉及線程阻塞

盡管NIO已經(jīng)是JAVA世界的絕對主流,但依舊存在幾個痛點:
- 輪詢開銷
如果事件比較少,輪詢會產(chǎn)生大量空轉,CPU資源被浪費。 - 需要手動處理細節(jié)
比如手動編寫捕獲when (ex.SocketErrorCode == SocketError.WouldBlock)來識別狀態(tài),
需要手動處理TPC粘包,以及各種異常處理。
AIO,Asynchronous I/O
AIO作為大魔王與終極優(yōu)化
,實現(xiàn)了真正的異步操作,當發(fā)起IO請求后,內(nèi)核完全接管IO處理,完成后通過回調(diào)或者事件來通知程序,開發(fā)者無需關心緩沖區(qū)管理、事件狀態(tài)跟蹤或輪詢開銷。
Java 7 引入 NIO.2(AIO),C# 通過IOCP+Async來實現(xiàn)
internal class Program
{
private static Socket _server;
private static Memory<byte> _buffer = new byte[1024 * 4];
private static readonly List<Socket> _clients = new List<Socket>();
static async Task Main(string[] args)
{
_server=new Socket(AddressFamily.InterNetwork,SocketType.Stream, ProtocolType.Tcp);
_server.Bind(new IPEndPoint(IPAddress.Any, 6666));
_server.Listen();
while (true)
{
var client = await _server.AcceptAsync();
HandleClientAsync(client);
}
}
private static async Task HandleClientAsync(Socket client)
{
var messageCount = await client.ReceiveAsync(_buffer);
var message = Encoding.UTF8.GetString(_buffer.ToArray(), 0, messageCount);
Console.WriteLine($"Client {client.RemoteEndPoint} Say:{message}");
}
}

Linux/Windows對模型的支持

NIO的改良,IO multiplexing
I/O Multiplexing 是一種高效處理多個I/O操作的技術,核心思想是通過少量線程
管理多個I/O流,避免因為單個I/O阻塞導致整體服務性能下降。
它通過事件機制(可讀,可寫,異常)
監(jiān)聽多個I/O源,當某個I/O流可操作時,才對其執(zhí)行讀寫操作,從而實現(xiàn)單線程處理多連接
的高效模型。
IO 多路復用本質(zhì)是NIO的改良
select/poll
參考上面的代碼,HandleClient方法中,我們遍歷了整個_Clients,用以尋找客戶端的Receive。
同樣是C10K問題,如果我們1萬,甚至100萬個客戶端連接。那么遍歷的效率太過低下。尤其是每調(diào)用一次Receive都是一次用戶態(tài)到內(nèi)核態(tài)
的切換。
那么,如果讓操作系統(tǒng)告訴我們,哪些連接是可用的
,我們就避免了在用戶態(tài)遍歷
,從而提高性能。

static void HandleClientSelect()
{
var clients = _clients.ToList();
var readyClients= Socket.Select(clients);
foreach (var client in readyClients)
{
if (client.IsReady)
{
var messageCount = client.Receive(_buffer, SocketFlags.None);
var message = Encoding.UTF8.GetString(_buffer, 0, messageCount);
Console.WriteLine($"Client {client.RemoteEndPoint} Say:{message}");
}
else
{
break;
}
}
}
通過監(jiān)聽一組文件描述符(File Descriptor, FD)的可讀、可寫或異常狀態(tài),當其中任意狀態(tài)滿足時,內(nèi)核返回就緒的 FD 集合。用戶需遍歷所有 FD 判斷具體就緒的 I/O 操作。
select模型受限于系統(tǒng)默認值,最大只能處理1024個連接。poll模型通過結構體數(shù)組替代select位圖的方式,避免了數(shù)量限制,其它無區(qū)別。
epoll
作為NIO的終極解決方案
,它解決了什么問題?
- 調(diào)用select需要傳遞整個List
var readyClients= Socket.Select(clients);
如果list中有10W+,那么這個copy的成本會非常高 - select依舊是線性遍歷
在內(nèi)核層面依舊是遍歷整個list,尋找可用的client,所以時間復雜度不變O(N),只是減少了從用戶態(tài)切換到內(nèi)核態(tài)的次數(shù)而已 - 僅僅對ready做標記,并不減少返回量
select僅僅返回就緒的數(shù)量,具體是哪個就緒,還要自己遍歷一遍。
所以epoll模型主要主要針對這三點,做出了如下優(yōu)化:
- 通過mmap,zero copy,減少數(shù)據(jù)拷貝
- 不再通過輪詢方式,而是通過異步事件通知喚醒,內(nèi)部使用紅黑樹來管理fd/handle
- 喚醒后,僅僅返回有變化的fd/handle,用戶無需遍歷整個list
基于事件驅(qū)動(Event-Driven)機制,內(nèi)核維護一個 FD 列表,通過epoll_ctl添加 / 刪除 FD 監(jiān)控,epoll_wait阻塞等待就緒事件。就緒的 FD 通過事件列表返回,用戶僅需處理就緒事件對應的 FD。

?點擊查看代碼
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#define SEVER_PORT 6666
#define BUFFER_SIZE 1024
#define MAX_EVENTS 10
#define handle_error(cmd,result)\
if(result<0){ \
perror(cmd); \
exit(EXIT_FAILURE); \
} \
char *read_buf=NULL;
char *write_buf=NULL;
void init_buf()
{
read_buf=malloc(sizeof(char)* BUFFER_SIZE);
if(!read_buf)
{
printf("讀緩存創(chuàng)建異常,斷開連接\n");
exit(EXIT_FAILURE);
}
write_buf=malloc(sizeof(char)* BUFFER_SIZE);
if(!write_buf)
{
printf("寫緩存創(chuàng)建異常,斷開連接\n");
exit(EXIT_FAILURE);
}
memset(read_buf,0,BUFFER_SIZE);
memset(write_buf,0,BUFFER_SIZE);
}
void clear_buf(char *buf)
{
memset(buf,0,BUFFER_SIZE);
}
void set_nonblocking(int sockfd)
{
int opts=fcntl(sockfd,F_GETFL);
if(opts<0)
{
perror("fcntl(F_GETFL)");
exit(EXIT_FAILURE);
}
opts|=O_NONBLOCK;
int res=fcntl(sockfd,F_SETFL,opts);
if(res<0)
{
perror("fcntl(F_GETFL)");
exit(EXIT_FAILURE);
}
}
int main(int argc, char const *argv[])
{
init_buf();
int sockfd,client_fd,temp_result;
struct sockaddr_in server_addr,client_addr;
memset(&server_addr,0,sizeof(server_addr));
memset(&client_addr,0,sizeof(client_addr));
server_addr.sin_family=AF_INET;
server_addr.sin_addr.s_addr=htonl(INADDR_ANY);
server_addr.sin_port=htons(SEVER_PORT);
sockfd=socket(AF_INET,SOCK_STREAM,0);
handle_error("socket",sockfd);
temp_result=bind(sockfd,(struct sockaddr *)&server_addr,sizeof(server_addr));
handle_error("bind",temp_result);
temp_result=listen(sockfd,128);
handle_error("listen",temp_result);
set_nonblocking(sockfd);
int epollfd,nfds;
struct epoll_event ev,events[MAX_EVENTS];
epollfd=epoll_create1(0);
handle_error("epoll_create1",epollfd);
ev.data.fd=sockfd;
ev.events=EPOLLIN;
temp_result=epoll_ctl(epollfd,EPOLL_CTL_ADD,sockfd,&ev);
handle_error("epoll_ctl",temp_result);
socklen_t client_addr_len=sizeof(client_addr);
while (1)
{
nfds=epoll_wait(epollfd,events,MAX_EVENTS,-1);
handle_error("epoll_wait",nfds);
for (int i = 0; i < nfds; i++)
{
if(events[i].data.fd==sockfd)
{
client_fd=accept(sockfd,(struct sockaddr *)&client_addr,&client_addr_len);
handle_error("accept",client_fd);
set_nonblocking(client_fd);
printf("與客戶端from %s at PORT %d 文件描述符 %d 建立連接\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port),client_fd);
ev.data.fd=client_fd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epollfd,EPOLL_CTL_ADD,client_fd,&ev);
}
else if(events[i].events&EPOLLIN)
{
int count=0,send_count=0;
client_fd=events[i].data.fd;
while ((count=recv(client_fd,read_buf,BUFFER_SIZE,0)>0))
{
printf("receive message from client_fd: %d: %s \n",client_fd,read_buf);
clear_buf(read_buf);
strcpy(write_buf,"receive~\n");
send_count=send(client_fd,write_buf,strlen(write_buf),0);
handle_error("send",send_count);
clear_buf(write_buf);
}
if(count==-1&&errno==EAGAIN)
{
printf("當前批次已經(jīng)讀取完畢。\n");
}
else if(count==0)
{
printf("客戶端client_fd:%d請求關閉連接......\n",client_fd);
strcpy(write_buf,"recevie your shutdown signal 收到你的關閉信號\n");
send_count=send(client_fd,write_buf,strlen(write_buf),0);
handle_error("send",send_count);
clear_buf(write_buf);
epoll_ctl(epollfd,EPOLL_CTL_DEL,client_fd,NULL);
printf("釋放client_fd:%d資源\n",client_fd);
shutdown(client_fd,SHUT_WR);
close(client_fd);
}
}
}
}
printf("服務端關閉后資源釋放\n");
close(epollfd);
close(sockfd);
free(read_buf);
free(write_buf);
return 0;
}
IOCP
由于Windows并不開源,關于IOCP的資料不多,可以參考此文。
IOCP:Input/Output Completion Port,I/O完成端口
.NET Core在Windows下基于IOCP,在Linux下基于epoll,在macOS中基于kqueue
https://www.cnblogs.com/lmy5215006/p/18571532
理論與現(xiàn)實的割裂
從上面的理論可以看出,AIO似乎是版本答案
,在C#中,AIO已經(jīng)充斥著每一個角落,但在JAVA的世界中,更加主流的是NIO,這是為什么呢?
1. Linux的支持不足
Linux 內(nèi)核直到 3.11 版本(2013 年)才支持真正的異步 IO(io_uring),從而間接影響了JAVA的發(fā)展,Java的 AIO直到 2011 年Java 7才正式發(fā)布,而其前一代 NIO已發(fā)展近 10 年。
而Windows的IOCP在Windows NT 4.0 (1996年)就登上了歷史舞臺,加上C#起步較晚,沒有歷史包袱,所以對AIO支持力度更大,尤其是2012年發(fā)布了async/await異步模型后,解決了回調(diào)地獄,實現(xiàn)了1+1>3的效果。
2. JAVA的路徑依賴
NIO生態(tài)過于強大,尤其是以Netty/Redis為首的經(jīng)典實現(xiàn),實在是太香了!
3. 理論優(yōu)勢并未轉換為實際收益
AIO的性能在特定場景(如超大規(guī)模文件讀寫、長連接低活躍)下可能優(yōu)于NIO,但在互聯(lián)網(wǎng)場景中,NIO的足夠高效,比如HTTP請求,AIO的異步回調(diào)優(yōu)勢相對輪詢并不明顯。
維度 | Java AIO未普及的原因 | C# AIO普及的原因 |
---|
歷史發(fā)展 | NIO早于AIO 9年推出,生態(tài)成熟;AIO定位模糊,未解決NIO的核心痛點(如編程復雜度) | AIO與async/await 同步推出,解決了異步編程的“回調(diào)地獄”,成為高并發(fā)編程的默認選擇 |
跨平臺 | 需適配多系統(tǒng)異步機制(如Linux的epoll 、macOS的kqueue ),實際性能提升有限 | 早期綁定Windows IOCP,性能穩(wěn)定;跨平臺后對AIO需求不迫切 |
生態(tài) | Netty等NIO框架統(tǒng)治市場,切換AIO成本高 | 缺乏NIO統(tǒng)治級框架,AIO通過async/await 成為原生選擇 |
開發(fā)者習慣 | NIO代碼雖復雜,但通過框架封裝已足夠易用;AIO回調(diào)模式學習成本更高 | async/await 語法糖讓異步代碼接近同步,開發(fā)者更易接受 |
性能場景 | 大多數(shù)場景下NIO已足夠高效,AIO的優(yōu)勢未顯著體現(xiàn) | Windows IOCP場景下AIO性能優(yōu)勢明顯,且覆蓋主流企業(yè)級需求 |
說人話就是,Netty太香了,完全沒動力切換成AIO,順帶吐槽C#中沒有類似的框架。dotnetty不算,已經(jīng)停止更新了。
轉自https://www.cnblogs.com/lmy5215006/p/18877083
該文章在 2025/6/3 10:23:22 編輯過