本文章不作任何商业用途 仅作学习与交流 教程来自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 created
void 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 = 1Mb
byte[] 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();
}
}
测试