Socket 协议详解
第二关: Socket
Modbus 诞生于自动化工厂,而 Socket 诞生于顶尖大学的实验室。
Socket 不是一种协议,它是一个“插座”或者说“API 接口”。
在 1970 年之前,计算机是非常昂贵且巨大的。它们像一个个孤岛,互不往来。
如果你想把 A 电脑的数据给 B 电脑,你得把数据录在磁带上,抱着磁带跑到 B 电脑那里插上去。
TCP/IP 协议 在 70 年代初被 Vint Cerf 和 Bob Kahn 发明出来。电脑之间可以相互传输数据了。
但是使用起来比较麻烦,需要在代码实现三次握手、滑动窗口、拥塞控制等极其复杂的逻辑。
1983年加州大学伯克利分校的一帮天才,当时正在魔改 Unix 操作系统。他们把 TCP/IP 协议植入到 Unix 内核里,并给程序员提供一套编程接口。
为了方便程序员理解,他们把这套接口命名为插座(Socket)。
意思是,你要用电,不需要电厂怎么发电,怎么传输。你只要把插头插进插座,就有电了。
同样,你要发数据,不需要TCP 握手怎么握,路由怎么跳,你只要使用 Socket就行了。
1983年,4.2BSD Unix 发布,Socket API 正式问世。
它确立了网络编程的 5 大金刚 步骤,至今 40 年未变:
socket():买个电话机。bind():插上电话线(绑定端口)。listen():坐在电话旁等。connect():拨号。accept():接听。
接下来讲讲 Socket “入侵”工业界的故事。
这个时间段,工厂内只有串口, RS-232 和 RS-485。
问题是,串口速度太慢了。而且不同厂商还有不同厂商的线,互不兼容。距离也短。
刚好,互联网开始普及,网线网卡降价了,速度还快。赶紧用上。
于是,网口进了工厂,Socket 也就跟着进来了。
这个阶段就是,Modbus TCP 。
虽然用上网口网线,但是工业界也不想重新开发一套复杂的协议,于是干了一件简单粗暴的事:打包(Wrapping)。把原本 Modbus RTU 去尾换头,然后底层使用 Socket 进行传输。
网线的速度可比串口快多了。能用网口,速度也快,符合这一阶段的需求。
随着工业 4.0 和 智能制造 的到来,大量非标、高吞吐量的设备进入工厂。老旧的 Modbus 协议扛不住了。
那不用 Modbus 用什么?工程师发现,Modbus TCP 简直是拿着金碗要饭。Socket 本来就是一辆载重无限的大卡车,Modbus 协议却非要规定:‘不管卡车多大,车厢里只能装寄存器那么大的小盒子’。这不是浪费运力吗? 所以工程师把 Modbus 这个紧箍咒扔了!这辆卡车想装图片就装图片,想装字符串就装字符串,彻底放飞自我,这就是 Raw Socket。
来看下面三种种场景:
工业相机 (机器视觉)。拍照,一张图片 5MB,一秒钟拍 10 张。Modbus 根本传不了图片!必须用原生的 TCP Socket,建立连接后,像倒水一样把二进制图片数据“流”过来。
扫码枪与 RFID。自动扫码。很多扫码枪很简单,它不想支持复杂的 Modbus。它就做一个 TCP Server,你连上它,它扫到码就直接发一串字符串 "CODE:123456" 给你。这叫 ASCII 流。
非标设备与 PC 互联。 上位机要和另一台机器(比如测试仪、贴标机)对话,两台电脑之间通讯,最简单的就是双方约定一个简单的字符串格式(比如 "START,100,OK"),直接用 Socket 发过去。
Socket 的高性能,是现代工业设备间通讯的基础。未来的 IIoT / MQTT 底层协议依然是 Socket 。所以掌握好 Socket 是重点。
首先明确一下:
先有的 TCP 和 UDP(协议/规则),后有的 Socket(编程接口)。
互联网之父 Vint Cerf 和 Bob Kahn 最早设计的时候,并没有分 TCP 和 IP,也没有 UDP。 那时候只有一个超级复杂的协议,叫 TCP (Transmission Control Protocol)。它既要负责找路(现在的 IP 的活),又要负责保证不丢包(现在的 TCP 的活)。结果就是非常安全,也非常笨重。
随着网络开始尝试传输语音,科学家发现用 TCP 传语音太难受了。因为 TCP 发现丢了一个包,就会立刻喊停:“停!第 5 句没听清,重传!”,语音通话会一直卡顿、延迟。其实打电话时,丢一个字没关系,实时性才是最重要的。
科学家 David Reed 说:“我们需要一个裸奔版的传输协议。不需要重传,不需要排序,只管发!”
于是,UDP (User Datagram Protocol) 诞生了。
虽然TCP 和 UDP 的规则(RFC 文档)已经写好了,但程序员写代码还是很痛苦。于是,Socket 出现了。
Socket 的设计初衷: 就是为了给程序员提供一套统一的工具,随心所欲地选择**是用 TCP 还是用 UDP。
| 对比维度 | TCP (流式套接字) | UDP (数据报套接字) |
|---|---|---|
| C# 关键字 | SocketType.Stream | SocketType.Dgram |
| 核心价值观 | 稳 (不丢包、不乱序) | 快 (低延迟、高吞吐) |
| 连接方式 | 打电话 📞 (必须接通才能说话) | 村口大喇叭 📢 (喊完就算) |
| 数据边界 | 无边界 (像水流,有粘包问题) | 有边界 (像包裹,一个个独立) |
| 工业领地 | 统治级 (占 90%) | 特种兵 (占 10%) |
明白了 TCP 和 UDP 的区别,工业界也是按照需求来选择的这两者的。
把工业界的设备分成三类,它们的选择逻辑如下:
第一类:控制与核心数据(90% 的场景)
PLC、机械手、温控器、变频器。
我发指令“启动电机”,电机必须启动。(不能丢)
我发“先向左,再向下”,顺序绝对不能反。(不能乱)
选择:TCP 。
所以 Modbus TCP,西门子 S7 通讯,三菱 MC 通讯,全是基于 TCP 的。工业环境干扰大,数据很容易在半路丢包。只有 TCP 能保证“只要我不报错,数据就是对的”。
第二类:高速采集与监控(5% 的场景)
振动传感器(高频采样)、工业摄像头(视频流)、雷达。
一秒钟传 50 张图片,或者 10000 个振动波形数据。如果为了纠结那丢失的一个像素点,导致画面卡顿了 3 秒,反而更危险。
**选择: UDP **
第三类:设备发现与广播(5% 的场景)
上位机软件刚打开,想自动搜索局域网里有几台 PLC。不知道 PLC 的 IP 是多少,也没法建立 TCP 连接。
选择: UDP
就像在广场大喊,只有 UDP 支持广播,TCP 只能一对一。
C# 提供了两层操作方式:
System.Net.Sockets.Socket):
bind/listen/connect 一模一样。TcpClient / TcpListener):
new TcpClient(), Connect(), GetStream()。
// 引入必要的命名空间
using System;
using System.Net; // 包含 IPAddress, IPEndPoint
using System.Net.Sockets; // 包含 Socket 类
using System.Text; // 包含 Encoding (处理字符串)
class CameraServer
{
static void Main()
{
// ========================================================================
// 步骤 1:装一部电话 (Socket)
// ========================================================================
// AddressFamily.InterNetwork = IPv4 地址
// SocketType.Stream = 流式 (TCP)
// ProtocolType.Tcp = TCP 协议
Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
Console.WriteLine("1. 电话(Socket)已安装。");
// ========================================================================
// 步骤 2:申请一个电话号码 (Bind)
// ========================================================================
// IPAddress.Any 表示监听本机所有网卡 (无论你是插网线还是连Wi-Fi)
// 8080 是端口号,就像电话分机号
IPEndPoint point = new IPEndPoint(IPAddress.Any, 8080);
// 绑定!告诉操作系统:8080 端口归我管了
serverSocket.Bind(point);
Console.WriteLine("2. 号码已申请 (绑定端口 8080)。");
// ========================================================================
// 步骤 3:接上电源,等待铃声响 (Listen)
// ========================================================================
// 10 代表“挂起连接队列”的长度。如果瞬间来了 100 个人,只能排队 10 个,剩下的占线。
serverSocket.Listen(10);
Console.WriteLine("3. [电话] 已接通电源,正在等待铃声响 (监听中)...");
// ========================================================================
// 步骤 4:铃声响了,拿起听筒 (Accept) -> ⚠️此处会卡住!
// ========================================================================
// 程序运行到这里会“停住”(阻塞),直到有客户端(上位机)连上来。
// 一旦连上,它会返回一个新的 Socket (connection),专门用来和这个客户端说话。
Socket connection = serverSocket.Accept();
Console.WriteLine($"4. [电话] 电话接通了!对方IP:{connection.RemoteEndPoint}");
// ========================================================================
// 步骤 5:听对方说话 (Recv)
// ========================================================================
// 准备一个“空杯子” (字节数组) 来接水 (数据)
byte[] buffer = new byte[1024];
// Receive 也会卡住,直到对方发了数据过来
// length 是实际接到了多少水 (字节数)
int length = connection.Receive(buffer);
// 把字节翻译成字符串
string msg = Encoding.UTF8.GetString(buffer, 0, length);
Console.WriteLine($"5. [电话] 听到对方说:{msg}");
// ========================================================================
// 步骤 6:回答对方 (Send)
// ========================================================================
// 模拟发送一张图片数据 (这里用文字代替)
string reply = "Image_Data: [0xFF, 0xA2, ...]";
byte[] data = Encoding.UTF8.GetBytes(reply); // 把话变成字节
connection.Send(data);
Console.WriteLine("6. [电话] 图片数据已发送。");
// 挂断
connection.Close();
serverSocket.Close();
Console.ReadKey();
}
}
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
class PCClient
{
static void Main()
{
// ========================================================================
// 步骤 1:掏出电话 (Socket)
// ========================================================================
// 参数必须和服务器一模一样
Socket clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
Console.WriteLine("1. [上位机] 电话(Socket)已准备好。");
try
{
// ========================================================================
// 步骤 2:拨号 (Connect)
// ========================================================================
// 本机,IP填 127.0.0.1,端口填电话的 8080
Console.WriteLine("2. [上位机] 正在拨号...");
clientSocket.Connect("127.0.0.1", 8080);
Console.WriteLine(" [上位机] 接通了!");
// ========================================================================
// 步骤 3:喊话 (Send)
// ========================================================================
string cmd = "把照片发给我!"; // 你的指令
byte[] cmdBytes = Encoding.UTF8.GetBytes(cmd); // 必须转成字节才能顺着网线发
clientSocket.Send(cmdBytes);
Console.WriteLine($"3. [上位机] 指令已发送:{cmd}");
// ========================================================================
// 步骤 4:听到对方传来的图片数据 (Recv)
// ========================================================================
byte[] buffer = new byte[1024]; // 准备大桶接图片
int length = clientSocket.Receive(buffer); // 等待接收
string response = Encoding.UTF8.GetString(buffer, 0, length);
Console.WriteLine($"4. [电话] 收到相机回复:{response}");
// ========================================================================
// 步骤 5:挂断 (Close)
// ========================================================================
clientSocket.Close();
Console.WriteLine("5. [电话] 通话结束。");
}
catch (Exception ex)
{
Console.WriteLine($"连接失败:{ex.Message}");
Console.WriteLine("请检查:1.服务器开了吗? 2.IP端口对吗?");
}
Console.ReadKey();
}
}
服务端 (TcpListener):模拟一个智能仪表,收到指令后回复数据。
客户端 (TcpClient):你的上位机软件,发送指令并读取回复。
服务端不再用 Socket 类监听,而是用 TcpListener。
using System;
using System.IO; // 核心:用于 StreamReader/StreamWriter
using System.Net;
using System.Net.Sockets; // 核心:TcpListener, TcpClient
class SimpleServer
{
static void Main()
{
// 1. 设置监听端口 (8888)
// IPAddress.Any 代表本机所有网卡
TcpListener listener = new TcpListener(IPAddress.Any, 8888);
// 2. 开始营业 (相当于 Socket.Listen)
listener.Start();
Console.WriteLine("服务端:服务已启动,等待连接...");
// 3. 等待客户 (相当于 Socket.Accept)
// 注意:这里返回的直接是一个 TcpClient 对象,而不是 Socket
TcpClient client = listener.AcceptTcpClient();
Console.WriteLine("服务端:有人连上了!");
// =========================================================
// 🌟 核心高光时刻:获取网络流 (NetworkStream)
// =========================================================
// 我们不直接操作 Socket,而是拿到一个“流”。
// 把它想象成一个管子,数据像水一样在里面流。
NetworkStream stream = client.GetStream();
// 4. 给管子装上“读写器” (像读写文件一样操作网络!)
// StreamReader: 专门读字符串
// StreamWriter: 专门写字符串
StreamReader reader = new StreamReader(stream);
StreamWriter writer = new StreamWriter(stream);
// *重要*: 开启 AutoFlush,否则数据会卡在缓存区发不出去
writer.AutoFlush = true;
try
{
// 5. 读一行 (ReadLine 会一直卡住,直到对方发来换行符 \n)
string msg = reader.ReadLine();
Console.WriteLine($"服务端收到指令:{msg}");
// 6. 回一行
string reply = "温度: 25.6℃, 湿度: 60%";
writer.WriteLine(reply); // 自动加换行符
Console.WriteLine($"服务端已回复:{reply}");
}
catch (Exception ex)
{
Console.WriteLine("断开了:" + ex.Message);
}
// 7. 关门送客
client.Close();
listener.Stop();
Console.Read();
}
}
using System;
using System.IO;
using System.Net.Sockets; // 只需要这一个
class SimpleClient
{
static void Main()
{
try
{
// 1. 创建客户端 (自动挡,无需设置参数)
TcpClient client = new TcpClient();
// 2. 连接服务器
Console.WriteLine("客户端:正在连接...");
client.Connect("127.0.0.1", 8888);
Console.WriteLine("客户端:连接成功!");
// =========================================================
// 🌟 核心高光时刻:获取网络流
// =========================================================
NetworkStream stream = client.GetStream();
// 包装成读写器
StreamReader reader = new StreamReader(stream);
StreamWriter writer = new StreamWriter(stream);
writer.AutoFlush = true; // 必加!只要用 StreamWriter 就要加
// 3. 发送指令 (WriteLine 自动带回车换行)
// 很多工业仪表要求指令必须以 \r\n 结尾,WriteLine 完美解决
string cmd = "GET_TEMP";
writer.WriteLine(cmd);
Console.WriteLine($"客户端发送:{cmd}");
// 4. 接收回复 (ReadLine)
// 它会等待直到收到回车换行符
string response = reader.ReadLine();
Console.WriteLine($"客户端收到:{response}");
// 5. 关闭
client.Close();
}
catch (Exception ex)
{
Console.WriteLine("连接异常:" + ex.Message);
}
Console.Read();
}
}
如果你学会了 Socket,你就拥有了**“重新发明任何协议”**的能力。