+从零实现一款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)

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

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

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Seebug漏洞平台

使用 XML 内部实体绕过 Chrome 和 IE 的 XSS 过滤器

来源:BypassingXSSFiltersusingXMLInternalEntities 原作者:DavidLitchfield (david@davidl...

40610
来自专栏草根专栏

使用Identity Server 4建立Authorization Server (1)

本文内容基本完全来自于Identity Server 4官方文档: https://identityserver4.readthedocs.io/ 官方文档很详...

52310
来自专栏大魏分享(微信公众号:david-share)

用Ansible自动供应vmware虚拟机--构建数据中心一体化运维平台第二篇

1.1 简述 一直以来,打开邮箱被ticket糊一脸的事情时有发生。我一直在想,能不能以一种简单的方案(不花老板的钱)来供应(provisioning)虚拟机呢...

7102
来自专栏护卫神小符的专栏

如何在护卫神镜像中安装 SQL SERVER?

针对很多腾讯云新上云的用户,在购买安装护卫神镜像系统后,需要使用到SQL SERVER但是又不清楚如何安装配置SQL SERVER。

3190
来自专栏魏艾斯博客www.vpsss.net

如何创建.htaccess 文件

关于.htaccess 文件,一般用于虚拟主机中,使用 VPS 建站的可以忽略了。对于使用虚拟主机建站的朋友来说.htaccess 文件可以用作伪静态化设置和 ...

4628
来自专栏Golang语言社区

gRPC服务发现&amp;负载均衡

构建高可用、高性能的通信服务,通常采用服务注册与发现、负载均衡和容错处理等机制实现。根据负载均衡实现所在的位置不同,通常可分为以下三种解决方案:

8202
来自专栏逸鹏说道

并发编程~先导篇上

并发 :一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。

2088
来自专栏逸鹏说道

直传文件到Azure Storage的Blob服务中

题记:为了庆祝获得微信公众号赞赏功能,忙里抽闲分享一下最近工作的一点心得:如何直接从浏览器中上传文件到Azure Storage的Blob服务中。 为什么 如果...

3597
来自专栏WindCoder

自用插件整理之表格bootstrap-table

本插件基于bootstrap,网上各种例子也比较多,本文就不详细列api一类的了,只将自己常用的记录一下。多数代码中存在的注释,就不再重写。

1.2K1
来自专栏JadePeng的技术博客

K8S集群安装

主要参考 https://github.com/opsnull/follow-me-install-kubernetes-cluster

6512

扫码关注云+社区

领取腾讯云代金券