C#通讯(上位机)常用知识点

1、什么是三次握手?什么是四次挥手?

一句话速记

三次握手 =建立 TCP 连接 ;四次挥手 =断开 TCP 连接

一、三次握手(建连接)

  1. 第一次:客户端发 SYN → 问服务器:我能发消息不?

  2. 第二次:服务器回 SYN+ACK → 我收得到,我也能发,你能收到我吗?

  3. 第三次:客户端发 ACK → 我能收到你,连接就绪。

    作用:确认双方收发都正常,避免旧无效请求占用资源。

二、四次挥手(断连接)

TCP 是全双工,两边通道要分别关闭,所以要 4 步:

  1. 第一次:主动方发 FIN → 我不发数据了。

  2. 第二次:被动方回 ACK → 收到,你不发我知道了(我还能继续发数据给你)。

  3. 第三次:被动方发 FIN → 我也发完了,我也不发了。

  4. 第四次:主动方回 ACK → 收到,彻底断开。

简单对比:建连接合并了确认,3 步搞定;断连接两边各自收尾,必须拆成 4 步。

2、C# 与 PLC 通讯(完整入门 + 实战代码)

C# 和 PLC 通讯是工业自动化最常用的开发场景,主流用 2 种协议

  1. Modbus(最通用、几乎所有 PLC 都支持)

  2. S7 Protocol(西门子专用,速度最快)

我直接给你可复制运行的完整代码,不用你自己拼。


一、最推荐:C# 使用 Modbus 与 PLC 通讯

1. 安装库(NuGet)

复制代码
Install-Package NModbus

或在 Visual Studio 里搜索安装 NModbus

2. C# 读写 PLC 完整代码(串口 / TCP 都支持)

TCP 方式(最常用)
复制代码
using System;
using System.Net.Sockets;
using Modbus.Device;
​
class ModbusPLC
{
    static void Main(string[] args)
    {
        try
        {
            // 1. 连接 PLC 的 IP 和 端口(Modbus TCP 默认 502)
            TcpClient tcp = new TcpClient("192.168.2.1", 502);
            ModbusIpMaster master = ModbusIpMaster.CreateIp(tcp);
​
            // 2. 读取保持寄存器(PLC 最常用:D区 / 保持寄存器)
            // 参数:从站号、起始地址、读取长度
            ushort[] readData = master.ReadHoldingRegisters(1, 0, 10);
            Console.WriteLine("读取成功:" + readData[0]);
​
            // 3. 写入单个保持寄存器
            master.WriteSingleRegister(1, 0, 666);
            Console.WriteLine("写入成功");
​
            tcp.Close();
        }
        catch (Exception ex)
        {
            Console.WriteLine("错误:" + ex.Message);
        }
    }
}
串口 RS485 / RS232 方式
复制代码
using System.IO.Ports;
​
SerialPort port = new SerialPort("COM3", 9600, Parity.None, 8, StopBits.One);
port.Open();
ModbusSerialMaster master = ModbusSerialMaster.CreateRtu(port);
​
// 读写同上

二、西门子 S7 PLC 专用通讯(S7.NET

如果你的 PLC 是 西门子 S7-200/300/400/1200/1500,用这个库最快。

1. 安装库

复制代码
Install-Package S7.NET

2. 完整代码

复制代码
using System;
using S7.Net;
​
class SiemensPLC
{
    static void Main(string[] args)
    {
        // 选择 PLC 类型 + IP
        Plc plc = new Plc(CpuType.S71200, "192.168.0.1, 0, 1");
​
        // 打开连接
        plc.Open();
​
        // 读取 DB 块数据(最常用)
        // 参数:DB块号, 起始地址, 数据类型
        int value = plc.ReadInt("DB1.DBW0");
        Console.WriteLine("读取 DB1.DBW0 = " + value);
​
        // 写入数据
        plc.Write("DB1.DBW0", 1234);
​
        // 关闭
        plc.Close();
    }
}

三、你必须知道的关键知识点

1. 常用地址对应

  • Modbus 保持寄存器 → 对应 PLC D 区 / VW / RW

  • Modbus 线圈 → 对应 PLC Y / Q 输出

  • Modbus 输入寄存器 → 对应 PLC AI 模拟量

2. 通讯失败常见原因

  1. PLC IP 和电脑不在同一网段

  2. 端口未开放(Modbus 502,西门子 102)

  3. 从站号(Slave ID)错误

  4. 地址格式写错

3. 工业开发最佳实践

  • 异步读写 不卡顿界面

  • 重连机制

  • 心跳包 检测连接状态


四、我可以直接帮你

你告诉我 3 个信息,我就能给你完全匹配你设备的可运行代码

  1. PLC 品牌型号(西门子 / 三菱 / 欧姆龙 / 台达 / 信捷...)

  2. 通讯方式(网口 TCP / 串口 RS485)

  3. 要读写的地址(如 D0、DB1.DBW0、M0.0)

总结

  1. Modbus(NModbus) → 通用所有 PLC

  2. S7.NET → 西门子专用,速度最快

  3. 代码都是直接复制可运行的工业级标准写法

3、线程、线程池、Task

Thread 是原生底层线程,ThreadPool 是复用线程池省资源,Task 是高级封装(基于池 + 异步回调),现代 C# 优先用 Task/async await。

1. Thread 原生线程

最底层、最麻烦的手动线程。

  • 特点:
  1. 手动创建销毁,开销大

  2. 无法返回结果、异常难捕获

  3. 生命周期全自己管

  • 简单示例
复制代码
Thread t = new Thread(()=>{
    Console.WriteLine("原生线程执行");
});
t.Start();

缺点:创建多了会卡、难管理,项目里基本不用。

2. ThreadPool 线程池

提前建好一批线程,任务来了直接复用。

  • 特点:
  1. 自动复用线程,减少创建开销

  2. 不能精细控制、没法获取返回值

  3. 后台线程,程序退出直接被杀

  • 示例
复制代码
ThreadPool.QueueUserWorkItem(state=>{
    Console.WriteLine("线程池任务");
});

场景:老项目遗留用,新项目不推荐。

3. Task & async/await(现在主力)

基于线程池封装,功能最强、最优雅。

  • 特点:
  1. 默认用线程池,性能好

  2. 支持返回值、异常捕获、链式延续

  3. 完美配合 async/await,UI 不卡顿

  • 基础示例
复制代码
// 无返回
Task.Run(()=>{
    Console.WriteLine("Task执行");
});
​
// 有返回
async Task<int> GetNumAsync()
{
    return await Task.Run(()=> 100);
}

三者对比速记

  1. Thread:重型、手动控、淘汰不用

  2. ThreadPool:复用线程、功能简陋、老代码用

  3. Task:轻量强大、支持异步链式、工业上位机 / WPF 首选

上位机开发小建议

UI 界面永远别用 Thread 死循环;耗时读写 PLC / 相机全用async Task+await,界面永不卡死。

4、C# 工业上位机通用模板:Task 异步 + 取消令牌 + PLC 通信 + 异常处理

一句话总结

整套模板用Task+async/await+CancellationToken,安全异步读写 PLC,UI 永不卡死、随时取消、异常全覆盖,直接可落地 WPF/WinForm 项目。

1. 核心工具类(通用 PLC 异步操作封装)

复制代码
using System;
using System.Threading;
using System.Threading.Tasks;
using Modbus.Device;
using System.Net.Sockets;

public class PlcAsyncHelper
{
    private TcpClient _tcpClient;
    private ModbusIpMaster _modbusMaster;
    // 连接状态
    public bool IsConnected => _tcpClient?.Connected ?? false;

    #region 异步连接PLC
    public async Task<bool> ConnectAsync(string ip, int port = 502, CancellationToken ct = default)
    {
        try
        {
            _tcpClient = new TcpClient();
            // 异步连接,支持取消
            await _tcpClient.ConnectAsync(ip, port, ct);
            _modbusMaster = ModbusIpMaster.CreateIp(_tcpClient);
            return true;
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("PLC连接已取消");
            return false;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"连接异常:{ex.Message}");
            Disconnect();
            return false;
        }
    }
    #endregion

    #region 异步读取寄存器
    public async Task<ushort[]> ReadRegistersAsync(byte slaveId, ushort startAddr, ushort length, CancellationToken ct = default)
    {
        if (!IsConnected) throw new InvalidOperationException("PLC未连接");
        // 耗时操作包装Task,防止阻塞
        return await Task.Run(() =>
        {
            ct.ThrowIfCancellationRequested();
            return _modbusMaster.ReadHoldingRegisters(slaveId, startAddr, length);
        }, ct);
    }
    #endregion

    #region 异步写入寄存器
    public async Task WriteRegisterAsync(byte slaveId, ushort addr, ushort value, CancellationToken ct = default)
    {
        if (!IsConnected) throw new InvalidOperationException("PLC未连接");
        await Task.Run(() =>
        {
            ct.ThrowIfCancellationRequested();
            _modbusMaster.WriteSingleRegister(slaveId, addr, value);
        }, ct);
    }
    #endregion

    #region 断开释放
    public void Disconnect()
    {
        _tcpClient?.Close();
        _tcpClient?.Dispose();
        _modbusMaster = null;
    }
    #endregion
}

2. UI 层调用示例(WinForm/WPF 通用逻辑)

复制代码
using System;
using System.Threading;
using System.Windows.Forms;

namespace PlcAsyncDemo
{
    public partial class MainForm : Form
    {
        private readonly PlcAsyncHelper _plcHelper = new PlcAsyncHelper();
        // 取消令牌源:控制所有异步任务停止
        private CancellationTokenSource _cts;

        public MainForm()
        {
            InitializeComponent();
        }

        // 连接按钮
        private async void btnConnect_Click(object sender, EventArgs e)
        {
            _cts = new CancellationTokenSource();
            bool res = await _plcHelper.ConnectAsync("192.168.1.10", 502, _cts.Token);
            lblStatus.Text = res ? "PLC连接成功" : "PLC连接失败";
        }

        // 异步读取PLC按钮
        private async void btnRead_Click(object sender, EventArgs e)
        {
            try
            {
                if (_cts == null) _cts = new CancellationTokenSource();
                // 读取从站1,起始地址0,读取2个寄存器
                var data = await _plcHelper.ReadRegistersAsync(1, 0, 2, _cts.Token);
                txtResult.Text = $"读取值:{data[0]} , {data[1]}";
            }
            catch (OperationCanceledException)
            {
                txtResult.Text = "读取操作已取消";
            }
            catch (Exception ex)
            {
                txtResult.Text = $"读取报错:{ex.Message}";
            }
        }

        // 停止所有任务按钮
        private void btnStop_Click(object sender, EventArgs e)
        {
            // 触发取消
            _cts?.Cancel();
            _cts?.Dispose();
            _cts = null;
        }

        // 窗口关闭释放资源
        protected override void OnFormClosing(FormClosingEventArgs e)
        {
            _cts?.Cancel();
            _cts?.Dispose();
            _plcHelper.Disconnect();
            base.OnFormClosing(e);
        }
    }
}

3. 关键知识点(适配上位机开发)

  1. CancellationToken

    随时终止异步读写,避免 PLC 卡死后台线程,关闭窗口必释放。

  2. Task.Run + async/await

    耗时 PLC 通信丢线程池,UI 主线程完全不阻塞,界面流畅。

  3. 异常分层捕获

    单独处理取消异常、连接异常、读写异常,工业现场稳定不崩溃。

  4. 资源自动释放

    断开、窗口关闭时清空连接和令牌,杜绝内存泄漏。

4. 扩展适配

  • 西门子 PLC:把 Modbus 逻辑替换成S7.NET异步方法,结构完全不变;

  • 循环轮询 PLC:加while(!ct.IsCancellationRequested)做后台周期读取。

5、C# 工业上位机终极模板:Task 异步 + 取消令牌 + PLC 轮询 + 断线自动重连

整套直接能用,适配 WPF/WinForm,后台常驻轮询、掉线自动重试、全程不卡 UI。

一、封装 PLC 核心帮助类(Modbus TCP)

复制代码
using System;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using Modbus.Device;

public class PlcPollHelper
{
    #region 基础变量
    private TcpClient _tcpClient;
    private ModbusIpMaster _master;

    public string Ip { get; set; }
    public int Port { get; set; } = 502;
    public byte SlaveId { get; set; } = 1;

    // 连接状态
    public bool IsConnected => _tcpClient?.Connected ?? false;

    // 轮询取消令牌
    private CancellationTokenSource _pollCts;
    private Task _pollTask;

    // 轮询间隔ms
    public int PollIntervalMs { get; set; } = 200;
    #endregion

    #region 连接&断开
    public async Task<bool> ConnectAsync(CancellationToken ct = default)
    {
        try
        {
            Disconnect();
            _tcpClient = new TcpClient();
            await _tcpClient.ConnectAsync(Ip, Port, ct);
            _master = ModbusIpMaster.CreateIp(_tcpClient);
            return true;
        }
        catch
        {
            Disconnect();
            return false;
        }
    }

    public void Disconnect()
    {
        StopPoll();
        _tcpClient?.Close();
        _tcpClient?.Dispose();
        _master = null;
        _tcpClient = null;
    }
    #endregion

    #region 读写寄存器
    public async Task<ushort[]> ReadRegAsync(ushort startAddr, ushort len, CancellationToken ct)
    {
        ct.ThrowIfCancellationRequested();
        if (!IsConnected) throw new Exception("PLC未连接");

        return await Task.Run(() =>
        {
            return _master.ReadHoldingRegisters(SlaveId, startAddr, len);
        }, ct);
    }

    public async Task WriteRegAsync(ushort addr, ushort val, CancellationToken ct)
    {
        ct.ThrowIfCancellationRequested();
        if (!IsConnected) throw new Exception("PLC未连接");

        await Task.Run(() =>
        {
            _master.WriteSingleRegister(SlaveId, addr, val);
        }, ct);
    }
    #endregion

    #region 后台自动轮询 + 断线重连
    // 开启轮询
    public void StartPoll(Func<ushort[], Task> onDataReceived, Action<string> onLog)
    {
        if (_pollCts != null) return;

        _pollCts = new CancellationTokenSource();
        var token = _pollCts.Token;

        _pollTask = Task.Run(async () =>
        {
            while (!token.IsCancellationRequested)
            {
                try
                {
                    // 没连接就自动重连
                    if (!IsConnected)
                    {
                        onLog?.Invoke("PLC离线,尝试重连...");
                        bool ok = await ConnectAsync(token);
                        if (ok)
                            onLog?.Invoke("PLC重连成功!");
                        else
                        {
                            await Task.Delay(1000, token); // 重连失败延时等待
                            continue;
                        }
                    }

                    // 正常读取:地址0,读取4个寄存器
                    var data = await ReadRegAsync(0, 4, token);
                    await onDataReceived?.Invoke(data);
                }
                catch (OperationCanceledException)
                {
                    break;
                }
                catch (Exception ex)
                {
                    onLog?.Invoke($"轮询异常:{ex.Message},准备重连");
                    Disconnect();
                    await Task.Delay(1000, token);
                }

                // 轮询间隔
                await Task.Delay(PollIntervalMs, token);
            }
        }, token);
    }

    // 停止轮询
    public void StopPoll()
    {
        _pollCts?.Cancel();
        _pollCts?.Dispose();
        _pollCts = null;
    }
    #endregion
}

二、UI 调用示例(WinForm 通用)

复制代码
using System;
using System.Windows.Forms;
using System.Threading.Tasks;

namespace PlcFullDemo
{
    public partial class MainForm : Form
    {
        private readonly PlcPollHelper _plc = new PlcPollHelper();

        public MainForm()
        {
            InitializeComponent();
            // 初始化PLC参数
            _plc.Ip = "192.168.1.10";
            _plc.Port = 502;
            _plc.SlaveId = 1;
            _plc.PollIntervalMs = 200;
        }

        // 启动轮询按钮
        private void btnStartPoll_Click(object sender, EventArgs e)
        {
            _plc.StartPoll(OnPlcDataUpdate, AddLog);
            AddLog("已启动后台轮询");
        }

        // 停止轮询按钮
        private void btnStopPoll_Click(object sender, EventArgs e)
        {
            _plc.Disconnect();
            AddLog("已停止轮询 & 断开PLC");
        }

        // PLC数据回调
        private async Task OnPlcDataUpdate(ushort[] data)
        {
            // 切回UI线程更新
            Invoke(new Action(() =>
            {
                txtData.Text = $"D0:{data[0]}  D1:{data[1]}  D2:{data[2]}  D3:{data[3]}";
            }));
        }

        // 日志打印
        private void AddLog(string msg)
        {
            Invoke(new Action(() =>
            {
                txtLog.AppendText($"{DateTime.Now:HH:mm:ss} {msg}\r\n");
            }));
        }

        // 窗口关闭释放资源
        protected override void OnFormClosing(FormClosingEventArgs e)
        {
            _plc.Disconnect();
            base.OnFormClosing(e);
        }

        // 写入PLC按钮
        private async void btnWrite_Click(object sender, EventArgs e)
        {
            try
            {
                await _plc.WriteRegAsync(10, 999, CancellationToken.None);
                AddLog("写入D10=999成功");
            }
            catch (Exception ex)
            {
                AddLog($"写入失败:{ex.Message}");
            }
        }
    }
}

三、核心亮点(工业项目刚需)

  1. 全程 async/Task:轮询跑后台,UI 永远不卡死

  2. 自动断线重连:网线拔掉、PLC 断电,恢复后自动连上

  3. CancellationToken 管控:停止、关闭窗口立刻释放任务,无残留线程

  4. 异常全局捕获:单个读写报错不崩整个程序

  5. UI 线程隔离:Invoke 回调更新界面,不会跨线程报错

四、西门子 S7 适配说明

只需替换读写底层:把 Modbus ReadHoldingRegisters 换成 S7.NET 的异步读写,轮询、重连、取消整套架构完全不用改

6、C# 上位机|S7.NET 完整版:异步 Task + 自动轮询 + 断线重连 + 取消令牌

直接替换就能用,架构和刚才 Modbus 完全一致,适配西门子 S7-1200/1500/300/400。

1. 先安装 NuGet

复制代码
Install-Package S7.NET

2. S7 专用 PLC 帮助类(核心封装)

复制代码
using System;
using System.Threading;
using System.Threading.Tasks;
using S7.Net;

public class S7PlcHelper
{
    #region 基础配置
    private Plc _s7Plc;

    // PLC参数
    public string IpAddress { get; set; }
    public CpuType CpuType { get; set; }
    public int Rack { get; set; }
    public int Slot { get; set; }

    // 连接状态
    public bool IsConnected => _s7Plc?.IsConnected ?? false;

    // 轮询控制
    private CancellationTokenSource _pollCts;
    private Task _pollTask;
    public int PollIntervalMs { get; set; } = 200;
    #endregion

    #region 连接 & 断开
    public async Task<bool> ConnectAsync(CancellationToken ct = default)
    {
        try
        {
            Disconnect();
            _s7Plc = new Plc(CpuType, IpAddress, Rack, Slot);
            
            // 异步连接包装
            await Task.Run(() =>
            {
                ct.ThrowIfCancellationRequested();
                _s7Plc.Open();
            }, ct);

            return _s7Plc.IsConnected;
        }
        catch
        {
            Disconnect();
            return false;
        }
    }

    public void Disconnect()
    {
        StopPoll();
        if (_s7Plc != null)
        {
            if (_s7Plc.IsConnected) _s7Plc.Close();
            _s7Plc.Dispose();
            _s7Plc = null;
        }
    }
    #endregion

    #region 常用读写封装
    // 读DB块 Int16
    public async Task<short> ReadDbIntAsync(int dbNum, int startOffset, CancellationToken ct)
    {
        ct.ThrowIfCancellationRequested();
        if (!IsConnected) throw new Exception("PLC未连接");

        return await Task.Run(() =>
        {
            return _s7Plc.ReadInt($"DB{dbNum}.DBW{startOffset}");
        }, ct);
    }

    // 写DB块 Int16
    public async Task WriteDbIntAsync(int dbNum, int startOffset, short value, CancellationToken ct)
    {
        ct.ThrowIfCancellationRequested();
        if (!IsConnected) throw new Exception("PLC未连接");

        await Task.Run(() =>
        {
            _s7Plc.Write($"DB{dbNum}.DBW{startOffset}", value);
        }, ct);
    }

    // 批量读取DB多个Int
    public async Task<short[]> ReadDbIntArrayAsync(int dbNum, int startOffset, int count, CancellationToken ct)
    {
        ct.ThrowIfCancellationRequested();
        if (!IsConnected) throw new Exception("PLC未连接");

        return await Task.Run(() =>
        {
            var res = new short[count];
            for (int i = 0; i < count; i++)
            {
                res[i] = _s7Plc.ReadInt($"DB{dbNum}.DBW{startOffset + i * 2}");
            }
            return res;
        }, ct);
    }
    #endregion

    #region 后台自动轮询 + 断线重连
    public void StartPoll(Func<short[], Task> dataCallback, Action<string> logCallback)
    {
        if (_pollCts != null) return;

        _pollCts = new CancellationTokenSource();
        var token = _pollCts.Token;

        _pollTask = Task.Run(async () =>
        {
            while (!token.IsCancellationRequested)
            {
                try
                {
                    // 断线自动重连
                    if (!IsConnected)
                    {
                        logCallback?.Invoke("西门子PLC离线,尝试重连...");
                        bool ok = await ConnectAsync(token);
                        if (ok)
                            logCallback?.Invoke("西门子PLC重连成功!");
                        else
                        {
                            await Task.Delay(1000, token);
                            continue;
                        }
                    }

                    // 读取DB1 从0开始4个Int
                    var data = await ReadDbIntArrayAsync(1, 0, 4, token);
                    await dataCallback?.Invoke(data);
                }
                catch (OperationCanceledException)
                {
                    break;
                }
                catch (Exception ex)
                {
                    logCallback?.Invoke($"轮询异常:{ex.Message},准备重连");
                    Disconnect();
                    await Task.Delay(1000, token);
                }

                await Task.Delay(PollIntervalMs, token);
            }
        }, token);
    }

    public void StopPoll()
    {
        _pollCts?.Cancel();
        _pollCts?.Dispose();
        _pollCts = null;
    }
    #endregion
}

3. WinForm UI 调用示例

复制代码
using System;
using System.Windows.Forms;

namespace S7PlcDemo
{
    public partial class MainForm : Form
    {
        private readonly S7PlcHelper _plc = new S7PlcHelper();

        public MainForm()
        {
            InitializeComponent();

            // 初始化S7参数
            _plc.IpAddress = "192.168.0.1";
            _plc.CpuType = CpuType.S71200;
            _plc.Rack = 0;
            _plc.Slot = 1;
            _plc.PollIntervalMs = 200;
        }

        private void btnStartPoll_Click(object sender, EventArgs e)
        {
            _plc.StartPoll(OnDataUpdate, AddLog);
            AddLog("已启动S7后台轮询");
        }

        private void btnStopPoll_Click(object sender, EventArgs e)
        {
            _plc.Disconnect();
            AddLog("停止轮询并断开PLC");
        }

        // 数据回调更新UI
        private async Task OnDataUpdate(short[] data)
        {
            Invoke(new Action(() =>
            {
                txtShow.Text = $"DB1.DBW0:{data[0]} DBW2:{data[1]} DBW4:{data[2]} DBW6:{data[3]}";
            }));
        }

        // 日志输出
        private void AddLog(string msg)
        {
            Invoke(new Action(() =>
            {
                txtLog.AppendText($"{DateTime.Now:HH:mm:ss} {msg}\r\n");
            }));
        }

        // 写入测试按钮
        private async void btnWrite_Click(object sender, EventArgs e)
        {
            try
            {
                await _plc.WriteDbIntAsync(1, 0, 1234, CancellationToken.None);
                AddLog("写入 DB1.DBW0 = 1234 成功");
            }
            catch (Exception ex)
            {
                AddLog($"写入失败:{ex.Message}");
            }
        }

        // 窗口关闭释放资源
        protected override void OnFormClosing(FormClosingEventArgs e)
        {
            _plc.Disconnect();
            base.OnFormClosing(e);
        }
    }
}

4. 快速配置说明

  1. S7-1200/1500:CpuType.S71200,Rack=0,Slot=1

  2. S7-300/400:对应修改 CpuType 即可

  3. PLC 需放开:PUT/GET 访问权限,取消 DB 块优化

5. 核心优势

  • 全程Task+async,UI 绝不卡顿

  • 掉线自动重试重连,工业现场稳定

  • CancellationToken 统一管理退出,无内存泄漏

  • 直接可用,不用再改架构

7、多线程中经常访问同一个资源可能造成什么问题

多线程同时抢同一个共享资源,核心问题就是数据错乱、逻辑崩坏、程序死锁崩溃

1. 最常见:竞态条件(数据脏读 / 脏写)

多个线程同时读写同一个变量,计算被覆盖、结果不对。

举个极简例子:

复制代码
private static int count = 0;
// 两个线程同时执行 count++
count++;

count++实际分三步:读→加→写。

线程互相插队,最后总数变少,数据不一致

2. 线程安全异常(集合报错)

List、Dictionary 这类非线程安全集合:

多线程同时增删改,直接抛异常、数据丢包甚至内存损坏。

3. 死锁(程序卡死不动)

多个线程互相拿着对方需要的锁:

A 锁 1 等锁 2,B 锁 2 等锁 1 → 永久卡死,界面僵死

4. 可见性问题(缓存没刷新)

一个线程改了变量,别的线程看不到最新值,一直用旧数据,逻辑卡死不走。

5. 重入异常 & 资源泄漏

连接、句柄被多线程重复释放 / 重复创建,直接崩程序、内存泄漏。


C# 上位机快速解决办法(你项目直接用)

  1. 简单变量:用 lock(obj) 加互斥锁

  2. 原子计数:用 Interlocked.Increment

  3. 集合:用 ConcurrentDictionary 线程安全集合

  4. 异步 PLC/IO:用 SemaphoreSlim 限流排队,防止并发抢通讯口

8、简单说明线程和进程之间的关系

进程是资源独立大房子,线程是屋里干活的人;一个进程至少有 1 个主线程,线程共享进程资源。

简单拆解

  1. 进程

    操作系统资源分配最小单位,相互独立,崩溃互不影响。

  2. 线程

    进程里的执行单元,调度 CPU 的最小单位;同进程线程共享内存、变量、文件句柄。

核心关系

  • 1 进程 ≥ 1 线程

  • 进程隔离,线程共享

  • 进程开销大;线程轻量、切换快

接地气比喻

电脑软件 =房子 (进程)

代码执行流 =住户线程

房子各自独立;同住一间房的人共用家具家电。

9、单线程和多线程

单线程串行干活、简单不冲突;多线程并行干活、效率高但要控共享资源。

1. 单线程

同一时间只干一件事,从头到尾排队执行。

  • 优点:代码简单、无资源冲突、不用加锁、不易 bug

  • 缺点:一个耗时操作卡住,整个程序卡死

  • 场景:简单小工具、UI 主线程

2. 多线程

同时开多条执行流,几件事一起跑

  • 优点:耗时任务后台跑,UI 不卡;多核 CPU 利用率高

  • 缺点:共享变量会乱序、要加锁防冲突;容易死锁、逻辑复杂

  • 场景:上位机轮询 PLC、读写相机、后台日志保存

3. 通俗对比

  • 单线程:一个人做饭,洗菜→切菜→炒菜排队来,卡一步全停。

  • 多线程:多人分工,一人洗菜、一人炒菜,效率高但抢工具要排队谦让。

上位机口诀

UI 界面用单线程不动后台;

通讯采集用多线程 Task放后台;

共享数据加锁,绝不裸奔读写。

1. 单线程版本(UI 直接卡死)

复制代码
// 按钮点击事件,UI主线程执行
private void btnSingleThread_Click(object sender, EventArgs e)
{
    txtResult.Text = "开始执行...";
    
    // 耗时循环,霸占主线程
    for (int i = 0; i < 500000000; i++)
    {
        
    }

    txtResult.Text = "执行结束!";
}

现象:点击后窗口拖不动、按钮点不了、直接白屏卡死,等循环跑完才恢复。


2. 多线程 Task 版本(UI 完全不卡)

复制代码
private async void btnMultiThread_Click(object sender, EventArgs e)
{
    txtResult.Text = "后台开始执行...";

    // 耗时任务丢线程池后台跑
    await Task.Run(() =>
    {
        for (int i = 0; i < 500000000; i++)
        {

        }
    });

    txtResult.Text = "后台执行结束!";
}

现象:点击后界面流畅可操作,后台默默算,完事自动更新文本。


核心区别一句话

耗时操作写 UI 主线程 = 卡死;

耗时操作丢Task.Run后台 = 流畅不卡顿。

10、什么是线程锁

线程锁就是给共享资源装一把排队钥匙,同一时间只允许一个线程进去操作,防止数据乱掉。

1. 为什么需要锁

多线程同时读写同一个变量:

  • 你改一半,我又来改

  • 最后数据错乱、结果错误

2. 线程锁最常用:lock

复制代码
// 定义锁对象
private readonly object _lockObj = new object();
private int count = 0;

void SafeAdd()
{
    lock (_lockObj)
    {
        // 这一段代码同一时间只能一个线程进来
        count++;
    }
}

3. 通俗理解

  • lock里面的代码 =单人卫生间

  • 线程进去关门上锁,别人只能外面排队

  • 用完开门走人,下一个再进

4. 简单优缺点

✅ 优点:解决竞态问题,数据安全不乱

❌ 缺点:

  • 加太多会变慢、阻塞

  • 乱用多个锁容易死锁卡死

上位机小口诀

共享变量读写必加锁;

通讯串口 / PLC 读写用一把锁排队,避免并发报错。

11、怎么给线程加锁

lock 锁一个私有只读对象,把读写共享变量的代码包起来,同一时间只允许一个线程执行。

1. 标准写法(工业项目最常用)

第一步:定义锁对象(只写 1 次)

复制代码
// 专门用来上锁,不要改它
private readonly object _locker = new object();
private int _count = 0; // 共享资源

第二步:读写共享资源时加锁

复制代码
public void SafeAdd()
{
    // 进门上锁,出门自动解锁
    lock (_locker)
    {
        _count++;  // 这里绝对不会多线程乱抢
    }
}

2. 对比:不加锁会错,加锁就正常

❌ 不加锁(多线程跑,数值算错)

复制代码
void BadCode()
{
    _count++; // 多线程同时改,结果偏小
}

✅ 加锁安全版

复制代码
void GoodCode()
{
    lock(_locker)
    {
        _count++;
    }
}

3. 上位机通讯专用锁(重点!)

PLC / 串口同一时间只能一个任务读写,必须加锁排队:

复制代码
private readonly object _plcLock = new object();

public void ReadPlcData()
{
    lock(_plcLock)
    {
        // 任何PLC读写操作
        plc.ReadMemory(...);
    }
}

作用:防止多处同时读写 PLC,报错、断线、乱数据。

4. 两个必记规则

  1. 同一个共享资源,必须用同一把锁

  2. 锁里面代码尽量短,别写耗时延迟,否则卡顿死锁

12、线程的五个状态

线程 5 大状态:新建→就绪→运行→阻塞→终止,循环流转。

1. 五个状态通俗讲解

  1. 新建 New

    刚 new 出来线程对象,还没 Start,没进入系统调度。

  2. 就绪 Runnable

    已 Start,资源备好,等着 CPU 有空就立刻执行

  3. 运行 Running

    抢到 CPU 时间片,代码正在全速跑。

  4. 阻塞 Blocked/Waiting

    被卡住暂停:

  • Sleep休眠

  • lock抢不到锁

  • 等待 IO/PLC 通信

    CPU 不再分配时间片,唤醒后回到

    就绪

5.终止 Terminated

代码跑完 / 异常结束,线程彻底销毁,不能重启。

流转口诀

新建就绪抢 CPU,运行遇阻变阻塞;

唤醒重回就绪队,跑完进入终止态。

C# 对应场景速记

  • New:Thread t = new Thread(...)

  • Runnable:t.Start()

  • Running:线程正在执行循环 / 业务

  • Blocked:Thread.Sleep、等待 lock、await 延时

  • Terminated:方法执行完毕

线程五状态流转极简图(超好记)

复制代码
【新建 New】
   ↓ Start()
【就绪 Runnable】 ←──────┐
   ↓ 抢到CPU时间片        │唤醒/等待结束
【运行 Running】          │
   ↙        ↘
阻塞等待     代码执行完毕
【阻塞 Blocked】   【终止 Terminated】

快速流转口诀

  1. 新建线程 →调用 Start→ 进入就绪排队

  2. 就绪抢到 CPU →变成运行

  3. 运行时 Sleep / 等锁 / IO →切到阻塞

  4. 阻塞结束唤醒 →回到就绪

  5. 代码跑完 / 异常退出 →进入终止永久结束

C# 对应场景秒懂

  • New:new Thread(xxx) 只创建不启动

  • Runnable:调用.Start() 等待调度

  • Running:线程正在执行业务代码

  • Blocked:Sleep、抢 lock 失败、await 延迟

  • Terminated:方法执行结束,线程销毁

13、服务器端接收客户消息并输出代码

C# TCP 服务端:接收客户端消息并控制台输出(最简可直接运行)

1. 控制台 TCP 服务器代码

复制代码
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

class TcpServerDemo
{
    // 监听IP + 端口
    private const string ListenIp = "127.0.0.1";
    private const int Port = 8888;

    static async Task Main(string[] args)
    {
        // 创建监听器
        var ipEndPoint = new IPEndPoint(IPAddress.Parse(ListenIp), Port);
        TcpListener listener = new TcpListener(ipEndPoint);

        try
        {
            listener.Start();
            Console.WriteLine($"服务器已启动,监听 {ListenIp}:{Port}");

            // 循环等待客户端连接
            while (true)
            {
                // 异步等待客户端接入
                TcpClient client = await listener.AcceptTcpClientAsync();
                Console.WriteLine("新客户端已连接!");

                // 开启独立任务处理当前客户端消息(不阻塞新连接)
                _ = HandleClientAsync(client);
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine("服务器异常:" + ex.Message);
        }
    }

    // 处理单个客户端收发
    private static async Task HandleClientAsync(TcpClient client)
    {
        using (client)
        using (var stream = client.GetStream())
        {
            byte[] buffer = new byte[1024 * 4];
            while (true)
            {
                try
                {
                    // 读取客户端发来的数据
                    int readLen = await stream.ReadAsync(buffer, 0, buffer.Length);
                    if (readLen <= 0) break; // 客户端断开

                    // 转字符串并控制台输出
                    string msg = Encoding.UTF8.GetString(buffer, 0, readLen);
                    Console.WriteLine($"收到客户端消息:{msg}");
                }
                catch
                {
                    break;
                }
            }
            Console.WriteLine("客户端已断开连接");
        }
    }
}

2. 配套极简客户端(测试用)

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

class TcpClientDemo
{
    static void Main(string[] args)
    {
        TcpClient client = new TcpClient();
        client.Connect("127.0.0.1", 8888);
        var stream = client.GetStream();

        Console.WriteLine("请输入要发送的消息:");
        string input = Console.ReadLine();
        byte[] data = Encoding.UTF8.GetBytes(input);
        stream.Write(data, 0, data.Length);

        Console.WriteLine("发送完成!");
        Console.ReadKey();
    }
}

使用步骤

  1. 先运行服务器,开启监听

  2. 再运行客户端,输入文字发送

  3. 服务器控制台自动打印收到的消息

关键特点

  1. 异步Task架构,多客户端同时连接不卡顿

  2. 自动释放 Socket 资源,断开自动回收

  3. UTF8 编码通用,工业 / 普通通信都能用

一、完整版 TCP 异步服务端(不变,支持多客户端长连接)

复制代码
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

class TcpLongServer
{
    const int Port = 8888;
    static async Task Main()
    {
        var listener = new TcpListener(IPAddress.Any, Port);
        listener.Start();
        Console.WriteLine($"服务端启动,监听端口 {Port}\n");

        while (true)
        {
            var client = await listener.AcceptTcpClientAsync();
            Console.WriteLine("新客户端接入");
            // 后台独立处理长连接
            _ = HandleClientLoop(client);
        }
    }

    private static async Task HandleClientLoop(TcpClient client)
    {
        using (client)
        using (var stream = client.GetStream())
        {
            byte[] buffer = new byte[1024];
            while (true)
            {
                try
                {
                    int len = await stream.ReadAsync(buffer, 0, buffer.Length);
                    if (len <= 0) break;

                    // 收到消息直接打印
                    string msg = Encoding.UTF8.GetString(buffer, 0, len);
                    Console.WriteLine($"收到客户端:{msg}");
                }
                catch
                {
                    break;
                }
            }
        }
        Console.WriteLine("客户端断开连接\n");
    }
}

二、长连接客户端(可无限输入、一直发消息不断开)

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

class TcpLongClient
{
    static void Main()
    {
        try
        {
            TcpClient client = new TcpClient();
            Console.WriteLine("正在连接服务器...");
            client.Connect("127.0.0.1", 8888);
            Console.WriteLine("连接成功!随时输入消息发送,exit退出\n");

            var stream = client.GetStream();

            while (true)
            {
                Console.Write("请输入消息:");
                string input = Console.ReadLine();

                // 输入 exit 关闭客户端
                if (input.Equals("exit", StringComparison.OrdinalIgnoreCase))
                    break;

                byte[] data = Encoding.UTF8.GetBytes(input);
                stream.Write(data, 0, data.Length);
                Console.WriteLine("发送完成\n");
            }

            client.Close();
        }
        catch (Exception ex)
        {
            Console.WriteLine("连接异常:" + ex.Message);
        }
    }
}

使用步骤

  1. 先运行服务端控制台

  2. 再运行客户端控制台

  3. 客户端随便打字回车发送,服务端实时打印

  4. 输入 exit 关闭客户端

特点总结

  • 服务端:多客户端同时接入、异步长连接、自动回收

  • 客户端:长连接不掉线,循环输入无限发消息

TCP 双向通信完整版:服务端收消息 + 自动回复 + 客户端长连接连发

1. 服务端(多客户端异步 + 接收打印 + 自动回信)

复制代码
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

namespace TcpServerTwoWay
{
    class Program
    {
        private const int Port = 8888;
        static async Task Main(string[] args)
        {
            var listener = new TcpListener(IPAddress.Any, Port);
            listener.Start();
            Console.WriteLine($"双向通信服务端启动,监听端口 {Port}\n");

            // 循环等待多个客户端连接
            while (true)
            {
                var client = await listener.AcceptTcpClientAsync();
                Console.WriteLine("✅ 新客户端已连接");
                _ = HandleClientAsync(client);
            }
        }

        // 单独处理每个客户端:收消息+自动回复
        private static async Task HandleClientAsync(TcpClient client)
        {
            using (client)
            using (NetworkStream stream = client.GetStream())
            {
                byte[] buffer = new byte[1024];
                while (true)
                {
                    try
                    {
                        // 读取客户端消息
                        int readLen = await stream.ReadAsync(buffer, 0, buffer.Length);
                        if (readLen <= 0) break;

                        // 解码打印
                        string recMsg = Encoding.UTF8.GetString(buffer, 0, readLen);
                        Console.WriteLine($"📥 收到客户端:{recMsg}");

                        // 组装回复消息
                        string reply = $"服务端已收到:{recMsg}";
                        byte[] sendData = Encoding.UTF8.GetBytes(reply);
                        await stream.WriteAsync(sendData, 0, sendData.Length);
                        Console.WriteLine($"📤 已自动回复客户端\n");
                    }
                    catch
                    {
                        break;
                    }
                }
            }
            Console.WriteLine("❌ 客户端断开连接\n");
        }
    }
}

2. 客户端(长连接 + 循环发消息 + 接收服务端回信)

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

namespace TcpClientTwoWay
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                TcpClient client = new TcpClient();
                Console.WriteLine("正在连接服务端 127.0.0.1:8888...");
                client.Connect("127.0.0.1", 8888);
                Console.WriteLine("✅ 连接成功!输入消息发送,exit退出\n");

                NetworkStream stream = client.GetStream();
                byte[] buffer = new byte[1024];

                while (true)
                {
                    Console.Write("请输入发送内容:");
                    string input = Console.ReadLine();

                    // 退出指令
                    if (input.Equals("exit", StringComparison.OrdinalIgnoreCase))
                        break;

                    // 发送消息给服务端
                    byte[] sendBytes = Encoding.UTF8.GetBytes(input);
                    stream.Write(sendBytes, 0, sendBytes.Length);

                    // 等待并读取服务端回复
                    int len = stream.Read(buffer, 0, buffer.Length);
                    string recReply = Encoding.UTF8.GetString(buffer, 0, len);
                    Console.WriteLine($"💬 服务端回复:{recReply}\n");
                }

                client.Close();
                Console.WriteLine("连接已关闭");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"异常:{ex.Message}");
            }
        }
    }
}

运行流程

  1. 先启动服务端控制台监听端口

  2. 再启动客户端自动连上服务端

  3. 客户端输入文字发送

  4. 服务端打印消息并自动回信

  5. 客户端收到回信并显示

  6. 输入exit即可退出客户端

核心亮点

  • 支持多个客户端同时连接互不干扰

  • 全程长连接不断开,双向收发

  • 异常自动断开回收资源,稳定好用

    贴合你上位机网络通信的开发场景,直接复用即可。

14、什么是线程池

线程池就是系统提前建好一堆备用线程,任务来了直接拿现成的用,用完不销毁、放回池里复用,省资源、启动快。

1. 为什么不用手动 new Thread

  • 每次新建线程:开销大、慢、难管理

  • 线程太多:CPU 爆满、内存泄漏

2. 线程池核心特点

  1. 提前创建线程缓存,待命等待任务

  2. 自动复用线程:任务做完线程不删,等下一个任务

  3. 自动控数量:防止线程爆炸拖垮程序

  4. .NET 里 ThreadPool / Task 默认都跑在线程池

3. 通俗比喻

  • new Thread = 每次干活现招人,干完立刻辞退,费钱费力

  • 线程池 = 常备一支固定小队,来活就上岗,没活就待命,永远不裁员

4. 最简代码演示

复制代码
// 往线程池丢任务
ThreadPool.QueueUserWorkItem(state =>
{
    Console.WriteLine("我是线程池里跑的任务");
});

5. 和你之前知识关联

  • Thread:原生重型线程,少用

  • ThreadPool:底层池,老旧写法

  • Task/async await:现代封装,底层默认就是线程池,上位机优先用这个

Thread / 线程池 / Task 对比(面试极简背诵版)

一句话总览

Thread = 原生重型手动线程;ThreadPool = 简易复用池子;Task = 高级封装(基于线程池 + 异步全能)。

对比表格

对比项 Thread 原生线程 ThreadPool 线程池 Task (.NET 现代)
底层来源 操作系统新建线程 预先创建一批线程复用 默认基于线程池封装
开销 很大,频繁创建销毁卡性能 小,线程复用不销毁 最轻量,智能调度
控制难度 复杂:启停、挂起全手动 几乎不能精细控制 灵活:取消、等待、回调齐全
返回结果 不支持 不支持 完美支持返回值
异常处理 极易崩主程序 难捕获 自带友好异常捕获
取消功能 支持 CancellationToken 取消
适用场景 老旧遗留项目,几乎淘汰 老代码维护 新项目 / 上位机 / 工业开发首选

核心口诀

  1. Thread:重量级,自己造线程,开销大,别用。

  2. ThreadPool:池子复用线程,功能简陋,现在少用。

  3. Task:线程池升级版,异步强、能取消、能返回,开发全靠它。

和上位机开发关联

  • PLC 轮询、相机采集、网络通信:全部用 async + Task

  • 不用自己 new Thread,不用手写 ThreadPool

  • 自动复用线程池资源,界面不卡、稳定不崩

15、线程池的工作流程

线程池流程:预先建好线程待命 → 任务入队排队 → 空闲线程认领执行 → 用完线程回收待命,不销毁

完整工作流程(5 步背会)

  1. 初始化预热

    程序启动时,线程池提前创建一批空闲线程,静静待命。

  2. 接收任务排队

    新业务任务进来,不新建线程,先放进任务队列排队。

  3. 空闲线程调度

    池里有空闲线程 → 立刻取出任务执行;

    无空闲且没到上限 → 自动少量新增线程;

    线程已满负荷 → 任务排队等待。

  4. 执行任务

    线程拿到业务逻辑,独立运行代码。

  5. 执行完回收复用

    任务结束,线程不销毁,回归池子变回空闲,等待下一个任务复用。

极简流程图

初始化创建空闲线程

新任务 → 进入队列

空闲线程取出任务执行

任务结束 → 线程回归池子待命(循环复用)

补充关键点(面试必考)

  • 限制最大线程数,防止线程爆炸卡死 CPU

  • 短任务适合线程池;超长死循环任务不建议用

  • C# 里Task.Run底层自动走这套线程池流程,不用自己管理

16、IP类

C# IP 相关常用类 快速总结(面试 + 开发够用)

一、核心 3 个类

  1. IPAddress :封装单个 IP 地址(如 127.0.0.1

  2. IPEndPoint:IP + 端口 组合(网络通信必备)

  3. Dns:域名解析、获取本机 IP

二、用法极简示例

1. IPAddress 使用

复制代码
// 字符串转IP
IPAddress ip = IPAddress.Parse("192.168.1.10");

// 监听所有网卡
IPAddress anyIp = IPAddress.Any; 

2. IPEndPoint(通信最常用)

复制代码
// IP + 端口绑定
IPAddress ip = IPAddress.Parse("127.0.0.1");
IPEndPoint endPoint = new IPEndPoint(ip, 8888);

👉 TCP 服务器、PLC 连接、Socket 全靠它。

3. Dns 域名解析

复制代码
// 域名转IP
IPHostEntry host = Dns.GetHostEntry("www.baidu.com");
foreach(var item in host.AddressList)
{
    Console.WriteLine(item);
}

三、面试 / 开发常考点

  1. IPAddress.Any = 监听本机所有网卡 IP

  2. 127.0.0.1 = 本地回环,只能自己访问

  3. IPEndPoint 是 socket 通信必须的终结点

  4. 本机多网卡时,绑定指定 IP 更稳定

C# IP 工具类:校验 IP + 获取本机所有 IP(直接能用)

复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;

public static class IpHelper
{
    /// <summary>
    /// 判断字符串是否是合法IPv4
    /// </summary>
    public static bool IsValidIPv4(string ipStr)
    {
        if (string.IsNullOrWhiteSpace(ipStr)) 
            return false;

        return IPAddress.TryParse(ipStr, out var ip) 
               && ip.AddressFamily == AddressFamily.InterNetwork;
    }

    /// <summary>
    /// 获取本机所有可用IPv4地址(排除回环)
    /// </summary>
    public static List<string> GetLocalAllIPv4()
    {
        List<string> ipList = new List<string>();

        foreach (var ni in NetworkInterface.GetAllNetworkInterfaces())
        {
            // 跳过禁用/断开网卡
            if (ni.OperationalStatus != OperationalStatus.Up) continue;

            var addrs = ni.GetIPProperties().UnicastAddresses;
            foreach (var addr in addrs)
            {
                // 只取IPv4、排除127.0.0.1
                if (addr.Address.AddressFamily == AddressFamily.InterNetwork 
                    && !IPAddress.IsLoopback(addr.Address))
                {
                    ipList.Add(addr.Address.ToString());
                }
            }
        }
        return ipList.Distinct().ToList();
    }

    /// <summary>
    /// 域名解析成IP
    /// </summary>
    public static List<IPAddress> DnsResolve(string hostName)
    {
        try
        {
            var host = Dns.GetHostEntry(hostName);
            return host.AddressList
                       .Where(x => x.AddressFamily == AddressFamily.InterNetwork)
                       .ToList();
        }
        catch
        {
            return new List<IPAddress>();
        }
    }
}

调用示例

复制代码
// 1.校验IP
Console.WriteLine(IpHelper.IsValidIPv4("192.168.1.10")); // True
Console.WriteLine(IpHelper.IsValidIPv4("999.1.1.1"));     // False

// 2.获取本机所有网卡IP
var localIps = IpHelper.GetLocalAllIPv4();
foreach(var ip in localIps)
{
    Console.WriteLine("本机IP:" + ip);
}

// 3.域名解析
var dnsIps = IpHelper.DnsResolve("www.baidu.com");

高频常量速记

  • IPAddress.Any:监听本机所有网卡 IP(服务器常用)

  • IPAddress.Loopback:127.0.0.1 本地回环

  • AddressFamily.InterNetwork:代表 IPv4

17、什么是IP地址,有什么作用?

IP 地址就是网络里每一台设备的唯一门牌号,用来找到对方、互相通信。

1. 什么是 IP 地址

  • 全称:互联网协议地址

  • 格式(IPv4):四段数字,192.168.1.10

  • 每台电脑 / PLC / 相机 / 服务器,联网后都有专属 IP

2. 核心作用

  1. 定位设备

    局域网里靠 IP 区分:哪台是 PLC、哪台是上位机、哪台是相机。

  2. 路由转发数据

    数据按 IP 寻址,准确发到目标设备,不会发错。

  3. 通信连接必备

    TCP/PLC/ 网络通信,必须知道对方 IP + 端口才能连上。

3. 常见特殊 IP

  • 127.0.0.1:本机回环,自己连自己测试用

  • 192.168.x.x/10.x.x.x:内网私有 IP,工厂局域网常用

  • IPAddress.Any:监听本机所有网卡 IP

4. 通俗比喻

房子 = 网络设备

IP 地址 = 详细住址

没有地址,快递(网络数据)就送不到人手里。

18、什么是子网掩码,有什么作用?

子网掩码用来区分 IP 里哪部分是网段、哪部分是设备号,判断两台设备是不是在同一个局域网。

1. 简单理解

IPv4 是 IP地址 + 子网掩码 成对用。

常见掩码:255.255.255.0

2. 核心作用

  1. 划分网络位 & 主机位

    IP:192.168.1.10

    掩码:255.255.255.0

    前面一样的是局域网网段 ,后面变化的是设备编号

  2. 判断是否同网段(能不能直连通信)

    两台设备:

  • IP & 子网掩码 做运算 → 网络地址一样 = 同局域网,直接互通

  • 不一样 = 跨网段,要经过路由器转发

3. 通俗比喻

IP = 完整住址:小区 + 楼栋 + 房间号

子网掩码 = 划分规则:规定「小区楼栋」是公共网段,「房间号」是单独设备。

4. 工控常用口诀

工控 PLC / 上位机默认:

IP:192.168.1.X

掩码:255.255.255.0

只要前 3 段一样,就是同一局域网,直接通信没问题。

相关推荐
NGC_66112 小时前
Java线程池七大核心参数介绍
java·开发语言
crescent_悦2 小时前
C++:Highest Price in Supply Chain
开发语言·c++
float_com2 小时前
【java进阶】------ Lambda表达式
java·开发语言
码云数智-大飞2 小时前
Java接口与抽象类:从本质区别到架构选型
开发语言
小碗羊肉2 小时前
【从零开始学Java | 第二十三篇】泛型(Generics)
java·开发语言·新手入门
m0_750580303 小时前
Java并发—Java线程
java·开发语言
我不是懒洋洋3 小时前
预处理详解
c语言·开发语言·c++·windows·microsoft·青少年编程·visual studio
计算机安禾3 小时前
【数据结构与算法】第14篇:队列(一):循环队列(顺序存储
c语言·开发语言·数据结构·c++·算法·visual studio
weixin_649555673 小时前
C语言程序设计第四版(何钦铭、颜晖)第十一章指针进阶之奇数值结点链表
c语言·开发语言·链表