I do not use Ensemble, but I would try using the JSON-Adaptor, something like this

Class MessageB Extends (Ens.Request, %JSON.Adaptor)
{
   Property ClientId As %String(MAXLEN = "");
   Property message As %Stream.TmpBinary;
}

For example

s r=##class(MessageB).%New()
s r.ClientId=12345
d r.message.Write("part1")
d r.message.Write("part2")
w r.%JSONExportToStream(.s)
d s.Rewind()
w s.Read(s.Size) --> {"ClientId":"12345","message":"cGFydDFwYXJ0Mg=="}

Your solution is nearly perfect, here my quick (untested) version.


ClassMethod Encode()
{
	// You read N bytes (which MUST be divisible by 3) and write N*4/3 encoded bytes
	// 3 * 8190 = 24570; 24570 * 4 / 3 = 32760;  32760 < 32768; to avoid (slow) long strings
	set CHUNK=24570
	set NOCR=1	// don't insert CRLF after each 72 written bytes
	set encodedData=##class(%Stream.TmpBinary).%New() // adapt this to your needs: %Stream.Whatever...
	
	set request=##class(%Net.HttpRequest).%New()
	set request.Server="..."
    do request.Get("/...")
    
    if request.HttpResponse.StatusCode = 200 {
    	while 'request.HttpResponse.Data.AtEnd {
	    	do encodedData.Write($system.Encryption.Base64Encode(request.HttpResponse.Data.Read(CHUNK),1))
		}
	}
    QUIT encodedData
    
    // as an alternative, you could return a string or a streamobject
    set YOURMAXSTRING = 32767 // or 3641144
    if encodedData.Size <= YOURMAXSTRING {
	    do encodedData.Rewind()
	    quit encodedData.Read(encodedData.Size)
    } else { quit encodedData }
}

You are mixing two different things...

Property Data1 As list of %String;
Property Data2 As %List;

are two very different things. The first (Data1, equates to your DataObj.Services) is an object while the second one (Data2) is a simple scalar value (in this case a string which in its structure casually matches the inner structure of a $list() respective $listbuild() function).

write $listvalid(oref.Data1) ==> 0 // NOT a list
write $listvalid(oref.Data2) ==> 1 // a VALID list
write $isobject(oref.Data1) ==> 1  // a valid (list)object
write $isobject(oref.Data2) ==> 0  // not a valid (list)object

$listnext() does NOT work on objects (your DataObj.Services) is an object

For a string like "hallo" Cache will use 5+2 = 7 bytes. If that "hallo..." is longer then 253 bytes then length_of_string + 4 bytes will be used and if your "hallo..." is longer then 65535 bytes then length_of_string + 6 bytes will be used.

But there is one more thing, you should know: the sum of the lengths of ALL properties, except the array(like) properties, can't be greater then that famous 3641144 magic number (if you use the standard Cache Storage). Array-like properties are those, which are stored in own nodes.

The documentation of the %ToJSON() method is correct and yes, you can do 

 do obj.%ToJSON()
 

merely, this works only "on devices without protocol" like terminal, (sequential) file, etc. Everywhere, wehere the data bytes goes direct to the target. WebSocket isn't such a device. There is a "header part", with information fields like the command, masking, the length of the data, etc.

You have two possibilities, a) you ask WRC for a "WriteStream()" method or b) you handle the whole WebSocket by hand (is not impossible) or c) you change your application logic and send the those messages in chunks.