VB.NET Socket 筆記(精進篇)
Socket 是程式進行網路通訊時使用的端點。伺服器端負責等待連線,客戶端負責主動連線;連線建立後,雙方就可以透過 Socket 傳送與接收位元組資料。
Socket 程式容易卡住的地方,不只是「能不能連上」。更重要的是:資料是不是完整、接收動作會不會卡住畫面、連線中斷時能不能正常結束,以及 TCP 串流要如何切出一筆一筆訊息。
先理解 Socket 在做什麼
Socket 是兩端溝通的出入口
Socket 可以理解成程式和網路之間的出入口。伺服器端先開好門等待連線,客戶端主動連進來。連線建立後,雙方都可以送資料,也都可以收資料。
- 伺服器 Server:綁定 IP 與 Port,呼叫
Listen等待客戶端連線。 - 客戶端 Client:知道伺服器 IP 與 Port,呼叫
Connect建立連線。 - Send:把資料轉成位元組後送出去。
- Receive:從 Socket 接收一段位元組資料。
Socket 程式設計要一起考慮四件事:
- 連線:IP、Port、伺服器是否啟動、通訊埠是否被占用。
- 接收:
Receive只代表收到一段資料,不保證剛好是一筆完整訊息。 - 執行緒:等待連線或等待資料時,不應讓 Windows Forms 畫面卡住。
- 關閉:Socket、背景工作與客戶端清單都需要明確結束流程。
| 項目 | 意思 | 常見問題 |
|---|---|---|
| Bind | 把伺服器 Socket 綁到指定 IP 與 Port。 | Port 被占用、權限不足、埠號錯誤。 |
| Listen | 讓伺服器開始等待連線。 | 只 Listen 但沒有 Accept,客戶端仍無法正常交換資料。 |
| Accept | 接受一個客戶端連線。 | 同步 Accept 會等待,不能直接放在 UI 執行緒。 |
| Connect | 客戶端連到伺服器。 | IP 錯誤、Port 錯誤、伺服器未啟動。 |
| Send / Receive | 送出與接收位元組資料。 | 文字編碼錯誤、訊息不完整、粘包與拆包。 |
Windows Forms 測試配置
建議測試方式
Socket 範例建議用兩個 Windows Forms 專案測試:一個當伺服器,一個當客戶端。若只想快速體驗,可先用本機位址 127.0.0.1 與固定埠號 9000。
- Server 專案:負責啟動監聽、顯示收到的訊息。
- Client 專案:負責輸入訊息、連線、送出資料、顯示回應。
- 測試順序:先啟動 Server,再按 Client 的送出按鈕。
基礎連線:Server 與 Client
場景一:建立本機留言伺服器
這個範例建立一個簡單的本機伺服器。Server 監聽 9000,Client 送出一段文字後,Server 顯示收到內容並回傳 OK。這個情境容易測試,也能清楚看出 Server 的基本流程。
需要的主控項
ButtonStartServer:啟動伺服器。ButtonStopServer:停止伺服器。LabelServerStatus:顯示伺服器狀態。TextBoxLog:顯示收到的訊息,建議設定Multiline=True、ScrollBars=Vertical。
範例程式碼
Imports System.Net
Imports System.Net.Sockets
Imports System.Text
Imports System.Threading
Public Class Form1
Private listener As Socket
Private serverThread As Thread
Private isServerRunning As Boolean = False
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
TextBoxLog.Multiline = True
TextBoxLog.ScrollBars = ScrollBars.Vertical
LabelServerStatus.Text = "伺服器狀態:未啟動"
End Sub
Private Sub ButtonStartServer_Click(sender As Object, e As EventArgs) Handles ButtonStartServer.Click
If isServerRunning Then
LabelServerStatus.Text = "伺服器狀態:已經啟動"
Return
End If
isServerRunning = True
serverThread = New Thread(AddressOf RunMessageServer)
serverThread.IsBackground = True
serverThread.Start()
LabelServerStatus.Text = "伺服器狀態:啟動中"
End Sub
Private Sub ButtonStopServer_Click(sender As Object, e As EventArgs) Handles ButtonStopServer.Click
StopServer()
End Sub
Private Sub RunMessageServer()
Try
listener = New Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)
listener.Bind(New IPEndPoint(IPAddress.Loopback, 9000))
listener.Listen(10)
SetStatusText("伺服器狀態:監聽中 127.0.0.1:9000")
While isServerRunning
Dim client As Socket = listener.Accept()
HandleClient(client)
End While
Catch ex As SocketException
If isServerRunning Then
SetStatusText("伺服器狀態:Socket 錯誤 - " & ex.SocketErrorCode.ToString())
End If
Catch ex As ObjectDisposedException
' 關閉 listener 時可能發生,屬於停止流程的一部分。
Catch ex As Exception
If isServerRunning Then
SetStatusText("伺服器狀態:錯誤 - " & ex.Message)
End If
End Try
End Sub
Private Sub HandleClient(ByVal client As Socket)
Using client
Dim buffer(1023) As Byte
Dim received As Integer = client.Receive(buffer)
If received > 0 Then
Dim message As String = Encoding.UTF8.GetString(buffer, 0, received)
AppendLog("收到:" & message)
Dim responseBytes() As Byte = Encoding.UTF8.GetBytes("OK")
client.Send(responseBytes)
End If
End Using
End Sub
Private Sub StopServer()
isServerRunning = False
If listener IsNot Nothing Then
listener.Close()
listener = Nothing
End If
LabelServerStatus.Text = "伺服器狀態:已停止"
End Sub
Private Sub SetStatusText(ByVal message As String)
If Me.IsHandleCreated AndAlso Not Me.IsDisposed Then
Me.BeginInvoke(New MethodInvoker(Sub()
LabelServerStatus.Text = message
End Sub))
End If
End Sub
Private Sub AppendLog(ByVal message As String)
If Me.IsHandleCreated AndAlso Not Me.IsDisposed Then
Me.BeginInvoke(New MethodInvoker(Sub()
TextBoxLog.AppendText(message & Environment.NewLine)
End Sub))
End If
End Sub
Private Sub Form1_FormClosing(sender As Object, e As FormClosingEventArgs) Handles MyBase.FormClosing
StopServer()
End Sub
End Class
邏輯解析
Bind把伺服器綁到本機127.0.0.1:9000。Accept會等待客戶端連線,因此放在背景執行緒中。BeginInvoke讓背景執行緒安全更新LabelServerStatus與TextBoxLog。StopServer會關閉listener,讓等待中的Accept結束。
場景二:建立本機留言客戶端
Client 負責連到本機伺服器,送出 TextBoxMessage 的內容,並顯示伺服器回應。這個範例搭配場景一測試。
需要的主控項
TextBoxMessage:輸入要送出的文字。ButtonSend:連線並送出。LabelClientStatus:顯示連線狀態。LabelResponse:顯示伺服器回應。
範例程式碼
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
TextBoxMessage.Text = "今天 15:00 開會"
LabelClientStatus.Text = "連線狀態:尚未送出"
LabelResponse.Text = "伺服器回應:無"
End Sub
Private Sub ButtonSend_Click(sender As Object, e As EventArgs) Handles ButtonSend.Click
Dim message As String = TextBoxMessage.Text.Trim()
If message = String.Empty Then
LabelClientStatus.Text = "連線狀態:訊息不可空白"
Return
End If
Try
Using client As New Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)
client.Connect(New IPEndPoint(IPAddress.Loopback, 9000))
LabelClientStatus.Text = "連線狀態:已連線"
Dim sendBytes() As Byte = Encoding.UTF8.GetBytes(message)
client.Send(sendBytes)
Dim buffer(1023) As Byte
Dim received As Integer = client.Receive(buffer)
Dim response As String = Encoding.UTF8.GetString(buffer, 0, received)
LabelResponse.Text = "伺服器回應:" & response
End Using
Catch ex As SocketException
LabelClientStatus.Text = "連線狀態:Socket 錯誤 - " & ex.SocketErrorCode.ToString()
Catch ex As Exception
LabelClientStatus.Text = "連線狀態:錯誤 - " & ex.Message
End Try
End Sub
End Class
邏輯解析
IPAddress.Loopback代表本機位址,也就是127.0.0.1。Encoding.UTF8.GetBytes把文字轉成 Socket 可送出的位元組。Using client可確保連線結束後釋放 Socket。
TCP 訊息邊界
TCP 最容易誤會的地方
TCP 是串流,不是「一個 Send 對一個 Receive」。一次 Send 的資料可能被拆成多次接收;多次 Send 的資料也可能在接收端黏在一起。因此正式程式不能只靠一次 Receive 判斷一筆完整訊息。
場景三:使用長度前綴建立訊息封包
常見做法是每筆訊息前面放 4 個位元組的長度。接收端先讀長度,再依照長度把內容收滿。這樣可避免粘包與拆包造成資料判斷錯誤。
封包格式
| 區段 | 長度 | 用途 |
|---|---|---|
| Length | 4 bytes | 表示後方內容有幾個位元組。 |
| Body | 可變 | 實際訊息內容,範例使用 UTF-8 文字。 |
範例程式碼
Imports System.Net.Sockets
Imports System.Text
Public Class PacketHelper
Public Shared Function BuildTextPacket(ByVal message As String) As Byte()
Dim body() As Byte = Encoding.UTF8.GetBytes(message)
Dim lengthBytes() As Byte = BitConverter.GetBytes(body.Length)
Dim packet(4 + body.Length - 1) As Byte
Array.Copy(lengthBytes, 0, packet, 0, 4)
Array.Copy(body, 0, packet, 4, body.Length)
Return packet
End Function
Public Shared Function ReceiveTextPacket(ByVal socket As Socket) As String
Dim lengthBytes() As Byte = ReceiveExact(socket, 4)
Dim bodyLength As Integer = BitConverter.ToInt32(lengthBytes, 0)
If bodyLength < 0 OrElse bodyLength > 1024 * 1024 Then
Throw New InvalidOperationException("訊息長度不合理。")
End If
Dim body() As Byte = ReceiveExact(socket, bodyLength)
Return Encoding.UTF8.GetString(body)
End Function
Private Shared Function ReceiveExact(ByVal socket As Socket, ByVal size As Integer) As Byte()
Dim buffer(size - 1) As Byte
Dim offset As Integer = 0
While offset < size
Dim readCount As Integer = socket.Receive(buffer, offset, size - offset, SocketFlags.None)
If readCount = 0 Then
Throw New SocketException(CInt(SocketError.ConnectionReset))
End If
offset += readCount
End While
Return buffer
End Function
End Class
邏輯解析
BuildTextPacket會把文字變成「長度 + 內容」格式。ReceiveExact會反覆呼叫Receive,直到收滿指定長度。- 收到長度後再收內容,可清楚判斷一筆訊息的邊界。
多客戶端與非同步接收
場景四:非同步接收多個客戶端留言
同步範例適合理解流程,但一次只處理一個連線。若要讓多個客戶端同時連線,可以使用非同步接收。此範例保留簡化結構,重點放在 AcceptTcpClientAsync、背景接收與 UI 更新。
需要的主控項
ButtonStartAsync:啟動非同步伺服器。ButtonStopAsync:停止非同步伺服器。LabelAsyncStatus:顯示伺服器狀態。ListBoxMessages:顯示多個客戶端送來的訊息。
範例程式碼
Imports System.Net
Imports System.Net.Sockets
Imports System.Text
Imports System.Threading
Imports System.Threading.Tasks
Public Class Form1
Private tcpListener As TcpListener
Private cancelSource As CancellationTokenSource
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
LabelAsyncStatus.Text = "非同步伺服器:未啟動"
End Sub
Private Async Sub ButtonStartAsync_Click(sender As Object, e As EventArgs) Handles ButtonStartAsync.Click
If tcpListener IsNot Nothing Then
LabelAsyncStatus.Text = "非同步伺服器:已經啟動"
Return
End If
cancelSource = New CancellationTokenSource()
tcpListener = New TcpListener(IPAddress.Loopback, 9100)
tcpListener.Start()
LabelAsyncStatus.Text = "非同步伺服器:監聽中 127.0.0.1:9100"
Try
Await AcceptClientsAsync(cancelSource.Token)
Catch ex As ObjectDisposedException
' Stop 時可能發生,屬於正常停止流程。
Catch ex As Exception
LabelAsyncStatus.Text = "非同步伺服器:錯誤 - " & ex.Message
End Try
End Sub
Private Sub ButtonStopAsync_Click(sender As Object, e As EventArgs) Handles ButtonStopAsync.Click
StopAsyncServer()
End Sub
Private Async Function AcceptClientsAsync(ByVal token As CancellationToken) As Task
While Not token.IsCancellationRequested
Dim client As TcpClient = Await tcpListener.AcceptTcpClientAsync()
_ = HandleClientAsync(client, token)
End While
End Function
Private Async Function HandleClientAsync(ByVal client As TcpClient,
ByVal token As CancellationToken) As Task
Using client
Dim stream As NetworkStream = client.GetStream()
Dim buffer(1023) As Byte
Dim readCount As Integer = Await stream.ReadAsync(buffer, 0, buffer.Length, token)
If readCount > 0 Then
Dim message As String = Encoding.UTF8.GetString(buffer, 0, readCount)
ListBoxMessages.Items.Add("收到:" & message)
Dim response() As Byte = Encoding.UTF8.GetBytes("OK")
Await stream.WriteAsync(response, 0, response.Length, token)
End If
End Using
End Function
Private Sub StopAsyncServer()
If cancelSource IsNot Nothing Then
cancelSource.Cancel()
cancelSource.Dispose()
cancelSource = Nothing
End If
If tcpListener IsNot Nothing Then
tcpListener.Stop()
tcpListener = Nothing
End If
LabelAsyncStatus.Text = "非同步伺服器:已停止"
End Sub
Private Sub Form1_FormClosing(sender As Object, e As FormClosingEventArgs) Handles MyBase.FormClosing
StopAsyncServer()
End Sub
End Class
邏輯解析
TcpListener是較高階的 TCP 伺服器封裝,適合簡化教學範例。AcceptTcpClientAsync等待連線時不會阻塞 UI 流程。_ = HandleClientAsync(...)讓每個客戶端獨立處理。- 正式高併發服務通常還需要連線清單、錯誤紀錄與封包解析。
檔案傳輸
場景五:傳送小型備份檔案
檔案是二進位資料,不適合先轉成文字再傳。基本做法是先送出檔案大小,再分段送出內容,接收端依照大小判斷何時收完。
需要的主控項
ButtonPickFile:選擇檔案。ButtonSendFile:送出檔案。LabelFileName:顯示檔案名稱。LabelSendStatus:顯示傳送狀態。ProgressBar1:顯示傳送進度。
範例程式碼
Imports System.IO
Imports System.Net
Imports System.Net.Sockets
Imports System.Threading.Tasks
Public Class Form1
Private selectedFilePath As String = String.Empty
Private Sub ButtonPickFile_Click(sender As Object, e As EventArgs) Handles ButtonPickFile.Click
Using dialog As New OpenFileDialog()
dialog.Title = "選擇要傳送的檔案"
dialog.Filter = "所有檔案 (*.*)|*.*"
If dialog.ShowDialog() = DialogResult.OK Then
selectedFilePath = dialog.FileName
LabelFileName.Text = "檔案:" & Path.GetFileName(selectedFilePath)
End If
End Using
End Sub
Private Async Sub ButtonSendFile_Click(sender As Object, e As EventArgs) Handles ButtonSendFile.Click
If selectedFilePath = String.Empty OrElse Not File.Exists(selectedFilePath) Then
LabelSendStatus.Text = "傳送狀態:尚未選擇檔案"
Return
End If
ButtonSendFile.Enabled = False
ProgressBar1.Value = 0
LabelSendStatus.Text = "傳送狀態:傳送中"
Try
Await SendFileAsync(selectedFilePath)
LabelSendStatus.Text = "傳送狀態:完成"
Catch ex As Exception
LabelSendStatus.Text = "傳送狀態:失敗 - " & ex.Message
Finally
ButtonSendFile.Enabled = True
End Try
End Sub
Private Async Function SendFileAsync(ByVal filePath As String) As Task
Using client As New TcpClient()
Await client.ConnectAsync(IPAddress.Loopback, 9200)
Using stream As NetworkStream = client.GetStream()
Dim fileInfo As New FileInfo(filePath)
Dim lengthBytes() As Byte = BitConverter.GetBytes(fileInfo.Length)
Await stream.WriteAsync(lengthBytes, 0, lengthBytes.Length)
Dim buffer(8191) As Byte
Dim sentTotal As Long = 0
Using fileStream As New FileStream(filePath, FileMode.Open, FileAccess.Read)
Dim readCount As Integer
Do
readCount = Await fileStream.ReadAsync(buffer, 0, buffer.Length)
If readCount > 0 Then
Await stream.WriteAsync(buffer, 0, readCount)
sentTotal += readCount
Dim percent As Integer = CInt((sentTotal * 100) / fileInfo.Length)
ProgressBar1.Value = Math.Min(100, percent)
End If
Loop While readCount > 0
End Using
End Using
End Using
End Function
End Class
邏輯解析
- 先送出 8 bytes 的檔案長度,接收端才知道要收多少資料。
FileStream.ReadAsync與NetworkStream.WriteAsync可避免 UI 被長時間阻塞。- 大型檔案不建議一次
ReadAllBytes,分段讀取較穩定。
場景六:接收備份檔案並存到本機
檔案傳輸只寫傳送端還不夠完整。接收端需要先讀取 8 bytes 的檔案長度,再持續接收內容,直到累積位元組數等於檔案大小。這樣才知道檔案是否真的收完。
需要的主控項
ButtonStartFileReceiver:啟動檔案接收端。LabelReceiveStatus:顯示接收狀態。ProgressBar1:顯示接收進度。
範例程式碼
Imports System.IO
Imports System.Net
Imports System.Net.Sockets
Imports System.Threading.Tasks
Public Class Form1
Private fileListener As TcpListener
Private Async Sub ButtonStartFileReceiver_Click(sender As Object,
e As EventArgs) Handles ButtonStartFileReceiver.Click
ButtonStartFileReceiver.Enabled = False
ProgressBar1.Value = 0
LabelReceiveStatus.Text = "接收端狀態:等待檔案"
Try
fileListener = New TcpListener(IPAddress.Loopback, 9200)
fileListener.Start()
Using client As TcpClient = Await fileListener.AcceptTcpClientAsync()
Await ReceiveFileAsync(client)
End Using
LabelReceiveStatus.Text = "接收端狀態:檔案接收完成"
Catch ex As Exception
LabelReceiveStatus.Text = "接收端狀態:失敗 - " & ex.Message
Finally
If fileListener IsNot Nothing Then
fileListener.Stop()
fileListener = Nothing
End If
ButtonStartFileReceiver.Enabled = True
End Try
End Sub
Private Async Function ReceiveFileAsync(ByVal client As TcpClient) As Task
Using stream As NetworkStream = client.GetStream()
Dim lengthBytes(7) As Byte
Await ReadExactAsync(stream, lengthBytes, lengthBytes.Length)
Dim fileLength As Long = BitConverter.ToInt64(lengthBytes, 0)
If fileLength <= 0 OrElse fileLength > 1024L * 1024L * 200L Then
Throw New InvalidOperationException("檔案大小不合理。")
End If
Dim savePath As String = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
"received-backup.bin")
Dim buffer(8191) As Byte
Dim receivedTotal As Long = 0
Using fileStream As New FileStream(savePath, FileMode.Create, FileAccess.Write)
While receivedTotal < fileLength
Dim needRead As Integer = CInt(Math.Min(buffer.Length, fileLength - receivedTotal))
Dim readCount As Integer = Await stream.ReadAsync(buffer, 0, needRead)
If readCount = 0 Then
Throw New IOException("連線已中斷,檔案尚未收完。")
End If
Await fileStream.WriteAsync(buffer, 0, readCount)
receivedTotal += readCount
Dim percent As Integer = CInt((receivedTotal * 100) / fileLength)
ProgressBar1.Value = Math.Min(100, percent)
End While
End Using
End Using
End Function
Private Async Function ReadExactAsync(ByVal stream As NetworkStream,
ByVal buffer() As Byte,
ByVal size As Integer) As Task
Dim offset As Integer = 0
While offset < size
Dim readCount As Integer = Await stream.ReadAsync(buffer, offset, size - offset)
If readCount = 0 Then
Throw New IOException("連線已中斷。")
End If
offset += readCount
End While
End Function
End Class
邏輯解析
- 接收端先用
ReadExactAsync收滿 8 bytes 長度欄位。 - 接著依檔案長度分段接收內容,避免一次吃下整個檔案。
- 若
ReadAsync回傳 0,代表連線中斷,需要停止並回報錯誤。 - 此範例固定存成
received-backup.bin,正式程式可再加入檔名欄位或檔案類型欄位。
實務補充:Socket 完整度檢查
Socket 程式不只檢查能不能連線,還需要檢查下列面向:
- 連線流程:Server 是否先啟動,Port 是否可用,Client 是否連到正確位置。
- 訊息格式:文字、命令、檔案是否有明確格式,不混在同一段資料裡猜測。
- 訊息邊界:是否能處理粘包、拆包與半包資料。
- 背景處理:等待連線、等待資料、檔案傳輸是否避免卡住 UI。
- 例外處理:
SocketException、連線中斷、超時、資料不完整是否有處理。 - 資源釋放:
Socket、TcpClient、NetworkStream、FileStream是否確實關閉。
文章定位
這篇屬於 Socket 精進篇,適合建立完整觀念與 Windows Forms 實作骨架。若要再往大型正式系統延伸,通常會再補通訊協定版本、心跳封包、斷線重連、傳輸加密、連線池、日誌追蹤與壓力測試。
常見錯誤與排查方向
連線失敗
- Server 尚未啟動,Client 就先呼叫
Connect。 - IP 或 Port 寫錯。
- Port 已被其他程式占用。
- 防火牆阻擋外部連線。
資料不完整或黏在一起
- TCP 是串流,
Receive不等於完整一筆訊息。 - 沒有設計訊息結尾或長度欄位。
- 文字與二進位資料混在一起,沒有明確格式。
- 接收端沒有累積緩衝區。
Windows Forms 畫面卡住
Accept、Receive或檔案傳輸直接寫在 UI 執行緒。- 大量傳輸中直接更新控制項太頻繁。
- 背景執行緒直接修改控制項,造成跨執行緒錯誤。
重點整理
Socket是網路通訊的端點,Server 與 Client 都透過它收發資料。- Server 的基本流程是
Bind、Listen、Accept、Receive、Send。 - Client 的基本流程是
Connect、Send、Receive、關閉連線。 - TCP 是串流,正式程式需要設計訊息邊界,例如長度前綴。
- Windows Forms 中不要把等待式 Socket 操作直接放在 UI 執行緒。
- 背景接收或非同步流程更新 UI 時,需要注意跨執行緒問題。
- 檔案傳輸應分段處理,並先告知接收端總長度。
- Socket 程式需要明確的停止與釋放流程,避免 Port 被占用或程式無法正常關閉。