???????????????本文章不作任何商業用途 僅作學習與交流 教程來自Unity唐老獅
? ? ? ? 關于練習題部分是我觀看教程之后自己實現 所以和老師寫法可能不太一樣
????????唐老師說掌握其基本思路即可,因為前端程序一般不需要去寫后端邏輯
1.認識Socket的重要API
Socket是什么
???Socket
(套接字)是計算機網絡編程中用于實現網絡通信的一種機制,它提供了不同主機之間進行數據交換的接口。通過?Socket
,程序可以在網絡上發送和接收數據,實現客戶端與服務器之間的通信。在 .NET 框架中,Socket
?類位于?System.Net.Sockets
?命名空間,它封裝了底層的網絡通信細節,使得開發者能夠方便地進行網絡編程
創建Socket
Socket serverTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream,ProtocolType.Tcp);
參數 | 含義 | 常見取值及說明 |
---|---|---|
AddressFamily | 網絡地址類型 | InterNetwork :IPv4;InterNetworkV6 :IPv6 |
SocketType | 通信方式 | Stream :面向連接,用 TCP;Dgram :無連接,用 UDP |
ProtocolType | 傳輸協議 | Tcp :配合?Stream ?實現可靠通信;Udp :配合?Dgram ?實現低延遲通信 |
Socket常用屬性
檢查接收緩沖區可用字節數
Socket.Available
獲取服務端本地綁定的地址信息
IPEndPoint localEp = clientSocket.LocalEndPoint as IPEndPoint;Console.WriteLine($"服務端本地地址:{localEp.Address}, 端口:{localEp.Port}");
屬性名稱 | 定義與作用 | 返回值類型 | 使用說明 |
---|---|---|---|
Available | 獲取當前接收緩沖區中可讀取的字節數,用于判斷當前有多少網絡數據已到達且可被讀取 | int | 直接返回可用字節數,如?int availableBytes = socketTcp.Available; ,輔助優化數據讀取邏輯 |
LocalEndPoint | 獲取套接字綁定的本地網絡端點(包含本地 IP 地址和端口號) | object | 需強制轉換為?IPEndPoint ?使用,如:IPEndPoint localIpEndPoint = socketTcp.LocalEndPoint as IPEndPoint; 轉換后可通過? .Address ?取 IP,.Port ?取端口 |
Socket常用方法
服務端
步驟 | 說明 | 關鍵代碼 |
---|---|---|
1-1 | 綁定 IP 和端口,為服務端指定通信地址與端口 | IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080); socketTcp.Bind(ipPoint); |
1-2 | 設置允許同時連接的客戶端最大數量,控制服務端連接隊列長度 | socketTcp.Listen(10); |
1-3 | 阻塞等待客戶端連接,成功連接后返回與客戶端通信的專屬套接字 | socketTcp.Accept(); |
客戶端
步驟 | 說明 | 關鍵代碼 |
---|---|---|
客戶端發起連接 | 讓客戶端套接字與目標服務端(通過 IP 地址和端口標識)建立網絡連接,構建通信鏈路 | socketTcp.Connect(IPAddress.Parse("118.12.123.11"), 8080); |
通用
分類 | 步驟 | 說明 | 關鍵代碼 |
---|---|---|---|
客戶端服務端通用操作 | 1 | 釋放連接并關閉 Socket,需先調用?Shutdown | socketTcp.Shutdown(SocketShutdown.Both); |
2 | 關閉連接,釋放所有 Socket 關聯資源 | socketTcp.Close(); | |
3 | 同步發送和接收數據 | 通常如?
| |
4 | 異步發送和接收數據 | 通常涉及?BeginSend ?BeginReceive ?等異步方法,如?socketTcp.BeginReceive(回調, 狀態對象); |
2.TCP連接(UDP同理)
服務端: 注意 服務端創建了兩個套接字
一個是服務端本體用于綁定、監聽客戶端連接
一個是通道 用于對 該 客戶端 收發數據
????????當然只是一個客戶端的情況 如果有多個客戶端 你可以將channel直接命名為對應客戶端的名字
如圖所示:
127.0.0.1是特殊地址 也就是 回環地址 可以不需要網絡 就讓本機成為服務端
using System.Net;
using System.Net.Sockets;
using System.Text;
//服務器端套接字流程//1 創建服務端套接字
Socket serverTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream,ProtocolType.Tcp);//2 服務端綁定門牌號 并 監聽請求客戶端連接 "127.0.0.1"是回環地址
IPEndPoint sIpoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"),8080);
serverTcp.Bind(sIpoint);
serverTcp.Listen(1024);
Console.WriteLine("等待客戶端連接");//3 建立連接和等待返回 通道套接字 (三次握手以后,所以該處會阻塞線程)
Socket channelSocket = serverTcp.Accept();
Console.WriteLine("未建立連接時 不會打印這句話");//4 通過中間套接字 進行數據收發
channelSocket.Send(Encoding.UTF8.GetBytes("服務端:歡迎連接服務端"));//服務端接收時需要有容器
byte[] result = new byte[1024];
int receiveNums = channelSocket.Receive(result);//返回接收的字節數
Console.WriteLine($"獲取的客戶端Ip和端口{channelSocket.RemoteEndPoint}," +$"接受到的消息為:{Encoding.UTF8.GetString(result, 0, receiveNums)}");//5 釋放連接
channelSocket.Shutdown(SocketShutdown.Both);
//6 關閉套接字
channelSocket.Close();
關于同步阻塞線程問題?
客戶端:客戶端只需要自己的套接字即可
using System.Net;
using System.Net.Sockets;
using System.Text;
using UnityEngine;public class Socatct : MonoBehaviour
{// Start is called once before the first execution of Update after the MonoBehaviour is createdvoid Start(){//Socket clientUdp = new Socket(AddressFamily.InterNetwork,SocketType.Dgram,ProtocolType.Udp);//Socket clientTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);//1 創建客戶端套接字Socket clientTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);//2 通過IPEndPoint確定服務端ip和端口IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);//3 嘗試連接clientTcp.Connect(ipPoint);//4 客戶端接受服務端數據 和 發送數據 byte[] receiveBytes = new byte[1024];int resultNum = clientTcp.Receive(receiveBytes);clientTcp.Send(Encoding.UTF8.GetBytes("這句話是客戶端發來的"));//打印服務端來的信息Debug.Log(Encoding.UTF8.GetString(receiveBytes,0, resultNum));//5 釋放連接和關閉套接字clientTcp.Shutdown(SocketShutdown.Both);clientTcp.Close();}
}
圖解
3.服務端練習題
問題1:
using System.Net;
using System.Net.Sockets;
using System.Text;Server server = new Server();server.Init(true,false);
server.CloseAllCannel();
//服務端類
class Server
{public Socket serverSocket;public List<Socket> cannelSockets = new List<Socket>();private Thread? acceptThread;private Thread? getMsgThread;private readonly object _lockObject = new object();/// <summary>/// 初始化 選擇TCP還是UDP連接/// </summary>/// <param name="tcp"></param>/// <param name="udp"></param>public void Init(bool tcp, bool udp){if (tcp)serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);if (udp)serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);BindIPEndPoint();InitThread();}/// <summary>/// 綁定IP地址和端口號/// </summary>/// <param name="ip"></param>/// <param name="port"></param>private void BindIPEndPoint(string ip = "127.0.0.1", int port = 8080){IPEndPoint iPEndPoint = new IPEndPoint(IPAddress.Parse(ip), port);serverSocket.Bind(iPEndPoint);serverSocket.Listen(1024);Console.WriteLine("等待客戶端連接");}private void InitThread(){acceptThread = new Thread(AcceptClient);acceptThread.Start();getMsgThread = new Thread(GetMsgFromClient);getMsgThread.Start();}/// <summary>/// 等待客戶端連接/// </summary>private void AcceptClient(){while (true){Socket cannelSocket = serverSocket.Accept();// 默認發送消息cannelSocket.Send(Encoding.UTF8.GetBytes("歡迎來到服務器"));lock (_lockObject){cannelSockets.Add(cannelSocket);}}}private void GetMsgFromClient(){//容器 1024 * 1Kb = 1Mbbyte[] buffer = new byte[1024 * 1024];int length;while (true){List<Socket> socketsCopy;lock (_lockObject){socketsCopy = new List<Socket>(cannelSockets);}foreach (Socket cannelSocket in socketsCopy){if (cannelSocket.Available > 0) //有數據可讀{length = cannelSocket.Receive(buffer);//消息處理交給新線程ThreadPool.QueueUserWorkItem(HandleMsg, (cannelSocket, Encoding.UTF8.GetString(buffer, 0, length)));}}}}private void HandleMsg(object msg){(Socket s, string srt) info = ((Socket s, string srt))msg;Console.WriteLine($"客戶端IP以及端口號=>{info.s.RemoteEndPoint}" +$"發送消息:{info.srt}");}public void CloseAllCannel(){string input = Console.ReadLine();while (true){if (input == "Quit"){lock (_lockObject){foreach (Socket cannelSocket in cannelSockets){cannelSocket.Shutdown(SocketShutdown.Both);cannelSocket.Close();}cannelSockets.Clear();Console.WriteLine("已關閉所有連接");}break;}}}
}
問題2?
就是把服務端的Channel(也就是對應Client的通道 封裝成一個類)?
using System.Net;
using System.Net.Sockets;
using System.Text;Server server = new Server();
server.Init(true, false);
server.CloseAllCannel();// 通道類,封裝channelSocket相關操作
class Channel
{private readonly Socket _socket;private readonly byte[] _buffer = new byte[1024 * 1024];public Channel(Socket socket){_socket = socket;}// 發送消息public void SendMessage(string message){byte[] data = Encoding.UTF8.GetBytes(message);_socket.Send(data);}// 接收消息(異步處理更合適,這里簡化示例)public string? ReceiveMessage(){if (_socket.Available > 0){int length = _socket.Receive(_buffer);return Encoding.UTF8.GetString(_buffer, 0, length);}return null;}// 關閉通道public void Close(){_socket.Shutdown(SocketShutdown.Both);_socket.Close();}public IPEndPoint RemoteEndPoint => (IPEndPoint)_socket.RemoteEndPoint!;
}// 服務端類
class Server
{public Socket serverSocket;private List<Channel> channelList = new List<Channel>();private Thread? acceptThread;private Thread? getMsgThread;private readonly object _lockObject = new object();/// <summary>/// 初始化 選擇TCP還是UDP連接/// </summary>/// <param name="tcp"></param>/// <param name="udp"></param>public void Init(bool tcp, bool udp, string ip = "127.0.0.1", int port = 8080){if (tcp)serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);if (udp)serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);BindIPEndPoint(ip, port);InitThread();}/// <summary>/// 綁定IP地址和端口號/// </summary>/// <param name="ip"></param>/// <param name="port"></param>public void BindIPEndPoint(string ip, int port){IPEndPoint iPEndPoint = new IPEndPoint(IPAddress.Parse(ip), port);serverSocket.Bind(iPEndPoint);serverSocket.Listen(1024);Console.WriteLine("等待客戶端連接");}private void InitThread(){acceptThread = new Thread(AcceptClient);acceptThread.Start();getMsgThread = new Thread(GetMsgFromClient);getMsgThread.Start();}/// <summary>/// 等待客戶端連接/// </summary>private void AcceptClient(){while (true){Socket channelSocket = serverSocket.Accept();Channel channel = new Channel(channelSocket);channel.SendMessage("歡迎來到服務器");lock (_lockObject){channelList.Add(channel);}}}private void GetMsgFromClient(){while (true){List<Channel> channelsCopy;lock (_lockObject){channelsCopy = new List<Channel>(channelList);}foreach (Channel channel in channelsCopy){string? msg = channel.ReceiveMessage();if (msg != null){// 消息處理交給新線程ThreadPool.QueueUserWorkItem(HandleMsg, (channel, msg));}}}}private void HandleMsg(object state){(Channel channel, string msg) info = ((Channel channel, string msg))state;Console.WriteLine($"客戶端IP以及端口號=>{info.channel.RemoteEndPoint} 發送消息:{info.msg}");}public void CloseAllCannel(){string input = Console.ReadLine();while (true){if (input == "Quit"){lock (_lockObject){foreach (Channel channel in channelList){channel.Close();}channelList.Clear();Console.WriteLine("已關閉所有連接");}break;}}}
}
4.客戶端?練習題
問題:
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using UnityEngine;public class ClientManager
{private Socket clientTcp;private Thread receiveThread;private bool isRunning;public void Connect(string ip, int port){try{// 1 創建客戶端套接字clientTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);// 2 通過 IPEndPoint 確定服務端 ip 和端口IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), port);// 3 嘗試連接clientTcp.Connect(ipPoint);isRunning = true;// 啟動接收線程receiveThread = new Thread(ReceiveData);receiveThread.Start();}catch (Exception e){Debug.LogError($"連接失敗: {e.Message}");}}private void ReceiveData(){try{while (isRunning){byte[] receiveBytes = new byte[1024];int resultNum = clientTcp.Receive(receiveBytes);if (resultNum > 0){// 打印服務端來的信息string message = Encoding.UTF8.GetString(receiveBytes, 0, resultNum);Debug.Log($"接收到服務端消息: {message}");}}}catch (Exception e){Debug.LogError($"接收數據出錯: {e.Message}");}}public void SendMessage(string message){try{if (clientTcp != null && clientTcp.Connected){clientTcp.Send(Encoding.UTF8.GetBytes(message));}}catch (Exception e){Debug.LogError($"發送消息出錯: {e.Message}");}}public void Disconnect(){isRunning = false;if (receiveThread != null && receiveThread.IsAlive){receiveThread.Join();}if (clientTcp != null && clientTcp.Connected){// 5 釋放連接和關閉套接字clientTcp.Shutdown(SocketShutdown.Both);clientTcp.Close();}}
}
使用:
using UnityEngine;public class UseSocketClient練習題 : MonoBehaviour
{ClientManager clientManager;void Start(){clientManager = new ClientManager();// 修改連接的 IP 地址和端口號clientManager.Connect("127.0.0.1",8080);// 修改發送的消息內容clientManager.SendMessage("這是修改后的消息");}void OnDestroy(){clientManager.Disconnect();}
}
?測試