C# Socket编程:时间管理与代码优化秘籍
C# Socket编程:时间管理与代码优化秘籍
在现代软件开发中,高效的网络编程是关键。本文深入探讨了如何通过C#中的Socket编程实现高性能网络应用。从基础理论到实际操作,我们将分享六大策略助你打造极速Socket体验,包括数据序列化、并发连接管理以及I/O重用技术等实践。此外,我们还会介绍网络库与框架的选择、缓存机制及分布式架构对性能的影响。掌握这些技巧,让你在网络编程的世界里如鱼得水!
C# Socket编程基础
在C#中,System.Net.Sockets.Socket
类是.NET Framework提供的核心类,用于处理网络套接字编程。使用Socket类,可以创建客户端和服务器应用程序来进行基于TCP、UDP和其他网络协议的通信。
TCP服务器端实现
创建监听套接字:服务端首先会创建一个
ServerSocket
对象(在Java中)或SOCKET
类型的套接字(在C/C++中通常使用socket()
函数创建),用于监听来自客户端的连接请求。绑定地址和端口:服务端必须将其套接字与本地主机上的特定IP地址和端口号绑定,这通常是通过
bind()
函数实现。服务端通常选择固定的知名端口或注册端口,以便客户端能够知道在哪里建立连接。监听连接请求:调用
listen()
函数使服务端进入监听状态,等待客户端的连接请求。接受连接:当有客户端连接请求时,服务端通过
accept()
函数接收连接,这个函数会返回一个新的套接字,用于与该客户端进行通信。
代码如下:
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace SocketServer
{
class Program
{
private static byte[] buffer = new byte[1024];
private static int port = 8885; // 定义服务器监听端口
static void Main(string[] args)
{
// 创建一个 IP 地址对象,绑定到本地主机
IPAddress ipAddress = IPAddress.Parse("127.0.0.1");
// 创建一个新的 Socket 对象,指定为 IPv4、面向流(TCP)协议
Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// 绑定服务器 socket 到特定的 IP 和端口
IPEndPoint localEndPoint = new IPEndPoint(ipAddress, port);
serverSocket.Bind(localEndPoint);
// 开始监听连接请求,最大同时排队的连接数为 10
serverSocket.Listen(10);
Console.WriteLine($"启动监听 {serverSocket.LocalEndPoint} 成功");
// 启动线程等待客户端连接
Thread listenThread = new Thread(ListenForClients);
listenThread.Start(serverSocket);
// 等待控制台输入以保持程序运行
Console.ReadLine();
}
private static void ListenForClients(object obj)
{
Socket listener = (Socket)obj;
while (true)
{
// 接受一个客户端的连接请求
Socket clientSocket = listener.Accept();
// 发送欢迎消息给客户端
string welcomeMessage = "ServerSayHello";
byte[] data = Encoding.ASCII.GetBytes(welcomeMessage);
clientSocket.Send(data);
// 在单独的线程中处理来自客户端的消息
Thread receiveThread = new Thread(ReceiveData);
receiveThread.Start(clientSocket);
}
}
private static void ReceiveData(object obj)
{
Socket clientSocket = (Socket)obj;
while (true)
{
try
{
// 接收客户端发来的数据
int received = clientSocket.Receive(buffer);
// 将接收的数据转换为字符串并输出
string message = Encoding.ASCII.GetString(buffer, 0, received);
}
catch (Exception)
{
// 处理异常情况
}
}
}
}
}
性能优化策略
在实际项目中,我们可能会遇到CPU占用率过高的问题。例如,在使用Sharp7.cs连接西门子PLC时,发现当在同一台工控机上连接多个(实际超过100)CPU时,工控机的CPU占用非常大,会去到20~30%。然而此时实际的网络流量并不高,只有10mbps。大量CPU资源消耗不知道在做什么。因为此时工控机上也没有跑其他业务代码。所以基本可以定位到是Sharp7.cs连接PLC这里出了问题。
问题分析
在Sharp7.cs中发现了如下代码来读取PLC发送过来的数据:
while ((BytesReaded < Size))
{
Thread.Sleep(2);
SizeAvail = TCPSocket.Available;
try
{
byte[] Flush = new byte[SizeAvail];
int BytesRead = TCPSocket.Receive(Buffer, Start + BytesReaded, SizeAvail, SocketFlags.None);
BytesReaded += BytesRead;
}
catch { }
}
这段代码通过不断的读取是否有可用字节来判断一次数据是否传输完成。这样写的是可以保证读取要需要长度的数据,很多简单的socket编程上都可以见到。由于代码中包含有Sleep()并且sleep的时间非常短,当同时有大量的线程都在读取数据的时候,大量的CPU资源消耗在了线程的切换上,造成CPU占用率高,效率严重不足。
解决方案
C#提供了Poll()方法来改善上述的问题。Poll()将在指定的时间段内阻塞程序的执行直到socket可用。这样socket所在线程就会在有可用数据的时候被唤醒,而不是不停的去测试是否有可读数据了,从而减少线程切换提高CPU的利用率。把代码修改如下,(注意Poll()方法中的时间单位是微秒)
while (BytesReaded < Size && TCPSocket.Poll(1000 * _ReadTimeout, SelectMode.SelectRead))
{
int BytesRead = TCPSocket.Receive(Buffer, Start + BytesReaded, Size - BytesReaded, SocketFlags.None);
if (BytesRead == 0)
{
LastError = S7Consts.errTCPDataReceive;
Close();
}
BytesReaded += BytesRead;
}
优化效果
经过上述修改,工控机从PLC单次读取数据的时间从16ms减少到10ms,极大地提高了CPU的利用率。
时间管理技巧
在TCP通信中,时间管理是一个重要的环节。以下是一些关键的时间管理技巧:
Nagle算法
Nagle算法的核心思想是,在一个TCP连接上,最多只能有一个未被确认的小数据包(小于MSS,即最大报文段大小)。
优势
- 减少网络拥塞:通过合并小数据包,减少了网络中的数据包数量,降低了拥塞的可能性。
- 提高网络效率:在低速网络中,Nagle算法可以显著提高传输效率。
劣势
- 增加延迟:在交互式应用中,Nagle算法可能导致显著的延迟,因为它等待ACK或合并数据包。
在C#中如何配置?
var _socket = new Socket(IPAddress.Any.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
_serverSocket.NoDelay = _options.NoDelay;
连接超时
在调用客户端Socket连接服务器的时候,可以设置连接超时机制,具体可以传入一个任务的取消令牌,并且设置超时时间。
CancellationTokenSource connectTokenSource = new CancellationTokenSource();
connectTokenSource.CancelAfter(3000); //3秒
await _socket.ConnectAsync(RemoteEndPoint, connectTokenSource.Token);
SSL加密传输
TCP使用SSL加密传输,通过非对称加密的方式,利用证书,保证双方使用了安全的密钥加密了报文。
在C#中如何配置?
服务端配置
//创建证书对象
var _certificate = _certificate = new X509Certificate2(_options.PfxCertFilename, _options.PfxPassword);
//与客户端进行验证
if (allowingUntrustedSSLCertificate) //是否允许不受信任的证书
{
SslStream = new SslStream(NetworkStream, false,
(obj, certificate, chain, error) => true);
}
else
{
SslStream = new SslStream(NetworkStream, false);
}
try
{
//serverCertificate:用于对服务器进行身份验证的 X509Certificate
//clientCertificateRequired:一个 Boolean 值,指定客户端是否必须为身份验证提供证书
//checkCertificateRevocation:一个 Boolean 值,指定在身份验证过程中是否检查证书吊销列表
await SslStream.AuthenticateAsServerAsync(new SslServerAuthenticationOptions()
{
ServerCertificate = x509Certificate,
ClientCertificateRequired = mutuallyAuthenticate,
CertificateRevocationCheckMode = checkCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck
}, cancellationToken).ConfigureAwait(false);
if (!SslStream.IsEncrypted || !SslStream.IsAuthenticated)
{
return false;
}
if (mutuallyAuthenticate && !SslStream.IsMutuallyAuthenticated)
{
return false;
}
}
catch (Exception)
{
throw;
}
//完成验证后,通过SslStream传输数据
int readCount = await SslStream.ReadAsync(buffer, _lifecycleTokenSource.Token)
.ConfigureAwait(false);
客户端配置
var _certificate = new X509Certificate2(_options.PfxCertFilename, _options.PfxPassword);
if (_options.IsSsl) //如果使用ssl加密传输
{
if (_options.AllowingUntrustedSSLCertificate)//是否允许不受信任的证书
{
_sslStream = new SslStream(_networkStream, false,
(obj, certificate, chain, error) => true);
}
else
{
_sslStream = new SslStream(_networkStream, false);
}
_sslStream.ReadTimeout = _options.ReadTimeout;
_sslStream.WriteTimeout = _options.WriteTimeout;
await _sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions()
{
TargetHost = RemoteEndPoint.Address.ToString(),
EnabledSslProtocols = System.Security.Authentication.SslProtocols.Tls12,
ClientCertificates = new X509Certificate2Collection(_certificate)
}, cancellationToken).ConfigureAwait(false);
}
通过以上技巧,可以有效提升C# Socket编程的性能和稳定性,为开发高性能网络应用奠定坚实基础。