编者注
这几天一直在写Unity当中的C#的网络传输,但是发现由于Unity采用的mono框架,支持的仅仅是.Net Framework 3.5的API,并不支持更高级别的HttpClient,则很多功能需要手动开发。最早简单测试过GET数据,并且JSON文件反序列化为对象成功。之后实现了POST的JSON文件,获取返回的JSON,所以麻痹大意,导致在uploadfile这个环节花费了大量的时间。
设计方法
笔者认为apache commons的库设计非常好,所以仿照了httpclient进行了设计。
└─Utils
├─GlobalConfig
│ UnityTemplate.cs - 用来存储Unity项目规范结构
│
├─HttpHelper
│ │ HttpHelper.cs - 针对访问http程序的主要方法
│ │
│ ├─http
│ │ HttpEntity.cs - HTTP Body中的内容
│ │ HttpHeader.cs - HTTP Headers
│ │
│ └─methods
│ HttpGet.cs - Get方法
│ HttpPost.cs - Post方法
│ HttpRequestBase.cs - 公共继承方法
│
└─JsonHelper - 另外的库,用来处理Json序列化与反序列化
HttpHelper
根据HttpClient方法,由于已经能够确认.net core开源版本已经集成HttpClient,如果未来Unity什么时候集成core,则不需要考虑现在的方法,则把现在的方法命名为HttpHelper
HttpHelper主要方法为execute,模仿httpclient,参数是存入的get或者post内容。由于返回内容不确定是键值对还是json,则直接返回HttpWebResponse
public static HttpWebResponse execute(HttpRequestBase methods)
HttpHeader
罗列了简单的http header信息,并设置默认值,方便使用。
public class HttpHeader
{
// 默认值经过http编码
public string ContentType = "application/x- www-form-urlencoded";
public string UserAgent = "Unity/HttpHelper";
public long ContentLength;
public System.Net.IWebProxy Proxy = null;
public bool KeepAlive = true;
}
HttpEntity
由于C#只能够通过ArrayList或者词典,对不定类型的对象进行存储,所以这里使用了ArrayList存储有可能发生的多种类型。
由于即便是多种类型,依旧是采用键值对的方式进行访问(json不通过键值对,不过会以键值对的方式声明。)
using System.Collections;
using System.Collections.Generic;
using System.IO;
public class HttpEntity
{
private ArrayList entitys;
public HttpEntity()
{
this.entitys = new ArrayList();
}
public void setKeyValuePair(KeyValuePair<string, long> keyValuePair)
{
this.entitys.Add(keyValuePair);
}
public void setKeyValuePair(KeyValuePair<string,int> keyValuePair)
{
this.entitys.Add(keyValuePair);
}
public void setKeyValuePair(KeyValuePair<string,string> keyValuePair)
{
this.entitys.Add(keyValuePair);
}
public void setKeyValuePair(KeyValuePair<string,FileInfo> keyValuePair)
{
this.entitys.Add(keyValuePair);
}
public ArrayList getHttpElements()
{
return this.entitys;
}
public long getContentLength()
{
return -1;
}
public bool hasFile()
{
foreach(object item in this.entitys)
{
if (item is KeyValuePair<string, FileInfo>)
{
return true;
}
}
return false;
}
}
分隔符的生成
根据http的需求,需要生成分隔符,但是根据浏览器的建议,不会真正生成一个和文件内容毫无关系的字符串作为分隔符。这样做的原因是要花费大量的时间去解决分隔符冲突的问题。所以通用做法是随机找一个尽可能复杂的分隔符降低重复的概率。
作者以guid的方式生成分隔符,使分隔符更复杂。
// 生成分隔符
string boundary = Guid.NewGuid().ToString("N");
文件传送的规则
KeyValuePair<string, FileInfo> kv = (KeyValuePair<string, FileInfo>)item;
// HTTP规定,起始的分割线
string fileboundary = "--" + boundary + "\r\n";
httpbody_length += getLength(fileboundary);
// HTTP规定,Content-Disposition
string ContentDisposition = "Content-Disposition: form-data; name=\"" + kv.Key + "\"; filename=\"" + kv.Value.Name + "\"\r\n";
httpbody_length += getLength(ContentDisposition);
// HTTP规定,ContentType
string ContentType = "Content-Type: application/octet-stream" + "\r\n";
httpbody_length += getLength(ContentType);
// HTTP规定,ContentTransferEncoding
string ContentTransferEncoding = "ContentTransferEncoding:binary" + "\r\n";
httpbody_length += getLength(ContentTransferEncoding);
// 区分文件头与文件内容
string separator = "\r\n";
httpbody_length += getLength(separator);
// 获取文件长度
FileStream fileStream = new FileStream(kv.Value.FullName, FileMode.Open, FileAccess.Read);
httpbody_length += fileStream.Length;
fileStream.Close();
// 再次添加separator
httpbody_length += getLength(separator);
碰到的坑
HttpWebRequest.ContentLength
HttpWebRequest必须先设置ContentLength这个参数,然后才能够通过GetRequestStream获取requestStream。
阅读API,描述为必须这样,猜测实现是根据ContentLength对Stream进行初始化导致的。这种方式直接导致,无法通过FileStream的大小来判断最终的ContentLength大小。
解决方法:
// 先计算body的长度,在写入stream中
request.ContentLength = httpbody_length;
// get http body bytes
Stream requestStream = request.GetRequestStream();
400 request bad
在Unity端报400 request bad,由于客户端的限制,实质是看不到Unity的实际报错内容。则需要到服务端去查看错误内容。
Required CommonsMultipartFile parameter 'file' is not present
由于需要手动编写http的body规则,手误导致编写错误,并且没有看到,导致调试很久。