SocketOutputStream和SocketChannel write方法的区别和底层实现

Java直接内存原理提到了SocketChannel#write的实现原理。
通过IOUtil#write将java堆内存拷贝到了直接内存,然后再把地址传给了I/O函数。
那么 BIO 是怎么实现往socket里面写数据的呢?

BIO

Socket#getOutputStream()获得SocketOutputStream
三个write方法最后都会调用native方法SocketOutputStream#socketWrite0

SocketOutputStream.c#Java_java_net_SocketOutputStream_socketWrite0方法

/*
 * Class:     java_net_SocketOutputStream
 * Method:    socketWrite
 * Signature: (Ljava/io/FileDescriptor;[BII)V
 */
JNIEXPORT void JNICALL
Java_java_net_SocketOutputStream_socketWrite0(JNIEnv *env, jobject this,
                                              jobject fdObj, jbyteArray data,
                                              jint off, jint len) {
    char *bufP; //中间临时缓冲区
    char BUF[MAX_BUFFER_LEN]; //如果堆栈可以存下,直接使用堆栈内存
    int buflen; //缓冲区大小,就是需要发送的数据大小
    int fd;

    /* 省略,入参校验*/

    /*
     * 尽可能使用堆栈分配缓冲区。
     * 对于large sizes,我们从堆中分配一个中间缓冲区(达到最大值)。
     * 如果堆不可用,我们只使用的堆栈缓冲区。
     */
    if (len <= MAX_BUFFER_LEN) {
        bufP = BUF;
        buflen = MAX_BUFFER_LEN;
    } else {
        buflen = min(MAX_HEAP_BUFFER_LEN, len);
        bufP = (char *)malloc((size_t)buflen);
        if (bufP == NULL) {
            bufP = BUF;
            buflen = MAX_BUFFER_LEN;
        }
    }

    while(len > 0) {
        int loff = 0;
        int chunkLen = min(buflen, len);
        int llen = chunkLen;
        int retry = 0;
        /*
         * 这个方法复制了java数组到native堆中!!!
         */
        (*env)->GetByteArrayRegion(env, data, off, chunkLen, (jbyte *)bufP);
		/*
         * 由于Windows套接字中的错误(在NT和Windows 2000上观察到),可能需要重试发送。
         */
        while(llen > 0) {
        	/* 循环调用发送*/
            int n = send(fd, bufP + loff, llen, 0);
            if (n > 0) {
                llen -= n;
                loff += n;
                continue;
            }

            /*
             * 由于Windows套接字中的错误(在NT和Windows 2000上观察到),可能需要重试发送。
             */
            if (WSAGetLastError() == WSAENOBUFS) {
                /* 省略,失败重试机制*/
            }

            /*
             * 发送失败 - 可能由关闭或写入错误引起。
             */
            if (WSAGetLastError() == WSAENOTSOCK) {
                JNU_ThrowByName(env, JNU_JAVANETPKG "SocketException", "Socket closed");
            } else {
                NET_ThrowCurrent(env, "socket write error");
            }
            /* 释放临时缓冲区内存*/
            if (bufP != BUF) {
                free(bufP);
            }
            return;
        }
        len -= chunkLen;
        off += chunkLen;
    }
	/* 释放临时缓冲区内存*/
    if (bufP != BUF) {
        free(bufP);
    }
}

jni.cpp宏定义

#define DEFINE_GETSCALARARRAYREGION(ElementTag,ElementType,Result, Tag) \
  DT_VOID_RETURN_MARK_DECL(Get##Result##ArrayRegion);\
\
JNI_ENTRY(void, \
jni_Get##Result##ArrayRegion(JNIEnv *env, ElementType##Array array, jsize start, \
             jsize len, ElementType *buf)) \
  /* 省略,动态判断应该去调用byte、int等哪个方法;还有一些动态追踪的逻辑?*/
  typeArrayOop src = typeArrayOop(JNIHandles::resolve_non_null(array)); \
  if (start < 0 || len < 0 || ((unsigned int)start + (unsigned int)len > (unsigned int)src->length())) { \
    THROW(vmSymbols::java_lang_ArrayIndexOutOfBoundsException()); \
  } else { \
    if (len > 0) { \
      int sc = TypeArrayKlass::cast(src->klass())->log2_element_size(); \
      /* 内存拷贝*/
      memcpy((u_char*) buf, \
             (u_char*) src->Tag##_at_addr(start), \
             len << sc);                          \
    } \
  } \
JNI_END

所以除了直接使用ByteBuffer#allocateDirect分配堆外内存之外,不管是BIO和NIO都需要将java堆内存拷贝到native堆(堆外内存)。
当然都不能避免从native堆拷贝到socket buffer(SO_RCVBUF和SO_SNDBUF)。

猜你喜欢

转载自blog.csdn.net/zxcc1314/article/details/89035111
今日推荐