Java SocketChannel控制接收数据字节长度及复用ByteBuffer

引言

SocketChannelread时,要求传入一个ByteBuffer,如果发送方发送的数据结构每次不是一个整体,且>每次接收缓冲区大小,那么此时我们从byteBuffer中读取数据就变的异常麻烦,我们得控制indexoffset,好让我们每次读取的数据按照发送方的结构接收。

混乱的数据结构

Java中我们不能像C/C++一样,通过recv指定接收的字节数

举个栗子:
发送方又发了一个int类型的数据,假定为文件名长度
发送方继续又发了一个字符串,为文件名
发送方发了一个int类型的数据,假定为文件Size
发送方继续发送文件内容

此时如果我们ByteBuffer大小为2048字节,我们使用SocketChannel#read(ByteBuffer),数据就会流入ByteBuffer中,而我们本意是文件内容通过sendfile或MapViewOfFile写到磁盘,但我们现在变成了从ByteBuffer先取一个int,再取字符串,再取一个int,如果后面还有,那么说明是文件内容,此时无论如何我们得先把ByteBuffer尾部的一部分文件内容取出来写到磁盘,再用TransferFrom接收剩余的,变的异常麻烦。

尝试读取固定大小数据

SocketChannel#read(ByteBuffer)读取时,ByteBuffer分配的是多少字节,那么读取不会超过这个buffer的大小,我们变成了这样

ByteBuffer intBuffer = ByteBuffer.allocate(4);
channel.read(intBuffer);
final int len = intBuffer.getInt();

ByteBuffer szBuffer = ByteBuffer.allocate(len);
channel.read(byteBuffer);
final byte[] strByte = new byte[len];
byteBuffer.get(strByte,0,len);
final String filename = new String(strByte);

intBuffer.clear();
channel.read(intBuffer);
final int fileSize = intBuffer.getInt();

 try(final FileOutputStream file = new FileOutputStream(pathName, false)){
     file.getChannel().transferFrom(channel, 0, fileSize);
 }

由于对方发送的数据,文件名在中间,导致我们为了接收固定数据,不得不重复申请文件名长度的szBuffer,这种方式无法复用szBuffer,我们必须不停的对szBuffer申请空间,然后丢弃它,因为字符串长度是无法确定的。

如何复用ByteBuffer

如何复用ByteBuffer呢,其实很简单,无论对方传的结构有多么混乱和诡异,记得之前说过的吗?

SocketChannel#read(ByteBuffer)读取时,ByteBuffer分配的是多少字节,那么读取不会超过这个buffer的大小

原因在于检查ByteBufferposition和limit。只要我们每次接收前clear后,通过limit限制其可用大小,那么无论对方发送的结构多复杂,我们都能只使用一个ByteBuffer接收。

文件传输Buffer复用及减少使用用户层缓冲区

源码发现FileChannel#transferFromsrcSocketChannel时是通过ByteBuffer(8K)接收,接收完成后ByteBuffer丢弃, srcFileChannel时是内存映射(每次最大8M)

只有FileChannel#transferTo发送时才会使用sendfile(Linux)零拷贝WindowsMappedByteBuffer(windows是MapViewOfFile)
WindowsCreateFile应该没有使用FILE_FLAG_NO_BUFFERING|FILE_FLAG_WRITE_THROUGH,因为NATIVE映射时并没有映射为扇区倍数,所以Windows只是少了一层用户态的缓冲区调用。

我们可以先封装出几个方法:
下面的方法是针对接收C/C++发送方的,所以关于小端转换和GBK编码可以忽略。

	private final int MAP_THRESHOLD = 4 * 1024 * 1024;

    private ThreadLocal<ByteBuffer> byteBufferThreadLocal = new ThreadLocal(){
        @Override
        protected ByteBuffer initialValue() {
            return ByteBuffer.allocateDirect(MAP_THRESHOLD);
        }
    };

    /**
     * Little-Endian 字节数组转int
     * @param byteVal 待转换的数组
     * @return 小端转换后的int
     */
    private int bytes2Int(byte[] byteVal) {
        if (byteVal==null || byteVal.length != 4) return 0;
        /*
        * 由于java没有unsigned,导致单byte>127时出现负数,左移会是符号扩展方式,所以需要&0xff进行零扩展转int后进行移位
        * */
        return byteVal[0] & 0xff | ((byteVal[1] & 0xff) << 8) | ((byteVal[2] & 0xff) << 16) | ((byteVal[3] & 0xff) << 24);
    }

    private byte[] getIpV4Bytes(String ipOrMask)
    {
        try{
            String[] addr = ipOrMask.split("\\.");
            int length = addr.length;
            byte[] addrBytes = new byte[length];
            for (int index = 0; index < length; index++)
            {
                addrBytes[index] = (byte) Integer.parseInt(addr[index]);
            }
            return addrBytes;
        }catch (Exception ignored){}
        return new byte[4];
    }

    /**
     * IPV4 str转int,大端转换
     * @param ipOrMask IP字符串
     * @return int 没有unsigned也无所谓,符号位参与与运算
     */
    private int getIpV4ToInt(String ipOrMask)
    {
        byte[] addr = getIpV4Bytes(ipOrMask);
        return addr[3] & 0xff | ((addr[2] & 0xff) << 8) | ((addr[1] & 0xff) << 16) | ((addr[0] & 0xff) << 24);
    }

    /**
     * 从channel中接收指定长度的数据
     * @param channel SocketChannel
     * @param byteBuffer ByteBuffer
     * @param len int
     * @throws IOException
     */
    private void recvFromBuffer(SocketChannel channel, ByteBuffer byteBuffer, int len) throws IOException,InterruptedException {
        if (len<=0) throw new IOException();
        int allReadyRead;
        int rateLimit = 1;
        while ((allReadyRead = channel.read(byteBuffer)) != -1) {
            if (allReadyRead == 0) {
                // 如果没有数据,则稍微等待一下
                Thread.sleep((rateLimit <<= 1) & 0xff);
                if (byteBuffer.position() == len)
                    break;
            }
        }
        if (allReadyRead==-1) throw new IOException();
        byteBuffer.flip();
    }

    /**
     * 接收4个byte,通过bytes2Int转换为int
     * @param byteBuffer ByteBuffer
     * @return bytes2Int转换后的int
     * @throws IOException
     * @see AbsFileTransServer#bytes2Int(byte[])
     */
    int intTransferFromChannel(SocketChannel channel, ByteBuffer byteBuffer) throws IOException, InterruptedException {
        byteBuffer.clear();
        byteBuffer.limit(4);
        recvFromBuffer(channel,byteBuffer,4);
        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
        return byteBuffer.getInt();
    }

    /**
     * 通过channel接收len长度的字节数组,之后将字符数组转为String
     * @param byteBuffer ByteBuffer
     * @param len 字符数组长度
     * @return 转转后的字符串
     * @throws IOException
     */
    String strTransferFromChannel(SocketChannel channel, ByteBuffer byteBuffer, int len) throws IOException, InterruptedException {
        byteBuffer.clear();
        byteBuffer.limit(len);
        recvFromBuffer(channel,byteBuffer,len);
        final byte[] strByte = new byte[len];
        byteBuffer.get(strByte,0,len);
        return new String(strByte,"gbk");
    }

    /**
     * 通过channel使用byteBuffer接收后传输文件。
     * @param channel SocketChannel
     * @param byteBuffer ByteBuffer
     * @param pathName 路径名
     * @param fileName 文件名
     * @param len 文件字节长度
     * @throws IOException
     */
    boolean fileTransferFromChannel(SocketChannel channel, ByteBuffer byteBuffer, String pathName, String fileName, int len) throws IOException, InterruptedException {
        byteBuffer.clear();
        byteBuffer.limit(len);
        recvFromBuffer(channel,byteBuffer,len);
        try(final RandomAccessFile file = new RandomAccessFile(pathName + fileName,  "rws")){
            return file.getChannel().write(byteBuffer) > 0;
        }
    }

    /**
     * 通过SocketChannel接收
     * @param channel SocketChannel
     * @param pathName 路径名
     * @param fileName 文件名
     * @param len 文件字节长度
     * @throws IOException
     */
    boolean fileTransferFromChannel(SocketChannel channel, String pathName, String fileName, int len) throws IOException, InterruptedException {
        if (len < MAP_THRESHOLD){
            ByteBuffer byteBuffer = byteBufferThreadLocal.get();
            byteBuffer.clear();
            byteBuffer.limit(len);
            return fileTransferFromChannel(channel, byteBuffer, pathName,fileName,len);
        }
        return fileMapTransferFromChannel(channel, pathName, fileName, len);
    }

    /**
     * 对大文件 使用内存映射
     * @param channel SocketChannel
     * @param pathName 路径名
     * @param fileName 文件名
     * @param len 文件字节长度
     * @throws IOException
     */
    final boolean fileMapTransferFromChannel(SocketChannel channel, String pathName, String fileName, int len) throws IOException {
        final File file = new File(pathName + fileName);
        try(final RandomAccessFile randomFile = new RandomAccessFile(file,  "rws")){
            final FileChannel fc = randomFile.getChannel();

            // 如果处于同一子网mmap直接映射到足够大
            final String remoteAddress = channel.getRemoteAddress().toString();
            final String localAddress = channel.getLocalAddress().toString();
            final Pattern pattern = Pattern.compile("\\d+\\.\\d+\\.\\d+\\.\\d+");
            Matcher removeMatcher = pattern.matcher(remoteAddress);
            Matcher localMatcher = pattern.matcher(localAddress);
            if (removeMatcher.find() && localMatcher.find()){
                int remotePoint = getIpV4ToInt(remoteAddress.substring(removeMatcher.start(), removeMatcher.end()));
                int localPoint = getIpV4ToInt(localAddress.substring(localMatcher.start(), localMatcher.end()));
                int mask = getIpV4ToInt("255.255.255.0");
                if ((mask & remotePoint) == (mask & localPoint)){
                    MappedByteBuffer map = fc.map(FileChannel.MapMode.READ_WRITE, 0, len);
                    int position = 0;
                    while( position < len){
                        position += channel.read(map);
                    }
                    //unmap 如果不unmap会导致文件内核对象无法关闭,文件一直被占用
                    Cleaner cleaner = ((DirectBuffer)map).cleaner();
                    if (cleaner != null) {
                        cleaner.clean();
                    }
                    return file.setLastModified(System.currentTimeMillis());
                }
            }

            // 否则每次映射 Math.min(len-position, MAP_THRESHOLD);
            int position = 0;
            while(position<len){
                int size = Math.min(len-position, MAP_THRESHOLD);
                MappedByteBuffer map = fc.map(FileChannel.MapMode.READ_WRITE, position, size);
                position += channel.read(map);

                //unmap 如果不unmap会导致文件内核对象无法关闭,文件一直被占用
                Cleaner cleaner = ((DirectBuffer)map).cleaner();
                if (cleaner != null) {
                    cleaner.clean();
                }
            }
            return file.setLastModified(System.currentTimeMillis());
        }
    }

使用上面的方法也很容易:

/*
申请一个tempBuffer,它对于当前线程将会一直存在,我们并没有将它放在方法栈中,并且IO多路复用的情况下我们只需要针对线程使用一个tempBuffer
由于这个buffer只是用来接收很小的数据结构,文件内容我们通过零拷贝传输
所以此时Direct方式并不适用,使用allocate分配256个字节就足够了
原因在文件名最大包含路径也无法超过260字节。
*/

final ByteBuffer tempBuffer = ByteBuffer.allocate(256);
 //方法中调用上面封装好的几个方法
 final int len = intTransferFromChannel(clientChannel,tempBuffer);

 final String szFileName = strTransferFromChannel(clientChannel,tempBuffer,len);

 final int fileSize = intTransferFromChannel(clientChannel,tempBuffer);

fileTransferFromChannel(clientChannel,filePath,szFileName,fileSize);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

没事干写博客玩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值