C# Socket网络通信【高并发场景】
用途
在 C# 中,Socket
类是用于在网络上进行低级别通信的核心类。它提供了对 TCP、UDP 等协议的支持,可以实现服务器和客户端之间的数据传输。Socket
提供了比 TcpClient
、UdpClient
等更细粒度的控制,因此通常用于需要更多控制的场景。
使用
服务器
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
class Server
{
static void Main()
{
// 创建 Socket
Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// 绑定到本地 IP 和端口
IPEndPoint localEndPoint = new IPEndPoint(IPAddress.Any, 11000);
listener.Bind(localEndPoint);
// 开始监听
//服务器开始监听客户端连接,请求队列长度为 10,这意味着可以同时接受最多 10 个等待连接的客户端。
listener.Listen(10);
Console.WriteLine("等待客户端连接...");
while (true) // 无限循环持续监听
{
// 接受客户端连接
//会阻塞主线程,直到有客户端发起连接。这种设计确保了服务器能够同步处理每个客户端的连接。
Socket handler = listener.Accept();
Console.WriteLine("客户端已连接!");
// 接收客户端发送的数据
byte[] buffer = new byte[1024];
int bytesRec = handler.Receive(buffer);
Console.WriteLine("收到消息: " + Encoding.ASCII.GetString(buffer, 0, bytesRec));
// 发送响应给客户端
byte[] msg = Encoding.ASCII.GetBytes("消息已收到!");
handler.Send(msg);
// 关闭与客户端的连接
handler.Shutdown(SocketShutdown.Both);
handler.Close();
}
}
}
客户端
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
class Client
{
static void Main()
{
// 创建 Socket
Socket sender = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// 连接到服务器
IPEndPoint remoteEP = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 11000);
sender.Connect(remoteEP);
Console.WriteLine("已连接到服务器!");
// 发送数据给服务器
byte[] msg = Encoding.ASCII.GetBytes("Hello Server!");
sender.Send(msg);
// 接收服务器的响应
byte[] buffer = new byte[1024];
int bytesRec = sender.Receive(buffer);
Console.WriteLine("服务器响应: " + Encoding.ASCII.GetString(buffer, 0, bytesRec));
// 关闭连接
sender.Shutdown(SocketShutdown.Both);
sender.Close();
}
}
服务器:Bind/Listen/Send/Close/
客户端:Connect/Send/Receive/
高并发场景
当前的服务器示例是单线程的,它只能顺序处理一个客户端的连接。一旦接受并处理了一个客户端的请求,才会继续监听下一个客户端的连接请求。
问题:
由于服务器是单线程的,它在处理某个客户端的连接时(调用 accept、receive 等),其他客户端的连接请求会被阻塞,直到当前请求完成。这意味着服务器无法并发处理多个客户端的请求。
解决方案:
为了使服务器能够并发处理多个客户端的连接,我们可以使用多线程或异步编程。
多线程实现:
通过为每个客户端连接创建一个独立的线程,服务器可以同时处理多个客户端。这样,每个客户端的请求都会在一个单独的线程中进行处理,主线程可以继续监听新的连接请求。
下面是修改后的代码,使用多线程来处理多个客户端的连接:
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
class Server
{
static void Main()
{
// 创建一个 TCP Socket
Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// 绑定到本地 IP 和端口
IPEndPoint localEndPoint = new IPEndPoint(IPAddress.Any, 11000);
listener.Bind(localEndPoint);
// 开始监听连接请求,队列长度为10
listener.Listen(10);
Console.WriteLine("服务器正在监听客户端连接...");
while (true) // 无限循环持续监听
{
// 接受客户端连接
Socket handler = listener.Accept();
Console.WriteLine("客户端已连接!");
// 为每个客户端连接创建一个新线程
Thread clientThread = new Thread(() => HandleClient(handler));
clientThread.Start();
}
}
// 处理客户端连接的逻辑
static void HandleClient(Socket clientSocket)
{
try
{
// 接收客户端发送的数据
byte[] buffer = new byte[1024];
int bytesRec = clientSocket.Receive(buffer);
Console.WriteLine("收到消息: " + Encoding.ASCII.GetString(buffer, 0, bytesRec));
// 发送响应给客户端
byte[] msg = Encoding.ASCII.GetBytes("消息已收到!");
clientSocket.Send(msg);
}
finally
{
// 关闭与客户端的连接
clientSocket.Shutdown(SocketShutdown.Both);
clientSocket.Close();
}
}
}
在每次有新的客户端连接时,服务器会调用 listener.Accept()
接受连接,然后为这个连接创建一个新的线程。
新线程会执行 HandleClient
方法,在该方法中处理客户端的通信(接收消息、发送响应等)。
并发处理:
服务器主线程通过 while (true)
持续监听客户端的连接请求。
每次有客户端连接时,服务器会创建一个新的线程处理该连接,这样主线程不会被阻塞,可以继续接受其他客户端的连接请求。
线程处理客户端:
HandleClient
方法会在线程中执行,处理特定客户端的通信。
在该线程中,服务器接收客户端的数据并发送响应,最后关闭连接。
多线程的优缺点:
- 优点:
- 能够同时处理多个客户端的连接,提升服务器的并发能力。
- 每个客户端的请求处理相互独立,不会因为某个客户端的处理时间过长而阻塞其他客户端的连接。
- 缺点:
- 每个客户端连接都创建一个新线程,随着客户端数量的增加,可能会消耗大量的系统资源(如 CPU 和内存)。
- 线程的创建和销毁是有开销的,在高并发场景下,可能不够高效。
异步编程实现:
在 C# 中,还可以使用异步编程(async/await)来处理多个客户端的连接,避免创建大量线程。异步方法允许服务器在等待 I/O 操作(如 Receive、Send)完成时,不会阻塞主线程,具有更好的扩展性。
异步服务器示例:
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
class Server
{
static async Task Main()
{
// 创建一个 TCP Socket
Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// 绑定到本地 IP 和端口
IPEndPoint localEndPoint = new IPEndPoint(IPAddress.Any, 11000);
listener.Bind(localEndPoint);
// 开始监听连接请求,队列长度为10
listener.Listen(10);
Console.WriteLine("服务器正在监听客户端连接...");
while (true)
{
// 异步接受客户端连接
Socket handler = await Task.Factory.FromAsync(listener.BeginAccept, listener.EndAccept, null);
Console.WriteLine("客户端已连接!");
// 异步处理客户端连接
_ = Task.Run(() => HandleClient(handler));
}
}
// 处理客户端连接的逻辑
static async Task HandleClient(Socket clientSocket)
{
try
{
byte[] buffer = new byte[1024];
// 异步接收客户端发送的数据
int bytesRec = await Task.Factory.FromAsync(
clientSocket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, null, null),
clientSocket.EndReceive);
Console.WriteLine("收到消息: " + Encoding.ASCII.GetString(buffer, 0, bytesRec));
// 异步发送响应给客户端
byte[] msg = Encoding.ASCII.GetBytes("消息已收到!");
await Task.Factory.FromAsync(
clientSocket.BeginSend(msg, 0, msg.Length, SocketFlags.None, null, null),
clientSocket.EndSend);
}
finally
{
// 关闭与客户端的连接
clientSocket.Shutdown(SocketShutdown.Both);
clientSocket.Close();
}
}
}
异步编程的优缺点:
- 优点:
- 异步编程模型更高效,因为它不需要为每个客户端连接创建新的线程。【而是通过申请线程池中的线程来完成任务】
- 能够处理大量并发请求而不会消耗太多系统资源。
- 缺点:
- 代码复杂度较高,需要熟悉异步编程模型和 async/await 语法。
总结:
- 单线程:示例中初始的服务器是单线程的,只能顺序处理一个客户端的连接请求。无法并发处理多个客户端。
- 多线程:可以通过为每个客户端连接创建一个新线程来实现并发处理,能够处理多个客户端,但线程的创建和销毁有开销。
- 异步编程:通过异步方法来处理客户端连接,在处理 I/O 操作时不会阻塞线程,能够处理大量并发请求,且资源消耗较低。
如果你的服务器需要处理大量并发请求,异步编程模型可能是更优的选择。如果并发量较小,多线程方式也是一个有效的方案。