webapi 客户端显示具体错误_c# webapi 服务端和客户端 - CSDN
精华内容
参与话题
  • 在js中调用web api如果报错,比如400 bad request,比如500都会在response中看到具体错误,方便我们及时修正,但是在c#中通过httpwebrequest调用报错是看不到的,所以往往需要我们把url拷出来在浏览器里查看,甚至...

        在js中调用web api如果报错,比如400 bad request,比如500都会在response中看到具体的错误,方便我们及时修正,但是在c#中通过httpwebrequest调用报错是看不到的,所以往往需要我们把url拷出来在浏览器里查看,甚至需要借助第三方工具来查看非get请求类的错误,还是比较麻烦的。

        先来看下普通的httprequest方式报错返回是什么样的,示例代码很简单,创建一条name为DTCC的account记录

     HttpWebRequest req = (HttpWebRequest)HttpWebRequest.
                    Create("http://121.40.75.24:5555/origin/api/data/v8.0/accounts");
                req.Credentials = new NetworkCredential(username, pwd, domain);
                req.Method = "post";
                req.Accept = "application/json";
                req.ContentType = "application/json; charset=utf-8";
                byte[] data = Encoding.UTF8.GetBytes("{\"name\":\"DTCC\"}");
                Stream newStream = req.GetRequestStream();
                newStream.Write(data, 0, data.Length);
                newStream.Close();
                using (HttpWebResponse res = (HttpWebResponse)req.GetResponse())
                {
                    StreamReader read = new StreamReader(res.GetResponseStream());
                    string result = read.ReadToEnd();
                }
       

        因为我写了个create的pre插件阻止了创建,所以这块提示是500错,但看不到具体的错误原因

        咱们来换一种方式,示例代码如下

    public static async Task Demo_API_02()
            {
                HttpClientHandler sHandler = new HttpClientHandler();
                sHandler.Credentials = new NetworkCredential(username, pwd, domain);
                using (var client = new HttpClient(sHandler))
                {
                    client.BaseAddress = new Uri("http://121.40.75.24:5555/origin/api/data/v8.0/accounts");
                    client.DefaultRequestHeaders.Accept.Clear();
                    HttpRequestMessage createRequest2 = new HttpRequestMessage(HttpMethod.Post, "");
                    client.DefaultRequestHeaders.Add("ContentType", "application/json; charset=utf-8");
                    client.DefaultRequestHeaders.Add("OData-MaxVersion", "4.0");
                    client.DefaultRequestHeaders.Add("OData-Version", "4.0");
                    createRequest2.Content = new StringContent("{\"name\":\"DTCC\"}", Encoding.UTF8, "application/json");
                    HttpResponseMessage createResponse2 = await client.SendAsync(createRequest2);
    
                    if (createResponse2.StatusCode == HttpStatusCode.NoContent)
                    {
                        var demoUri = createResponse2.Headers.GetValues("OData-EntityId").FirstOrDefault();
                    }
                    else
                    {
                        var exception = new CrmHttpResponseException(createResponse2.Content);
                    }
                }
            }
      看下结果,准确的抓到了我插件中抛出的exception错


       示例代码中用到的CrmHttpResponseException类代码如下

     public class CrmHttpResponseException : System.Exception
            {
                #region Properties
                private static string _stackTrace;
                /// <summary>
                /// Gets a string representation of the immediate frames on the call stack.
                /// </summary>
                public override string StackTrace
                {
                    get { return _stackTrace; }
                }
    
                #endregion Properties
                #region Constructors
                /// <summary>
                /// Initializes a new instance of the CrmHttpResponseException class.
                /// </summary>
                /// <param name="content">The populated HTTP content in Json format.</param>
    
                public CrmHttpResponseException(HttpContent content) : base(ExtractMessageFromContent(content)) { }
    
    
                /// <summary>
                /// Initializes a new instance of the CrmHttpResponseException class.
                /// </summary>
                /// <param name="content">The populated HTTP content in Json format.</param>
                /// <param name="innerexception">The exception that is the cause of the current exception,or a null reference
                /// if no innerexception is specified.</param>
    
                public CrmHttpResponseException(HttpContent content, Exception innerexception) : base(ExtractMessageFromContent(content), innerexception) { }
    
                #endregion Constructors
                #region Methods
                /// <summary>
                /// Extracts the CRM specific error message and stack trace from an HTTP content. 
                /// </summary>
                /// <param name="content">The HTTP content in Json format.</param>
                /// <returns>The error message.</returns>
    
    
                private static string ExtractMessageFromContent(HttpContent content)
                {
                    string message = String.Empty;
                    string downloadedContent = content.ReadAsStringAsync().Result;
                    if (content.Headers.ContentType.MediaType.Equals("text/plain"))
                    {
                        message = downloadedContent;
                    }
                    else if (content.Headers.ContentType.MediaType.Equals("application/json"))
                    {
                        JObject jcontent = (JObject)JsonConvert.DeserializeObject(downloadedContent);
                        IDictionary<string, JToken> d = jcontent;
    
                        // An error message is returned in the content under the 'error' key. 
    
                        if (d.ContainsKey("error"))
                        {
                            JObject error = (JObject)jcontent.Property("error").Value;
                            message = (String)error.Property("message").Value;
                        }
                        else if (d.ContainsKey("Message"))
                            message = (String)jcontent.Property("Message").Value;
                        if (d.ContainsKey("StackTrace"))
                            _stackTrace = (String)jcontent.Property("StackTrace").Value;
                    }
                    else if (content.Headers.ContentType.MediaType.Equals("text/html"))
                    {
                        message = "HTML content that was returned is shown below.";
                        message += "\n\n" + downloadedContent;
                    }
                    else
                    {
                        message = String.Format("No handler is available for content in the {0} format.", content.Headers.ContentType.MediaType.ToString());
                    }
                    return message;
                    #endregion Methods
                }
            }
        最后感谢路人甲提供的参考代码。



    展开全文
  • 六步实现Rest风格的API

    万次阅读 2012-08-21 14:58:01
    Rest的作者认为计算机发展到现在,最大的成就不是企业应用,而是web,是漫漫无边的互联网web世界。Web能有这么大的成就,它值得我们研究。所以Rest的作者仔细研究了Web,按照Web的世界一些关键特性,提出了我们在...

    Rest的作者认为计算机发展到现在,最大的成就不是企业应用,而是web,是漫漫无边的互联网web世界。Web能有这么大的成就,它值得我们研究。所以Rest的作者仔细研究了Web,按照Web的世界一些关键特性,提出了我们在实现企业应用的时候应该遵循的一种风格,就是Restful。

    Rest风格的API可以给我们很多好处,比如:简洁,统一,性能,可扩展性等等。可惜的是,在实现Rest的时候,总有一些Rest的关键特性没有实现,比如,无状态性,这在我做过的两个项目和我知道的另外一个项目都存在。事实上要实现无状态性在java里不是那么容易,因为那意味着要把servlet的session抛弃了。除此之外,Rest的一些其他特性在各个项目中实现的也是各有不同。

    接下来,我会列出一些我认为的,要实现Rest风格API的关键步骤:


    1. 所有东西都是资源(Resource)

    所有要给API操作的对象都只能是资源。不管实际上存在的,还是抽象上的。所有资源都会有一个不变的标识(ID),对资源的任何API操作都不应该改变资源的标识。资源和其他资源会有关系,资源与资源的关系通过资源的标识来引用。对资源的操作都应该是完整的,比如获取资源拿到的应该是一个完整的资源对象(根据企业引用特点有些例外,后面会提到)。

    事实上,上面的这些完完全全是按照互联网的特性提出来的。互联网中,一个URL就是一个资源;资源的内容就是HTML页面;不管怎么改HTML内容,URL都不会改变;资源之间通过HTML里的连接联系起来;每次获取的时候,获取到的都是完整的HTML内容。

    假设有一个博客系统,那么其中的资源可以包括:博主,每个博主都是一个资源;博客,每篇博客都是一个资源,博客和博主之间有联系,通过ID联系起来;每篇博客都会有回复,回复也算是资源,但是它是隶属于博客的,可以认为回复是博客的子资源(你也可以认为博客是博主的子资源,怎么抽象取决于具体的应用,这里不讨论)。

    这步通常不难实现,因为这和ORM中的对象概念是类似的,实现上,如果用了Hibernate之类的框架,改动也应该很小。


    2. 规范对资源的操作,最好只包括CRUD

    CRUD指创建(Create),读取(Read), 更新(Update),删除(Delete)

    通常对资源的操作只包含CRUD是不可能的,CRUD里甚至连查找的操作的都没有。但这不妨碍我们对Rest的理解,Rest提出的要求是,对资源的操作都应该是统一的,不管是操作哪种类型的资源,API都应该是一致的。这样当调用API的客户端知道某种资源的时候,它不需要去学习对这个资源该怎么操作,因为对所有资源的操作都是一致的,它们都应该支持CRUD操作,以及一些其他操作,比如list(用来查找,或者列出所有资源), merge(部分更新资源,这应该是唯一的不操作资源所有内容的API)。

    这和Web也是一样的,HTTP里只有GET,PUT,POST,HEAD等等几个统一的请求(参考:http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html)。

    要实现简单的几个操作不难,难在这几个简单操作没法支撑整个系统的需求。但是想想吧,互联网也够复杂了吧,还是不是成功了,而且鱼和熊掌不可兼得。有时候服务器端也不一定要实现所有东西,可以把一些逻辑交给客户端去做。比如显示,Rest里显示是完全交给客户端去处理的,服务器只管数据的存储,不管客户端是网页,还是一个手机应用程序,还是另外一个企业应用。各种各样的客户端进来,他们会有各种各样的需求,服务器端不可能一一满足,只能客户端使用统一的API去组合,实现自己的需求。


    3. 规范URL的使用

    好了,对资源的操作统一了,但是客户端还是要知道怎么触发对资源的某个具体的操作。Rest用URL的规范来保证这种统一性。

    创建并保存一个博客:

    POST /blog/save

    这个请求需要返回博客的保存后的结果,其中包括博客的标识(ID)。 获取一个已经保存的博客,并更新它:

    GET /blog/get/345
    //更新它
    POST /blog/update/345

    这个博客的标识是345。获取博客的某个回复:

    GET /blog/get/345/reply/456

    对待子资源,通常的做法就是和这个例子一样,是一级一级的往下找。我们还可以有其他方法,比如remove用来删除,merge用来部分更新,list用来查找。

    有三种方式可以将参数传给API操作:

    第一种是通过URL的地址传递,如前面的例子中把标识放在URL里;

    第二种是通过URL的参数,比如,对于一个查找请求,可以把查找的过滤条件放在参数里:

    GET /blog/list?name=Azure:用InstanceInputEndpoint直接和指定instance通信

    第三种是PUT或者POST请求的时候,把内容放在HTTP body里面。这里通常就是博客的内容。

    前面我们的例子中有些请求是GET,有些是POST,其实这是有原则的。通常对资源内容没有改变的操作都实用GET,比如获取资源,查找资源;对资源有改变的操作都用POST,比如保存资源。

    如果想做的更好,我们应该近一步的使用HTTP的请求方法,直接把HTTP方法和要做的操作映射起来。比如我喜欢认为GET请求就是获取资源(get),PUT方法就是更新整个内容(save,update,我觉得这两个没必要区分),POST方法就是更新部分内容(merge),DELETE方法就是删除资源(remove)。如果这样的话,请求的URL又能简化:

    PUT   /blog           //创建保存一个新的博客
    GET   /blog/345    //获取博客345内容
    PUT   /blog/345    //更新博客345
    GET   /blog/345/reply/456     //获取博客345的回复456
    POST /blog/345    //更新博客345的部分内容
    DELETE /blog/345   //删除博客345

    当然对于list操作,这里就没法满足了,还是需要在URL层面上做些区别。

    对于merge操作,有很多人认为是不必要的,Rest不应该提供这个API,但是我觉得在某些情况下很有用。比如某个资源对象,它的内容在不断的扩充,怎么让老的客户端在内容扩充后还能继续使用呢? 如果我们要求所有更新请求都必须把所有内容都放在请求的body中,对于客户端来说就不是那么好做了,但是如果我们允许merge请求,客户端可以可以完全忽略新增加的字段,而只把自己知道的字段放在请求内容中即可。

    4. 资源的多重表述

    这一步我觉得不是必须的。

    Rest里,资源的内容通常直接作为一段JSON或者XML返回给客户端。资源多重表述指的是,一个资源应该能够支持根据客户端的请求,返回相应的格式给客户端。服务器应该按照请求HTTP头中的Accept属性决定返回格式。比如对于Ajax请求,Accept头是application/json,服务端返回JSON格式;对于android请求,Accept头是application/xhtml+xml,服务器返回XML格式。

    我觉得这一步不是必须的因为至少从项目前期来说,我们应该都只会支持一种格式。资源的多重表述给我们一种处理多重请求格式的方式,但是我们不需要一开始就支持它。


    5. 进一步合理利用HTTP

    前面我们已经应用了HTTP的一些东西,比如请求方法,Accept头。事实上我们可以利用更多。

    HTTP支持客户端缓存,在HTTP响应里利用Cache-Control,Expires,Last-Modified三个头字段,我们可以让浏览器缓存资源一段时间。Rest也可以利用这些头,告诉客户端在一定时间内不需要再次请求资源。这对提高性能有很大好处。更多HTTP头信息,可以参考http://en.wikipedia.org/wiki/List_of_HTTP_header_fields

    Rest的请求会出错,HTTP的请求也会出错。我们可以直接利用HTTP的response code来告诉客户端Rest请求出了什么错误。比如500,告诉客户端,服务器出错了;401告诉客户端需要把安全验证信息附上,需要登录系统;404告诉请求的资源不存在,等等。更多HTTP响应码,可以参考http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html。在实际的业务中,HTTP的那些response code肯定是不能满足所有需求的,适当的在response body中加上更详细的错误信息也是必须的。

    还有其他很多,总之能利用上的就利用上,不比再次发明轮子。


    6. 实现请求的无状态

    Rest是无状态的。Rest的请求之间不应该有依赖,在调用一个请求前,不需要一定要去提前调用另外一个请求。Rest里面不应该有session,特别是Rest请求不应该保存信息在sesssion里,以便在后面的调用中使用。甚至包括安全验证,客户端不应该需要提前登录,然后把权限信息保存在session里,后面的请求用同一个session来调用。

    实现无状态的方法就是,把所有信息都包含在当前的请求中,包括验证信息。HTTP是无状态的,HTTP里有一个Authorization头,HTTP的要求是在每次请求的时候都把验证信息放在里面,服务器每次处理请求前都去验证这个信息。为了安全,我们可以提供一个生成token的Rest API,客户端调用这个API生成token(可以附上用户名/密码来生成token)。在后面的所有请求中都把这个token放在Authentication头中。

    实现无状态最大的好处是能够方便的扩展服务器,也即scalability。否则的话,我们要么把Session绑定到具体服务器上,要么用一个共享的空间存储Session。而实现无状态后,我们可以随意增加,减少服务器数量,都不会对当前用户造成影响。


    关键字: Rest, Resful, 实现Rest

     版权所有,转载请注明来源



    展开全文
  • python flask api接口开发编程

    万次阅读 多人点赞 2016-10-10 19:48:46
    使用 Python 和 Flask 设计 RESTful API 近些年来 REST (REpresentational State Transfer) 已经变成了 web services 和 web APIs 的标配。 在本文中我将向你展示如何简单地使用 Python 和 Flask 框架来创建一...

    使用 Python 和 Flask 设计 RESTful API

    近些年来 REST (REpresentational State Transfer) 已经变成了 web services 和 web APIs 的标配。

    在本文中我将向你展示如何简单地使用 Python 和 Flask 框架来创建一个 RESTful 的 web service。

    什么是 REST?

    六条设计规范定义了一个 REST 系统的特点:

    • 客户端-服务器: 客户端和服务器之间隔离,服务器提供服务,客户端进行消费。
    • 无状态: 从客户端到服务器的每个请求都必须包含理解请求所必需的信息。换句话说, 服务器不会存储客户端上一次请求的信息用来给下一次使用。
    • 可缓存: 服务器必须明示客户端请求能否缓存。
    • 分层系统: 客户端和服务器之间的通信应该以一种标准的方式,就是中间层代替服务器做出响应的时候,客户端不需要做任何变动。
    • 统一的接口: 服务器和客户端的通信方法必须是统一的。
    • 按需编码: 服务器可以提供可执行代码或脚本,为客户端在它们的环境中执行。这个约束是唯一一个是可选的。

    什么是一个 RESTful 的 web service?

    REST 架构的最初目的是适应万维网的 HTTP 协议。

    RESTful web services 概念的核心就是“资源”。 资源可以用 URI 来表示。客户端使用 HTTP 协议定义的方法来发送请求到这些 URIs,当然可能会导致这些被访问的”资源“状态的改变。

    HTTP 标准的方法有如下:

    ==========  =====================  ==================================
    HTTP 方法   行为                   示例
    ==========  =====================  ==================================
    GET         获取资源的信息         http://example.com/api/orders
    GET         获取某个特定资源的信息 http://example.com/api/orders/123
    POST        创建新资源             http://example.com/api/orders
    PUT         更新资源               http://example.com/api/orders/123
    DELETE      删除资源               http://example.com/api/orders/123
    ==========  ====================== ==================================
    

    REST 设计不需要特定的数据格式。在请求中数据可以以 JSON 形式, 或者有时候作为 url 中查询参数项。

    设计一个简单的 web service

    坚持 REST 的准则设计一个 web service 或者 API 的任务就变成一个标识资源被展示出来以及它们是怎样受不同的请求方法影响的练习。

    比如说,我们要编写一个待办事项应用程序而且我们想要为它设计一个 web service。要做的第一件事情就是决定用什么样的根 URL 来访问该服务。例如,我们可以通过这个来访问:

    http://[hostname]/todo/api/v1.0/

    在这里我已经决定在 URL 中包含应用的名称以及 API 的版本号。在 URL 中包含应用名称有助于提供一个命名空间以便区分同一系统上的其它服务。在 URL 中包含版本号能够帮助以后的更新,如果新版本中存在新的和潜在不兼容的功能,可以不影响依赖于较旧的功能的应用程序。

    下一步骤就是选择将由该服务暴露(展示)的资源。这是一个十分简单地应用,我们只有任务,因此在我们待办事项中唯一的资源就是任务。

    我们的任务资源将要使用 HTTP 方法如下:

    ==========  ===============================================  =============================
    HTTP 方法   URL                                              动作
    ==========  ===============================================  ==============================
    GET         http://[hostname]/todo/api/v1.0/tasks            检索任务列表
    GET         http://[hostname]/todo/api/v1.0/tasks/[task_id]  检索某个任务
    POST        http://[hostname]/todo/api/v1.0/tasks            创建新任务
    PUT         http://[hostname]/todo/api/v1.0/tasks/[task_id]  更新任务
    DELETE      http://[hostname]/todo/api/v1.0/tasks/[task_id]  删除任务
    ==========  ================================================ =============================
    

    我们定义的任务有如下一些属性:

    • id: 任务的唯一标识符。数字类型。
    • title: 简短的任务描述。字符串类型。
    • description: 具体的任务描述。文本类型。
    • done: 任务完成的状态。布尔值。

    目前为止关于我们的 web service 的设计基本完成。剩下的事情就是实现它!

    Flask 框架的简介

    如果你读过 Flask Mega-Tutorial 系列,就会知道 Flask 是一个简单却十分强大的 Python web 框架。

    在我们深入研究 web services 的细节之前,让我们回顾一下一个普通的 Flask Web 应用程序的结构。

    我会首先假设你知道 Python 在你的平台上工作的基本知识。 我将讲解的例子是工作在一个类 Unix 操作系统。简而言之,这意味着它们能工作在 Linux,Mac OS X 和 Windows(如果你使用Cygwin)。 如果你使用 Windows 上原生的 Python 版本的话,命令会有所不同。

    让我们开始在一个虚拟环境上安装 Flask。如果你的系统上没有 virtualenv,你可以从https://pypi.python.org/pypi/virtualenv 上下载:

    $ mkdir todo-api
    $ cd todo-api
    $ virtualenv flask
    New python executable in flask/bin/python
    Installing setuptools............................done.
    Installing pip...................done.
    $ flask/bin/pip install flask
    

    既然已经安装了 Flask,现在开始创建一个简单地网页应用,我们把它放在一个叫 app.py 的文件中:

    #!flask/bin/python
    from flask import Flask
    
    app = Flask(__name__)
    
    @app.route('/')
    def index():
        return "Hello, World!"
    
    if __name__ == '__main__':
        app.run(debug=True)
    

    为了运行这个程序我们必须执行 app.py:

    $ chmod a+x app.py
    $ ./app.py
     * Running on http://127.0.0.1:5000/
     * Restarting with reloader
    

    现在你可以启动你的网页浏览器,输入 http://localhost:5000 看看这个小应用程序的效果。

    简单吧?现在我们将这个应用程序转换成我们的 RESTful service!

    使用 Python 和 Flask 实现 RESTful services

    使用 Flask 构建 web services 是十分简单地,比我在 Mega-Tutorial 中构建的完整的服务端的应用程序要简单地多。

    在 Flask 中有许多扩展来帮助我们构建 RESTful services,但是在我看来这个任务十分简单,没有必要使用 Flask 扩展。

    我们 web service 的客户端需要添加、删除以及修改任务的服务,因此显然我们需要一种方式来存储任务。最直接的方式就是建立一个小型的数据库,但是数据库并不是本文的主体。学习在 Flask 中使用合适的数据库,我强烈建议阅读 Mega-Tutorial

    这里我们直接把任务列表存储在内存中,因此这些任务列表只会在 web 服务器运行中工作,在结束的时候就失效。 这种方式只是适用我们自己开发的 web 服务器,不适用于生产环境的 web 服务器, 这种情况一个合适的数据库的搭建是必须的。

    我们现在来实现 web service 的第一个入口:

    #!flask/bin/python
    from flask import Flask, jsonify
    
    app = Flask(__name__)
    
    tasks = [
        {
            'id': 1,
            'title': u'Buy groceries',
            'description': u'Milk, Cheese, Pizza, Fruit, Tylenol',
            'done': False
        },
        {
            'id': 2,
            'title': u'Learn Python',
            'description': u'Need to find a good Python tutorial on the web',
            'done': False
        }
    ]
    
    @app.route('/todo/api/v1.0/tasks', methods=['GET'])
    def get_tasks():
        return jsonify({'tasks': tasks})
    
    if __name__ == '__main__':
        app.run(debug=True)
    

    正如你所见,没有多大的变化。我们创建一个任务的内存数据库,这里无非就是一个字典和数组。数组中的每一个元素都具有上述定义的任务的属性。

    取代了首页,我们现在拥有一个 get_tasks 的函数,访问的 URI 为 /todo/api/v1.0/tasks,并且只允许 GET 的 HTTP 方法。

    这个函数的响应不是文本,我们使用 JSON 数据格式来响应,Flask 的 jsonify 函数从我们的数据结构中生成。

    使用网页浏览器来测试我们的 web service 不是一个最好的注意,因为网页浏览器上不能轻易地模拟所有的 HTTP 请求的方法。相反,我们会使用 curl。如果你还没有安装 curl 的话,请立即安装它。

    通过执行 app.py,启动 web service。接着打开一个新的控制台窗口,运行以下命令:

    $ curl -i http://localhost:5000/todo/api/v1.0/tasks
    HTTP/1.0 200 OK
    Content-Type: application/json
    Content-Length: 294
    Server: Werkzeug/0.8.3 Python/2.7.3
    Date: Mon, 20 May 2013 04:53:53 GMT
    
    {
      "tasks": [
        {
          "description": "Milk, Cheese, Pizza, Fruit, Tylenol",
          "done": false,
          "id": 1,
          "title": "Buy groceries"
        },
        {
          "description": "Need to find a good Python tutorial on the web",
          "done": false,
          "id": 2,
          "title": "Learn Python"
        }
      ]
    }
    

    我们已经成功地调用我们的 RESTful service 的一个函数!

    现在我们开始编写 GET 方法请求我们的任务资源的第二个版本。这是一个用来返回单独一个任务的函数:

    from flask import abort
    
    @app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['GET'])
    def get_task(task_id):
        task = filter(lambda t: t['id'] == task_id, tasks)
        if len(task) == 0:
            abort(404)
        return jsonify({'task': task[0]})
    

    第二个函数有些意思。这里我们得到了 URL 中任务的 id,接着 Flask 把它转换成 函数中的 task_id 的参数。

    我们用这个参数来搜索我们的任务数组。如果我们的数据库中不存在搜索的 id,我们将会返回一个类似 404 的错误,根据 HTTP 规范的意思是 “资源未找到”。

    如果我们找到相应的任务,那么我们只需将它用 jsonify 打包成 JSON 格式并将其发送作为响应,就像我们以前那样处理整个任务集合。

    调用 curl 请求的结果如下:

    $ curl -i http://localhost:5000/todo/api/v1.0/tasks/2
    HTTP/1.0 200 OK
    Content-Type: application/json
    Content-Length: 151
    Server: Werkzeug/0.8.3 Python/2.7.3
    Date: Mon, 20 May 2013 05:21:50 GMT
    
    {
      "task": {
        "description": "Need to find a good Python tutorial on the web",
        "done": false,
        "id": 2,
        "title": "Learn Python"
      }
    }
    $ curl -i http://localhost:5000/todo/api/v1.0/tasks/3
    HTTP/1.0 404 NOT FOUND
    Content-Type: text/html
    Content-Length: 238
    Server: Werkzeug/0.8.3 Python/2.7.3
    Date: Mon, 20 May 2013 05:21:52 GMT
    
    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
    <title>404 Not Found</title>
    <h1>Not Found</h1>
    <p>The requested URL was not found on the server.</p><p>If you     entered the URL manually please check your spelling and try again.</p>
    

    当我们请求 id #2 的资源时候,我们获取到了,但是当我们请求 #3 的时候返回了 404 错误。有关错误奇怪的是返回的是 HTML 信息而不是 JSON,这是因为 Flask 按照默认方式生成 404 响应。由于这是一个 Web service 客户端希望我们总是以 JSON 格式回应,所以我们需要改善我们的 404 错误处理程序:

    from flask import make_response
    
    @app.errorhandler(404)
    def not_found(error):
        return make_response(jsonify({'error': 'Not found'}), 404)
    

    我们会得到一个友好的错误提示:

    $ curl -i http://localhost:5000/todo/api/v1.0/tasks/3
    HTTP/1.0 404 NOT FOUND
    Content-Type: application/json
    Content-Length: 26
    Server: Werkzeug/0.8.3 Python/2.7.3
    Date: Mon, 20 May 2013 05:36:54 GMT
    
    {
      "error": "Not found"
    }
    

    接下来就是 POST 方法,我们用来在我们的任务数据库中插入一个新的任务:

    from flask import request
    
    @app.route('/todo/api/v1.0/tasks', methods=['POST'])
    def create_task():
        if not request.json or not 'title' in request.json:
            abort(400)
        task = {
            'id': tasks[-1]['id'] + 1,
            'title': request.json['title'],
            'description': request.json.get('description', ""),
            'done': False
        }
        tasks.append(task)
        return jsonify({'task': task}), 201
    

    添加一个新的任务也是相当容易地。只有当请求以 JSON 格式形式,request.json 才会有请求的数据。如果没有数据,或者存在数据但是缺少 title 项,我们将会返回 400,这是表示请求无效。

    接着我们会创建一个新的任务字典,使用最后一个任务的 id + 1 作为该任务的 id。我们允许 description 字段缺失,并且假设 done 字段设置成 False。

    我们把新的任务添加到我们的任务数组中,并且把新添加的任务和状态 201 响应给客户端。

    使用如下的 curl 命令来测试这个新的函数:

    $ curl -i -H "Content-Type: application/json" -X POST -d '{"title":"Read a book"}' http://localhost:5000/todo/api/v1.0/tasks
    HTTP/1.0 201 Created
    Content-Type: application/json
    Content-Length: 104
    Server: Werkzeug/0.8.3 Python/2.7.3
    Date: Mon, 20 May 2013 05:56:21 GMT
    
    {
      "task": {
        "description": "",
        "done": false,
        "id": 3,
        "title": "Read a book"
      }
    }
    

    注意:如果你在 Windows 上并且运行 Cygwin 版本的 curl,上面的命令不会有任何问题。然而,如果你使用原生的 curl,命令会有些不同:

    curl -i -H "Content-Type: application/json" -X POST -d "{"""title""":"""Read a book"""}" http://localhost:5000/todo/api/v1.0/tasks
    

    当然在完成这个请求后,我们可以得到任务的更新列表:

    $ curl -i http://localhost:5000/todo/api/v1.0/tasks
    HTTP/1.0 200 OK
    Content-Type: application/json
    Content-Length: 423
    Server: Werkzeug/0.8.3 Python/2.7.3
    Date: Mon, 20 May 2013 05:57:44 GMT
    
    {
      "tasks": [
        {
          "description": "Milk, Cheese, Pizza, Fruit, Tylenol",
          "done": false,
          "id": 1,
          "title": "Buy groceries"
        },
        {
          "description": "Need to find a good Python tutorial on the web",
          "done": false,
          "id": 2,
          "title": "Learn Python"
        },
        {
          "description": "",
          "done": false,
          "id": 3,
          "title": "Read a book"
        }
      ]
    }
    

    剩下的两个函数如下所示:

    @app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['PUT'])
    def update_task(task_id):
        task = filter(lambda t: t['id'] == task_id, tasks)
        if len(task) == 0:
            abort(404)
        if not request.json:
            abort(400)
        if 'title' in request.json and type(request.json['title']) != unicode:
            abort(400)
        if 'description' in request.json and type(request.json['description']) is not unicode:
            abort(400)
        if 'done' in request.json and type(request.json['done']) is not bool:
            abort(400)
        task[0]['title'] = request.json.get('title', task[0]['title'])
        task[0]['description'] = request.json.get('description', task[0]['description'])
        task[0]['done'] = request.json.get('done', task[0]['done'])
        return jsonify({'task': task[0]})
    
    @app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['DELETE'])
    def delete_task(task_id):
        task = filter(lambda t: t['id'] == task_id, tasks)
        if len(task) == 0:
            abort(404)
        tasks.remove(task[0])
        return jsonify({'result': True})
    

    delete_task 函数没有什么特别的。对于 update_task 函数,我们需要严格地检查输入的参数以防止可能的问题。我们需要确保在我们把它更新到数据库之前,任何客户端提供我们的是预期的格式。

    更新任务 #2 的函数调用如下所示:

    $ curl -i -H "Content-Type: application/json" -X PUT -d '{"done":true}' http://localhost:5000/todo/api/v1.0/tasks/2
    HTTP/1.0 200 OK
    Content-Type: application/json
    Content-Length: 170
    Server: Werkzeug/0.8.3 Python/2.7.3
    Date: Mon, 20 May 2013 07:10:16 GMT
    
    {
      "task": [
        {
          "description": "Need to find a good Python tutorial on the web",
          "done": true,
          "id": 2,
          "title": "Learn Python"
        }
      ]
    }
    

    优化 web service 接口

    目前 API 的设计的问题就是迫使客户端在任务标识返回后去构造 URIs。这对于服务器是十分简单的,但是间接地迫使客户端知道这些 URIs 是如何构造的,这将会阻碍我们以后变更这些 URIs。

    不直接返回任务的 ids,我们直接返回控制这些任务的完整的 URI,以便客户端可以随时使用这些 URIs。为此,我们可以写一个小的辅助函数生成一个 “公共” 版本任务发送到客户端:

    from flask import url_for
    
    def make_public_task(task):
        new_task = {}
        for field in task:
            if field == 'id':
                new_task['uri'] = url_for('get_task', task_id=task['id'], _external=True)
            else:
                new_task[field] = task[field]
        return new_task
    

    这里所有做的事情就是从我们数据库中取出任务并且创建一个新的任务,这个任务的 id 字段被替换成通过 Flask 的 url_for 生成的 uri 字段。

    当我们返回所有的任务列表的时候,在发送到客户端之前通过这个函数进行处理:

    @app.route('/todo/api/v1.0/tasks', methods=['GET'])
    def get_tasks():
        return jsonify({'tasks': map(make_public_task, tasks)})
    

    这里就是客户端获取任务列表的时候得到的数据:

    $ curl -i http://localhost:5000/todo/api/v1.0/tasks
    HTTP/1.0 200 OK
    Content-Type: application/json
    Content-Length: 406
    Server: Werkzeug/0.8.3 Python/2.7.3
    Date: Mon, 20 May 2013 18:16:28 GMT
    
    {
      "tasks": [
        {
          "title": "Buy groceries",
          "done": false,
          "description": "Milk, Cheese, Pizza, Fruit, Tylenol",
          "uri": "http://localhost:5000/todo/api/v1.0/tasks/1"
        },
        {
          "title": "Learn Python",
          "done": false,
          "description": "Need to find a good Python tutorial on the web",
          "uri": "http://localhost:5000/todo/api/v1.0/tasks/2"
        }
      ]
    }
    

    我们将会把上述的方式应用到其它所有的函数上以确保客户端一直看到 URIs 而不是 ids。

    加强 RESTful web service 的安全性

    我们已经完成了我们 web service 的大部分功能,但是仍然有一个问题。我们的 web service 对任何人都是公开的,这并不是一个好主意。

    我们有一个可以管理我们的待办事项完整的 web service,但在当前状态下的 web service 是开放给所有的客户端。 如果一个陌生人弄清我们的 API 是如何工作的,他或她可以编写一个客户端访问我们的 web service 并且毁坏我们的数据。

    大部分初级的教程会忽略这个问题并且到此为止。在我看来这是一个很严重的问题,我必须指出。

    确保我们的 web service 安全服务的最简单的方法是要求客户端提供一个用户名和密码。在常规的 web 应用程序会提供一个登录的表单用来认证,并且服务器会创建一个会话为登录的用户以后的操作使用,会话的 id 以 cookie 形式存储在客户端浏览器中。然而 REST 的规则之一就是 “无状态”, 因此我们必须要求客户端在每一次请求中提供认证的信息。

    我们一直试着尽可能地坚持 HTTP 标准协议。既然我们需要实现认证我们需要在 HTTP 上下文中去完成,HTTP 协议提供了两种认证机制: Basic 和 Digest

    有一个小的 Flask 扩展能够帮助我们,我们可以先安装 Flask-HTTPAuth:

    $ flask/bin/pip install flask-httpauth
    

    比方说,我们希望我们的 web service 只让访问用户名 miguel 和密码 python 的客户端访问。 我们可以设置一个基本的 HTTP 验证如下:

    from flask.ext.httpauth import HTTPBasicAuth
    auth = HTTPBasicAuth()
    
    @auth.get_password
    def get_password(username):
        if username == 'miguel':
            return 'python'
        return None
    
    @auth.error_handler
    def unauthorized():
        return make_response(jsonify({'error': 'Unauthorized access'}), 401)
    

    get_password 函数是一个回调函数,Flask-HTTPAuth 使用它来获取给定用户的密码。在一个更复杂的系统中,这个函数是需要检查一个用户数据库,但是在我们的例子中只有单一的用户因此没有必要。

    error_handler 回调函数是用于给客户端发送未授权错误代码。像我们处理其它的错误代码,这里我们定制一个包含 JSON 数据格式而不是 HTML 的响应。

    随着认证系统的建立,所剩下的就是把需要认证的函数添加 @auth.login_required 装饰器。例如:

    @app.route('/todo/api/v1.0/tasks', methods=['GET'])
    @auth.login_required
    def get_tasks():
        return jsonify({'tasks': tasks})
    

    如果现在要尝试使用 curl 调用这个函数我们会得到:

    $ curl -i http://localhost:5000/todo/api/v1.0/tasks
    HTTP/1.0 401 UNAUTHORIZED
    Content-Type: application/json
    Content-Length: 36
    WWW-Authenticate: Basic realm="Authentication Required"
    Server: Werkzeug/0.8.3 Python/2.7.3
    Date: Mon, 20 May 2013 06:41:14 GMT
    
    {
      "error": "Unauthorized access"
    }
    

    为了能够调用这个函数我们必须发送我们的认证凭据:

    $ curl -u miguel:python -i http://localhost:5000/todo/api/v1.0/tasks
    HTTP/1.0 200 OK
    Content-Type: application/json
    Content-Length: 316
    Server: Werkzeug/0.8.3 Python/2.7.3
    Date: Mon, 20 May 2013 06:46:45 GMT
    
    {
      "tasks": [
        {
          "title": "Buy groceries",
          "done": false,
          "description": "Milk, Cheese, Pizza, Fruit, Tylenol",
          "uri": "http://localhost:5000/todo/api/v1.0/tasks/1"
        },
        {
          "title": "Learn Python",
          "done": false,
          "description": "Need to find a good Python tutorial on the web",
          "uri": "http://localhost:5000/todo/api/v1.0/tasks/2"
        }
      ]
    }
    

    认证扩展给予我们很大的自由选择哪些函数需要保护,哪些函数需要公开。

    为了确保登录信息的安全应该使用 HTTP 安全服务器(例如: https://...),这样客户端和服务器之间的通信都是加密的,以防止传输过程中第三方看到认证的凭据。

    让人不舒服的是当请求收到一个 401 的错误,网页浏览都会跳出一个丑陋的登录框,即使请求是在后台发生的。因此如果我们要实现一个完美的 web 服务器的话,我们就需要禁止跳转到浏览器显示身份验证对话框,让我们的客户端应用程序自己处理登录。

    一个简单的方式就是不返回 401 错误。403 错误是一个令人青睐的替代,403 错误表示 “禁止” 的错误:

    @auth.error_handler
    def unauthorized():
        return make_response(jsonify({'error': 'Unauthorized access'}), 403)
    

    可能的改进

    我们编写的小型的 web service 还可以在不少的方面进行改进。

    对于初学者来说,一个真正的 web service 需要一个真实的数据库进行支撑。我们现在使用的内存数据结构会有很多限制不应该被用于真正的应用。

    另外一个可以提高的领域就是处理多用户。如果系统支持多用户的话,不同的客户端可以发送不同的认证凭证获取相应用户的任务列表。在这样一个系统中的话,我们需要第二个资源就是用户。在用户资源上的 POST 的请求代表注册换一个新用户。一个 GET 请求表示客户端获取一个用户的信息。一个 PUT 请求表示更新用户信息,比如可能是更新邮箱地址。一个 DELETE 请求表示删除用户账号。

    GET 检索任务列表请求可以在几个方面进行扩展。首先可以携带一个可选的页的参数,以便客户端请求任务的一部分。另外,这种扩展更加有用:允许按照一定的标准筛选。比如,用户只想要看到完成的任务,或者只想看到任务的标题以 A 字母开头。所有的这些都可以作为 URL 的一个参数项。

    参考:

    http://www.pythondoc.com/flask-restful/first.html

    http://blog.miguelgrinberg.com/post/designing-a-restful-api-with-python-and-flask

    展开全文
  • WebSocket 实战

    万次阅读 2015-07-18 23:05:16
    本文介绍了 HTML5 WebSocket 的由来,运作机制及客户端和服务端的 API 实现,重点介绍服务端(基于 Tomcat7)及客户端(基于浏览器原生 HTML5 API)实现的详细步骤;并通过实际客户案例描述了客户端如何在 WebSocket...

    本文介绍了 HTML5 WebSocket 的由来,运作机制及客户端和服务端的 API 实现,重点介绍服务端(基于 Tomcat7)及客户端(基于浏览器原生 HTML5 API)实现的详细步骤;并通过实际客户案例描述了客户端如何在 WebSocket 架构下使用 HTTP 长连接与服务器实现实时通信及消息推送的功能,读者通过阅读本文中案例示例代码的实现,能够更深刻理解 WebSocket 框架的技术原理和开发方法。

    唐 清原, 咨询顾问

    2015 年 2 月 05 日

    • +内容

    WebSocket 前世今生

    众所周知,Web 应用的交互过程通常是客户端通过浏览器发出一个请求,服务器端接收请求后进行处理并返回结果给客户端,客户端浏览器将信息呈现,这种机制对于信息变化不是特别频繁的应用尚可,但对于实时要求高、海量并发的应用来说显得捉襟见肘,尤其在当前业界移动互联网蓬勃发展的趋势下,高并发与用户实时响应是 Web 应用经常面临的问题,比如金融证券的实时信息,Web 导航应用中的地理位置获取,社交网络的实时消息推送等。

    传统的请求-响应模式的 Web 开发在处理此类业务场景时,通常采用实时通讯方案,常见的是:

    • 轮询,原理简单易懂,就是客户端通过一定的时间间隔以频繁请求的方式向服务器发送请求,来保持客户端和服务器端的数据同步。问题很明显,当客户端以固定频率向服务器端发送请求时,服务器端的数据可能并没有更新,带来很多无谓请求,浪费带宽,效率低下。
    • 基于 Flash,AdobeFlash 通过自己的 Socket 实现完成数据交换,再利用 Flash 暴露出相应的接口为 JavaScript 调用,从而达到实时传输目的。此方式比轮询要高效,且因为 Flash 安装率高,应用场景比较广泛,但在移动互联网终端上 Flash 的支持并不好。IOS 系统中没有 Flash 的存在,在 Android 中虽然有 Flash 的支持,但实际的使用效果差强人意,且对移动设备的硬件配置要求较高。2012 年 Adobe 官方宣布不再支持 Android4.1+系统,宣告了 Flash 在移动终端上的死亡。

    从上文可以看出,传统 Web 模式在处理高并发及实时性需求的时候,会遇到难以逾越的瓶颈,我们需要一种高效节能的双向通信机制来保证数据的实时传输。在此背景下,基于 HTML5 规范的、有 Web TCP 之称的 WebSocket 应运而生。

    早期 HTML5 并没有形成业界统一的规范,各个浏览器和应用服务器厂商有着各异的类似实现,如 IBM 的 MQTT,Comet 开源框架等,直到 2014 年,HTML5 在 IBM、微软、Google 等巨头的推动和协作下终于尘埃落地,正式从草案落实为实际标准规范,各个应用服务器及浏览器厂商逐步开始统一,在 JavaEE7 中也实现了 WebSocket 协议,从而无论是客户端还是服务端的 WebSocket 都已完备,读者可以查阅HTML5 规范,熟悉新的 HTML 协议规范及 WebSocket 支持。

    WebSocket 机制

    以下简要介绍一下 WebSocket 的原理及运行机制。

    WebSocket 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯,它建立在 TCP 之上,同 HTTP 一样通过 TCP 来传输数据,但是它和 HTTP 最大不同是:

    • WebSocket 是一种双向通信协议,在建立连接后,WebSocket 服务器和 Browser/Client Agent 都能主动的向对方发送或接收数据,就像 Socket 一样;
    • WebSocket 需要类似 TCP 的客户端和服务器端通过握手连接,连接成功后才能相互通信。

    非 WebSocket 模式传统 HTTP 客户端与服务器的交互如下图所示:

    图 1. 传统 HTTP 请求响应客户端服务器交互图
    图 1. 传统 HTTP 请求响应客户端服务器交互图

    使用 WebSocket 模式客户端与服务器的交互如下图:

    图 2.WebSocket 请求响应客户端服务器交互图
    图 2.WebSocket 请求响应客户端服务器交互图

    上图对比可以看出,相对于传统 HTTP 每次请求-应答都需要客户端与服务端建立连接的模式,WebSocket 是类似 Socket 的 TCP 长连接的通讯模式,一旦 WebSocket 连接建立后,后续数据都以帧序列的形式传输。在客户端断开 WebSocket 连接或 Server 端断掉连接前,不需要客户端和服务端重新发起连接请求。在海量并发及客户端与服务器交互负载流量大的情况下,极大的节省了网络带宽资源的消耗,有明显的性能优势,且客户端发送和接受消息是在同一个持久连接上发起,实时性优势明显。

    我们再通过客户端和服务端交互的报文看一下 WebSocket 通讯与传统 HTTP 的不同:

    在客户端,new WebSocket 实例化一个新的 WebSocket 客户端对象,连接类似 ws://yourdomain:port/path 的服务端 WebSocket URL,WebSocket 客户端对象会自动解析并识别为 WebSocket 请求,从而连接服务端端口,执行双方握手过程,客户端发送数据格式类似:

    清单 1.WebSocket 客户端连接报文
    GET /webfin/websocket/ HTTP/1.1
    Host: localhost
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Key: xqBt3ImNzJbYqRINxEFlkg==
    Origin: http://localhost:8080
    Sec-WebSocket-Version: 13

    可以看到,客户端发起的 WebSocket 连接报文类似传统 HTTP 报文,”Upgrade:websocket”参数值表明这是 WebSocket 类型请求,“Sec-WebSocket-Key”是 WebSocket 客户端发送的一个 base64 编码的密文,要求服务端必须返回一个对应加密的“Sec-WebSocket-Accept”应答,否则客户端会抛出“Error during WebSocket handshake”错误,并关闭连接。

    服务端收到报文后返回的数据格式类似:

    清单 2.WebSocket 服务端响应报文
    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: K7DJLdLooIwIG/MOpvWFB3y3FE8=

    “Sec-WebSocket-Accept”的值是服务端采用与客户端一致的密钥计算出来后返回客户端的,“HTTP/1.1 101 Switching Protocols”表示服务端接受 WebSocket 协议的客户端连接,经过这样的请求-响应处理后,客户端服务端的 WebSocket 连接握手成功, 后续就可以进行 TCP 通讯了。读者可以查阅WebSocket 协议栈了解 WebSocket 客户端和服务端更详细的交互数据格式。

    在开发方面,WebSocket API 也十分简单,我们只需要实例化 WebSocket,创建连接,然后服务端和客户端就可以相互发送和响应消息,在下文 WebSocket 实现及案例分析部分,可以看到详细的 WebSocket API 及代码实现。

    WebSocket 实现

    如上文所述,WebSocket 的实现分为客户端和服务端两部分,客户端(通常为浏览器)发出 WebSocket 连接请求,服务端响应,实现类似 TCP 握手的动作,从而在浏览器客户端和 WebSocket 服务端之间形成一条 HTTP 长连接快速通道。两者之间后续进行直接的数据互相传送,不再需要发起连接和相应。

    以下简要描述 WebSocket 服务端 API 及客户端 API。

    WebSocket 服务端 API

    WebSocket 服务端在各个主流应用服务器厂商中已基本获得符合 JEE JSR356 标准规范 API 的支持(详见JSR356 WebSocket API 规范),以下列举了部分常见的商用及开源应用服务器对 WebSocket Server 端的支持情况:

    表 1.WebSocket 服务端支持
    厂商 应用服务器 备注
    IBM WebSphere WebSphere 8.0 以上版本支持,7.X 之前版本结合 MQTT 支持类似的 HTTP 长连接
    甲骨文 WebLogic WebLogic 12c 支持,11g 及 10g 版本通过 HTTP Publish 支持类似的 HTTP 长连接
    微软 IIS IIS 7.0+支持
    Apache Tomcat Tomcat 7.0.5+支持,7.0.2X 及 7.0.3X 通过自定义 API 支持
      Jetty Jetty 7.0+支持

    以下我们使用 Tomcat7.0.5 版本的服务端示例代码说明 WebSocket 服务端的实现:

    JSR356 的 WebSocket 规范使用 javax.websocket.*的 API,可以将一个普通 Java 对象(POJO)使用 @ServerEndpoint 注释作为 WebSocket 服务器的端点,代码示例如下:

    清单 3.WebSocket 服务端 API 示例
     @ServerEndpoint("/echo")
     public class EchoEndpoint {
    
     @OnOpen
     public void onOpen(Session session) throws IOException {
     //以下代码省略...
     }
     
     @OnMessage
     public String onMessage(String message) {
     //以下代码省略...
     }
    
     @Message(maxMessageSize=6)
     public void receiveMessage(String s) {
     //以下代码省略...
     } 
    
     @OnError
     public void onError(Throwable t) {
     //以下代码省略...
     }
     
     @OnClose
     public void onClose(Session session, CloseReason reason) {
     //以下代码省略...
     } 
     
     }

    代码解释:

    上文的简洁代码即建立了一个 WebSocket 的服务端,@ServerEndpoint("/echo") 的 annotation 注释端点表示将 WebSocket 服务端运行在 ws://[Server 端 IP 或域名]:[Server 端口]/websockets/echo 的访问端点,客户端浏览器已经可以对 WebSocket 客户端 API 发起 HTTP 长连接了。

    使用 ServerEndpoint 注释的类必须有一个公共的无参数构造函数,@onMessage 注解的 Java 方法用于接收传入的 WebSocket 信息,这个信息可以是文本格式,也可以是二进制格式。

    OnOpen 在这个端点一个新的连接建立时被调用。参数提供了连接的另一端的更多细节。Session 表明两个 WebSocket 端点对话连接的另一端,可以理解为类似 HTTPSession 的概念。

    OnClose 在连接被终止时调用。参数 closeReason 可封装更多细节,如为什么一个 WebSocket 连接关闭。

    更高级的定制如 @Message 注释,MaxMessageSize 属性可以被用来定义消息字节最大限制,在示例程序中,如果超过 6 个字节的信息被接收,就报告错误和连接关闭。

    注意:早期不同应用服务器支持的 WebSocket 方式不尽相同,即使同一厂商,不同版本也有细微差别,如 Tomcat 服务器 7.0.5 以上的版本都是标准 JSR356 规范实现,而 7.0.2x/7.0.3X 的版本使用自定义 API (WebSocketServlet 和 StreamInbound, 前者是一个容器,用来初始化 WebSocket 环境;后者是用来具体处理 WebSocket 请求和响应,详见案例分析部分),且 Tomcat7.0.3x 与 7.0.2x 的 createWebSocketInbound 方法的定义不同,增加了一个 HttpServletRequest 参数,使得可以从 request 参数中获取更多 WebSocket 客户端的信息,如下代码所示:

    清单 4.Tomcat7.0.3X 版本 WebSocket API
    public class EchoServlet extends WebSocketServlet {
    @Override
    protected StreamInbound createWebSocketInbound(String subProtocol,
    HttpServletRequest request) {
     //以下代码省略....
    return new MessageInbound() {
     //以下代码省略....
    }
    protected void onBinaryMessage(ByteBuffer buffer)
    throws IOException {
     //以下代码省略...
    }
    protected void onTextMessage(CharBuffer buffer) throws IOException {
     getWsOutbound().writeTextMessage(buffer);
     //以下代码省略...
    }
    };
    }
    }

    因此选择 WebSocket 的 Server 端重点需要选择其版本,通常情况下,更新的版本对 WebSocket 的支持是标准 JSR 规范 API,但也要考虑开发易用性及老版本程序移植性等方面的问题,如下文所述的客户案例,就是因为客户要求统一应用服务器版本所以使用的 Tomcat 7.0.3X 版本的 WebSocketServlet 实现,而不是 JSR356 的 @ServerEndpoint 注释端点。

    WebSocket 客户端 API

    对于 WebSocket 客户端,主流的浏览器(包括 PC 和移动终端)现已都支持标准的 HTML5 的 WebSocket API,这意味着客户端的 WebSocket JavaScirpt 脚本具备良好的一致性和跨平台特性,以下列举了常见的浏览器厂商对 WebSocket 的支持情况:

    表 2.WebSocket 客户端支持
    浏览器 支持情况
    Chrome Chrome version 4+支持
    Firefox Firefox version 5+支持
    IE IE version 10+支持
    Safari IOS 5+支持
    Android Brower Android 4.5+支持

    客户端 WebSocket API 基本上已经在各个主流浏览器厂商中实现了统一,因此使用标准 HTML5 定义的 WebSocket 客户端的 JavaScript API 即可,当然也可以使用业界满足 WebSocket 标准规范的开源框架,如 Socket.io。

    以下以一段代码示例说明 WebSocket 的客户端实现:

    清单 5.WebSocket 客户端 API 示例
    var ws = new WebSocket(“ws://echo.websocket.org”); 
     ws.onopen = function(){ws.send(“Test!”); }; 
     ws.onmessage = function(evt){console.log(evt.data);ws.close();}; 
     ws.onclose = function(evt){console.log(“WebSocketClosed!”);}; 
     ws.onerror = function(evt){console.log(“WebSocketError!”);};

    第一行代码是在申请一个 WebSocket 对象,参数是需要连接的服务器端的地址,同 HTTP 协议开头一样,WebSocket 协议的 URL 使用 ws://开头,另外安全的 WebSocket 协议使用 wss://开头。

    第二行到第五行为 WebSocket 对象注册消息的处理函数,WebSocket 对象一共支持四个消息 onopen, onmessage, onclose 和 onerror,有了这 4 个事件,我们就可以很容易很轻松的驾驭 WebSocket。

    当 Browser 和 WebSocketServer 连接成功后,会触发 onopen 消息;如果连接失败,发送、接收数据失败或者处理数据出现错误,browser 会触发 onerror 消息;当 Browser 接收到 WebSocketServer 发送过来的数据时,就会触发 onmessage 消息,参数 evt 中包含 Server 传输过来的数据;当 Browser 接收到 WebSocketServer 端发送的关闭连接请求时,就会触发 onclose 消息。我们可以看出所有的操作都是采用异步回调的方式触发,这样不会阻塞 UI,可以获得更快的响应时间,更好的用户体验。

    WebSocket 案例分析

    以下我们以一个真实的客户案例来分析说明 WebSocket 的优势及具体开发实现(为保护客户隐私,以下描述省去客户名,具体涉及业务细节的代码在文中不再累述)。

    案例介绍

    该客户为一个移动设备制造商,移动设备装载的是 Android/IOS 操作系统,设备分两类(以下简称 A,B 两类),A 类设备随时处于移动状态中,B 类设备为 A 类设备的管理控制设备,客户需要随时在 B 类设备中看到所属 A 类设备的地理位置信息及状态信息。如 A 类设备上线,离线的时候,B 类设备需要立即获得消息通知,A 类设备上报时,B 类设备也需要实时获得该上报 A 类设备的地理位置信息。

    为降低跨平台的难度及实施工作量,客户考虑轻量级的 Web App 的方式屏蔽 Android/IOS 平台的差异性,A 类设备数量众多,且在工作状态下 A 类设备处于不定时的移动状态,而 B 类设备对 A 类设备状态变化的感知实时性要求很高(秒级)。

    根据以上需求,A/B 类设备信息存放在后台数据库中,A/B 类设备的交互涉及 Web 客户端/服务器频繁和高并发的请求-相应,如果使用传统的 HTTP 请求-响应模式,B 类设备的 Web App 上需要对服务进行轮询,势必会对服务器带来大的负载压力,且当 A 类设备没有上线或者上报等活动事件时,B 类设备的轮询严重浪费网络资源。

    解决方案

    综上所述,项目采用 WebSocket 技术实现实时消息的通知及推送,每当 A 类设备/B 类设备上线登录成功即打开 WebSocket 的 HTTP 长连接,新的 A 类设备上线,位置变化,离线等状态变化通过 WebSocket 发送实时消息,WebSocket Server 端处理 A 类设备的实时消息,并向所从属的 B 类设备实时推送。

    WebSocket 客户端使用 jQuery Mobile(jQuery Mobile 移动端开发在本文中不再详细描述,感兴趣的读者可以参考jQuery Mobile 简介),使用原生 WebSocket API 实现与服务端交互。

    服务端沿用客户已有的应用服务器 Tomcat 7.0.33 版本,使用 Apache 自定义 API 实现 WebSocket Server 端,为一个上线的 A 类设备生成一个 WebSocket 的 HTTP 长连接,每当 A 类设备有上线,位置更新,离线等事件的时候,客户端发送文本消息,服务端识别并处理后,向所属 B 类设备发送实时消息,B 类设备客户端接收消息后,识别到 A 类设备的相应事件,完成对应的 A 类设备位置刷新以及其他业务操作。

    其涉及的 A 类设备,B 类设备及后台服务器交互时序图如下:

    图 3:A/B 类设备 WebSocket 交互图
    图 3:A/B 类设备 WebSocket 交互图

    A/B 类设备的 WebSocket 客户端封装在 websocket.js 的 JavaScript 代码中,与 jQuery MobileApp 一同打包为移动端 apk/ipa 安装包;WebSocket 服务端实现主要为 WebSocketDeviceServlet.java, WebSocketDeviceInbound.java,WebSocketDeviceInboundPool.java 几个类。下文我们一一介绍其具体代码实现。

    代码实现

    在下文中我们把本案例中的主要代码实现做解释说明,读者可以下载完整的代码清单做详细了解。

    WebSocketDeviceServlet 类

    A 类设备或者 B 类设备发起 WebSocket 长连接后,服务端接受请求的是 WebSocketDeviceServlet 类,跟传统 HttpServlet 不同的是,WebSocketDeviceServlet 类实现 createWebSocketInbound 方法,类似 SocketServer 的 accept 方法,新生产的 WebSocketInbound 实例对应客户端 HTTP 长连接,处理与客户端交互功能。

    WebSocketDeviceServlet 服务端代码示例如下:

    清单 6.WebSocketDeviceServlet.java 代码示例
    public class WebSocketDeviceServlet extends org.apache.catalina.websocket.WebSocketServlet {
    
    private static final long serialVersionUID = 1L;
    
     @Override
     protected StreamInbound createWebSocketInbound(String subProtocol,HttpServletRequest request) {
     
     WebSocketDeviceInbound newClientConn = new WebSocketDeviceInbound(request);
     WebSocketDeviceInboundPool.addMessageInbound(newClientConn);
     return newClientConn;
     
     }
    
    }

    代码解释:

    WebSocketServlet 是 WebSocket 协议的后台监听进程,和传统 HTTP 请求一样,WebSocketServlet 类似 Spring/Struct 中的 Servlet 监听进程,只不过通过客户端 ws 的前缀指定了其监听的协议为 WebSocket。

    WebSocketDeviceInboundPool 实现了类似 JDBC 数据库连接池的客户端 WebSocket 连接池功能,并统一处理 WebSocket 服务端对单个客户端/多个客户端(同组 A 类设备)的消息推送,详见 WebSocketDeviceInboundPool 代码类解释。

    WebSocketDeviceInboundl 类

    WebSocketDeviceInbound 类为每个 A 类和 B 类设备验证登录后,客户端建立的 HTTP 长连接的对应后台服务类,类似 Socket 编程中的 SocketServer accept 后的 Socket 进程,在 WebSocketInbound 中接收客户端发送的实时位置信息等消息,并向客户端(B 类设备)发送下属 A 类设备实时位置信息及位置分析结果数据,输入流和输出流都是 WebSocket 协议定制的。WsOutbound 负责输出结果,StreamInbound 和 WsInputStream 负责接收数据:

    清单 7.WebSocketDeviceInbound.java 类代码示例
    public class WebSocketDeviceInbound extends MessageInbound {
    private final HttpServletRequest request;
    private DeviceAccount connectedDevice;
    
    public DeviceAccount getConnectedDevice() {
    return connectedDevice;
    }
    
    
    public void setConnectedDevice(DeviceAccount connectedDevice) {
    this.connectedDevice = connectedDevice;
    }
    
    
    public HttpServletRequest getRequest() {
    return request;
    }
    
    
    public WebSocketDeviceInbound(HttpServletRequest request) {
    this.request = request;
    DeviceAccount connectedDa = (DeviceAccount)request.getSession(true).getAttribute("connectedDevice");
    if(connectedDa==null)
    {
    String deviceId = request.getParameter("id");
    DeviceAccountDao deviceDao = new DeviceAccountDao();
    connectedDa = deviceDao.getDaById(Integer.parseInt(deviceId));
    }
    this.setConnectedDevice(connectedDa);
    }
    
    	
    @Override
    protected void onOpen(WsOutbound outbound) {
     /
    
    }
    
    @Override
    protected void onClose(int status) {
    WebSocketDeviceInboundPool.removeMessageInbound(this);
    
    }
    
    @Override
    protected void onBinaryMessage(ByteBuffer message) throws IOException {
    throw new UnsupportedOperationException("Binary message not supported.");
    }
    
    @Override
    protected void onTextMessage(CharBuffer message) throws IOException {
    WebSocketDeviceInboundPool.processTextMessage(this, message.toString());
    
    }
    
    
    public void sendMessage(BaseEvent event)
    {
    String eventStr = JSON.toJSONString(event);
    try {
    this.getWsOutbound().writeTextMessage(CharBuffer.wrap(eventStr));
    //…以下代码省略
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }

    代码解释:

    connectedDevice 是当前连接的 A/B 类客户端设备类实例,在这里做为成员变量以便后续处理交互。

    sendMessage 函数向客户端发送数据,使用 Websocket WsOutbound 输出流向客户端推送数据,数据格式统一为 JSON。

    onTextMessage 函数为客户端发送消息到服务器时触发事件,调用 WebSocketDeviceInboundPool 的 processTextMessage 统一处理 A 类设备的登入,更新位置,离线等消息。

    onClose 函数触发关闭事件,在连接池中移除连接。

    WebSocketDeviceInbound 构造函数为客户端建立连接后,WebSocketServlet 的 createWebSocketInbound 函数触发,查询 A 类/B 类设备在后台数据库的详细数据并实例化 connectedDevice 做为 WebSocketDeviceInbound 的成员变量,WebSocketServlet 类此时将新的 WebSocketInbound 实例加入自定义的 WebSocketDeviceInboundPool 连接池中,以便统一处理 A/B 设备组员关系及位置分布信息计算等业务逻辑。

    WebSocketDeviceInboundPool 类

    WebSocketInboundPool 类: 由于需要处理大量 A 类 B 类设备的实时消息,服务端会同时存在大量 HTTP 长连接,为统一管理和有效利用 HTTP 长连接资源,项目中使用了简单的 HashMap 实现内存连接池机制,每次设备登入新建的 WebSocketInbound 都放入 WebSocketInbound 实例的连接池中,当设备登出时,从连接池中 remove 对应的 WebSocketInbound 实例。

    此外,WebSocketInboundPool 类还承担 WebSocket 客户端处理 A 类和 B 类设备间消息传递的作用,在客户端发送 A 类设备登入、登出及位置更新消息的时候,服务端 WebSocketInboundPool 进行位置分布信息的计算,并将计算完的结果向同时在线的 B 类设备推送。

    清单 8.WebSocketDeviceInboundPool.java 代码示例
    public class WebSocketDeviceInboundPool {
    
    private static final ArrayList<WebSocketDeviceInbound> connections =
    new ArrayList<WebSocketDeviceInbound>();
    
    public static void addMessageInbound(WebSocketDeviceInbound inbound){
    //添加连接
    DeviceAccount da = inbound.getConnectedDevice();
    System.out.println("新上线设备 : " + da.getDeviceNm());
    connections.add(inbound);
    }
    
    public static ArrayList<DeviceAccount> getOnlineDevices(){
    ArrayList<DeviceAccount> onlineDevices = new ArrayList<DeviceAccount>();
    for(WebSocketDeviceInbound webClient:connections)
    {
    onlineDevices.add(webClient.getConnectedDevice());
    }
    return onlineDevices;
    }
    
    public static WebSocketDeviceInbound getGroupBDevices(String group){
    WebSocketDeviceInbound retWebClient =null;
    for(WebSocketDeviceInbound webClient:connections)
    {
    if(webClient.getConnectedDevice().getDeviceGroup().equals(group)&&
    webClient.getConnectedDevice().getType().equals("B")){
    retWebClient = webClient;
    }
    }
    return retWebClient;
    }
    public static void removeMessageInbound(WebSocketDeviceInbound inbound){
    //移除连接
    System.out.println("设备离线 : " + inbound.getConnectedDevice());
    connections.remove(inbound);
    }
    
    public static void processTextMessage(WebSocketDeviceInbound inbound,String message){
    
    
    BaseEvent receiveEvent = (BaseEvent)JSON.parseObject(message.toString(),BaseEvent.class);
    DBEventHandleImpl dbEventHandle = new DBEventHandleImpl();
    dbEventHandle.setReceiveEvent(receiveEvent);
    dbEventHandle.HandleEvent();
    if(receiveEvent.getEventType()==EventConst.EVENT_MATCHMATIC_RESULT||
    receiveEvent.getEventType()==EventConst.EVENT_GROUP_DEVICES_RESULT||
    receiveEvent.getEventType()==EventConst.EVENT_A_REPAIRE){
    String clientDeviceGroup = ((ArrayList<DeviceAccount>)
    receiveEvent.getEventObjs()).get(0).getDeviceGroup();
    WebSocketDeviceInbound bClient = getGroupBDevices(clientDeviceGroup);
    if(bClient!=null){
    sendMessageToSingleClient(bClient,dbEventHandle.getReceiveEvent());
    }
    }
    }
    }
    public static void sendMessageToAllDevices(BaseEvent event){
    try {
    for (WebSocketDeviceInbound webClient : connections) {
    webClient.sendMessage(event);
    }
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    public static void sendMessageToSingleClient(WebSocketDeviceInbound webClient,BaseEvent event){
    
    try {
    webClient.sendMessage(event);
    
    }
    catch (Exception e) {
    e.printStackTrace();
    }
    }
    }

    代码解释:

    addMessageInbound 函数向连接池中添加客户端建立好的连接。

    getOnlineDevices 函数获取所有的连线的 A/B 类设备。

    removeMessageInbound 函数实现 A 类设备或者 B 类设备离线退出(服务端收到客户端关闭 WebSocket 连接事件,触发 WebSocketInbound 中的 onClose 方法),从连接池中删除连接设备客户端的连接实例。

    processTextMessage 完成处理客户端消息,这里使用了消息处理的机制,包括解码客户端消息,根据消息构造 Event 事件,通过 EventHandle 多线程处理,处理完后向客户端返回,可以向该组 B 设备推送消息,也可以向发送消息的客户端推送消息。

    sendMessageToAllDevices 函数实现发送数据给所有在线 A/B 类设备客户端。sendMessageToSingleClient 函数实现向某一 A/B 类设备客户端发送数据。

    websocket.js 客户端代码

    客户端代码 websocket.js,客户端使用标准 HTML5 定义的 WebSocket API,从而保证支持 IE9+,Chrome,FireFox 等多种浏览器,并结合 jQueryJS 库 API 处理 JSON 数据的处理及发送。

    清单 9:客户端 WebSocket.js 脚本示例
    var websocket=window.WebSocket || window.MozWebSocket; 
    var isConnected = false;
    
    function doOpen(){
     isConnected = true;
    if(deviceType=='B'){
     mapArea='mapB';
     doLoginB(mapArea);
     }
     else{
     mapArea='mapA';
     doLoginA(mapArea);
     }
    
    }
    
    function doClose(){
    showDiagMsg("infoField","已经断开连接", "infoDialog");
    isConnected = false;
    }
    
    function doError() {
    showDiagMsg("infoField","连接异常!", "infoDialog");
    isConnected = false;
    
    }
    
    function doMessage(message){
    var event = $.parseJSON(message.data);
    doReciveEvent(event);
    }
    
    function doSend(message) {
    if (websocket != null) {
    websocket.send(JSON.stringify(message));
    } else {
    showDiagMsg("infoField","您已经掉线,无法与服务器通信!", "infoDialog");
    }
    }
    
    //初始话 WebSocket
    function initWebSocket(wcUrl) {
    if (window.WebSocket) {
    websocket = new WebSocket(encodeURI(wcUrl));
    websocket.onopen = doOpen;
    websocket.onerror = doError;
    websocket.onclose = doClose;
    websocket.onmessage = doMessage;
    }
    else{
    showDiagMsg("infoField","您的设备不支持 webSocket!", "infoDialog");
    
    }
    };
    
    function doReciveEvent(event){
    //设备不存在,客户端断开连接
    if(event.eventType==101){
    showDiagMsg("infoField","设备不存在或设备号密码错!", "infoDialog");
    websocket.close();
    }
    //返回组设备及计算目标位置信息,更新地图
    else if(event.eventType==104||event.eventType==103){
    clearGMapOverlays(mapB); 
     $.each(event.eventObjs,function(idx,item){
     var deviceNm = item.deviceNm;
     //google api
    // var deviceLocale = new google.maps.LatLng(item.lag,item.lat);
    //baidu api
     var deviceLocale = new BMap.Point(item.lng,item.lat);
     var newMarker;
     if(item.status=='target'){
     newMarker = addMarkToMap(mapB,deviceLocale,deviceNm,true);
     //…以下代码省略
     }
     else{
     newMarker = addMarkToMap(mapB,deviceLocale,deviceNm);
     } 
     markArray.push(newMarker);
     });
     showDiagMsg("infoField","有新报修设备或设备离线, 地图已更新!", "infoDialog");
    }
    
    }

    代码解释:

    doOpen 回调函数处理打开 WebSocket,A 类设备或者 B 类设备连接上 WebSocket 服务端后,将初始化地图并显示默认位置,然后向服务端发送设备登入的消息。

    doReciveEvent 函数处理关闭 WebSocket,A 类/B 类设备离线(退出移动终端上的应用)时,服务端关闭 HTTP 长连接,客户端 WebSocket 对象执行 onclose 回调句柄。

    initWebSocket 初始化 WebSocket,连接 WebSocket 服务端,并设置处理回调句柄,如果浏览器版本过低而不支持 HTML5,提示客户设备不支持 WebSocket。

    doSend 函数处理客户端向服务端发送消息,注意 message 是 JSON OBJ 对象,通过 JSON 标准 API 格式化字符串。

    doMessage 函数处理 WebSocket 服务端返回的消息,后台返回的 message 为 JSON 字符串,通过 jQuery 的 parseJSON API 格式化为 JSON Object 以便客户端处理 doReciveEvent 函数时客户端收到服务端返回消息的具体处理,由于涉及大量业务逻辑在此不再赘述。

    结束语

    以上简要介绍了 WebSocket 的由来,原理机制以及服务端/客户端实现,并以实际客户案例指导并讲解了如何使用 WebSocket 解决实时响应及服务端消息推送方面的问题。本文适用于熟悉 HTML 协议规范和 J2EE Web 编程的读者,旨在帮助读者快速熟悉 HTML5 WebSocket 的原理和开发应用。文中的服务端及客户端项目代码可供下载,修改后可用于用户基于 WebSocket 的 HTTP 长连接的实际生产环境中。

    展开全文
  • 本期内容 微服务系列文章的第一篇介绍了微服务架构模式,讨论了使用微服务的优缺点,以及为什么微服务虽然复杂度高却是复杂应用程序的理想选择。...在本篇文章中,我们来看它如何影响客户端到服务端通信
  • 先使用抓包工具确定服务器接口反回的数据是否正确,这样就可以排除是否是服务器原因如果返回的是服务器报错那麼就是服务器原因, 如果反回的数据与接口文档不符那麼就是接口问题, 最後就是客户端问题.
  • Vue.js——使用$.ajax和vue-resource实现OAuth的注册、登录、注销和API调用 ...这个应用始终遗留了一个问题,Web App在访问REST API时,没有经过任何认证,这使得服务端的REST API是不安全的,只
  • C# WebApi 返回详细错误信息

    千次阅读 2019-03-26 21:09:59
    笔者在写一个 WebApi 项目时,出现 500 错误时访问 API 地址总是返回 An error has occurred. 却无法看到详细错误信息,导致无法调试,本文通过修改全局设置让发生错误显示详细信息。
  • 14.app后端如何设计api

    万次阅读 2016-06-18 20:29:51
    app和后端的交互,一般都是通过后端提供的api实现。api的设计,估计很多刚进入app后端的小伙伴会一无头绪,不知道怎么入门。下面根据自己3年的app后端经验,总结出下几个api设计原则,给小伙伴参考。
  • 服务端向客户端主动发送消息

    万次阅读 2018-07-16 14:33:15
    通常情况下,无论是web浏览器还是移动app,我们与服务器之间的交互都是主动的,客户端向服务器端发出请求,然后服务器端返回数据给客户端客户端浏览器再将信息呈现,客户端与服务端对应的模式是: 客户端请求--...
  • HTTP协议分析

    万次阅读 多人点赞 2019-06-23 12:30:24
    HTTP(HyperText Transfer Protocol)即超文本传输协议,是一种详细规定了浏览器和万维网服务器之间互相通信的规则,它是万维网交换信息的基础,它允许将HTML(超文本标记语言)文档从Web服务器传送到Web浏览器。...
  • Http status code 状态码

    万次阅读 2013-07-15 11:56:27
    HTTP 状态代码 本部分描述 HTTP IIS 7.0 使用的 HTTP 状态代码。 ...本文只包括 ...例如,自定义 Internet Server API (ISAPI) 筛选器或自定义 HTTP 模块可以设置其自己的 HTTP 状态代码。 1xx - 信息 这
  • Python使用Flask搭建RESTful API

    千次阅读 2016-10-19 18:01:11
    使用 Python 和 Flask 设计 RESTful API 近些年来 REST (REpresentational State Transfer) 已经变成了 web services 和 web APIs 的标配。 在本文中我将向你展示如何简单地使用 Python 和 Flask 框架来创建一个 ...
  • 面试官:你连RESTful都不知道我怎么敢要你?

    万次阅读 多人点赞 2020-03-05 21:34:09
    干货,2019 RESTful最贱实践
  • 接口请求返回状态码总结

    万次阅读 2018-07-03 12:49:22
    前端在调用接口时都要关注接口返回给我们的状态码,之前只是大概的了解比如404是前端的错,可能是路径错误等,300+可能是重定向了,500以上是服务端或者服务器的错误,并没有一个很好的总结。今天中午看群里有人问这...
  • 基于REST的Web服务器客户端

    千次阅读 2017-06-06 09:55:04
    摘要 : 基于REST的Web服务客户端是一款可以使用Chrome插件来模拟REST请求来测试REST风格的web Api的谷歌浏览器插件。   基于REST的Web服务客户端的开发背景 REST风格的web架构系统,又称为RESTful架构,它...
  • 理解RESTful架构

    千次阅读 2012-07-27 21:00:28
     这种"互联网软件"采用客户端/服务器模式,建立在分布式体系上,通过互联网通信,具有高延时(high latency)、高并发等特点。  网站开发,完全可以采用软件开发的模式。但是传统上,软件和网络是两个不同的领域...
  • 周 婷 (zhouting@cn.ibm.com), 软件工程师, IBM 中国软件开发技术实验室 2007 年 8 月 31 日很多应用譬如监控、即时通信、即时报价系统都需要将后台发生的变化实时传送到客户端而无须客户端不停地刷新、发送请求。...
  • 架构设计(4)--API网关

    万次阅读 2018-01-25 11:13:12
    随着业务的发展,发现nginx配置越来越复杂,但又没有统一的管理,于是把Nginx这层改造成基于 OpenResty的Nginx 应用的API Gateway。于是上网总结和梳理网关相关知识。 问题: 由于我们使用的服务系统...
  • RESTful 详解

    千次阅读 2016-09-10 16:13:54
    1. 什么是REST  REST全称是Representational State Transfer,中文意思是表述(编者注:通常译为表征)性状态转移。... 他在论文中提到:“我这篇文章的写作目的,就是想在符合架构原理的前提下,理解和评估以网络为...
1 2 3 4 5 ... 20
收藏数 45,778
精华内容 18,311
关键字:

webapi 客户端显示具体错误