使用 TcpClient class C# 的异步套接字客户端
Async socket client using TcpClient class C#
我已经使用 TcpClient class 实现了套接字客户端。所以我可以发送和接收数据,一切正常。但是我问了一些专家 :) 我的实现有什么问题吗?也许有更好的做事方式。特别是,我如何处理断开连接?是否有一些指示器(或者我可以自己写一个)告诉我套接字已断开连接?
我也研究了 Socket class 的异步等待功能,但无法理解 "SocketAsyncEventArgs",为什么它首先在那里。
为什么我不能只是:等待 Client.SendAsync("data"); ?
public class Client
{
private TcpClient tcpClient;
public void Initialize(string ip, int port)
{
try
{
tcpClient = new TcpClient(ip, port);
if (tcpClient.Connected)
Console.WriteLine("Connected to: {0}:{1}", ip, port);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
Initialize(ip, port);
}
}
public void BeginRead()
{
var buffer = new byte[4096];
var ns = tcpClient.GetStream();
ns.BeginRead(buffer, 0, buffer.Length, EndRead, buffer);
}
public void EndRead(IAsyncResult result)
{
var buffer = (byte[])result.AsyncState;
var ns = tcpClient.GetStream();
var bytesAvailable = ns.EndRead(result);
Console.WriteLine(Encoding.ASCII.GetString(buffer, 0, bytesAvailable));
BeginRead();
}
public void BeginSend(string xml)
{
var bytes = Encoding.ASCII.GetBytes(xml);
var ns = tcpClient.GetStream();
ns.BeginWrite(bytes, 0, bytes.Length, EndSend, bytes);
}
public void EndSend(IAsyncResult result)
{
var bytes = (byte[])result.AsyncState;
Console.WriteLine("Sent {0} bytes to server.", bytes.Length);
Console.WriteLine("Sent: {0}", Encoding.ASCII.GetString(bytes));
}
}
以及用法:
static void Main(string[] args)
{
var client = new Client();
client.Initialize("127.0.0.1", 8778);
client.BeginRead();
client.BeginSend("<Names><Name>John</Name></Names>");
Console.ReadLine();
}
好吧,我花了 10 秒才找到你可以解决的最大问题:
public void BeginRead()
{
var buffer = new byte[4096];
var ns = tcpClient.GetStream();
ns.BeginRead(buffer, 0, buffer.Length, EndRead, buffer);
}
不过别担心,这就是我们采用 SO 的原因。
首先让我解释一下为什么这是一个大问题。
假设您发送的消息长度为 4097 字节。您的缓冲区只能接受 4096 字节,这意味着您不能将整个消息打包到此缓冲区中。
假设您要发送 12 字节 长的消息。您仍在内存中分配 4096 字节 只是为了存储 12 字节.
如何处理?
每次使用网络时,您都应该考虑制定某种 协议 (有些人称之为 消息框架 ,但它只是协议 ) 将帮助您识别传入的包。
协议示例可以是:
[1B = type of message][4B = length][XB = message]
- where X == BitConvert.ToInt32(length);
接收方:
byte messageType = (byte)netStream.ReadByte();
byte[] lengthBuffer = new byte[sizeof(int)];
int recv = netStream.Read(lengthBuffer, 0, lengthBuffer.Length);
if(recv == sizeof(int))
{
int messageLen = BitConverter.ToInt32(lengthBuffer, 0);
byte[] messageBuffer = new byte[messageLen];
recv = netStream.Read(messageBuffer, 0, messageBuffer.Length);
if(recv == messageLen)
{
// messageBuffer contains your whole message ...
}
}
发件人:
byte messageType = (1 << 3); // assume that 0000 1000 would be XML
byte[] message = Encoding.ASCII.GetBytes(xml);
byte[] length = BitConverter.GetBytes(message.Length);
byte[] buffer = new byte[sizeof(int) + message.Length + 1];
buffer[0] = messageType;
for(int i = 0; i < sizeof(int); i++)
{
buffer[i + 1] = length[i];
}
for(int i = 0; i < message.Length; i++)
{
buffer[i + 1 + sizeof(int)] = message[i];
}
netStream.Write(buffer);
您的其余代码看起来没问题。但在我看来,在你的情况下使用异步操作是没有用的。您可以对同步调用执行相同的操作。
很难回答,因为这里没有确切的问题,但更多的是某种代码审查。但还是有些提示:
- 您的连接机制似乎有误。我认为
TcpClient.Connected
在建立连接之前不会阻塞。因此,它通常会在连接进行时失败,然后您重新开始。您应该切换到使用阻塞或异步 Connect
方法。
- SocketAsyncEventArgs 是一种高性能异步数据传输机制。很少需要它。你应该忽略它
- 如果您想异步发送数据,您应该使用
Async
方法,其中 return 和 Task
,因为这些方法可以很容易地与 async/await 结合使用。
- APM 模型 (BeginXYZ/EndXYZ) 已被弃用,您不应再在新代码中使用它。它的一个问题是,有时在 Begin 方法内同步调用 End 方法,这会导致令人惊讶的行为。如果不是这种情况,完成回调将从 ThreadPool 上的随机线程执行。这通常也不是您想要的。 TPL 方法避免了这种情况。
- 对于您的简单用例,阻塞方法也完全可以,并且不会带来各种异步方法的复杂性。
使用 TPL 方法的代码读取端(未测试):
public async Task Initialize(string ip, int port)
{
tcpClient = new TcpClient;
await tcpClient.ConnectAsync(ip, port);
Console.WriteLine("Connected to: {0}:{1}", ip, port);
}
public async Task Read()
{
var buffer = new byte[4096];
var ns = tcpClient.GetStream();
while (true)
{
var bytesRead = await ns.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead == 0) return; // Stream was closed
Console.WriteLine(Encoding.ASCII.GetString(buffer, 0, bytesRead));
}
}
在初始化部分你会做:
await client.Initialize(ip, port);
// Start reading task
Task.Run(() => client.Read());
要使用同步方法,请删除所有 Async
次事件并将任务替换为线程。
我已经使用 TcpClient class 实现了套接字客户端。所以我可以发送和接收数据,一切正常。但是我问了一些专家 :) 我的实现有什么问题吗?也许有更好的做事方式。特别是,我如何处理断开连接?是否有一些指示器(或者我可以自己写一个)告诉我套接字已断开连接?
我也研究了 Socket class 的异步等待功能,但无法理解 "SocketAsyncEventArgs",为什么它首先在那里。 为什么我不能只是:等待 Client.SendAsync("data"); ?
public class Client
{
private TcpClient tcpClient;
public void Initialize(string ip, int port)
{
try
{
tcpClient = new TcpClient(ip, port);
if (tcpClient.Connected)
Console.WriteLine("Connected to: {0}:{1}", ip, port);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
Initialize(ip, port);
}
}
public void BeginRead()
{
var buffer = new byte[4096];
var ns = tcpClient.GetStream();
ns.BeginRead(buffer, 0, buffer.Length, EndRead, buffer);
}
public void EndRead(IAsyncResult result)
{
var buffer = (byte[])result.AsyncState;
var ns = tcpClient.GetStream();
var bytesAvailable = ns.EndRead(result);
Console.WriteLine(Encoding.ASCII.GetString(buffer, 0, bytesAvailable));
BeginRead();
}
public void BeginSend(string xml)
{
var bytes = Encoding.ASCII.GetBytes(xml);
var ns = tcpClient.GetStream();
ns.BeginWrite(bytes, 0, bytes.Length, EndSend, bytes);
}
public void EndSend(IAsyncResult result)
{
var bytes = (byte[])result.AsyncState;
Console.WriteLine("Sent {0} bytes to server.", bytes.Length);
Console.WriteLine("Sent: {0}", Encoding.ASCII.GetString(bytes));
}
}
以及用法:
static void Main(string[] args)
{
var client = new Client();
client.Initialize("127.0.0.1", 8778);
client.BeginRead();
client.BeginSend("<Names><Name>John</Name></Names>");
Console.ReadLine();
}
好吧,我花了 10 秒才找到你可以解决的最大问题:
public void BeginRead()
{
var buffer = new byte[4096];
var ns = tcpClient.GetStream();
ns.BeginRead(buffer, 0, buffer.Length, EndRead, buffer);
}
不过别担心,这就是我们采用 SO 的原因。
首先让我解释一下为什么这是一个大问题。
假设您发送的消息长度为 4097 字节。您的缓冲区只能接受 4096 字节,这意味着您不能将整个消息打包到此缓冲区中。
假设您要发送 12 字节 长的消息。您仍在内存中分配 4096 字节 只是为了存储 12 字节.
如何处理?
每次使用网络时,您都应该考虑制定某种 协议 (有些人称之为 消息框架 ,但它只是协议 ) 将帮助您识别传入的包。
协议示例可以是:
[1B = type of message][4B = length][XB = message]
- where X == BitConvert.ToInt32(length);
接收方:
byte messageType = (byte)netStream.ReadByte(); byte[] lengthBuffer = new byte[sizeof(int)]; int recv = netStream.Read(lengthBuffer, 0, lengthBuffer.Length); if(recv == sizeof(int)) { int messageLen = BitConverter.ToInt32(lengthBuffer, 0); byte[] messageBuffer = new byte[messageLen]; recv = netStream.Read(messageBuffer, 0, messageBuffer.Length); if(recv == messageLen) { // messageBuffer contains your whole message ... } }
发件人:
byte messageType = (1 << 3); // assume that 0000 1000 would be XML byte[] message = Encoding.ASCII.GetBytes(xml); byte[] length = BitConverter.GetBytes(message.Length); byte[] buffer = new byte[sizeof(int) + message.Length + 1]; buffer[0] = messageType; for(int i = 0; i < sizeof(int); i++) { buffer[i + 1] = length[i]; } for(int i = 0; i < message.Length; i++) { buffer[i + 1 + sizeof(int)] = message[i]; } netStream.Write(buffer);
您的其余代码看起来没问题。但在我看来,在你的情况下使用异步操作是没有用的。您可以对同步调用执行相同的操作。
很难回答,因为这里没有确切的问题,但更多的是某种代码审查。但还是有些提示:
- 您的连接机制似乎有误。我认为
TcpClient.Connected
在建立连接之前不会阻塞。因此,它通常会在连接进行时失败,然后您重新开始。您应该切换到使用阻塞或异步Connect
方法。 - SocketAsyncEventArgs 是一种高性能异步数据传输机制。很少需要它。你应该忽略它
- 如果您想异步发送数据,您应该使用
Async
方法,其中 return 和Task
,因为这些方法可以很容易地与 async/await 结合使用。 - APM 模型 (BeginXYZ/EndXYZ) 已被弃用,您不应再在新代码中使用它。它的一个问题是,有时在 Begin 方法内同步调用 End 方法,这会导致令人惊讶的行为。如果不是这种情况,完成回调将从 ThreadPool 上的随机线程执行。这通常也不是您想要的。 TPL 方法避免了这种情况。
- 对于您的简单用例,阻塞方法也完全可以,并且不会带来各种异步方法的复杂性。
使用 TPL 方法的代码读取端(未测试):
public async Task Initialize(string ip, int port)
{
tcpClient = new TcpClient;
await tcpClient.ConnectAsync(ip, port);
Console.WriteLine("Connected to: {0}:{1}", ip, port);
}
public async Task Read()
{
var buffer = new byte[4096];
var ns = tcpClient.GetStream();
while (true)
{
var bytesRead = await ns.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead == 0) return; // Stream was closed
Console.WriteLine(Encoding.ASCII.GetString(buffer, 0, bytesRead));
}
}
在初始化部分你会做:
await client.Initialize(ip, port);
// Start reading task
Task.Run(() => client.Read());
要使用同步方法,请删除所有 Async
次事件并将任务替换为线程。