专栏首页高性能服务器开发+从零实现一款12306刷票软件1.2

+从零实现一款12306刷票软件1.2

咱们接着上一篇《从零实现一款12306刷票软件1.1》继续介绍。

当然,这里需要说明一下的就是,由于全国的火车站点信息文件比较大,我们程序解析起来时间较长,加上火车站编码信息并不是经常变动,所以,我们我们没必要每次都下载这个station_name.js,所以我在写程序模拟这个请求时,一般先看本地有没有这个文件,如果有就使用本地的,没有才发http请求向12306服务器请求。这里我贴下我请求站点信息的程序代码(C++代码):

1/**  
2 * 获取全国车站信息 
3 * @param si 返回的车站信息 
4 * @param bForceDownload 强制从网络上下载,即不使用本地副本 
5 */  
6bool GetStationInfo(vector<stationinfo>& si, bool bForceDownload = false);

1#define URL_STATION_NAMES   "https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.9053"  

  1bool Client12306::GetStationInfo(vector<stationinfo>& si, bool bForceDownload/* = false*/)  
  2{    
  3    FILE* pfile;  
  4    pfile = fopen("station_name.js", "rt+");  
  5    //文件不存在,则必须下载  
  6    if (pfile == NULL)  
  7    {  
  8        bForceDownload = true;  
  9    }  
 10    string strResponse;  
 11    if (bForceDownload)  
 12    {  
 13        if (pfile != NULL)  
 14            fclose(pfile);  
 15        pfile = fopen("station_name.js", "wt+");  
 16        if (pfile == NULL)  
 17        {  
 18            LogError("Unable to create station_name.js");  
 19            return false;  
 20        }  
 21        CURLcode res;  
 22        CURL* curl = curl_easy_init();  
 23        if (NULL == curl)  
 24        {  
 25            fclose(pfile);  
 26            return false;  
 27        }  
 28        //URL_STATION_NAMES  
 29        curl_easy_setopt(curl, CURLOPT_URL, URL_STATION_NAMES);  
 30        //响应结果中保留头部信息  
 31        //curl_easy_setopt(curl, CURLOPT_HEADER, 1);  
 32        curl_easy_setopt(curl, CURLOPT_COOKIEFILE, "");  
 33        curl_easy_setopt(curl, CURLOPT_READFUNCTION, NULL);  
 34        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, OnWriteData);  
 35        curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&strResponse);  
 36        curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1);  
 37        //设定为不验证证书和HOST  
 38        curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, false);  
 39        curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, false);  
 40        curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10);  
 41        curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10);  
 42        res = curl_easy_perform(curl);  
 43        bool bError = false;  
 44        if (res == CURLE_OK)  
 45        {  
 46            int code;  
 47            res = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code);  
 48            if (code != 200)  
 49            {  
 50                bError = true;  
 51                LogError("http response code is not 200, code=%d", code);  
 52            }  
 53        }  
 54        else  
 55        {  
 56            LogError("http request error, error code = %d", res);  
 57            bError = true;  
 58        }  
 59        curl_easy_cleanup(curl);  
 60        if (bError)  
 61        {  
 62            fclose(pfile);  
 63            return !bError;  
 64        }  
 65        if (fwrite(strResponse.data(), strResponse.length(), 1, pfile) != 1)  
 66        {  
 67            LogError("Write data to station_name.js error");              
 68            return false;  
 69        }  
 70        fclose(pfile);  
 71    }  
 72    //直接读取文件  
 73    else  
 74    {  
 75        //得到文件大小  
 76        fseek(pfile, 0, SEEK_END);  
 77        int length = ftell(pfile);  
 78        if (length < 0)  
 79        {  
 80            LogError("invalid station_name.js file");  
 81            fclose(pfile);  
 82        }  
 83        fseek(pfile, 0, SEEK_SET);  
 84        length++;  
 85        char* buf = new char[length];  
 86        memset(buf, 0, length*sizeof(char));  
 87        if (fread(buf, length-1, 1, pfile) != 1)  
 88        {  
 89            LogError("read station_name.js file error");  
 90            fclose(pfile);  
 91            return false;  
 92        }  
 93        strResponse = buf;  
 94        fclose(pfile);  
 95    }  
 96    /* 
 97    返回结果为一个js文件, 
 98    var station_names = '@bjb|北京北|VAP|beijingbei|bjb|0@bjd|北京东|BOP|beijingdong|bjd|1@bji|北京|BJP|beijing|bj|2" 
 99    */  
100    //LogInfo("recv json = %s", strResponse.c_str());  
101    OutputDebugStringA(strResponse.c_str());  
102    vector<string> singleStation;  
103    split(strResponse, "@", singleStation);  
104    size_t size = singleStation.size();  
105    for (size_t i = 1; i < size; ++i)  
106    {  
107        vector<string> v;  
108        split(singleStation[i], "|", v);  
109        if (v.size() < 6)  
110            continue;  
111        stationinfo st;  
112        st.code1 = v[0];  
113        st.hanzi = v[1];  
114        st.code2 = v[2];  
115        st.pingyin = v[3];  
116        st.simplepingyin = v[4];  
117        st.no = atol(v[5].c_str());  
118        si.push_back(st);  
119    }  
120    return true;  
121}  

这里用了一个站点信息结构体stationinfo,定义如下:

 1//var station_names = '@bjb|北京北|VAP|beijingbei|bjb|0@bjd|北京东|BOP|beijingdong|bjd|1@bji|北京|BJP|beijing|bj|2  
 2struct stationinfo  
 3{  
 4    string code1;  
 5    string hanzi;  
 6    string code2;  
 7    string pingyin;  
 8    string simplepingyin;  
 9    int no;  
10};  

因为我们这里目的是为了模拟http请求做买火车票相关的操作,而不是技术方面本身,所以为了快速实现我们的目的,我们就使用curl库。这个库是一个强大的http相关的库,例如12306服务器返回的数据可能是分块的(chunked),这个库也能帮我们组装好;再例如,服务器返回的数据是使用gzip格式压缩的,curl也会帮我们自动解压好。所以,接下来的所有12306的接口,都基于我封装的curl库一个接口:

 1/** 
 2 * 发送一个http请求 
 3 *@param url 请求的url 
 4 *@param strResponse http响应结果 
 5 *@param get true为GET,false为POST 
 6 *@param headers 附带发送的http头信息 
 7 *@param postdata post附带的数据     
 8 *@param bReserveHeaders http响应结果是否保留头部信息 
 9 *@param timeout http请求超时时间 
10 */  
11 bool HttpRequest(const char* url, string& strResponse, bool get = true, const char* headers = NULL, const char* postdata = NULL, bool bReserveHeaders = false, int timeout = 10); 

函数各种参数已经在函数注释中写的清清楚楚了,这里就不一一解释了。这个函数的实现代码如下:

 1bool Client12306::HttpRequest(const char* url,   
 2                              string& strResponse,   
 3                              bool get/* = true*/,   
 4                              const char* headers/* = NULL*/,   
 5                              const char* postdata/* = NULL*/,   
 6                              bool bReserveHeaders/* = false*/,   
 7                              int timeout/* = 10*/)  
 8{  
 9    CURLcode res;  
10    CURL* curl = curl_easy_init();  
11    if (NULL == curl)  
12    {  
13        LogError("curl lib init error");  
14        return false;  
15    }  
16    curl_easy_setopt(curl, CURLOPT_URL, url);  
17    //响应结果中保留头部信息  
18    if (bReserveHeaders)  
19       curl_easy_setopt(curl, CURLOPT_HEADER, 1);  
20    curl_easy_setopt(curl, CURLOPT_COOKIEFILE, "");  
21    curl_easy_setopt(curl, CURLOPT_READFUNCTION, NULL);  
22    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, OnWriteData);  
23    curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&strResponse);  
24    curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1);  
25    //设定为不验证证书和HOST  
26    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, false);  
27    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, false);  
28    //设置超时时间  
29    curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, timeout);  
30    curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout);  
31    curl_easy_setopt(curl, CURLOPT_REFERER, URL_REFERER);  
32    //12306早期版本是不需要USERAGENT这个字段的,现在必须了,估计是为了避免一些第三方的非法刺探吧。  
33    //如果没有这个字段,会返回  
34    /* 
35        HTTP/1.0 302 Moved Temporarily 
36        Location: http://www.12306.cn/mormhweb/logFiles/error.html 
37        Server: Cdn Cache Server V2.0 
38        Mime-Version: 1.0 
39        Date: Fri, 18 May 2018 02:52:05 GMT 
40        Content-Type: text/html 
41        Content-Length: 0 
42        Expires: Fri, 18 May 2018 02:52:05 GMT 
43        X-Via: 1.0 PSshgqdxxx63:10 (Cdn Cache Server V2.0) 
44        Connection: keep-alive 
45        X-Dscp-Value: 0 
46     */  
47    curl_easy_setopt(curl, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36");  
48    //不设置接收的编码格式或者设置为空,libcurl会自动解压压缩的格式,如gzip  
49    //curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "gzip, deflate, br");  
50    //添加自定义头信息  
51    if (headers != NULL)  
52    {  
53        //LogInfo("http custom header: %s", headers);  
54        struct curl_slist *chunk = NULL;          
55        chunk = curl_slist_append(chunk, headers);        
56        curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk);  
57    }  
58    if (!get && postdata != NULL)  
59    {  
60        //LogInfo("http post data: %s", postdata);  
61        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postdata);  
62    }  
63    LogInfo("http %s: url=%s, headers=%s, postdata=%s", get ? "get" : "post", url, headers != NULL ? headers : "", postdata!=NULL?postdata : "");  
64    res = curl_easy_perform(curl);  
65    bool bError = false;  
66    if (res == CURLE_OK)  
67    {  
68        int code;  
69        res = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code);  
70        if (code != 200 && code != 302)  
71        {  
72            bError = true;  
73            LogError("http response code is not 200 or 302, code=%d", code);  
74        }  
75    }  
76    else  
77    {  
78        LogError("http request error, error code = %d", res);  
79        bError = true;  
80    }  
81    curl_easy_cleanup(curl);  
82    LogInfo("http response: %s", strResponse.c_str());  
83   return !bError;  
84} 

正如上面注释中所提到的,浏览器在发送http请求时带的某些字段,不是必须的,我们在模拟这个请求时可以不添加,如查票接口浏览器可能会发以下http数据包:

 1GET /otn/leftTicket/query?leftTicketDTO.train_date=2018-05-30&leftTicketDTO.from_station=SHH&leftTicketDTO.to_station=BJP&purpose_codes=ADULT HTTP/1.1  
 2Host: kyfw.12306.cn  
 3Connection: keep-alive  
 4Cache-Control: no-cache  
 5Accept: */*  
 6X-Requested-With: XMLHttpRequest  
 7If-Modified-Since: 0  
 8User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36  
 9Referer: https://kyfw.12306.cn/otn/leftTicket/init  
10Accept-Encoding: gzip, deflate, br  
11Accept-Language: zh-CN,zh;q=0.9,en;q=0.8  
12Cookie: JSESSIONID=ACD9CB098169C4D73CDE80D6F6C38E5A; RAIL_EXPIRATION=1526978933395; RAIL_DEVICEID=WKxIYg-q1zjIPVu7VjulZ9PqEGvW2gUB9LvoM1Vx8fa7l3SUwnO_BVSatbTq506c6VYNOaxAiRaUcGFTMjCz9cPayEIc9vJ0pHaXdSqDlujJP8YrIoXbpAAs60l99z8bEtnHgAJzxLzKiv2nka5nmLY_BMNur8b8; _jc_save_fromStation=%u4E0A%u6D77%2CSHH; _jc_save_toStation=%u5317%u4EAC%2CBJP; _jc_save_wfdc_flag=dc; route=c5c62a339e7744272a54643b3be5bf64; BIGipServerotn=1708720394.50210.0000; _jc_save_fromDate=2018-05-30; _jc_save_toDate=2018-05-20 

其中像ConnectionCache-ControlAcceptIf-Modified-Since等字段都不是必须的,所以我们在模拟我们自己的http请求时可以不用可以添加这些字段,当然据我观察,12306服务器现在对发送过来的http数据包要求越来越严格了,如去年的时候,User-Agent这个字段还不是必须的,现在如果你不带上这个字段,可能12306返回的结果就不一定正确。当然,不正确的结果中一定不会有明确的错误信息,充其量可能会告诉你页面不存在或者系统繁忙请稍后再试,这是服务器自我保护的一种重要的措施,试想你做服务器程序,会告诉非法用户明确的错误信息吗?那样不就给了非法攻击服务器的人不断重试的机会了嘛。

需要特别注意的是:查票接口发送的http协议的头还有一个字段叫Cookie,其值是一串非常奇怪的东西:

 1JSESSIONID=ACD9CB098169C4D73CDE80D6F6C38E5A; 
 2RAIL_EXPIRATION=1526978933395; 
 3RAIL_DEVICEID=WKxIYg-q1zjIPVu7VjulZ9PqEGvW2gUB9LvoM1Vx8fa7l3SUwnO_BVSatbTq506c6VYNOaxAiRaUcGFTMjCz9cPayEIc9vJ0pHaXdSqDlujJP8YrIoXbpAAs60l99z8bEtnHgAJzxLzKiv2nka5nmLY_BMNur8b8; 
 4_jc_save_fromStation=%u4E0A%u6D77%2CSHH;
 5_jc_save_toStation=%u5317%u4EAC%2CBJP;
 6_jc_save_wfdc_flag=dc;
 7route=c5c62a339e7744272a54643b3be5bf64;
 8BIGipServerotn=1708720394.50210.0000;
 9_jc_save_fromDate=2018-05-30;
10_jc_save_toDate=2018-05-2

注意:原代码中各个字段都是连在一起的,我这里为了读者方便阅读,将各个字段单独放在一行。在这串字符中有一个JSESSIONID,在不需要登录的查票接口,我们可以传或者不传这个字段值。但是在购票以及查询常用联系人这些需要在已经登录的情况下才能进行的操作,我们必须带上这个数据,这是服务器给你的token(验证令牌),而这个令牌是在刚进入12306站点时,服务器发过来的,你后面的登录等操作必须带上这个token,否则服务器会认为您的请求是非法请求。我第一次去研究12306的买票流程时,即使在用户名、密码和图片验证码正确的情况下,也无法登录就是这个原因。这是12306为了防止非法登录使用的一个安全措施。

本文分享自微信公众号 - 高性能服务器开发(easyserverdev),作者:张小方

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2018-05-21

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 一个创业程序员的35岁人生总结(五)

    那么创业的本质到底是什么呢?思考的方向对了,答案很快就找到了,但是得出结论时的心情很复杂:通透、兴奋、失落、迷茫、无奈,可谓五味杂陈。这个结论就是:利润!!!看...

    范蠡
  • 如何招到一个靠谱的程序员

    我的上一份工作是在一家世界500强金融集团担任架构师,当时,公司的IT团队规模将近2000人。与其他IT公司一样,程序员的流动性也比较高,而作为架构师,我需要为...

    范蠡
  • libevent源码深度剖析(五) libevent的核心:事件event

    (1)libevent源码深度剖析一 序 (2)libevent源码深度剖析二 Reactor模式 (3)libevent源码深度剖析三 libevent基...

    范蠡
  • PHP使用curl取HTTP状态码

    V站CEO-西顾
  • 浅谈 php 采用curl 函数库获取网页 cookie 和 带着cookie去访问 网页的方法!!!!

    由于近段时间帮朋友开发一个能够查询正方教务系统的微信公众平台号。有所收获。这里总结下个人经验。 开讲前,先吐槽一下新浪云服务器,一个程序里的   同一个函数  ...

    林冠宏-指尖下的幽灵
  • 10个 ThinkPHP 开发常用代码片段

    在编写代码的时候有个神奇的汇总是好的!下面这里收集了 10+ PHP 代码片段,可以帮助你开发 PHP 项目。这些 PHP 片段对于 PHP 初学者也非常有帮助...

    php007
  • 微信开发笔记

    要实现网页版微信扫码登录必须: 1 有开发平台账号 2 网站服务已经被绑定到开发平台账号

    lilugirl
  • PHP网络爬虫之CURL

    php的curl可以实现模拟http的各种请求,这也是php做网络爬虫的基础,也多用于接口api的调用。 PHP 支持 Daniel Stenberg 创建的 ...

    benny
  • Linux常用命令08 - curl

    curl 是一个命令行实用程序,用于将数据从服务器或传输到服务器,该服务器设计用于在没有用户交互的情况下工作。 使用 curl,您可以使用支持的协议(包括 HT...

    叉叉敌
  • PHP扩展功能--cURL

    cURL 表示以命令行的形式请求某个 url, 提交数据或获取相应数据。在日常的程序开发中会用到,因此,了解 cURL 的原理和过程,有助于实际工作和项目中的应...

    程序小工

扫码关注云+社区

领取腾讯云代金券