天天看點

.net 中異步SOCKET發送資料時碰到的記憶體問題

做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裡直接編譯)

using System;  

using System.Collections.Generic;  

using System.Linq;  

using System.Text;  

using System.Net.Sockets;  

namespace TestAsyncSendMem  

{  

    class Program  

    {  

        static TcpListener m_lisnter;  

        static AsyncCallback m_acb = new AsyncCallback(DoAcceptSocketCallback);  

        static void Main(string[] args)  

        {  

            m_lisnter = new System.Net.Sockets.TcpListener(System.Net.IPAddress.Any, 8001);  

            m_lisnter.Start(5 * 1000);  

            try  

            {  

                m_lisnter.BeginAcceptSocket(m_acb, null);  

            }  

            catch (Exception ex)  

                m_lisnter.Stop();  

                m_lisnter = null;  

                System.Diagnostics.Debug.WriteLine("BeginAcceptSocket err.Start fail!" + ex);  

                return;  

            Console.WriteLine("Begin receiving connection... Press any key to quit.");  

            Console.ReadKey();  

            m_lisnter.Stop();  

        }  

        static void DoAcceptSocketCallback(IAsyncResult ar)  

            System.Net.Sockets.Socket s = null;  

                s = m_lisnter.EndAcceptSocket(ar);  

                System.Diagnostics.Debug.WriteLine("End Accept socket err" + ex);  

                s = null;  

                System.Diagnostics.Debug.WriteLine("after accept client socket,Re beginAcceptSocket fail." + ex);  

            if (s != null)  

                #region...  

                CTcpClientSync c = new CTcpClientSync(s);  

                Console.WriteLine(string.Format("accept client.{0}", c.Socket.RemoteEndPoint));  

                if (c.BeginRcv() == true)  

                {  

                    c.OnDisconnected += (CTcpClientSync client) =>  

                    {  

                        System.Diagnostics.Debug.WriteLine(string.Format("client {0} disconected", client.RemoteIP));  

                    };  

                }  

                else  

                    c.Stop();  

                    System.Diagnostics.Debug.WriteLine(string.Format("accepted client {0} removed.cannot begin rcv", c.RemoteIP));  

                #endregion  

    }  

    public class CTcpClientSync  

        #region delegate  

        public delegate void dlgtDisconnected(CTcpClientSync c);  

        public event dlgtDisconnected OnDisconnected;  

        #endregion  

        #region prop  

        Socket m_skt = null;  

        public Socket Socket { get { return m_skt; } }  

        string m_strRemoteIP;  

        public string RemoteIP { get { return m_strRemoteIP; } }  

        byte[] m_arybytBuf = new byte[1024];  

        AsyncCallback m_acb = null;  

        public CTcpClientSync(Socket skt)  

            m_acb = new AsyncCallback(DoBeginRcvData);  

            m_skt = skt;  

                m_strRemoteIP = skt.RemoteEndPoint.ToString();  

                System.Diagnostics.Debug.WriteLine("get remote end point exception."+ ex);  

        public void Stop()  

            m_skt.Close();  

        #region Raise event  

        void RaiseDisconnectedEvent()  

            dlgtDisconnected handler = OnDisconnected;  

            if (handler != null)  

                try  

                    handler(this);  

                catch (Exception ex)  

                    System.Diagnostics.Debug.WriteLine("Raise disconn event exception." + ex.Message);  

        public bool BeginRcv()  

                m_skt.BeginReceive(m_arybytBuf, 0, m_arybytBuf.Length, SocketFlags.None, m_acb, null);  

                System.Diagnostics.Debug.WriteLine("BeginRcv exception." + ex);  

                return false;  

            return true;  

        void DoBeginRcvData(IAsyncResult ar)  

            int iReaded = 0;  

                iReaded = m_skt.EndReceive(ar);  

                Stop();  

                RaiseDisconnectedEvent();  

            if (iReaded > 0)  

                //收到後發送回一個資料包  

                SendAsync(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 });  

                if (BeginRcv() == false)  

                    Stop();  

                    RaiseDisconnectedEvent();  

            else  

        public bool SendAsync(byte[] bytsCmd)  

            SocketAsyncEventArgs e = new SocketAsyncEventArgs();  

                e.SetBuffer(bytsCmd, 0, bytsCmd.Length);  

                System.Diagnostics.Debug.WriteLine("SetBuffer exception." + ex);  

            }              

                if (m_skt.SendAsync(e))  

                {//Returns true if the I/O operation is pending.  

                    return true;  

                System.Diagnostics.Debug.WriteLine("SendAsync exception." + ex);                  

            //Returns false if the I/O operation completed synchronously.   

            //In this case, The SocketAsyncEventArgs.Completed event on the e parameter will not be raised and   

            //the e object passed as a parameter may be examined immediately after the method call returns to retrieve the result of the operation.  

}  

.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.

條件1:當實體記憶體極低時會調用

如上所說,我在一個伺服器上測試此程式,8G記憶體,2W個連接配接,每5秒種給所有的連接配接發送一次。在大概4個小時就把所有的記憶體完了。從任務管理器上看,記憶體占用了7.9個G。并且,此時SERVER程式已經無法接受發送來自用戶端的資料了。是以,按這個情況,記憶體回收肯定應該工作了!但沒有!

條件2:已經在托管heap上配置設定的對象所占用的記憶體超過一個閥值時會調用。這個閥值會動态變更。

如上一個測試,實體記憶體都已經用光了,并導緻程式不能正常運作了。這個閥值還沒有超過?!!這個閥值是怎麼定的呢?(需要找一下文檔,網友了解的提供一下 :))

假定是因為某種原因,GC沒有執行。那我們手動的執行一下,添加一個全局變量 s_iRcvTimes  ,每接收5000次就執行一下回收

public bool SendAsync(byte[] bytsCmd)  

    if (s_iRcvTimes > 5000)  

        s_iRcvTimes = 0;  

        GC.Collect(2);  

    s_iRcvTimes += 1;  

...//原來的代碼省略  

測試結果如下:(程式啟動後,每過一段時間記錄一下SERVER程式的記憶體占用情況)

程式的啟動記憶體占用為:4.5M 

序号

時間

時間間隔

記憶體占用

記憶體增長

1

16:07:00

1分鐘

22,023K

--

2

16:08:00

22,900K

677K

3

16:10:00

2分鐘

26,132K

3,232K

4

16:12:00

30,172K

4,040K

5

16:17:00

5分鐘

116,032K

85,860K

6

16:22:00

200,146K

84,114K

7

16:27:00

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

修改後的代碼如下:添加一個變量

public static List<SocketAsyncEventArgs> s_lst = new List<SocketAsyncEventArgs>();  

然後修改SendAsync 函數如下:

              SocketAsyncEventArgs e = null;//new SocketAsyncEventArgs();  

            lock (Program.s_lst)  

                if (Program.s_lst.Count > 0)  

                    e = Program.s_lst[Program.s_lst.Count - 1];  

                    Program.s_lst.RemoveAt(Program.s_lst.Count - 1);  

            if (e == null)  

                e = new SocketAsyncEventArgs();  

                e.Completed += (object sender, SocketAsyncEventArgs _e) =>  

                    lock (Program.s_lst)  

                        Program.s_lst.Add(e);  

                };  

            }   

                lock (Program.s_lst)  

                    Program.s_lst.Add(e);  

                Program.s_lst.Add(e);  

        }   

方法應該比較簡單:不是每次都建立新的對象,而是用完後儲存起來給下次調用時使用。

現在的問題比較明确了:

為什麼這裡的 new SocketAsyncEventArgs() 會無法被回收呢? 也就是說:一直被某個對象引用着,無法成為 dead object.

明天繼續... :)

轉 http://blog.csdn.net/ani/article/details/7182035

之前筆者也遇到這個問題,後來根據此文得以解決,特轉載下來。

QQ:519841366

本頁版權歸作者和部落格園所有,歡迎轉載,但未經作者同意必須保留此段聲明,

且在文章頁面明顯位置給出原文連結,否則保留追究法律責任的權利