【超详细】TCP编程与UDP编程

目录

1.Socket编程

2.TCP编程

2.1.具体步骤

2.2.TCP编程步骤核心特点

2.3.注意

2.4.实现TCP编程

2.5.测试TCP编程

3.UDP编程

3.1.具体步骤

3.2.UDP编程步骤核心特点

3.3.注意

3.4.实现UDP编程

3.5.测试UDP编程

4.总结


本篇文章来分享一下Socket编程,主要了解TCP编程、UDP编程。

1.Socket编程

1)Socket编程基于TCP/UDP协议的网络编程,是程序员写代码实现设备通信的具体步骤;

2)所有网络通信都是服务端+客户端模式:服务端是提供数据/服务的一方,客户端是请求数据/服务的一方

2.TCP编程

2.1.具体步骤

TCP是C/S架构,核心:有连接、双向交互

✅ 服务端(被动方:等待客户端连接,永远先启动)【4步】

【1】①创建服务端的通讯对象/监听对象 → 准备好通信管道;

【2】②启动监听对象,绑定IP地址+端口号 → 服务端开始在指定端口等待客户端连接;

【5】③阻塞等待,接受客户端的连接请求 → TCP 的核心步骤,和客户端完成「三次握手」,建立可靠连接;

【6】④数据交换:收发数据(本质是网络流的读写) → 客户端发数据,服务端收;服务端发数据,客户端收,双向通信;

✅ 客户端(主动方:主动连接服务端,后启动)【3 步】

【3】①创建客户端的通讯对象 → 准备好通信管道;

【4】②调用连接方法,连接指定的服务器(IP +端口) → 向服务端发起连接请求,完成三次握手;

【7】③数据交换:收发数据(网络流的读写) → 和服务端互相传输数据;

2.2.TCP编程步骤核心特点

1)必须先启动服务端,再启动客户端,否则客户端连接失败;

2)建立连接后,服务端和客户端是一对一的专属通信,稳定可靠;

3)所有步骤不可逆,必须按顺序执行。

2.3.注意

1)TCP 传输完成后,服务端和客户端还需要执行「关闭连接」,完成四次挥手。

2)标注的数字(如【1】)是执行顺序,绝对不能颠倒!

2.4.实现TCP编程

使用本地测试,服务端是控制台程序,本地启动后,Unity客户端连127.0.0.1即可通信,完全免费、无网络限制、稳定。

1)新建控制台程序

2)实现服务端

cs 复制代码
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace UnityTCPServer
{
    /// <summary>
    /// TCP服务端
    /// </summary>
    class TCPServer
    {
        static void Main(string[] args)
        {
            Console.WriteLine("TCP服务端:本地控制台服务端");
            Console.WriteLine("服务端已启动,监听端口:9999");
            Console.WriteLine("等待Unity客户端连接...\n");

            //【1】①创建监听对象
            TcpListener server = new TcpListener(IPAddress.Any, 9999);
            try
            {
                //【2】②启动监听
                server.Start();
                //【5】③阻塞等待客户端连接 (控制台不怕阻塞)
                Socket socket = server.AcceptSocket();
                Console.WriteLine($"[{DateTime.Now}]客户端连接成功!客户端IP:{socket.RemoteEndPoint}");

                if (socket.Connected)
                {
                    //【6】④数据交换:服务端【先发后收】
                    NetworkStream stream = new NetworkStream(socket);
                    StreamReader reader = new StreamReader(stream, Encoding.UTF8);
                    StreamWriter writer = new StreamWriter(stream, Encoding.UTF8) { AutoFlush = false };

                    //第一步:服务端发送消息给服务端
                    Console.WriteLine("\n请输入要发送给Unity客户端的信息:");
                    string sendMsg = Console.ReadLine();
                    writer.WriteLine(sendMsg);
                    //StreamWriter有缓冲区,写入的数据会先存到缓冲区,不会立刻发送到网络,只有调用Flush()才会强制把缓冲区的所有数据发送出去,不写的话,数据发不出去,服务端就永远收不到消息
                    writer.Flush();//清空缓冲区,必须写,否则数据发不出去
                    Console.WriteLine($"[{DateTime.Now}]服务端已发送:{sendMsg}");

                    //第二步:服务端接收服务端的消息
                    string recvMsg = reader.ReadLine();
                    Console.WriteLine($"[{DateTime.Now}]服务端收到客户端消息:{recvMsg}");

                    //关闭资源
                    writer.Close();
                    reader.Close();
                    stream.Close();
                    socket.Close();
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"[{DateTime.Now}]服务端异常:{ex.Message}" );
            }
            finally
            {
                server.Stop();
                Console.WriteLine($"\n[{DateTime.Now}]服务端已关闭,按任意键退出...");
                Console.ReadKey();
            }
        }
    }
}

3)实现客户端

cs 复制代码
using System;
using System.IO;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

namespace SocketProgram
{
    /// <summary>
    /// TCP客户端
    /// </summary>
    public class TCPClientPanel : MonoBehaviour
    {
        private Button connectBtn;
        private TMP_Text tipContent;
        //服务器配置(本地测试:本地控制台服务端)
        private string serverIp = "127.0.0.1";//"tcpbin.com";
        private int serverPort = 9999;//4242;

        //TCP核心对象
        private TcpClient tcpClient;
        private NetworkStream netStream;
        private StreamReader reader;
        private StreamWriter writer;

        private void Awake()
        {
            //绑定UI组件
            tipContent = transform.Find("TipContent").GetComponent<TMP_Text>();
            connectBtn = transform.Find("ConnectBtn").GetComponent<Button>();

            connectBtn.onClick.AddListener(OnConnectBtnClicked);

            tipContent.text = "点击按钮连接TCP服务端";
        }
        private async void OnConnectBtnClicked()
        {
            connectBtn.interactable = false;//防止重复点击
            tipContent.text = "正在连接服务端...";
            await ConnectAndCommunicate();//异步执行,不会卡死Unity
            connectBtn.interactable = true;
        }
        /// <summary>
        /// 异步TCP连接+数据交互(核心关键)
        /// </summary>
        /// <returns></returns>
        private async Task ConnectAndCommunicate()
        {
            try
            {
                //【3】①创建客户端对象
                tcpClient = new TcpClient();
                //【4】②异步连接服务器(无阻塞)
                //控制台的reader.ReadLine()、writer.WriteLine()都是同步阻塞方法
                //Unity是单线程渲染,一旦执行这些方法,整个Unity编辑器会卡死,直到有数据/连接
                //使用异步方法(async/await)可以解决该问题
                await tcpClient.ConnectAsync(serverIp, serverPort);

                if (tcpClient.Connected)
                {
                    tipContent.text = $"[{DateTime.Now}]连接服务端成功!等待接收消息...";
                    Socket socket = tcpClient.Client;
                    netStream = new NetworkStream(socket);
                    reader = new StreamReader(netStream, Encoding.UTF8);
                    writer = new StreamWriter(netStream, Encoding.UTF8) { AutoFlush = false };

                    //【7】③数据交换:客户端【先收后发】
                    //第一步:客户端接收服务端的消息
                    string recvMsg = await reader.ReadLineAsync();
                    tipContent.text += $"\n[{DateTime.Now}]收到服务端消息:{recvMsg}";

                    //第二步:客户端发送消息给服务端
                    string sendMsg = $"[{DateTime.Now}]Unity客户端收到消息啦!已回复";
                    await writer.WriteLineAsync(sendMsg);
                    writer.Flush();//必须刷新缓冲区,数据才会发出去
                    tipContent.text += $"\n[{DateTime.Now}]已发送消息给服务端:{sendMsg}";
                }
            }
            catch (Exception ex)
            {
                tipContent.text = $"[{DateTime.Now}]连接/通信失败:{ex.Message}";
            }
            finally
            {
                //关闭所有资源,防止内存泄漏
                CloseAll();
                tipContent.text += $"\n\n[{DateTime.Now}]连接已关闭,可重新点击连接";
            }
        }
        /// <summary>
        /// 关闭TCP所有资源的方法
        /// </summary>
        private void CloseAll()
        {
            if (writer != null) writer.Close();
            if (reader != null) reader.Close();
            if (netStream != null) netStream.Close();
            tcpClient?.Close();
            tcpClient = null;
        }
        /// <summary>
        /// Unity退出时关闭连接
        /// </summary>
        private void OnDestroy()
        {
            CloseAll();
        }
    }
}

2.5.测试TCP编程

1)先启动TCP服务端,若是不先启动,会无法连接

2)在客户端点击"连接服务端"

3)在服务端输入要给客户端发送的信息,如:Hi,我是服务端!客户端收到信息并回复后,服务端和客户端的连接断开。

也可以使用免费公网TCP测试服务器,IP为tcpbin.com,端口为4242,该服务器的功能:发什么,它就原封不动给返回什么(回声服务器),适合测试TCP收发是否正常。

3.UDP编程

3.1.具体步骤

UDP无连接,步骤极简,核心:无连接、直接收发

UDP没有服务端/客户端的严格区分,只有发送方/接收方,双方平等,无需建立连接。

✅ 接收方【2 步】

①创建接收方的通讯对象/监听对象 → 准备好通信管道,绑定IP +端口;

②数据交换:直接收发数据(网络流读写) → 无需等待连接,只要有数据过来,直接接收即可。

✅ 发送方【2 步】

①创建发送方的通讯对象 → 准备好通信管道;

②数据交换:直接收发数据 → 无需连接,直接把数据发送到目标IP +端口即可,发完即结束。

3.2.UDP编程步骤核心特点

1)无需建立连接,没有握手和挥手,代码极简;

2)发送方发完数据就完事,不管接收方是否在线、是否收到。

3.3.注意

UDP无连接,客户端可以先发数据,服务端后接收;或者服务端先发,客户端后接收。

3.4.实现UDP编程

使用本地测试,服务端是控制台程序,本地启动后,Unity客户端连127.0.0.1即可通信,完全免费、无网络限制、稳定。

1)新建控制台程序,步骤同TCP

2)实现服务端

cs 复制代码
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace ConsoleUDPServer
{
    /// <summary>
    /// UDP服务端
    /// </summary>
    class UDPServer
    {
        static void Main(string[] args)
        {
            Console.WriteLine("UDP服务端:本地控制台服务端");
            Console.WriteLine("服务端绑定端口:8888 (接收客户端消息)");
            Console.WriteLine("客户端绑定端口:9999 (接收服务端消息)");

            //UDP配置,端口分离
            int serverListenPort = 8888;
            string clientIP = "127.0.0.1";
            int clientListenPort = 9999;

            //①创建创建监听对象
            //绑定8888端口,持续监听+发送
            UdpClient udpServer = new UdpClient(serverListenPort);
            IPEndPoint clientPoint = new IPEndPoint(IPAddress.Parse(clientIP), clientListenPort);

            try
            {
                //②数据交换
                //1)作为接收方,接收数据
                //新开线程:持续监听8888端口,接收客户端发送的消息
                System.Threading.Thread receiveThread = new System.Threading.Thread(() =>
                {
                    //初始化在循环外,避免每次接收都重新创建,导致无法正确获取客户端地址
                    IPEndPoint remotePoint = new IPEndPoint(IPAddress.Any, 0);
                    while (true)
                    {
                        try
                        {
                            byte[] recvBytes = udpServer.Receive(ref remotePoint);
                            string recvInfo = Encoding.UTF8.GetString(recvBytes);
                            Console.WriteLine($"\n[{DateTime.Now}]【收到客户端消息】:{recvInfo}");
                            Console.WriteLine($"消息来源:{remotePoint}\n");
                        }
                        catch { }
                    }
                });
                receiveThread.IsBackground = true;
                receiveThread.Start();

                //2)作为发送方,发送数据
                //主线程:持续输入内容,发送消息给客户端
                while (true)
                {
                    Console.WriteLine("\n请输入要发送给客户端的消息:");
                    string sendInfo = Console.ReadLine();
                    byte[] sendBytes = Encoding.UTF8.GetBytes(sendInfo);
                    udpServer.Send(sendBytes, sendBytes.Length, clientPoint);
                    Console.WriteLine($"[{DateTime.Now}]服务端已发送:{sendInfo} → 客户端({clientIP}:{clientListenPort})");
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"[{DateTime.Now}]发送失败:{ex.Message}");
            }
            finally
            {
                udpServer.Close();
                Console.WriteLine($"[{DateTime.Now}]服务端已关闭");
            }
        }
    }
}

3)实现客户端

cs 复制代码
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

namespace SocketProgram
{
    /// <summary>
    /// UDP客户端
    /// </summary>
    public class UDPClientPanel : MonoBehaviour
    {
        private TMP_Text tipContent;
        private Button sendBtn;
        private TMP_InputField inputMsg;

        //UDP配置:端口分离,永不冲突
        private int clientListenPort = 9999;//客户端自己的监听端口(收消息用)
        private string serverIP = "127.0.0.1";
        private int serverListenPort = 8888;//服务端的监听端口(给服务端发消息必须发这个端口)
        private UdpClient udpClient_Receive;//专门用于【接收】的UDP对象
        private UdpClient udpClient_Send;//专门用于【发送】的UDP对象

        private void Awake()
        {
            tipContent = transform.Find("TipContent").GetComponent<TMP_Text>();
            sendBtn = transform.Find("SendBtn").GetComponent<Button>();
            inputMsg = transform.Find("SendInputField").GetComponent<TMP_InputField>();

            sendBtn.onClick.AddListener(OnSendBtnClick);

            StartReceive();
            tipContent.text = $"UDP客户端就绪\n1.已监听端口:{clientListenPort},等待接收服务端消息\n2.输入内容点击发送:给服务端发消息";
        }
        private async void StartReceive()
        {
            await ReceiveUDPData();
        }
        /// <summary>
        /// 异步接收UDP数据,持续监听
        /// </summary>
        private async Task ReceiveUDPData()
        {
            try
            {
                //1)作为接收方
                //①创建监听对象
                //绑定9999端口,固定监听,永不随机
                udpClient_Receive = new UdpClient(clientListenPort);
                //持续接收,直到手动停止
                while (true)
                {
                    //②数据交换
                    UdpReceiveResult result = await udpClient_Receive.ReceiveAsync();
                    string recvInfo = Encoding.UTF8.GetString(result.Buffer);
                    if (tipContent != null)
                    {
                        tipContent.text = $"[{DateTime.Now}]收到消息:{recvInfo}\n来源:{result.RemoteEndPoint}\n持续监听9999端口中...";
                    }
                }
            }
            catch (SocketException ex)
            {
                if (tipContent != null)
                {
                    if (ex.SocketErrorCode == SocketError.AddressAlreadyInUse)
                    {
                        tipContent.text = $"[{DateTime.Now}]接收失败:9999端口被占用,请关闭其他程序重试";
                    }
                    else
                    {
                        tipContent.text = $"[{DateTime.Now}]接收异常:{ex.Message}";
                    }
                }
            }
            catch (Exception ex)
            {
                if (tipContent != null) tipContent.text = $"[{DateTime.Now}]接收失败:{ex.Message}";
            }
            finally
            {
                CloseAll();
            }
        }
        private async void OnSendBtnClick()
        {
            if (string.IsNullOrEmpty(inputMsg?.text))
            {
                if (tipContent != null) tipContent.text = "请输入要发送的消息!";
                return;
            }
            if (sendBtn != null) sendBtn.interactable = false;
            string sendStr = inputMsg.text;
            if (tipContent != null) tipContent.text = $"正在发送消息:{sendStr}";

            await SendUDPData(sendStr);

            if (sendBtn != null) sendBtn.interactable = true;
            if (tipContent != null)
            {
                tipContent.text = $"[{DateTime.Now}]发送成功:{sendStr}\n发送至:{serverIP}:{serverListenPort}\n客户端可继续收发消息";
            }
        }
        private async Task SendUDPData(string sendStr)
        {
            try
            {
                //2)作为发送方
                //①创建监听对象
                //发送时不要绑定9999端口,让系统随机分配发送端口
                using (UdpClient sendClient = new UdpClient())
                {
                    //②数据交换
                    byte[] sendBytes = Encoding.UTF8.GetBytes(sendStr);
                    IPEndPoint serverPoint = new IPEndPoint(IPAddress.Parse(serverIP), serverListenPort);
                    await sendClient.SendAsync(sendBytes, sendBytes.Length, serverPoint);
                }
            }
            catch (Exception ex)
            {
                if (tipContent != null) tipContent.text = $"[{DateTime.Now}]发送失败:{ex.Message}";
            }
        }
        /// <summary>
        /// 安全关闭UDP资源,释放端口
        /// </summary>
        private void CloseAll()
        {
            udpClient_Receive?.Close();
            udpClient_Send?.Close();
            udpClient_Receive = null;
            udpClient_Send = null;
        }
        /// <summary>
        /// Unity退出/销毁时,安全关闭所有资源
        /// </summary>
        private void OnDestroy()
        {
            CloseAll();
            //清空所有引用
            tipContent = null;
            sendBtn = null;
            inputMsg = null;
        }
    }
}

3.5.测试UDP编程

1)启动服务端、客户端

2)客户端发数据,服务端接收

3)服务端发数据,客户端接收

双方可以反复发送,持续监听。

4.总结

TCP:面向连接、可靠、有序、慢

UDP:无连接、不可靠、无序、快

想继续了解TCP和UDP可以参考【一文了解】TCP协议和UDP协议

好了,本次的分享到这里就结束啦,希望对你有所帮助~

相关推荐
Godspeed Zhao2 小时前
现代智能汽车中的无线技术33——V2X(5)
网络·汽车
Ankie Wan2 小时前
SOME/IP: Scalable service-Oriented MiddlewarE over IP车载以太网的服务化通信协议
网络协议·tcp/ip·ecu·can总线·some/ip·autostar
军哥系统集成号2 小时前
PIA协同落地指南:打破六类评估壁垒,实现合规证据复用(合规协同系列第四篇)
网络·安全·web安全
teunyu2 小时前
在Unity中使用LineRenderer实现A点到B点的贝塞尔曲线。并且曲线为虚线。方向为A点流向B点。效果图如下
unity·游戏引擎
Mar_mxs2 小时前
win11网线连接两台电脑传输文件(解决共享时不知道网络凭据的用户名和密码)
网络
海棠蚀omo2 小时前
从初识到深入:一次完整的 HTTP 协议系统性理解之旅
网络·网络协议·http
Black蜡笔小新2 小时前
国标设备如何在EasyCVR视频汇聚平台获取RTSP/RTMP流?
网络·ffmpeg·音视频
nnsix2 小时前
Unity URP用于 光照贴图(Lightmap)的材质Shader
unity·材质·贴图