目录
Unity HttpClient 之 使用MultipartFormDataContent 发起 内容类型为 multipart/form-data 的数据 Post 请求(正常与流式响应处理)
一、简单介绍
二、实现原理
三、注意事项
四、示例效果
五、示例实现简单步骤
六、关键脚本
Unity 在开发中,网络访问:
可以使用 UnityWebRequest 访问,不过好似只能用协程的方式,并且访问只能在主线程中;
所以这里使用 C# 中的 HttpClient,进行网络访问,而且 HttpClient,允许在子线程中访问,在一些情况下可以大大减少主线程的网络访问压力;
这里使用 HttpClient,进行 Post 访问,发请Content-Type内容类型为 multipart/form-data 的请求,并且Task 结合 async (await) 的进行异步访问,最后使用正常方式以及 Stream 流式的形式获取数据,在这里做一个简单的记录,以便后期使用的时候参考。
示例请求参数如下:
1)Header参数:
参数名 参数值 参数类型 必填 备注 Content-Type
multipart/form-data
String
是
2)Body参数:
参数名 参数类型 备注 是否必填 id
Integer
应用id
必填
conversationId
String
对话id
必填
deviceId
String
设备号Id
必填
text
String
对话内容
必填
file
File
文件
必填
type
String
对话类型 "text":文本, "image":图片
必填
1、HttpClient 创建 Http 客户体
2、MultipartFormDataContent 创建 multipart/form-data 数据体,文件使用注意转为二进制数组(ByteArrayContent)
3、使用 HttpClient.PostAsync 发起请求,HttpResponseMessage 接收结果
4、其中 HttpResponseMessage.Content.ReadAsStringAsync().Result 接收正常的数据结果
5、其中 Stream stream = await HttpResponseMessage.Content.ReadAsStreamAsync()流式获取数据,然后 StreamReader reader = new StreamReader(stream) 和
string line = await reader.ReadLineAsync() 流式读取解析每一行数据
1、根据服务端返回数据的形式,选择合适的读取响应的数据
2、实现形式仅供参考,如有更好的可以留言讨论
1、创建一个 Unity工程,添加一个空物体用来挂载测试脚本
2、添加脚本,实现 HttpClient 发起 multipart/form-data Post 请求,包括异步发起和流式接收响应数据
3、把测试脚本挂载到场景物体上
4、运行场景,可以看到打印信息
1、HttpClientMultipartFormDataContentRequest
using System.IO;
using System;
using UnityEngine;
using System.Collections.Generic;
using System.Net.Http;
///
/// 对话响应结果
///
public class ResponeseResult {
public string ThreadId { get; set; }
public string ChatAnswer { get; set; }
}
///
/// Http Client 发起 Content-Type 内容类型 multipart / form - data 的请求 实例类
///
public class HttpClientMultipartFormDataContentRequest : Singleton, IHttpClientMultipartFormDataContentRequest
{
#region Data
///
/// TAG
///
const string TAG = "[GptChatHttpWebRequest] ";
///
/// 网址 URL_BASE
///
const string URL_BASE = "https://YourHttpURL.com";
///
/// URL_VERSION
///
const string URL_VERSION = "/xrlauncherhwapi/v1";
///
/// URL_TARGET
///
const string URL_TARGET = "/gpts/chat";
///
/// URL
///
string URL = URL_BASE + URL_VERSION + URL_TARGET;
#endregion
#region SendChatRequest
///
/// 发起对话请求
///
///
///
///
///
///
///
///
///
///
///
public ResponeseResult SendChatRequest(int id, string conversationId, string deviceId, string chatAsk, string type, string filepath, string filename,
Action onSuccess, Action onFailed)
{
Dictionary keyValues = DataToDictionary(id, conversationId, deviceId, chatAsk, type);
return PostFormData(URL, keyValues, filepath, filename, onSuccess, onFailed);
}
///
/// 发起对话请求
///
///
///
///
///
///
///
///
///
///
///
public ResponeseResult SendChatRequest(int id, string conversationId, string deviceId, string chatAsk, string type, byte[] fileBinaryData, string filename,
Action onSuccess, Action onFailed)
{
Dictionary keyValues = DataToDictionary(id,conversationId,deviceId,chatAsk,type);
return PostFormData(URL, keyValues, fileBinaryData, filename, onSuccess, onFailed);
}
///
/// Post multipart/form-data 数据
///
/// <服务器地址>
///
/// <文件的本地储存地址:包括文件名>
/// <文件的名字>
///
private ResponeseResult PostFormData(string url, Dictionary dic, string filepath, string filename,
Action onSuccess, Action onFailed)
{
return PostFormData(url, dic, File.ReadAllBytes(filepath), filename, onSuccess, onFailed);
}
///
/// Post multipart/form-data 数据
///
///
///
///
///
///
///
///
private ResponeseResult PostFormData(string url, Dictionary dic, byte[] fileBinaryData, string filename,
Action onSuccess, Action onFailed)
{
Debug.Log(TAG + " PostFormData fileBinaryData.Length " + fileBinaryData.Length);
string str = "";
ResponeseResult chatResponeseResult = null;
try
{
HttpClient client = new HttpClient();
var postContent = new MultipartFormDataContent();
string boundary = string.Format("--{0}", DateTime.Now.Ticks.ToString("x"));
postContent.Headers.Add("ContentType", $"multipart/form-data, boundary={boundary}");
string filekeyname = "file";
postContent.Add(new ByteArrayContent(fileBinaryData), filekeyname, filename);
foreach (var key in dic.Keys)
{
postContent.Add(new StringContent(dic[key].ToString()), key);
}
Debug.Log(TAG + " PostFormData Url " + url);
HttpResponseMessage response = client.PostAsync(url, postContent).Result;
str = response.Content.ReadAsStringAsync().Result;
Debug.Log(TAG + " PostFormData response Content " + str);
chatResponeseResult = HandleResult(str);
onSuccess?.Invoke(chatResponeseResult);
}
catch (Exception ex)
{
Debug.LogError(TAG + "PostJsonData:" + ex.ToString());
onFailed?.Invoke(ex.ToString());
}
return chatResponeseResult;
}
///
/// 拼凑数据结果
///
///
///
private ResponeseResult HandleResult(string content) {
// 将字符串按换行符分割成多行
string[] lines = content.Split("data");
Debug.Log(TAG + " HandleResult lines.Length " + lines.Length);
// 存储结果的列表
List texts = new List();
// 存储结果的列表
List threadIds = new List();
// 遍历每一行,提取包含 "text":"xx" 的行中的文本内容
foreach (string line in lines)
{
if (line.Contains("text"))
{
int startIndex = line.IndexOf("text") + 7; // 找到 "text":" 的位置
int endIndex = line.IndexOf("\"", startIndex); // 找到下一个引号位置
string text = line.Substring(startIndex, endIndex - startIndex);
texts.Add(text);
}
if (line.Contains("thread_id"))
{
int startIndex = line.IndexOf("thread_id") + 12; // 找到 "thread_id":" 的位置
int endIndex = line.IndexOf("\"", startIndex); // 找到下一个引号位置
string threadId = line.Substring(startIndex, endIndex - startIndex);
threadIds.Add(threadId);
}
}
string stringBuilder = "";
// 输出结果
foreach (string text in texts)
{
stringBuilder += (text);
}
Debug.Log(TAG + " result text " + stringBuilder);
return new ResponeseResult() { ThreadId = threadIds.Count > 1 ? threadIds[0] : "", ChatAnswer = string.IsNullOrEmpty(stringBuilder) ? "The server returns empty data" : stringBuilder };
}
#endregion
#region SendChatRequestStream
///
/// 流式处理数据
///
///
///
///
///
///
///
///
///
///
///
public void SendChatRequestStream(int id, string conversationId, string deviceId, string chatAsk, string type, string filepath, string filename,
Action onResponesingGettingData, Action onSuccess, Action onFailed)
{
Dictionary keyValues = DataToDictionary(id, conversationId, deviceId, chatAsk, type);
PostFormDataStreamAsync(URL, keyValues, File.ReadAllBytes(filepath), filename, onResponesingGettingData, onSuccess, onFailed);
}
///
/// 流式处理数据
///
///
///
///
///
///
///
///
///
///
///
///
public void SendChatRequestStream(int id, string conversationId, string deviceId, string chatAsk, string type, byte[] fileBinaryData, string filename,
Action onResponesingGettingData, Action onSuccess, Action onFailed)
{
Dictionary keyValues = DataToDictionary(id, conversationId, deviceId, chatAsk, type);
PostFormDataStreamAsync(URL, keyValues, fileBinaryData, filename, onResponesingGettingData, onSuccess, onFailed);
}
///
/// 异步流式处理数据
///
///
///
///
///
///
///
///
private async void PostFormDataStreamAsync(string url, Dictionary dic, byte[] fileBinaryData, string filename,
Action onResponesingGettingData, Action onSuccess, Action onFailed)
{
Debug.Log(TAG + " PostFormDataStreamAsync fileBinaryData.Length " + fileBinaryData.Length);
try
{
using (HttpClient client = new HttpClient())
using (var postContent = new MultipartFormDataContent())
{
string boundary = string.Format("--{0}", DateTime.Now.Ticks.ToString("x"));
postContent.Headers.Add("ContentType", $"multipart/form-data, boundary={boundary}");
string filekeyname = "file";
postContent.Add(new ByteArrayContent(fileBinaryData), filekeyname, filename);
foreach (var key in dic.Keys)
{
postContent.Add(new StringContent(dic[key].ToString()), key);
Debug.Log(TAG + $" PostFormDataStreamAsync StringContent {key} : {dic[key].ToString()} ");
}
Debug.Log(TAG + " PostFormDataStreamAsync Url " + url);
// 使用异步方法发送请求
using (HttpResponseMessage response = await client.PostAsync(url, postContent))
{
// 检查响应是否成功
response.EnsureSuccessStatusCode();
// 使用异步方法读取响应流
using (Stream stream = await response.Content.ReadAsStreamAsync())
using (StreamReader reader = new StreamReader(stream))
{
ResponeseResult rslt = new ResponeseResult();
// 读取每一行并实时处理
while (!reader.EndOfStream)
{
string line = await reader.ReadLineAsync();
// 处理当前行的数据
ResponeseResult tmp = ProcessResponseLine(line, onResponesingGettingData);
if (tmp != null) { rslt.ThreadId = tmp.ThreadId; rslt.ChatAnswer += tmp.ChatAnswer; }
}
// 处理响应
//chatResponeseResult = HandleResult(str);
onSuccess?.Invoke(rslt);
}
}
}
}
catch (Exception ex)
{
Debug.LogError(TAG + "PostFormDataStreamAsync:" + ex.ToString());
onFailed?.Invoke(ex.ToString());
}
}
///
/// 处理单条数据 (根据自己的数据形式处理)
///
///
///
private ResponeseResult ProcessResponseLine(string line, Action onChatResponesingGettingResult)
{
// 在这里处理每一行的数据
Debug.Log(TAG + " ProcessResponseLine: Response Line: " + line);
ResponeseResult rslt = null;
if (line.Contains("event:close")) { }
// 可以根据实际需求进行处理
if (line.Contains("text"))
{
int startIndex = line.IndexOf("text") + 7; // 找到 "text":" 的位置
int endIndex = line.IndexOf("\"", startIndex); // 找到下一个引号位置
string text = line.Substring(startIndex, endIndex - startIndex);
string threadId = "";
if (line.Contains("thread_id"))
{
int startIndex_S = line.IndexOf("thread_id") + 12; // 找到 "thread_id":" 的位置
int endIndex_S = line.IndexOf("\"", startIndex_S); // 找到下一个引号位置
threadId = line.Substring(startIndex_S, endIndex_S - startIndex_S);
}
Debug.Log(TAG + $" ProcessResponseLine: text {text}, threadId {threadId} ");
rslt = new ResponeseResult() { ThreadId = threadId, ChatAnswer = text };
onChatResponesingGettingResult.Invoke(rslt);
return rslt;
}
return rslt;
}
#endregion
#region Other
///
/// 数据转为字典像是
///
///
///
///
///
///
///
Dictionary DataToDictionary(int id, string conversationId, string deviceId, string chatAsk, string type)
{
Dictionary keyValues = new Dictionary();
keyValues.Add("id", id);//id 是服务器返回的凭证 这里的凭证只是针对这个项目后端人员定义的
keyValues.Add("conversationId", conversationId);//conversationId 这里的凭证只是针对这个项目后端人员定义的
keyValues.Add("deviceId", deviceId);//deviceId 这里的凭证只是针对这个项目后端人员定义的
keyValues.Add("text", chatAsk);//text 这里的凭证只是针对这个项目后端人员定义的
keyValues.Add("type", type);//type 这里的凭证只是针对这个项目后端人员定义的
return keyValues;
}
#endregion
}
2、IHttpClientMultipartFormDataContentRequest
using System;
///
/// Http Client 发起 Content-Type 内容类型 multipart / form - data 的请求 接口类
///
public interface IHttpClientMultipartFormDataContentRequest
{
///
/// 发起对话请求
///
///
///
///
///
///
///
///
///
///
///
ResponeseResult SendChatRequest(int id, string conversationId, string deviceId, string chatAsk, string type, string filepath, string filename, Action onSuccess, Action onFailed);
///
/// 发起对话请求
///
///
///
///
///
///
///
///
///
///
///
ResponeseResult SendChatRequest(int id, string conversationId, string deviceId, string chatAsk, string type, byte[] fileBinaryData, string filename,
Action onSuccess, Action onFailed);
///
/// 流式处理数据
///
///
///
///
///
///
///
///
///
///
///
void SendChatRequestStream(int id, string conversationId, string deviceId, string chatAsk, string type, string filepath, string filename,
Action onResponesingGettingData, Action onSuccess, Action onFailed);
///
/// 流式处理数据
///
///
///
///
///
///
///
///
///
///
///
///
void SendChatRequestStream(int id, string conversationId, string deviceId, string chatAsk, string type, byte[] fileBinaryData, string filename,
Action onResponesingGettingData, Action onSuccess, Action onFailed);
}
3、TestHttpClientMultipartFormDataContentRequest
using UnityEngine;
public class TestHttpClientMultipartFormDataContentRequest : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
//TestNormal();
TestStream();
}
void TestNormal() {
int id = 1;
string conversationId = "";
string deviceId = "abcdef";
string chatAsk = "你平时喜欢做什么";
string type = "text";
string filepath = "E:\\YourPath\\Imgs\\CameraImag.png";
string filename = "CameraImag.png";
HttpClientMultipartFormDataContentRequest.Instance.SendChatRequest(
id, conversationId, deviceId, chatAsk, type, filepath, filename, (rlt) => {
Debug.Log($" TestHttpClientMultipartFormDataContentRequest ThreadId = {rlt.ThreadId}, ChatAnswer = {rlt.ChatAnswer}");
}, (error) => {
Debug.Log($" TestHttpClientMultipartFormDataContentRequest Fail error = " + error);
});
}
void TestStream() {
int id = 1;
string conversationId = "";
string deviceId = "abcdef";
string chatAsk = "你平时喜欢做什么";
string type = "text";
string filepath = "E:\\YourPath\\Imgs\\CameraImag.png";
string filename = "CameraImag.png";
HttpClientMultipartFormDataContentRequest.Instance.SendChatRequestStream(
id, conversationId, deviceId, chatAsk, type, filepath, filename, (rlt) => {
Debug.Log($" TestHttpClientMultipartFormDataContentRequest ThreadId = {rlt.ThreadId}, ChatAnswer = {rlt.ChatAnswer}");
}, (rslt) => {
Debug.Log($" TestHttpClientMultipartFormDataContentRequest Fail success : ThreadId {rslt.ThreadId}, ChatAnswer {rslt.ChatAnswer}" );
}, (error) => {
Debug.Log($" TestHttpClientMultipartFormDataContentRequest Fail error = " + error);
});
}
}
4、Singleton
using System;
///
/// 单例脚本
///
///
public abstract class Singleton where T : Singleton
{
private static T m_Instance;
private static object m_Locker = new object();
public static T Instance
{
get
{
if (m_Instance == null)
{
lock (m_Locker)
{
if (m_Instance == null)
{
m_Instance = Activator.CreateInstance();
m_Instance.OnSingletonInit();
}
}
}
return m_Instance;
}
}
protected virtual void OnSingletonInit()
{
}
}