没想到啊,没想到。被最熟悉的requests给坑了一把。

公司最近在更换客服系统,之前的马上要过期了,而现在的新系统需要对接。结果编码的任务就交给了我这么个实习生了。没办法,硬着头皮来吧。

代码上难度倒不是很大,就是对于这个业务流程。业务流程以及交互顺序搞懂了,代码上就很轻松了。

客户信息-->tornado平台chatback接口-->客服系统

客服系统-->tornado平台send接口-->zmq消息队列-->客户

我的任务就是这两条线,也就是四条通路。其中比较重要的一步就是:
客服系统–> tornado平台send接口 。 为什么说它比较重要呢,因为对于一个公司来说,对外暴露的接口必须是足够安全才行的。所以基本上要联系运维同学将虚拟机上的某一个端口做下映射。做到既要让外界能够访问,也能有很高的安全度。

好了,废话不多说,还是看看requests的坑吧。


在没有使用requests之前,一直用的而是python的标准库中自带的urllib和URLlib2模块。使用他俩完成一个比较普通的POST请求可以这么写(Python2方式):

import urllib, urllib2
url = "http://xxxxxxxxxx"
payload = {
    "param1": "value1",
    "param2": "value2"
}
payload = urllib.urlencode(payload)
req = urllib2.Request(url=url, data=payload)
res = urllib2.urlopen(req).read()

看起来还不错,代码量也不多,而且基本上也能满足正常的需要。

下面来一个PHP页面,来看看这样的客户端请求中包含了什么内容。

$vars = $_SERVER;
echo json_encode($vars);

具体的URL地址为:

http://localhost/learn/forrequests/post.php

普通的POST请求


没有发现什么异常,然后后来学到了requests库,从此涉及到网络操作的,我基本上都是用它来做了。但是一直没发现的是它竟然还有这么多需要注意的地方。其中一个还是我到公司后才发现的问题。

记得有一次,使用requests进行POST请求的时候,不管怎么尝试就是不成功。

如果没记错的话, 当时的代码应该是这样的。

import requests
url = "http://dasa.com/dasdas/"
payload = {
    "name": "zhangsan",
    "age": 22
}
response = requests.post(url=url, data=payload)
print response.text

平时我也都是这么写的,但是当时对那个接口就是不成立。然后看网上介绍的,对于POST方式的HTTP请求,要先把字典转换成POST字符串才行。

import json
payload = json.dumps(payload)

但是尝试之后,还是不行。旁白工位上有个使用了Python很多年的大佬,在问了他关于这个问题的时候,一开始他也很纳闷,没能解决这个问题,但是5分钟后,他在POST方法中换了一个关键字,并且取消了json.dump这一个操作。

response = requests.post(url=url, json=payload)

然后神奇的事情发生了,原本怎么都不成立的操作,一下子又跑通了。还真的奇怪。不过当时也没怎么在意,就觉得多了一个解决问题的方法罢了。

但是,没想到的是,这个坑并没打算抛弃我,今天系统对接测试的时候,就暴露出来了。在我的虚拟机上,代码跑的好好的,功能测试也通过了。但是代码一部署就发现了有些事情不对了。

客服发给客户的消息能接收到,但是客户发给客服的却怎么都不行了。

然后就一点点的定位这条线路,发现客户发给tornado的chatback接口也是没问题的,所以就剩下了最后一条路了。

chatback接口到客服系统出现了问题

因为消息到客服系统创建的步骤比较麻烦,需要好几个操作,所以定位起来还是比较麻烦的。而对nohup跑起来的tornado,光是打印消息又不太方便,没办法,只好通过logging模块,将信息一点点的打印到日志中。然后利用tmux开了2个pane,左边用来编码,右边用来打印实时的错误日志信息(tail -f /home/log/ddddddd.log

在测试的时候,发现本地脚本的的确确是没问题,但是一到了线上,就会出下没有json这个关键字,最终定位到了requests.post方法上。真的是费解。

为什么会出现这样的问题呢?难道是requests的版本问题?

import requests
print requests.__version__

分别在虚拟机和线上服务器上执行了这两行代码,发现了这样的问题:

  • 线上服务器:requests2.0.0
  • 虚拟机环境: requests2.18.0

这就尴尬了。而且比较让人郁闷的是线上的环境不能变,也就是说虚拟机中调试好的代码不能正常的在线上环境跑。

没办法,改代码吧,既然requests这条路走不通了,那就还是用urllib和urllib2吧。然后使用之前的方式,结果竟然不行。获取的错误信息是:

Symbol  Integer ...

然后我又仔仔细细的对比了下和之前代码关于使用urllib2进行POST请求的不同之处,也没发现有哪里不对啊。真实奇怪,最后在网上看到这样一句话:

urllub2.Request(url, data, {"Content-Type": "application/json"})

然后就死马当作活马医吧。一试,竟然可以了。


为什么会这么神奇,这个客服系统让我拥有了两次神奇的遭遇,真服了。
虽然问题解决了,但是我心中的疑虑还是没有消散,为什么接二连三的在这么简单的问题上栽坑呢?

然后我决定去看下requests的源码。

结果在requests2.13.0版本中看到了这样的代码:

def post(url, data=None, json=None, **kwargs):
    """Sends a POST request.

    :param url: URL for the new :class:`Request` object.
    :param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`.
    :param json: (optional) json data to send in the body of the :class:`Request`.
    :param \*\*kwargs: Optional arguments that ``request`` takes.
    :return: :class:`Response <Response>` object
    :rtype: requests.Response
    """

    return request('post', url, data=data, json=json, **kwargs)

这下使用json还是使用data关键字就不会再混淆了吧。然后我卸载了本机的requests2.13.0,

pip uninstall requests

专门去下载了requests2.0.0

pip install requests==2.0.0

然后点进去源代码发现了如下的内容:


def post(url, data=None, **kwargs):
    """Sends a POST request. Returns :class:`Response` object.

    :param url: URL for the new :class:`Request` object.
    :param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`.
    :param \*\*kwargs: Optional arguments that ``request`` takes.
    """

    return request('post', url, data=data, **kwargs)

原来是这样,怪不得之前在公司不知道有json这么个关键字,原来是后面版本才有的啊。真的是恍然大悟。

收获: 经常关注下常用的库有哪些更新,不要让陈旧的习惯束缚了你前进的脚步。

然后是关于POST失败的另一点内容。为什么添加了:

{"Content-Type": "application/json"}

之后就成功了呢?不加为什么不成功!!!

其实说白了这也就是个header罢了。它起到的作用就是告诉服务器端,来自客户端的请求是以什么样的内容发过来的,告诉服务器应该以怎样的解析方式去处理比较合适。 之前的话,都是用的requests,它本身会帮我们做一些处理,有兴趣的可以一路查看源码。直到你看到有这么个类:PrepareRequest,他里面有这么几个方法:

    def prepare_body(self, data, files):
        """Prepares the given HTTP body data."""

        # Check if file, fo, generator, iterator.
        # If not, run through normal process.

        # Nottin' on you.
        body = None
        content_type = None
        length = None

        is_stream = all([
            hasattr(data, '__iter__'),
            not isinstance(data, basestring),
            not isinstance(data, list),
            not isinstance(data, dict)
        ])

        try:
            length = super_len(data)
        except (TypeError, AttributeError, UnsupportedOperation):
            length = None

        if is_stream:
            body = data

            if files:
                raise NotImplementedError('Streamed bodies and files are mutually exclusive.')

            if length is not None:
                self.headers['Content-Length'] = str(length)
            else:
                self.headers['Transfer-Encoding'] = 'chunked'
        else:
            # Multi-part file uploads.
            if files:
                (body, content_type) = self._encode_files(files, data)
            else:
                if data:
                    body = self._encode_params(data)
                    if isinstance(data, str) or isinstance(data, builtin_str) or hasattr(data, 'read'):
                        content_type = None
                    else:
                        content_type = 'application/x-www-form-urlencoded'

            self.prepare_content_length(body)

            # Add content-type if it wasn't explicitly provided.
            if (content_type) and (not 'content-type' in self.headers):
                self.headers['Content-Type'] = content_type

        self.body = body

然后你会发现之前自己只是随意的传递了一个字典或者一个数组就可以进行的POST请求,其实再底层被处理了很多步,我们只不过是没有看到罢了。

一般来说,服务器端程序也能比较智能的处理来自客户单的请求,但是刚好不好的是这次的客服系统接口恰恰是需要客户端类传递一个请求头,明确的告诉接口后台要怎么处理这个请求。所以这也是为什么一开始使用requests的data关键字不行,而json关键字却可以; 使用不加Content-Type的URLlib2不行,而添加了header请求头的urllib2却能正确运行的原因了。

至此,客服系统的坑算是解决了。但是这同时也暴露了一些问题:

  • 库的出现对开发人员而言是好事,因为这能大大提高工作效率,但是同时也是一件坏事,库本身会有较强的封装性,本能的屏蔽一些实现的底层细节,这有可能让我们陷入“温柔乡”,一个“佯装”简单的世界!其实,世界很残酷,你没看到罢了。
  • 对服务器端而言,接口的设计应该具有一定的容错性,即便是要有固定的请求格式,也应该具体的描述出来,而不是让人一点点的去尝试。

对我而言,作为一个服务器端开发实习生,颇有感触。设计出功能完美的接口不是目的吗,设计出能和客户端完美契合,文档说明齐全的接口才是应该追求的目标。

好了,这个坑就先填这么多, 不知道今后还会不会遇到其他的坑,有的话再来填一下咯。


本文转载:CSDN博客