C#中异步SOCKET发送数据时内存问题

来源:互联网 发布:深圳市信诚网络 编辑:程序博客网 时间:2024/05/17 22:08

做CS的开发一直都是这样的方式:

server端用 C++编写,采用IOCP机制处理大量客户端连接、数据接收发送的问题

client端用 C++ 或C# 写,没什么特殊要求。


最近工作时间上比较宽裕,决定采用新的方式来处理服务端的工作: C# + SOCKET异步机制(.net里没有IOCP的直接支持)

目前正可行性分析阶段,第一步的工作:接收3W个SOCKET连接, 结果还是不错的,很快就建立起来了,速度也可以。

但是第二步测试,接收、发送数据时,就发生了点问题:

   运行的SERVER程序在较短的时间内就占用了大量的内存!


我的测试环境:i3 +2G内存 + Win732位

客户端创建5000个连接,每间隔1秒种对所有的连接发送、接收一次数据。每次发送20bytes到server。

服务端与客户端不在同一台机器上

一般情况下,程序的启动内存占用为4.5M ,运行5分钟后,SERVER程序内存占用超过 100M,并且还在不停的快速增长


在一台服务器上测试(2W个连接),4个小时内,把8G内存全部用光(从任务管理器上看,使用了7.9G内存)



先看SERVER端的完整代码:(大家可以COPY到自己的IDE里直接编译)

[csharp] view plaincopy
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Linq;  
  4. using System.Text;  
  5. using System.Net.Sockets;  
  6.   
  7. namespace TestAsyncSendMem  
  8. {  
  9.     class Program  
  10.     {  
  11.         static TcpListener m_lisnter;  
  12.         static AsyncCallback m_acb = new AsyncCallback(DoAcceptSocketCallback);  
  13.         static void Main(string[] args)  
  14.         {  
  15.             m_lisnter = new System.Net.Sockets.TcpListener(System.Net.IPAddress.Any, 8001);  
  16.             m_lisnter.Start(5 * 1000);  
  17.   
  18.             try  
  19.             {  
  20.                 m_lisnter.BeginAcceptSocket(m_acb, null);  
  21.             }  
  22.             catch (Exception ex)  
  23.             {  
  24.                 m_lisnter.Stop();  
  25.                 m_lisnter = null;  
  26.   
  27.                 System.Diagnostics.Debug.WriteLine("BeginAcceptSocket err.Start fail!" + ex);  
  28.                 return;  
  29.             }  
  30.             Console.WriteLine("Begin receiving connection... Press any key to quit.");  
  31.   
  32.             Console.ReadKey();  
  33.   
  34.             m_lisnter.Stop();  
  35.         }  
  36.   
  37.         static void DoAcceptSocketCallback(IAsyncResult ar)  
  38.         {  
  39.             System.Net.Sockets.Socket s = null;  
  40.             try  
  41.             {  
  42.                 s = m_lisnter.EndAcceptSocket(ar);  
  43.             }  
  44.             catch (Exception ex)  
  45.             {  
  46.                 System.Diagnostics.Debug.WriteLine("End Accept socket err" + ex);  
  47.                 s = null;  
  48.             }  
  49.   
  50.             try  
  51.             {  
  52.                 m_lisnter.BeginAcceptSocket(m_acb, null);  
  53.             }  
  54.             catch (Exception ex)  
  55.             {  
  56.                 System.Diagnostics.Debug.WriteLine("after accept client socket,Re beginAcceptSocket fail." + ex);  
  57.             }  
  58.   
  59.             if (s != null)  
  60.             {  
  61.                 #region...  
  62.                 CTcpClientSync c = new CTcpClientSync(s);  
  63.                 Console.WriteLine(string.Format("accept client.{0}", c.Socket.RemoteEndPoint));  
  64.   
  65.                 if (c.BeginRcv() == true)  
  66.                 {  
  67.                     c.OnDisconnected += (CTcpClientSync client) =>  
  68.                     {  
  69.                         System.Diagnostics.Debug.WriteLine(string.Format("client {0} disconected", client.RemoteIP));  
  70.                     };  
  71.                 }  
  72.                 else  
  73.                 {  
  74.                     c.Stop();  
  75.                     System.Diagnostics.Debug.WriteLine(string.Format("accepted client {0} removed.cannot begin rcv", c.RemoteIP));  
  76.                 }  
  77.                 #endregion  
  78.             }  
  79.   
  80.         }  
  81.     }  
  82.   
  83.     public class CTcpClientSync  
  84.     {  
  85.         #region delegate  
  86.   
  87.         public delegate void dlgtDisconnected(CTcpClientSync c);  
  88.         public event dlgtDisconnected OnDisconnected;  
  89.  
  90.         #endregion  
  91.  
  92.         #region prop  
  93.   
  94.         Socket m_skt = null;  
  95.         public Socket Socket { get { return m_skt; } }  
  96.   
  97.         string m_strRemoteIP;  
  98.         public string RemoteIP { get { return m_strRemoteIP; } }  
  99.   
  100.         byte[] m_arybytBuf = new byte[1024];  
  101.   
  102.         AsyncCallback m_acb = null;  
  103.  
  104.         #endregion  
  105.   
  106.         public CTcpClientSync(Socket skt)  
  107.         {  
  108.             m_acb = new AsyncCallback(DoBeginRcvData);  
  109.   
  110.             m_skt = skt;  
  111.             try  
  112.             {  
  113.                 m_strRemoteIP = skt.RemoteEndPoint.ToString();  
  114.             }  
  115.             catch (Exception ex)  
  116.             {  
  117.                 System.Diagnostics.Debug.WriteLine("get remote end point exception."+ ex);  
  118.             }  
  119.   
  120.         }  
  121.   
  122.         public void Stop()  
  123.         {  
  124.             m_skt.Close();  
  125.         }  
  126.  
  127.         #region Raise event  
  128.         void RaiseDisconnectedEvent()  
  129.         {  
  130.             dlgtDisconnected handler = OnDisconnected;  
  131.             if (handler != null)  
  132.             {  
  133.                 try  
  134.                 {  
  135.                     handler(this);  
  136.                 }  
  137.                 catch (Exception ex)  
  138.                 {  
  139.                     System.Diagnostics.Debug.WriteLine("Raise disconn event exception." + ex.Message);  
  140.                 }  
  141.             }  
  142.         }  
  143.         #endregion  
  144.   
  145.         public bool BeginRcv()  
  146.         {  
  147.             try  
  148.             {  
  149.                 m_skt.BeginReceive(m_arybytBuf, 0, m_arybytBuf.Length, SocketFlags.None, m_acb, null);  
  150.             }  
  151.             catch (Exception ex)  
  152.             {  
  153.                 System.Diagnostics.Debug.WriteLine("BeginRcv exception." + ex);  
  154.                 return false;  
  155.             }  
  156.   
  157.             return true;  
  158.         }  
  159.   
  160.         void DoBeginRcvData(IAsyncResult ar)  
  161.         {  
  162.             int iReaded = 0;  
  163.             try  
  164.             {  
  165.                 iReaded = m_skt.EndReceive(ar);  
  166.             }  
  167.             catch (Exception ex)  
  168.             {  
  169.                 System.Diagnostics.Debug.WriteLine("BeginRcv exception." + ex);  
  170.   
  171.                 Stop();  
  172.                 RaiseDisconnectedEvent();  
  173.   
  174.                 return;  
  175.             }  
  176.   
  177.             if (iReaded > 0)  
  178.             {  
  179.                 //收到后发送回一个数据包  
  180.                 SendAsync(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 });  
  181.   
  182.                 if (BeginRcv() == false)  
  183.                 {  
  184.                     Stop();  
  185.                     RaiseDisconnectedEvent();  
  186.                 }  
  187.             }  
  188.             else  
  189.             {  
  190.                 Stop();  
  191.                 RaiseDisconnectedEvent();  
  192.             }  
  193.         }  
  194.   
  195.         public bool SendAsync(byte[] bytsCmd)  
  196.         {  
  197.             SocketAsyncEventArgs e = new SocketAsyncEventArgs();  
  198.             try  
  199.             {  
  200.                 e.SetBuffer(bytsCmd, 0, bytsCmd.Length);  
  201.             }  
  202.             catch (Exception ex)  
  203.             {  
  204.                 System.Diagnostics.Debug.WriteLine("SetBuffer exception." + ex);  
  205.                 return false;  
  206.             }              
  207.   
  208.             try  
  209.             {  
  210.                 if (m_skt.SendAsync(e))  
  211.                 {//Returns true if the I/O operation is pending.  
  212.                     return true;  
  213.                 }  
  214.             }  
  215.             catch (Exception ex)  
  216.             {  
  217.                 System.Diagnostics.Debug.WriteLine("SendAsync exception." + ex);                  
  218.   
  219.                 return false;  
  220.             }  
  221.   
  222.             //Returns false if the I/O operation completed synchronously.   
  223.             //In this case, The SocketAsyncEventArgs.Completed event on the e parameter will not be raised and   
  224.             //the e object passed as a parameter may be examined immediately after the method call returns to retrieve the result of the operation.  
  225.   
  226.             return true;  
  227.   
  228.         }  
  229.     }  
  230. }  

.net中的内存是由系统自行回收的,一旦一个对象(内存块)发现没有被其它任何人使用(引用)则会被回收。

当满足以下条件之一时将发生垃圾回收:

  • The system has low physical memory.

  • The memory that is used by allocated objects on the managed heap surpasses an acceptable threshold. This means that a threshold of acceptable memory usage has been exceeded on the managed heap.This threshold is continuously adjusted as the process runs.

  • The GC.Collect method is called. In almost all cases, you do not have to call this method, because the garbage collector runs continuously. This method is primarily used for unique situations and testing.

具体请看这里(垃圾回收)

条件1:当物理内存极低时会调用

如上所说,我在一个服务器上测试此程序,8G内存,2W个连接,每5秒种给所有的连接发送一次。在大概4个小时就把所有的内存完了。从任务管理器上看,内存占用了7.9个G。并且,此时SERVER程序已经无法接受发送来自客户端的数据了。所以,按这个情况,内存回收肯定应该工作了!但没有!

条件2:已经在托管heap上分配的对象所占用的内存超过一个阀值时会调用。这个阀值会动态变更。

如上一个测试,物理内存都已经用光了,并导致程序不能正常运行了。这个阀值还没有超过?!!这个阀值是怎么定的呢?(需要找一下文档,网友了解的提供一下 :))

假定是因为某种原因,GC没有执行。那我们手动的执行一下,添加一个全局变量 s_iRcvTimes  ,每接收5000次就执行一下回收

[csharp] view plaincopy
  1. public bool SendAsync(byte[] bytsCmd)  
  2. {  
  3.     if (s_iRcvTimes > 5000)  
  4.     {  
  5.         s_iRcvTimes = 0;  
  6.         GC.Collect(2);  
  7.     }  
  8.     s_iRcvTimes += 1;  
[csharp] view plaincopy
  1. ...//原来的代码省略  

测试结果如下:(程序启动后,每过一段时间记录一下SERVER程序的内存占用情况)

程序的启动内存占用为:4.5M 

序号时间时间间隔内存占用内存增长116:07:001分钟22,023K--216:08:001分钟22,900K 677K316:10:002分钟26,132K 3,232K416:12:002分钟30,172K 4,040K516:17:005分钟116,032K 85,860K616:22:005分钟200,146K 84,114K716:27:005分钟274,120K 73,974K

内存占用:对应时刻Server程序所占用的内存(从windows任务管理器看到的数据)


从测试结果来看,应该没有起到作用!


我感觉,还是程序有问题!理论上来说,一旦内存不够,则系统自动进行回收,但是,为什么这里的情况不进行回收呢?!!MSDN里有这样一句话:

When a garbage collection is triggered, the garbage collector reclaims the memory that is occupied by dead objects.

所以,有可能有些对象根本都没有成为 dead objects,从而使GC没办法将其回收。


OK ,那先找到内存爆涨的地方,再来分析为什么这些对象没办法成为 dead object !


内存出问题,那肯定是NEW出来的没有被回收!

程序中,NEW的地方有几个:

1.收到数据后,回送的地方:SendAsync(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 });

2.接收数据里的异步操作支持:SocketAsyncEventArgs e = new SocketAsyncEventArgs();


下意识的觉得第二个地方比较可疑。所以,修改此处并测试。 内存果然有明显的提升:

两个测试:

1. 5K个连接,每次给每个连接发送20个bytes,发完后停止1秒再继续。

2. 1K个连接,每次给每个连接发送20个bytes,发完后停止1秒再继续。

测试的结果,可以坚持10分钟以上没有任何问题,这期间内存一直在30M以下,启动后当所有的连接连上来之后(开始发送数据之前)程序大概占用20M


其实MSDN里也有对这个的例子,例子中对这个 SocketAsyncEventArgs也是给缓存起来,而不是直接每次都NEW一个对象。它的例子详细请看这里


修改后的代码如下:添加一个变量

[csharp] view plaincopy
  1. public static List<SocketAsyncEventArgs> s_lst = new List<SocketAsyncEventArgs>();  

然后修改SendAsync 函数如下:

[csharp] view plaincopy
  1.     
  2. public bool SendAsync(byte[] bytsCmd)  
  3.         {  
  4.               SocketAsyncEventArgs e = null;//new SocketAsyncEventArgs();  
  5.             lock (Program.s_lst)  
  6.             {  
  7.                 if (Program.s_lst.Count > 0)  
  8.                 {  
  9.                     e = Program.s_lst[Program.s_lst.Count - 1];  
  10.                     Program.s_lst.RemoveAt(Program.s_lst.Count - 1);  
  11.                 }  
  12.             }  
  13.             if (e == null)  
  14.             {  
  15.                 e = new SocketAsyncEventArgs();  
  16.                 e.Completed += (object sender, SocketAsyncEventArgs _e) =>  
  17.                 {  
  18.                     lock (Program.s_lst)  
  19.                         Program.s_lst.Add(e);  
  20.                 };  
  21.             }   
  22.   
  23.             try  
  24.             {  
  25.                 e.SetBuffer(bytsCmd, 0, bytsCmd.Length);  
  26.             }  
  27.             catch (Exception ex)  
  28.             {  
  29.                 lock (Program.s_lst)  
  30.                     Program.s_lst.Add(e);  
  31.                 System.Diagnostics.Debug.WriteLine("SetBuffer exception." + ex);  
  32.                 return false;  
  33.             }  
  34.   
  35.             try  
  36.             {  
  37.                 if (m_skt.SendAsync(e))  
  38.                 {//Returns true if the I/O operation is pending.  
  39.                     return true;  
  40.                 }  
  41.             }  
  42.             catch (Exception ex)  
  43.             {  
  44.                 System.Diagnostics.Debug.WriteLine("SendAsync exception." + ex);                  
  45.   
  46.                 return false;  
  47.             }  
  48.   
  49.             //Returns false if the I/O operation completed synchronously.   
  50.             //In this case, The SocketAsyncEventArgs.Completed event on the e parameter will not be raised and   
  51.             //the e object passed as a parameter may be examined immediately after the method call returns to retrieve the result of the operation.  
  52.             lock (Program.s_lst)  
  53.                 Program.s_lst.Add(e);  
  54.             return true;  
  55.   
  56.         }   

方法应该比较简单:不是每次都创建新的对象,而是用完后保存起来给下次调用时使用。

基本上我们可以认为发送数据的那一部分中有问题:每一次发送,有两个NEW的动作,产生两个新的对象。现在问题的关键是:为什么这两个新的对象没办法被回收!我们先做一实验,代码如下:

[csharp] view plaincopy
  1. namespace Test2  
  2. {  
  3.     class Program  
  4.     {  
  5.         static void Main(string[] args)  
  6.         {  
  7.             Console.WriteLine("Press any key to start.");  
  8.             Console.ReadKey();  
  9.   
  10.             for (int i = 0; i < 1000000; ++i)  
  11.             {  
  12.                 for (int j = 0; j < 10000; ++j)  
  13.                 {  
  14.                     SocketAsyncEventArgs e = new SocketAsyncEventArgs();  
  15.                 }  
  16.                 System.Threading.Thread.Sleep(1000);  
  17.             }  
  18.         }  
  19.     }  
  20. }  

观察了几次,每次5分钟左右,内存波动范围:

最低:8,512K

最高:13,516K

内存不是固定在某个值,而是在这个区间内不停的变动,而且幅度比较大。估计是内存增长到一定的范围(上一章说的那个阀值?)就自动进行回收了,所 以,内存马上又下降,从而使得我们看到内存在不停的波动。


现在我们每次得到10000个对象之后,就手动处理一下内存回收,看什么结果,代码改为这样:

[csharp] view plaincopy
  1. for (int i = 0; i < 1000000; ++i)    
  2. {    
  3.     for (int j = 0; j < 10000; ++j)    
  4.     {    
  5.         SocketAsyncEventArgs e = new SocketAsyncEventArgs();    
  6.     }    
  7.     GC.Collect(2);  
  8.     System.Threading.Thread.Sleep(1000);    
  9. }    

最低:8992

最高:10608

内存看起来(至少在我的机器上试了几次)稳定多了,变动的幅度也相比小一些,甚至有一会一直在(9800K-9900K)两个数字之间变化


上面的两个实验可能不是很准确,我的大概的意图是了解一下回收的效果,也可以让大家大概了解一下。

现在我们继续这个实验,当然要结合我上一文章的例子。代码如下:

[csharp] view plaincopy
  1. namespace Test2  
  2. {  
  3.     class Program  
  4.     {  
  5.         static void Main(string[] args)  
  6.         {  
  7.             Console.WriteLine("Press any key to start.");  
  8.             Console.ReadKey();  
  9.   
  10.             for (int i = 0; i < 1000000; ++i)  
  11.             {  
  12.                 for (int j = 0; j < 10000; ++j)  
  13.                 {  
  14.                     SocketAsyncEventArgs e = new SocketAsyncEventArgs();  
  15.                     e.SetBuffer(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1 }, 0, 10);  
  16.                 }  
  17.                 GC.Collect();  
  18.                 System.Threading.Thread.Sleep(1000);  
  19.             }  
  20.         }  
  21.     }  
  22. }  

这里把上一篇文章中的两个NEW动作都包含进来了,测试了一下,几乎是在几十秒内内存增长了60多M!

OK,问题就是出在这里了。

这里有一个明显的疑点:为什么加了这个SetBuffer 之后,内存的需求量就开始暴增呢?难道是这里new出来的byte数组没有被释放?从而导致两个新创建的对象都无法成为 dead object ?

修改代码如下:

[csharp] view plaincopy
  1. for (int i = 0; i < 1000000; ++i)  
  2. {  
  3.     for (int j = 0; j < 10000; ++j)  
  4.     {  
  5.         SocketAsyncEventArgs e = new SocketAsyncEventArgs();  
  6.         e.SetBuffer(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1 }, 0, 10);  
  7.   
  8.         <em>e.SetBuffer(null, 0, 0)</em>;  
  9.     }  
  10.     GC.Collect();  
  11.     System.Threading.Thread.Sleep(1000);  
  12. }  
测试了一下,非常正确,内存稳定了~~~


现在根据这个发现去修改上一文章中的例子,每次发送完数据就取消SocketAsyncEventArgs 对 byte数组的引用。代码如下:

[csharp] view plaincopy
  1. public bool SendAsync(byte[] bytsCmd)  
  2. {  
  3.     //下面5行为新增加、修改的行  
  4.     SocketAsyncEventArgs e = new SocketAsyncEventArgs();  
  5.     e.Completed += (object s, SocketAsyncEventArgs _e) =>  
  6.     {  
  7.         e.SetBuffer(null, 0, 0);  
  8.     };  
  9.   
  10.     try  
  11.     {  
  12.         e.SetBuffer(bytsCmd, 0, bytsCmd.Length);  
  13.     }  
  14.     catch (Exception ex)  
  15.     {  
  16.         lock (Program.s_lst)  
  17.             Program.s_lst.Add(e);  
  18.         System.Diagnostics.Debug.WriteLine("SetBuffer exception." + ex);  
  19.         return false;  
  20.     }  
  21.   
  22.     try  
  23.     {  
  24.         if (m_skt.SendAsync(e))  
  25.         {//Returns true if the I/O operation is pending.  
  26.             return true;  
  27.         }  
  28.     }  
  29.     catch (Exception ex)  
  30.     {  
  31.         System.Diagnostics.Debug.WriteLine("SendAsync exception." + ex);                  
  32.   
  33.         return false;  
  34.     }  
  35.   
  36.     e.SetBuffer(null, 0, 0);//新增加的行  
  37.   
  38.     return true;  
  39. }  

上面的代码还是按最初的方式处理:发送一次数据生成一个SocketAsyncEventArgs对象,但是,为了能使GC能正确的回收,在命令完成后,我们增加了e.SetBuffer(null,0,0) 这一句,以表明这几个对象都不用了,已经是 dead objects 了!

修改完后的代码测试 了20分钟,内存一直稳定在30M左右!



结论:

GC不是我们想象的那样聪明!两个dead object如果互相引用,它就没办法判断是否应该回收了!所以,用.net编程时,也要小心内存的处理,时刻要为GC着想、帮它一把!:)


0 0
原创粉丝点击