我有一个套接字。BeginReceive 异步套接字连接,它启动自己的线程。 我正在接收大量不适合该方法第一遍的数据,并且由于数据是许多重复出现的 xml 标记,因此无法知道何时完成接收。
接收到数据时,它被解析并使用 graphics.fromImage(bitmap) 绘制到内存中的位图图像上。 为了绘制这个,图形。DrawImage(位图)在我的重写绘制事件中,以允许在调整大小,最小化等时重新绘制表单。
我使用 Invoke 调用来发出信号以使表单无效以启动绘制事件,这可能不是正确的方法,这就是我问你们的原因。
我遇到的问题是图形对象在从 BeginReceive 方法创建的线程以及主 UI 线程上被调用,同时引发异常。
问题 1:有没有一种好方法可以知道我何时收到套接字上的所有数据? 请记住,它不会在第一次传递时接收所有数据。 如果是这样,我可以以某种方式发出绘画事件的信号。
或
问题 2:我是否应该在某处阻止非 UI 线程以避免跨线程错误,然后调用主线程来绘制表单? 如果是这样,什么是好的流程?
这是我大部分时间都在做的事情:
private void paint_the_image()
{
Invoke(new Action(() =>
{
this.Invalidate();
}));
}
// the bitmap image is created when the program loads
public void paint_bitmap(int data_type, string text, int x, int y)
{
if (data_type == 1)
{
using( Graphics gr2 = Graphics.FromImage(bitmap_image))
{
gr2.FillRectangle(
Brushes.White,
35 + (x * 14), 200 + (y * 18), 14, 18);
gr2.DrawString(
text, new Font("Lucida Console", 14),
newBrush, 35 + (x * 14), 200 + (y * 18));
}
}
}
//paint event
private void main_sockets_Paint(object sender, PaintEventArgs e)
{
using(Graphics gr2 = Graphics.FromImage(bitmap_image))
{
gr2.DrawImage(bitmap_image, 35, 200, 1500, 700);
}
}
// this is called right after the socket connection is made.
public void wait_for_data()
{
if (receive_callBack == null)
{
receive_callBack = new AsyncCallback(on_data_received);
}
async_result = client_socket_connect.BeginReceive(
socket_data_buffer, 0, socket_data_buffer.Length,
SocketFlags.None, receive_callBack, null);
}
public void on_data_received(IAsyncResult async)
{
int char_count = 0;
char_count = client_socket_connect.EndReceive(async);
char[] chars = new char[char_count + 1];
System.Text.Decoder decode = System.Text.Encoding.UTF8.GetDecoder();
int char_length = decode.GetChars(socket_data_buffer, 0, char_count, chars, 0);
//processing data....
paint_bitmap(1, txt.ToString(), ex, why);
wait_for_data();
}
我是异步连接的新手,所以请随意将其分开。 任何建议都会有所帮助。
这么多错误...从哪里开始?
好吧,让我们从您的问题开始,重点关注您看到需要关注的线程安全问题:
我使用 Invoke 调用来发出信号以使表单无效以启动 paint 事件,这可能不是正确的方法
实际上,这是在 WinForms 中处理屏幕更新的预期方法:当某些数据更改需要反映在屏幕上时,您将使显示该数据的控件无效,并让控件在引发Paint
事件的情况下执行绘图。在许多情况下,控件在内部处理这一切;例如TextBox
、Label
、ListBox
等,代码通常会更新控件在内部存储的数据,并且控件处理屏幕刷新。
在您的情况下,您将位图直接绘制到屏幕上(嗯,有点......代码看起来不起作用,但我可以推断出您要做什么)。因此,正确的方法是更新位图,然后使显示位图的控件失效,以便该控件的Paint
处理可以将位图绘制到屏幕上。
有没有好方法可以知道我何时收到套接字上的所有数据?
这取决于您所说的"所有数据"是什么意思。您尚未解释应用程序的网络协议,因此我们不知道这意味着什么。在某些情况下,单个 TCP 连接用于传输多个数据块;在这种情况下,应用程序协议必须定义某种方法来分隔这些块。对于二进制协议,这通常是实际数据之前的字节计数。对于基于文本的协议,这可以是特殊字符(例如'\0',换行符,LF/CR对等),甚至可以是某种结构化数据(例如.XML元素)。
在其他情况下,"所有数据"是指在单个连接中发送的每个最后一个字节。在这种情况下,您只需等待流结束指示,即接收操作以 0 字节返回值完成。
我遇到的问题是图形对象在从 BeginReceive 方法创建的线程以及主 UI 线程上被调用,同时引发异常。
据我所知,以上是您的主要问题。解决此问题的最直接方法是同步两个线程。这确实意味着在执行网络 I/O 代码时,UI 线程可能会被暂时阻止,但网络 I/O 代码应该能够足够快地运行,并且运行时间足够短,这不会成为问题。
同步可能如下所示:
private readonly object _lock = new object();
// the bitmap image is created when the program loads
public void paint_bitmap(int data_type, string text, int x, int y)
{
if (data_type == 1)
{
lock (_lock)
{
using( Graphics gr2 = Graphics.FromImage(bitmap_image))
{
gr2.FillRectangle(
Brushes.White,
35 + (x * 14), 200 + (y * 18), 14, 18);
gr2.DrawString(
text, new Font("Lucida Console", 14),
newBrush, 35 + (x * 14), 200 + (y * 18));
}
}
}
}
//paint event
private void main_sockets_Paint(object sender, PaintEventArgs e)
{
lock (_lock)
{
e.Graphics.DrawImage(bitmap_image, 35, 200, 1500, 700);
}
}
(我不知道我对Paint
事件处理程序所做的更改是否完全正确,但它肯定比您以前的代码更正确,在以前的代码中,您只是出于某种原因将位图的内容绘制到自身)。
上面使用_lock
对象来同步对bitmap_image
对象的访问,确保一次只有一个线程实际使用它。
<旁白>旁白>关于您提供的代码示例的一些要点:示例中没有任何地方实际调用paint_the_image()
,也没有显示txt
变量的声明或初始化。ex
和why
变量(顺便说一下,命名非常糟糕)也没有显示,但这似乎不是主要问题的核心。
底线:该示例远非好的、最小的、完整的代码示例,这些示例将成为每个好的 Stack Overflow 问题的一部分,但鉴于这些关键细节的遗漏,它尤其糟糕。如果你觉得你没有得到你需要的帮助,你应该花时间清理你的问题,以便它 a) 更集中(即一次处理一个问题,并确保你在继续下一个问题之前让该部分干净利落地工作),以及 b) 包括所需的 MCVE。
现在,关于此代码的其他一些想法:
- 上述同步的替代方法是对渲染进行双缓冲(甚至三重缓冲)。无论如何,您似乎每次都会清除位图(顺便说一下,您可以使用
Graphics.Clear()
方法),因此您无需将图像从一帧保留到下一帧。
因此,您可以只维护两个Bitmap
对象并交替绘制它们。您仍然需要同步,但这可以通过以下形式完成:在绘制时锁定 UI 线程,并仅在交换缓冲区时锁定网络 I/O 代码。由于交换缓冲区相当于简单地交换数组中的引用甚至标志或索引(取决于您如何实现缓冲区"链"),因此可以保证快速,并且永远不会延迟 UI 线程任何长时间。
例如:
private readonly object _lock = new object();
private bool _useBufferB;
// the bitmap image is created when the program loads
public void paint_bitmap(int data_type, string text, int x, int y)
{
if (data_type == 1)
{
// Note that this test is reversed from the Paint handler one
using( Graphics gr2 =
Graphics.FromImage(_useBufferB ? bitmap_image : bitmap_imageB))
{
gr2.FillRectangle(
Brushes.White,
35 + (x * 14), 200 + (y * 18), 14, 18);
gr2.DrawString(
text, new Font("Lucida Console", 14),
newBrush, 35 + (x * 14), 200 + (y * 18));
}
lock (_lock)
{
_useBufferB = !_useBufferB;
}
}
}
//paint event
private void main_sockets_Paint(object sender, PaintEventArgs e)
{
lock (_lock)
{
e.Graphics.DrawImage(
_useBufferB ? bitmap_imageB : bitmap_image, 35, 200, 1500, 700);
}
}
- 接收
- 数据时遇到多个问题。
由于以下几个原因,以下代码存在问题:
int char_count = 0;
char_count = client_socket_connect.EndReceive(async);
char[] chars = new char[char_count + 1];
首先,如果您只是要在下一条语句中为char_count
赋值,则没有理由将初始化为0
。但更成问题的是,除非您使用单字节字符编码(例如 ASCII),否则没有理由期望EndReceive()
返回的字节计数对应于字符计数。
这已经够糟糕的了,但是你有这个:
System.Text.Decoder decode = System.Text.Encoding.UTF8.GetDecoder();
int char_length = decode.GetChars(socket_data_buffer, 0, char_count, chars, 0);
使用Decoder
对象可能是一件好事。 除了你做错了。使用Decoder
而不仅仅是调用Encoding.GetString()
或类似内容的全部意义在于,Decoder
对象有一个内部缓冲区来存储不完整的字符数据,以便在后续接收时它可以从中断的地方继续,并确保正确处理在接收操作中拆分的多字节字符。当您丢弃Decoder
对象并为每个接收操作创建一个全新的对象时,您也会丢弃此内部缓冲区并否定使用Decoder
的好处。
别这样。
更好的可能是这样的:
private readonly Decoder _decoder = new Encoding.UTF8.GetDecoder();
public void on_data_received(IAsyncResult async)
{
int byte_count = client_socket_connect.EndReceive(async);
char[] chars = new char[_decoder.GetCharCount(socket_data_buffer, 0, byte_count)];
_decoder.GetChars(socket_data_buffer, 0, byte_count, chars, 0);
//processing data....
paint_bitmap(1, new string(chars), ex, why);
wait_for_data();
}
虽然是肯定的,但如果没有一个好的代码示例,就不可能确定这是完全正确的事情(即,你真的想把解码的文本直接传递给paint_bitmap()
方法,还是你打算先进一步处理该文本?)。
当然,在某些时候,您肯定会希望引入对
paint_the_image()
方法的调用。毕竟,没有它,UI 线程将不知道重绘任何东西。也许这个评论应该是第一个......我不知道。我只是想把以上所有内容都排除在外,因为如果你想以后使用这些技术,无论如何你都需要知道这些东西。
但是,真的:我想知道你为什么要使用Bitmap
来完成所有这些。您似乎所做的只是将文本数据复制到位图,Winforms确实具有各种其他文本友好控件,您可以使用它们来执行此操作。即使是一个简单的TextBox
(将Multiline
设置为true
以记录多行,如果您希望使用无法修改contets,请将ReadOnly
设置为true
),或ListBox
,或RichTextBox
,或Label
,或...
有很多选择,所有这些选择都比处理自己的位图缓冲区要渲染更容易,可能更有效。