Java Socket开发遇坑小记

351 查看

前言

最近接手了一个老项目,涉及到Java Socket编程,之前做Web开发,很少接触网络编程,
在维护这个项目的时候,遇到了一些问题。
刚学java的时候学过socket,不过,长时间不用基本都忘记了,所以,自己最近在解决问
题的同时,也把socket和io的知识重新梳理了一下。把项目中遇到的问题,用小例子举例
说明一下。

问题及分析

比如,在开发中,我们可能需要完成这样的功能,向服务器端发送这样的报文:

  <info>
    <name>网络编程好</name>      
    <phone>110110110</phone>  
  </info>  

在老代码中,我遇到了这样的服务端代码(大意):

ServerSocket ss = new ServerSocket(8081);
Socket socket = ss.accept();
InputStream in = socket.getInputStream();

byte[] bytes = new byte[60];
int len = in.read(bytes);
String recStr = new String(bytes,"GBK");
System.out.println(recStr);   
socket.close();
ss.close();

问题1 报文读取不全

这样读取网络数据有什么问题呢?
我们来写一个这样的客户端来发送报文:

Socket socket = new Socket("127.0.0.1",8081);
OutputStream os = socket.getOutputStream();
String str = "<info>" +
                "<name>网络编程好</name>" +
                "<phone>110110110</phone>" +
            "</info>";
byte[] bytes = str.getBytes("GBK");
os.write(bytes,0,20);
os.flush();
Thread.sleep(50);
os.write(bytes,20,bytes.length-20);
os.flush();

socket.close();

运行后,只会收到前半部分报文。

<info><name>网络编程  

问题就出在读取报文的这段代码上:

int len = in.read(bytes);

这种读取网络流的方式,就是错误的。读取socket和读取文件都应该是循环的去读,
如果返回-1才结束。于是,改成了下面的方式

ServerSocket ss = new ServerSocket(8081);
Socket socket = ss.accept();
InputStream in = socket.getInputStream();

byte[] tmp = new byte[60];
int len;
StringBuffer sb = new StringBuffer();
while((len=in.read(tmp)) != -1){
    sb.append(new String(tmp,0,len,"GBK"));
}

String recStr = sb.toString();
System.out.println(recStr);
socket.close();
ss.close();

问题2 字节流截断导致乱码

改用了读取方式后,自己却不小心给自己挖了个坑。调整一下客户端的代码,模拟一下问题。

Socket socket = new Socket("127.0.0.1",8081);
OutputStream os = socket.getOutputStream();
String str = "<info>" +
        "<name>网络编程好</name>" +
        "<phone>110110110</phone>" +
        "</info>";
byte[] bytes = str.getBytes("GBK");
os.write(bytes,0,21);
os.flush();
Thread.sleep(50);
os.write(bytes,21,bytes.length-21);
os.flush();

socket.close();

注意,代码调整了分包发送的大小。运行结果:

<info><name>网络编程��/name><phone>110110110</phone></info>

出现了乱码,虽然解决了网络流读取的问题,但是却挖了一个编码的坑。注意,这里转换成字符
串有问题,那是因为中文转换成字节流被截断,再转回来的时候就出问题了。所以,正确的方式
是读取所有的字节流之后再进行转换。

ServerSocket ss = new ServerSocket(8081);
Socket socket = ss.accept();
InputStream in = socket.getInputStream();

byte[] tmp = new byte[60];
byte[] bytes = new byte[512];
int len;
int pos = 0;
while((len=in.read(tmp)) != -1){
   System.arraycopy(tmp,0,bytes,pos,len);
   pos+=len;
}

String recStr = new String(bytes,"GBK");
System.out.println(recStr);
socket.close();
ss.close();

这个时候,定义了两个缓冲区,读取到的数据最终放到大的缓冲区,最后进行转换。

问题3 多大的缓冲合适

大的缓冲区定义多大合适呢?很多时候,网络数据来的时候大小不定,缓冲区定义太大浪费空间,
太小就放不下数据,更好的方式是使用内存流。

ServerSocket ss = new ServerSocket(8081);
Socket socket = ss.accept();
InputStream in = socket.getInputStream();

byte[] tmp = new byte[60];
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int len;
while((len=in.read(tmp)) != -1){
   baos.write(tmp,0,len);
}

byte[] bytes = baos.toByteArray();

String recStr = new String(bytes,"GBK");
System.out.println(recStr);
socket.close();
ss.close();  

问题4 是否需要在socket关闭前关闭输入输出流

之前看到有到的资料,有的说要关闭,但是看到有的书却没有关闭,于是查了一下java文档:

public void close() throws IOException
Closes this socket.

Any thread currently blocked in an I/O operation upon this socket will throw a SocketException.
Once a socket has been closed, it is not available for further networking use (i.e. can't be reconnected or rebound). A new socket needs to be created.
Closing this socket will also close the socket's InputStream and OutputStream.
If this socket has an associated channel then the channel is closed as well.

其实,socket的close方法已经关闭了输入输出流,所以,只要调用它就好了。

总结

Java socket编程的细节还是蛮多的,所以在开发的时候一定不能大意,对于每行代码都要清楚,
特别要注意编码问题。对编码不清楚的同学,一定要去了解一下字符集,字符编码以及Java默认的字符编码。