一、开发环境及工具:
· 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)发送至对方服务器..."< 0){
cout<<"以下是服务器的响应信息(Response Headers)"< 0){
cout<
用刚刚的浏览器打开教务系统登陆界面,运行这段代码,并输入浏览器中显示的验证码:
输入验证码完毕,点击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)发送至对方服务器..."< 0){
cout<
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
#include
#include
#include
#include
#include
#define max 20480
using 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)发送至对方服务器..."< 0){
cout<<"以下是服务器的响应信息(Response Headers)"< 0){
cout<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)发送至对方服务器..."< 0){
cout<
程序完整运行情况如下图:
7.提速小Tips:
ignoreTimes对应消息接收的频繁程度,将ignoreTimes的值设为1000,表示每发送1000次请求,才接受一次响应消息。
如下图所示,将ignoreTimes的值调高,可以显著提高每秒执行次数。
毕竟发送一次就马上接收一次,肯定不够发送千次才接收一次运行的快。