使用 Java 多播图像
在网络内分发图像对于演示或仅在会议室中缺少投影仪时非常有用。本文展示了如何多播图像和屏幕截图,以便网络中的任何人都可以查看它们。
Multcasting 是基于UDP 数据包的。UDP 提供(与TCP不同)没有流量控制。这意味着不能保证包裹按正确的顺序交付,或者可能根本不会交付。计算机可以加入由 IP 号码和端口表示的所谓多播组。
组播组的每个成员将接收属于该组的组播数据包。
概述
在这个示例应用程序中,我们将多播屏幕截图和图像,以便它们可以在网络中的任何地方显示。
该应用程序由发送方和接收方 Java 应用程序组成。
如果您不热衷于了解所有详细信息,您可以直接跳到下面的运行应用程序部分并测试应用程序。
基础
接收组播包
组播报文的接收可以分为以下几个步骤:
- 在第一步中,通过调用MulticastSocket()创建多播套接字 ,该方法将套接字绑定到的端口作为参数。
- 下一步是加入组播组。
多播组由范围从224.0.0.0到239.255.255.255的 IP 号表示。要加入组,调用joinGroup()方法。 - 创建一个字节缓冲区、一个 DatagramPacket 对象并调用 MulticastSocket 方法receive() 以接受数据报包。
- 通过 在 DatagramPacket 上调用getData()接收和处理数据报包后, 最后一步是离开多播组并关闭多播套接字。
这是通过调用 多播套接字上的leaveGroup() 和close()方法来完成的。
以下代码说明了这四个步骤。
/* Step one */
int port = 4444;
MulticastSocket ms = new MulticastSocket(port);
/* Step two */
String multicastAddress = "225.4.5.6";
InetAddress ia = InetAddress.getByName(multicastAddress);
ms.joinGroup(ia);
/* Step three */
DatagramPacket dp = new DatagramPacket(buffer, buffer.length);
ms.receive(dp);
byte[] data = dp.getData();
/* Step four */
ms.leaveGroup(ia);
ms.close();
Receiving a Multicast Packet
传输组播数据包
指定在丢弃数据包之前要通过的路由器数量(跳数)。
每个通过数据包的路由器都会将 TTL 减一。TTL 为零的数据包将被丢弃。
对于 MulticastSockets(在 Java 中),TTL 默认为 1,这意味着所有数据包都将保留在同一网络中。
可以通过在 MulticastSocket 上调用setTimeToLive()来更改该参数
传输组播数据包比接收数据包要容易一些。
如果只是发送数据包,则不必加入组。
这些是传输多播数据包所需的步骤:
- 在第一步中,为指定的多播组 IP 地址创建一个 InetAddress。
- 下一步是构造一个多播套接字和一个数据报数据包。
- 现在唯一剩下的就是通过多播套接字发送数据报并关闭套接字。
这四个步骤如下所示。
/* Step one */
String multicastAddress = "225.4.5.6";
InetAddress ia = InetAddress.getByName(multicastAddress);
/* Step two */
int port = 4444;
ms = new MulticastSocket();
DatagramPacket dp = new DatagramPacket(imageData, imageData.length,
ia, port);
/* Step three */
ms.send(dp);
ms.close();
Transmitting a Multicast Packet
现在我们知道如何传输和接收多播数据包,我们将看看图像是如何创建的。
创建屏幕截图
通过网络发送屏幕截图相当有趣,因为它使我们可以向其他人展示演示文稿或屏幕内容。
在 Java 中创建屏幕截图非常简单。从 Java 1.3 开始,Robot 类可用,这也使我们可以创建屏幕截图。 Robot 类的createScreenCapture()
方法采用一个矩形来指定要捕获的屏幕部分。在我们的例子中,我们将始终捕获整个屏幕。 下面是 用于在我们的程序中获取屏幕截图的方法getScreenshot()的源代码。
public static BufferedImage getScreenshot() throws AWTException,
ImageFormatException, IOException {
Toolkit toolkit = Toolkit.getDefaultToolkit();
Dimension screenSize = toolkit.getScreenSize();
Rectangle screenRect = new Rectangle(screenSize);
Robot robot = new Robot();
BufferedImage image = robot.createScreenCapture(screenRect);
return image;
}
Creating a Screenshot
从文件系统读取图像
除了发送屏幕截图,应用程序还可以发送存储在文件系统中的图像。
该应用程序将按随机顺序从目录中读取 JPEG 图像。
public static BufferedImage getRandomImageFromDir(File dir) throws IOException {
String[] images = dir.list(new ImageFileFilter());
int random = new Random().nextInt(images.length);
String fileName = dir.getAbsoluteFile() + File.separator + images[random];
File imageFile = new File(fileName);
return ImageIO.read(imageFile);
}
class ImageFileFilter implements FilenameFilter
{
public boolean accept( File dir, String name )
{
String nameLc = name.toLowerCase();
return nameLc.endsWith(".jpg") ? true : false;
}
}
Reading Imges from the Filesystem
传输数据
既然我们知道了如何传输多播数据包以及如何获取图像,那么我们就来仔细看看如何通过网络传输大图像。
UDP 数据包大小
UDP(以及 TCP)封装在 IP 数据包中。
最大 IP 数据包大小为 65535。从最大 IP 数据包大小中,我们必须减去 IP 头的 20 个字节和 UDP 头的 8 个字节。
因此,UDP 数据包中可以传输的最大数据大小为 65507 字节。
1 | 2 | 3 |
1 - IP 报头(20 字节)2 - UDP 报头(8 字节)3 - UDP 数据(最大 65507 字节)
65507 字节的限制对于小图像来说可能足够了,但对于全屏截图来说这还不够。
可以缩小图像以使其适合单个 UDP 数据包,但这会使这些图像难以查看。
在我们的解决方案中,我们将把图像分成适当大小的块并通过网络传输它们。
如上所述,UDP 内部没有流量控制,因此我们必须自己处理。
交通管制
为了确保传输的图像切片以正确的顺序重新组合,一些标头被添加到 UDP 数据包中。
这将减少可以在数据包内传输的数据量,但这应该是可以接受的。
旗帜 | 8 位 | 包含 SESSION_END 和 SESSION_START 标志 |
会话编号 | 8 位 | 数据包所属的会话 |
数据包 | 8 位 | 总包数 |
最大数据包大小 | 16 位 | 每个数据包的最大大小 |
包号 | 8 位 | 当前包的数量 |
尺寸 | 16 位 | 当前包的数据大小 |
发送方确定每个图像大小的大小,然后通过网络将切片与上述附加头信息一起发送。
根据附加头信息中给出的信息,图像接收器可以确定图像的最终尺寸、当前图像数据的位置以及图像完成的天气。
发件人代码片段如下所示。
while(true) {
BufferedImage image = getScreenshot();
image = shrink(image, scalingFactor);
byte[] imageByteArray = bufferedImageToByteArray(image, OUTPUT_FORMAT);
int packets = (int) Math.ceil(imageByteArray.length / (float)DATAGRAM_MAX_SIZE);
int HEADER_SIZE = 8;
int MAX_PACKETS = 255;
int SESSION_START = 128;
int SESSION_END = 64;
if(packets > MAX_PACKETS) {
System.out.println("Image is too large to be transmitted!");
continue;
}
for(int i = 0; i <= packets; i++) {
int flags = 0;
flags = i == 0 ? flags | SESSION_START: flags;
flags = (i + 1) * DATAGRAM_MAX_SIZE > imageByteArray.length ? flags | SESSION_END : flags;
int size = (flags & SESSION_END) != SESSION_END ? DATAGRAM_MAX_SIZE : imageByteArray.length - i * DATAGRAM_MAX_SIZE;
byte[] data = new byte[HEADER_SIZE + size];
data[0] = (byte)flags;
data[1] = (byte)sessionNumber;
data[2] = (byte)packets;
data[3] = (byte)(DATAGRAM_MAX_SIZE >> 8);
data[4] = (byte)DATAGRAM_MAX_SIZE;
data[5] = (byte)i;
data[6] = (byte)(size >> 8);
data[7] = (byte)size;
System.arraycopy(imageByteArray, i * DATAGRAM_MAX_SIZE, data, HEADER_SIZE, size);
sender.sendImage(data, "225.4.5.6", 4444);
if((flags & SESSION_END) == SESSION_END) break;
}
Thread.sleep(SLEEP_MILLIS);
sessionNumber = sessionNumber < MAX_SESSION_NUMBER ? ++sessionNumber : 0;
}
Image Sender
接收器检查 UDP 数据并确定图像属于当前会话的天气以及图像切片是否已经存储。
采集完所有切片后,将显示图像。
byte[] buffer = new byte[DATAGRAM_MAX_SIZE];
while (true) {
DatagramPacket dp = new DatagramPacket(buffer, buffer.length);
ms.receive(dp);
byte[] data = dp.getData();
int SESSION_START = 128;
int SESSION_END = 64;
int HEADER_SIZE = 8;
short session = (short)(data[1] & 0xff);
short slices = (short)(data[2] & 0xff);
int maxPacketSize = (int)((data[3] & 0xff) << 8 | (data[4] & 0xff)); // mask the sign bit
short slice = (short)(data[5] & 0xff);
int size = (int)((data[6] & 0xff) << 8 | (data[7] & 0xff)); // mask the sign bit
if((data[0] & SESSION_START) == SESSION_START) {
if(session != currentSession) {
currentSession = session;
slicesStored = 0;
imageData = new byte[slices * maxPacketSize];
slicesCol = new int[slices];
sessionAvailable = true;
}
}
if(sessionAvailable && session == currentSession){
if(slicesCol != null && slicesCol[slice] == 0) {
slicesCol[slice] = 1;
System.arraycopy(data, HEADER_SIZE, imageData, slice * maxPacketSize, size);
slicesStored++;
}
}
if(slicesStored == slices) {
ByteArrayInputStream bis= new ByteArrayInputStream(imageData);
BufferedImage image = ImageIO.read(bis);
labelImage.setIcon(new ImageIcon(image));
windowImage.setIcon(new ImageIcon(image));
frame.pack();
}
}
Image Receiver
原则上就是这样。现在让我们来看看应用程序。
应用程序
该应用程序的工作原理如上所述,但除此之外还有一些附加功能。
这些功能在本文中没有解释,但通过阅读源代码应该很容易理解。
这些附加功能是:
- 发送者缩放图像
- 显示鼠标位置
- 以全屏模式显示图像(接收器)
运行应用程序
首先下载应用程序。
ZIP 文件包含两个 jar 文件和源代码。
对于第一个测试,只需双击 jar 文件(ImageSender.jar 和 ImageReceiver.jar)。
这将启动发送方和接收方应用程序。
您应该会看到类似于以下屏幕截图的内容。
在一台机器上运行这两个程序没有多大意义,但这是一个测试,如果两个应用程序都在工作。
对于真正的测试,在两台不同的计算机上启动 jar 文件。发送方的屏幕截图应显示在接收方一侧。
如果运行发送器的目录中有 图像文件夹,则将发送这些图像(JPEG 格式)而不是屏幕截图。
要以全屏模式查看图像,只需在多播图像接收器窗口中按任意键即可。
以下是一些示例屏幕截图。
命令行参数
程序也可以通过指定命令行参数来运行。
当未指定命令行参数时,将使用以下默认值:
端口: 4444
显示鼠标光标: 1
以秒为单位的更新间隔(发送方): 2
缩放系数: 0.5
以下命令行参数可用:
java -jar ImageReceiver.jar 端口 ip_address
命令行选项不必完全指定,但中间不能省略任何选项。
所以指定以下选项是可以的: java -jar ImageSender.jar 0.7 3 1 2048
无线网络
无线网络可能是一个问题,因为并非所有路由器都支持多播到具有无线连接的计算机。
结果可能是图像没有及时显示或根本没有显示。
如果您的无线连接遇到问题,请尝试通过电缆直接将您的计算机连接到路由器。
而已。我希望你喜欢这篇文章。
如有疑问,请给我留言。
快乐编码!
(joschi)
版本:0.2
作者的电子邮件地址:jochen [at] fun2code.de