局域网通过端口通讯(UDP)

前言

使用DatagramSocket代表UDP协议的Socket,DatagramSocket本身只是码头,不维护状态,不能产生IO流,它的唯一作用就是接收和发送数据报,Java使用DatagramPacket来代表数据报,DatagramSocket接收和发送的数据都是通过DatagramPacket对象完成的。

DatagramSocket用于创建发送端和接收端对象,然而在创建发送端和接收端的DatagramSocket对象时,使用的构造方法有所不同,下面对DatagramSocket类中常用的构造方法进行讲解。

● DatagramSocket()

该构造方法用于创建发送端的DatagramSocket对象,在创建DatagramSocket对象时,并没有指定端口号,此时,系统会分配一个没有被其它网络程序所使用的端口号。

● DatagramSocket(int port)

该构造方法既可用于创建接收端的DatagramSocket对象,也可以创建发送端的DatagramSocket对象,在创建接收端的DatagramSocket对象时,必须要指定一个端口号,这样就可以监听指定的端口。

● DatagramSocket(int port,InetAddress addr)

使用该构造方法在创建DatagramSocket时,不仅指定了端口号还指定了相关的IP地址,这种情况适用于计算机上有多块网卡的情况,可以明确规定数据通过哪块网卡向外发送和接收哪块网卡的数据。由于计算机中针对不同的网卡会分配不同的IP,因此在创建DatagramSocket对象时需要通过指定IP地址来确定使用哪块网卡进行通信。

方法声明 功能描述
void receive(DatagramPacket p) 该方法用于接收DatagramPacket数据报,在接收到数据之前会一直处于阻塞状态,如果发送消息的长度比数据报长,则消息将会被截取
void send(DatagramPacket p) 该方法用于发送DatagramPacket数据报,发送的数据报中包含将要发送的数据、数据的长度、远程主机的IP地址和端口号
void close() 关闭当前的Socket,通知驱动程序释放为这个Socket保留的资源

单播/广播/多播(组播)

使用UDP协议进行信息的传输之前不需要建议连接。换句话说就是客户端向服务器发送信息,客户端只需要给出服务器的ip地址和端口号,然后将信息封装到一个待发送的报文中并且发送出去。至于服务器端是否存在,或者能否收到该报文,客户端根本不用管。

通常我们讨论的udp的程序都是一对一的单播程序。

这里将讨论一对多的服务:

  • 广播(broadcast)
  • 多播(multicast)

UDP广播只能在内网(同一网段)有效

类型 特点 范围
单播 指定主机获取到消息。 UDP单播可以跨网段,只要两个IP是互通的。
广播 广播使用广播地址255.255.255.255,将消息发送到在同一广播网络上的每个主机 UDP广播只能在内网(同一网段)有效。
多播 消息只是发送到一个多播地址,网络只是将数据分发给哪些表示想要接收发送到该多播地址的数据的主机。

值得强调的是:

广播信息是不会被路由器转发。当然这是十分容易理解的,因为如果路由器转发了广播信息,那么势必会引起网络瘫痪。这也是为什么IP协议的设计者故意没有定义互联网范围的广播机制。

这三者都需要发送方指定接收方的IP和端口,区别在于单播是接收方的本机IP,广播是固定的广播地址255.255.255.255,多播是对应的多播地址。

多播数据报的接收是主动的。主机主动加入指定的多播组,才会接收该组的多播数据报。
不同子网内的A,B进行组播通信,依靠IGMP协议;

多播地址

多播地址是保留的D类地址从224.0.0.0—239.255.255.255

IP段 作用 用户是否可用
224.0.0.0~224.0.0.255 预留的组播地址(永久组地址),地址224.0.0.0保留不做分配,其它地址供路由协议使用。
224.0.1.0~224.0.1.255 公用组播地址,可以用于Internet,需要申请才能用。
224.0.2.0~238.255.255.255 用户可用的组播地址(临时组地址),全网范围内有效。
239.0.0.0~239.255.255.255 本地管理组播地址,可供组织内部使用,类似于私有 IP 地址,不能用于 Internet,可限制多播范围。

这里我们就选取230.0.0.1作为我们的广播地址。

注意

  1. 同一网段如果UDP不通,要排查交换机是否禁用广播/组播。
  2. 不同网段UDP不通,还要排查是否开启IGMP协议。
  3. 跨网段的时候,有的交换机只能设置为单向广播。比如配置的192.168.3.x=>192.168.2.x,这时候只能从3段发送消息到2段。

UDP单播

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace udp_test
{
internal class Program
{
private static void Main(string[] args)
{
UdpClient UDPsend = new UdpClient(new IPEndPoint(IPAddress.Any, 0));
IPEndPoint endpoint = new IPEndPoint(IPAddress.Parse("192.168.3.95"), 55666);
Thread receThread = new Thread(new ThreadStart(RecvThread));
receThread.IsBackground = true;
receThread.Start();
while (true)
{
string str = Console.ReadLine();
byte[] buf = Encoding.UTF8.GetBytes(str);
UDPsend.Send(buf, buf.Length, endpoint);
}
}

//接收
private static void RecvThread()
{
UdpClient UDPrece = new UdpClient(new IPEndPoint(IPAddress.Any, 55666));
IPEndPoint endpoint = new IPEndPoint(IPAddress.Any, 0);
while (true)
{
byte[] buf = UDPrece.Receive(ref endpoint);
string msg = Encoding.UTF8.GetString(buf);
Console.WriteLine("收到:" + msg);
}
}
}
}

发送

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
UdpClient udPsend = new UdpClient(
new IPEndPoint(
IPAddress.Any,
0
)
);
IPEndPoint endpoint = new IPEndPoint(
IPAddress.Parse("192.168.2.223"),
10099
);

//发送
udPsend.Send(
imageBytes,
imageBytes.Length,
endpoint
);

接收

接收方只需要端口就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private static void Main(string[] args)
{
Thread receiveThread = new Thread(RecvThread) { IsBackground = true };
receiveThread.Start();
}

//接收
private static void RecvThread()
{
UdpClient udPrece = new UdpClient(
new IPEndPoint(
IPAddress.Any,
55666
)
);
IPEndPoint endpoint = new IPEndPoint(
IPAddress.Any,
0
);
while (true)
{
byte[] buf = udPrece.Receive(ref endpoint);
string msg = Encoding.UTF8.GetString(buf);
Console.WriteLine($@"收到:{msg}");
}
}

分包发送

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
private void SendPicUdp()
{
long picIndex = 0;
UdpClient udPsend = new UdpClient(
new IPEndPoint(
IPAddress.Any,
0
)
);
IPEndPoint endpoint = new IPEndPoint(
IPAddress.Parse("192.168.2.223"),
10099
);
new Thread(
() =>
{
while (true)
{
var imageBytes = ZScreenUtils.GetScreenshot();
picIndex += 1;
int packetSize = 1024;
byte[] picIndexHeader = BitConverter.GetBytes(picIndex);
// 获取需要发送的总包数
int totalPackets = (int)Math.Ceiling((double)imageBytes.Length / packetSize);
// 图片大小
byte[] picSizeHeader = BitConverter.GetBytes(imageBytes.Length);

// 对每个包进行分割并发送
for (int packetIndex = 0; packetIndex < totalPackets; packetIndex++)
{
int offset = packetIndex * packetSize;
int size = Math.Min(
packetSize,
imageBytes.Length - offset
);

// 获取当前包的数据
byte[] packetData = new byte[size];
Array.Copy(
imageBytes,
offset,
packetData,
0,
size
);

// 将包的序号和总包数添加到包头,并在发送数据时一并发送
byte[] packetIndexHeader = BitConverter.GetBytes(packetIndex);
byte[] packetTotalHeader = BitConverter.GetBytes(totalPackets);

//拼接 图片编号(8字节)+图片大小(4字节)+当前索引(4字节)+总数量(4字节)+图片数据
byte[] data = picIndexHeader
.Concat(picSizeHeader)
.Concat(packetIndexHeader)
.Concat(packetTotalHeader)
.Concat(packetData)
.ToArray();

// 发送数据包
UdpClient udpClient = new UdpClient();
udpClient.Send(
data,
data.Length,
endpoint
);
}
}
}
).Start();
}

解析拼接的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

private void GetDataInfo(byte[] data)
{
byte[] picIndexHeader = new byte[8];
byte[] picSizeHeader = new byte[4];
byte[] packetIndexHeader = new byte[4];
byte[] packetTotalHeader = new byte[4];
byte[] packetData = new byte[data.Length - 8 - 4 - 4 - 4];
Buffer.BlockCopy(
data,
0,
picIndexHeader,
0,
picIndexHeader.Length
);
Buffer.BlockCopy(
data,
8,
picSizeHeader,
0,
picSizeHeader.Length
);
Buffer.BlockCopy(
data,
12,
packetIndexHeader,
0,
packetIndexHeader.Length
);
Buffer.BlockCopy(
data,
16,
packetTotalHeader,
0,
packetTotalHeader.Length
);
Buffer.BlockCopy(
data,
20,
packetData,
0,
packetData.Length
);
long picIndex = BitConverter.ToInt64(
picIndexHeader,
0
);
int picSize = BitConverter.ToInt32(
picSizeHeader,
0
);
int packetIndex = BitConverter.ToInt32(
packetIndexHeader,
0
);
int packetTotal = BitConverter.ToInt32(
packetTotalHeader,
0
);
Console.WriteLine($@"当前图片编号:{picIndex} 图片大小:{picSize} 数据包索引:{packetIndex} 数据包总数:{packetTotal}");
}

UDP广播

1
2
3
4
5
string str = Console.ReadLine();
byte[] buf = Encoding.UTF8.GetBytes(str);
byte[] lenByte = BitConverter.GetBytes(buf.Length);
byte[] allByte = lenByte.Concat(buf).ToArray();
UDPsend.Send(allByte, allByte.Length, endpoint);

这里我们不拼接字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace udp_test
{
internal class Program
{
private static void Main(string[] args)
{
UdpClient UDPsend = new UdpClient(new IPEndPoint(IPAddress.Any, 0));
//IPAddress.Broadcast 就是 255.255.255.255
IPEndPoint endpoint = new IPEndPoint(IPAddress.Broadcast, 55666);
Thread receThread = new Thread(new ThreadStart(RecvThread));

receThread.IsBackground = true;
receThread.Start();

while (true)
{
string str = Console.ReadLine();
byte[] buf = Encoding.UTF8.GetBytes(str);
UDPsend.Send(buf, buf.Length, endpoint);
Thread.Sleep(1000);
}
}

//接收
private static void RecvThread()
{
UdpClient UDPrece = new UdpClient(new IPEndPoint(IPAddress.Any, 55666));
IPEndPoint endpoint = new IPEndPoint(IPAddress.Any, 0);
while (true)
{
byte[] buf = UDPrece.Receive(ref endpoint);
string msg = Encoding.UTF8.GetString(buf);
Console.WriteLine(msg);
}
}
}
}

其中

1
IPEndPoint endpoint = new IPEndPoint(IPAddress.Broadcast, 55666);

和下面的是等效的

1
IPEndPoint endpoint = new IPEndPoint(IPAddress.Parse("255.255.255.255"), 55666);

UDP多播

使用默认网卡还是多网卡发送端和接收端都要考虑。

使用默认网卡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace udp_test
{
internal class Program
{
private static string broadcast_address = "239.8.8.8";

private static void Main(string[] args)
{
UdpClient UDPsend = new UdpClient();
UDPsend.Ttl = 5;
UDPsend.JoinMulticastGroup(IPAddress.Parse(broadcast_address));
IPEndPoint endpoint = new IPEndPoint(IPAddress.Parse(broadcast_address), 55666);
Thread receThread = new Thread(new ThreadStart(RecvThread));
receThread.IsBackground = true;
receThread.Start();
while (true)
{
string str = Console.ReadLine();
byte[] buf = Encoding.UTF8.GetBytes(str);
UDPsend.Send(buf, buf.Length, endpoint);
}
}

//接收
private static void RecvThread()
{
UdpClient UDPrece = new UdpClient(55666);
UDPrece.JoinMulticastGroup(IPAddress.Parse(broadcast_address));
IPEndPoint endpoint = new IPEndPoint(IPAddress.Parse(broadcast_address), 55666);
while (true)
{
byte[] buf = UDPrece.Receive(ref endpoint);
string msg = Encoding.UTF8.GetString(buf);
Console.WriteLine("收到:" + msg);
}
}
}
}

多网卡指定网卡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Net.NetworkInformation;
using System.Collections.Generic;

namespace udp_test
{
internal class Program
{
private static string broadcast_address = "239.8.8.8";

//发送或接收消息的网卡IP
private static string receive_address = "";

private static void Main(string[] args)
{
int ipindex = 0;
Console.WriteLine("请输入网卡编号:[0]");
List<string> ipList = GetAllNameIP();
for (int i = 0; i < ipList.Count; i++)
{
string ip = ipList[i];
Console.WriteLine(string.Format("[{0}] {1}", i, ip));
}
ipindex = int.Parse(Console.ReadLine());
receive_address = ipList[ipindex].Split(':')[1];
if (receive_address.Equals(""))
{
Console.WriteLine("使用默认网卡");
}
else
{
Console.WriteLine("接收信息的网卡IP:" + receive_address);
}

UdpClient UDPsend = new UdpClient();
UDPsend.Ttl = 5;
UDPsend.JoinMulticastGroup(IPAddress.Parse(broadcast_address), IPAddress.Parse(receive_address));
IPEndPoint endpoint = new IPEndPoint(IPAddress.Parse(broadcast_address), 55666);
Thread receThread = new Thread(new ThreadStart(RecvThread));
receThread.IsBackground = true;
receThread.Start();
while (true)
{
Console.WriteLine("\n请输入发送的消息:");
string str = Console.ReadLine();
byte[] buf = Encoding.UTF8.GetBytes(str);
UDPsend.Send(buf, buf.Length, endpoint);
}
}

//接收
private static void RecvThread()
{
UdpClient UDPrece = new UdpClient(55666);
if (receive_address.Equals(""))
{
UDPrece.JoinMulticastGroup(IPAddress.Parse(broadcast_address));
}
else
{
UDPrece.JoinMulticastGroup(IPAddress.Parse(broadcast_address), IPAddress.Parse(receive_address));
}

IPEndPoint endpoint = new IPEndPoint(IPAddress.Parse(broadcast_address), 55666);
while (true)
{
byte[] buf = UDPrece.Receive(ref endpoint);
string msg = Encoding.UTF8.GetString(buf);
Console.WriteLine("收到:" + msg);
}
}

public static List<string> GetAllNameIP()
{
List<string> ipList = new List<string>();
NetworkInterface[] interfaces = NetworkInterface.GetAllNetworkInterfaces();
int len = interfaces.Length;

for (int i = 0; i < len; i++)
{
NetworkInterface ni = interfaces[i];
// 获取状态为使用中的
if (ni.OperationalStatus == OperationalStatus.Up && ni.NetworkInterfaceType != NetworkInterfaceType.Loopback)
{
IPInterfaceProperties property = ni.GetIPProperties();
foreach (UnicastIPAddressInformation ip in property.UnicastAddresses)
{
if (ip.Address.AddressFamily == AddressFamily.InterNetwork)
{
string address = ip.Address.ToString();
string name = ni.Name.ToString();
ipList.Add(name + ":" + address);
}
}
}
}

return ipList;
}
}
}

验证网卡是否加入多播组

多播组监听只和接收者有关。

windows: 执行

1
netsh interface ipv4 show joins

注意多播的地址一定要绑定在正确的网卡上。

image-20230206152323854

如果要清除多播组,只需要禁用再启用对应的网卡即可。

Linux: 执行

1
netstat -g

获取网卡信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void GetNameIP()
{
NetworkInterface[] interfaces = NetworkInterface.GetAllNetworkInterfaces();
int len = interfaces.Length;

for (int i = 0; i < len; i++)
{
NetworkInterface ni = interfaces[i];
// 获取状态为使用中的
if (ni.OperationalStatus == OperationalStatus.Up && ni.NetworkInterfaceType != NetworkInterfaceType.Loopback)
{
IPInterfaceProperties property = ni.GetIPProperties();
foreach (UnicastIPAddressInformation ip in property.UnicastAddresses)
{
if (ip.Address.AddressFamily == AddressFamily.InterNetwork)
{
string address = ip.Address.ToString();
string name = ni.Name.ToString();
Console.WriteLine("【" + name + "】:" + address);
}
}
}
}
}