简介

Socket(套接字)是计算机网络中的一套编程接口,是网络编程的核心,它将复杂的网络协议封装为简单的API,是应用层(HTTP)与传输层(TCP)之间的桥梁。

应用程序通过调用Socket API,比如connect、send、recv,无需处理IP包封装,路由选择等复杂网络操作,屏蔽底层细节将网络通信简化为建立连接-数据接收-数据发送-连接断开,降低了开发复杂度。

FD&Handle

  1. FD

    文件描述符,在linux系统中,一切皆文件,它是内核为了管理已打开的文件,而给每个进程维护的一个文件描述符表,而FD就是一个文件的索引。
  2. Handle

    而在windows平台下,这个概念被称为Handle(句柄),都为应用程序提供了一种统一的方式来访问和操作资源,隐藏了底层资源管理的复杂性。

FD主要用于标识文件、套接字、管道等输入输出资源;而Handle的应用范围更广,除了文件和网络资源外,还可以用于标识窗口、进程、线程、设备对象等各种系统资源。

Socket 网络模型

BIO,Blocking I/O

BIO 是最传统的 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)
{
//BIO核心,线程阻塞,等待客户端连接
var client = _server.Accept();
Console.WriteLine($"Client {client.RemoteEndPoint} connect. "); //BIO核心,线程阻塞,等待客户端发送消息
var messageCount = client.Receive(_buffer);
var message = Encoding.UTF8.GetString(_buffer, 0, messageCount);
Console.WriteLine($"Client {client.RemoteEndPoint} Say:{message}");
}
}

从代码中可以看出,有两个地方阻塞,一是Accept(),二是Receive(),如果客户端一直不发送数据,那么线程会一直阻塞在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)
{
//BIO核心,线程阻塞,等待客户端连接
var client = _server.Accept();
Console.WriteLine($"Client {client.RemoteEndPoint} connect. "); //多线程读取客户端数据,避免主线程阻塞
Task.Run(() => HandleClient(client));
}
}
static void HandleClient(Socket client)
{
while (true)
{
//BIO核心,线程阻塞,等待客户端发送消息
var messageCount = client.Receive(_buffer);
var message = Encoding.UTF8.GetString(_buffer, 0, messageCount);
Console.WriteLine($"Client {client.RemoteEndPoint} Say:{message}");
}
}
}

当给客户端建立好连接后,会启用一个新的线程来单独处理Receive(),避免了主线程阻塞。

但有一个严重的缺陷,就是当一万个客户端同时连接,服务端要创建一万个线程来接。一万个线程带来的CPU上下文切换与内存成本,非常容易会拖垮服务器。这就是C10K问题来由来。

因此,BIO的痛点在于:

  1. 高并发下资源耗尽

    当连接数激增时,线程数量呈线性增长(如 10000 个连接对应 10000 个线程),导致内存占用过高、上下文切换频繁,系统性能急剧下降。
  2. 阻塞导致效率低下

    线程在等待 IO 时无法做其他事情,CPU 利用率低。

NIO,Non-Blocking I/O

为了解决此问题,需要跪舔操作系统,为用户态程序提供一个真正非阻塞的Accept/Receive的函数

该函数的效果应该是,当没有新连接/新数据到达时,不阻塞线程。而是返回一个特殊标识,来告诉线程没有活干。

Java 1.4 引入 NIO,C# 通过Begin/End异步方法或SocketAsyncEventArgs实现类似逻辑。

    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(); //NIO核心,设为非阻塞模式
_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)
{
//没有新连接时,调用Accept触发WouldBlock异常,无视即可。
}
//一个线程同时管理Accept与Receive,已经有了多路复用的意思。
HandleClient(); }
}
static void HandleClient()
{
//一个一个遍历,寻找可用的客户端,
foreach (var client in _clients.ToList())
{
try
{
//NIO核心,非阻塞读取数据,无数据时立刻返回
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)
{
//没有新数据读取时,调用Receive触发WouldBlock异常,无视即可。
} }
}
}

通过NIO,我们可以非常惊喜的发现。我们仅用了一个线程就完成对客户端的连接与监听,相对BIO有了质的变化。

但有一个细节,在数据没拷贝到内核缓冲区之前,这个阶段是非阻塞的。当已经到达内核缓冲区时,此时调用Accept/Receive是会阻塞的,因为flag已经填充了,需要等待一个从内核缓冲区拷贝到用户缓存区的时间。

尽管NIO已经是JAVA世界的绝对主流,但依旧存在几个痛点:

  1. 轮询开销

    如果事件比较少,轮询会产生大量空转,CPU资源被浪费。
  2. 需要手动处理细节

    比如手动编写捕获when (ex.SocketErrorCode == SocketError.WouldBlock)来识别状态,

    需要手动处理TPC粘包,以及各种异常处理。

AIO,Asynchronous I/O

AIO作为大魔王与终极优化,实现了真正的异步操作,当发起IO请求后,内核完全接管IO处理,完成后通过回调或者事件来通知程序,开发者无需关心缓冲区管理、事件状态跟踪或轮询开销。

Java 7 引入 NIO.2(AIO),C# 通过IOCP+Async来实现

    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)
{
//异步读取数据,由操作系统完成IO后唤醒
var messageCount = await client.ReceiveAsync(_buffer);
var message = Encoding.UTF8.GetString(_buffer.ToArray(), 0, messageCount);
Console.WriteLine($"Client {client.RemoteEndPoint} Say:{message}");
}
}

Linux/Windows对模型的支持

IOCP:nput/Output Completion Port,I/O完成端口

.NET Core在Windows下基于IOCP,在Linux下基于epoll,在macOS中基于kqueue

NIO的改良,IO multiplexing

I/O Multiplexing 是一种高效处理多个I/O操作的技术,核心思想是通过少量线程管理多个I/O流,避免因为单个I/O阻塞导致整体服务性能下降。

它通过事件机制(可读,可写,异常)监听多个I/O源,当某个I/O流可操作时,才对其执行读写操作,从而实现单线程处理多连接的高效模型。

IO 多路复用本质是NIO的改良

select/poll

参考上面的代码,HandleClient方法中,我们遍历了整个_Clients,用以寻找客户端的Receive。

同样是C10K问题,如果我们1万,甚至100万个客户端连接。那么遍历的效率太过低下。尤其是每调用一次Receive都是一次用户态到内核态的切换。

那么,如果让操作系统告诉我们,哪些连接是可用的,我们就避免了在用户态遍历,从而提高性能。

        /// <summary>
/// 伪代码
/// </summary>
static void HandleClientSelect()
{
var clients = _clients.ToList();
//自己不遍历,交给内核态去遍历.
//这里会有一次list copy到内核态的过程,如果list量很大,开销也不小.
var readyClients= Socket.Select(clients); //内核会帮你标记好哪些client已经就绪
foreach (var client in readyClients)
{
//用户态依旧需要遍历一遍,但避免无意义的系统调用,用户态到内核态的切换.只有真正就绪的client才处理
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;
}
}
}

通过监听一组文件描述符(File Descriptor, FD)的可读、可写或异常状态,当其中任意状态满足时,内核返回就绪的 FD 集合。用户需遍历所有 FD 判断具体就绪的 I/O 操作。

select模型受限于系统默认值,最大只能处理1024个连接。poll模型通过结构体数组替代select位图的方式,避免了数量限制,其它无区别。

epoll

作为NIO的终极解决方案,它解决了什么问题?

  1. 调用select需要传递整个List

    var readyClients= Socket.Select(clients);

    如果list中有10W+,那么这个copy的成本会非常高
  2. select依旧是线性遍历

    在内核层面依旧是遍历整个list,寻找可用的client,所以时间复杂度不变O(N),只是减少了从用户态切换到内核态的次数而已
  3. 仅仅对ready做标记,并不减少返回量

    select仅仅返回就绪的数量,具体是哪个就绪,还要自己遍历一遍。

所以epoll模型主要主要针对这三点,做出了如下优化:

  1. 通过mmap,zero copy,减少数据拷贝
  2. 不再通过轮询方式,而是通过异步事件通知唤醒,内部使用红黑树来管理fd/handle
  3. 唤醒后,仅仅返回有变化的fd/handle,用户无需遍历整个list

基于事件驱动(Event-Driven)机制,内核维护一个 FD 列表,通过epoll_ctl添加 / 删除 FD 监控,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("读缓存创建异常,断开连接\n");
exit(EXIT_FAILURE);
} //写内存分配判断
write_buf=malloc(sizeof(char)* BUFFER_SIZE);
if(!write_buf)
{
printf("写缓存创建异常,断开连接\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(); //声明sockfd,clientfd
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)); //声明IP协议
server_addr.sin_family=AF_INET;
//绑定主机地址
server_addr.sin_addr.s_addr=htonl(INADDR_ANY);
//绑定端口
server_addr.sin_port=htons(SEVER_PORT); //创建socket
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); //将sockfd设为非阻塞模式
set_nonblocking(sockfd); int epollfd,nfds;
struct epoll_event ev,events[MAX_EVENTS]; //创建epoll
epollfd=epoll_create1(0);
handle_error("epoll_create1",epollfd);
//将sockfd加入到监控列表
ev.data.fd=sockfd;
//将关联的文件描述符设为可读,可读说明有连接进入,就会被epoll触发
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);
//接受client连接
while (1)
{
//挂起等待,有可读信息
//nfds表示有多少个客户端连接与多少条消息
nfds=epoll_wait(epollfd,events,MAX_EVENTS,-1);
handle_error("epoll_wait",nfds); for (int i = 0; i < nfds; i++)
{
//第一个是sockfd,要预处理一下。
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); //将获取到的client连接也添加到监控列表
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("当前批次已经读取完毕。\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文件描述法符中移除该client_fd
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;
}

理论与现实的割裂

从上面的理论可以看出,AIO似乎是版本答案,在C#中,AIO已经充斥着每一个角落,但在JAVA的世界中,更加主流的是NIO,这是为什么呢?

1. Linux的支持不足

Linux 内核直到 3.11 版本(2013 年)才支持真正的异步 IO(io_uring),从而间接影响了JAVA的发展,Java的 AIO直到 2011 年Java 7才正式发布,而其前一代 NIO已发展近 10 年。

而Windows的IOCP在Windows NT 3.5就登上了历史舞台,加上C#起步较晚,没有历史包袱,所以对AIO支持力度更大,尤其是2012年发布了async/await异步模型后,解决了回调地狱,实现了1+1>3的效果。

2. JAVA的路径依赖

NIO生态过于强大,尤其是以Netty/Redis为首的经典实现,实在是太香了!

3. 理论优势并未转换为实际收益

AIO的性能在特定场景(如超大规模文件读写、长连接低活跃)下可能优于NIO,但在互联网场景中,NIO的足够高效,比如HTTP请求,AIO的异步回调优势相对轮询并不明显。

维度 Java AIO未普及的原因 C# AIO普及的原因
历史发展 NIO早于AIO 9年推出,生态成熟;AIO定位模糊,未解决NIO的核心痛点(如编程复杂度) AIO与async/await同步推出,解决了异步编程的“回调地狱”,成为高并发编程的默认选择
跨平台 需适配多系统异步机制(如Linux的epoll、macOS的kqueue),实际性能提升有限 早期绑定Windows IOCP,性能稳定;跨平台后对AIO需求不迫切
生态 Netty等NIO框架统治市场,切换AIO成本高 缺乏NIO统治级框架,AIO通过async/await成为原生选择
开发者习惯 NIO代码虽复杂,但通过框架封装已足够易用;AIO回调模式学习成本更高 async/await语法糖让异步代码接近同步,开发者更易接受
性能场景 大多数场景下NIO已足够高效,AIO的优势未显著体现 Windows IOCP场景下AIO性能优势明显,且覆盖主流企业级需求

说人话就是,Netty太香了,完全没动力切换成AIO,顺带吐槽C#中没有类似的框架。dotnetty不算,已经停止更新了。

C#网络编程(六)----Socket编程模型的更多相关文章

  1. 网络协议 11 - Socket 编程(下):眼见为实耳听为虚

    系列文章传送门: 网络协议 1 - 概述 网络协议 2 - IP 是怎么来,又是怎么没的? 网络协议 3 - 从物理层到 MAC 层 网络协议 4 - 交换机与 VLAN:办公室太复杂,我要回学校 网 ...

  2. 多线程编程以及socket编程_Linux程序设计4chapter15

    看了Linux程序设计4中文版,学习了多线程编程和socket编程.本文的程序参考自Linux程序设计4的第15章. 设计了一个客户端程序,一个服务端程序.使用TCP协议进行数据传输. 客户端进程创建 ...

  3. 第九章:Python高级编程-Python socket编程

    第九章:Python高级编程-Python socket编程 Python3高级核心技术97讲 笔记 9.1 弄懂HTTP.Socket.TCP这几个概念 Socket为我们封装好了协议 9.2 cl ...

  4. linux网络编程之socket编程(十六)

    继续学习socket编程,今天的内容会有些难以理解,一步步来分解,也就不难了,正入正题: 实际上sockpair有点像之前linux系统编程中学习的pipe匿名管道,匿名管道它是半双工的,只能用于亲缘 ...

  5. 网络协议 10 - Socket 编程(上):实践是检验真理的唯一标准

    系列文章传送门: 网络协议 1 - 概述 网络协议 2 - IP 是怎么来,又是怎么没的? 网络协议 3 - 从物理层到 MAC 层 网络协议 4 - 交换机与 VLAN:办公室太复杂,我要回学校 网 ...

  6. 网络协议 10 - Socket 编程(上)

    前面一直在说各种协议,偏理论方面的知识,这次咱们就来认识下基于 TCP 和 UDP 协议这些理论知识的 Socket 编程.     说 TCP 和 UDP 的时候,我们是分成客户端和服务端来认识的, ...

  7. linux网络编程之socket编程(一)

    今天开始,继续来学习linux编程,这次主要是研究下linux下的网络编程,而网络编程中最基本的需从socket编程开始,下面正式开始学习: 什么是socket: 在学习套接口之前,先要回顾一下Tcp ...

  8. linux网络编程之socket编程(八)

    学习socket编程继续,今天要学习的内容如下: 先来简单介绍一下这五种模型分别是哪些,偏理论,有个大致的印象就成,做个对比,因为最终只会研究一个I/O模型,也是经常会用到的, 阻塞I/O: 先用一个 ...

  9. 关于网络协议和socket编程基本概念

    TCP协议可以说已经是IT人耳熟能详的协议,最近在学习socket网络编程时后重新温习一下这个协议,针对一些问题做了一些总结,很多理解可能还不是很准确. 1. 协议是什么?所谓的各种网络协议无非是一种 ...

  10. iOS网络编程笔记——Socket编程

    一.什么是Socket通信: Socket是网络上的两个程序,通过一个双向的通信连接,实现数据的交换.这个双向连路的一端称为socket.socket通常用来实现客户方和服务方的连接.socket是T ...

随机推荐

  1. ADF - [02] 概念

    题记部分 001 || 管道和活动 一个数据工厂可以有一个或多个管道(Pipeline).管道是共同执行一项任务的活动的逻辑分组.管道可以包含一组活动(Activity),这些活动引入和清除日志数据, ...

  2. php获取类名

    <?php class ParentClass { public static function getClassName() { return __CLASS__; } } class Chi ...

  3. C# 多线程编程及其几种方式

    引言: 进程(process):应用程序的实例要使用的资源的集合.每个进程被赋予了一个虚拟地址空间,确保在一个进程中使用的代码和数据无法由另一个进程访问. 线程(thread):程序中的一个执行流,每 ...

  4. 基于近红外与可见光双目摄像头的人脸识别与活体检测,文末附Demo

    基于近红外与可见光双目摄像头的活体人脸检测原理 人脸活体检测(Face Anti-Spoofing)是人脸识别系统中的重要一环,它负责验证捕捉到的人脸是否为真实活体,以抵御各种伪造攻击,如彩色纸张打印 ...

  5. docx4j转换HTML并生成word文档实践

    一.背景 在项目开发中,有一个需求需要将富文本编辑器中的内容转换为word文档.在网上看了很多开源第三方工具包的对比,最终选择了docx4j,主要原因有一下几点: 可以将html转换为word 对wo ...

  6. Delphi编写的一款锁屏小工具

    Delphi编写的一款锁屏小工具,双击程序立即锁屏,木有界面的.解除锁屏密码:alt+空格. unit Unit1; interface uses Windows, Messages, SysUtil ...

  7. 【SpringCloud】SpringCloud Alibaba Seata处理分布式事务

    SpringCloud Alibaba Seata处理分布式事务 分布式事务问题 分布式前 单机库存没这个问题 O(∩_∩)O 从1:1->1:N->N:N 分布式之后 单体应用被拆分成微 ...

  8. 【Maven】仓库

    在 Maven 的术语中,仓库是一个位置(place). Maven 仓库是项目中依赖的第三方库,这个库所在的位置叫做仓库. 在 Maven 中,任何一个依赖.插件或者项目构建的输出,都可以称之为构件 ...

  9. langchain0.3教程:聊天机器人进阶之方法调用

    我们思考一个问题:大语言模型是否能帮我们做更多的事情,比如帮我们发送邮件.默认情况下让大模型帮我们发送邮件,大模型会这样回复我们: 可以看到,大模型无法发送邮件,它只会帮我们生成一个邮件模板,然后让我 ...

  10. [T.4] 团队项目:团队代码管理准备

    团队的代码仓库地址 [GitHub - Meng-XuanYu/JayJay-TeamVersionControl: A public repo for BUAASE2025 course homew ...