一 同屏软件原理
客户端请求服务端,服务端截屏(如果需要录音,也可用第三方库调用录音设备,获取声音数据),转成字节数组,通过网络传输给客户端,客户端将原始的画面数据还原,展示在组件中(awt或swing组件都可以)。
服务端和客户端之间的多次传输、就可以将画面在可控的时间间隔内一张张的展示出来。形成一种屏幕"连续"展示效果。
具体而言可以有如下几种实现方式。
1 长连接
即客户端请求了服务端之后,从此不断开(除非异常需要重新发送连接请求之外),服务端保存客户端的连接(list.add(socket))。不释放(除非客户端异常需要移除无用连接,添加新连接之外)。
2 短连接。客户端和服务端一站式请求,即一次请求一次响应。此后断开连接。
两种方式各有利弊。
利 | 弊 | |
长连接 | 不需要每次都发送连接请求。避免了三次握手的额外开销。 | ①服务端必须使用容器来保存客户端连接。这个容器可以是数组、List、Map等。增加内存消耗。 ②多客户端请求,意味着多线程。必须使用额外的代码保证多线程对连接的容器的安全访问。 ③考虑到客户端有可能频繁断开连接,服务端必须增加额外的代码(可以是单独的管理线程)来剔除那些已经断开了的、或者已经发生了异常了的客户端连接。 |
短连接 | 不会出现长连接的所有弊端。 | 没有了长链接的优势。意味着每次都要发送连接请求服务端。客户端开销也比较大。 |
本项目应用场景是在公司内部搭建局域网(路由器 + 交换机)开会时使用。所以不需要传输声音。用了几个月,效果还可以,既不会出现卡顿。内存也还在本人可接受范围内。其实压力最大的是服务端。几个人的话,在200M左右。十几个人连接。高峰期在600-700M。吃内存大户。挺吓人。
二 代码实现
本案例使用短连接实现。
客户端、服务端、屏幕工具类、压缩工具类。一共四个文件
1 客户端代码 com.sharescreen.Client.java
package com.sharescreen;import java.awt.Image; import java.awt.Rectangle;import java.awt.event.MouseAdapter;import java.awt.event.MouseEvent;import java.io.InputStream;import java.net.Socket;import java.util.zip.ZipInputStream;import javax.imageio.ImageIO;import javax.swing.ImageIcon;import javax.swing.JButton;import javax.swing.JFrame;import javax.swing.JLabel;import javax.swing.JPanel;import javax.swing.JTextField;import com.utils.ScreenUtils; public class Client { private static JLabel jl; private static String IP=null; private static int PORT=-1; private static JFrame jf; private static JPanel loginpanel; public static void main(String[] args) throws Exception{//先写再读 //先读再写 initFrame(); connect(); } public static void connect() throws InterruptedException{ while(PORT ==-1|| IP ==null){ Thread.sleep(10); } jf.remove(loginpanel);//移除登录框。 while(true){ Socket c = null; try { c = new Socket(IP,PORT); InputStream in = c.getInputStream(); ZipInputStream zin = new ZipInputStream(in); zin.getNextEntry(); Image img = ImageIO.read( zin ); jl.setIcon(new ImageIcon(img)); Thread.sleep(400);//客户端延时。可调。 } catch (Exception e) { e.printStackTrace(); } } } public static void initFrame(){ jf = new JFrame(); jf.setUndecorated(true); jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); jf.setBounds(new Rectangle(ScreenUtils.getScreensize())); JPanel jp = new JPanel(); jl = new JLabel(); jp.add(jl); jl.setBounds(new Rectangle(ScreenUtils.getScreensize())); jf.getContentPane().add(jp); loginpanel = new JPanel(); jf.add(loginpanel,"North"); loginpanel.add(new JLabel("IP")); JTextField ipinput = new JTextField(10); loginpanel.add(ipinput); loginpanel.add(new JLabel("端口")); JTextField portinput = new JTextField(10); loginpanel.add(portinput); JButton loginbtn = new JButton("登录"); loginpanel.add(loginbtn); jf.getContentPane().add(loginpanel,"North"); jf.setVisible(true); loginbtn.addMouseListener(new MouseAdapter() { public void mouseClicked(MouseEvent e) { String ip = ipinput.getText(); String port = portinput.getText(); //未添加校验。 IP = ip; PORT = Integer.parseInt( port ); } }); }}
2 服务端代码com.sharescreen.Server.java
package com.sharescreen;import java.awt.image.BufferedImage;import java.io.OutputStream;import java.net.ServerSocket;import java.net.Socket;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.zip.ZipEntry;import java.util.zip.ZipOutputStream;import javax.imageio.ImageIO;import com.utils.ScreenUtils;/** * 优化手段 : 加入线程池+压缩 */public class Server { public static void main(String[] args) throws Exception { ServerSocket s = new ServerSocket(8088); //创建一个带缓冲的线程池。 //Executors.newCachedThreadPool(); //通过比较发现,我的机子上。用固定线程池性能更好。 ExecutorService pools = Executors.newFixedThreadPool(10); while (true) { Socket c = s.accept();// 接收客户端连接。 System.out.println(c.getInetAddress().getHostAddress()); pools.submit(new MyTask(c));//将任务加入线程池。 c = null; } } private static class MyTask implements Runnable{ private Socket c; public MyTask(Socket c) { this.c = c; } @Override public void run() { try { OutputStream out = c.getOutputStream(); ZipOutputStream zout = new ZipOutputStream(out); zout.putNextEntry(new ZipEntry("test.jpg"));//指定入口。必须的。 zout.setLevel(5);//设置压缩等级。0-9 BufferedImage buf = ScreenUtils.getFullScreen(); ImageIO.write(buf, "jpg", zout); zout.closeEntry(); c.shutdownOutput(); //帮助垃圾回收期回收。 out = null; zout = null; c = null; buf = null; }catch (Exception e) { e.printStackTrace(); } } }}
3 屏幕工具类 com.utils.ScreenUtils.java
package com.utils;import java.awt.AWTException;import java.awt.Dimension;import java.awt.Rectangle;import java.awt.Robot;import java.awt.Toolkit;import java.awt.image.BufferedImage;public class ScreenUtils { private final static Dimension screensize=Toolkit.getDefaultToolkit().getScreenSize(); private static Robot robot = null; static{ try { robot = new Robot(); } catch (AWTException e) { e.printStackTrace(); } } public static BufferedImage getFullScreen(){ BufferedImage buf = robot.createScreenCapture(new Rectangle(screensize)); return buf; } public static Dimension getScreensize() { return screensize; } }
4 压缩工具类 com.utils.ZipUtils.java
package com.utils;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.IOException;import java.util.zip.GZIPInputStream;import java.util.zip.GZIPOutputStream; public class ZipUtils { // 压缩 public static byte[] zip(byte[] bs) throws IOException { if (bs == null || bs.length == 0) { return null; } try( ByteArrayOutputStream out = new ByteArrayOutputStream(); GZIPOutputStream gzip = new GZIPOutputStream(out); ){ gzip.write(bs); gzip.close(); return out.toByteArray(); }catch (Exception e) { } return null; } // 解压缩 public static byte[] unzip(byte[] bs) throws IOException { if (bs == null || bs.length == 0) { return bs; } ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayInputStream in = new ByteArrayInputStream(bs); GZIPInputStream gunzip = new GZIPInputStream(in); byte[] readby = new byte[100*1024]; int readnum; while (true) { readnum = gunzip.read(readby); if(readnum==-1)break; out.write(readby, 0, readnum); } gunzip.close(); in.close(); return out.toByteArray(); }}