一、开发环境及工具:
· Microsoft Visual Studio(本程序使用的是Microsoft Visual Studio 2012)
· Chrome浏览器(其他浏览器也可以)
二、原理及实现:
1.原理概述:
用浏览器登录教务系统,实际上是Request了一个POST请求。
而在浏览器的选课界面中,点击“选课”按钮,则相当于向对方服务器Request了一个GET请求。
本程序使用C++编程语言,利用Windows网络编程接口Winsock2模拟浏览器的Request操作,
不断地将请求发送至对方服务器,当有人抛课时,实现自动抢课。
2.浏览器登录及点击的工作原理。
我们用浏览器打开一个新的标签页,按F12进入开发者工具的Network选项,在地址栏中输入教务系统网站,进入教务系统登录界面。
表面上看,我们输入教务系统的网址并跳转,浏览器就给出这个网址的界面了。
细节上,则是客户机(我们的电脑)给对方服务器(教务系统的服务器)提交了一个GET的请求。
(具体请求信息保存在Request Headers里面并发送给对方,包括对方服务器主机名、浏览器版本、cookie等信息)
对方服务器收到我们的请求,于是响应这一请求,也用一个Request Headers的方式发给我们,并为我们的请求提供服务
(例如网页界面的HTML代码、图片等)。
利用F12开发者工具的Network选项查看Request和Response的具体信息:
接下来,我们填写上用户名、密码及验证码。
如果用户名、密码和验证码均输入无误,点击“登录”按钮后,则可成功登录进入学生个人中心,如下图。
我们一开始在浏览器中输入网址后跳转,请求方式为GET,作用是从服务器上获取登陆界面的数据内容。
而这一次,我们提交登录信息的请求类型为POST,作用是向服务器传送数据,包括用户名、密码和验证码等。
以下是这一POST请求以及服务器对这个请求的响应:
2.利用Winsock2在程序中模拟上述登录及点击过程
由于本程序要利用模拟浏览器发送请求的原理,程序要获取到这些请求信息,因此我们必须预先完成以下工作:
①.打开浏览器 --> 打开开发者工具
②.在浏览器中登录一次 --> 记录登录时的request headers(类型为POST)
③.进入一次选课系统 --> 记录打开选课系统时的request headers(类型为GET)
④.点击“选课”按钮选一门要抢的课 --> 记录选这一门课的request headers(类型为GET)
a.登录过程中发送的request headers如下:
在代码中创建并初始化一个字符串变量request_login,用于保存以上请求信息,将上述Request Headers的信息原封不动地抄下来:
//登录POST请求(Request Headers)
string request_login ="POST /jsxsd/xk/LoginToXkLdap HTTP/1.1\r\n""Host: jxgl.gdufs.edu.cn\r\n""Connection: keep-alive\r\n""Content-Length: 54\r\n" //账号密码的长度不同,Content-Length的值也不同,登录不同账号时要注意"Cache-Control: max-age=0\r\n""Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\n""Origin: http://jxgl.gdufs.edu.cn\r\n""Upgrade-Insecure-Requests: 1\r\n""User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36\r\n""Content-Type: application/x-www-form-urlencoded\r\n""Referer: http://jxgl.gdufs.edu.cn/jsxsd/""\r\n""Accept-Encoding: gzip, deflate\r\n""Accept-Language: zh-CN,zh;q=0.8\r\n""Cookie: _gscu_526019281=882896108k64eh79; JSESSIONID=80E45C39F94A1EC5A40C9D1407FBFE32" //重启浏览器后,cookie的值要及时更新"\r\n""\r\n""USERNAME=666666" //填写学号,例如666666"&PASSWORD=999999" //填写密码,例如999999"&RANDOMCODE="; //验证码这里先不写,程序运行时再进行字符串拼接
b.进入选课界面,把选课界面的Request Headers抄下来(如果是新开一个弹窗,刷新一下即可),用一个string变量保存:
//进入选课界面的GET请求信息(Request Headers)
string request_choose_course ="GET /jsxsd/xsxk/xsxk_index?jx0502zbid=0B2F353D2507431F9D45F18AA941AFE0 HTTP/1.1\r\n" "Host: jxgl.gdufs.edu.cn\r\n""Connection: keep-alive\r\n""Cache-Control: max-age=0\r\n""Upgrade-Insecure-Requests: 1\r\n""User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36\r\n""Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\n""Referer: http://jxgl.gdufs.edu.cn/jsxsd/xsxk/xklc_list?Ves632DSdyV=NEW_XSD_PYGL""\r\n""Accept-Encoding: gzip, deflate, sdch\r\n""Accept-Language: zh-CN,zh;q=0.8\r\n""Cookie: _gscu_526019281=882896108k64eh79; JSESSIONID=80E45C39F94A1EC5A40C9D1407FBFE32" //重启浏览器后,cookie的值要及时更新"\r\n";
c.在选修选课界面,找到我们需要抢的课(这里以编译原理为例),点击“选课”按钮:
同理,新建一个string变量,保存刚刚点击编译原理“选课”按钮后,提交的Request Headers:
//点击编译原理“选课”按钮的GET请求信息(Request Headers)
string request_compilers_principles ="GET /jsxsd/xsxkkc/xxxkOper?jx0404id=201620172009027 HTTP/1.1\r\n" //每门课的jx0404id都不同,编译原理这门课对应的是201620172009027 "Host: jxgl.gdufs.edu.cn\r\n""Connection: keep-alive\r\n""Accept: */*\r\n""X-Requested-With: XMLHttpRequest\r\n""User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36\r\n""Referer: http://jxgl.gdufs.edu.cn/jsxsd/xsxkkc/comeInXxxk""\r\n""Accept-Encoding: gzip, deflate, sdch\r\n""Accept-Language: zh-CN,zh;q=0.8\r\n""Cookie: _gscu_526019281=882896108k64eh79; JSESSIONID=80E45C39F94A1EC5A40C9D1407FBFE32" //重启浏览器后,cookie的值要及时更新"\r\n";
这样一来,上述的三个Request Headers已经保存好了,下面开始用程序模拟登录及进入选课系统。
以下代码用于启动Winsock,创建socket,实现模拟登陆以及模拟进入选课界面:
/*启动Winsock*/WSADATA wd;WSAStartup(MAKEWORD(2, 2), &wd); //SOCKET temp = socket(AF_INET, SOCK_STREAM, 0); /*创建socket*/sock = socket(AF_INET, SOCK_STREAM, 0); if (sock == INVALID_SOCKET){cout << "建立socket失败! 错误码: " << WSAGetLastError() << endl;return;}/*绑定:将本地地址 附加到 套接字上 以便能够有效地标识套接字*/sockaddr_in sa = { AF_INET }; //套接字地址,AF _!NET ,表示该socket位于Internet域;int n = bind(sock, (sockaddr*)&sa, sizeof(sa));if (n == SOCKET_ERROR){cout << "绑定失败! 错误码: " << WSAGetLastError() << endl;return;}struct hostent *p = gethostbyname(host);if (p == NULL){cout << "主机无法解析出ip! 错误代码: " << WSAGetLastError() << endl;return;}sa.sin_port = htons(80);memcpy(&sa.sin_addr, p->h_addr, 4);// with some problems ???/*连接:当客户机要与网络中的服务器建立连接时,需要调用connect 函数*/n = connect(sock, (sockaddr*)&sa, sizeof(sa));if (n == SOCKET_ERROR) {cout << "连接失败! 错误码: " << WSAGetLastError() << endl;return;}if (isLogin == false){/*登录教务系统*/if(isCookie == false){isCookie = true;}string verification_code = ""; //验证码cout<<"请输入验证码: ";cin>>verification_code;string request_login_full = request_login + verification_code;cout<<"开始将登录请求(Request Headers)发送至对方服务器..."<<endl;if (send(sock, request_login_full.c_str(), request_login_full.size(), 0) == SOCKET_ERROR){cout << "发送失败! 错误代码: " << WSAGetLastError() << endl;closesocket(sock);return;}else{cout<<"发送成功!"<<endl;}if(recv(sock, buf, sizeof(buf)-1, 0) > 0){cout<<"以下是服务器的响应信息(Response Headers)"<<endl;printf("%s",buf);cout<<endl;}/*进入选课系统*/cout<<"开始将进入选课界面的请求信息(Request Headers)发送至对方服务器..."<<endl;if (send(sock, request_choose_course.c_str(), request_choose_course.size(), 0) == SOCKET_ERROR){cout << "发送失败! 错误代码: " << WSAGetLastError() << endl;closesocket(sock);return;}else{cout<<"发送成功!"<<endl;}if(recv(sock, buf, sizeof(buf)-1, 0) > 0){cout<<endl<<"以下是服务器的响应信息(Response Headers)"<<endl;printf("%s",buf);cout<<endl;}system("pause");isLogin = true;}
用刚刚的浏览器打开教务系统登陆界面,运行这段代码,并输入浏览器中显示的验证码:
输入验证码完毕,点击Enter,程序输出登录请求和选课请求的响应情况:
观察程序输出的登录请求和选课请求的响应情况,发现和之前浏览器的两次响应情况相吻合(除了响应时间Date)。
这说明本次程序登陆成功:
登录请求的Response Headers,和控制台的响应信息相吻合
进入选课界面请求的Response Headers,和控制台的响应信息相吻合
这时候,我们可以试试在浏览器中输入上面蓝色方框的地址,也就是登陆成功后重定向跳转后的地址。
我们会发现,即使没有在浏览器没有填写用户名、密码等登录信息,我们也可以直接进入学生个人中心:
这是因为我们的程序已经成功模拟了浏览器的登录过程。
3.利用循环语句模拟点击“选课”按钮实现自动抢课
//开始抢课init_sec = time(0); //计时开始int count_num = 0;/*循环抢课*/while (true){//每一次都重新建立连接SOCKET temp = socket(AF_INET, SOCK_STREAM, 0); sock = temp;if (sock == INVALID_SOCKET){cout << "建立socket失败! 错误代码: " << WSAGetLastError() << endl;return;}sockaddr_in sa = { AF_INET }; struct hostent *p = gethostbyname(host);if (p == NULL){cout << "主机无法解析出ip! 错误代码: " << WSAGetLastError() << endl;return;}sa.sin_port = htons(80);memcpy(&sa.sin_addr, p->h_addr, 4);/*连接:当客户机要与网络中的服务器建立连接时,需要调用connect 函数*/n = connect(sock, (sockaddr*)&sa, sizeof(sa));if (n == SOCKET_ERROR) {cout << "连接失败! 错误代码: " << WSAGetLastError() << endl;return;}count_num++;tot_sec = time(0) - init_sec;tot_sec_main = time(0) - main_time;//抢课:编译原理cout<<"开始将编译原理选课请求(Request Headers)发送至对方服务器..."<<endl;if (send(sock, request_compilers_principles.c_str(), request_compilers_principles.size(), 0) == SOCKET_ERROR){cout << "发送失败! 错误代码: " << WSAGetLastError() << endl;grabCourse(); //递归return;}else{cout<<"发送成功"<<endl;}if(count_num % ignoreTimes == 0){if(recv(sock, recv_buf, sizeof(recv_buf)-1, 0) > 0){cout<<endl<<"以下是服务器的响应信息(Response Headers)"<<endl;printf("%s",recv_buf);}}cout<<"程序运行总计时长(单位:秒):"<<tot_sec_main<<endl;cout<<"本次循环时长(单位:秒):"<<tot_sec<<endl;cout<<"执行次数:"<<count_num<<endl;cout<<"平均每秒执行次数:"<<(double)count_num/tot_sec<<endl;closesocket(temp);}
4.代码说明:
①.每一次循环都重新新建了一个名为temp的临时socket实例,用于避免错误代码10054。
产生错误代码10054的原因:现有连接被远程主机强制关闭。
解决办法:每一次循环都重新生成一个新的socket实例,无论上次连接成功与否,每一次都重新建立一次连接。
②.每一次循环结束前,调用closesocket()函数及时释放socket对象,以免系统资源耗尽。
③.点击“选课”按钮时,不同课的Request Headers的唯一不同点就是jx0404id,例如编译原理这门课对应的是jx0404id=201620172009027。如果要抢多门课,只需修改jx0404id,其余的Request Headers信息直接复制即可。
④.控制台输出乱码情况有待解决。
5.注意:
①.如果浏览器重启,或者电脑重启后,所有的Request Headers中的cookie信息都要重新输入。
②.在登录请求的Request Headers中,不同用户账号密码的长度不同,Content-Length的值也不同,不同账号登录时要注意。
6.本程序完整代码及运行截图:
#include <iostream>
#include <cstring>
#include<ctime>
#include <winsock2.h>
#include <Windows.h>
#include <string>
#define max 20480using namespace std;
#pragma comment(lib, "ws2_32.lib")//全局变量
char host[500]="jxgl.gdufs.edu.cn";
char buf[1024];int num = 1;
string allHtml;int ignoreTimes = 1000; //设置消息接收频率,目前默认每发送1000次接受一次
SOCKET sock;bool isLogin = false;
bool isCookie = false;int main_time;
int init_sec; //计时开始的初始时间
int tot_sec; //耗时
int tot_sec_main; //耗时char recv_buf[1024];//登录请求(Request Headers)
string request_login ="POST /jsxsd/xk/LoginToXkLdap HTTP/1.1\r\n""Host: jxgl.gdufs.edu.cn\r\n""Connection: keep-alive\r\n""Content-Length: 54\r\n""Cache-Control: max-age=0\r\n""Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\n""Origin: http://jxgl.gdufs.edu.cn\r\n""Upgrade-Insecure-Requests: 1\r\n""User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36\r\n""Content-Type: application/x-www-form-urlencoded\r\n""Referer: http://jxgl.gdufs.edu.cn/jsxsd/""\r\n""Accept-Encoding: gzip, deflate\r\n""Accept-Language: zh-CN,zh;q=0.8\r\n""Cookie: _gscu_526019281=882896108k64eh79; JSESSIONID=80E45C39F94A1EC5A40C9D1407FBFE32""\r\n""\r\n""USERNAME=666666" //填写学号,例如666666"&PASSWORD=999999" //填写密码,例如999999"&RANDOMCODE=";//进入选课界面的请求信息(Request Headers)
string request_choose_course ="GET /jsxsd/xsxk/xsxk_index?jx0502zbid=0B2F353D2507431F9D45F18AA941AFE0 HTTP/1.1\r\n""Host: jxgl.gdufs.edu.cn\r\n""Connection: keep-alive\r\n""Cache-Control: max-age=0\r\n""Upgrade-Insecure-Requests: 1\r\n""User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36\r\n""Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\n""Referer: http://jxgl.gdufs.edu.cn/jsxsd/xsxk/xklc_list?Ves632DSdyV=NEW_XSD_PYGL""\r\n""Accept-Encoding: gzip, deflate, sdch\r\n""Accept-Language: zh-CN,zh;q=0.8\r\n""Cookie: _gscu_526019281=882896108k64eh79; JSESSIONID=80E45C39F94A1EC5A40C9D1407FBFE32""\r\n";//要抢的课:编译原理
string request_compilers_principles ="GET /jsxsd/xsxkkc/xxxkOper?jx0404id=201620172009027 HTTP/1.1\r\n""Host: jxgl.gdufs.edu.cn\r\n""Connection: keep-alive\r\n""Accept: */*\r\n""X-Requested-With: XMLHttpRequest\r\n""User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36\r\n""Referer: http://jxgl.gdufs.edu.cn/jsxsd/xsxkkc/comeInXxxk""\r\n""Accept-Encoding: gzip, deflate, sdch\r\n""Accept-Language: zh-CN,zh;q=0.8\r\n""Cookie: _gscu_526019281=882896108k64eh79; JSESSIONID=80E45C39F94A1EC5A40C9D1407FBFE32""\r\n";//抢课
void grabCourse(){/*启动Winsock*/WSADATA wd;WSAStartup(MAKEWORD(2, 2), &wd); //SOCKET temp = socket(AF_INET, SOCK_STREAM, 0); /*创建socket*/sock = socket(AF_INET, SOCK_STREAM, 0); if (sock == INVALID_SOCKET){cout << "建立socket失败! 错误码: " << WSAGetLastError() << endl;return;}/*绑定:将本地地址 附加到 套接字上 以便能够有效地标识套接字*/sockaddr_in sa = { AF_INET }; //套接字地址,AF _!NET ,表示该socket位于Internet域;int n = bind(sock, (sockaddr*)&sa, sizeof(sa));if (n == SOCKET_ERROR){cout << "绑定失败! 错误码: " << WSAGetLastError() << endl;return;}struct hostent *p = gethostbyname(host);if (p == NULL){cout << "主机无法解析出ip! 错误代码: " << WSAGetLastError() << endl;return;}sa.sin_port = htons(80);memcpy(&sa.sin_addr, p->h_addr, 4);// with some problems ???/*连接:当客户机要与网络中的服务器建立连接时,需要调用connect 函数*/n = connect(sock, (sockaddr*)&sa, sizeof(sa));if (n == SOCKET_ERROR) {cout << "连接失败! 错误码: " << WSAGetLastError() << endl;return;}if (isLogin == false){/*登录教务系统*/if(isCookie == false){isCookie = true;}string verification_code = ""; //验证码cout<<"请输入验证码: ";cin>>verification_code;string request_login_full = request_login + verification_code;cout<<"开始将登录请求(Request Headers)发送至对方服务器..."<<endl;if (send(sock, request_login_full.c_str(), request_login_full.size(), 0) == SOCKET_ERROR){cout << "发送失败! 错误代码: " << WSAGetLastError() << endl;closesocket(sock);return;}else{cout<<"发送成功!"<<endl;}if(recv(sock, buf, sizeof(buf)-1, 0) > 0){cout<<"以下是服务器的响应信息(Response Headers)"<<endl;printf("%s",buf);cout<<endl;}/*进入选课系统*/cout<<"开始将进入选课界面的请求信息(Request Headers)发送至对方服务器..."<<endl;if (send(sock, request_choose_course.c_str(), request_choose_course.size(), 0) == SOCKET_ERROR){cout << "发送失败! 错误代码: " << WSAGetLastError() << endl;closesocket(sock);return;}else{cout<<"发送成功!"<<endl;}if(recv(sock, buf, sizeof(buf)-1, 0) > 0){cout<<endl<<"以下是服务器的响应信息(Response Headers)"<<endl;printf("%s",buf);cout<<endl;}system("pause");isLogin = true;}//开始抢课init_sec = time(0); //计时开始int count_num = 0;/*循环抢课*/while (true){//每一次都重新建立连接SOCKET temp = socket(AF_INET, SOCK_STREAM, 0); sock = temp;if (sock == INVALID_SOCKET){cout << "建立socket失败! 错误代码: " << WSAGetLastError() << endl;return;}sockaddr_in sa = { AF_INET }; struct hostent *p = gethostbyname(host);if (p == NULL){cout << "主机无法解析出ip! 错误代码: " << WSAGetLastError() << endl;return;}sa.sin_port = htons(80);memcpy(&sa.sin_addr, p->h_addr, 4);/*连接:当客户机要与网络中的服务器建立连接时,需要调用connect 函数*/n = connect(sock, (sockaddr*)&sa, sizeof(sa));if (n == SOCKET_ERROR) {cout << "连接失败! 错误代码: " << WSAGetLastError() << endl;return;}count_num++;tot_sec = time(0) - init_sec;tot_sec_main = time(0) - main_time;//抢课:编译原理cout<<"开始将编译原理选课请求(Request Headers)发送至对方服务器..."<<endl;if (send(sock, request_compilers_principles.c_str(), request_compilers_principles.size(), 0) == SOCKET_ERROR){cout << "发送失败! 错误代码: " << WSAGetLastError() << endl;grabCourse(); //递归return;}else{cout<<"发送成功"<<endl;}if(count_num % ignoreTimes == 0){if(recv(sock, recv_buf, sizeof(recv_buf)-1, 0) > 0){cout<<endl<<"以下是服务器的响应信息(Response Headers)"<<endl;printf("%s",recv_buf);}}cout<<"程序运行总计时长(单位:秒):"<<tot_sec_main<<endl;cout<<"本次循环时长(单位:秒):"<<tot_sec<<endl;cout<<"执行次数:"<<count_num<<endl;cout<<"平均每秒执行次数:"<<(double)count_num/tot_sec<<endl;closesocket(temp);}
}int main(){main_time = time(0); //开始计时grabCourse();system("pause");return 0;
}
程序完整运行情况如下图:
7.提速小Tips:
ignoreTimes对应消息接收的频繁程度,将ignoreTimes的值设为1000,表示每发送1000次请求,才接受一次响应消息。
如下图所示,将ignoreTimes的值调高,可以显著提高每秒执行次数。
毕竟发送一次就马上接收一次,肯定不够发送千次才接收一次运行的快。