半步多 玄玉V笔记

Socket网络编程中的1448问题

2013-07-27
玄玉

介绍

Socket开发网络应用时,有时会遇到这种情况:byte[]收到的字节中,前1448个是有值的,从1449开始竟然都是0x00

这是因为 InputStream().read() 每次最多只能获得1448个字节的数据,所以就要考虑到多次接收数据

详见本文 MyServer.java 里面的实现:readLen = in.read(datas, dataLen-needDataLen, needDataLen);

另外针对输入流读取字节时,补充两个需要注意的细节

  1. java.io.InputStream.read(byte[] b, int off, int len)
    它并不是一定会读取 len 个字节,它只保证最少读一个字节,最多读 len 个字节
    详见本文 MyServer.java 里面的实现:readLen += in.read(buffer, readLen, 6-readLen);
  2. java.io.InputStream.available()
    该方法返回此输入流下一个方法调用可以不受阻塞地从此输入流读取(或跳过)的估计字节数
    它没有保证返回的是绝对的字节数,且有的 InputStream 实现返回的就不是流中的字节总数,故不能对其new byte[count]
    另外注意:用这个方法从本地文件读取数据时,一般不会遇到问题,但在网络操作时,就很有可能发生非预期的结果
    比如说对方发来了1000个字节,但是 available() 却只得到了600、或者200个、甚至0个
    这是因为网络通讯是间断性的,一串字节往往会分几批进行发送,所以 available() 得到的只是估计字节数

举例

下面演示的是:包含了服务端和客户端的完整示例代码

这是服务端的代码实现 MyServer.java

package com.xuanyuv.server;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * Socket网络编程中的1448问题
 * Created by 玄玉<https://www.xuanyuv.com/> on 2013/07/27 12:16
 */
public class MyServer {
    public static void main(String[] args) {
        ServerSocket serverSocket = null;
        //ServerSocket的默认参数空的构造方法用途是,允许服务器在绑定到特定端口之前,先设置一些ServerSocket选项
        //否则一旦服务器与特定端口绑定,有些选项就不能在改变了,比如SO_REUSEADDR选项
        try {
            serverSocket = new ServerSocket();
            //设定允许端口重用,无论Socket还是ServetSocket都要在绑定端口之前设定此属性,否则端口绑定后再设置此属性是徒劳的
            serverSocket.setReuseAddress(true);
            //服务器绑定端口
            serverSocket.bind(new InetSocketAddress(8060));
            //从连接请求队列中(backlog)取出一个客户的连接请求,然后创建与客户连接的Socket对象,并将它返回
            //如果队列中没有连接请求,accept()就会一直等待,直到接收到了连接请求才返回
            //Socket socket = serverSocket.accept();
        } catch (Exception e) {
            System.out.println("服务器启动失败,堆栈轨迹如下:");
            e.printStackTrace();
            //isBound()---判断是否已与一个端口绑定,只要ServerSocket已经与一个端口绑定,即使它已被关闭,isBound()也会返回true
            //isClosed()--判断ServerSocket是否关闭,只有执行了close()方法,isClosed()才返回true
            //isClosed()--否则即使ServerSocket还没有和特定端口绑定,isClosed()也会返回false
            //下面的判断就是要确定一个有引用的,已经与特定端口绑定,并且还没有被关闭的ServerSocket
            if(null!=serverSocket && serverSocket.isBound() && !serverSocket.isClosed()){
                try {
                    //serverSocket.close()可以使服务器释放占用的端口,并断开与所有客户机的连接
                    //当服务器程序运行结束时,即使没有执行serverSocket.close()方法,操作系统也会释放此服务器占用的端口
                    //因此服务器程序并不一定要在结束前执行serverSocket.close()方法
                    //但某些情景下若希望及时释放服务器端口,以便其它程序能够占用该端口,则可显式调用serverSocket.close()
                    serverSocket.close();
                    System.out.println("服务器已关闭");
                } catch (IOException ioe) {
                    //ignore the exception
                }
            }
        }
        System.out.println("服务器启动成功,开始监听" + serverSocket.getLocalSocketAddress());
        //服务器开始监听
        run(serverSocket);
    }

    private static void run(ServerSocket serverSocket){
        while(true){
            Socket socket = null;
            try {
                socket = serverSocket.accept();
                System.out.println("New connection accepted " + socket.getRemoteSocketAddress());
                InputStream in = socket.getInputStream();
                int dataLen = 0;             //报文前六个字节所标识的完整报文长度
                int readLen = 0;             //已成功读取的字节数
                int needDataLen = 0;         //剩余需要读取的报文长度,即报文正文部分的长度
                byte[] buffer = new byte[6]; //假设报文协议为:前6个字节表示报文长度(不足6位左补0),第7个字节开始为报文正文
                while(readLen < 6){
                    readLen += in.read(buffer, readLen, 6-readLen);
                }
                dataLen = Integer.parseInt(new String(buffer));
                System.out.println("dataLen=" + dataLen);

                byte[] datas = new byte[dataLen];
                System.arraycopy(buffer, 0, datas, 0, 6);
                needDataLen = dataLen - 6;
                while(needDataLen > 0){
                    readLen = in.read(datas, dataLen-needDataLen, needDataLen);
                    System.out.println("needDataLen=" + needDataLen + " readLen=" + readLen);
                    needDataLen = needDataLen - readLen;
                }
                System.out.println("Receive request " + new String(datas, "UTF-8"));

                OutputStream out = socket.getOutputStream();
                out.write("The server status is opening".getBytes("UTF-8"));
                //The flush method of OutputStream does nothing
                //out.flush();
            } catch (IOException e) {
                //当服务端正在进行发送数据的操作时,如果客户端断开了连接,那么服务器会抛出一个IOException的子类SocketException异常
                //java.net.SocketException: Connection reset by peer
                //这只是服务器与单个客户端通信时遇到了异常,这种异常应该被捕获,使得服务器能够继续与其它客户端通信
                e.printStackTrace();
            } finally {
                if(null != socket){
                    try {
                        //与一个客户通信结束后要关闭Socket,此时socket的输出流和输入流也都会被关闭
                        //若先后调用shutdownInput()和shutdownOutput()方法,也仅关闭了输入流和输出流,并不等价于调用close()
                        //通信结束后,仍然要调用Socket.close()方法,因为只有该方法才会释放Socket所占用的资源,如占用的本地端口等
                        socket.close();
                    } catch (IOException e) {
                        //ignore the exception
                    }
                }
            }
        }
    }
}

这是客户端的代码实现 MyClient.java

package com.xuanyuv.client;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;

public class MyClient {
    public static void main(String[] args) {
        StringBuilder reqData = new StringBuilder();
        for(int i=0; i<22; i++){
            reqData.append("0003961000510110199201209222240000020120922000069347814303000700000813``中国联通交费充值`为号码18655228826交费充值100.00元`UDP1209222238312219411`10000```20120922`chinaunicom-payFeeOnline`UTF-8`20120922223831`MD5`20120922020103806276`1`02`10000`20120922223954`20120922`BOCO_B2C```http://192.168.20.2:5545/ecpay/pay/elecChnlFrontPayRspBackAction.action`1`立即支付,交易成功`");
        }
        //UTF-8编码的,上面的reqData循环后的字符串长度就是8712,再加上报文协议中表示完整报文长度的前六个字节就是8718
        reqData.insert(0, "008718");
        //socket = SSLSocketFactory.getDefault().createSocket("127.0.0.1", 8060);
        Socket socket = new Socket();
        try {
            socket.setSoTimeout(30000);
            socket.connect(new InetSocketAddress("127.0.0.1", 8060), 1000);
            //发送TCP请求
            OutputStream out = socket.getOutputStream();
            out.write(reqData.toString().getBytes("UTF-8"));
            //接收TCP响应
            InputStream in = socket.getInputStream();
            ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
            byte[] buffer = new byte[512];
            int len = -1;
            while((len=in.read(buffer)) != -1){
                bytesOut.write(buffer, 0, len);
            }
            System.out.println("收到服务器应答=[" + bytesOut.toString("UTF-8") + "]");
        } catch (Exception e) {
            System.out.println("请求通信[127.0.0.1:8060]时偶遇异常,堆栈轨迹如下:");
            e.printStackTrace();
        } finally {
            if(null != socket){
                try {
                    socket.close();
                } catch (IOException e) {
                    //ignore the exception
                }
            }
        }
    }
}

Content