网络通信常见错误产生原因及解决方案
如今,很多软件为实现某些功能,都会涉及到网络通信的应用(像是这次的远程桌面控制),而在通信的过程中,是会出现各种各样错误的。这些错误虽然形式不同,但其产生原因大都涵盖在以下十种中。
一、IP或者端口错误
java.net.SocketException: Network is unreachable: connect
产生原因:建立连接时使用了错误的IP地址或者端口。
解决方案:在建立连接对象时,明确服务器的IP地址以及通信软件占用的端口。这种属于人为的错误。
二、保持连接时,不同的循环方式的使用
代码实例:服务器接受客户端连接.
public void SetupServer(int port) { try { //创建服务器 java.net.ServerSocket server = new java.net.ServerSocket(port); System.out.println("服务器创建成功!!");
while (true) { java.net.Socket client = server.accept(); clientAddr = client.getRemoteSocketAddress().toString(); System.out.println("有客户机连接"+clientAddr); ServerThread st=new ServerThread(client); st.start(); }
} catch (Exception ef) { ef.printStackTrace(); System.out.println("服务器创建失败!!"); } } |
以上是一段正确的代码,再看下面这段错误代码。
public void SetupServer(int port) { try { //创建服务器 java.net.ServerSocket server = new java.net.ServerSocket(port); System.out.println("服务器创建成功!!"); java.net.Socket client = server.accept(); clientAddr = client.getRemoteSocketAddress().toString(); System.out.println("有客户机连接"+clientAddr);
while (true) { ServerThread st=new ServerThread(client); st.start(); }
} catch (Exception ef) { ef.printStackTrace(); System.out.println("服务器创建失败!!"); } } |
我们发现这两段代码的区别在于while(true){}循环体的不同。两者不同的循环方式导致了不同的结果。
第一段代码能理想的运行、能允许多个客户端连接;第二段代码会导致系统堆溢出、而且只允许一个客户端连接进入。
产生原因:对连接流程理解的偏差。
解决方案:弄清楚服务器端在创建连接对象时的正确流程。按我们的设想,是在每一个客户端连接进入后,都要创建一个连接对象,并启动一个线程去处理这个连接对象。所以,对一段代码就是正确的。
三、协议不相符
这种错误可以说是最常见的错误了,而这种错误一旦出现,将会使整个通信过程变得毫无意义。
简单的说,网络通信就是服务器、客户端对 “字节流”的处理操作,“字节流”是在输入输出流对象中读取或写入的。而网络通信的关键就是通信协议的制定,服务器、客户端对“字节流”处理的规则就是通信协议。
可以想象,如果服务器或客户端没有按照协议“办事”(如:少读取了几个字节),那在下次读取的时候,必然会再次犯错,然后就这样一直犯错下去。也就造成了“失之毫厘谬以千里”的后果,整个通信也就没有意义了。
代码实例:
服务器发送一张截屏图片的代码:
if (null != imageData) { // 1 代表这条消息是一张图片 dous.writeByte(1); // 图片字节的长度 dous.writeInt(imageData.length); // 发送图片数据 dous.write(imageData); dous.flush();
count++; System.out.println("第 "+count+" 张图片发送完毕!!"); |
客户端读取一张截屏图片的代码:
while (true) { byte type = dins.readByte();// 得到消息类型 switch (type) { case 1:// 接收到图片信息 { int len = dins.readInt();// 图片数据的长度 byte[] data = new byte[len];
// 读取字节数组 for (int i = 0; i < len; i++) { data[i] = dins.readByte(); }
count++; System.out.println("第 " + count + " 张图片读取完毕,\r\n长度是: " + len); } |
可以看出这个流程是:服务器先发送一个byte代表发送消息的类型(1代表是截屏图片),再发送一个int代表截屏图片的长度,最后发送截屏图片的字节数组。 而在客户端也是先读取一个byte的得到消息的类型(1代表是截屏图片),再读取一个int得到截屏图片的长度,最后再循环读取截屏图片的字节数组。这就是按照协议正确的处理操作。
发生场景:如果在客户端读到一个byte后,直接就循环读取截屏图片的字节数组,那么就会造成4个字节的偏差。而遗漏的4个字节又会在下次读取时被读到,这样显然得不到正确的结果。
产生原因:服务器或客户端违背了通信协议。
解决方案:要明确的制定好通信协议,并保证服务器、客户端能严格遵守。
四、read(byte a[])和readFully(byte a[])的区别
因为网络在传送数据时会做一些处理,使服务器发出的消息不是作为一个整体被发送出去的。而客户端在读取时,若采用这两种不同的读取方法,就会有不同的结果。
产生原因:用read(byte a[])读取,可能会造成实际读取到的字节数并没有a.length那么多,从而造成错误。
解决方案:1.可以用循环读取a.length个字节。2.用readFully(byte a[])。
五、异常内存错误
OutOfMemoryError
代码实例:服务器接受客户端连接.
public void SetupServer(int port) { try { //创建服务器 java.net.ServerSocket server = new java.net.ServerSocket(port); System.out.println("服务器创建成功!!"); java.net.Socket client = server.accept(); clientAddr = client.getRemoteSocketAddress().toString(); System.out.println("有客户机连接"+clientAddr);
while (true) { ServerThread st=new ServerThread(client); st.start(); }
} catch (Exception ef) { ef.printStackTrace(); System.out.println("服务器创建失败!!"); } } |
产生原因:死循环、或者在不停地创建对象。
解决方案:在while(true){}中,谨慎的创建对象。再者,尽量避免“死循环”。
六、EOFException
产生原因:要读取n个字节,输入流中传来了m个字节,n>m。产生这种异常,主要还是在协议的制定上出了问题,发送的字节数和要接受的字节数不等。
解决方案:弄清楚通信流程,制定好协议。
七、断开
客户端和服务器的连接终归是要断开的,而断开分为意料中的和意料之外的(像是网线断了)。
产生原因:客户端断开,服务器还在发送或等待接收消息。
解决方案:意料中的比较容易避免,可以在客户端准备退出时,先发一条消息通知服务器,让服务器断开连接,停止工作。而意料之外的就比较麻烦了,因为不清楚连接是如何断开的。
八、try-catch 放的位置不同
throws异常 还是用try-catch处理异常。
产生原因:try-catch放的位置不同,得到的是不同的异常,如果放错了地方,就会产生错误。
解决方案:分析清楚特定代码会抛出什么异常,在catch里正确的打印出来。
九、性能问题(不必要的对象的创建,初始化)
代码实例:服务器端截取屏幕图片的方法
public static byte[] printScreen(){ try{ java.awt.Robot robot=new java.awt.Robot(); Rectangle rt=new Rectangle(0,0,1440,900); //取得截屏图片 BufferedImage image= robot.createScreenCapture(rt);
//改变全屏截图的大小 AffineTransformOp op = new AffineTransformOp(AffineTransform .getScaleInstance(1.0/2.0, 1.0/2.0), null); image = op.filter(image, null);
//转为字节数组 byte[] imageGet=imageToBytes(image); return imageGet; }catch(Exception ef){ ef.printStackTrace(); System.out.println("截取屏幕图片失败!!"); } return null; } |
因为这个方法会被频繁的调用,而每次调用都要创建一个Robot和Rectangle对象,毫无疑问,这会降低程序的运行效率。
产生原因:创建了很多不必要的对象,或是不必要的初始化,降低了程序的运行效率。
解决方案:尽量少的创建对象和初始化对象。
十、对程序的一点改进
代码示例:
//将截屏图片转换成字节数组的方法 private static byte[] imageToBytes(BufferedImage image) { // BufferedImage bImage = new BufferedImage(image.getWidth(null), image // .getHeight(null), BufferedImage.TYPE_INT_ARGB); Graphics bg = image.getGraphics(); bg.drawImage(image, 0, 0, null); bg.dispose();
ByteArrayOutputStream out = new ByteArrayOutputStream(); try { ImageIO.write(image, "jpeg", out); } catch (IOException e) { e.printStackTrace(); } return out.toByteArray(); } |
这个方法是在服务器发送截屏图片时将截屏图片转换成字节数组,也会被频繁的调用。而之前传入的是一个Image对象,之后还要创建一个BufferedImage对象,这会降低程序的运行效率。
因为BufferedImage继承了Image,所以可以传入一个BufferedImage对象来解决上述问题。