在 WebSocket、SignalR、gRPC Streaming 普及之前,TcpClient/TcpListener 搭配專用執行緒是構建即時雙向通訊的標準做法。本文完整展示一個金融報價即時推播系統,涵伺服器端多客戶端管理、客戶端非同步接收、ListView 跨執行緒 UI 更新、文字/Xml 雙格式協定解析、資料檔案載入推播等核心功能,為理解現代即時通訊底層機制提供珍貴參考。
┌─────────────┐ ┌─────────────┐ │ TcpServer │ │ TcpClient │ │ (Port 8002)│ │ │ │ │ 1. Listen │ │ │ ┌────────┐ │◄───────────────────┤ │ │ │Thread │ │ 2. Accept │ │ │ │Pool │ │ │ │ │ └────┬───┘ │ │ │ │ │ │ 3. Spawn Thread │ 4. Connect │ │ ┌────▼────┐│◄───────────────────┤ (IP/Port) │ │ │ClientMgr││ │ │ │ │Hashtable││ 5. Send Credentials │ │ └────┬────┘│───────────────────►│ │ │ │ │ 6. Spawn Recv Thread │ │ ┌────▼────┐│◄───────────────────┤ ┌────────┐ │ │ │RecvLoop ││ │ │RecvLoop│ │ │ └────┬────┘│ 7. Push Data │ └────┬───┘ │ │ │ │───────────────────►│ │ │ │ ┌────▼────┐│ (Text/Xml) │ ┌────▼───┐ │ │ │ListView ││ │ │ListView│ │ │ │Update ││ 8. UI Invoke │ │Update │ │ │ └─────────┘│◄───────────────────┤ └────────┘ │ └─────────────┘ └─────────────┘
public partial class ServerForm : Form
{
private TcpListener _listener;
private Hashtable _socketTable = new Hashtable(); // id → Socket
private Hashtable _threadTable = new Hashtable(); // id → Thread
private Hashtable _userTable = new Hashtable(); // id → UserName
private int _connectId = 0;
private const int MaxConnected = 100;
public ServerForm()
{
InitializeComponent();
_listener = new TcpListener(8002);
_listener.Start();
lblStatus.Text = $"Listen at: {_listener.LocalEndpoint}";
Thread acceptThread = new Thread(WaitingForClient);
acceptThread.Start();
}
private void WaitingForClient()
{
while (true)
{
Socket clientSocket = _listener.AcceptSocket(); // 阻塞等待
int id = Interlocked.Increment(ref _connectId) % 10000;
if (_socketTable.Count < MaxConnected)
{
lock (_socketTable)
{
while (_socketTable.ContainsKey(id))
id = Interlocked.Increment(ref _connectId) % 10000;
Thread worker = new Thread(ReadSocket);
_socketTable[id] = clientSocket;
_threadTable[id] = worker;
worker.Start();
}
}
}
}
private void ReadSocket()
{
long id = _connectId; // 捕獲當前 ID
Socket socket = (Socket)_socketTable[id];
int listViewIndex = -1;
while (true)
{
if (!socket.Connected) break;
byte[] buffer = new byte[37]; // 固定長度登入封包
try
{
int received = socket.Receive(buffer, buffer.Length, SocketFlags.None);
if (received <= 0) break;
string text = Encoding.ASCII.GetString(buffer, 0, received);
string[] parts = text.Split(':');
string userName = parts[0];
int authCode = CheckUserInfo(userName); // 1=已存在, 2=新用戶
if (authCode == 2)
{
_userTable[id] = userName;
// 跨執行緒更新 ListView
ListViewItem item = new ListViewItem(userName);
item.SubItems.Add(DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"));
item.ImageIndex = 0; // 綠燈
Invoke(() =>
{
listView2.Items.Add(item);
listViewIndex = listView2.Items.IndexOf(item);
});
}
else if (authCode == 1)
{
// 用戶已登入,拒絕或踢除舊連線
}
}
catch (Exception)
{
if (!socket.Connected) break;
}
}
CloseConnection(id, listViewIndex);
}
private void LoadData_Click(object sender, EventArgs e)
{
Thread loadThread = new Thread(LoadThread);
loadThread.Start();
}
private void LoadThread()
{
string[] lines = File.ReadAllLines("Issue.txt");
foreach (string line in lines)
{
if (string.IsNullOrEmpty(line)) break;
// 解析固定寬度欄位
IssueData data = new IssueData
{
Symbol = line.Substring(0, 4),
Bid = line.Substring(4, 5),
Offer = line.Substring(9, 5),
Volume = line.Substring(16)
};
// 組裝封包:\v 開頭 \n 結尾
string packet = $"\v{line}\n";
BroadcastToAllClients(packet);
// 等待 UI 更新完成 (生產者-消費者模式)
_jobDone.WaitOne();
}
}
private void BroadcastToAllClients(string data)
{
byte[] bytes = Encoding.ASCII.GetBytes(data);
lock (_socketTable)
{
foreach (Socket s in _socketTable.Values)
{
if (s.Connected) s.Send(bytes, bytes.Length, SocketFlags.None);
}
}
}
private void StartReceive()
{
_receiveThread = new Thread(() =>
{
byte[] buffer = new byte[1];
StringBuilder sb = new StringBuilder();
byte firstByte = 0;
int charCount = 0;
while (true)
{
try
{
int read = _socket.Receive(buffer, 1, SocketFlags.None);
if (read <= 0) break;
byte b = buffer[0];
if (charCount == 0) firstByte = b;
switch (b)
{
case 11: // VT (Vertical Tab) - 封包開始
charCount = 0;
sb.Clear();
break;
case 10: // LF (Line Feed) - 封包結束
charCount = 0;
string complete = sb.ToString();
if (firstByte == ':') HandleCommand(complete);
else if (firstByte == '<') HandleXml(complete);
else HandleText(complete);
sb.Clear();
break;
default:
sb.Append((char)b);
charCount++;
break;
}
}
catch (Exception)
{
if (!_socket.Connected) break;
}
}
});
_receiveThread.Start();
}
private void UpdateListView()
{
int index = FindItemIndex(_currentData.Symbol);
Color highlight = Color.FromArgb(128, 0, 0); // 深紅高亮
Color normal = Color.FromArgb(255, 255, 175); // 淺黃正常
Color altRow = Color.FromArgb(248, 248, 248); // 交替行
if (index == -1) // 新增
{
ListViewItem item = new ListViewItem(_currentData.Symbol);
item.SubItems.AddRange(new[] { _currentData.Bid, _currentData.Offer, _currentData.Volume });
listView1.Items.Add(item);
index = listView1.Items.IndexOf(item);
SetRowColor(index, index % 2 == 1 ? normal : altRow);
}
else // 更新
{
listView1.Items[index].SubItems[1].Text = _currentData.Bid;
listView1.Items[index].SubItems[2].Text = _currentData.Offer;
listView1.Items[index].SubItems[3].Text = _currentData.Volume;
}
// 閃爍效果:高亮 300ms → 恢復正常
SetCellColor(index, 0, highlight);
SetCellColor(index, 1, highlight);
listView1.Update();
Thread.Sleep(300);
SetCellColor(index, 0, index % 2 == 1 ? normal : altRow);
SetCellColor(index, 1, index % 2 == 1 ? normal : altRow);
_jobDone.Set(); // 通知生產者繼續
}
// 呼叫方式
Invoke(new MethodInvoker(UpdateListView));
private void HandleXml(string xmlContent)
{
XmlDocument doc = new XmlDocument();
doc.LoadXml(xmlContent);
foreach (XmlNode node in doc.SelectNodes("//Issue"))
{
_currentData = new IssueData
{
Symbol = node.SelectSingleNode("Symbol").InnerText,
Bid = node.SelectSingleNode("Bid").InnerText,
Offer = node.SelectSingleNode("Offer").InnerText,
Volume = node.SelectSingleNode("Volume").InnerText
};
Invoke(new MethodInvoker(UpdateListView));
_jobDone.WaitOne();
}
}
private void LoadXmlThread()
{
XmlTextReader reader = new XmlTextReader("issue.xml");
StringBuilder xmlBuilder = new StringBuilder();
xmlBuilder.Append("\v");
int recordFlag = -1, textCount = 0;
string currentElement = "";
while (reader.Read())
{
switch (reader.NodeType)
{
case XmlNodeType.Element:
if (reader.Name == "Issue")
{
recordFlag++;
if (recordFlag > 0) // 發送上一筆完整 XML
{
BroadcastToAllClients(xmlBuilder.ToString());
xmlBuilder.Clear();
xmlBuilder.Append("\v");
Invoke(new MethodInvoker(UpdateListView));
_jobDone.WaitOne();
}
currentElement = reader.Name;
xmlBuilder.Append($"<{reader.Name}>");
}
else if (recordFlag >= 0)
{
currentElement = reader.Name;
xmlBuilder.Append($"<{reader.Name}>");
}
break;
case XmlNodeType.Text:
textCount++;
xmlBuilder.Append(reader.Value);
xmlBuilder.Append($"{currentElement}>");
// 同步更新本地資料物件
switch (textCount)
{
case 1: _currentData.Symbol = reader.Value; break;
case 2: _currentData.Bid = reader.Value; break;
case 3: _currentData.Offer = reader.Value; break;
case 4: _currentData.Volume = reader.Value; break;
}
break;
}
}
}
| 傳統 Socket 技術 | 現代替代方案 | 核心優勢 |
|---|---|---|
| 手動管理 Thread/ThreadPool | async/await + ValueTask + Pipe | 無執行緒阻塞、高擴展性 |
| 自訂二進制/文字協定 | Protobuf / MessagePack / JSON | 標準化、版本容忍、跨語言 |
| Control.Invoke/BeginInvoke | Dispatcher.Invoke / SynchronizationContext | 統一 UI 執行緒調度模型 |
| Hashtable + lock 同步 | ConcurrentDictionary / Channel<T> | 無鎖高效能、背壓支援 |
| 固定封包長度/分隔符 | Length-Prefixed / WebSocket Frame | 邊界清晰、支援二進制 |
| XmlTextReader (DOM/SAX) | System.Text.Json / Utf8JsonReader | 高效能、零分配、強型別 |
TcpServer.exeTcpClient.exe注意:確保 Issue.txt、Issue.xml、MaskedTextBox.dll 與執行檔同目錄。
本系統雖基於 .NET 1.1 時代技術,但完整展示了「專用執行緒接收、協定狀態機解析、跨執行緒 UI 同步、生產者-消費者流控、XML 即時序列化推播」等即時通訊系統的核心模式。這些模式在今天的 WebSocket Server、SignalR Hub、gRPC Streaming 實作中依然清晰可見。理解 Socket 層面的細節,能讓開發者在排查高階框架疑難雜症、設計自訂傳輸協定、優化延遲敏感系統時具備底層視角。