_send(emsgOrHeader, body, callback) {
		// header fields: msg, proto, sourceJobID, targetJobID
		let header = typeof emsgOrHeader === 'object' ? emsgOrHeader : {msg: emsgOrHeader};
		let emsg = header.msg;

		let canWeSend = this.steamID ||
			(
				this._tempSteamID &&
				[EMsg.ChannelEncryptResponse, EMsg.ClientLogon, EMsg.ClientHello, EMsg.ServiceMethodCallFromClientNonAuthed].includes(emsg)
			);
		if (!canWeSend) {
			// We're disconnected, drop it
			this.emit('debug', 'Dropping outgoing message ' + emsg + ' because we\'re not logged on.');
			return;
		}

		const Proto = protobufs[emsg];
		if (Proto) {
			header.proto = header.proto || {};
			body = SteamUserMessages._encodeProto(Proto, body);
		} else if (ByteBuffer.isByteBuffer(body)) {
			body = body.toBuffer();
		}

		let jobIdSource = null;
		if (callback) {
			jobIdSource = ++this._currentJobID;
			this._jobs.add(jobIdSource.toString(), callback);
		}

		let emsgName = EMsg[emsg] || emsg;
		if ([EMsg.ServiceMethodCallFromClient, EMsg.ServiceMethodCallFromClientNonAuthed].includes(emsg) && header.proto && header.proto.target_job_name) {
			emsgName = header.proto.target_job_name;
		}

		this.emit(VERBOSE_EMSG_LIST.includes(emsg) ? 'debug-verbose' : 'debug', 'Sending message: ' + emsgName);

		// Make the header
		let hdrBuf;
		if (header.msg == EMsg.ChannelEncryptResponse) {
			// since we're setting up the encrypted channel, we use this very minimal header
			hdrBuf = ByteBuffer.allocate(4 + 8 + 8, ByteBuffer.LITTLE_ENDIAN);
			hdrBuf.writeUint32(header.msg);
			hdrBuf.writeUint64(header.targetJobID || JOBID_NONE);
			hdrBuf.writeUint64(jobIdSource || header.sourceJobID || JOBID_NONE);
		} else if (header.proto) {
			// if we have a protobuf header, use that
			let shouldIncludeSessionId = ![EMsg.ClientHello, EMsg.ServiceMethodCallFromClientNonAuthed].includes(header.msg);
			header.proto.client_sessionid = shouldIncludeSessionId ? (this._sessionID || 0) : 0;
			header.proto.steamid = shouldIncludeSessionId ? (this.steamID || this._tempSteamID).getSteamID64() : '0';
			header.proto.jobid_source = jobIdSource || header.proto.jobid_source || header.sourceJobID || JOBID_NONE;
			header.proto.jobid_target = header.proto.jobid_target || header.targetJobID || JOBID_NONE;
			let hdrProtoBuf = SteamUserMessages._encodeProto(Schema.CMsgProtoBufHeader, header.proto);
			hdrBuf = ByteBuffer.allocate(4 + 4 + hdrProtoBuf.length, ByteBuffer.LITTLE_ENDIAN);
			hdrBuf.writeUint32(header.msg | PROTO_MASK);
			hdrBuf.writeUint32(hdrProtoBuf.length);
			hdrBuf.append(hdrProtoBuf);
		} else {
			// this is the standard non-protobuf extended header
			hdrBuf = ByteBuffer.allocate(4 + 1 + 2 + 8 + 8 + 1 + 8 + 4, ByteBuffer.LITTLE_ENDIAN);
			hdrBuf.writeUint32(header.msg);
			hdrBuf.writeByte(36);
			hdrBuf.writeUint16(2);
			hdrBuf.writeUint64(header.targetJobID || JOBID_NONE);
			hdrBuf.writeUint64(jobIdSource || header.sourceJobID || JOBID_NONE);
			hdrBuf.writeByte(239);
			hdrBuf.writeUint64((this.steamID || this._tempSteamID).getSteamID64());
			hdrBuf.writeUint32(this._sessionID || 0);
		}

		let outputBuffer = Buffer.concat([hdrBuf.flip().toBuffer(), body]);
		this.emit('debug-traffic-outgoing', outputBuffer, header.msg);
		this._connection.send(outputBuffer);
	}
@param {int|object} emsgOrHeader
@param {object|Buffer|ByteBuffer} body
@param {function} [callback]
@protected