[C#][Console] 群益 API 串接報價 + Socket Server 教學 #CH1
群益是國內證券期貨交易券商,提供股票、期貨、選擇權等金融商品買賣,我們平常可以使用券商的下單軟體交易,也可以利用程式串接券商 API 取得報價及程式下單。
此範例會教學如何在 C# 主控台 (Console) 建立群益報價連線,然後再建立 TCP/IP 的 Socket Server,提供其他程式串接取得報價。
這次的範例成品就稱之為 “報價機” ,之後的教學會再建立 ASP.NET Core MVC 串接我們的報價機,將取得的價格顯示在網頁上。
Contents
開發前準備
在串接群益 API 之前,需要先有群益帳戶、申請 API 和安裝憑證等動作,
關於群益 API 的安裝環境可參考另一篇文章:群益 API 官方範例下載與安裝環境
建立專案
開啟 Visual Studio 2022,建立新專案為「主控台應用程式」。
輸入專案名稱、路徑,架構選擇「.NET 6.0」版本,按下「建立」就會建立此專案。
從 .NET 6.0 開始,Console 主程式預設程式變的很簡短,第一次看會有點不習慣,怎麼看不到 Main()
呢?
這是因為 .NET 6.0 新架構寫法把 Main()
隱藏起來了,看不習慣的話,改成以下語法還是可以正常執行,而且這兩種寫法代表相同的程式:
1 2 3 4 5 6 7 |
internal class Program { static void Main(string[] args) { Console.WriteLine("Hello World!"); } } |
附加群益 API 元件
接著要引用群益的元件至專案之中,群益 API 可至官網下載。
下載之後解壓縮就會有元件。
在專案的「相依性 > 新增 COM 參考」點擊,
然後選擇「瀏覽」,
尋找本機電腦的群益 API 元件,檔名是「SKCOM.dll」,要注意版本是 x64 或 x86 (現在大部份電腦都已升級至 x64 位元了)。
完成後在專案底下會出現此元件。
建立主程式類別
這裡我新增一個類別叫 “CapitalQuote”,接下來群益 API 串接和 Socket Server 程式碼都會寫在這類別裡面。
同時在 Program.cs 增加語法物件化此類別。
CapitalQuote quote = new CapitalQuote();
群益報價連線
在 CapitalQuote 屬性宣告群益 API 物件:
1 2 3 4 5 6 |
public SKQuoteLib m_SKQuoteLib = new SKQuoteLib();// 國內報價物件 public SKCenterLib m_pSKCenter = new SKCenterLib();// 登入&環境物件 public SKReplyLib m_pSKReply = new SKReplyLib();// 回應物件 double dDigitNum = 100.000; // 小數位 public Dictionary<string, Int16> tickPageNo = new Dictionary<string, Int16>(); int nCode; |
宣告好 Api 物件後,就可以引用元件。
註冊群益事件及登入群益
群益 API 有許多事件都需要由券商主動回報,在回報之前,我們需要先註冊事件接收方法,讓券商呼叫。
註冊事件之後就直接登入群益帳戶。
我在 CapitalQuote 的建構子加入以下語法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
/// <summary> /// 建構子 /// </summary> internal CapitalQuote() { m_pSKReply.OnReplyMessage += new _ISKReplyLibEvents_OnReplyMessageEventHandler(m_pSKReply_OnAnnouncement); // 註冊公告事件 m_SKQuoteLib.OnConnection += new _ISKQuoteLibEvents_OnConnectionEventHandler(m_SKQuoteLib_OnConnection);// 註冊國內報價連線狀態事件 m_SKQuoteLib.OnNotifyTicksLONG += new _ISKQuoteLibEvents_OnNotifyTicksLONGEventHandler(m_SKQuoteLib_OnNotifyTicks);// 國內 Tick 回傳事件 m_SKQuoteLib.OnNotifyBest5LONG += new _ISKQuoteLibEvents_OnNotifyBest5LONGEventHandler(m_SKQuoteLib_OnNotifyBest5);// 註冊國內 Best5 回傳事件 //群益登入帳密 string CapitalLoginID = "XXXXXXXXXXX"; //建議由設定檔讀取 string CapitalLoginPwd = "XXXXXXXXXXX"; //建議由設定檔讀取 // 群益登入 m_pSKCenter.SKCenterLib_SetAuthority(1);// 不用 SGX DMA // 登入群益帳戶 nCode = m_pSKCenter.SKCenterLib_Login(CapitalLoginID, CapitalLoginPwd); if (nCode != 0 && nCode != 2003) { if (nCode == 507) { ShowMessage(this, "請檢查群益憑證安裝狀態"); } ShowMessage(this, "請檢查群益憑證安裝狀態"); GetCapitalMessage("登入", nCode); return; } ShowMessage(this, "群益登入成功"); //國內報價進線 nCode = m_SKQuoteLib.SKQuoteLib_EnterMonitorLONG(); GetCapitalMessage("國內報價連線", nCode); } |
接著我們宣告一些事件接收方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 |
/// <summary> /// 公告 /// </summary> void m_pSKReply_OnAnnouncement(string strUserID, string bstrMessage, out short nConfirmCode) { nConfirmCode = -1; } /// <summary> /// 國內報價連線回應事件 /// </summary> /// <param name="nKind"></param> /// <param name="nCode"></param> void m_SKQuoteLib_OnConnection(int nKind, int nCode) { if (nKind == 3001) { // 連線中 ShowMessage(this, "連線狀態:連線中"); } else if (nKind == 3002) { // 連線中斷 ShowMessage(this, "連線狀態:中斷"); } else if (nKind == 3003) { // 連線成功 ShowMessage(this, "連線狀態:正常"); } else if (nKind == 3021) { //網路斷線 ShowMessage(this, "連線狀態:網路斷線"); } } /// <summary> /// 國內 Tick 回傳事件 /// </summary> void m_SKQuoteLib_OnNotifyTicks(short sMarketNo, int nStockIdx, int nPtr, int nDate, int lTimehms, int lTimemillismicros, int nBid, int nAsk, int nClose, int nQty, int nSimulate) { RequestSymbol? request = listRequestBest5.FirstOrDefault(w => w.MarketNo == sMarketNo.ToString() && w.StockIdx == nStockIdx); if (request != null) { List<UserRquest> items = listUserRquest.Where(w => w.Symbol == request.Symbol).ToList(); foreach (var item in items) { TickPacket packet = new TickPacket(); packet.Symbol = request.Symbol; packet.Close = nClose / dDigitNum; packet.Qty = nQty; ClientInfo? CI = Clients.FirstOrDefault(x => x.ID == item.ID); if (CI != null) { SendTCP(packet, CI.Client); } } } } /// <summary> /// 國內 Best5 回傳事件 /// </summary> void m_SKQuoteLib_OnNotifyBest5(short sMarketNo, int nStockIdx, int nBestBid1, int nBestBidQty1, int nBestBid2, int nBestBidQty2, int nBestBid3, int nBestBidQty3, int nBestBid4, int nBestBidQty4, int nBestBid5, int nBestBidQty5, int nExtendBid, int nExtendBidQty, int nBestAsk1, int nBestAskQty1, int nBestAsk2, int nBestAskQty2, int nBestAsk3, int nBestAskQty3, int nBestAsk4, int nBestAskQty4, int nBestAsk5, int nBestAskQty5, int nExtendAsk, int nExtendAskQty, int nSimulate) { RequestSymbol? request = listRequestBest5.FirstOrDefault(w => w.MarketNo == sMarketNo.ToString() && w.StockIdx == nStockIdx); if (request != null) { List<UserRquest> items = listUserRquest.Where(w => w.Symbol == request.Symbol).ToList(); foreach (var item in items) { Best5Packet packet = new Best5Packet(); packet.Symbol = request.Symbol; packet.Bid1Price = nBestBid1 / dDigitNum; packet.Ask1Price = nBestAsk1 / dDigitNum; packet.Bid1Qty = nBestBidQty1; packet.Ask1Qty = nBestAskQty1; packet.Bid2Price = nBestBid2 / dDigitNum; packet.Bid2Qty = nBestBidQty2; packet.Bid3Price = nBestBid3 / dDigitNum; packet.Bid3Qty = nBestBidQty3; packet.Bid4Price = nBestBid4 / dDigitNum; packet.Bid4Qty = nBestBidQty4; packet.Bid5Price = nBestBid5 / dDigitNum; packet.Bid5Qty = nBestBidQty5; packet.Ask2Price = nBestAsk2 / dDigitNum; packet.Ask2Qty = nBestAskQty2; packet.Ask3Price = nBestAsk3 / dDigitNum; packet.Ask3Qty = nBestAskQty3; packet.Ask4Price = nBestAsk4 / dDigitNum; packet.Ask4Qty = nBestAskQty4; packet.Ask5Price = nBestAsk5 / dDigitNum; packet.Ask5Qty = nBestAskQty5; ClientInfo? CI = Clients.FirstOrDefault(x => x.ID == item.ID); if (CI != null) { SendTCP(packet, CI.Client); } } } } /// <summary> /// 取得群益api回傳訊息說明 /// </summary> /// <param name="strType"></param> /// <param name="nCode"></param> /// <returns></returns> private void GetCapitalMessage(string strType, int nCode) { string strInfo = ""; if (nCode != 0) strInfo = "【" + m_pSKCenter.SKCenterLib_GetLastLogInfo() + "】"; string message = "【" + strType + "】【" + m_pSKCenter.SKCenterLib_GetReturnCodeMessage(nCode) + "】" + strInfo; ShowMessage(this, message); } /// <summary> /// 顯示訊息 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> public void ShowMessage(object? sender, string e) { Console.WriteLine(DateTime.Now.ToString("HH:mm:ss ") + e); } |
其中兩個事件 m_SKQuoteLib_OnNotifyTicks()
與 m_SKQuoteLib_OnNotifyBest5()
都是群益回報最新的 Tick 與五檔價格,當我們收到之後,就會利用 Socket 再回傳給前端,而回傳方法 SendTCP()
等到下面建立 Socket 時再新增。
Socket Server
接著要增加 Socket Server 的程式碼,在 CapitalQuote 屬性宣告 Socket Server 相關物件:
1 2 3 4 5 |
TcpListener? Tcp; bool isTcpListen = false; List<ClientInfo> Clients = new List<ClientInfo>(); List<RequestSymbol> listRequestBest5 = new List<RequestSymbol>(); //訂閱商品名稱 List<UserRquest> listUserRquest = new List<UserRquest>(); //使用者訂閱名單 |
Socket 類別
這裡有一些新的物件,我將 Socket 所傳遞的內容都宣告成不同的類別,在傳送時,先將物件轉換成 Json,再轉換成 Byte[] 後傳送。
當收到 Byte[] 時反向處理,將 Byte[] 轉 Json,再轉成物件。
我建了一個 “Models” 目錄,將會用到類別都放在這裡。
在這個範例我會用到兩個報價類別,分別是 Tick 和 Best5 ,一個使用者類別 ClientInfo,一個報價需求類別 RequestQuotePacket,但是我針對傳送的類別宣告一個介面 (interface) 來規範必要欄位。
在 “Models” 目錄下,新增介面: IPacket
1 2 3 4 |
public interface IPacket { string ID { get; set; } } |
接著新增類別: TickPacket
1 2 3 4 5 6 7 8 |
[Serializable] public class TickPacket : IPacket { public string ID { get; set; } public string Symbol { get; set; } public double Close { get; set; } public int Qty { get; set; } } |
新增類別: Best5Packet
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
[Serializable] public class Best5Packet : IPacket { public string ID { get; set; } public string Symbol { get; set; } public double Bid1Price { get; set; } public double Bid1Qty { get; set; } public double Bid2Price { get; set; } public double Bid2Qty { get; set; } public double Bid3Price { get; set; } public double Bid3Qty { get; set; } public double Bid4Price { get; set; } public double Bid4Qty { get; set; } public double Bid5Price { get; set; } public double Bid5Qty { get; set; } public double Ask1Price { get; set; } public double Ask1Qty { get; set; } public double Ask2Price { get; set; } public double Ask2Qty { get; set; } public double Ask3Price { get; set; } public double Ask3Qty { get; set; } public double Ask4Price { get; set; } public double Ask4Qty { get; set; } public double Ask5Price { get; set; } public double Ask5Qty { get; set; } } |
新增使用者類別: ClientInfo
1 2 3 4 5 6 7 8 9 10 11 12 |
[Serializable] public class ClientInfo : IPacket { public string ID { get; set; } [NonSerialized] public TcpClient Client; public ClientInfo(string id) { this.ID = id; } } |
新增報價需求類別: RequestQuotePacket
1 2 3 4 5 6 7 8 9 10 11 |
[Serializable] public class RequestQuotePacket : IPacket { public string ID { get; set; } public List<string> Symbol { get; set; } public RequestQuotePacket(string id) { this.ID = id; } } |
新增訂閱商品名稱類別 RequestSymbol
1 2 3 4 5 6 |
internal class RequestSymbol { public string Symbol { get; set; } public string MarketNo { get; set; } public int StockIdx { get; set; } } |
新增使用者訂閱名單類別 UserRquest
1 2 3 4 5 |
internal class UserRquest { public string ID { get; set; } public string Symbol { get; set; } } |
建立 Socket Server
在 CapitalQuote 的建構子加入以下語法:
1 2 3 4 5 6 |
// TCP Server IPEndPoint TcpEndPoint = new IPEndPoint(IPAddress.Any, 8888); Tcp = new TcpListener(TcpEndPoint); Thread ThreadTCP = new Thread(new ThreadStart(TCPListen)); isTcpListen = true; ThreadTCP.Start(); |
TCP 監聽
在建構子中的語法 TCPListen
是一個新方法,目的在監聽封包,而我建立新執行緒來執行這個方法。
新增 TCPListen()
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
/// <summary> /// TCP 監聽 /// </summary> private void TCPListen() { Tcp.Start(); ShowMessage(this, "TCP Listener Started"); while (isTcpListen) { try { TcpClient NewClient = Tcp.AcceptTcpClient(); Action<object> ProcessData = new Action<object>(delegate (object _Client) { TcpClient Client = (TcpClient)_Client; Client.Client.IOControl(IOControlCode.KeepAliveValues, GetKeepAliveData(), null); Client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); byte[] receiveBuffer = new byte[0]; byte[] processBuffer = new byte[0]; byte[] packet = new byte[1024]; byte[] lenPacket = new byte[8]; int size = 0; int bytesRead = 0; while (Client.Connected) { try { bytesRead = Client.GetStream().Read(packet, 0, packet.Length); if (bytesRead > 0) { receiveBuffer = MargeByte(receiveBuffer, packet, bytesRead); if (receiveBuffer.Length < 8 && bytesRead < 8) { continue; } lenPacket = GetByteData(receiveBuffer, 0, 8); size = int.Parse(Encoding.UTF8.GetString(lenPacket)); while (size > 0) { if (size <= receiveBuffer.Length - 8) { processBuffer = GetByteData(receiveBuffer, 8, size); IPacket? Item = ByteToPacket(processBuffer); ProcessReceive(Item, Client); receiveBuffer = GetByteData(receiveBuffer, 8 + size, receiveBuffer.Length - size - 8); if (receiveBuffer.Length < 8) { break; } lenPacket = GetByteData(receiveBuffer, 0, 8); size = int.Parse(Encoding.UTF8.GetString(lenPacket)); } else { break; } } } else { break; } } catch (Exception ex) { if (ex.Message.IndexOf("遠端主機已強制關閉一個現存的連線") == -1) { ShowMessage(this, "Client TCP Error: " + ex.Message + "\n" + ex.StackTrace); } break; } } Disconnect(Client); }); Thread ThreadProcessData = new Thread(new ParameterizedThreadStart(ProcessData)); ThreadProcessData.Start(NewClient); } catch (Exception ex) { ShowMessage(this, "TCP Error: " + ex.Message + "\n" + ex.StackTrace); isTcpListen = false; } } } |
補充共用方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
/// <summary> /// 連線心跳檢測 /// </summary> /// <returns></returns> public byte[] GetKeepAliveData() { uint dummy = 0; byte[] inOptionValues = new byte[System.Runtime.InteropServices.Marshal.SizeOf(dummy) * 3]; BitConverter.GetBytes((uint)1).CopyTo(inOptionValues, 0); BitConverter.GetBytes((uint)3000).CopyTo(inOptionValues, System.Runtime.InteropServices.Marshal.SizeOf(dummy));//keep-alive間隔 BitConverter.GetBytes((uint)500).CopyTo(inOptionValues, System.Runtime.InteropServices.Marshal.SizeOf(dummy) * 2);// 嘗試間隔 return inOptionValues; } /// <summary> /// 合併位元組 /// </summary> /// <param name="a"></param> /// <param name="b"></param> /// <param name="bsz"></param> /// <returns></returns> public byte[] MargeByte(byte[] a, byte[] b, int bsz) { using (MemoryStream ms = new MemoryStream()) { ms.Write(a, 0, a.Length); ms.Write(b, 0, (bsz == 0) ? b.Length : bsz); return ms.ToArray(); } } /// <summary> /// 取得位元組 /// </summary> /// <param name="buf"></param> /// <param name="pos"></param> /// <param name="length"></param> /// <returns></returns> public byte[] GetByteData(byte[] buf, int pos, int length) { byte[] b = new byte[length]; Array.Copy(buf, pos, b, 0, length); return b; } /// <summary> /// Byte 轉傳送物件 /// </summary> /// <param name="bytes"></param> /// <returns></returns> /// <exception cref="Exception"></exception> public IPacket ByteToPacket(byte[] bytes) { string jsonString = Encoding.UTF8.GetString(bytes); string type = jsonString.Split('|')[0]; try { switch (type) { case "ClientInfo": return JsonConvert.DeserializeObject<ClientInfo>(jsonString.Split('|')[1]); case "RequestQuotePacket": return JsonConvert.DeserializeObject<RequestQuotePacket>(jsonString.Split('|')[1]); case "TickPacket": return JsonConvert.DeserializeObject<TickPacket>(jsonString.Split('|')[1]); case "Best5Packet": return JsonConvert.DeserializeObject<Best5Packet>(jsonString.Split('|')[1]); default: throw new Exception("Not Support Type, \nSource:" + jsonString); } } catch (Exception ex) { throw new Exception("Convert Error: " + ex.Message + "\nSource:" + jsonString); } } |
處理接收封包內容
我們在 TCPListen()
方法內會接收封包流量,當收到完整的封包內容後,就會執行解析內容,解析方法是 ProcessReceive()
,所以繼續新增此方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
/// <summary> /// 處理接收項目 /// </summary> /// <param name="Item"></param> /// <param name="Protocol"></param> /// <param name="EP"></param> /// <param name="Client"></param> private void ProcessReceive(IPacket Item, TcpClient Client) { if (Item.GetType() == typeof(ClientInfo)) { // 使用者連線 ClientInfo CI = Clients.FirstOrDefault(x => x.ID == ((ClientInfo)Item).ID); if (CI == null) { CI = (ClientInfo)Item; Clients.Add(CI); if (Client != null) { ShowMessage(this, string.Format("Client Added: ID: {0}, TCP EP: {1}:{2}", CI.ID, ((IPEndPoint)Client.Client.RemoteEndPoint).Address, ((IPEndPoint)Client.Client.RemoteEndPoint).Port)); CI.Client = Client; } } } else if (Item.GetType() == typeof(RequestQuotePacket)) { // 報價商品訂閱 RequestQuotePacket req = (RequestQuotePacket)Item; int nConnected = m_SKQuoteLib.SKQuoteLib_IsConnected(); if (nConnected == 1) { foreach (string symbol in req.Symbol) { // 加入使用者訂閱名單 if (!listUserRquest.Any(w => w.ID == req.ID && w.Symbol == symbol)) { listUserRquest.Add(new UserRquest() { ID = req.ID, Symbol = symbol }); } // 訂閱報價 if (!listRequestBest5.Any(w => w.Symbol == symbol)) { // 取回商品報價的相關資訊 SKSTOCKLONG pSKStockLONG = new SKSTOCKLONG(); nCode = m_SKQuoteLib.SKQuoteLib_GetStockByNoLONG(symbol, ref pSKStockLONG); if (nCode != 0) { return; } GetCapitalMessage("訂閱商品資訊 [" + symbol + "]", nCode); //加入清單 listRequestBest5.Add(new RequestSymbol() { Symbol = symbol, MarketNo = pSKStockLONG.bstrMarketNo, StockIdx = pSKStockLONG.nStockIdx }); // 訂閱商品 Tick & Best5 if (!tickPageNo.ContainsKey(symbol)) { tickPageNo.Add(symbol, Convert.ToInt16(tickPageNo.Count)); } //訂閱 Tick & Best5,訂閱後等待 OnNotifyTicks 及 OnNotifyBest5 事件回報 Int16 TickPage = tickPageNo[symbol]; nCode = m_SKQuoteLib.SKQuoteLib_RequestTicks(ref TickPage, symbol); GetCapitalMessage("訂閱商品 Tick & Best5 [" + symbol + "]", nCode); } } } else { ShowMessage(this, "尚未報價連線,稍後再試"); } } } |
我們從封包收到的內容,都會先轉型回物件,這是需要發送端也使用相同的物件發送才行,發送端就留到下一篇章再來介紹。
傳送封包至客戶端
在前面收到群益 Tick 與五檔時,會將此價格重新傳送給客戶端,這裡新增傳送方法 SendTCP()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
/// <summary> /// 傳送 TCP /// </summary> /// <param name="Item"></param> /// <param name="Client"></param> private void SendTCP(IPacket Item, TcpClient Client) { if (Client != null && Client.Connected) { byte[] Data = PacketToByteArray(Item); NetworkStream NetStream = Client.GetStream(); NetStream.Write(Data, 0, Data.Length); } } /// <summary> /// 傳送物件轉 Byte /// </summary> /// <param name="packet"></param> /// <returns></returns> /// <exception cref="Exception"></exception> public byte[] PacketToByteArray(IPacket packet) { string type = packet.GetType().Name; string jsonString = JsonConvert.SerializeObject(packet); byte[] data = Encoding.UTF8.GetBytes(type + "|" + jsonString); int len = data.Length; if (len > 99999999) { throw new Exception("傳送字串超過長度限制"); } byte[] lenData = Encoding.UTF8.GetBytes(len.ToString("00000000")); byte[] newData = MargeByte(lenData, data, 0); return newData; } |
客戶端離線事件
TCP Socket 有連線事件,那也就會有離線事件,在 TCPListen()
方法內會接收封包,如果客戶端斷線了,我們就執行客戶端離線事件方法 Disconnect()
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
/// <summary> /// 離線 /// </summary> /// <param name="Client"></param> private void Disconnect(TcpClient Client) { ClientInfo CI = Clients.FirstOrDefault(x => x.Client == Client); if (CI != null) { listUserRquest.RemoveAll(w => w.ID == CI.ID); Clients.Remove(CI); ShowMessage(this, "Client Disconnected " + Client.Client.RemoteEndPoint.ToString()); Client.Close(); } } |
好了,我們所有的程式碼都貼完了,是不是覺得有點複雜,第一次看 Socket 的語法一定會看不懂,這是很正常的,要搞懂 Socket 的封包需要慢慢 Debug 才會比較明白。
目前我們執行的語法只有一半而已,這是伺服器端的程式,在下一章節我們再來介紹客戶端的程式。
等客戶端開始串接伺服器端程式後,這個範例就可以正式運作了。
範例下載
下一篇教學文章
相關學習文章
- 【C# 群益 API 開發教學】帳號登入、取得下單帳號教學 #CH2
- 【C# 群益 API 開發教學】取得商品報價、Tick、最佳 5 檔教學 #CH3
- 如何在 Visual Studio 2022 安裝 CUDA GUP 計算功能
如果你在學習上有不懂的地方,需要諮詢服務,可以參考站長服務,我想辨法解決你的問題
如果文章內容有過時、不適用或錯誤的地方,幫我在下方留言通知我一下,謝謝