docs: protected VirtualConnection read(long numBytes, TCPReadCompletedCallback userCallback, boolean forceQueue, int timeout, boolean fromQueue) {
          if (TraceComponent.isAnyTracingEnabled() && tc.isEntryEnabled()) {
              Tr.entry(tc, "readAsynch, numBytes=" + numBytes + ", fromQueue=" + fromQueue + ", vc=" + getVCHash());
          }
  
          // Access the read interface of the device side channel.
          if (deviceReadContext == null) {
              deviceReadContext = getConnLink().getDeviceReadInterface();
          }
  
          // Handle timing out of former read request.
          if (timeout == IMMED_TIMEOUT || timeout == ABORT_TIMEOUT) {
              if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
                  Tr.debug(tc, "Requested to timeout former request.  Calling device side.");
              }
  
              if (userCallback != null) {
                  //If this is called to timeout a previous request to read the original callback should be used
                  //The original callback would have been set on the first call to the read
                  readCallback.setCallBack(userCallback);
              }
  
              deviceReadContext.read(numBytes, readCallback, forceQueue, timeout);
              if (TraceComponent.isAnyTracingEnabled() && tc.isEntryEnabled()) {
                  Tr.exit(tc, "readAsynch: " + getVC());
              }
              return getVC();
          }
  
          // Reset some object variables.
          this.decryptedNetBufferReleaseRequired = false;
          this.callerRequiredAllocation = false;
  
          // Look for errors in the request.
          IOException exceptionInRequest = checkRequest(numBytes, true);
          if (exceptionInRequest != null) {
              handleAsyncError(forceQueue, exceptionInRequest, userCallback);
              // Return null indicating that the callback will handle the response.
              if (TraceComponent.isAnyTracingEnabled() && tc.isEntryEnabled()) {
                  Tr.exit(tc, "readAsynch: null");
              }
              return null;
          }
  
          callback = userCallback;
          VirtualConnection vc = null;
          // Track the number of bytes requested.
          bytesRequested = numBytes;
          // First handle any left over data from a the previous read request.
          bytesProduced = readUnconsumedDecData();
          long bytesToRead = bytesRequested - bytesProduced;
          boolean requestSatisfied = false;
          if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
              Tr.debug(tc, "Data left over from previous request: " + bytesProduced);
          }
  
          // Was enough data read to satisfy the request?
          if (bytesRequested > bytesProduced || bytesRequested == 0L) {
              // Left over data didn't exist or was not to satisfy the request.
              // Access the read interface of the device side channel.
              if (deviceReadContext == null) {
                  deviceReadContext = getConnLink().getDeviceReadInterface();
              }
  
              // If bytesProduced > 0, then some date was left over in unconsumedDecData but it
              // wasn't enough. The data is currently between pos and lim, but now we need to
              // treat it like data that was already decrypted by the JSSE, pushing the data
              // behind the position.
              if (bytesProduced > 0L) {
                  SSLUtils.positionToLimit(decryptedNetBuffers);
                  // While we're at it, maximize the room for future reads.
                  SSLUtils.limitToCapacity(decryptedNetBuffers);
                  if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
                      Tr.debug(tc, "Adjusted left over decNetBuffers: "
                                   + SSLUtils.getBufferTraceInfo(decryptedNetBuffers));
                  }
              }
  
              // See if there is data in a device side network buffer already.
              // This can happen when a handshake occurs and data is left in the buffer encrypted.
              WsByteBuffer deviceBuffer = deviceReadContext.getBuffer();
              if (null != deviceBuffer && 0 != deviceBuffer.position() && 0 != deviceBuffer.remaining()) {
                  // Use this buffer. Note, we have the responsibility to free it in close().
                  if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
                      Tr.debug(tc, "Found data in existing network buffer, "
                                   + SSLUtils.getBufferTraceInfo(deviceBuffer));
                  }
                  this.netBuffer = deviceBuffer;
                  // Don't mess with this netBuffer until entering decryptMessage. Position is not zero.
                  vc = getConnLink().getVirtualConnection();
              } else {
                  // Allocate/reuse the buffer to be read into. Data remaining from a previous,
                  // unconsumed read, will be put in these buffers before return. Note the size
                  // provided is a guess. bytesToRead is decNet data (decrypted). This buffer
                  // is for net data (encrypted).
                  if (bytesRequested == 0L) {
                      bytesToRead = 0L;
                  } else {
                      bytesToRead = bytesRequested - bytesProduced;
                  }
                  getNetworkBuffer(bytesToRead);
  
                  if (TraceComponent.isAnyTracingEnabled() && tc.isEventEnabled()) {
                      Tr.event(tc, "do async read of : " + bytesToRead + " bytes");
                  }
                  readCallback.setCallBack(userCallback);
                  if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
                      Tr.debug(tc, "Calling device side read with netBuffers, "
                                   + SSLUtils.getBufferTraceInfo(this.netBuffer));
                  }
                  vc = deviceReadContext.read(bytesToRead, readCallback, forceQueue, timeout);
                  // This buffer needs to be flipped before entering decryptMessage.
                  if (vc != null) {
                      this.netBuffer.limit(this.netBuffer.position());
                      this.netBuffer.position(netBufferMark);
                  }
              }
          } else {
              // Left over data was enough to satisfy the request.
              if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
                  Tr.debug(tc, "Left over data was enough to satisfy the request.");
              }
              vc = getConnLink().getVirtualConnection();
              requestSatisfied = true;
              prepareDataForNextChannel();
              // Don't mess with this buffer until entering decryptMessage. Position is not zero.
          }
  
          // Determine if the callback will handle the result.
          if (!requestSatisfied && vc != null) {
              if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
                  Tr.debug(tc, "Data is ready, no callback necessary.");
              }
              Exception exception = null;
              boolean bCont;
              do {
                  bCont = false;
                  exception = decryptMessage(true);
                  if (exception == null) {
                      prepareDataForNextChannel();
                  } else if (exception instanceof ReadNeededInternalException) {
                      if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
                          Tr.debug(tc, "More data needs to be read. vc=" + getVCHash());
                      }
                      getNetworkBuffer(1);
                      readCallback.setCallBack(userCallback);
                      vc = deviceReadContext.read(1, readCallback, forceQueue, timeout);
                      // This buffer needs to be flipped before entering decryptMessage.
                      if (vc != null) {
                          if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
                              Tr.debug(tc, "Read done. No callback necessary, buffers "
                                           + SSLUtils.getBufferTraceInfo(this.netBuffer));
                          }
                          this.netBuffer.limit(this.netBuffer.position());
                          this.netBuffer.position(netBufferMark);
                          bCont = true;
                      }
                  } else if (exception instanceof SessionClosedException) {
                      if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
                          Tr.debug(tc, "SSL Session has been closed.");
                      }
                      handleAsyncError(forceQueue,
                                       new IOException("SSL connection closed by peer"), userCallback);
                      vc = null;
                  } else {
                      FFDCFilter.processException(exception, getClass().getName(), "192", this);
                      if (TraceComponent.isAnyTracingEnabled() && tc.isDebugEnabled()) {
                          Tr.debug(tc, "Caught exception during unwrap, " + exception);
                      }
                      IOException ioe = new IOException("SSL decryption failed");
                      ioe.initCause(exception);
                      handleAsyncError(forceQueue, ioe, userCallback);
                      vc = null;
                  }
              } while (bCont);
          }
  
          // If data is ready and
          // the request came from a callback (so the callback must be used to notify of complete) or
          // the request has requested to have its response on a separate thread (forceQueue == true)
          if ((vc != null) && (fromQueue || forceQueue)) {
              handleAsyncComplete(forceQueue, callback);
              // Mark that the response was given to the channel above already by setting vc to null.
              vc = null;
          }
  
          if (TraceComponent.isAnyTracingEnabled() && tc.isEntryEnabled()) {
              Tr.exit(tc, "readAsynch: " + vc);
          }
          return vc;
      }