做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
本頁版權歸作者和部落格園所有,歡迎轉載,但未經作者同意必須保留此段聲明,
且在文章頁面明顯位置給出原文連結,否則保留追究法律責任的權利