海康設備單機限 6 客戶端。方案:中轉伺服器 單一連線取流 → 內存緩存 → Socket 分發給多個 Web ActiveX 客戶端。
NET_DVR_Init() // 初始化 SDK
NET_DVR_Login_V30(ip, port, user, pwd) // 登入設備
NET_DVR_RealPlay_V30(..., RealDataCallBack) // 開始預覽,註冊回調
// RealDataCallBack 中接收視訊資料
class Server
{
int _userId = -1;
byte[] _headStream; // 40 bytes 頭部
ClientList _clients = ClientList.Instance;
Socket _listenSocket;
Semaphore _maxClients;
public Server(IPEndPoint localEp, int maxClients, int channel)
{
if (!InitSdk()) return;
StartRealPlay(channel);
_listenSocket = new Socket(localEp.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
_listenSocket.Bind(localEp);
_maxClients = new Semaphore(maxClients, maxClients);
_listenSocket.Listen(100);
BeginAccept(null);
}
bool InitSdk()
{
if (!CHCNetSDK.NET_DVR_Init()) return false;
var info = new CHCNetSDK.NET_DVR_DEVICEINFO_V30();
_userId = CHCNetSDK.NET_DVR_Login_V30("DEV_IP", 8000, "admin", "pwd", ref info);
return _userId != -1;
}
void StartRealPlay(int channel)
{
var clientInfo = new CHCNetSDK.NET_DVR_CLIENTINFO { lChannel = channel, lLinkMode = 0 };
var cb = new CHCNetSDK.REALDATACALLBACK(RealDataCallback);
CHCNetSDK.NET_DVR_RealPlay_V30(_userId, ref clientInfo, cb, IntPtr.Zero, 1);
}
void RealDataCallback(int handle, uint dataType, IntPtr buf, uint size, IntPtr user)
{
byte[] data = new byte[size];
Marshal.Copy(buf, data, 0, (int)size);
if (dataType == CHCNetSDK.NET_DVR_SYSHEAD) _headStream = data; // 保存 40 bytes
_clients.Broadcast(data); // 分發給所有客戶端
}
// Socket Async Accept 實作...
}
class ClientList
{
static ClientList _instance;
List<Client> _clients = new List<Client>();
public static ClientList Instance { get { if (_instance==null) _instance=new ClientList(); return _instance; } }
public void AddClient(Client c) { _clients.Add(c); }
public void Broadcast(byte[] data)
{
// 移除斷開連線的客戶端
_clients.RemoveAll(c => c.Error != SocketError.Success);
// CPU 保護:使用率 > 50% 時微延遲
var cpu = new PerformanceCounter("Processor", "% Processor Time", "_Total");
foreach (var c in _clients)
{
c.Send(data);
if (cpu.NextValue() > 50) Thread.Sleep(10);
}
}
}
class Client
{
Socket _socket;
byte[] _headStream;
ManualResetEvent _sendSignal = new ManualResetEvent(false);
byte[] _pendingData;
Thread _sendThread;
SocketError _error = SocketError.Success;
public Client(Socket socket, byte[] headStream)
{
_socket = socket; _headStream = headStream;
_sendThread = new Thread(() => {
while (true)
{
_sendSignal.WaitOne();
if (_error == SocketError.Success)
{
try { _socket.Send(_pendingData); }
catch { Destroy(); break; }
}
_sendSignal.Reset();
}
}) { IsBackground = true };
_sendThread.Start();
}
public void Send(byte[] data)
{
if (_error != SocketError.Success) return;
// 首次發送頭部 40 bytes,之後發送資料幀
_pendingData = (_pendingData == null && _headStream.Length == 40) ? _headStream : data;
_sendSignal.Set();
}
void Destroy() { /* 關閉 Socket、結束執行緒、標記錯誤 */ }
public SocketError Error { get { return _error; } }
}
| 決策 | 理由 |
|---|---|
| Client 專用發送執行緒而非 ThreadPool | 持續高頻發送,避免執行緒池耗盡、排隊延遲。 |
| ManualResetEvent 控制發送順序 | 確保幀序嚴格遞增,防止亂序導致解碼錯誤/跳幀。 |
| Server 獨立保存 Head 40 bytes | 每個新客戶端連線必須先收到 Head 才能初始化解碼器。 |
| PerformanceCounter 限流 | 防止 CPU 100% 導致取流延遲、丟幀。 |
第三篇將實作 Web ActiveX 播放器,接收中轉伺服器資料並解碼顯示。