所讀版本:機械工業出版社

這一篇在做什麼

這是整個 Unity 網絡遊戲筆記系列的起點。先把最基本的概念講清楚:

  • 什麼是 Socket
  • 端口在連接裡扮演什麼角色
  • 客戶端與服務端之間的基本通信流程
  • 如何用一個最簡單的 Echo 範例把整套流程跑起來

先理解 Socket 是什麼

兩個程序如果要通過網絡交換數據,雙方都需要一個通信端點。這個端點就是 Socket

一個 Socket 通常包含幾項關鍵信息:

  • 使用的通信協議
  • 本地主機 IP
  • 本地端口
  • 遠程主機 IP
  • 遠程端口

可以把它理解成一個「帶地址的雙向通道端口」。真正的數據收發,是在這個端點上完成的。

端口的作用

每台設備都可以分配 0~65535 的端口。當兩台機器建立一條 Socket 連接時,本質上就是:

  • 本地某個端口
  • 經過網絡介質
  • 連到遠程某個端口

同一個進程可以持有多個 Socket,因此也可能同時使用多個端口對外通信。

對應到常見的客戶端 / 服務端模型,可以這樣理解:

  • 客戶端通常使用不同的本地端口主動發起連接
  • 服務端通常固定在某個端口上持續監聽
  • 客戶端連入後,服務端才會為這次連接建立對應的通信 Socket

一條 Socket 連接怎麼建立

最基本的 TCP 通信流程可以拆成:

  1. 客戶端與服務端各自建立 Socket。
  2. 服務端開始監聽端口。
  3. 客戶端發起連接,也就是三次握手。
  4. 服務端接受連接。
  5. 雙方開始收發數據。
  6. 任一方關閉連接,也就是四次揮手。

用 Echo 程序跑通最小案例

Echo 程序的目標很簡單:

  • 客戶端送出一段文字
  • 服務端收到後原樣回傳
  • 客戶端再把回應顯示出來

雖然功能極小,但這個例子已經包含了網絡程序最基本的骨架。

客戶端實作

客戶端要做兩件事:

  1. 建立連接
  2. 發送請求並接收回應
public class Echo : MonoBehaviour
{
    Socket socket;

    public TMP_InputField reqField;
    public TMP_Text responseText;

    public void Connect()
    {
        //InterNetwork = IPV4
        socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        socket.Connect("127.0.0.1", 8888); //阻塞
    }

    public void Send()
    {
        //Send Req
        string sendStr = reqField.text;
        byte[] sendBytes = System.Text.Encoding.UTF8.GetBytes(sendStr);
        socket.Send(sendBytes); //阻塞

        //Receive Rsp
        byte[] readBuff = new byte[1024];
        int count = socket.Receive(readBuff); //阻塞
        string recevieStr = System.Text.Encoding.UTF8.GetString(readBuff, 0, count);
        responseText.text = recevieStr;
        socket.Close();
    }
}

這裡有幾個重要參數:

AddressFamily

用來指定地址族,常見的有:

  • InterNetwork:IPv4
  • InterNetworkV6:IPv6

SocketType

用來指定 Socket 類型,這裡最常見的是兩種:

Dgram

  • 使用 UDP
  • 不基於連接
  • 可以和多個主機通信
  • 不保證可靠送達

Stream

  • 使用 TCP
  • 基於連接
  • 通常對應單一主機連接
  • 有可靠傳輸保證

ProtocolType

用來指定底層協議,例如:

  • Tcp
  • Udp
  • Raw

這個範例選的是 Stream + Tcp,也就是最常見的 TCP 流式連接。

服務端實作

服務端的工作順序通常是:

  1. 建立 Socket
  2. 綁定 IP 與端口
  3. 開始監聽
  4. 接受客戶端連接
  5. 接收數據並回傳
using System.Net;
using System.Net.Sockets;

//Create Socket
Socket listenSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

//Bind
IPAddress ipAdr = IPAddress.Parse("127.0.0.1");
IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888); //綁定IP和端口
listenSocket.Bind(ipEp);

//Listen
listenSocket.Listen(0);
Console.WriteLine("Server Activated");

while (true)
{
    //Accept Client Connection
    Socket connectSocket = listenSocket.Accept(); //阻塞
    Console.WriteLine("Server Accept Connection");

    //Receive
    byte[] readBuff = new byte[1024];
    int count = connectSocket.Receive(readBuff);
    string readStr = System.Text.Encoding.UTF8.GetString(readBuff, 0, count);
    Console.WriteLine("Server Receive: " + readStr);

    //Send Rsp
    byte[] sendBytes = System.Text.Encoding.UTF8.GetBytes(readStr);
    connectSocket.Send(sendBytes);
}

服務端幾個關鍵步驟

Bind

Bind 的作用是把 Socket 綁到指定的 IP 與端口上。綁定後,這個 Socket 才知道自己要在哪個地址對外提供服務。

Listen

Listen 表示開始監聽。從這一步開始,服務端會等待外部客戶端連入。

Accept

Accept 用來接受客戶端的連接。這個方法是阻塞的,也就是在真的有客戶端連進來之前,它會一直卡在那裡等待。

IPAddress 與 IPEndPoint

  • IPAddress:表示 IP 地址
  • IPEndPoint:把 IP 與端口組合成完整的網絡端點

這個 Echo 例子真正教會了什麼

雖然整個程序很短,但它已經完整展示了最基礎的 TCP 編程模型:

  • 客戶端主動連接
  • 服務端被動監聽
  • 雙方都用字節流發送與接收數據
  • 同步 ConnectSendReceiveAccept 都可能阻塞

這也為後面幾篇的異步、消息隊列、粘包半包問題打下了基礎。

這一篇先記住的重點

如果只先記最核心的東西,可以收斂成下面幾句:

  • Socket 是網絡通信的端點
  • TCP 連接依賴 IP + 端口建立
  • 服務端先 Bind、再 Listen、最後 Accept
  • 客戶端通常主動 Connect
  • SendReceive 處理的都是字節數據
  • 最簡單的 Echo 程序,已經足夠展示完整的最小通信流程