遇到Https網站,c# http請求的時候,總是報SSL連接錯誤。后來經搜索,發現有解決方案:
.net 2.0 需要引入一個第三方組件:BouncyCastle.dll,這是我寫的一個例子:
public static string RequestWebServerByTCP(Uri uri, string method, NameValueCollection parameter, string cookie, Encoding encoding)
{
try
{
StringBuilder RequestHeaders = new StringBuilder();
RequestHeaders.Append(method + " " + uri.PathAndQuery + " HTTP/1.1\r\n");
method = method.ToUpper();
if (method == POSTMETHOD)
RequestHeaders.Append("Content-Type:application/x-www-form-urlencoded\r\n");
RequestHeaders.Append("User-Agent:Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11\r\n");
RequestHeaders.Append("Cookie:" + cookie + "\r\n");
RequestHeaders.Append("Accept:*/*\r\n");
RequestHeaders.Append("Host:" + uri.Host + "\r\n");
byte[] postdata = null;
StringBuilder sb = new StringBuilder();
if (method == GETMETHOD)
{
uri = GetMethodQueryString(uri, parameter, encoding);
}
else if (method == POSTMETHOD)
{
if (parameter != null)
{
foreach (string key in parameter)
{
sb.Append(string.Format(FORMATSTR1, System.Web.HttpUtility.UrlEncode(key, encoding), System.Web.HttpUtility.UrlEncode(parameter[key], encoding)));
}
}
if (sb.Length != 0)
{
sb = sb.Remove(sb.Length - 1, 1);
}
postdata = encoding.GetBytes(sb.ToString());
RequestHeaders.Append("Content-Length:" + postdata.Length + "\r\n");
}
RequestHeaders.Append("Connection:close\r\n\r\n");
byte[] req = Encoding.UTF8.GetBytes(RequestHeaders.ToString() + sb.ToString());
int port = 443;
MyTlsClient client = new MyTlsClient();
var protocol = OpenTlsConnection(uri.Host, port, client);
Stream tlsStream = protocol.Stream;
tlsStream.Write(req, 0, req.Length);
tlsStream.Flush();
StreamReader reader = new StreamReader(tlsStream);
String line;
StringBuilder html = new StringBuilder();
string firstLine = "";
int i = 0;
while ((line = reader.ReadLine()) != null)
{
if (i == 0)
{
firstLine = line;
i++;
}
html.AppendLine(line);
if (line.Contains("</html>"))
{
break;
}
}
protocol.Close();
string httpstatusCode = "";
string[] httpstatus = firstLine.Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries);
if (httpstatus.Length > 2)
{
httpstatusCode = httpstatus[1];
}
else
{
//請求無效
httpstatusCode = "400";
}
return html.ToString();
}
catch
{
return "";
}
}
請求到的html,為什么需要一行一行讀呢?我在調試的時候發現有個bug,如果一次性讀取的時候,它停不下來,最終報錯,所以我做了一個讀到html末尾的判斷。
繼承了提供的默認類:
class MyTlsClient : DefaultTlsClient
{
public override TlsAuthentication GetAuthentication()
{
return new MyTlsAuthentication();
}
}
// Need class to handle certificate auth
class MyTlsAuthentication : TlsAuthentication
{
public TlsCredentials GetClientCredentials(CertificateRequest certificateRequest)
{
// return client certificate
return null;
}
public void NotifyServerCertificate(Certificate serverCertificate)
{
// validate server certificate
}
}
internal static TlsClientProtocol OpenTlsConnection(string hostname, int port, TlsClient client)
{
TcpClient tcp = new TcpClient(hostname, port);
TlsClientProtocol protocol = new TlsClientProtocol(tcp.GetStream(), secureRandom);
protocol.Connect(client);
return protocol;
}
拼接url參數的方法:
private static Uri GetMethodQueryString(Uri uri, NameValueCollection parameter, Encoding encoding)
{
List<KeyValuePair<string, string>> parameter1 = new List<KeyValuePair<string, string>>();
foreach (string key in parameter)
{
parameter1.Add(new KeyValuePair<string, string>(key, parameter[key]));
}
return GetMethodQueryString(uri, parameter1, encoding);
}
private static Uri GetMethodQueryString(Uri uri, List<KeyValuePair<string, string>> parameter, Encoding encoding)
{
string format = string.Empty;
UriBuilder uribuilfer = new UriBuilder(uri);
string QueryString = string.Empty;
if (string.IsNullOrEmpty(uribuilfer.Query))
{
format = FORMATSTR1;
}
else
{
format = FORMATSTR2;
}
QueryString = uribuilfer.Query;
if (parameter != null)
{
foreach (KeyValuePair<string, string> item in parameter)
{
QueryString += string.Format(format, System.Web.HttpUtility.UrlEncode(item.Key, encoding), System.Web.HttpUtility.UrlEncode(item.Value, encoding));
}
}
QueryString = QueryString.TrimEnd(new char[] { '&' });
QueryString = QueryString.TrimStart(new char[] { '?' });
uribuilfer.Query = QueryString;
uri = uribuilfer.Uri;
return uri;
}
注意:List<KeyValuePair<string, string>>和NameValueCollection類型的參數有什么區別呢?它們都包含相同的key,只不過存儲的時候,NameValueCollection會把含有相同Key的值用逗號隔開,存在一起。這樣請求有可能會失敗,拿不到數據。本人因此問題,折騰了很久,用python實現了請求,后來在.net core中實現了一遍,最后終於低下了高傲的頭顱,才看到傳參時候,有點問題。
.net 4.0中,只需要添加一句話:ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072;
.net 4.5中,什么都不用管。
2.0中連TCP都用上了,不過我們看到了http請求的本質,把一段具有格式的請求頭+請求數據轉為二進制發送到主機的某個端口,返回流,通過讀取流,就可以拿到結果。
說到這,我們來看看Request消息格式:
GET https://www.baidu.com/ HTTP/1.1 Accept: text/html, application/xhtml+xml, */* Accept-Language: zh-CN User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko Accept-Encoding: gzip, deflate Connection: Keep-Alive Host: www.baidu.com Cookie: BAIDUID=C1EFC3A3466AAAEBE74C6F6E7F413FA8:FG=1; BIDUPSID=C1EFC3A3466AAAEBE74C6F6E7F413FA8; PSTM=1525339270; BD_LAST_QID=12260391193367555241
1、請求行,包含請求的方法,url,http協議版本
2、請求頭,接收的格式,瀏覽器代理,cookie等等
3、空行
4、請求體,傳遞數據
Response格式:
HTTP/1.1 200 OK Bdpagetype: 1 Bdqid: 0x9a1ff959000016d0 Cache-Control: private Connection: Keep-Alive Content-Encoding: gzip Content-Type: text/html; charset=utf-8 Cxy_all: baidu+77e5655ffd82ce31adf5edff251fc585 Date: Thu, 03 May 2018 09:21:10 GMT Expires: Thu, 03 May 2018 09:21:03 GMT Server: BWS/1.1 Set-Cookie: BDSVRTM=0; path=/ Set-Cookie: BD_HOME=0; path=/ Set-Cookie: H_PS_PSSID=1428_21080_20719; path=/; domain=.baidu.com Strict-Transport-Security: max-age=172800 Vary: Accept-Encoding X-Powered-By: HPHP X-Ua-Compatible: IE=Edge,chrome=1 Transfer-Encoding: chunked
html
1、狀態行
2、消息報頭,content-type,Date,Set-Cookie
3、空行
4、正文

