VB.NET Socket 筆記 (精進篇)
在網路程式設計的世界裡,Socket 就像是「電話系統」Socket 就像電話一樣,需要撥號連線、通話交流,最後掛斷結束。。伺服器就像是「總機」伺服器監聽特定的通訊埠,等待客戶端的連線請求,就像總機等待來電。,等待著客戶端的來電;而客戶端則像是「撥號者」客戶端主動發起連線請求,就像撥打電話號碼。,主動撥號建立連線。透過 Socket,我們可以讓不同的電腦程式在網路上互相「對話」,傳遞訊息、分享資料。這種「雙向通訊」雙向通訊意味著雙方都可以發送和接收資料,就像電話交談一樣。的能力,是現代網路應用的基礎,從聊天室、線上遊戲到分散式系統,都離不開 Socket 技術。
認識 Socket
Socket: Socket 是網路程式設計中用於「端點通訊」端點通訊指的是網路中兩個節點之間的直接資料交換。的基本元件。它提供了一個抽象層,讓程式設計師可以透過簡單的 API 來建立網路連線、傳送和接收資料,而不需要深入了解底層的網路協定細節。在 VB.NET 中,我們主要使用 System.Net.Sockets 命名空間中的類別來操作 Socket。
Socket 通訊主要有兩種角色:
- 伺服器 (Server): 被動等待客戶端的連線請求。伺服器需要「綁定」綁定是指將 Socket 與特定的 IP 位址和通訊埠號碼關聯起來。到特定的通訊埠(Port),然後開始「監聽」監聽意味著伺服器處於等待狀態,準備接受客戶端的連線。。當有客戶端連線進來時,伺服器會接受連線並建立一個新的 Socket 來與該客戶端通訊。
- 客戶端 (Client): 主動向伺服器發起連線請求。客戶端需要知道伺服器的 IP 位址和通訊埠號碼,然後呼叫 Connect 方法來建立連線。連線成功後,客戶端就可以與伺服器進行資料交換。
透過這種「一對一」或「一對多」的通訊模式,Socket 為各種網路應用提供了強大的基礎架構。
Socket 基礎操作
要建立一個完整的 Socket 通訊,我們需要了解伺服器端和客戶端的基本操作流程。
功能一:建立 TCP 伺服器(同步模式)
TCP 伺服器是 Socket 程式設計的基礎。伺服器需要監聽特定的通訊埠,等待客戶端連線。當客戶端連線進來時,伺服器接受連線並可以接收客戶端發送的訊息。這個範例展示了如何建立一個簡單的「同步」同步模式意味著程式會等待操作完成才繼續執行下一步,可能會造成介面凍結。 TCP 伺服器。
使用的控制項:
- Button1:用於啟動伺服器開始監聽。
- Button2:用於停止伺服器。
- Label1:用於顯示伺服器狀態。
- Label2:用於顯示接收到的訊息。
- TextBox1:用於輸入要監聽的通訊埠號碼。
範例程式碼
' 引入必要的命名空間以使用網路功能
Imports System.Net
Imports System.Net.Sockets
Imports System.Text
Imports System.Threading
' 定義主要的表單類別
Public Class Form1
' 宣告伺服器 Socket 物件
Private serverSocket As Socket = Nothing
' 宣告客戶端 Socket 物件
Private clientSocket As Socket = Nothing
' 宣告監聽執行緒
Private listenThread As Thread = Nothing
' 宣告伺服器運行狀態標記
Private isRunning As Boolean = False
' 表單載入時的初始化
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
' 設定 TextBox1 的預設通訊埠號碼
TextBox1.Text = "8080"
' 設定 Label1 的初始文字
Label1.Text = "伺服器狀態: 未啟動"
' 設定 Label2 的初始文字
Label2.Text = "接收訊息: 無"
End Sub
' 按下 Button1 時啟動伺服器
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
' 宣告變數存放通訊埠號碼
Dim port As Integer = 0
' 嘗試解析 TextBox1 的文字為整數
If Not Integer.TryParse(TextBox1.Text, port) OrElse port <= 0 OrElse port > 65535 Then
' 輸入無效時顯示錯誤訊息
Label1.Text = "請輸入有效的通訊埠號碼 (1-65535)"
' 結束方法執行
Return
End If
' 設定伺服器運行狀態為 True
isRunning = True
' 建立監聽執行緒
listenThread = New Thread(Sub() StartServer(port))
' 設定為背景執行緒,隨主程式關閉
listenThread.IsBackground = True
' 啟動執行緒
listenThread.Start()
' 更新狀態標籤
Label1.Text = "伺服器狀態: 監聽中,通訊埠 " & port
End Sub
' 按下 Button2 時停止伺服器
Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
' 設定伺服器運行狀態為 False
isRunning = False
' 檢查客戶端 Socket 是否存在
If clientSocket IsNot Nothing Then
' 關閉客戶端 Socket
clientSocket.Close()
' 將客戶端 Socket 設為 Nothing
clientSocket = Nothing
End If
' 檢查伺服器 Socket 是否存在
If serverSocket IsNot Nothing Then
' 關閉伺服器 Socket
serverSocket.Close()
' 將伺服器 Socket 設為 Nothing
serverSocket = Nothing
End If
' 更新狀態標籤
Label1.Text = "伺服器狀態: 已停止"
End Sub
' 啟動伺服器的方法
Private Sub StartServer(port As Integer)
Try
' 建立 TCP Socket 物件
serverSocket = New Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)
' 建立 IPEndPoint 物件,綁定到所有網路介面
Dim endPoint As New IPEndPoint(IPAddress.Any, port)
' 將 Socket 綁定到指定的端點
serverSocket.Bind(endPoint)
' 開始監聽,最多允許 10 個等待連線
serverSocket.Listen(10)
' 當伺服器運行時持續監聽
While isRunning
' 接受客戶端連線 (此處會阻塞等待)
clientSocket = serverSocket.Accept()
' 在 UI 執行緒上更新顯示
Me.Invoke(Sub() Label1.Text = "伺服器狀態: 客戶端已連線")
' 建立位元組陣列用於接收資料
Dim buffer(1024) As Byte
' 接收客戶端發送的資料
Dim bytesRead As Integer = clientSocket.Receive(buffer)
' 將接收到的位元組轉換為字串
Dim message As String = Encoding.UTF8.GetString(buffer, 0, bytesRead)
' 在 UI 執行緒上更新訊息顯示
Me.Invoke(Sub() Label2.Text = "接收訊息: " & message)
' 準備回應訊息
Dim response As String = "伺服器已收到: " & message
' 將回應字串轉換為位元組陣列
Dim responseBytes() As Byte = Encoding.UTF8.GetBytes(response)
' 發送回應給客戶端
clientSocket.Send(responseBytes)
' 關閉客戶端連線
clientSocket.Close()
' 在 UI 執行緒上更新狀態
Me.Invoke(Sub() Label1.Text = "伺服器狀態: 監聽中,通訊埠 " & port)
End While
Catch ex As Exception
' 發生錯誤時在 UI 執行緒上顯示錯誤訊息
Me.Invoke(Sub() Label1.Text = "錯誤: " & ex.Message)
End Try
End Sub
End Class
詳細講解
此範例展示了 TCP 伺服器的基本架構。當按下 Button1 時,程式會在新的執行緒中啟動伺服器。伺服器首先建立一個 Socket 物件,指定使用 IPv4(AddressFamily.InterNetwork)、串流類型(SocketType.Stream)和 TCP 協定(ProtocolType.Tcp)。
接著使用 Bind 方法將 Socket 綁定到指定的通訊埠,IPAddress.Any 表示監聽所有網路介面。然後呼叫 Listen 方法開始監聽,參數 10 表示最多允許 10 個等待連線的佇列長度。
Accept 方法會「阻塞」阻塞意味著程式會停在這一行,等待客戶端連線,這就是為什麼要放在獨立執行緒中。等待客戶端連線。當有客戶端連線時,Accept 會返回一個新的 Socket 物件代表與該客戶端的連線。然後使用 Receive 方法接收資料,並用 Send 方法發送回應。
需要注意的是,由於網路操作在背景執行緒中進行,更新 UI 時必須使用 Me.Invoke 來確保在主執行緒上執行,這是 Windows Forms 的要求。
功能二:建立 TCP 客戶端
TCP 客戶端負責主動連線到伺服器並發送訊息。客戶端需要知道伺服器的 IP 位址和通訊埠號碼。這個範例展示如何建立一個簡單的 TCP 客戶端,連線到伺服器並發送訊息。
使用的控制項:
- TextBox1:用於輸入伺服器的 IP 位址。
- TextBox2:用於輸入伺服器的通訊埠號碼。
- TextBox3:用於輸入要發送的訊息。
- Button1:用於連線到伺服器並發送訊息。
- Label1:用於顯示連線狀態。
- Label2:用於顯示伺服器的回應。
範例程式碼
' 引入必要的命名空間
Imports System.Net
Imports System.Net.Sockets
Imports System.Text
' 定義主要的表單類別
Public Class Form1
' 表單載入時的初始化
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
' 設定 TextBox1 的預設 IP 位址
TextBox1.Text = "127.0.0.1"
' 設定 TextBox2 的預設通訊埠
TextBox2.Text = "8080"
' 設定 TextBox3 的預設訊息
TextBox3.Text = "Hello Server!"
' 設定 Label1 的初始文字
Label1.Text = "連線狀態: 未連線"
' 設定 Label2 的初始文字
Label2.Text = "伺服器回應: 無"
End Sub
' 按下 Button1 時連線並發送訊息
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
' 宣告變數存放 IP 位址
Dim ipAddress As IPAddress = Nothing
' 宣告變數存放通訊埠號碼
Dim port As Integer = 0
' 嘗試解析 IP 位址
If Not IPAddress.TryParse(TextBox1.Text, ipAddress) Then
' IP 位址無效時顯示錯誤訊息
Label1.Text = "連線狀態: IP 位址格式錯誤"
' 結束方法執行
Return
End If
' 嘗試解析通訊埠號碼
If Not Integer.TryParse(TextBox2.Text, port) OrElse port <= 0 OrElse port > 65535 Then
' 通訊埠號碼無效時顯示錯誤訊息
Label1.Text = "連線狀態: 通訊埠號碼無效"
' 結束方法執行
Return
End If
' 宣告客戶端 Socket 變數
Dim clientSocket As Socket = Nothing
Try
' 建立 TCP Socket 物件
clientSocket = New Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)
' 建立 IPEndPoint 物件
Dim endPoint As New IPEndPoint(ipAddress, port)
' 更新連線狀態
Label1.Text = "連線狀態: 連線中..."
' 連線到伺服器
clientSocket.Connect(endPoint)
' 更新連線狀態為已連線
Label1.Text = "連線狀態: 已連線"
' 取得要發送的訊息
Dim message As String = TextBox3.Text
' 將訊息轉換為位元組陣列
Dim messageBytes() As Byte = Encoding.UTF8.GetBytes(message)
' 發送訊息給伺服器
clientSocket.Send(messageBytes)
' 建立位元組陣列用於接收伺服器回應
Dim buffer(1024) As Byte
' 接收伺服器回應
Dim bytesRead As Integer = clientSocket.Receive(buffer)
' 將接收到的位元組轉換為字串
Dim response As String = Encoding.UTF8.GetString(buffer, 0, bytesRead)
' 顯示伺服器回應
Label2.Text = "伺服器回應: " & response
Catch ex As SocketException
' 捕獲 Socket 相關的例外
Label1.Text = "連線狀態: 連線失敗 - " & ex.Message
Catch ex As Exception
' 捕獲其他例外
Label1.Text = "連線狀態: 發生錯誤 - " & ex.Message
Finally
' 檢查 Socket 是否存在
If clientSocket IsNot Nothing Then
' 關閉 Socket 連線
clientSocket.Close()
' 更新連線狀態
Label1.Text = "連線狀態: 已斷線"
End If
End Try
End Sub
End Class
詳細講解
此範例展示了 TCP 客戶端的基本操作。程式首先驗證使用者輸入的 IP 位址和通訊埠號碼是否有效。然後建立一個 Socket 物件,使用 Connect 方法連線到指定的伺服器。
Connect 方法需要一個 IPEndPoint 物件,它封裝了 IP 位址和通訊埠號碼。連線成功後,使用 Send 方法發送訊息。訊息需要先透過 Encoding.UTF8.GetBytes 轉換為位元組陣列。
發送完畢後,使用 Receive 方法接收伺服器的回應。Receive 會返回實際接收到的位元組數量,然後使用 Encoding.UTF8.GetString 將位元組陣列轉換回字串。
程式使用 Try...Catch...Finally 結構來處理可能發生的例外,特別是 SocketException,它會在網路連線失敗時被拋出。在 Finally 區塊中確保 Socket 被正確關閉,釋放網路資源。
Socket 進階功能
在實際應用中,我們經常需要處理更複雜的情況,例如非同步通訊、處理多個客戶端連線、以及資料的持續收發。
功能三:非同步伺服器 (支援多客戶端)
在實際應用中,伺服器通常需要同時處理多個客戶端的連線。使用「非同步」非同步模式允許程式在等待操作完成時繼續執行其他任務,不會阻塞主執行緒。模式可以有效地管理多個連線,避免介面凍結。這個範例展示如何建立一個支援多客戶端的非同步伺服器。
使用的控制項:
- Button1:用於啟動伺服器。
- Button2:用於停止伺服器。
- TextBox1:用於輸入監聽的通訊埠號碼。
- Label1:用於顯示伺服器狀態。
- Label2:用於顯示已連線的客戶端數量。
- Label3:用於顯示最新接收到的訊息。
範例程式碼
' 引入必要的命名空間
Imports System.Net
Imports System.Net.Sockets
Imports System.Text
Imports System.Collections.Generic
' 定義主要的表單類別
Public Class Form1
' 宣告伺服器 Socket 物件
Private serverSocket As Socket = Nothing
' 宣告客戶端 Socket 清單
Private clientSockets As New List(Of Socket)
' 宣告伺服器運行狀態標記
Private isRunning As Boolean = False
' 宣告連線數量
Private clientCount As Integer = 0
' 表單載入時的初始化
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
' 設定 TextBox1 的預設通訊埠號碼
TextBox1.Text = "8080"
' 設定 Label1 的初始文字
Label1.Text = "伺服器狀態: 未啟動"
' 設定 Label2 的初始文字
Label2.Text = "已連線客戶端: 0"
' 設定 Label3 的初始文字
Label3.Text = "最新訊息: 無"
End Sub
' 按下 Button1 時啟動伺服器
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
' 宣告變數存放通訊埠號碼
Dim port As Integer = 0
' 嘗試解析通訊埠號碼
If Not Integer.TryParse(TextBox1.Text, port) OrElse port <= 0 OrElse port > 65535 Then
' 輸入無效時顯示錯誤訊息
Label1.Text = "請輸入有效的通訊埠號碼 (1-65535)"
' 結束方法執行
Return
End If
Try
' 設定伺服器運行狀態為 True
isRunning = True
' 建立 TCP Socket 物件
serverSocket = New Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)
' 建立 IPEndPoint 物件
Dim endPoint As New IPEndPoint(IPAddress.Any, port)
' 將 Socket 綁定到指定的端點
serverSocket.Bind(endPoint)
' 開始監聽,最多允許 100 個等待連線
serverSocket.Listen(100)
' 更新狀態標籤
Label1.Text = "伺服器狀態: 監聽中,通訊埠 " & port
' 開始非同步接受客戶端連線
serverSocket.BeginAccept(New AsyncCallback(AddressOf AcceptCallback), serverSocket)
Catch ex As Exception
' 發生錯誤時顯示錯誤訊息
Label1.Text = "錯誤: " & ex.Message
End Try
End Sub
' 按下 Button2 時停止伺服器
Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
' 設定伺服器運行狀態為 False
isRunning = False
' 關閉所有客戶端連線
For Each client In clientSockets
' 檢查客戶端 Socket 是否存在
If client IsNot Nothing Then
' 關閉客戶端 Socket
client.Close()
End If
Next
' 清空客戶端清單
clientSockets.Clear()
' 檢查伺服器 Socket 是否存在
If serverSocket IsNot Nothing Then
' 關閉伺服器 Socket
serverSocket.Close()
' 將伺服器 Socket 設為 Nothing
serverSocket = Nothing
End If
' 重置客戶端計數
clientCount = 0
' 更新狀態標籤
Label1.Text = "伺服器狀態: 已停止"
' 更新客戶端數量顯示
Label2.Text = "已連線客戶端: 0"
End Sub
' 接受客戶端連線的回呼方法
Private Sub AcceptCallback(ar As IAsyncResult)
' 檢查伺服器是否仍在運行
If Not isRunning Then
' 伺服器已停止,結束方法
Return
End If
Try
' 從 IAsyncResult 取得伺服器 Socket
Dim listener As Socket = CType(ar.AsyncState, Socket)
' 完成接受連線操作,取得客戶端 Socket
Dim client As Socket = listener.EndAccept(ar)
' 將客戶端 Socket 加入清單
clientSockets.Add(client)
' 增加客戶端計數
clientCount += 1
' 在 UI 執行緒上更新顯示
Me.Invoke(Sub()
' 更新客戶端數量顯示
Label2.Text = "已連線客戶端: " & clientCount
End Sub)
' 開始接收客戶端資料
Dim buffer(1024) As Byte
' 建立狀態物件用於傳遞資料
Dim state As New StateObject With {.socket = client, .buffer = buffer}
' 開始非同步接收資料
client.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, New AsyncCallback(AddressOf ReceiveCallback), state)
' 繼續接受下一個客戶端連線
listener.BeginAccept(New AsyncCallback(AddressOf AcceptCallback), listener)
Catch ex As Exception
' 發生錯誤時在 UI 執行緒上顯示錯誤訊息
Me.Invoke(Sub() Label1.Text = "錯誤: " & ex.Message)
End Try
End Sub
' 接收資料的回呼方法
Private Sub ReceiveCallback(ar As IAsyncResult)
Try
' 從 IAsyncResult 取得狀態物件
Dim state As StateObject = CType(ar.AsyncState, StateObject)
' 從狀態物件取得客戶端 Socket
Dim client As Socket = state.socket
' 完成接收操作,取得接收到的位元組數
Dim bytesRead As Integer = client.EndReceive(ar)
' 檢查是否有接收到資料
If bytesRead > 0 Then
' 將接收到的位元組轉換為字串
Dim message As String = Encoding.UTF8.GetString(state.buffer, 0, bytesRead)
' 在 UI 執行緒上更新訊息顯示
Me.Invoke(Sub()
' 顯示接收到的訊息
Label3.Text = "最新訊息: " & message
End Sub)
' 準備回應訊息
Dim response As String = "伺服器已收到: " & message
' 將回應轉換為位元組陣列
Dim responseBytes() As Byte = Encoding.UTF8.GetBytes(response)
' 發送回應給客戶端
client.Send(responseBytes)
' 繼續接收下一個訊息
Dim newBuffer(1024) As Byte
' 更新狀態物件的緩衝區
state.buffer = newBuffer
' 繼續非同步接收資料
client.BeginReceive(newBuffer, 0, newBuffer.Length, SocketFlags.None, New AsyncCallback(AddressOf ReceiveCallback), state)
Else
' 客戶端已斷線
client.Close()
' 從清單中移除客戶端
clientSockets.Remove(client)
' 減少客戶端計數
clientCount -= 1
' 在 UI 執行緒上更新顯示
Me.Invoke(Sub()
' 更新客戶端數量顯示
Label2.Text = "已連線客戶端: " & clientCount
End Sub)
End If
Catch ex As Exception
' 發生錯誤時忽略,通常是客戶端異常斷線
End Try
End Sub
' 狀態物件類別,用於在非同步操作間傳遞資料
Private Class StateObject
' 客戶端 Socket 物件
Public socket As Socket
' 接收資料的緩衝區
Public buffer() As Byte
End Class
End Class
詳細講解
此範例展示了非同步 Socket 程式設計的強大功能。與同步模式不同,非同步模式使用 BeginAccept 和 BeginReceive 等方法,這些方法會立即返回,不會阻塞執行緒。
當有客戶端連線時,AcceptCallback 方法會被呼叫。在這個方法中,使用 EndAccept 完成連線操作並取得客戶端 Socket。然後將客戶端加入清單,並開始非同步接收資料。重要的是,在處理完當前客戶端後,立即呼叫 BeginAccept 繼續接受下一個連線,這樣伺服器就能同時處理多個客戶端。
ReceiveCallback 方法處理接收到的資料。使用 EndReceive 完成接收操作,如果接收到的位元組數大於 0,表示有資料;如果等於 0,表示客戶端已斷線。接收到資料後,發送回應並繼續呼叫 BeginReceive 接收下一個訊息。
程式使用了一個自定義的 StateObject 類別來在非同步操作之間傳遞資料,這是非同步程式設計的「常見模式」在非同步操作中,狀態物件用於攜帶上下文資訊,讓回呼方法能夠存取必要的資料。。
功能四:檔案傳輸 (二進位資料)
Socket 不僅可以傳送文字訊息,還可以傳送「二進位資料」二進位資料包括圖片、音訊、視訊等任何類型的檔案。,例如圖片、文件等檔案。這個範例展示如何透過 Socket 傳送檔案,包括檔案大小資訊和檔案內容。
使用的控制項:
- Button1:用於選擇要傳送的檔案。
- Button2:用於開始傳送檔案。
- TextBox1:用於輸入伺服器 IP 位址。
- TextBox2:用於輸入伺服器通訊埠。
- Label1:用於顯示選擇的檔案路徑。
- Label2:用於顯示傳送狀態。
- Label3:用於顯示傳送進度。
範例程式碼
' 引入必要的命名空間
Imports System.Net
Imports System.Net.Sockets
Imports System.IO
' 定義主要的表單類別
Public Class Form1
' 宣告檔案路徑變數
Private filePath As String = ""
' 表單載入時的初始化
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
' 設定 TextBox1 的預設 IP 位址
TextBox1.Text = "127.0.0.1"
' 設定 TextBox2 的預設通訊埠
TextBox2.Text = "8080"
' 設定 Label1 的初始文字
Label1.Text = "選擇的檔案: 未選擇"
' 設定 Label2 的初始文字
Label2.Text = "傳送狀態: 就緒"
' 設定 Label3 的初始文字
Label3.Text = "傳送進度: 0%"
End Sub
' 按下 Button1 時選擇檔案
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
' 建立 OpenFileDialog 物件
Dim openFileDialog As New OpenFileDialog()
' 設定對話框標題
openFileDialog.Title = "選擇要傳送的檔案"
' 設定檔案篩選器
openFileDialog.Filter = "所有檔案 (*.*)|*.*"
' 顯示對話框並檢查使用者是否選擇了檔案
If openFileDialog.ShowDialog() = DialogResult.OK Then
' 儲存選擇的檔案路徑
filePath = openFileDialog.FileName
' 顯示檔案路徑
Label1.Text = "選擇的檔案: " & Path.GetFileName(filePath)
End If
End Sub
' 按下 Button2 時傳送檔案
Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
' 檢查是否已選擇檔案
If String.IsNullOrEmpty(filePath) OrElse Not File.Exists(filePath) Then
' 未選擇檔案時顯示錯誤訊息
Label2.Text = "傳送狀態: 請先選擇有效的檔案"
' 結束方法執行
Return
End If
' 宣告變數存放 IP 位址
Dim ipAddress As IPAddress = Nothing
' 宣告變數存放通訊埠號碼
Dim port As Integer = 0
' 嘗試解析 IP 位址
If Not IPAddress.TryParse(TextBox1.Text, ipAddress) Then
' IP 位址無效時顯示錯誤訊息
Label2.Text = "傳送狀態: IP 位址格式錯誤"
' 結束方法執行
Return
End If
' 嘗試解析通訊埠號碼
If Not Integer.TryParse(TextBox2.Text, port) OrElse port <= 0 OrElse port > 65535 Then
' 通訊埠號碼無效時顯示錯誤訊息
Label2.Text = "傳送狀態: 通訊埠號碼無效"
' 結束方法執行
Return
End If
' 宣告客戶端 Socket 變數
Dim clientSocket As Socket = Nothing
Try
' 建立 TCP Socket 物件
clientSocket = New Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)
' 建立 IPEndPoint 物件
Dim endPoint As New IPEndPoint(ipAddress, port)
' 更新傳送狀態
Label2.Text = "傳送狀態: 連線中..."
' 連線到伺服器
clientSocket.Connect(endPoint)
' 更新傳送狀態為已連線
Label2.Text = "傳送狀態: 已連線,開始傳送..."
' 讀取檔案內容為位元組陣列
Dim fileBytes() As Byte = File.ReadAllBytes(filePath)
' 取得檔案大小
Dim fileSize As Long = fileBytes.Length
' 先傳送檔案大小資訊(使用 8 位元組表示)
Dim fileSizeBytes() As Byte = BitConverter.GetBytes(fileSize)
' 發送檔案大小
clientSocket.Send(fileSizeBytes)
' 分批傳送檔案內容
Dim bufferSize As Integer = 8192
' 宣告變數追蹤已傳送的位元組數
Dim totalSent As Integer = 0
' 當還有資料未傳送時繼續迴圈
While totalSent < fileSize
' 計算本次要傳送的大小
Dim remaining As Integer = CInt(fileSize - totalSent)
' 取較小值作為本次傳送大小
Dim sendSize As Integer = Math.Min(remaining, bufferSize)
' 傳送資料
Dim sent As Integer = clientSocket.Send(fileBytes, totalSent, sendSize, SocketFlags.None)
' 累加已傳送的位元組數
totalSent += sent
' 計算傳送進度百分比
Dim progress As Integer = CInt((totalSent / fileSize) * 100)
' 更新進度顯示
Label3.Text = "傳送進度: " & progress & "%"
' 刷新介面
Application.DoEvents()
End While
' 更新傳送狀態為完成
Label2.Text = "傳送狀態: 傳送完成"
' 更新進度顯示為 100%
Label3.Text = "傳送進度: 100%"
Catch ex As SocketException
' 捕獲 Socket 相關的例外
Label2.Text = "傳送狀態: 連線失敗 - " & ex.Message
Catch ex As Exception
' 捕獲其他例外
Label2.Text = "傳送狀態: 發生錯誤 - " & ex.Message
Finally
' 檢查 Socket 是否存在
If clientSocket IsNot Nothing Then
' 關閉 Socket 連線
clientSocket.Close()
End If
End Try
End Sub
End Class
詳細講解
此範例展示了如何透過 Socket 傳送二進位檔案。與傳送文字訊息不同,檔案傳輸需要考慮資料大小和傳輸效率。
程式首先使用 File.ReadAllBytes 讀取整個檔案到記憶體中的位元組陣列。然後使用 BitConverter.GetBytes 將檔案大小轉換為 8 位元組的陣列並先傳送給伺服器,讓伺服器知道即將接收多少資料。
接著使用迴圈分批傳送檔案內容。每次傳送 8192 位元組(8KB),這是一個常用的緩衝區大小,既不會太大造成記憶體壓力,也不會太小導致傳輸效率低下。使用 Socket.Send 的多載版本,可以指定要傳送陣列的哪個部分。
在傳送過程中,程式會計算並顯示傳送進度。使用 Application.DoEvents() 來保持介面回應,讓使用者能看到即時的進度更新。這對於「大檔案傳輸」大檔案傳輸可能需要較長時間,顯示進度可以讓使用者了解傳輸狀態。特別重要。
Socket 程式設計最佳實務
建議一:正確處理例外
Socket 程式設計中,網路錯誤是常見且不可避免的。務必使用 Try...Catch 結構捕獲 SocketException 和其他可能的例外。常見的錯誤包括:連線被拒絕、連線逾時、遠端主機強制關閉連線等。妥善處理這些例外,能讓程式更加穩定可靠。
建議二:及時釋放資源
Socket 是系統資源,使用完畢後必須呼叫 Close() 方法關閉。建議在 Finally 區塊中關閉 Socket,確保即使發生例外也能正確釋放資源。未關閉的 Socket 會佔用系統資源,可能導致「通訊埠耗盡」系統可用的通訊埠數量有限,未釋放的連線會佔用通訊埠,導致無法建立新連線。。
建議三:選擇適當的緩衝區大小
接收和發送資料時需要使用緩衝區。對於一般的訊息傳輸,1024 或 2048 位元組通常足夠;對於檔案傳輸,可以使用 4096 或 8192 位元組以提高效率。緩衝區太小會增加網路往返次數,太大則會浪費記憶體。根據實際應用場景選擇合適的大小。
注意事項:處理粘包和拆包問題
TCP 是「串流協定」TCP 保證資料按順序傳送,但不保證資料的邊界,多次發送的資料可能被合併或分割。,不保證資料的邊界。多次發送的資料可能被合併(粘包),或一次發送的資料被分成多次接收(拆包)。解決方法包括:在每個訊息前加上長度資訊、使用固定長度的訊息格式、或使用特殊的分隔符號。這在設計通訊協定時是必須考慮的重要問題。
注意事項:非同步操作中的執行緒安全
在非同步 Socket 程式設計中,多個回呼方法可能在不同的執行緒上同時執行。如果這些方法存取共享資源(如客戶端清單),必須使用適當的「同步機制」如 SyncLock、Mutex 等,確保同一時間只有一個執行緒能存取共享資源。來保護。同時,更新 UI 時必須使用 Invoke 或 BeginInvoke 確保在主執行緒上執行。
| Socket 類型 | 優點 | 適用場景與限制 |
|---|---|---|
| 同步 Socket | 程式碼簡單易懂,邏輯清晰,適合學習和簡單應用。 | 適用: 簡單的客戶端、點對點通訊、命令列工具。 限制: 會阻塞執行緒,不適合需要同時處理多個連線的伺服器。 |
| 非同步 Socket | 高效能,能同時處理大量連線,不會阻塞主執行緒。 | 適用: 需要處理多個客戶端的伺服器、高併發應用。 限制: 程式碼較複雜,需要處理回呼和狀態管理。 |
| UDP Socket | 無連線、低延遲,適合即時應用。 | 適用: 線上遊戲、串流媒體、即時監控。 限制: 不保證資料送達和順序,需要自行實作可靠性機制。 |
通訊協定設計
在實際應用中,良好的通訊協定設計至關重要。通訊協定定義了客戶端和伺服器之間的「對話規則」,包括訊息格式、命令類型、錯誤處理等。
功能五:簡單的訊息協定實作
這個範例展示如何設計和實作一個簡單但實用的訊息協定。協定格式為:訊息長度(4 位元組) + 訊息類型(1 位元組) + 訊息內容。這種設計可以有效解決「粘包拆包」透過在每個訊息前加上長度資訊,接收端可以準確知道一個完整訊息的邊界。問題。
使用的控制項:
- Button1:用於發送文字訊息。
- Button2:用於發送命令訊息。
- TextBox1:用於輸入伺服器 IP 位址。
- TextBox2:用於輸入伺服器通訊埠。
- TextBox3:用於輸入要發送的訊息內容。
- Label1:用於顯示連線狀態。
- Label2:用於顯示伺服器回應。
範例程式碼
' 引入必要的命名空間
Imports System.Net
Imports System.Net.Sockets
Imports System.Text
' 定義主要的表單類別
Public Class Form1
' 宣告客戶端 Socket 物件
Private clientSocket As Socket = Nothing
' 定義訊息類型列舉
Private Enum MessageType As Byte
' 文字訊息類型
TextMessage = 1
' 命令訊息類型
CommandMessage = 2
' 檔案訊息類型
FileMessage = 3
End Enum
' 表單載入時的初始化
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
' 設定 TextBox1 的預設 IP 位址
TextBox1.Text = "127.0.0.1"
' 設定 TextBox2 的預設通訊埠
TextBox2.Text = "8080"
' 設定 TextBox3 的預設訊息
TextBox3.Text = "Hello Server!"
' 設定 Label1 的初始文字
Label1.Text = "連線狀態: 未連線"
' 設定 Label2 的初始文字
Label2.Text = "伺服器回應: 無"
End Sub
' 按下 Button1 時發送文字訊息
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
' 呼叫連線方法
If ConnectToServer() Then
' 呼叫發送訊息方法,傳入文字訊息類型
SendMessage(MessageType.TextMessage, TextBox3.Text)
' 呼叫接收回應方法
ReceiveResponse()
' 呼叫斷線方法
DisconnectFromServer()
End If
End Sub
' 按下 Button2 時發送命令訊息
Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
' 呼叫連線方法
If ConnectToServer() Then
' 呼叫發送訊息方法,傳入命令訊息類型
SendMessage(MessageType.CommandMessage, TextBox3.Text)
' 呼叫接收回應方法
ReceiveResponse()
' 呼叫斷線方法
DisconnectFromServer()
End If
End Sub
' 連線到伺服器的方法
Private Function ConnectToServer() As Boolean
' 宣告變數存放 IP 位址
Dim ipAddress As IPAddress = Nothing
' 宣告變數存放通訊埠號碼
Dim port As Integer = 0
' 嘗試解析 IP 位址
If Not IPAddress.TryParse(TextBox1.Text, ipAddress) Then
' IP 位址無效時顯示錯誤訊息
Label1.Text = "連線狀態: IP 位址格式錯誤"
' 返回 False 表示連線失敗
Return False
End If
' 嘗試解析通訊埠號碼
If Not Integer.TryParse(TextBox2.Text, port) OrElse port <= 0 OrElse port > 65535 Then
' 通訊埠號碼無效時顯示錯誤訊息
Label1.Text = "連線狀態: 通訊埠號碼無效"
' 返回 False 表示連線失敗
Return False
End If
Try
' 建立 TCP Socket 物件
clientSocket = New Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)
' 建立 IPEndPoint 物件
Dim endPoint As New IPEndPoint(ipAddress, port)
' 更新連線狀態
Label1.Text = "連線狀態: 連線中..."
' 連線到伺服器
clientSocket.Connect(endPoint)
' 更新連線狀態為已連線
Label1.Text = "連線狀態: 已連線"
' 返回 True 表示連線成功
Return True
Catch ex As SocketException
' 捕獲 Socket 相關的例外
Label1.Text = "連線狀態: 連線失敗 - " & ex.Message
' 返回 False 表示連線失敗
Return False
Catch ex As Exception
' 捕獲其他例外
Label1.Text = "連線狀態: 發生錯誤 - " & ex.Message
' 返回 False 表示連線失敗
Return False
End Try
End Function
' 發送訊息的方法
Private Sub SendMessage(msgType As MessageType, content As String)
Try
' 將訊息內容轉換為位元組陣列
Dim contentBytes() As Byte = Encoding.UTF8.GetBytes(content)
' 計算訊息總長度 (類型 1 位元組 + 內容長度)
Dim totalLength As Integer = 1 + contentBytes.Length
' 建立完整的訊息封包
Dim packet(4 + totalLength - 1) As Byte
' 將訊息長度轉換為位元組並複製到封包開頭
Dim lengthBytes() As Byte = BitConverter.GetBytes(totalLength)
' 複製長度資訊(4 位元組)
Array.Copy(lengthBytes, 0, packet, 0, 4)
' 設定訊息類型 (1 位元組)
packet(4) = CByte(msgType)
' 複製訊息內容到封包
Array.Copy(contentBytes, 0, packet, 5, contentBytes.Length)
' 發送完整的封包
clientSocket.Send(packet)
Catch ex As Exception
' 發生錯誤時顯示錯誤訊息
Label1.Text = "發送錯誤: " & ex.Message
End Try
End Sub
' 接收伺服器回應的方法
Private Sub ReceiveResponse()
Try
' 先接收訊息長度 (4 位元組)
Dim lengthBuffer(3) As Byte
' 接收長度資訊
clientSocket.Receive(lengthBuffer, 0, 4, SocketFlags.None)
' 將位元組轉換為整數
Dim messageLength As Integer = BitConverter.ToInt32(lengthBuffer, 0)
' 接收訊息類型 (1 位元組)
Dim typeBuffer(0) As Byte
' 接收類型資訊
clientSocket.Receive(typeBuffer, 0, 1, SocketFlags.None)
' 取得訊息類型
Dim msgType As MessageType = CType(typeBuffer(0), MessageType)
' 接收訊息內容
Dim contentLength As Integer = messageLength - 1
' 建立接收內容的緩衝區
Dim contentBuffer(contentLength - 1) As Byte
' 接收內容資料
Dim totalReceived As Integer = 0
' 當還有資料未接收完時繼續迴圈
While totalReceived < contentLength
' 接收剩餘的資料
Dim received As Integer = clientSocket.Receive(contentBuffer, totalReceived, contentLength - totalReceived, SocketFlags.None)
' 累加已接收的位元組數
totalReceived += received
End While
' 將接收到的位元組轉換為字串
Dim response As String = Encoding.UTF8.GetString(contentBuffer)
' 根據訊息類型顯示不同的提示
Dim typeText As String = ""
' 判斷訊息類型
Select Case msgType
Case MessageType.TextMessage
' 文字訊息
typeText = "[文字] "
Case MessageType.CommandMessage
' 命令訊息
typeText = "[命令] "
Case MessageType.FileMessage
' 檔案訊息
typeText = "[檔案] "
End Select
' 顯示伺服器回應
Label2.Text = "伺服器回應: " & typeText & response
Catch ex As Exception
' 發生錯誤時顯示錯誤訊息
Label2.Text = "接收錯誤: " & ex.Message
End Try
End Sub
' 斷開連線的方法
Private Sub DisconnectFromServer()
' 檢查 Socket 是否存在
If clientSocket IsNot Nothing Then
' 關閉 Socket 連線
clientSocket.Close()
' 將 Socket 設為 Nothing
clientSocket = Nothing
' 更新連線狀態
Label1.Text = "連線狀態: 已斷線"
End If
End Sub
End Class
詳細講解
此範例展示了一個實用的訊息協定設計。協定的核心是在每個訊息前加上「標頭資訊」,包括訊息長度和類型。這種設計解決了 TCP 串流傳輸中的邊界問題。
訊息格式分為三個部分:前 4 位元組表示訊息長度(不包括長度欄位本身),第 5 位元組表示訊息類型,剩餘部分是實際的訊息內容。使用 BitConverter.GetBytes 將整數長度轉換為位元組陣列。
發送時,先計算總長度,然後建立完整的封包。使用 Array.Copy 將各部分資料複製到封包中,最後一次性發送整個封包。這樣可以確保訊息的「原子性」原子性確保訊息作為一個完整單位被發送,不會被其他操作中斷。。
接收時,先讀取 4 位元組的長度資訊,再讀取 1 位元組的類型,最後根據長度讀取完整的內容。使用迴圈確保接收到完整的資料,因為 Receive 可能一次接收不完。這種「分段接收」的處理方式在實際應用中非常重要。