您的位置 首页 java

Java课程设计项目实例《远程屏幕分享监视》第1部分

java 课程设计项目实例《远程屏幕分享监视》第1部分

1、项目相关程序实现的基本原理

本项目由于采用C/S模式(客户机/服务器)的系统架构实现方案,其中的服务器端主机也就是被分享(被监视)屏幕的主机(只有一台主机),而客户机就是显示出被分享屏幕的主机(监视主机可以是多台),从而形成一台服务器但多个客户机的应用状况(参看如下示例图所示的网络连接模式)。

程序实现的原理其实也很简单,在被监视的主机上(也就是本项目中的SocketServer服务器端),针对每个成功连接的客户端(也就是本项目中的SocketClient客户端)运行一个线程,每隔一段时间就自动截图产生出对应的图像数据,并把截图数据进行压缩后再发送到指定的监视主机中;而在监视主机中,也需要同步运行一个线程,接收从被监视的主机发送来的图像数据的压缩包,解压所获得的压缩包,最终获得截图的图像数据,并绘制到监视主机的屏幕上,从而显示出被监视的屏幕。

也就是将当前被监视的主机屏幕的显示内容捕捉为图像,然后将捕捉的图像数据发送到远程控制主机中,最后远程监视主机接收图像数据并在本机屏幕中显示出。当然,其中还需要应用多线程技术实现自动循环捕获、发送和接收、显示以达到实时更新的应用效果。如下示例图为本项目执行后的某个时刻截图。

远程屏幕监视系统应用很广泛,学校机房的机房管理系统、PC版QQ的远程演示功能、在线教学的远程演示等都属于远程屏幕监视系统。而如果要再进一步实现远程控制,则必须要有监控端与被监控端,而且两端的程序都要保持启动。在监控端的监视窗体上执行鼠标点击事件,并记录鼠标点击的坐标位置及键值,然后再发送到被监控端。被监控端程序接受发送来的鼠标坐标位置及键值等信息,然后再在本地屏幕上模拟同样的点击动作以响应监控端程序的操作。

2、与网络通讯相关的 Java 核心类Server Socket 和Secket类

(1)套接字Socket及java.net.Socket类

网络中双向通讯中的某一端称为一个Socket(套接字是对双向通信中的端点的抽象),但在Java系统中分为客户端套接字java.net.Socket类和服务器端套接字java.net.ServerSocket类。Socket通讯的基本原理如下:Socket可以看成在两个程序进行通讯连接中的一个端点,一个程序将一段信息写入Socket中,该Socket将这段信息发送给另外一个Socket,从而也就使得这段信息能传送到其它程序中;而基于Socket通讯的主要过程包括:Socket的建立、监听、连接、发送数据和接收数据等环节。

由于服务器端的程序可能会对外提供多个不同类型的服务,如何标识和区分不同的服务?在生活中的服务机构中一般是通过不同的窗口号加以区分,而在网络通讯中的服务器系统则以不同的 端口号 加以标识和区分。因此,网络通讯中的端口号主要是区分服务类别(服务器端所提供的功能)。

在Java平台下的网络通讯系统中的端口号反映在程序中是一个16位无符号整数,数值范围是0-65535。低于256的端口号保留给标准的应用程序服务所使用,比如WWW服务的端口号为80,telnet服务的端口号为21,ftp服务的端口号为23,smtp服务的端口号为25,pop3服务的端口号为110等。

(2)java.net.ServerSocket类

在客户/服务器通信模式中,服务器端需要创建监听特定端口的ServerSocket类的对象实例,ServerSocket类的对象实例主要负责监听并接收客户端的连接请求。监听则必须要有一个目标,这主要是通过端口来区分。但如果ServerSocket类的对象实例所监听的端口已经被其它的服务器程序的进程所占用,从而导致无法绑定到此端口,此时将会抛出IOException异常。

因此,在服务器端程序中首先要创建出监听特定端口的ServerSocket对象(参看如下示例代码),从而使得ServerSocket能够负责接收客户端的连接请求。

 ServerSocket serverSocket = null;
int monitoredPortNo = 3721;
serverSocket = new ServerSocket(monitoredPortNo);  

在服务器端所接收到的每个客户机的连接请求都会存储在一个先进先出的队列中,而这个队列的容量是有限的。只有当服务器端的程序进程通过ServerSocket类中的accept()方法从队列中取出某个Socket连接请求后,存储连接请求的队列才能继续加入新的客户端的Socket连接请求。前面的程序代码示例所创建的ServerSocket类的对象实例相关的队列长度为系统默认的长度,可以采用如下的示例代码创建指定队列长度(也就是服务器可以接受请求的客户端Socket的最大数量)的ServerSocket类的对象实例。

 ServerSocket serverSocket = null;
int monitoredPortNo = 3721;
int maxQueueLength =100;
serverSocket = new ServerSocket(monitoredPortNo, maxQueueLength);  

3、在服务器端程序中循环监听客户端请求连接的状况

服务器端进程成功绑定待监听的某个端口后,由于客户端的连接请求是随时都可能产生的,服务器端程序则必须要一直不断地查询是否产生了连接请求。因此,在程序设计方面就需要应用一个while循环语句不断地执行ServerSocket.accept()方法(参看如下代码示例),该方法将从连接请求的队列中取出成功连接到服务器的Socket对象。然后再应用Socket.getInetAddress()方法可以查询获得成功连接到服务器的客户端连接地址以识别客户;当然,如果此时的连接请求队列中没有新的客户产生的连接请求,accept()方法将需要再次查询,从而进入一直监听等待的状况,直到接收到了新的连接请求才再次返回。

ServerSocket类的accept()方法从连接请求队列中取出成功连接到服务器的某个客户的连接请求,然后再创建出与客户连接的Socket对象,并将它返回。每当某个客户机成功连接到服务器程序后,就为该客户创建一个线程,同时将代表该客户的Socket传入到该线程中,由该线程负责该客户端的所有交互行为。这样的编程实现机制将使得服务器端程序能够产生“一对多”的应用效果——也就是一个服务器端程序可以同时响应多个不同的客户端程序的请求和服务交互。工作原理参看如下示例图所示。

4、服务器程序需要指定待绑定的端口和所在的主机IP地址

在应用ServerSocke类构建出ServerSocke类的对象实例时,如果没有指定目标IP地址(此时意味着服务器主机只有一个IP地址),则在默认的情况下,服务器程序就与服务器程序所在的主机的唯一IP地址绑定(参看前面的示例代码)。但如果服务器主机安装有两个网卡,从而使得同一个服务器主机出现有多个不同的IP地址的应用状况。此时,在创建ServerSocke类的对象实例时需要指定待绑定的端口所在的IP地址(参看如下代码示例)。

 ServerSocket serverSocket = null;
int monitoredPortNo = 3721;
int maxClientNumber = 50,
String serverIPAddress = "192.168.0.1"
serverSocket = new ServerSocket (serverIPAddress , monitoredPortNo);
serverSocket = new ServerSocket(monitoredPortNo, maxClientNumber, InetAddress.getByName(serverIPAddress));  

5、如何获得服务器主机及客户机的IP地址

在网络通讯中的每一台主机必须要加以区分,为此需要采用一种唯一的标识,这是通过IP地址(Internet Protocol Address,互联网协议地址)加以实现。IP地址可以识别网络中的各个主机,IP地址是一个逻辑地址,它必须要具有唯一性。在Java程序中如何获得服务器主机及客户机的IP地址?这可以通过编程和应用InetAddress和Socket类加以实现。

在Java程序中是通过应用InetAddress类的对象实例表示IP地址,但由于InetAddress类没有提供public类型的构造方法,因此不能直接应用new 操作符创建出InetAddress类的对象实例,而必须通过InetAddress类中所提供的静态方法获取其对象实例。比如InetAddress类的getByName()方法可以根据所给定的主机名称获得对应的IP地址。如下示例图所示的程序代码是获得某个Web服务器的IP地址。

当然,也可以依据某个IP地址获得对应的 主机名 称。但由于创建InetAddress对象实例的方式不同,通过InetAddress类中的getHostName()方法返回的结果值是不同的。如下示例图所示的程序代码说明了三种不同的方式下它们之间的差别。

在第一种方式下,由于InetAddress对象是用getLocalHost方法创建的,因此getHostName方法返回的却是本机名而不是远程主机名;在第二种方式下,用域名作为getByName方法的参数返回InetAddress对象实例后,系统会自动记住这个域名。当再通过调用getHostName方法时,就无需再访问DNS服务器,而是直接将这个域名返回;而在第三种方式使用IP地址创建InetAddress对象时,则会用DNS反向解析来找到对应的域名/机器名。但如果安全检查不允许通过IP查询对应的域名的映射,程序会一直尝试查询, 大概10秒的时间,并且在这10秒期间是阻塞的(好像程序死机的感觉)。getHostName方法最终就直接返回这个IP地址,而不是对应的主机名。

而如果在网络通讯过程中,服务器端程序希望获得正在连接的客户机的IP地址或者客户端程序希望查询目标服务器主机的IP地址,则可以通过Socket类中的相关方法加以实现。

1)通过调用Socket类中的getInetAddress()方法可以获得 远程主机 (对方主机)的IP地址、getPort()方法可以获得远程主机的端口号。但要注意的是,如果是在服务器端的程序中调用getInetAddress()方法将获得客户机的IP地址,而在客户机程序中调用getInetAddress()方法将获得是服务器主机的IP地址。

2)通过调用Socket类中的getLocalAddress()方法可以获得 本机 的IP地址(自己的IP地址)、getLocalPort()方法可以获得本机的端口号。因此,在服务器端程序中调用getLocalAddress()方法将获得服务器主机的IP地址,而在客户机程序中调用getLocalAddress()方法将获得客户机自身的IP地址。

6、正确地编程循环监听客户端连接的相关程序并合理地处理SocketException异常

Java的基于TCP的网络编程非常简单化,不再需要开发人员考虑底层的网络TCP/IP协议等复杂的编程实现细节。只需要按照通过 Socket获得输入输出流对象,再用标准的 I/O 流的数据读写进行数据传送和接受。在Java程序中实现Socket通讯的编程步骤如下:

首先,根据程序的类型不同在服务器端程序中创建出ServerSocket类对象实例、而在客户端程序中直接创建出Socket类的对象实例并联通目标服务器。

然后双方利用Socket类中的getInputStream()、getOutputStream()等方法获得对应的输入输出流对象实例,从而可以相互发送和接收信息(类似于文件读写的过程)。

再其次,服务器端和客户端程序根据应用的业务处理规则,双方读写数据(输入输出)从而最终实现相互发送和接收信息。

最后,在双方的程序中都需要及时地先关闭基于Socket的IO流对象实例,再关闭Socket类的对象以释放所占用的系统资源。

但在服务器端程序与客户机程序处于通讯的数据传输过程中,比如如果服务器在某个时候正在向某个客户机发送数据,而此时的客户端如果断开了Socket连接(比如直接关闭客户机程序等情况),那么服务器端程序就会抛出一个IOException类的子类SocketException类型的异常,错误信息为(参看如下示例图所示):java.net.SocketException: Connection reset by peer: socket write error

在服务器端程序中必须要捕获这个异常,因为这个异常的产生只是说明服务器与某个客户机程序在通信过程中出现了异常,而不应该让这个异常导致服务器主机程序直接退出,从而影响到服务器与其它客户机的通信过程也被动地终止;此外,一旦出现了这样的异常,需要及时地将无用的客户端的Socket对象销毁,以释放所占用的系统资源。

因此,在服务器端程序中需要正确地编程循环监听客户端连接的程序以合理地处理SocketException类型的异常,如下示例图所示为相关的程序代码。

在程序中由于正确地识别了IOException类型的异常产生的原因,从而也就可以准确地显示出对应的错误提示信息,提醒和指导用户的进一步操作过程(参看如下示例图所示的执行结果)。当然,也可以屏蔽不显示出原始的异常信息而只给出更明确的错误提示信息。

7、避免服务器程序多次绑定某个端口或者端口已经被占用所造成的异常抛出

当Socket服务器程序已经执行,目标服务也已经启动后,如果此时再次启动执行服务器端程序,将会由于需要应用服务器所绑定的端口,但此时已经被在内存中执行的服务器程序所占用而抛出如下示例图所示的BindException类型的异常,其该异常所对应的错误信息为:java.net.BindException: Address already in use: JVM_Bind。

解决此问题的主要思路是如果能够在服务器端程序启动之前先测试所要绑定的端口是否已经被占用了,如果此时的端口已经被占用,则意味着服务器程序已经启动,此时就不再需要再次启动服务器,而是直接退出当前的进程。实现此功能的示例程序代码参看如下示例图所示的程序代码。

该段程序代码所反映出的测试原理主要是基于如下的规则:如果可以正常地创建出Socket对象实例,则意味着指定端口上已经启动了服务,也就意味着可以证明主机上的此目标端口已经被使用(但并非是此Socket所使用的);反之则意味着证明这个端口并没有由程序使用。

8、应用java.awt.Robot类中的相关方法实现屏幕截图

java.awt 程序包中的Robot类主要用于为测试自动化、自运行演示程序和其它需要控制鼠标和键盘的应用程序生成本机系统的输入事件,从而可以在程序中实现自动操作鼠标和键盘。提供Robot类的主要目的是便于在Java系统平台中实现对程序的自动化测试,而创建Robot类的对象实例也很简单,就像创建普通Java程序类的对象实例一样。

首先生成一个Robot类的对象实例(参看如下的代码示例),通过这个对象实例调用Robot类中的相关方法来操作和控制程序运行时的主机键盘和鼠标、包括截取屏幕的图像等功能。

Robot currentRobot = new Robot();

其次,通过如下示例所示的两个方法可以模拟实现按下一个键盘的按键的应用效果。而其中的int keycode参数代表所要按键的键码值。但为什么每次需要调用两个方法才模拟实现按下一个键盘的按键?因为在用户平时按下一个按键时,而对于键盘来说则其实涉及两次按键行为:一次是按下这个按键(也就是将用户的手指按下不动),第二次是抬起这个按键(也就是将用户的手指抬起)。

 currentRobot.keyPress(int keycode);
currentRobot.keyRelease(int keycode);  

如下的示例代码则表示模拟按下键盘的Alt + Tab键,从而模拟实现切换当前的窗口。

 currentRobot.keyPress(KeyEvent.VK_ALT);
currentRobot.keyPress(KeyEvent.VK_TAB);
currentRobot.keyRelease(KeyEvent.VK_TAB);
currentRobot.keyRelease(KeyEvent.VK_ALT);  

此外,在Robot类中还提供有截取屏幕图像的方法createScreenCapture,该方法的定义如下:public BufferedImage createScreenCapture(Rectangle screenRect); 其中的参数screenRect代表需要截取的屏幕的区域。如果需要截取主机的整个屏幕,则可以通过Toolkit.getDefaultToolkit().getScreenSize()获得当前屏幕的尺寸,然后再进行截图。程序示例请参看如下的代码或者如下示例图中所示的程序代码:

 Rectangle screenRectangle = new Rectangle(Toolkit.getDefaultToolkit().getScreenSize());
BufferedImage oneBufferedImage = currentRobot.createScreenCapture(screenRectangle);  

因此,在被监视的主机中每隔一段时间就自动截图产生出对应的图像数据,并把截图数据进行压缩后再发送到指定的监视主机中。在监视主机中只需要获得此截图的图像数据,然后将此截图的图像数据转换回指定格式的图片,最后再屏幕中显示。

9、实现BufferedImage所包装的图像数据和byte类型的数组之间相互转换

(1)java.awt.Image类和java.awt.image.BufferedImage类

在Java语言中对图像的抽象描述是由Image类承担,但由于Image类是抽象类,在实际应用中一般都是通过编程其子类BufferedImage。BufferedImage代表某个图像(或者图片)在内存缓冲区中的数据,通过BufferedImage类的的对象实例可以实现对映射到内存缓冲区中的数据进行操作,从而最终实现了对应图像(或者图片)的操作——比如,获得绘图对象、对图像缩放、选择图像平滑度、对图像进行变换、图片变灰处理、设置透明度等。

(2)BufferedImage由于是非 序列化 类,在网络上不能直接传送其对象实例

在网络传输中,图片(图像)格式的数据是不能直接传送的。因此需要把某种图片格式的数据转变为字节数组,也就是将包装图像(或者图片)的BufferedImage类的对象实例转换为byte类型的数组。为此,需要新建一个ByteArrayOutputStream类型的字节输出流对象实例,然后再应用ImageIO.write()方法将包装图像(或者图片)的BufferedImage类的对象实例输出到此ByteArrayOutputStream类型的字节输出流对象实例中,最后将图片(图像)格式的数据转换为字节数组。参看如下的示例代码或者如下示例图所示的程序代码。

 ByteArrayOutputStream oneByteArrayOutputStream = null;
oneByteArrayOutputStream = new ByteArrayOutputStream();
ImageIO.write(oneBufferedImage, "jpg", oneByteArrayOutputStream);
byte[] imageBytesArray = oneByteArrayOutputStream.toByteArray();  

而在接收图像的客户程序中可以采用如下示例所示的程序代码根据所获得的字节数组,重新构建出BufferedImage类的对象实例。然后就可以在图像的接收端对图像根据业务的需要进行处理。实现的过程及相关的程序代码如下:首先,从基于Socket的对象输入流中反序列化出所接收到的信息对象,从而获得包装图像的字节数组(由imageByteArrayFromServer变量定义)。

 byte[] imageByteArrayFromServer = null;
ObjectInputStream socketObjectInputStream = new ObjectInputStream(socketGZIPInputStream);
MessageInfoPO oneMessageInfoPO = (MessageInfoPO)socketObjectInputStream.readObject();
imageByteArrayFromServer =oneMessageInfoPO.getImageBytesArray();  

其次,再根据所获得的包装图像的字节数组,将它转换为基于字节数据的输入流,然后再从字节数据输入流中获得包装图像的BufferedImage类的对象实例(参看如下示例程序代码或者参看如下示例图所示的程序代码)。

 ByteArrayInputStream oneByteArrayInputStream = new ByteArrayInputStream(imageByteArrayFromServer);
oneBufferedImage = ImageIO.read(oneByteArrayInputStream);  

10、对传输的可序列化对象进行压缩以提高数据在网络传输中的效率

GZip格式的数据压缩是目前在IT界广泛应用的一种数据压缩方式,它具有很高的压缩比和压缩效率。在JDK的系统库中也包含有java.util.zip程序包提供对GZip格式的数据压缩和解压缩的技术支持,从而可以在Java程序中很方便地应用GZip格式的数据压缩和解压缩技术。这主要是应用java.util.zip.GZIPInputStream和java.util.zip.GZIPOutputStream两个流类加以实现。

GZIPInputStream流类的构造方法的定义如下:public GZIPInputStream(InputStream in) throws IOException,因此只需要为GZIPInputStream流类的构造方法传递一个输入流,就可以创建出GZIPInputStream流类的对象实例。

而GZIPOutputStream 流类的构造方法的定义如下:public GZIPOutputStream(OutputStream out) throws IOException,同样也只需要为GZIPOutputStream 流类的构造方法传递一个输出流,就可以创建出GZIPOutputStream流类的对象实例。

在Socket网络通讯中,为了提高数据在网络上的传输效率,可以对需要传输的数据(包括可序列化的对象)应用GZIPInputStream和GZIPOutputStream两个流类实现GZip格式的数据压缩和解压缩。比如在远程屏幕监视的程序中由于需要达到实时监视,因此需要考虑对图像数据传输的效率问题,有必要应用数据压缩技术。

在本项目中,首先,对截图的图像采用JPG格式的图像进行传输。因为JPG格式是一种支持高度压缩技术的图片格式,它所存储的信息不包含透明度,同等质量的状况下相对来说其数据容量要比png、gif等格式的图片容量小;其次,再应用GZip格式的数据压缩和解压缩技术对需要传输的JPG格式的图像数据进行压缩,在接收端再解压缩出对应的图像数据,以进一步提高网络中数据的传输效率。

在本项目的服务器端程序中,直接从Socket对象实例中获得对应的OutputStream输出流类的对象实例,然后再将此输出流类的对象实例包装转换为GZIPOutputStream输出流类的对象实例(参看如下示例图所示的程序代码),从而可以实现对截图的图像采用GZip格式进行数据压缩。

而在本项目的客户端程序中,同样从Socket对象实例中获得对应的InputStream输入流类的对象实例,然后再将此输入流类的对象实例包装转换为GZIPInputStream输入流类的对象实例(参看如下示例图所示的程序代码),从而可以实现从输入流中获得截图的图像数据,并采用GZip格式进行数据的解压缩最终获得对应的图像数据。

11、检测服务器端程序是否已经执行,只在启动了服务器后客户端才能连接服务器

客户端程序要想与远程的服务器进行Socket连接,首先是远程的服务器必须要正常执行、对应的服务已经启动,否则客户端是无法连接服务器的,而且还会抛出如下示例图所示的ConnectException类型的异常。因此,客户端程序启动或者连接服务器之前,有必要判断远程服务器目前是否已经正常启动了。如果服务器目前还没有启动,则客户端程序也就没必要连接服务器程序。

在java.net程序包中提供有InetSocketAddress类,该类实现了可序列化Serializable接口并直接继承自java.net.SocketAddress类。InetSocketAddress类不仅可以实现对IP 套接字地址(IP 地址 + 端口号)的封装,也还可以实现对主机名套接字(主机名 + 端口号)的封装。因此,在程序中可以应用“IP 地址 + 端口号”或者“主机名 + 端口号”的参数方式创建出InetSocketAddress类的对象实例。

InetSocketAddress类与InetAddress类在应用方面存在有如下的不同之处:InetAddress类只实现了对IP 地址和主机名的封装,不包括端口号。在本项目的客户端程序中为了能够检测服务器是否已经启动,可以应用InetSocketAddress类的对象实例所包装的服务器IP地址和端口号尝试连接服务器,然后再测试连接是否成功,从而可以检测出服务器目前是否已经启动。实现此功能的程序代码示例可以参看如下示例图所示的程序代码。

客户端程序在启动时首先识别服务器是否已经启动,如果此时的服务器没有启动,则提示出相关的信息,并直接退出进程——参看如下示例图所示。

12、在客户机中监视服务器主机是否已经关闭

在网络通讯过程中可能会出现各种各样的问题,比如服务器可能由于某种原因而导致宕机或者服务已经退出或关闭等状况,而此时的客户程序可能还需要发送信息或者尝试再次连接服务器,但此时肯定会出现如下示例图所示的SocketException类型的异常(参看如下示例图所示)。

因此,有必要检测服务器目前是否还处于正常提供服务状况或者识别服务器主机是否已经关闭等。功能实现的程序代码参看如下示例图所示的程序代码。

13、客户程序如何识别服务器是否仍处于活动状况或者Socket连接是否已经断开

客户端程序在发送信息之前,需要判断远端服务器目前是否已经断开了Socket连接,如果断开了Socket连接,则需要重新连接服务器以保证信息发送的正确性。尽管Socket类中提供有isClosed ()、isConnected ()、isOutputShutdown ()包括isInputShutdown等方法,但这些方法都是反映本地端的状态(也就是说在客户端程序中通过Socket对象调用这些方法的返回状态结果都是客户端程序自己的状态),而无法判断远端服务器是否已经处于断开Socket连接的状态。

但Socket类中提供有sendUrgentData()方法,该方法实现向输出流发送一个字节的数据(尽管该方法的参数为int类型的4个字节,但只有低8位是有效的),只要对方Socket对象的SO_OOBINLINE属性没有打开(通过 public void setOOBInline(boolean on) throws SocketException 方法可以设置该属性的状态),就会自动舍弃这个字节,而SO_OOBINLINE属性在默认情况下是关闭的。代码示例如下或者参看如下示例图所示的程序代码。

 try{
currentClientSocket.sendUrgentData(0xff);
}catch(IOException e){
System.out.println("由于出现了IOException异常,可能是"+ currentClientSocket.getInetAddress().getHostAddress()+"的客户机异常退出。原始异常信息如下:");
e.printStackTrace();
break;
}
  

文章来源:智云一二三科技

文章标题:Java课程设计项目实例《远程屏幕分享监视》第1部分

文章地址:https://www.zhihuclub.com/172639.shtml

关于作者: 智云科技

热门文章

网站地图