前言
使用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保留的资源 |
单播
发送端
1 | import java.io.ByteArrayOutputStream; |
接收端
1 | import java.net.DatagramPacket; |
广播/多播(组播)
使用UDP协议进行信息的传输之前不需要建议连接。换句话说就是客户端向服务器发送信息,客户端只需要给出服务器的ip地址和端口号,然后将信息封装到一个待发送的报文中并且发送出去。至于服务器端是否存在,或者能否收到该报文,客户端根本不用管。
通常我们讨论的udp的程序都是一对一的单播程序。
这里将讨论一对多的服务:
- 广播(broadcast)
- 多播(multicast)
对于广播,网络中的所有主机都会接收一份数据副本。
对于多播,消息只是发送到一个多播地址,网络只是将数据分发给哪些表示想要接收发送到该多播地址的数据的主机。
总得来说,只有UDP套接字允许广播或多播。
UDP广播
广播UDP与单播UDP的区别就是IP地址不同,广播使用广播地址255.255.255.255
,将消息发送到在同一广播网络上的每个主机。
值得强调的是:
本地广播信息是不会被路由器转发。当然这是十分容易理解的,因为如果路由器转发了广播信息,那么势必会引起网络瘫痪。这也是为什么IP协议的设计者故意没有定义互联网范围的广播机制。
广播地址通常用于在网络游戏中处于同一本地网络的玩家之间交流状态信息等。
其实广播顾名思义,就是想局域网内所有的人说话,但是广播还是要指明接收者的端口号的,因为不可能接受者的所有端口都来收听广播。
UDP多播
同样的UDP多播也要指明接受者的端口号,而且与广播相似的是多播与单播之间的区别还在于地址。
ipv4中的多播地址范围是:224.0.0.0到239.255.255.255
。
在JAVA中,多播一样十分好实现,要实现多播,就要用到MulticastSocket类,其实该类就是DatagramSocket的子类,在使用时除了多播自己的一些特性外,把它当做DatagramSocket类使用就可以了。
使用Java 的UDP进行多播,要分两步走,首先要加入到广播组地址,其次要建立套接字传输信息
关于多播,涉及到MulticastSocket,他用于接收广播的信息,前提是要将它加入到广播组,
组播的地址是保留的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作为我们的广播地址。
UDP广播
发送端
1 | import java.io.ByteArrayOutputStream; |
接收端
1 | import java.io.ByteArrayInputStream; |
UDP广播(处理消息字节)
发送端
1 | import java.io.ByteArrayOutputStream; |
接收端
1 | import java.io.ByteArrayInputStream; |
字节操作
字符传转字节及拼接
1 | ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); |
整数转字节
1 | //整数转字节 |
字节截取及转字符串
1 | ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data); |
字节转整数
1 | //字节转整数 |
UDP多播
发送端
1 | import java.io.IOException; |
接收端
默认网卡监听
1 | import java.net.*; |
所有网卡监听
1 | import java.net.DatagramPacket; |
UDP多播(互发)
接收端要占用端口,所以下面的这种情况都是发送端和接收端,就要在两台机器上运行。
发送端接收回发
1 | import java.io.ByteArrayOutputStream; |
接收端回发消息
1 | import java.io.ByteArrayInputStream; |
验证网卡是否加入多播组
windows: 执行
1 | netsh interface ipv4 show joins |
Linux: 执行
1 | netstat -g |
获取网卡信息
1 | import java.net.InterfaceAddress; |
工具类
1 | import java.net.Inet4Address; |
根据IP获取网卡
1 | String localhost_address = "192.168.3.95"; |
获取默认
1 | InetAddress addr = InetAddress.getLocalHost(); |
C#与Java的byte[]兼容
数字/布尔/字符的处理
在开发的过程中使用C#发送的byte[],在Java中接收后,发现数据不一致,为什么呢?
有人说是取值范围不同,经过测试发现,跟这个无关。
在Java中 byte
的范围在 [-128,127]
在C#中 byte
的范围在 [0,255]
在C#中sbyte
是有符号的 8 位整数类型,它的范围是从 -128 到 127
。
真正导致这个问题的原因是:
Java和C#在网络传输中使用不同的字节序(Byte Order)。
Java使用大端字节序(Big Endian),C#使用小端字节序(Little Endian)。
所以,有两种解决方案:
- C#发送端转换字节序并发送网络字节序数据
- Java接收端转换字节序
注意
两种方式任取其一,不要两边都进行转换。
建议
在C#端进行转换。即使用大端字节序(Big Endian)。
C#发送网络字节序数据
在C#中,可以使用BitConverter类进行字节序转换,然后发送网络字节序(Big Endian)的数据。
具体步骤如下:
使用BitConverter.IsLittleEndian
判断当前系统的字节序,如果是Little Endian
,才需要转换。
1 | byte[] data = BitConverter.GetBytes(value); |
这样,无论C#运行环境的字节序是LittleEndian
还是BigEndian
,它发送出去的UDP数据字节序都是BigEndian
,符合网络字节序。
所以Java
接收端在这种情况下就不需要再进行字节序转换,直接按BigEndian
解析接收到的数据即可。
Java接收端转换字节序
可以使用如下方法转换:
1 | public static void arrayReverse(byte[] array){ |
使用
1 | public static void main(String[] args) throws IOException { |
注意
我们要把每段数据进行反转,不要把整体反转。
这样,Java
接收到数据后,转换字节序,然后处理,就可以正确解析C#
通过UDP发送过来的byte[]
了。
另外,如果C#端已经进行字节序转换,发送网络字节序(Big Endian
)的数据,那么Java接收就不需要转换了。
字符串的处理
建议
使用UTF-8编码传输字符串。
注意
字符串要根据编码和环境决定。
因为UTF-8
编码的字符串在任何系统上都是按大端字节序存储的,那么转换得到的byte[]
就已经是大端字节序(网络字节序)了,不需要反转。
但是如果字符串使用系统默认编码(Windows
上是GBK
,Linux/Unix
上是ASCII
等),那么转换得到的byte[]
的字节序就取决于该系统的存储规则了。
系统的存储规则
大端字节序系统(
Java
、Linux
等)小端字节序系统(
C#
、Windows
等)
图片的处理
建议
使用
PNG/JPG
编码传输图片。
图片转换得到的byte[]
是否需要反转字节序,也取决于两点:
- 图片的存储格式
- 要传输到的系统的字节序
常见的图片格式有:
PNG/JPG
:使用大端字节序存储,所以转换得到的byte[]
默认就是大端字节序,无需反转。BMP
:使用小端字节序存储,转换得到的byte[]
是小端字节序。
所以:
- 传输PNG/JPG图片到任何系统,都无需反转
byte[]
传输BMP图片:
虽然存储的都是小端字节序,但是读取的数据还是按照平台的字节序,所以同字节序的不用反转,不同字节序的要反转。
例如,
在C#中:
1 | byte[] pngBytes = File.ReadAllBytes("test.png"); |
在Java中:1
2
3
4
5
6byte[] pngBytes = Files.readAllBytes(Paths.get("test.png"));
// 发送给任何系统,都无需反转
byte[] bmpBytes = Files.readAllBytes(Paths.get("test.bmp"));
// 发送给C#,需要反转
// 发送给Java,无需反转
BMP格式使用小端字节序存储数据,这意味着低序字节存储在低地址,高序字节存储在高地址。
C#
是小端字节序平台,所以C#中的BMP数据的字节序和文件中的一致。
Java
是大端字节序平台,所以Java中读取的BMP数据的字节序与文件中的不同,需要反转。
具体地,如果BMP文件的一个像素的RGB值存储为:
1 | R: 0x12 (0001 0010) |
则:
C#中,这个像素的RGB值仍然是0x123456
Java中,这个像素的RGB值是0x563412
,因为Java是大端字节序,高位字节在前,需要反转每个短整数的字节序
所以,在C#向Java传输BMP图片时,需要反转整个图片数据的字节序;
反之,Java向C#传输也需要反转字节序。
总之,
两个不同的字节序平台传输BMP格式的图片,都需要对图片数据进行字节序反转,以适配接收方平台的字节序
BMP正确的反转方法:
1 | C#: |
所以,在C#和Java之间传输BMP图像,需要反转上下的顺序和像素的顺序。
字节序不同的原因
Java和C#选择不同的字节序主要有两个原因:
- 遵循不同的存储标准:
Java遵循网络传输标准,使用大端字节序。大端字节序也被称为网络字节序,是Internet协议中数据存储的标准格式。
而C#遵循Intel CPU存储标准,使用小端字节序。因为Intel的CPU采用小端字节序保存数据。 - 面向不同的系统:
Java作为一门跨平台语言,为了在不同的系统中都能正确解析数据,选择使用大端字节序这种平台无关的格式。
而C#最初设计只面向Windows平台,跟随Windows的standard选择小端字节序。
所以简单来说,主要原因是:
Java遵循网络标准和跨平台要求,选择大端字节序。
C#遵循Intel CPU和Windows平台标准,选择小端字节序。