关于HTTP请求的那些事儿

关于 HTTP 请求,如果你知道有GET、POST请求,GET是在url 里用键值对传参,POST 只是换一个请求方法或者有时还可以发送一些json格式参数的话。如果你还想知道为什么有时候用 Ajax 请求明明和接口要求的参数一致却提示4xx之类的错误,为什么客户端请求的参数和服务器收到的不一致,为什么服务器端收不到上传的文件这类问题的话,那就请继续往下看吧。

有些基础知识你可能之前就知道

HTTP 请求是由请求行(request line)、请求头部(header)、空行和请求数据四部分组成的,一般格式是这样的:
关于HTTP请求的那些事儿
看格式其实也没多复杂,我们下面会着重说的是请求行里的 请求方法部分请求头

请求行

请求行里包括:请求方法、URL和协议版本。
常见的请求方法有 GETPOSTPUTDELETEPATCH,当然 HTTP 协议里还定义了更多的请求方法,如果你使用过 Postman (下面的示例会用此工具做演示)地址栏左侧就提供请求方法的选择框如图:
关于HTTP请求的那些事儿
URL 可以包含请求地址和参数,类似https://www.baidu.com?k=xxx&from=xxx
协议版本就是HTTP的版本号,到现在为止总共经历了3个版本的演化,从0.9 -> 1.0 -> 1.1 -> 2.0,但常用的还是 HTTP/1.1

要发送一个简单的请求其实只要有 URL 和 HTTP Method就够了,比如我们在浏览器里输入https://www.baidu.com/,浏览器会默认使用 GET 方法、默认的协议版本和请求头发起请求,你就可以看到百度首页了。

请求数据 BODY

请求的本质其实是完成客户端和服务器端的数据交互,无论通过什么方式的请求,客户端把需要告诉服务器端信息以参数的形式传递过去就完成了请求。简单且常用的就是 GET 请求没有请求 body ,所以请求参数是在 URL 里跟在问号(?)后边的,以等号(=)分隔的键值对,如:?id=1&type=new这种形式的,POST 请求可以把更复杂的信息传给服务器端,就需要把信息放在 body 里,但是 body 里的数据是什么格式的还需要额外的告诉服务器端,也就是请求头里各个字段的作用。

请求头

RFC2616 中定义了47种请求头字段,每个字段都有对应的作用,这里是对特殊的几个做说明。

1. Content 相关

请求内容相关的字段有:Content-Encoding、Content-Language、Content-Length、Content-Location、Content-MD5、Content-Range和Content-Type。这里主要说Content-Type顾名思义就是body里内容的类型,文章开头说的几个问题,多数情况是因为对这个字段理解不足导致的。
Content-Type 用于指示资源的MIME类型 media type 。常见的Content-Type格式是:Content-Type: text/html; charset=utf-8,分号前面是媒体类型也就是类型的可选值,后边是字符编码。还有一种不常用的了解一下就可以了,格式是这样的Content-Type: multipart/form-data; boundary=something,这类请求包含多部分请求体,boundary 是一组由1到70个字符组成的分隔符,用来分隔请求体。

请求中 (如POST 或 PUT),Content-Type客户端通过告诉服务器实际发送的数据类型。

  • application/json : JSON数据格式,在微信小程序和vue的一些请求库中的content-type一般默认为该类型。
  • application/x-www-form-urlencoded : HTML中 form 的 encType默认值,表单的数据将被编码为key/value格式发送到服务器。
  • multipart/form-data : 需要在表单中进行文件上传时需要使用该格式。
    下面用C#写了一个工具类,用来模拟请求:
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
namespace My.Helper
{
    public class WebUtils
    {
        private static int _timeout = 60000;
        public static HttpWebRequest GetWebRequest(string url, string method, CookieContainer cookies = null)
        {
            HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url);
            req.ServicePoint.Expect100Continue = false;
            req.Method = method;
            req.KeepAlive = true;
            req.UserAgent = "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)";
            req.Timeout = _timeout;
            if (cookies != null)
            {
                req.CookieContainer = cookies;
            }

            return req;
        }

        /// <summary>
        /// 执行 GET 请求。
        /// </summary>
        /// <param name="url">请求地址</param>
        /// <param name="parameters">请求参数</param>
        /// <returns>HTTP响应</returns>
        public static string DoGet(string url, IDictionary<string, string> parameters)
        {
            url = BuildGetUrl(url, parameters);

            HttpWebRequest req = GetWebRequest(url, "GET");
            HttpWebResponse rsp = (HttpWebResponse)req.GetResponse();
            Encoding encoding = string.IsNullOrEmpty(rsp.CharacterSet) ? Encoding.UTF8 : Encoding.GetEncoding(rsp.CharacterSet);
            return GetResponseAsString(rsp, encoding);
        }

        /// <summary>
        /// 执行 POST 请求
        /// </summary>
        /// <param name="url"></param>
        /// <param name="jsonObject"></param>
        /// <returns></returns>
        public static string DoPost(string url, object jsonObject)
        {
            HttpWebRequest req = GetWebRequest(url, "POST");
            req.ContentType = "application/json;charset=utf-8";

            byte[] postData = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(jsonObject));
            Stream reqStream = req.GetRequestStream();
            reqStream.Write(postData, 0, postData.Length);
            reqStream.Close();

            HttpWebResponse rsp = (HttpWebResponse)req.GetResponse();
            Encoding encoding = string.IsNullOrEmpty(rsp.CharacterSet) ? Encoding.UTF8 : Encoding.GetEncoding(rsp.CharacterSet);
            return GetResponseAsString(rsp, encoding);
        }



        /// <summary>
        /// 执行模拟Form提交的 POST 请求 
        /// </summary>
        /// <param name="url"></param>
        /// <param name="parameters"></param>
        /// <returns></returns>
        public static string DoPost(string url, IDictionary<string, string> parameters)
        {
            HttpWebRequest req = GetWebRequest(url, "POST");
            req.ContentType = "application/x-www-form-urlencoded;charset=utf-8";

            byte[] postData = Encoding.UTF8.GetBytes(BuildQuery(parameters));
            Stream reqStream = req.GetRequestStream();
            reqStream.Write(postData, 0, postData.Length);
            reqStream.Close();

            HttpWebResponse rsp = (HttpWebResponse)req.GetResponse();
            Encoding encoding = string.IsNullOrEmpty(rsp.CharacterSet) ? Encoding.UTF8 : Encoding.GetEncoding(rsp.CharacterSet);
            return GetResponseAsString(rsp, encoding);
        }

        /// <summary>
        /// 执行带文件上传的HTTP POST请求。
        /// </summary>
        /// <param name="url">请求地址</param>
        /// <param name="textParams">请求文本参数</param>
        /// <param name="fileParams">请求文件参数</param>
        /// <returns>HTTP响应</returns>
        public static string DoPost(string url, IDictionary<string, string> textParams, IDictionary<string, FileItem> fileParams)
        {
            // 如果没有文件参数,则走普通POST请求
            if (fileParams == null || fileParams.Count == 0)
            {
                return DoPost(url, textParams);
            }

            string boundary = DateTime.Now.Ticks.ToString("X"); // 随机分隔线

            HttpWebRequest req = GetWebRequest(url, "POST");
            req.ContentType = "multipart/form-data;charset=utf-8;boundary=" + boundary;

            Stream reqStream = req.GetRequestStream();
            byte[] itemBoundaryBytes = Encoding.UTF8.GetBytes("\r\n--" + boundary + "\r\n");
            byte[] endBoundaryBytes = Encoding.UTF8.GetBytes("\r\n--" + boundary + "--\r\n");

            // 组装文本请求参数
            string textTemplate = "Content-Disposition:form-data;name=\"{0}\"\r\nContent-Type:text/plain\r\n\r\n{1}";
            IEnumerator<KeyValuePair<string, string>> textEnum = textParams.GetEnumerator();
            while (textEnum.MoveNext())
            {
                string textEntry = string.Format(textTemplate, textEnum.Current.Key, textEnum.Current.Value);
                byte[] itemBytes = Encoding.UTF8.GetBytes(textEntry);
                reqStream.Write(itemBoundaryBytes, 0, itemBoundaryBytes.Length);
                reqStream.Write(itemBytes, 0, itemBytes.Length);
            }

            // 组装文件请求参数
            string fileTemplate = "Content-Disposition:form-data;name=\"{0}\";filename=\"{1}\"\r\nContent-Type:{2}\r\n\r\n";
            IEnumerator<KeyValuePair<string, FileItem>> fileEnum = fileParams.GetEnumerator();
            while (fileEnum.MoveNext())
            {
                string key = fileEnum.Current.Key;
                FileItem fileItem = fileEnum.Current.Value;
                string fileEntry = string.Format(fileTemplate, key, fileItem.FileName, fileItem.MimeType);
                byte[] itemBytes = Encoding.UTF8.GetBytes(fileEntry);
                reqStream.Write(itemBoundaryBytes, 0, itemBoundaryBytes.Length);
                reqStream.Write(itemBytes, 0, itemBytes.Length);

                byte[] fileBytes = fileItem.Content;
                reqStream.Write(fileBytes, 0, fileBytes.Length);
            }

            reqStream.Write(endBoundaryBytes, 0, endBoundaryBytes.Length);
            reqStream.Close();

            HttpWebResponse rsp = (HttpWebResponse)req.GetResponse();
            Encoding encoding = string.IsNullOrEmpty(rsp.CharacterSet) ? Encoding.UTF8 : Encoding.GetEncoding(rsp.CharacterSet);
            return GetResponseAsString(rsp, encoding);
        }

        /// <summary>
        /// 组装GET请求URL。
        /// </summary>
        /// <param name="url">请求地址</param>
        /// <param name="parameters">请求参数</param>
        /// <returns>带参数的GET请求URL</returns>
        private static string BuildGetUrl(string url, IDictionary<string, string> parameters)
        {
            if (parameters != null && parameters.Count > 0)
            {
                if (url.Contains("?"))
                {
                    url = url + "&" + BuildQuery(parameters);
                }
                else
                {
                    url = url + "?" + BuildQuery(parameters);
                }
            }
            return url;
        }

        /// <summary>
        /// 组装普通文本请求参数。
        /// </summary>
        /// <param name="parameters">Key-Value形式请求参数字典</param>
        /// <returns>URL编码后的请求数据</returns>
        private static string BuildQuery(IDictionary<string, string> parameters)
        {
            StringBuilder postData = new StringBuilder();
            bool hasParam = false;

            IEnumerator<KeyValuePair<string, string>> dem = parameters.GetEnumerator();
            while (dem.MoveNext())
            {
                string name = dem.Current.Key;
                string value = dem.Current.Value;
                // 忽略参数名或参数值为空的参数
                if (!string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(value))
                {
                    if (hasParam)
                    {
                        postData.Append("&");
                    }
                    postData.Append(name);
                    postData.Append("=");
                    postData.Append(Uri.EscapeDataString(value));
                    //postData.Append(value);
                    hasParam = true;
                }
            }

            return postData.ToString();
        }

        private static byte[] GetResponseAsBytes(HttpWebResponse rsp)
        {
            using (var input = rsp.GetResponseStream())
            {
                using (MemoryStream ms = new MemoryStream())
                {
                    input.CopyTo(ms);
                    return ms.ToArray();
                }
            }
        }

        private static string GetResponseAsString(HttpWebResponse rsp, Encoding encoding)
        {
            using (var stream = rsp.GetResponseStream())
            {
                using (var reader = new StreamReader(stream, encoding))
                {
                    return reader.ReadToEnd();
                }
            }
        }
    }
}

响应中,Content-Type标头告诉客户端实际返回的内容的内容类型。浏览器会在某些情况下进行MIME查找,并不一定遵循此标题的值; 为了防止这种行为,可以将标题 X-Content-Type-Options 设置为 nosniff。这里最直观的就是在IIS里有个MIME类型设置,并且设置一些默认的配置。
关于HTTP请求的那些事儿
关于HTTP请求的那些事儿
但是当你网站提供的内容类型不在默认配置中的时候,web站点就不知道以什么类型返回给客户端或者是直接不响应了。这就是为什么有时候明明文件放在了服务器客户却请求不到,例如:安卓或苹果的安装文件.apk/.ipa ,一些音视频文件.mp3.mp4播放不了,一些字体文件.woff找不到等,这类情况只需在IIS配置添加相应的类型就行或者在web.config文件里添加下面的节点:

<staticContent>
   <mimeMap fileExtension=".woff" mimeType="application/font-woff" />
   <mimeMap fileExtension=".mp4" mimeType="video/mp4" />
   <mimeMap fileExtension=".apk" mimeType="application/vnd.android" />
</staticContent>

还有一种情况是服务器端错误返回content-type,当服务器不能正确识别PDF文件时,会把本该返回的application/pdf设置为了application/octet-stream,浏览器拿到的是octet-stream只能当文件下载,不能直接打开。
详细的MIME类型和文件后缀的对应可以查看这里媒体类型(MIME types)

查资料时发现这么一个话题,感兴趣的可以看看这个讨论是否要需要在GET 请求中指定 Content-Type

断断续续写了2天,感觉要写的东西太多了,后边还有几个话题只列了标题,有时间了再补吧,先发上来跟大家共同学习讨论吧...

2. 缓存 Cache-Control

3. 授权 Authorization

参考文献:
RFC2616
MDN Doc MIME_types
https://www.runoob.com/http/http-content-type.html
https://learning.postman.com/docs/sending-requests/requests/

关于HTTP请求的那些事儿

上一篇:js防止重复点击或者点击过快方法


下一篇:Js 日期格式化