在 C# 上位机开发中,除了串口通信之外,Socket 网络通信也是非常重要的一部分。

例如:

上位机与 PLC 通信
上位机与机器人通信
上位机与视觉软件通信
客户端与服务器通信
多台电脑之间的数据通信
设备之间通过以太网通信

C# 中常见的网络通信方式主要有:

TCP
UDP

而 Socket 是进行网络通信的基础类之一。

一、什么是 Socket?

可以把 Socket 理解成:

程序进行网络通信的一个接口。

例如两台电脑:

电脑A 电脑B
客户端 服务器
│ │
│ 网络通信 │
└──────── Socket ─────────────┘

Socket 本身不是 TCP 或 UDP。

它更像是一个通信工具,可以使用:

TCP
UDP

等不同的通信方式。

二、IP地址和端口

学习 Socket,首先要理解两个东西:

IP地址
端口

例如:

192.168.1.100:60000

可以理解成:

192.168.1.100
↓
哪台电脑

60000
↓
哪个程序

所以:

IP 地址用于找到哪台计算机,端口用于找到这台计算机上的哪个网络程序。

例如:

服务器:
IP:192.168.1.100
Port:60000

客户端需要连接:

192.168.1.100
60000
三、TCP和UDP的区别

Socket 最常见的两种通信方式就是:

TCP
UDP

两者的区别非常重要。

特点 TCP UDP
是否建立连接 是 否
是否可靠 是 不保证
是否保证顺序 是 不保证
是否保证到达 是 不保证
是否有连接状态 有 无
速度 相对较慢 相对较快
数据形式 字节流 数据报
常见用途 聊天、文件、控制 广播、实时数据

简单理解:

TCP:
我先和你建立连接,再通信。

UDP:
我直接把数据扔给你。
四、TCP Socket通信

TCP通信通常采用:

服务器
↓
创建Socket
↓
Bind
↓
Listen
↓
Accept
↓
等待客户端

客户端:

创建Socket
↓
Connect
↓
连接服务器
↓
Send / Receive

整体流程:

      TCP服务器
          │
       Socket
          │
        Bind
          │
       Listen
          │
       Accept
          │
          │
      TCP客户端
          │
       Socket
          │
       Connect
          │
          ▼
        通信
     Send / Receive

五、TCP服务器Socket

先创建 Socket:

Socket serverSocket = new Socket(
AddressFamily.InterNetwork,
SocketType.Stream,
ProtocolType.Tcp
);

这里三个参数非常重要。

AddressFamily
AddressFamily.InterNetwork

表示使用 IPv4。

SocketType

TCP 使用:

SocketType.Stream

表示流式 Socket。

ProtocolType

TCP:

ProtocolType.Tcp

所以:

Socket serverSocket = new Socket(
AddressFamily.InterNetwork,
SocketType.Stream,
ProtocolType.Tcp
);

就是:

创建一个 IPv4 + TCP + 流式 Socket。

六、Bind绑定IP和端口

创建 Socket 后,需要绑定服务器的 IP 和端口。

IPAddress ip = IPAddress.Parse(“192.168.1.100”);

IPEndPoint endPoint =
new IPEndPoint(ip, 60000);

serverSocket.Bind(endPoint);

这里:

IPAddress.Parse()

负责把字符串:

192.168.1.100

转换成 IP 地址对象。

而:

IPEndPoint

表示:

IP + Port

例如:

192.168.1.100:60000
七、Listen监听

服务器绑定以后:

serverSocket.Listen(100);

表示服务器开始监听客户端连接。

其中:

100

表示等待连接队列的相关容量。

八、Accept接收客户端

服务器可以:

Socket clientSocket =
serverSocket.Accept();

Accept() 的作用:

等待客户端连接。

客户端连接以后:

服务器
│
Listen
│
Accept
│
└────── 客户端连接

Accept() 返回一个新的 Socket。

这个 Socket 专门负责:

服务器 ↔ 这个客户端
九、一个简单的TCP服务器

完整代码:

Socket serverSocket = new Socket(
AddressFamily.InterNetwork,
SocketType.Stream,
ProtocolType.Tcp
);

IPAddress ip =
IPAddress.Parse(“192.168.1.100”);

IPEndPoint endPoint =
new IPEndPoint(ip, 60000);

serverSocket.Bind(endPoint);

serverSocket.Listen(100);

Socket clientSocket =
serverSocket.Accept();

执行流程:

创建Socket
↓
Bind
↓
Listen
↓
Accept
↓
客户端连接成功
十、TCP客户端Socket

客户端也需要创建 Socket:

Socket clientSocket = new Socket(
AddressFamily.InterNetwork,
SocketType.Stream,
ProtocolType.Tcp
);

然后连接服务器:

IPAddress ip =
IPAddress.Parse(“192.168.1.100”);

IPEndPoint endPoint =
new IPEndPoint(ip, 60000);

clientSocket.Connect(endPoint);

也可以直接:

clientSocket.Connect(
“192.168.1.100”,
60000
);

连接成功以后:

客户端
│
Connect
│
▼
服务器
十一、TCP发送数据

Socket发送数据使用:

Send()

例如:

string message = “Hello”;

byte[] data =
Encoding.UTF8.GetBytes(message);

clientSocket.Send(data);

数据流程:

string
↓
Encoding
↓
byte[]
↓
Send()
↓
网络
十二、TCP接收数据

接收数据:

byte[] buffer = new byte[1024];

int length =
clientSocket.Receive(buffer);

Receive() 返回值:

length

表示实际收到多少个字节。

例如:

buffer大小:

1024 byte

实际收到:

5 byte

那么:

length == 5
十三、把接收的字节转换成字符串

如果发送的是 UTF-8 字符串:

string message =
Encoding.UTF8.GetString(
buffer,
0,
length
);

完整:

byte[] buffer = new byte[1024];

int length =
clientSocket.Receive(buffer);

string message =
Encoding.UTF8.GetString(
buffer,
0,
length
);
十四、TCP完整通信过程

例如客户端发送:

Hello

过程:

客户端

“Hello”
↓
Encoding.UTF8
↓
byte[]
↓
Send()
↓

  网络

================
↓
Receive()
↓
byte[]
↓
Encoding.UTF8
↓
“Hello”
↓
服务器

所以 TCP 通信本质上仍然是:

发送和接收 byte[]。

十五、TCP是字节流

这是 Socket 学习中非常重要的一点。

TCP:

没有消息边界。

例如客户端连续发送:

Hello
World

服务器不一定正好收到:

Hello
World

也可能收到:

HelloWorld

或者:

Hel
loWorld

甚至:

HelloW
orld

这就是 TCP 的:

粘包 / 拆包问题

十六、为什么会出现粘包拆包?

因为 TCP 本质上是:

字节流

而不是:

一条一条的消息

例如:

发送:

[消息A][消息B][消息C]

TCP只看到:

[一大串字节]

所以接收端需要自己判断:

消息A在哪里结束?
消息B在哪里结束?
十七、解决TCP粘包拆包

常见的方法:

方法一:固定长度

例如规定:

每条消息固定100字节

那么:

0~99 → 消息1
100~199 → 消息2
方法二:结束符

例如:

Hello\r\n
World\r\n

规定:

\r\n

就是消息结束。

接收:

Hello\r\nWorld\r\n

解析成:

Hello
World
方法三:长度 + 数据

例如:

长度
+
数据

假设:

05Hello
05World

程序先读取:

05

知道后面有:

5个字节

再读取:

Hello

这是实际项目中非常常见的方式。

十八、Socket异步通信

在 WinForms 中,不建议使用:

while (true)
{
socket.Receive(buffer);
}

直接放在 UI 线程。

因为:

Receive()

可能一直等待数据。

这样会导致:

UI线程
↓
一直等待
↓
界面卡死

因此实际项目中应该使用:

Task
async / await

或者后台线程。

例如:

private async Task ReceiveData()
{
byte[] buffer = new byte[1024];

while (true)
{
    int length =
        await clientSocket.ReceiveAsync(buffer);

    if (length == 0)
    {
        break;
    }

    string message =
        Encoding.UTF8.GetString(
            buffer,
            0,
            length
        );
}

}

这样接收数据时不会直接卡住 UI。

十九、TcpClient和Socket是什么关系?

C# 中除了直接使用:

Socket

还经常使用:

TcpClient
TcpListener

它们可以理解为对 Socket 的进一步封装。

例如:

Socket
↑
底层

而:

TcpClient
TcpListener
↑
更方便的TCP封装

所以如果只是做普通 TCP 上位机通信:

TcpClient
TcpListener

通常比直接操作 Socket 更简单。

例如你之前做的 TCP 项目就使用了:

TcpListener server;
TcpClient client;

这其实就是 C# 中非常常见的 TCP 编程方式。

二十、TcpListener服务器

使用 TcpListener 可以更加简单地创建 TCP 服务器:

TcpListener server =
new TcpListener(
IPAddress.Parse(“192.168.1.100”),
60000
);

server.Start();

等待客户端:

TcpClient client =
await server.AcceptTcpClientAsync();

然后:

NetworkStream stream =
client.GetStream();

发送:

byte[] data =
Encoding.UTF8.GetBytes(“Hello”);

await stream.WriteAsync(data, 0, data.Length);

接收:

byte[] buffer = new byte[1024];

int length =
await stream.ReadAsync(
buffer,
0,
buffer.Length
);
二十一、UDP Socket

TCP需要建立连接:

客户端
↓
Connect
↓
服务器

UDP则不需要建立连接。

UDP更像:

发送方
│
│ 数据
↓
网络
│
↓
接收方

UDP Socket创建:

Socket socket = new Socket(
AddressFamily.InterNetwork,
SocketType.Dgram,
ProtocolType.Udp
);

这里:

SocketType.Dgram

表示数据报。

二十二、UDP发送数据

UDP可以使用:

SendTo()

例如:

byte[] data =
Encoding.UTF8.GetBytes(“Hello”);

IPEndPoint endPoint =
new IPEndPoint(
IPAddress.Parse(“192.168.1.100”),
60000
);

socket.SendTo(data, endPoint);
二十三、UDP接收数据

UDP接收:

byte[] buffer = new byte[1024];

EndPoint remoteEndPoint =
new IPEndPoint(
IPAddress.Any,
0
);

int length =
socket.ReceiveFrom(
buffer,
ref remoteEndPoint
);

然后转换:

string message =
Encoding.UTF8.GetString(
buffer,
0,
length
);
二十四、TCP和UDP怎么选择?

实际开发中可以简单参考:

需要可靠传输
↓
TCP

例如:

聊天
文件传输
设备控制
配置参数
数据库通信

如果更加关注:

实时性
广播
组播

可以考虑:

UDP

例如:

设备发现
局域网广播
实时状态
视频/音频相关数据

但具体选择还是要根据设备协议和业务要求决定。

二十五、Socket通信中常见问题

  1. IP地址不对

例如服务器:

192.168.1.100

客户端却连接:

192.168.1.101

当然无法连接。

  1. 端口不一致

服务器:

60000

客户端:

50000

也无法连接。

  1. 防火墙

即使:

IP正确
端口正确
代码正确

Windows 防火墙也可能阻止通信。

  1. 服务器没有启动

客户端:

Connect()

但是服务器没有:

Listen()

客户端自然连接不上。

  1. Receive一直等待

例如:

socket.Receive(buffer);

如果没有数据:

Receive()
↓
等待
↓
等待
↓
等待

如果这个代码运行在 UI 线程:

界面卡死

所以需要考虑异步或者后台线程。

二十六、TCP连接断开怎么判断?

TCP中:

Receive()

如果返回:

0

通常表示:

对方已经正常关闭连接。

例如:

int length =
await stream.ReadAsync(buffer, 0, buffer.Length);

if (length == 0)
{
// 客户端断开
}

这个判断在实际项目中非常重要。

另外:

client.Connected

不能简单理解为:

只要是 true 就代表对方现在一定在线。

它不能单独作为可靠的连接存活判断。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐