0%

Other

FreeNAS 简介

FreeNAS 是什么? FreeNAS 是一款广受赞誉的开源免费 NAS 操作系统。它能把普通台式机瞬间变成一台多功能 NAS 服务器。不但适用于企业文件共享,同样适用于打造家庭媒体中心。 FreeNAS 支持多种共享协议,包括 SMB/CIFS、NFS、AFP、WebDAV、iSCSI、FTP/TFTP、RSync 等。 官方网站

iSCSI

iSCSI 技术是一种由 IBM 公司研究开发的,是一个供硬件设备使用的可以在 IP 协议的上层运行的 SCSI 指令集,这种指令集合可以实现在 IP 网络上运行 SCSI 协议,使其能够在诸如高速千兆以太网上进行路由选择。iSCSI 技术是一种新储存技术,该技术是将现有 SCSI 接口与以太网络(Ethernet)技术结合,使服务器可与使用 IP 网络的储存装置互相交换资料。 iSCSI:Internet 小型计算机系统接口 (iSCSI:Internet Small Computer System Interface)。

本篇目标

那么本篇文章的目标就是记录一下怎样使用 FreeNAS 配置一个输入我们的网络存储服务。

下载安装

首先我们要下载 FreeNAS 的镜像,由于 FreeNAS 9 对系统的要求比较高,在这里我们用到的是 FreeNAS 8。 镜像下载 下载完成之后我们利用 VMware 安装即可。 安装界面提供了四个选项:

  1. Install/Upgrade 安装/升级
  2. Shell 终端
  3. Reboot System 重启系统
  4. Shutdown System 关闭系统

20150129171248 使用键盘上的方向键切换菜单,选择第一项,按回车键确认。 接下来系统会提示选择 FreeNAS 系统盘,在如下图所示的界面中会显示出所有可以用作安装 FreeNAS 系统的设备。你需要根据自己界面上显示的实际内容进行选择,此处应该选择我们准备作为 FreeNAS 系统盘的另一块 U 盘,选择好以后按回车键确认。 20150129171601 此时,界面上显示了一些警告信息,大意为“你选择作为 FreeNAS 系统盘的设备上的所有数据都会被清空,而且该设备将完全被系统占用,不能用作数据存储。” 按回车键确认。 20150129171850 确认后,系统开始执行安装,如下图所示。 20150129172137 系统安装完成后会给出成功提示,提醒我们移除安装盘,如下图。不用理会,按回车键继续。 20150129172300 此时,系统又回到了最初的安装界面,如下图所示。用方向键切换选择第四项,按回车键关闭系统。 20150129172526 待系统完全关闭以后,再次启动电脑,并设置从 U 盘系统盘引导,成功启动以后,看到如下所示的界面,代表系统已经安装完成。 20151213171035 接下来,你可以使用浏览器访问界面中给出的 IP 地址打开 FreeNAS 的 WebGUI 管理界面了。

网页配置

接下来,我们就可以通过浏览器来配置我们的 FreeNAS 了。 通过系统的提示,我们访问以下链接即可: http://192.168.231.131/ 20151213171159 访问之后,可以看到如上内容。 点击 setting,先切换一下语言,简体中文。 20151213171352 接下来我们便开始 iSCSI 的配置啦。

iSCSI 配置

1.首先开启 iSCSI 服务,然后点击右侧的设置图标进入设置界面。 20151213173030 2.添加 iSCSI 端口,选择默认设置即可。 20151213171558 3.添加网络授权信息 20151213171648 两个全部填写为 ALL 即可。 4.添加 iSCSI 用户信息 20151213171733 填入用户信息和秘钥即可,下方的输入框是加密可选的。 5.添加 iSCSI 属性信息 20151213171855 6.添加存储设备 在这里我们首先要添加一块磁盘,在 VMware 中的设置里添加一块磁盘,在这里分配为 20G。 2015121317195120151213172007 在 FreeNAS 设置界面中,添加扩展设备。 20151213172115 7.关联目标 将用户信息和磁盘信息关联起来。 20151213172143 通过以上流程,我们便完成了一个网络硬盘的配置。

Windows 使用

好,配置好了网络硬盘,那么我们就在 Windows 下使用一下吧,Windows 7 以上的系统自带了 iSCSI 服务。 我们可以直接在控制面板里面找到它。在控制面板中搜索。 20151213172329 然后,会出现一个属性设置窗口。点击发现,发现门户,输入刚才 FreeNAS 的地址即可。 20151213172518 接下来在目标的选项卡中我们会看到刚才配置的一个目标,点击下方的连接即可。 20151213172541 连接成功之后,我们便可以完成连接了。 打开磁盘设置,我们便可以看到那个 20G 的磁盘了,然后新建简单卷。 20151213172806 格式化为 NTFS 系统,刷新一下,打开计算机。 20151213172840 我们可以发现,一个新的网络磁盘便安装成功啦。 可以向其中放置任何文件,与其他的磁盘没有任何区别!

结语

FreeNAS 是一个非常强大的工具,在这里我们只涉及到了 iSCSI 服务的使用,还有更多等待着我们的探索,小伙伴们快来试验一下吧。

C/C++

最近在研究信息安全,需要用到 OpenSSL 库,我用到的开发 IDE 是 VS2012,所以,在这里也记录一下我配置 VS2012 的 OpenSSL 库的过程。

下载 OpenSSL 库

OpenSSL 库大家可以自行下载源码然后用 ruby 进行编译,另外我们也可以选择直接下载编译好的类库。 这里我们利用的后者,在此提供一个下载链接。 OpenSSL-Win32 下载完成之后解压,比如我的放到了 D 盘。 20151207162627

新建项目

首先,我们找一段测试代码,在此利用的是 AES 算法的示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <stdio.h>
#include <openssl/aes.h>
#include <stdlib.h>
#include <string.h>

int main()
{
unsigned char key[16+1] = "my-key-i-choosed"; // 128bits key (应该是真正的随机数才好)
char pt1[16+5+1] = "0123456789abcdef12345"; // 明文
char ct[16+5+1]; // 密文
char pt2[16+5+1]; // 解密后的明文

AES_KEY k;
unsigned char iv1[16+1] = {"1023456789abcdef"}; // 16+1,加密用
unsigned char iv2[16+1] = {"1023456789abcdef"}; // 16+1,解密用

{ // single blcok test
AES_set_encrypt_key(key, 16*8, &k);
AES_encrypt((unsigned char*)pt1, (unsigned char*)ct, &k);

AES_set_decrypt_key(key, 16*8, &k);
AES_decrypt((unsigned char*)ct, (unsigned char*)pt2, &k);

if (memcmp(pt1, pt2, 16)==0)
puts("AES block ok");
else
puts("AES block err");
}

{ // cfb mode (stream mode)
int num=0;
AES_set_encrypt_key(key, 16*8, &k);
AES_cfb128_encrypt((unsigned char*)pt1, (unsigned char*)ct,
16+5, &k, (unsigned char*)iv1, &num, AES_ENCRYPT);

num=0;
AES_set_encrypt_key(key, 16*8, &k); // MUST as enc
AES_cfb128_encrypt((unsigned char*)ct, (unsigned char*)pt2,
16+5, &k, (unsigned char*)iv2, &num, AES_DECRYPT);

if (memcmp(pt1, pt2, 16+5)==0)
puts("AES CFB mode ok");
else
puts("AES CFB mode err");
}
system("pause");
return 0;
}

接下来新建一个项目,win32 控制台程序,空项目,完成。 20151207161749 20151207161824 新建源文件,我取名叫做 aes.cpp,将代码复制进去,可以看到代码最初是在报错的。 20151207162418 好,接下来我们进行环境配置。

环境配置

右键项目名称,弹出一个菜单,选择属性。 在 VC++目录选项卡中,添加包含目录和库目录。 在这里,我的包含目录就是刚才解压的 OpenSSL 目录的 include 目录,库目录则是 lib 目录。 注意:分号要是英文分号,英文分号! 20151207162929 接下来选择连接器选项卡,输入 libeay.lib 和 ssleay32.lib 两个附加依赖项。 20151207163915 现在右击项目,重新生成。 我们可以看到,程序可以正常生成 exe 了。 20151207164034 但是直接运行的话会报错,是因为缺少 dll 文件。 20151207164304 之后,将项目中的 libeay32.dll 和 ssleay32.dll 文件放入项目的 debug 目录即可。 20151207164444 最后项目的 debug 目录如下 20151207164405 重新运行 exe 程序,发现已经正常运行。 20151207164736 至此,VS 配置 OpenSSL 环境的过程已经全部完成。 其他项目类似,大家可以试着配一下。 如有问题,欢迎留言交流~

Linux

最近服务器要过期了,需要进行迁移,新服务器如果上面配置的是 Apache 服务器该怎么办呢? 系统:Ubuntu 14.04

环境配置

首先新主机上配置好 apache 环境,这个就不多说了,直接执行下面的命令即可。

1
2
3
4
5
6
7
sudo apt-get install apache2
sudo apt-get install php5 php5-cgi php5-mysql php5-curl php5-gd php5-idn php-pear php5-imagick php5-imap php5-mcrypt php5-memcache php5-mhash php5-ming php5-pspell php5-recode php5-snmp php5-tidy php5-xmlrpc php5-sqlite php5-xsl
sudo apt-get install mysql-server mysql-client
sudo apt-get install libapache2-mod-php5
sudo apt-get install libapache2-mod-auth-mysql
sudo apt-get install phpmyadmin
sudo ln -s /usr/share/phpmyadmin/ /var/www/html/phpmyadmin

通过以上配置,新主机便可以实现 lamp 环境的配置了。

代码迁移

首先旧主主机上打包一下代码,比如一个文件夹名字叫 wonder

1
tar -zcvf wonder.tar.gz wonder

然后,打包完成之后,便会出现一个名字叫做 wonder.tar.gz 的文件 可以利用 wget 方式直接下载。

1
wget http://xxx.xxx.xxx.xxx/wonder.tar.gz

下载完成之后,直接解压即可。 这样代码就取到了。

数据库迁移

数据库迁移无非就是在 phpmyadmin 之间导入导出,这个很简单。 但是重要的一点是,需要把 wp-options 表中的两个 URL 配置改掉,比如原来是一个域名链接,现在需要改为 IP+文件名。 20151128144223 否则,浏览器会提示重定向循环的问题。

服务器配置

首先我们需要将域名解析到这个主机。 配置示例域名:wonderlee.me 20151128144413 然后配置一下,vhost,在 apache 下配置是这样的 首先在 /etc/apache2/apache2.conf 中加入如下两行

1
2
# Include all the user configurations:
Include httpd.conf

然后我们需要在 httpd.conf 配置一下域名解析 新建一个 /etc/apach2/httpd.conf,加入如下内容

1
2
3
4
5
6
7
8
9
10
11
12
ServerName 115.28.24.44:80

<VirtualHost 115.28.24.44:80>
DocumentRoot /var/www/html
ServerName 115.28.24.44
</VirtualHost>

<VirtualHost 115.28.24.44:80>
DocumentRoot /var/www/html/wonder
ServerName wonderlee.me
ServerAlias wonderlee.me
</VirtualHost>

然后执行服务器重启操作。

1
sudo service apache2 restart

好,这样的话我们的域名配置解析就好了。 输入 wonderlee.me 即可解析到 wonder 文件夹啦。 可以输入你的域名试试看,已经可以了吧。 然后我们需要开启 rewrite 模块。 输入命令

1
sudo a2enmod rewrite

然后修改 /etc/apache2/apache2.conf 文件

1
2
3
4
5
<Directory /var/www/>
Options Indexes FollowSymLinks
AllowOverride None
Require all granted
</Directory>

改为

1
2
3
4
5
<Directory /var/www/>
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>

即可,结束之后重启一下服务器。

1
sudo service apache2 restart

在项目目录下新建一个文件 .htaccess,来支持重写

1
2
3
4
5
6
7
8
9
10
11
# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /wonder/
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /wonder/index.php [L]
</IfModule>

# END WordPress

好,这样,重写过程就完成啦。

网站配置

最后,需要将网站的配置修改一下,比如固定链接 20151128145507 比如网站的基地址 20151128145620 至此,网站配置工作全部完成,欢乐地上网体验一下吧 如有问题,欢迎留言。

Linux

之前一直都用 Apache 服务器,由于网站访问量比较大,另外加上旧服务器快到期了,准备迁移到新的服务器上,所以决定采用 Nginx 服务器。 迁移过程比较心酸,之前一直用 apache,对 nginx 服务器配置不熟悉,踩了很多坑。下面说一下我的网站从旧主机(配有 apache 服务器)迁移到新主机(配有 nginx 服务器)的过程。

代码迁移

这个过程其实也是比较心酸的,查看了一下目录结构占用空间已经足足快 1 个 G 了,可想而知里面占用的大部分空间是上传的图片素材。 不过要是迁移全部图片的话工程量实在是巨大。不过,好消息是我从开始就使用了七牛 CDN 加速,所以,上传的图片会自动存放到七牛,只不过也在主机本地留了备份而已,所以,我可以安心地删掉它们了。 那么对代码进行瘦身之后,这里就有两种方法来迁移了: 1.可以用 git 上传到 github,然后用另一台主机把代码拉下来即可,在此不再赘述。 2.打包上传,然后直接在另一台主机上下载下来,由于我的两台主机在同一局域网内,所以我直接采用了这种方式,传输速度快。

打包

由于代码中含有 .git 目录,所以这部分我们不需要打包,那么压缩时我们就需要排除这个文件夹。 20151113155701 文件夹名叫 cqc,那么我们就打包一下,排除.git 目录,使用如下命令

1
tar -zcvf cqc.tar.gz --exclude=cqc/.git cqc

运行结束后会出现 cqc.tar.gz 文件,这就是目录压缩包。 然后我们只需要在另一台主机上输入

1
wget http://xxx.xxx.xxx.xxx/cqc.tar.gz

即可完成下载,速度可是嗖嗖的 然后解压即可,代码便完成了迁移。

数据库迁移

数据库用二者的 phpmyadmin 导出和上传即可。我导出 .sql 文件,大小为 9M,而 phpMyAdmin 的上传限制大小是 2M,怎么办?其实我们可以压缩 .sql 文件为 zip 格式,压缩之后就有了 1.4M 了,分分钟完成上传。要知道 phpMyAdmin 可是支持 .sql.zip 文件的。 接下来是一个比较重要的部分,那就是配置一下站点信息。直接修改数据库的两个 URL。 分别是 siteurl 和 home,一定要修改为 http://xxx.xxx.xxx.xxx/cqc 的形式,也就是把原来的域名改成 IP 加目录的形式,要不然网站是无法访问的,会出现多重循环定向的提示。 好,其他的没什么问题,连接数据库错误的话就修改一下目录的 wp-config.php 文件吧,连接数据库的信息修改正确就好了。

配置 vhosts

和 apache 一样,我们多个域名肯定要可以解析到不同的目录吧,nginx 当然也是支持的。 接下来我们需要把新域名解析到 cqc 目录,在 nginx 下怎么做呢?其实还是比较简单的。 在 /etc/nginx 目录下可以新建一个 vhosts 文件夹。在这里我们要解析 cqc 目录,那么我就新建一个 cqc.conf 文件。 现在例如我要把 blog.cuiqingcai.com 解析到 cqc 文件夹,配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
server {
listen 80;
server_name cuiqingcai.com blog.cuiqingcai.com;

index index.html index.htm index.php;
root /var/www/cqc;

location / {

if (!-e $request_filename) {
rewrite ^([_0-9a-zA-Z-]+)?(/wp-.*) $2 last;
rewrite ^([_0-9a-zA-Z-]+)?(/.*\.php)$ $2 last;
rewrite ^ /index.php last;
}
}

location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
# # With php5-fpm:
# fastcgi_pass unix:/var/run/php5-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME /var/www/cqc$fastcgi_script_name;
include fastcgi_params;
}
}

其中

1
2
3
4
5
6
7
8
location / {

if (!-e $request_filename) {
rewrite ^([_0-9a-zA-Z-]+)?(/wp-.*) $2 last;
rewrite ^([_0-9a-zA-Z-]+)?(/.*\.php)$ $2 last;
rewrite ^ /index.php last;
}
}

这一部分是伪静态重写,因为我的博客用的是 wordpress,所以伪静态重写是这样的。当然还有其他的重写方式可以尝试。 之后在 /etc/nginx/nginx.conf 中的 http{} 中添加一行

1
include /etc/nginx/vhosts/cqc.conf;

则代表引用了这个文件。 注意,还要把 域名设置一下,添加一条 A 记录到主机上。 好了,一切大功告成了。

后记

迁移和配置的过程坑实在是太多了,列列吧,警醒世人呐。 (1)代码迁移过程上传 git,整个项目差不多 1 个 G,由于数据量太大,导致内存不够无法正常上传。后来删除了图片,发现项目还是很大,结果发现是 .git 目录已经占用了上百兆,后来打包排除这个目录迁移的。 (2)数据库迁移的时候由于 phpMyAdmin 上传大小限制,修改了一番上传大小结果发现没生效,还倒腾了一下 php-fpm,后来发现可以直接上传压缩包,那就分分钟完成了。 (3)配置完之后发现网站首页正常访问了,可是其他页面全部出现了 404 错误,后来配置了一番伪静态解析发现配置代码直接写在了 localhost server 里面,后来发现可以直接新写一个 server,然后配置域名 servername,然后配置伪静态重写才成功。 总之,坎坷是多,但是,自己慢慢摸索出来,也是一种不错的体验。 当你成功之后,会觉得世界又是那么美好。

Java

之前是在eclipse上写的,后面换成了android sudio。 2048游戏的UI整体可以采用线性布局,即LinearLayout,其中嵌套一个线性布局和一个GridLayout,内嵌的线性布局填充文本框,以显示分数,GridLayout中填充4x4的继承自FrameLayout的card类作为主要的游戏界面。由于大部分操作都在GridLayout中进行,可以自定义一个继承自GridLayout的类GameView,类中定义判定上下左右滑动的方法和每次滑动后自动添加一个随机数字的方法以及每次滑动后判断游戏是否可以继续进行的方法。 主布局activity_main.xml代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
//match_parent表示布局充满整个屏幕
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.administractor.game2048.MainActivity"
//里面的组件垂直放置
android:orientation="vertical"
tools:ignore="MergeRootFrame">

<LinearLayout
//宽度充满整个屏幕,高度自适应。
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
//显示当前分数的文本框
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Your Score:"/>
<TextView
android:id="@+id/tvScore"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
//使用自定义的GridLayout
<com.example.administractor.game2048.GameView
android:layout_width="fill_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:id="@+id/GameView" >
</com.example.administractor.game2048.GameView>
</LinearLayout>

GameView.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
package com.example.administrator.game2048;

import java.util.ArrayList;
import java.util.List;

import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Point;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.GridLayout;

public class GameView extends GridLayout {
//调用类构造方法
public GameView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
//初始化游戏
InitGameView();
}

public GameView(Context context, AttributeSet attrs) {
super(context, attrs);
InitGameView();
}

public GameView(Context context) {
super(context);
InitGameView();
}
private void InitGameView(){
//设置为4x4个方格
setColumnCount(4);
//设置背景颜色
setBackgroundColor(0xffeee4da);

//判定滑动方向
setOnTouchListener(new OnTouchListener() {
private float startx,starty,offsetx,offsety;
@Override
public boolean onTouch(View v, MotionEvent event) {
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
startx=event.getX();
starty=event.getY();
break;
case MotionEvent.ACTION_UP:
offsetx=event.getX()-startx;
offsety=event.getY()-starty;
if(Math.abs(offsetx)>Math.abs(offsety)){
if(offsetx<-5){
swipeLeft();
}else if(offsetx>5){
swipeRight();
}
}else{
if(offsety<-5){
swipeUp();
}else if(offsetx>3){
swipeDown();
}
}
break;
}
return true;
}
});
}
//适应不同大小的屏幕
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
int cardWidth=(Math.min(h, w))/4;
addCards(cardWidth,cardWidth);
startGame();
}
//在4x4的方格上添加满卡片
public void addCards(int cardwidth,int cardheight){
Card c;
for (int y = 0; y < 4; y++) {
for (int x = 0; x < 4; x++) {
c=new Card(getContext());
c.setNum(0);
addView(c, cardwidth, cardheight);
cardmap[x][y]=c;
}
}
}
//游戏开始时每个卡片默认值设为0,并随机添加两张带数字的卡片
private void startGame(){
MainActivity.getMainActivity().clearScore();
for (int y = 0; y < 4; y++) {
for (int x = 0; x < 4; x++) {
cardmap[x][y].setNum(0);
}
}
addRandomNum();
addRandomNum();
}
private void addRandomNum() {
//使用emptypoints将数字为0的card提取出来,并随即选择一个空card赋值
emptyPoints.clear();
for (int y = 0; y < 4; y++) {
for (int x = 0; x < 4; x++) {
if(cardmap[x][y].getNum()<=0){
emptyPoints.add(new Point(x,y));
}
}
}
Point p=emptyPoints.remove((int)(Math.random()*emptyPoints.size()));
//2和4出现的概率控制在1:9
cardmap[p.x][p.y].setNum(Math.random()>0.1?2:4);
}
//左滑方法
private void swipeLeft(){
//merge作为判断能否滑动的flag
boolean merge = false;
for (int y = 0; y < 4; y++) {
for (int x = 0; x < 4; x++) {
for (int x1 = x+1; x1 <4; x1++) {
if(cardmap[x1][y].getNum()>0){
if(cardmap[x][y].getNum()<=0){
cardmap[x][y].setNum(cardmap[x1][y].getNum());
cardmap[x1][y].setNum(0);
merge=true;
x--;
}else if(cardmap[x][y].equal(cardmap[x1][y])){
cardmap[x][y].setNum(cardmap[x][y].getNum()*2);
cardmap[x1][y].setNum(0);
MainActivity.getMainActivity().addScore(cardmap[x][y].getNum());
merge=true;
}
break;
}
}
}
}
if(merge){
addRandomNum();
checkComplete();
}
}
//下滑
private void swipeDown(){

boolean merge = false;

for (int x = 0; x < 4; x++) {
for (int y = 3; y >=0; y--) {

for (int y1 = y-1; y1 >=0; y1--) {
if (cardmap[x][y1].getNum()>0) {

if (cardmap[x][y].getNum()<=0) {
cardmap[x][y].setNum(cardmap[x][y1].getNum());
cardmap[x][y1].setNum(0);

y++;
merge = true;
}else if (cardmap[x][y].equal(cardmap[x][y1])) {
cardmap[x][y].setNum(cardmap[x][y].getNum()*2);
cardmap[x][y1].setNum(0);
MainActivity.getMainActivity().addScore(cardmap[x][y].getNum());
merge = true;
}

break;
}
}
}
}

if (merge) {
addRandomNum();
checkComplete();
}
}
//上滑
private void swipeUp(){

boolean merge = false;

for (int x = 0; x < 4; x++) {
for (int y = 0; y < 4; y++) {

for (int y1 = y+1; y1 < 4; y1++) {
if (cardmap[x][y1].getNum()>0) {

if (cardmap[x][y].getNum()<=0) {
cardmap[x][y].setNum(cardmap[x][y1].getNum());
cardmap[x][y1].setNum(0);

y--;

merge = true;
}else if (cardmap[x][y].equal(cardmap[x][y1])) {
cardmap[x][y].setNum(cardmap[x][y].getNum()*2);
cardmap[x][y1].setNum(0);
MainActivity.getMainActivity().addScore(cardmap[x][y].getNum());
merge = true;
}

break;

}
}
}
}

if (merge) {
addRandomNum();
checkComplete();
}
}
//右滑
private void swipeRight(){
boolean merge = false;
for (int y = 0; y < 4; y++) {
for (int x = 3; x >=0; x--) {
for (int x1 = x-1; x1 >=0; x1--) {
if(cardmap[x1][y].getNum()>0){
if(cardmap[x][y].getNum()<=0){
cardmap[x][y].setNum(cardmap[x1][y].getNum());
cardmap[x1][y].setNum(0);
x++;
merge=true;
}else if(cardmap[x][y].equal(cardmap[x1][y])){
cardmap[x][y].setNum(cardmap[x][y].getNum()*2);
cardmap[x1][y].setNum(0);
MainActivity.getMainActivity().addScore(cardmap[x][y].getNum());
merge=true;
}
break;
}
}
}
}
if(merge){
addRandomNum();
checkComplete();
}
}
//如果有空卡片或者相邻的值相同卡片则游戏还能进行
public void checkComplete(){
boolean complete=true;
ALL:
for (int y = 0; y <4; y++) {
for (int x = 0; x <4; x++) {
if(cardmap[x][y].getNum()==0||
x>0&&cardmap[x][y].equal(cardmap[x-1][y])||
x<3&&cardmap[x][y].equal(cardmap[x+1][y])||
y>0&&cardmap[x][y].equal(cardmap[x][y-1])||
y<3&&cardmap[x][y].equal(cardmap[x][y+1])){
complete=false;
break ALL;
}
}
}
//游戏结束弹出alert提示窗口
if(complete){
new AlertDialog.Builder(getContext()).setTitle("大林哥温馨提示").setMessage("游戏结束").setPositiveButton("重来",new DialogInterface.OnClickListener() {

@Override
public void onClick(DialogInterface arg0, int arg1) {
startGame();
}
}).show();
}

}
private Card[][] cardmap=new Card[4][4];
private List<Point> emptyPoints=new ArrayList<Point>();
}

主类MainActivity.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.example.administrator.game2048;

import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;

public class MainActivity extends Activity {

public MainActivity(){
mainActivity=this;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tvscore = (TextView) findViewById(R.id.tvScore);
}
public void clearScore(){
score=0;
showScore();
}
public void showScore(){
tvscore.setText(score+"");
}
public void addScore(int s){
score+=s;
showScore();
}
private TextView tvscore;
private int score=0;
public static MainActivity mainActivity=null;
public static MainActivity getMainActivity() {
return mainActivity;
}
}

Card.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
package com.example.administrator.game2048;

import android.content.Context;
import android.view.Gravity;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.TextView;

public class Card extends FrameLayout {

public Card(Context context) {
super(context);
LayoutParams lp = null;

View background = new View(getContext());
//参数-1表示layoutparams填充满整个父容器
lp = new LayoutParams(-1, -1);
//设置卡片之间有10像素的间隔
lp.setMargins(10, 10, 0, 0);
background.setBackgroundColor(0x33ffffff);
addView(background, lp);

label = new TextView(getContext());
label.setTextSize(28);
label.setGravity(Gravity.CENTER);

lp = new LayoutParams(-1, -1);
lp.setMargins(10, 10, 0, 0);
addView(label, lp);

setNum(0);
}



private int n=0;
public int getNum(){
return n;
}
//设置数字及对应的背景颜色
public void setNum(int n){
this.n=n;
if(n<=0){
label.setText("");
}else{
label.setText(n+"");
}
switch (n) {
case 0:
label.setBackgroundColor(0x00000000);
break;
case 2:
label.setBackgroundColor(0xffeee4da);
break;
case 4:
label.setBackgroundColor(0xffede0c8);
break;
case 8:
label.setBackgroundColor(0xfff2b179);
break;
case 16:
label.setBackgroundColor(0xfff59563);
break;
case 32:
label.setBackgroundColor(0xfff67c5f);
break;
case 64:
label.setBackgroundColor(0xfff65e3b);
break;
case 128:
label.setBackgroundColor(0xffedcf72);
break;
case 256:
label.setBackgroundColor(0xffedcc61);
break;
case 512:
label.setBackgroundColor(0xffedc850);
break;
case 1024:
label.setBackgroundColor(0xffedc53f);
break;
case 2048:
label.setBackgroundColor(0xffedc22e);
break;
default:
label.setBackgroundColor(0xff3c3a32);
break;
}
}
//判断卡片是否相等
public boolean equal(Card o){
return getNum()==o.getNum();
}
private TextView label;
}

Net

openvpn 原理

VPN 直译就是虚拟专用通道,是提供给企业之间或者个人与公司之间安全数据传输的隧道,OpenVPN 无疑是 Linux 下开源 VPN 的先锋,提供了良好的性能和友好的用户 GUI。 它大量使用了 OpenSSL 加密库中的 SSLv3/TLSv1 协议函数库。 目前 OpenVPN 能在 Solaris、Linux、OpenBSD、FreeBSD、NetBSD、Mac OS X 与 Microsoft Windows 以及 Android 和 iOS 上运行,并包含了许多安全性的功能。它并不是一个基于 Web 的 VPN 软件,也不与 IPsec 及其他 VPN 软件包兼容。 openvpn 通过使用公开密钥(非对称密钥,加密解密使用不同的 key,一个称为 Publice key,另外一个是 Private key)对数据进行加密的。这种方式称为 TLS 加密。 openvpn 使用 TLS 加密的工作过程是,首先 VPN Sevrver 端和 VPN Client 端要有相同的 CA 证书,双方通过交换证书验证双方的合法性,用于决定是否建立 VPN 连接。 然后使用对方的 CA 证书,把自己目前使用的数据加密方法加密后发送给对方,由于使用的是对方 CA 证书加密,所以只有对方 CA 证书对应的 Private key 才能解密该数据,这样就保证了此密钥的安全性,并且此密钥是定期改变的,对于窃听者来说,可能还没有破解出此密钥,VPN 通信双方可能就已经更换密钥了。 扩展阅读: openvpn

安装 openvpn

首先,你需要有一台长期运行的服务器,大家可以用自己的闲置的电脑或者买一台阿里云啦。 我的服务器是 Ubuntu 14.04,下面就演示一下我的配置过程。 安装

1
sudo apt-get -y install openvpn libssl-dev openssl

查看下版本并记录下来

1
openvpn --version

20151030145358 在这里我们的版本是 2.3.2

安装 easy-rsa

easy-rsa 是用来制作 openvpn 相关证书的,使用如下命令安装

1
sudo apt-get -y install easy-rsa

好,一切准备就绪后,我们就开始制作证书啦,我们需要制作的有三个证书 CA 证书、Server 端证书、Client 端证书。行动起来。

制作 CA 证书

openvpn 与 easy-rsa 安装完毕后,我们需要在/etc/openvpn/目录下创建 easy-rsa 文件夹,如下

1
sudo mkdir /etc/openvpn/easy-rsa/

然后把/usr/share/easy-rsa/目录下的所有文件全部复制到/etc/openvpn/easy-rsa/下

1
sudo cp -r /usr/share/easy-rsa/* /etc/openvpn/easy-rsa/

当然,我们也可以直接在/usr/share/easy-rsa/制作相关的证书,但是为了后续的管理证书的方便,我们还是把 easy-rsa 放在了 openvpn 的启动目录下。 注意:由于我们现在使用的是 ubuntu 系统,所以我们必须切换到 root 用户下才能制作相关证书,否则 easy-rsa 会报错。如果是 centos 系统,则不存在此问题。 切换到 root 用户下,使用如下命令:

1
sudo su

在开始制作 CA 证书之前,我们还需要编辑 vars 文件,修改如下相关选项内容即可

1
sudo vi /etc/openvpn/easy-rsa/vars
1
2
3
4
5
6
7
export KEY_COUNTRY="CN"
export KEY_PROVINCE="SD"
export KEY_CITY="JiNan"
export KEY_ORG="germy"
export KEY_EMAIL="cqc@cuiqingcai.com"
export KEY_OU="germy"
export KEY_NAME="germy"

如图所示 20151030150125 之后,我们需要利用这个文件来制作我们的证书,保存一下。 然后一个很重要的一步,赋予权限,否则在制作证书的时候,值还是初始化的值。

1
sudo chmod 777 /etc/openvpn/easy-rsa/vars

vars 文件主要用于设置证书的相关组织信息,红色部分的内容可以根据自己的实际情况自行修改。 其中 export KEY_NAME=”germy” 这个要记住下,我们下面在制作 Server 端证书时,会使用到。 注意:以上内容,我们也可以使用系统默认的,也就是说不进行修改也是可以使用的。 然后使用 source vars 命令使其生效,如下:

1
2
source vars
./clean-all

注意:执行 clean-all 命令会删除,当前目录下的 keys 文件夹。 现在开始正式制作 CA 证书,使用如下命令:

1
2
cd /etc/openvpn/easy-rsa/
./build-ca

一路回车即可。 制作完成后,我们可以查看 keys 目录里有什么东西。 如果你的目录下出现了 ca.crt 和 ca.key 两个文件,其中 ca.crt 就是我们所说的 CA 证书。如此,CA 证书制作完毕。 现在把该 CA 证书的 ca.crt 文件复制到 openvpn 的启动目录/etc/openvpn 下,如下:

1
cp keys/ca.crt /etc/openvpn/

制作 Server 端证书

CA 证书制作完成后,我们现在开始制作 Server 端证书。如下:

1
./build-key-server germy

上述命令中 germy,就是我们前面 vars 文件中设置的 KEY_NAME 查看 keys 目录 20151030150704 如果可以发现出现了 germy.crt,germy.csr,germy.key 文件,就说明成功了。 现在再为服务器生成加密交换时的 Diffie-Hellman 文件,如下:

1
./build-dh

你会发现目录下多了一个 dh2048.pem 文件。 以上操作完毕后,把 germy.crt,germy.key,dh2048.pem 复制到 /etc/openvpn/ 目录下,如下:

1
2
cd /etc/openvpn/easy-rsa/
cp keys/germy.crt keys/germy.key keys/dh2048.pem /etc/openvpn/

如此,Server 端证书就制作完毕。

制作 Client 端证书

Server 端证书制作完成后,我们现在开始制作 Client 端证书,如下:

1
./build-key cqc

其中上述命令的 cqc 就是客户端证书名称,可以自定义 如果发现 keys 目录已经生成了 cqc.csr、cqc.crt 和 cqc.key 这个三个文件。其中 cqc.crt 和 cqc.key 两个文件是我们要使用的。 如此,Client 端证书就制作完毕。

配置 Server 端

所有证书制作完毕后,我们现在开始配置 Server 端。Server 端的配置文件,我们可以从 openvpn 自带的模版中进行复制。如下:

1
2
cp /usr/share/doc/openvpn/examples/sample-config-files/server.conf.gz /etc/openvpn/
cd /etc/openvpn/

解压 server.conf.gz 文件,使用如下命令:

1
gzip -d server.conf.gz

注意:上述命令的意思是解压 server.conf.gz 文件后,然后删除原文件。 现在我们来修改 server.conf 文件 20151030151728 一共要修改 3 处文件 (1)修改了 openvpn 运行时使用的协议,由原来的 UDP 协议修改为 TCP 协议。生成环境建议使用 TCP 协议。 (2)修改了 openvpn 服务器的相关证书,由原来的 server.csr、server.key 修改为 germy.crt、germy.key。 (3)修改了 Diffie-Hellman 文件,由原来的 dh1024.pem 修改为 dh2048.pem。 配置文件修改完毕后,我们现在来启动 openvpn,使用如下命令:

1
/etc/init.d/openvpn start

至此,服务器端的 VPN 已经配置完毕了。

客户端的配置

服务器端配置好了,我们需要用另一台机器来连接,这里我们的客户端依然是 Ubuntu 14.04 首先我们需要从服务器上取到刚才生成的证书文件,那么我们需要的有什么呢? 20151030152200 首先这三个,ca.crt,cqc.crt,cqc.key 另外是一个模板,它是 /usr/share/doc/openvpn/examples/sample-config-files/client.conf 把这四个文件下载下来,然后放到客户端里。 比如我们保存到客户机的 home/user 文件夹下 20151030152421 把 client.conf 文件重命名为 client.ovpn 然后修改下面 4 处

1
2
3
4
5
proto tcp
remote 121.42.14.158 1194
ca ca.crt
cert cqc.crt
key cqc.key

其中 remote 就是你的服务器地址 配置好了之后,我们运行

1
sudo openvpn --config client.ovpn

如果最后的结果是 Sequence Completed 那就证明连接成功啦。 输入

1
ifconfig

你会发现多了一个 tun0 适配器,这就是 openvpn 的适配器。 至此,openvpn 的配置和连接就全部完成啦。

参考来源

参考文献 如有问题,欢迎留言交流。

HTML

HTML5中包含一个帮助检测device orientation的特性,使用这个特性可以在移动设备浏览器中判断用户设备的旋转重力方向。

基本知识

Alpha, Beta, Gamma角度旋转。 当用户旋转手机的时候,HTML5中定义了三个轴方向的旋转,如下: 上图可以看考,分别是z,x,y轴,对应分别是:Alpha,Beta,Gamma,下面图将更清楚的展示: 上图是Alpha旋转, 围绕Z轴旋转(绿线旋转方向,水平) 上图是Beta旋转, 围绕X轴旋转(绿线旋转方向,前后) 上图是Beta旋转, 围绕Y轴旋转(绿线旋转方向,左右)

属性

  • alpha: (float 类型 )以z方向为轴心的旋转角度 浮点数类型,只读属性,取值范围为0到360(不等于360)。
  • beta: (float 类型 )以x方向为轴心的旋转角度 浮点数类型,只读属性,取值范围为-180到180(不等于180)。
  • gamma: (float 类型 )以y方向为轴心的旋转角度 浮点数类型,只读属性,取值范围为-180到180(不等于180)。

参考

原文链接

Net

综述

在上一篇文章中,客户机可以借助路由机直接上网,并没有什么登录限制。接下来我们将加入上网登录验证,只有输入了正确的用户名和密码才可以通过验证,然后才可以访问互联网。 接下来,就跟随我用 PHP 来实现登录验证吧。

环境配置

在这之前,你需要配置一下 LAMP 环境,也就是 Apache,MySQL,PHP 开发环境,依次执行如下命令即可。

1
2
3
4
5
6
7
sudo apt-get install apache2
sudo apt-get install php5 php5-cgi php5-mysql php5-curl php5-gd php5-idn php-pear php5-imagick php5-imap php5-mcrypt php5-memcache php5-mhash php5-ming php5-pspell php5-recode php5-snmp php5-tidy php5-xmlrpc php5-sqlite php5-xsl
sudo apt-get install mysql-server mysql-client
sudo apt-get install libapache2-mod-php5
sudo apt-get install libapache2-mod-auth-mysql
sudo apt-get install phpmyadmin
sudo ln -s /usr/share/phpmyadmin/ /var/www/html/phpmyadmin

如果配置出现问题,请查阅相关资料。 apache 默认的目录为 /var/www/html,我们这时访问 localhost 或者 192.168.122.4,都可以出现 apache 的欢迎界面,就证明我们配置成功了。

路由初始设置

为了在登录之前限制主机的上网,我们需要利用 iptables 规则来对数据包的转发加以限制。同时,将网页重定向到本机的登录界面。 初始路由设置如下

1
2
3
4
5
6
7
8
9
iptables -F
iptables -t nat -F
iptables -t mangle -F
iptables -P INPUT ACCEPT
iptables -P FORWARD ACCEPT
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
iptables -t filter -A FORWARD -s 192.168.122.0/24 -o eth0 -j REJECT
iptables -t filter -A FORWARD -s 192.168.122.0/24 -d 119.29.29.29/32 -j ACCEPT
iptables -t nat -A PREROUTING -s 192.168.122.0/24 -p tcp -j DNAT --to 192.168.122.4

首先清除所有的 iptables 规则,然后设置前一篇我们说的 IP 伪装,这时可以客户机可以通过主机上网。 接下来的一条规则则禁用了来自 192.168.122.0 网段的所有 IP 的数据包转发,然后设置可访问 DNS 服务器,最后一条则设置了所有的 tcp 连接自动跳转到 192.168.122.4,也就是我们刚才配置的服务器。 可以把以上规则保存为脚本,比如叫 init.sh 来运行,也可以添加到 /etc/rc.local 中,开机自动运行。

登录页面

访问到 192.168.122.4 时,我们需要给用户呈现的当然不是刚才显示的 apache 欢迎页面,而是登录的输入框以及登录按钮界面。 所以,登录界面代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Auth Login</title>

<!-- Bootstrap -->
<link rel="stylesheet" href="css/bootstrap.min.css">
</head>
<body>
<form id="auth" method="post">
<div class="input-group">
<span class="input-group-addon" id="basic-addon1">Username</span>
<input type="text" class="form-control" placeholder="Username" aria-describedby="basic-addon1" name="username">
</div>
<div class="input-group">
<span class="input-group-addon" id="basic-addon1">Password</span>
<input type="text" class="form-control" placeholder="Password" aria-describedby="basic-addon1" name="password">
</div>
<input type="button" id="login" class="btn btn-primary" value="Login">
<input type="button" id="logout" class="btn btn-primary" value="Logout">
</form>
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="js/jquery.min.js"></script>
<!-- Include all compiled plugins (below), or include individual files as needed -->
<script src="js/bootstrap.min.js"></script>
</body>
<style>
form {
max-width:400px;
margin:0 auto;
}
.input-group {
margin-bottom:20px;
}
</style>
<script>
$(function(){
$("#login").on("click", function() {
$("#auth").attr("action", "/login.php");
$("#auth").submit();
});
$("#logout").on("click", function() {
$("#auth").attr("action", "/logout.php");
$("#auth").submit();
});
});
</script>
</html>

其中的 js,jquery 文件请大家自行引入。 预览一下效果 20151008151728 在这里我们设置了两个按钮,一个是登录,一个是下线。

数据库查询验证

接下来我们新建一个数据库,例如我新建了一个数据库叫 auth,然后数据表 user,里面有三个字段。分别是 id,username,password,我插入了一条数据。 20151008152059 接下来我们就尝试一下登录,提交到 login.php 文件验证一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php
$mysql_server_name = "localhost";
$mysql_username = "root";
$mysql_password = "123456";
$mysql_database = "auth";
$username = @$_POST['username'];
$password = @$_POST['password'];
$ip=$_SERVER["REMOTE_ADDR"];
$conn=mysql_connect($mysql_server_name, $mysql_username,
$mysql_password);
if ($conn) {
$sql = "select * from user where username = '".$username."'";
$result = mysql_fetch_array(mysql_db_query($mysql_database, $sql, $conn));
if ($result) {
if ($result['password'] == $password) {
$status = -1;
system("sudo ./bash/login.sh $ip", $status);
if ($status == 0) {
echo "Login Successfully";
} else {
echo "Login Failed";
}
} else {
echo "Wrong Password";
}
} else {
echo "Not";
}
} else {
die("Could Not Connect");
}
?>

其中,最重要的部分莫过于

1
system("sudo ./bash/login.sh $ip", $status);

这一行代码了,此处便是登录验证用户名和密码之后执行的一个 Linux 脚本命令。 在这里我把要执行的脚本写入了 login.sh 文件中,传入的参数便是 ip 地址。 那么 login.sh 里面发生了什么事情呢,我们来看一下。

1
2
3
4
5
6
iptables -t nat -D PREROUTING -s $1/32 -j ACCEPT
iptables -t nat -D PREROUTING -s $1/32 -p tcp -j ACCEPT
iptables -t filter -D FORWARD -s $1/32 -o eth0 -j ACCEPT
iptables -t nat -I PREROUTING -s $1/32 -j ACCEPT
iptables -t nat -I PREROUTING -s $1/32 -p tcp -j ACCEPT
iptables -t filter -I FORWARD -s $1/32 -o eth0 -j ACCEPT

$1 的意思就是获取第一个参数,在这里就是 IP 地址,脚本主要做的事情就是放行来自这个 IP 地址的数据包,让其正常访问互联网。 保存脚本后,记得给脚本赋予权限

1
sudo chmod 777 login.sh

-D 的意思就是删除,因为 iptables 是可以添加多次相同的规则的,在添加之前删除一下,以防止多次添加。 在这里

1
sudo ./bash/login.sh $ip

执行命令脚本前,我们加了 sudo,意思就是管理员身份运行,但是仍然可能导致权限问题,因为命令的执行者是 PHP(其实是 www-data),而并不是 root 用户,所以我们需要修改一下执行权限。 首先通过 PHP 文件获取执行该命令的用户是叫什么,比如新建一个 info.php 文件,输入如下内容:

1
2
3
<?php
echo shell_exec("id -a");
?>

看一下运行结果 20151008153424 嗯,果然,执行用户是 www-data,这样我们只需要给 www-data 添加一个执行权限就好了。 修改 /etc/sudoers 文件 添加一行

1
www-data ALL=(ALL) NOPASSWD:ALL

意思是 www-data 以 root 身份运行并且不需要密码。 20151008153700 好,保存之后,我们尝试一下,就可以登录啦。

测试登录

在路由主机(Ubuntu Route)里面,初始化一下 iptables 规则,然后查看当前规则。 我们发现当前访问都是被阻止的,而且 tcp 连接会自动跳转到 192.168.122.4 20151008154216 现在我们登录客户机,随机打开一个网址,比如百度,就发现自动跳转到了登录界面 20151008154629 输入用户名密码,尝试登陆,比如之前插入数据库的是 cqc,123456,输入之后登录。 20151008162712 提示登录成功之后,我们便可以欢乐地上网啦。 20151008162926 好,这样我们就完成了验证之后上网啦。

下线操作

同样的,下线操作我们同样写一个 logout.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php
$mysql_server_name = "localhost";
$mysql_username = "root";
$mysql_password = "123456";
$mysql_database = "auth";
$username = @$_POST['username'];
$password = @$_POST['password'];
$ip=$_SERVER["REMOTE_ADDR"];
echo $ip;
$conn=mysql_connect($mysql_server_name, $mysql_username,
$mysql_password);
if ($conn) {
$sql = "select * from user where username = '".$username."'";
$result = mysql_fetch_array(mysql_db_query($mysql_database, $sql, $conn));
if ($result) {
if ($result['password'] == $password) {
$status = -1;
system("sudo ./bash/logout.sh $ip", $status);
if ($status == 0) {
echo "Login Successfully";
} else {
echo "Login Failed";
}
} else {
echo "Wrong Password";
}
} else {
echo "Not";
}
} else {
die("Could Not Connect");
}
?>

登出的脚本如下,其实就是单纯去除了刚才添加的路由规则

1
2
3
iptables -t nat -D PREROUTING -s $1/32 -j ACCEPT
iptables -t nat -D PREROUTING -s $1/32 -p tcp -j ACCEPT
iptables -t filter -D FORWARD -s $1/32 -o eth0 -j ACCEPT

配置方式和登录一样,大家可以尝试下。

源代码

在这里提供大家源代码下载 源码下载 如有问题,欢迎交流。

Net

综述

大家好,这次我们需要实现的是实现双网卡主机共享上网,就是一台主机通过连接另一台可以访问外网的双网卡主机来正常上网。所以我们需要两台机器来进行测试,在这里我们用的是两台 Ubuntu 14.04,其中一台是单网卡,一台是双网卡。废话不多说,行动起来吧。

配置系统

博主使用了 Vmware 来安装了两台 Ubuntu 主机,一台当路由机,名称是 Ubuntu Route,另一台是客户机,名称是 Ubuntu Desktop,具体的网络配置如下: Ubuntu Route: 一个网卡 eth0 通过 NAT 方式来与外部主机共享上网,这个网卡也就是 VMnet8 网卡,网段是 192.168.231.0 20151007164043 另一个网卡 eth1 连接了一个自定义的仅主机模式的网卡 VMnet2,网段是 192.168.122.0 20151007164114 网络适配器设置如下,eth0 开启了 DHCP,ech1 没有开启 DHCP 20151007164343 Ubuntu Desktop: 一个网卡 eth0 连接刚才那个自定义的仅主机模式的网卡 VMnet2,网段是 192.168.122.0 20151007164821 好了,以上就是基本硬件的配置

设置 IP

接下来我们设置一下 Ubuntu Route 的 IP 地址,修改 /etc/network/interfaces

1
2
3
4
5
6
7
8
auto lo
iface lo inet loopback
auto eth0
iface eth0 inet dhcp
auto eth1
iface eth1 inet static
address 192.168.122.4
netmask 255.255.255.0

在这里,eth0 因为我们在 VMware 里面设置了 DHCP,所以这里我们设置 dhcp 即可,eth1 需要手动配置一下,我们分配了 192.168.122.4 这个 IP 地址,当然你可以随意指定,子网掩码如上,不需要写网关,因为它本身作为一个路由。 可以通过执行如下命令来使之生效

1
sudo /etc/init.d/networking restart

如果上述方法不行,则可以尝试使用关闭网卡和开启网卡的命令。

1
2
sudo ifup eth0
sudo ifdown eth0

eth1 的开启和关闭同上

开启路由转发

修改 /etc/sysctl.conf 文件,将

1
net.ipv4.ip_forward=1

这一行取消注释,代表开启了路由转发功能。 也可以通过执行

1
echo 1 > /proc/sys/net/ipv4/ip_forward

命令来实现

设置 iptables 规则

iptables 是非常重要的一个环节,如果大家不熟悉,可以去搜相关资料了解一下。 执行如下命令,来设置一下 iptables 规则,可以直接在命令行逐条执行,也可以写成一个脚本来执行。

1
2
3
4
5
6
iptables -F
iptables -t nat -F
iptables -t mangle -F
iptables -P INPUT ACCEPT
iptables -P FORWARD ACCEPT
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

其中最后一条是最重要的,代表将数据包通过 eth0 网卡来转发,也是 IP 伪装的一个常用方法,有了这条指令,从 eth1 网卡流经的一些数据包可以通过 eth0 来转发,这样就相当于连通了两个网卡,这样与 eth1 网卡连接的主机便可以上网了。

客户主机设置

因为客户机的 eth0 连接了 VMnet2 网卡,而 VMnet2 网卡又与路由主机的 eth1 连接,我们只需要简单设置一下 IP 地址就好了。 修改 /etc/network/interfaces

1
2
3
4
5
6
auto eth0
iface eth0 inet static
address 192.168.122.5
netmask 255.255.255.0
gateway 192.168.122.4
dns-nameservers 119.29.29.29

这里很重要的一个设置就是网关,设置成路由主机的 IP 地址。 设置完了同样重启一下网卡使其生效。 还可以选择性设置下 DNS 服务器。 至此,所有配置都完成了,测试一下吧。

测试

我们在客户机里打开浏览器,输入随意一个网页测试一下。 20151007172000 嗯,客户机可以正常上网啦,一切都是那么轻松加愉快! 如有问题,欢迎留言交流~

Python

2022 年最新 Python3 网络爬虫教程

大家好,我是崔庆才,由于爬虫技术不断迭代升级,一些旧的教程已经过时、案例已经过期,最前沿的爬虫技术比如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等技术层出不穷,我最近新出了一套最新最全面的 Python3 网络爬虫系列教程。

博主自荐:截止 2022 年,可以将最前沿最全面的爬虫技术都涵盖的教程,如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等,市面上目前就这一套了。

最新教程对旧的爬虫技术内容进行了全面更新,搭建了全新的案例平台进行全面讲解,保证案例稳定有效不过期。

教程请移步:

【2022 版】Python3 网络爬虫学习教程

如下为原文。

综述

最近山大软件园校区 QLSC_STU 无线网掉线掉的厉害,连上之后平均十分钟左右掉线一次,很是让人心烦,还能不能愉快地上自习了?能忍吗?反正我是不能忍了,嗯,自己动手,丰衣足食!写个程序解决掉它! 假若你不能连这个无线,那就照照思路啦~

决战前夕

首先我们看一下那个验证页面是咋样滴,上个图先 20150920192557 嘿,这界面还算可以把,需要我们输入的东西就是俩,一个就是学号,另一个是身份证号后六位,然后就可以登录,享受免费的无线网啦。 不过不知道谁设置了个登录时长,一段时间后就会掉线了,于是,自动模拟登陆系统就要应运而生啦。 来,我们先点击一下连接,看一下浏览器怎么工作的。 按下 F12,监听网络,我们点击第一个响应,也就是 login.jsp,看一下。 20150920192957 我们具体看一下 headers,里面 form 提交了什么东西,真的是茫茫多的数据啊。 20150920193146 嗯,一目了然 POST 的数据和提交的地址。 让我们来分析几个数据吧:

ClientIP:当前客户端的 IP 地址,在山大软件园校区这个地址是 211.87 开头的

timeoutvalue:连接等待时间,也就是俗话说的 timeout

StartTime:登录时间,也就是在你登录的那一刻的时间戳,这个时间戳是 13 位的,精确到了毫秒,不过一般是 10 位的,我们加 3 个 0 就好了

shkOvertime:登录持续时间,这个数据默认是 720,也就是 12 分钟之后,登录就失效了,自动掉线,我们可以手动更改

username:学号

password:密码,也就是我们身份证号后六位

我们需要在登录的时候把 form 表单中的所有信息都 POST 一下,然后就可以完成登录啦。 万事俱备,只欠东风,来来来,程序写起来!

一触即发

说走咱就走啊,天上的星星参北斗啊! 登陆地址:Request URL:http://192.168.8.10/portal/login.jsp?Flag=0 首先,我们需要验证一下 IP 地址,先写一个获取 IP 地址的函数,首先判断当前 IP 是不是 211.87 开头的,如果是的话,证明连接的 IP 是有效的。 首先我们写一个获取本机 IP 的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
self.ip_pre = "211.87"
#获取本机无线IP
def getIP(self):
local_iP = socket.gethostbyname(socket.gethostname())
if self.ip_pre in str(local_iP):
return str(local_iP)
ip_lists = socket.gethostbyname_ex(socket.gethostname())

for ip_list in ip_lists:
if isinstance(ip_list, list):
for i in ip_list:
if self.ip_pre in str(i):
return str(i)
elif type(ip_list) is types.StringType:
if self.ip_pre in ip_list:
return ip_list

这个方法利用了 gethostbyname 和 gethostbyname_ex 方法,获取了各个网卡的 IP 地址,遍历一下,找到那个 211.87 开头的 IP,返回 接下来,获取到 IP 之后,我们便可以构建 form,然后进行模拟登陆了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#模拟登录
def login(self):
print self.getCurrentTime(), u"正在尝试认证QLSC_STU无线网络"
ip = self.getIP()
data = {
"username": self.username,
"password": self.password,
"serverType": "",
"isSavePass": "on",
"Submit1": "",
"Language": "Chinese",
"ClientIP": self.getIP(),
"timeoutvalue": 45,
"heartbeat": 240,
"fastwebornot": False,
"StartTime": self.getNowTime(),
#持续时间,超过这个时间自动掉线,可进行设置
"shkOvertime": self.overtime,
"strOSName": "",
"iAdptIndex": "",
"strAdptName": "",
"strAdptStdName": "",
"strFileEncoding": "",
"PhysAddr": "",
"bDHCPEnabled": "",
"strIPAddrArray": "",
"strMaskArray": "",
"strMask": "",
"iDHCPDelayTime": "",
"iDHCPTryTimes": "",
"strOldPrivateIP": self.getIP(),
"strOldPublicIP": self.getIP(),
"strPrivateIP": self.getIP(),
"PublicIP": self.getIP(),
"iIPCONFIG":0,
"sHttpPrefix": "http://192.168.8.10",
"title": "CAMS Portal"
}
#消息头
headers = {
'User-Agent' : 'Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.111 Safari/537.36',
'Host': '192.168.8.10',
'Origin': 'http://192.168.8.10',
'Referer': 'http://192.168.8.10/portal/index_default.jsp?Language=Chinese'
}
post_data = urllib.urlencode(data)
login_url = "http://192.168.8.10/portal/login.jsp?Flag=0"
request = urllib2.Request(login_url, post_data, headers)
response = urllib2.urlopen(request)
result = response.read().decode('gbk')

比较多的内容就在于 form 表单的数据内容以及请求头,后来利用 urllib2 的 urlopen 方法实现模拟登陆。 如果大家对此不熟悉,可以参见 Urllib 的基本使用 这样,登录后的结果就会保存在 result 变量中,我们只需要从 result 中提取出我们需要的数据就可以了。

乘胜追击

接下来,我们就分析一下数据啦,结果有这么几种:

1.登录成功 2.已经登录 3.用户不存在 4.密码错误 5.未知错误

好,利用 result 分析一下结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#打印登录结果
def getLoginResult(self, result):
if u"用户上线成功" in result:
print self.getCurrentTime(),u"用户上线成功,在线时长为",self.overtime/60,"分钟"
elif u"您已经建立了连接" in result:
print self.getCurrentTime(),u"您已经建立了连接,无需重复登陆"
elif u"用户不存在" in result:
print self.getCurrentTime(),u"用户不存在,请检查学号是否正确"
elif u"用户密码错误" in result:
pattern = re.compile('<td class="tWhite">.*?2553:(.*?)</b>.*?</td>', re.S)
res = re.search(pattern, result)
if res:
print self.getCurrentTime(),res.group(1),u"请重新修改密码"
else:
print self.getCurrentTime(),u"未知错误,请检查学号密码是否正确"

通过字符串匹配和正则表达式,我们分辨并提取出了上述五种情况。 增加循环检测 既然是检测网络是否断开,那么我们只需要每隔一段时间检测一下就好了,那就 10 秒吧。 因为这个 10 秒是可配置的,为了方便配置,统一配置到init方法里面。

1
2
#检测间隔时间,单位为秒
self.every = 10

然后,我们写一个循环来检测一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
while True:
nowIP = self.getIP()
if not nowIP:
print self.getCurrentTime(), u"请检查是否正常连接QLSC_STU无线网络"
else:
print self.getCurrentTime(),u"成功连接了QLSC_STU网络,本机IP为",nowIP
self.login()
while True:
can_connect = self.canConnect()
if not can_connect:
nowIP = self.getIP()
if not nowIP:
print self.getCurrentTime(), u"当前已经断线,请确保连接上了QLSC_STU网络"
else:
print self.getCurrentTime(), u"当前已经断线,正在尝试重新连接"
self.login()
else:
print self.getCurrentTime(), u"当前网络连接正常"
time.sleep(self.every)
time.sleep(self.every)

其中我们用到了 canConnect 方法,这个就是检测网络是否已经断开的方法,我们可以利用 ping 百度的方法来检测一下。 方法实现如下

1
2
3
4
5
6
7
8
9
#判断当前是否可以联网
def canConnect(self):
fnull = open(os.devnull, 'w')
result = subprocess.call('ping www.baidu.com', shell = True, stdout = fnull, stderr = fnull)
fnull.close()
if result:
return False
else:
return True

好啦,所有的要点我们已经逐一击破,等着凯旋吧

收拾战场

好了,所有的代码要点已经被我们攻破了,接下来就整理一下,让他们组合起来,变成一个应用程序吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
__author__ = 'CQC'
#-*- coding:utf-8 -*-

import urllib
import urllib2
import socket
import types
import time
import re
import os
import subprocess

class Login:

#初始化
def __init__(self):
#学号密码
self.username = '201200131012'
self.password = 'XXXXXX'
#山大无线STU的IP网段
self.ip_pre = '211.87'
#登录时长
self.overtime = 720
#检测间隔时间,单位为秒
self.every = 10

#模拟登录
def login(self):
print self.getCurrentTime(), u"正在尝试认证QLSC_STU无线网络"
ip = self.getIP()
data = {
"username": self.username,
"password": self.password,
"serverType": "",
"isSavePass": "on",
"Submit1": "",
"Language": "Chinese",
"ClientIP": self.getIP(),
"timeoutvalue": 45,
"heartbeat": 240,
"fastwebornot": False,
"StartTime": self.getNowTime(),
#持续时间,超过这个时间自动掉线,可进行设置
"shkOvertime": self.overtime,
"strOSName": "",
"iAdptIndex": "",
"strAdptName": "",
"strAdptStdName": "",
"strFileEncoding": "",
"PhysAddr": "",
"bDHCPEnabled": "",
"strIPAddrArray": "",
"strMaskArray": "",
"strMask": "",
"iDHCPDelayTime": "",
"iDHCPTryTimes": "",
"strOldPrivateIP": self.getIP(),
"strOldPublicIP": self.getIP(),
"strPrivateIP": self.getIP(),
"PublicIP": self.getIP(),
"iIPCONFIG":0,
"sHttpPrefix": "http://192.168.8.10",
"title": "CAMS Portal"
}
#消息头
headers = {
'User-Agent' : 'Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.111 Safari/537.36',
'Host': '192.168.8.10',
'Origin': 'http://192.168.8.10',
'Referer': 'http://192.168.8.10/portal/index_default.jsp?Language=Chinese'
}
post_data = urllib.urlencode(data)
login_url = "http://192.168.8.10/portal/login.jsp?Flag=0"
request = urllib2.Request(login_url, post_data, headers)
response = urllib2.urlopen(request)
result = response.read().decode('gbk')
self.getLoginResult(result)


#打印登录结果
def getLoginResult(self, result):
if u"用户上线成功" in result:
print self.getCurrentTime(),u"用户上线成功,在线时长为",self.overtime/60,"分钟"
elif u"您已经建立了连接" in result:
print self.getCurrentTime(),u"您已经建立了连接,无需重复登陆"
elif u"用户不存在" in result:
print self.getCurrentTime(),u"用户不存在,请检查学号是否正确"
elif u"用户密码错误" in result:
pattern = re.compile('<td class="tWhite">.*?2553:(.*?)</b>.*?</td>', re.S)
res = re.search(pattern, result)
if res:
print self.getCurrentTime(),res.group(1),u"请重新修改密码"
else:
print self.getCurrentTime(),u"未知错误,请检查学号密码是否正确"

#获取当前时间戳,13位
def getNowTime(self):
return str(int(time.time()))+"000"

#获取本机无线IP
def getIP(self):
local_iP = socket.gethostbyname(socket.gethostname())
if self.ip_pre in str(local_iP):
return str(local_iP)
ip_lists = socket.gethostbyname_ex(socket.gethostname())

for ip_list in ip_lists:
if isinstance(ip_list, list):
for i in ip_list:
if self.ip_pre in str(i):
return str(i)
elif type(ip_list) is types.StringType:
if self.ip_pre in ip_list:
return ip_list

#判断当前是否可以联网
def canConnect(self):
fnull = open(os.devnull, 'w')
result = subprocess.call('ping www.baidu.com', shell = True, stdout = fnull, stderr = fnull)
fnull.close()
if result:
return False
else:
return True

#获取当前时间
def getCurrentTime(self):
return time.strftime('[%Y-%m-%d %H:%M:%S]',time.localtime(time.time()))

#主函数
def main(self):
print self.getCurrentTime(), u"您好,欢迎使用模拟登陆系统"
while True:
nowIP = self.getIP()
if not nowIP:
print self.getCurrentTime(), u"请检查是否正常连接QLSC_STU无线网络"
else:
print self.getCurrentTime(),u"成功连接了QLSC_STU网络,本机IP为",nowIP
self.login()
while True:
can_connect = self.canConnect()
if not can_connect:
nowIP = self.getIP()
if not nowIP:
print self.getCurrentTime(), u"当前已经掉线,请确保连接上了QLSC_STU网络"
else:
print self.getCurrentTime(), u"当前已经掉线,正在尝试重新连接"
self.login()
else:
print self.getCurrentTime(), u"当前网络连接正常"
time.sleep(self.every)
time.sleep(self.every)

login = Login()
login.main()

来,我们来运行一下,看下效果吧! 执行

1
python login.py

当前是可以联网的,我分别在网页上操作执行了断开,操作,程序自动检测到掉线,自动重新连接。 接下来我直接断开了 QLSC_STU 网络的链接,程序同样检测到 QLSC_STU 这个热点没有连接上,提示用户链接。 接下来我重新连接上了这个热点,由于刚才已经登录上线,且持续时间较短,网络自动恢复正常。 下面是运行结果: 20150920205618 嗯,这样我们就是实现了自动掉线的检测和模拟登录。

凯旋而归

咿呀伊尔哟,想约妹子上自习吗?那就赶紧来试试吧!一网在手,天下我有!追男神女神都不再是梦想! 如果有问题,欢迎留言讨论,代码肯定有不完善的地方,仅供参考。

Other

CSRF是什么?

CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSRF/XSRF。

CSRF可以做什么?

你这可以这么理解CSRF攻击:攻击者盗用了你的身份,以你的名义发送恶意请求。CSRF能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账……造成的问题包括:个人隐私泄露以及财产安全。

CSRF漏洞现状

CSRF这种攻击方式在2000年已经被国外的安全人员提出,但在国内,直到06年才开始被关注,08年,国内外的多个大型社区和交互网站分别爆出CSRF漏洞,如:NYTimes.com(纽约时报)、Metafilter(一个大型的BLOG网站),YouTube和百度HI……而现在,互联网上的许多站点仍对此毫无防备,以至于安全业界称CSRF为“沉睡的巨人”。

CSRF的原理

下图简单阐述了CSRF攻击的思想: 从上图可以看出,要完成一次CSRF攻击,受害者必须依次完成两个步骤:

  1.登录受信任网站A,并在本地生成Cookie。 2.在不登出A的情况下,访问危险网站B。

看到这里,你也许会说:“如果我不满足以上两个条件中的一个,我就不会受到CSRF的攻击”。是的,确实如此,但你不能保证以下情况不会发生:

  1.你不能保证你登录了一个网站后,不再打开一个tab页面并访问另外的网站。 2.你不能保证你关闭浏览器了后,你本地的Cookie立刻过期,你上次的会话已经结束。(事实上,关闭浏览器不能结束一个会话,但大多数人都会错误的认为关闭浏览器就等于退出登录/结束会话了……) 3.上图中所谓的攻击网站,可能是一个存在其他漏洞的可信任的经常被人访问的网站。

上面大概地讲了一下CSRF攻击的思想,下面我将用几个例子详细说说具体的CSRF攻击,这里我以一个银行转账的操作作为例子(仅仅是例子,真实的银行网站没这么傻:>)

示例1:

银行网站A,它以GET请求来完成银行转账的操作,如:http://www.mybank.com/Transfer.php?toBankId=11&money=1000 危险网站B,它里面有一段HTML的代码如下:

1
<img src=http://www.mybank.com/Transfer.php?toBankId=11&money=1000>

首先,你登录了银行网站A,然后访问危险网站B,噢,这时你会发现你的银行账户少了1000块…… 为什么会这样呢?原因是银行网站A违反了HTTP规范,使用GET请求更新资源。在访问危险网站B的之前,你已经登录了银行网站A,而B中的以GET的方式请求第三方资源(这里的第三方就是指银行网站了,原本这是一个合法的请求,但这里被不法分子利用了),所以你的浏览器会带上你的银行网站A的Cookie发出Get请求,去获取资源“http://www.mybank.com/Transfer.php?toBankId=11&money=1000”,结果银行网站服务器收到请求后,认为这是一个更新资源操作(转账操作),所以就立刻进行转账操作……

  示例2:

为了杜绝上面的问题,银行决定改用POST请求完成转账操作。 银行网站A的WEB表单如下:

1
2
3
4
5
  <form action="Transfer.php" method="POST">
    <p>ToBankId: <input type="text" name="toBankId" /></p>
    <p>Money: <input type="text" name="money" /></p>
    <p><input type="submit" value="Transfer" /></p>
  </form>

后台处理页面Transfer.php如下:

1
2
3
4
5
6
7
  <?php
    session_start();
    if (isset($_REQUEST['toBankId'] && isset($_REQUEST['money']))
    {
        buy_stocks($_REQUEST['toBankId'], $_REQUEST['money']);
    }
  ?>

危险网站B,仍然只是包含那句HTML代码:

1
<img src=http://www.mybank.com/Transfer.php?toBankId=11&money=1000>

和示例1中的操作一样,你首先登录了银行网站A,然后访问危险网站B,结果…..和示例1一样,你再次没了1000块~T_T,这次事故的原因是:银行后台使用了$_REQUEST去获取请求的数据,而$_REQUEST既可以获取GET请求的数据,也可以获取POST请求的数据,这就造成了在后台处理程序无法区分这到底是GET请求的数据还是POST请求的数据。在PHP中,可以使用$_GET和$_POST分别获取GET请求和POST请求的数据。在JAVA中,用于获取请求数据request一样存在不能区分GET请求数据和POST数据的问题。

  示例3:

经过前面2个惨痛的教训,银行决定把获取请求数据的方法也改了,改用$_POST,只获取POST请求的数据,后台处理页面Transfer.php代码如下:

1
2
3
4
5
6
7
  <?php
    session_start();
    if (isset($_POST['toBankId'] && isset($_POST['money']))
    {
        buy_stocks($_POST['toBankId'], $_POST['money']);
    }
  ?>

然而,危险网站B与时俱进,它改了一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<html>
  <head>
    <script type="text/javascript">
      function steal()
      {
               iframe = document.frames["steal"];
               iframe.document.Submit("transfer");
      }
    </script>
  </head>

  <body onload="steal()">
    <iframe name="steal" display="none">
      <form method="POST" name="transfer" action="http://www.myBank.com/Transfer.php">
        <input type="hidden" name="toBankId" value="11">
        <input type="hidden" name="money" value="1000">
      </form>
    </iframe>
  </body>
</html>

如果用户仍是继续上面的操作,很不幸,结果将会是再次不见1000块……因为这里危险网站B暗地里发送了POST请求到银行! 总结一下上面3个例子,CSRF主要的攻击模式基本上是以上的3种,其中以第1,2种最为严重,因为触发条件很简单,一个就可以了,而第3种比较麻烦,需要使用JavaScript,所以使用的机会会比前面的少很多,但无论是哪种情况,只要触发了CSRF攻击,后果都有可能很严重。 理解上面的3种攻击模式,其实可以看出,CSRF攻击是源于WEB的隐式身份验证机制!WEB的身份验证机制虽然可以保证一个请求是来自于某个用户的浏览器,但却无法保证该请求是用户批准发送的!

CSRF的防御

我总结了一下看到的资料,CSRF的防御可以从服务端和客户端两方面着手,防御效果是从服务端着手效果比较好,现在一般的CSRF防御也都在服务端进行。

  1.服务端进行CSRF防御

服务端的CSRF方式方法很多样,但总的思想都是一致的,就是在客户端页面增加伪随机数。 (1).Cookie Hashing(所有表单都包含同一个伪随机值): 这可能是最简单的解决方案了,因为攻击者不能获得第三方的Cookie(理论上),所以表单中的数据也就构造失败了:>

1
2
3
4
5
  <?php
    //构造加密的Cookie信息
    $value = “DefenseSCRF”;
    setcookie(”cookie”, $value, time()+3600);
  ?>

在表单里增加Hash值,以认证这确实是用户发送的请求。

1
2
3
4
5
6
7
8
9
  <?php
    $hash = md5($_COOKIE['cookie']);
  ?>
  <form method=”POST” action=”transfer.php”>
    <input type=”text” name=”toBankId”>
    <input type=”text” name=”money”>
    <input type=”hidden” name=”hash” value=<?=$hash;?>”>
    <input type=”submit” name=”submit” value=”Submit”>
  </form>

然后在服务器端进行Hash值验证

1
2
3
4
5
6
7
8
9
10
11
12
      <?php
        if(isset($_POST['check'])) {
             $hash = md5($_COOKIE['cookie']);
             if($_POST['check'] == $hash) {
                  doJob();
             } else {
        //...
             }
        } else {
      //...
        }
      ?>

这个方法个人觉得已经可以杜绝99%的CSRF攻击了,那还有1%呢….由于用户的Cookie很容易由于网站的XSS漏洞而被盗取,这就另外的1%。一般的攻击者看到有需要算Hash值,基本都会放弃了,某些除外,所以如果需要100%的杜绝,这个不是最好的方法。

2.验证码

这个方案的思路是:每次的用户提交都需要用户在表单中填写一个图片上的随机字符串,厄….这个方案可以完全解决CSRF,但个人觉得在易用性方面似乎不是太好,还有听闻是验证码图片的使用涉及了一个被称为MHTML的Bug,可能在某些版本的微软IE中受影响。

原文链接

在网上发现这篇文章说得很明了,转载啦,非常感谢原作者的辛苦付出。 原文出处

Other

综述

博主没事就喜欢瞎倒腾,今天登录自己的 SAE 一看,很久之前挂的几个应用早已杳无人烟,才消耗几个云豆,真是觉得太浪费了。所以博主就瞎倒腾一下,准备好好利用起自己的云豆,尽可能地为我提供服务。 好了,废话不多说,让我们开始动起来!

新建应用

既然想利用好里面的服务,那么我们就需要新建一个专属应用,取个专属名称,那么我名字叫崔庆才,那就叫 cuiqingcai 好了,幸运的是,并没有同名应用,可以使用,不错不错。 20150912153748

数据库

SAE 上最近出了一种独享数据库,不像之前的数据库一样每建一个应用自动生成一个数据库,这个数据库就像平常我们用到的 phpMyAdmin 一样一样的。可以新建好多个数据库,可以远程连接,可以登录网页设置。 有了这个,如果你没有 VPS,也不需要去买个 RDS 来用啦! 20150912152842 首先建立好应用之后,到应用管理界面去,点击你的 MySQL,然后就会让你选择是哪一种数据库,右边的独享型 MySQL 是 SAE 新出的,功能也非常强,可以建立数据库,可以建用户,授权数据库,监控数据等等。 20150912153909 点击独享型 MySQL,创建属于自己的数据库吧。创建好了,建立自己的账户,登进来,一切都是那么轻松加愉快,以后有什么需要数据库,扔到这里就好了。 20150912154219 好,相信这个一定非常简单,小伙伴们快试一下吧!

Storage

新浪云中提供了 SCS,也是一个云存储服务,但是费用太坑了,而且不是消耗云豆的,然而应用中的 Storage 是使用云豆的,如果不是什么私密存储的话,还是可以利用一下的。 在这里我想让它发挥 SCS 的作用并绑定到我的域名上,这样通过 scs.cuiqingcai.com 便可以访问了。 我们新建一个 Storage Domain 20150912155842 上传一张图片进去,比如 1.jpg,上传后访问一下 20150912160138 恩,轻松加愉快!接下来,我发现这个域名简直太难记了,我要换成自己的域名,比如 scs.cuiqingcai.com 这样,图片就可以通过 scs.cuiqingcai.com/1.jpg 来访问了。那么怎么配置呢?

1.绑定域名

到应用首页里面,点击域名绑定,SAE 会告诉你两条要解析的内容,自己设置一下 20150912160935 接下来我们需要配置一下代码

2.修改 config.yaml

需要在代码管理中,更改一下 config.yaml 文件,让它做一个跳转。 比如我的代码中 config.yaml 文件就更改为

1
2
3
4
name: cuiqingcai
version: 1
handle:
- rewrite: if (path ~ "/(.*)") goto "http://cuiqingcai-scs.stor.sinaapp.com/$1"

其实就是加了一条 reweite 语句,把所有的路径定位到新地址的新路径中。 接下来,我访问一下 scs.cuiqingcai.com/1.jpg,嗯,图片出来了!这样,又一个免费的 CDN 建好了! 20150912161437 嗯,如果有什么图片需要存放,它会是你的一个非常好的选择! 上传的话,SAE 提供了上传的客户端,大家可以自行下载尝试。 之后博主将研究一下怎样配置上传的接口,这样我们就可以通过程序来上传了,我也很期待呢!

结语

以上是博主瞎倒腾的,可以为自己提供一些方便,大家尝试下吧,如有问题,欢迎留言交流!

Linux

博主之前一直用的是 apache,随着网站负荷量增高,感觉 apache 稍微有点力不从心了。随着 nginx 越来越流行,而且其功能强大,博主准备采用 nginx 作为自己的服务器啦。 每当到了环境配置的时候,博主便会去网上各种搜集资料,然而感觉他们讲的都条理不一,有的地方并不符合自己的配置习惯,所以博主习惯自己配置的同时把配置过程总结一下,方便自己,也方便大家。 好,接下来我们就开始我们的环境配置之旅吧。

Nginx

1.更新源

1
sudo apt-get update

2.安装 nginx

1
sudo apt-get install nginx

3.检查是否安装成功

输入 localhost 或者远程地址,若出现 Welcome To Nginx 则证明安装成功。 20150911144903 如果没有看到,可以运行

1
sudo killall apache2

杀掉 apache 进程,因为可能 80 端口被占用了。

4.更改运行目录

默认的 nginx 目录是/usr/share/nginx/html,我们将其修改为/var/www,当然可以根据个人习惯灵活更改。 修改/etc/nginx/sites-available/default 文件

1
root /usr/share/nginx/html;

更改为

1
root /var/www

再将

1
index index.html index.htm;

更改为

1
index index.html index.php index.htm;

重启 nginx

1
sudo service nginx restart

我们在/var/www 目录下新建 index.html 文件,写入一些测试文字。 重新访问 localhost 或者远程地址,可以看到刚才设置的 index.html 文件中的内容,说明目录已经更改成功了。

PHP

安装 PHP 以及相关扩展。

1
sudo apt-get install php5 php5-cgi php5-mysql php5-curl php5-gd php5-idn php-pear php5-imagick php5-imap php5-mcrypt php5-memcache php5-mhash php5-ming php5-pspell php5-recode php5-snmp php5-tidy php5-xmlrpc php5-sqlite php5-xsl

执行上述指令即可。 此时有的小伙伴想要测试运行 PHP 文件了,然而很悲剧地告诉你是不可以的,因为你还没有配置 fastcgi,继续往下看。

MySQL

执行如下命令,安装 MySQL 服务端和客户端。

1
sudo apt-get install mysql-server mysql-client

在安装时可能提示你输入 root 用户的密码,设置即可。

phpMyAdmin

执行如下命令,安装 phpMyAdmin。

1
sudo apt-get install phpmyadmin

创建软连接

1
sudo ln -s /usr/share/phpmyadmin/ /var/www/phpmyadmin

我们将创建一个根目录为/var/www,链接名为 phpmyadmin 的文件,指向/usr/share/phpmyadmin

spawn-fcgi

1.安装 fastcgi

1
sudo apt-get install spawn-fcgi

2.配置 fastcgi

修改 /etc/nginx/fastcgi_params 文件,增加下面一行

1
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

修改/etc/php5/cgi/php.ini 文件,将下面一行取消注释

1
cgi.fix_pathinfo=1;

这样 php-cgi 方能正常使用 SCRIPT_FILENAME 这个变量 修改/etc/nginx/sites-available/default 文件,我们之前修改的目录地址是/var/www,将

1
2
3
4
5
6
7
8
9
10
11
#location ~ \.php$ {
# fastcgi_split_path_info ^(.+\.php)(/.+)$;
# # NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini
#
# # With php5-cgi alone:
# fastcgi_pass 127.0.0.1:9000;
# # With php5-fpm:
# fastcgi_pass unix:/var/run/php5-fpm.sock;
# fastcgi_index index.php;
# include fastcgi_params;
#}

修改为

1
2
3
4
5
6
7
8
9
10
11
12
location ~ \.php$ {
# fastcgi_split_path_info ^(.+\.php)(/.+)$;
# # NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini
#
# # With php5-cgi alone:
fastcgi_pass 127.0.0.1:9000;
# # With php5-fpm:
# fastcgi_pass unix:/var/run/php5-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME /var/www$fastcgi_script_name;
include fastcgi_params;
}

3.开启 fastcgi 进程

1
sudo /usr/bin/spawn-fcgi -a 127.0.0.1 -p 9000 -C 5 -u www-data -g www-data -f /usr/bin/php5-cgi -P /var/run/fastcgi-php.pid

参数含义如下

-f 指定调用 FastCGI 的进程的执行程序位置,根据系统上所装的 PHP 的情况具体设置 -a 绑定到地址 addr -p 绑定到端口 port -s 绑定到 unix socket 的路径 path -C 指定产生的 FastCGI 的进程数,默认为 5(仅用于 PHP) -P 指定产生的进程的 PID 文件路径 * -u 和-g FastCGI 使用什么身份(-u 用户 -g 用户组)运行,Ubuntu 下可以使用 www-data,其他的根据情况配置,如 nobody、apache 等现在可以在 web 根目录下放个探针或 php 文件测试一下了

运行结果类似如下

1
spawn-fcgi: child spawned successfully: PID: 11775

4.设置开机启动 fastcgi

修改/etc/rc.local 文件,添加下面一行

1
/usr/bin/spawn-fcgi -a 127.0.0.1 -p 9000 -C 5 -u www-data -g www-data -f /usr/bin/php5-cgi -P /var/run/fastcgi-php.pid

修改完之后,重启 nginx

1
sudo service nginx restart

我们可以在/var/www 目录下新建 index.php 文件测试运行,发现已经可以运行 PHP 文件了。

测试运行

所有配置已经完毕,现在我们输入 localhost 和 localhost/phpmyadmin 便可以轻松加愉快地访问了。 如有问题,欢迎交流。

Python

2022 年最新 Python3 网络爬虫教程

大家好,我是崔庆才,由于爬虫技术不断迭代升级,一些旧的教程已经过时、案例已经过期,最前沿的爬虫技术比如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等技术层出不穷,我最近新出了一套最新最全面的 Python3 网络爬虫系列教程。

博主自荐:截止 2022 年,可以将最前沿最全面的爬虫技术都涵盖的教程,如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、大规模分布式、Kubernetes 等,市面上目前就这一套了。

最新教程对旧的爬虫技术内容进行了全面更新,搭建了全新的案例平台进行全面讲解,保证案例稳定有效不过期。

教程请移步:

【2022 版】Python3 网络爬虫学习教程

如下为原文。

大家好,本次为大家带来的是抓取爱问知识人的问题并将问题和答案保存到数据库的方法,涉及的内容包括:

  • Urllib 的用法及异常处理
  • Beautiful Soup 的简单应用
  • MySQLdb 的基础用法
  • 正则表达式的简单应用

环境配置

在这之前,我们需要先配置一下环境,我的 Python 的版本为 2.7,需要额外安装的库有两个,一个是 Beautiful Soup,一个是 MySQLdb,在这里附上两个库的下载地址, Beautiful Soup MySQLdb 大家可以下载之后通过如下命令安装

1
python setup.py install

环境配置好之后,我们便可以开心地撸爬虫了

框架思路

首先我们随便找一个分类地址,外语学习 - 爱问知识人,打开之后可以看到一系列的问题列表。 我们在这个页面需要获取的东西有: 总的页码数,每一页的所有问题链接。 接下来我们需要遍历所有的问题,来抓取每一个详情页面,提取问题,问题内容,回答者,回答时间,回答内容。 最后,我们需要把这些内容存储到数据库中。

要点简析

其实大部分内容相信大家会了前面的内容,这里的爬虫思路已经融汇贯通了,这里就说一下一些扩展的功能

1.日志输出

日志输出,我们要输出时间和爬取的状态,比如像下面这样:

[2015-08-10 03:05:20] 113011 号问题存在其他答案 我个人认为应该是樱桃沟很美的 [2015-08-10 03:05:20] 保存到数据库,此问题的 ID 为 113011 [2015-08-10 03:05:20] 当前爬取第 2 的内容,发现一个问题 百度有一个地方,花儿带着芳香,水儿流淌奔腾是什么意思 多多帮忙哦 回答数量 1 [2015-08-10 03:05:19] 保存到数据库,此问题的 ID 为 113010

所以,我们需要引入时间函数,然后写一个获取当前时间的函数

1
2
3
4
5
6
7
8
9
import time

#获取当前时间
def getCurrentTime(self):
return time.strftime('[%Y-%m-%d %H:%M:%S]',time.localtime(time.time()))

#获取当前时间
def getCurrentDate(self):
return time.strftime('%Y-%m-%d',time.localtime(time.time()))

以上分别是获取带具体时间和获取日期的函数,在输出时,我们可以在输出语句的前面调用这函数即可。 然后我们需要将缓冲区设置输出到 log 中,在程序的最前面加上这两句即可

1
2
f_handler=open('out.log', 'w')
sys.stdout=f_handler

这样,所有的 print 语句输出的内容就会保存到 out.log 文件中了。

2.页码保存

爬虫爬取过程中可能出现各种各样的错误,这样会导致爬虫的中断,如果我们重新运行爬虫,那么就会导致爬虫从头开始运行了,这样显然是不合理的。所以,我们需要把当前爬取的页面保存下来,比如可以保存到文本中,假如爬虫中断了,重新运行爬虫,读取文本文件的内容,接着爬取即可。 大家可以稍微参考一下函数的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#主函数
def main(self):
f_handler=open('out.log', 'w')
sys.stdout=f_handler
page = open('page.txt', 'r')
content = page.readline()
start_page = int(content.strip()) - 1
page.close()
print self.getCurrentTime(),"开始页码",start_page
print self.getCurrentTime(),"爬虫正在启动,开始爬取爱问知识人问题"
self.total_num = self.getTotalPageNum()
print self.getCurrentTime(),"获取到目录页面个数",self.total_num,"个"
if not start_page:
start_page = self.total_num
for x in range(1,start_page):
print self.getCurrentTime(),"正在抓取第",start_page-x+1,"个页面"
try:
self.getQuestions(start_page-x+1)
except urllib2.URLError, e:
if hasattr(e, "reason"):
print self.getCurrentTime(),"某总页面内抓取或提取失败,错误原因", e.reason
except Exception,e:
print self.getCurrentTime(),"某总页面内抓取或提取失败,错误原因:",e
if start_page-x+1 < start_page:
f=open('page.txt','w')
f.write(str(start_page-x+1))
print self.getCurrentTime(),"写入新页码",start_page-x+1
f.close()

这样,不管我们爬虫中途遇到什么错误,妈妈也不会担心了

3.页面处理

页面处理过程中,我们可能遇到各种各样奇葩的 HTML 代码,和上一节一样,我们沿用一个页面处理类即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import re

#处理页面标签类
class Tool:

#将超链接广告剔除
removeADLink = re.compile('<div class="link_layer.*?</div>')
#去除img标签,1-7位空格,&nbsp;
removeImg = re.compile('<img.*?>| {1,7}|&nbsp;')
#删除超链接标签
removeAddr = re.compile('<a.*?>|</a>')
#把换行的标签换为\n
replaceLine = re.compile('<tr>|<div>|</div>|</p>')
#将表格制表<td>替换为\t
replaceTD= re.compile('<td>')
#将换行符或双换行符替换为\n
replaceBR = re.compile('<br><br>|<br>')
#将其余标签剔除
removeExtraTag = re.compile('<.*?>')
#将多行空行删除
removeNoneLine = re.compile('\n+')

def replace(self,x):
x = re.sub(self.removeADLink,"",x)
x = re.sub(self.removeImg,"",x)
x = re.sub(self.removeAddr,"",x)
x = re.sub(self.replaceLine,"\n",x)
x = re.sub(self.replaceTD,"\t",x)
x = re.sub(self.replaceBR,"\n",x)
x = re.sub(self.removeExtraTag,"",x)
x = re.sub(self.removeNoneLine,"\n",x)
#strip()将前后多余内容删除
return x.strip()

我们可以用一段含有 HTML 代码的文字,经过调用 replace 方法之后,各种冗余的 HTML 代码就会处理好了。 比如我们这么一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
<article class="article-content">
<h2>前言</h2>
<p>最近发现MySQL服务隔三差五就会挂掉,导致我的网站和爬虫都无法正常运作。自己的网站是基于MySQL,在做爬虫存取一些资料的时候也是基于MySQL,数据量一大了,MySQL它就有点受不了了,时不时会崩掉,虽然我自己有网站监控和邮件通知,但是好多时候还是需要我来手动连接我的服务器重新启动一下我的MySQL,这样简直太不友好了,所以,我就觉定自己写个脚本,定时监控它,如果发现它挂掉了就重启它。</p>
<p>好了,闲言碎语不多讲,开始我们的配置之旅。</p>
<p>运行环境:<strong>Ubuntu Linux 14.04</strong></p>
<h2>编写Shell脚本</h2>
<p>首先,我们要编写一个shell脚本,脚本主要执行的逻辑如下:</p>
<p>显示mysqld进程状态,如果判断进程未在运行,那么输出日志到文件,然后启动mysql服务,如果进程在运行,那么不执行任何操作,可以选择性输出监测结果。</p>
<p>可能大家对于shell脚本比较陌生,在这里推荐官方的shell脚本文档来参考一下</p>
<p><a href="http://wiki.ubuntu.org.cn/Shell%E7%BC%96%E7%A8%8B%E5%9F%BA%E7%A1%80" data-original-title="" title="">Ubuntu Shell 编程基础</a></p>
<p>shell脚本的后缀为sh,在任何位置新建一个脚本文件,我选择在 /etc/mysql 目录下新建一个 listen.sh 文件。</p>
<p>执行如下命令:</p>

经过处理后便会变成如下的样子:

1
2
3
4
5
6
7
8
9
10
11
前言
最近发现MySQL服务隔三差五就会挂掉,导致我的网站和爬虫都无法正常运作。自己的网站是基于MySQL,在做爬虫存取一些资料的时候也是基于MySQL,数据量一大了,MySQL它就有点受不了了,时不时会崩掉,虽然我自己有网站监控和邮件通知,但是好多时候还是需要我来手动连接我的服务器重新启动一下我的MySQL,这样简直太不友好了,所以,我就觉定自己写个脚本,定时监控它,如果发现它挂掉了就重启它。
好了,闲言碎语不多讲,开始我们的配置之旅。
运行环境:UbuntuLinux14.04
编写Shell脚本
首先,我们要编写一个shell脚本,脚本主要执行的逻辑如下:
显示mysqld进程状态,如果判断进程未在运行,那么输出日志到文件,然后启动mysql服务,如果进程在运行,那么不执行任何操作,可以选择性输出监测结果。
可能大家对于shell脚本比较陌生,在这里推荐官方的shell脚本文档来参考一下
UbuntuShell编程基础
shell脚本的后缀为sh,在任何位置新建一个脚本文件,我选择在/etc/mysql目录下新建一个listen.sh文件。
执行如下命令:

经过上面的处理,所有乱乱的代码都会被处理好了。

4.保存到数据库

在这里,我们想实现一个通用的方法,就是把存储的一个个内容变成字典的形式,然后执行插入语句的时候,自动构建对应的 sql 语句,插入数据。 比如我们构造如下的字典:

1
2
3
4
5
6
7
8
#构造最佳答案的字典
good_ans_dict = {
"text": good_ans[0],
"answerer": good_ans[1],
"date": good_ans[2],
"is_good": str(good_ans[3]),
"question_id": str(insert_id)
}

构造 sql 语句并插入到数据库的方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#插入数据
def insertData(self, table, my_dict):
try:
self.db.set_character_set('utf8')
cols = ', '.join(my_dict.keys())
values = '"," '.join(my_dict.values())
sql = "INSERT INTO %s (%s) VALUES (%s)" % (table, cols, '"'+values+'"')
try:
result = self.cur.execute(sql)
insert_id = self.db.insert_id()
self.db.commit()
#判断是否执行成功
if result:
return insert_id
else:
return 0
except MySQLdb.Error,e:
#发生错误时回滚
self.db.rollback()
#主键唯一,无法插入
if "key 'PRIMARY'" in e.args[1]:
print self.getCurrentTime(),"数据已存在,未插入数据"
else:
print self.getCurrentTime(),"插入数据失败,原因 %d: %s" % (e.args[0], e.args[1])
except MySQLdb.Error,e:
print self.getCurrentTime(),"数据库错误,原因%d: %s" % (e.args[0], e.args[1])

这里我们只需要传入那个字典,便会构建出对应字典键值和键名的 sql 语句,完成插入。

5.PHP 读取日志

我们将运行结果输出到了日志里,那么怎么查看日志呢?很简单,在这里提供两种方法 方法一: PHP 倒序输出所有日志内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="refresh" content = "5">
</head>
<body>
<?php
$fp = file("out.log");
if ($fp) {
for($i = count($fp) - 1;$i >= 0; $i --)
echo $fp[$i]."<br>";
}
?>
</body>
</html>

此方法可以看到所有的输入日志,但是如果日志太大了,那么就会报耗费内存太大,无法输出。为此我们就有了第二种方法,利用 linux 命令,输出后十行内容。 方法二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="refresh" content = "5">
</head>
<body>
<?php
$ph = popen('tail -n 100 out.log','r');
while($r = fgets($ph)){
echo $r."<br>";
}
pclose($ph);
?>
</body>
</html>

上面两种方法都是 5 秒刷新一次网页来查看最新的日志。

源代码放送

好了,闲言碎语不多讲,直接上源码了

1
spider.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# -*- coding:utf-8 -*-

import urllib
import urllib2
import re
import time
import types
import page
import mysql
import sys
from bs4 import BeautifulSoup

class Spider:

#初始化
def __init__(self):
self.page_num = 1
self.total_num = None
self.page_spider = page.Page()
self.mysql = mysql.Mysql()

#获取当前时间
def getCurrentTime(self):
return time.strftime('[%Y-%m-%d %H:%M:%S]',time.localtime(time.time()))

#获取当前时间
def getCurrentDate(self):
return time.strftime('%Y-%m-%d',time.localtime(time.time()))

#通过网页的页码数来构建网页的URL
def getPageURLByNum(self, page_num):
page_url = "http://iask.sina.com.cn/c/978-all-" + str(page_num) + ".html"
return page_url


#通过传入网页页码来获取网页的HTML
def getPageByNum(self, page_num):
request = urllib2.Request(self.getPageURLByNum(page_num))
try:
response = urllib2.urlopen(request)
except urllib2.URLError, e:
if hasattr(e, "code"):
print self.getCurrentTime(),"获取页面失败,错误代号", e.code
return None
if hasattr(e, "reason"):
print self.getCurrentTime(),"获取页面失败,原因", e.reason
return None
else:
page = response.read().decode("utf-8")
return page

#获取所有的页码数
def getTotalPageNum(self):
print self.getCurrentTime(),"正在获取目录页面个数,请稍候"
page = self.getPageByNum(1)
#匹配所有的页码数,\u4e0b\u4e00\u9875是下一页的UTF8编码
pattern = re.compile(u'<span class="more".*?>.*?<span.*?<a href.*?class="">(.*?)</a>\s*<a.*?\u4e0b\u4e00\u9875</a>', re.S)
match = re.search(pattern, page)
if match:
return match.group(1)
else:
print self.getCurrentTime(),"获取总页码失败"

#分析问题的代码,得到问题的提问者,问题内容,回答个数,提问时间
def getQuestionInfo(self, question):
if not type(question) is types.StringType:
question = str(question)
#print question
pattern = re.compile(u'<span.*?question-face.*?>.*?<img.*?alt="(.*?)".*?</span>.*?<a href="(.*?)".*?>(.*?)</a>.*?answer_num.*?>(\d*).*?</span>.*?answer_time.*?>(.*?)</span>', re.S)
match = re.search(pattern, question)
if match:
#获得提问者
author = match.group(1)
#问题链接
href = match.group(2)
#问题详情
text = match.group(3)
#回答个数
ans_num = match.group(4)
#回答时间
time = match.group(5)
time_pattern = re.compile('\d{4}\-\d{2}\-\d{2}', re.S)
time_match = re.search(time_pattern, time)
if not time_match:
time = self.getCurrentDate()
return [author, href, text, ans_num, time]
else:
return None

#获取全部问题
def getQuestions(self, page_num):
#获得目录页面的HTML
page = self.getPageByNum(page_num)
soup = BeautifulSoup(page)
#分析获得所有问题
questions = soup.select("div.question_list ul li")
#遍历每一个问题
for question in questions:
#获得问题的详情
info = self.getQuestionInfo(question)
if info:
#得到问题的URL
url = "http://iask.sina.com.cn/" + info[1]
#通过URL来获取问题的最佳答案和其他答案
ans = self.page_spider.getAnswer(url)
print self.getCurrentTime(),"当前爬取第",page_num,"的内容,发现一个问题",info[2],"回答数量",info[3]
#构造问题的字典,插入问题
ques_dict = {
"text": info[2],
"questioner": info[0],
"date": info[4],
"ans_num": info[3],
"url": url
}
#获得插入的问题的自增ID
insert_id = self.mysql.insertData("iask_questions",ques_dict)
#得到最佳答案
good_ans = ans[0]
print self.getCurrentTime(),"保存到数据库,此问题的ID为",insert_id
#如果存在最佳答案,那么就插入
if good_ans:
print self.getCurrentTime(),insert_id,"号问题存在最佳答案",good_ans[0]
#构造最佳答案的字典
good_ans_dict = {
"text": good_ans[0],
"answerer": good_ans[1],
"date": good_ans[2],
"is_good": str(good_ans[3]),
"question_id": str(insert_id)
}
#插入最佳答案
if self.mysql.insertData("iask_answers",good_ans_dict):
print self.getCurrentTime(),"保存最佳答案成功"
else:
print self.getCurrentTime(),"保存最佳答案失败"
#获得其他答案
other_anses = ans[1]
#遍历每一个其他答案
for other_ans in other_anses:
#如果答案存在
if other_ans:
print self.getCurrentTime(),insert_id,"号问题存在其他答案",other_ans[0]
#构造其他答案的字典
other_ans_dict = {
"text": other_ans[0],
"answerer": other_ans[1],
"date": other_ans[2],
"is_good": str(other_ans[3]),
"question_id": str(insert_id)
}
#插入这个答案
if self.mysql.insertData("iask_answers",other_ans_dict):
print self.getCurrentTime(),"保存其他答案成功"
else:
print self.getCurrentTime(),"保存其他答案失败"

#主函数
def main(self):
f_handler=open('out.log', 'w')
sys.stdout=f_handler
page = open('page.txt', 'r')
content = page.readline()
start_page = int(content.strip()) - 1
page.close()
print self.getCurrentTime(),"开始页码",start_page
print self.getCurrentTime(),"爬虫正在启动,开始爬取爱问知识人问题"
self.total_num = self.getTotalPageNum()
print self.getCurrentTime(),"获取到目录页面个数",self.total_num,"个"
if not start_page:
start_page = self.total_num
for x in range(1,start_page):
print self.getCurrentTime(),"正在抓取第",start_page-x+1,"个页面"
try:
self.getQuestions(start_page-x+1)
except urllib2.URLError, e:
if hasattr(e, "reason"):
print self.getCurrentTime(),"某总页面内抓取或提取失败,错误原因", e.reason
except Exception,e:
print self.getCurrentTime(),"某总页面内抓取或提取失败,错误原因:",e
if start_page-x+1 < start_page:
f=open('page.txt','w')
f.write(str(start_page-x+1))
print self.getCurrentTime(),"写入新页码",start_page-x+1
f.close()

spider = Spider()
spider.main()
1
page.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# -*- coding:utf-8 -*-
import urllib
import urllib2
import re
import time
import types
import tool
from bs4 import BeautifulSoup

#抓取分析某一问题和答案
class Page:

def __init__(self):
self.tool = tool.Tool()

#获取当前时间
def getCurrentDate(self):
return time.strftime('%Y-%m-%d',time.localtime(time.time()))

#获取当前时间
def getCurrentTime(self):
return time.strftime('[%Y-%m-%d %H:%M:%S]',time.localtime(time.time()))

#通过页面的URL来获取页面的代码
def getPageByURL(self, url):
try:
request = urllib2.Request(url)
response = urllib2.urlopen(request)
return response.read().decode("utf-8")
except urllib2.URLError, e:
if hasattr(e, "code"):
print self.getCurrentTime(),"获取问题页面失败,错误代号", e.code
return None
if hasattr(e, "reason"):
print self.getCurrentTime(),"获取问题页面失败,原因", e.reason
return None


#传入一个List,返回它的标签里的内容,如果为空返回None
def getText(self, html):
if not type(html) is types.StringType:
html = str(html)
#提取出<pre>标签里的内容
pattern = re.compile('<pre.*?>(.*?)</pre>', re.S)
match = re.search(pattern, html)
#如果匹配成功
if match:
return match.group(1)
else:
return None

#传入最佳答案的HTML,分析出回答者和回答时间
def getGoodAnswerInfo(self, html):
pattern = re.compile('"answer_tip.*?<a.*?>(.*?)</a>.*?<span class="time.*?>.*?\|(.*?)</span>', re.S)
match = re.search(pattern, html)
#如果匹配,返回回答者和回答时间
if match:
time = match.group(2)
time_pattern = re.compile('\d{2}\-\d{2}\-\d{2}', re.S)
time_match = re.search(time_pattern, time)
if not time_match:
time = self.getCurrentDate()
else:
time = "20"+time
return [match.group(1),time]
else:
return [None,None]

#获得最佳答案
def getGoodAnswer(self, page):
soup = BeautifulSoup(page)
text = soup.select("div.good_point div.answer_text pre")
if len(text) > 0:
#获得最佳答案的内容
ansText = self.getText(str(text[0]))
ansText = self.tool.replace(ansText)
#获得最佳答案的回答者信息
info = soup.select("div.good_point div.answer_tip")
ansInfo = self.getGoodAnswerInfo(str(info[0]))
#将三者组合成一个List
answer = [ansText, ansInfo[0], ansInfo[1],1]
return answer
else:
#如果不存在最佳答案,那么就返回空
return None

#传入回答者HTML,分析出回答者,回答时间
def getOtherAnswerInfo(self, html):
if not type(html) is types.StringType:
html = str(html)
pattern = re.compile('"author_name.*?>(.*?)</a>.*?answer_t">(.*?)</span>', re.S)
match = re.search(pattern, html)
#获得每一个回答的回答者信息和回答时间
if match:
time = match.group(2)
time_pattern = re.compile('\d{2}\-\d{2}\-\d{2}', re.S)
time_match = re.search(time_pattern, time)
if not time_match:
time = self.getCurrentDate()
else:
time = "20"+time
return [match.group(1),time]
else:
return [None,None]


#获得其他答案
def getOtherAnswers(self, page):
soup = BeautifulSoup(page)
results = soup.select("div.question_box li.clearfix .answer_info")
#所有答案,包含好多个List,每个List包含了回答内容,回答者,回答时间
answers = []
for result in results:
#获得回答内容
ansSoup = BeautifulSoup(str(result))
text = ansSoup.select(".answer_txt span pre")
ansText = self.getText(str(text[0]))
ansText = self.tool.replace(ansText)
#获得回答者和回答时间
info = ansSoup.select(".answer_tj")
ansInfo = self.getOtherAnswerInfo(info[0])
#将三者组合成一个List
answer = [ansText, ansInfo[0], ansInfo[1],0]
#加入到answers
answers.append(answer)
return answers

#主函数
def getAnswer(self, url):
if not url:
url = "http://iask.sina.com.cn/b/gQiuSNCMV.html"
page = self.getPageByURL(url)
good_ans = self.getGoodAnswer(page)
other_ans = self.getOtherAnswers(page)
return [good_ans,other_ans]


page = Page()
page.getAnswer(None)
1
tool.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#-*- coding:utf-8 -*-
import re

#处理页面标签类
class Tool:

#将超链接广告剔除
removeADLink = re.compile('<div class="link_layer.*?</div>')
#去除img标签,1-7位空格,&nbsp;
removeImg = re.compile('<img.*?>| {1,7}|&nbsp;')
#删除超链接标签
removeAddr = re.compile('<a.*?>|</a>')
#把换行的标签换为\n
replaceLine = re.compile('<tr>|<div>|</div>|</p>')
#将表格制表<td>替换为\t
replaceTD= re.compile('<td>')
#将换行符或双换行符替换为\n
replaceBR = re.compile('<br><br>|<br>')
#将其余标签剔除
removeExtraTag = re.compile('<.*?>')
#将多行空行删除
removeNoneLine = re.compile('\n+')

def replace(self,x):
x = re.sub(self.removeADLink,"",x)
x = re.sub(self.removeImg,"",x)
x = re.sub(self.removeAddr,"",x)
x = re.sub(self.replaceLine,"\n",x)
x = re.sub(self.replaceTD,"\t",x)
x = re.sub(self.replaceBR,"\n",x)
x = re.sub(self.removeExtraTag,"",x)
x = re.sub(self.removeNoneLine,"\n",x)
#strip()将前后多余内容删除
return x.strip()
1
mysql.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# -*- coding:utf-8 -*-


import MySQLdb
import time

class Mysql:

#获取当前时间
def getCurrentTime(self):
return time.strftime('[%Y-%m-%d %H:%M:%S]',time.localtime(time.time()))

#数据库初始化
def __init__(self):
try:
self.db = MySQLdb.connect('ip','username','password','db_name')
self.cur = self.db.cursor()
except MySQLdb.Error,e:
print self.getCurrentTime(),"连接数据库错误,原因%d: %s" % (e.args[0], e.args[1])

#插入数据
def insertData(self, table, my_dict):
try:
self.db.set_character_set('utf8')
cols = ', '.join(my_dict.keys())
values = '"," '.join(my_dict.values())
sql = "INSERT INTO %s (%s) VALUES (%s)" % (table, cols, '"'+values+'"')
try:
result = self.cur.execute(sql)
insert_id = self.db.insert_id()
self.db.commit()
#判断是否执行成功
if result:
return insert_id
else:
return 0
except MySQLdb.Error,e:
#发生错误时回滚
self.db.rollback()
#主键唯一,无法插入
if "key 'PRIMARY'" in e.args[1]:
print self.getCurrentTime(),"数据已存在,未插入数据"
else:
print self.getCurrentTime(),"插入数据失败,原因 %d: %s" % (e.args[0], e.args[1])
except MySQLdb.Error,e:
print self.getCurrentTime(),"数据库错误,原因%d: %s" % (e.args[0], e.args[1])

数据库建表 SQL 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
CREATE TABLE IF NOT EXISTS `iask_answers` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
`text` text NOT NULL COMMENT '回答内容',
`question_id` int(18) NOT NULL COMMENT '问题ID',
`answerer` varchar(255) NOT NULL COMMENT '回答者',
`date` varchar(255) NOT NULL COMMENT '回答时间',
`is_good` int(11) NOT NULL COMMENT '是否是最佳答案',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `iask_questions` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '问题ID',
`text` text NOT NULL COMMENT '问题内容',
`questioner` varchar(255) NOT NULL COMMENT '提问者',
`date` date NOT NULL COMMENT '提问时间',
`ans_num` int(11) NOT NULL COMMENT '回答数量',
`url` varchar(255) NOT NULL COMMENT '问题链接',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

运行的时候执行如下命令即可

1
nohup python spider.py &

代码写的不好,仅供大家学习参考使用,如有问题,欢迎留言交流。

运行结果查看

我们把 PHP 文件和 log 文件放在同一目录下,运行 PHP 文件,便可以看到如下的内容: 20150908222744 小伙伴们赶快试一下吧。

Other

前言

最近发现 MySQL 服务隔三差五就会挂掉,导致我的网站和爬虫都无法正常运作。自己的网站是基于 MySQL,在做爬虫存取一些资料的时候也是基于 MySQL,数据量一大了,MySQL 它就有点受不了了,时不时会崩掉,虽然我自己有网站监控和邮件通知,但是好多时候还是需要我来手动连接我的服务器重新启动一下我的 MySQL,这样简直太不友好了,所以,我就觉定自己写个脚本,定时监控它,如果发现它挂掉了就重启它。 好了,闲言碎语不多讲,开始我们的配置之旅。 运行环境:Ubuntu Linux 14.04

编写 Shell 脚本

首先,我们要编写一个 shell 脚本,脚本主要执行的逻辑如下: 显示 mysqld 进程状态,如果判断进程未在运行,那么输出日志到文件,然后启动 mysql 服务,如果进程在运行,那么不执行任何操作,可以选择性输出监测结果。 可能大家对于 shell 脚本比较陌生,在这里推荐官方的 shell 脚本文档来参考一下 Ubuntu Shell 编程基础 shell 脚本的后缀为 sh,在任何位置新建一个脚本文件,我选择在 /etc/mysql 目录下新建一个 listen.sh 文件。 执行如下命令:

1
2
3
cd /etc/mysql
touch listen.sh
vi listen.sh

进入到 vi 中,我们添加如下脚本内容:

1
2
3
4
5
6
7
8
9
#!/bin/bash
pgrep mysqld &> /dev/null
if [ $? -gt 0 ]
then
echo "`date` mysql is stop"
service mysql start
else
echo "`date` mysql running"
fi

其中 pgrep mysqld 是监测 mysqld 服务的运行状态,&> /dev/null 是将其结果输出到空文件,也就是不保存输出信息 $? 是拿到上一条命令的运行结果,-gt 0 是判断是否大于 0,后面则是输出时间到日志文件,然后启动 mysql,否则不启动 mysql 保存好了,那么我们执行如下的命令,来测试一下。 20150816164552 贴心的命令文字版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
root@iZ28uogb3laZ:/etc/mysql# vi listen.sh
root@iZ28uogb3laZ:/etc/mysql# pgrep mysqld
3359
root@iZ28uogb3laZ:/etc/mysql# chmod 777 listen.sh
root@iZ28uogb3laZ:/etc/mysql# ./listen.sh
Sun Aug 16 16:44:58 CST 2015 mysql running
root@iZ28uogb3laZ:/etc/mysql# sudo service mysql stop
mysql stop/waiting
root@iZ28uogb3laZ:/etc/mysql# ./listen.sh
Sun Aug 16 16:45:17 CST 2015 mysql is stop
mysql start/running, process 4084
root@iZ28uogb3laZ:/etc/mysql# ./listen.sh
Sun Aug 16 16:45:24 CST 2015 mysql running
root@iZ28uogb3laZ:/etc/mysql#

嗯,编辑完了.sh 文件之后,我们首先要对其进行授权,增加可执行的权限。

1
sudo chmod 777 listen.sh

然后运行脚本测试一下,显示 mysql 正在运行。把 mysql 关掉,运行脚本,便会检测到 mysql 已关闭,然后重新启动了 mysql,再次运行,便会发现 mysql 正常运行了。 注:这里我比较纳闷 shell 脚本中 pgrep mysqld 的返回结果是什么。为什么它大于 0 便代表 mysql 服务挂掉了呢?感觉逻辑有点是相反的,不过实测可用有效。如果大家明白为什么,非常希望您可以给我一个解释。

修改日志输出

好,接下来我们把输出的内容保存到日志里。修改脚本文件如下

1
2
3
4
5
6
7
8
9
#!/bin/bash
pgrep mysqld &> /dev/null
if [ $? -gt 0 ]
then
echo "`date` mysql is stop" >> /var/log/mysql_listen.log
service mysql start
else
echo "`date` mysql running" >> /var/log/mysql_listen.log
fi

这样,每执行一次脚本,输出结果都会被保存到 /var/log/mysql_listen.log 中了。

添加定时任务

好了,脚本可以顺利执行了,那么我们就需要定时调用一下这个脚本来运行了,我们需要用到 cron。 首先我们需要编辑一下 corn 调度表格,命令如下:

1
crontab -e

如果你是第一次编辑这个,他会让你选择文件打开方式,随便选一个数字就好了。 比如我们用 GNU 打开的,我们就在它的最后一行添加下面的一句话即可。 20150816165506、 文字版本:

1
*/5 * * * * /etc/mysql/mysql_listen.sh

/5 代表五分钟执行一次,后面的四个点依次代表了,小时,日,月,星期。如果想要时间长一些,比如一小时调度一次,那就设置一下后面第一个*就好了。 好,保存一下,重启 cron 服务。

1
service cron restart

嗯,调度任务已经添加进去了,这样,每五分钟系统就会调用一下刚才写的那个脚本。 过一段时间,我们来看一下运行效果,嗯,监控跑的很顺利呐。 20150816165858 文字版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Sun Aug 16 15:39:12 CST 2015 mysql running
Sun Aug 16 15:40:01 CST 2015 mysql running
Sun Aug 16 15:45:02 CST 2015 mysql running
Sun Aug 16 15:50:01 CST 2015 mysql running
Sun Aug 16 15:55:01 CST 2015 mysql running
Sun Aug 16 16:00:01 CST 2015 mysql running
Sun Aug 16 16:05:01 CST 2015 mysql running
Sun Aug 16 16:10:01 CST 2015 mysql running
Sun Aug 16 16:15:01 CST 2015 mysql running
Sun Aug 16 16:20:01 CST 2015 mysql running
Sun Aug 16 16:25:01 CST 2015 mysql running
Sun Aug 16 16:30:01 CST 2015 mysql running
Sun Aug 16 16:35:01 CST 2015 mysql running
Sun Aug 16 16:40:01 CST 2015 mysql running
Sun Aug 16 16:51:04 CST 2015 mysql running

哈哈,是不是五分钟监测了一次呢?大功告成。

后记

这样,我们就实现了五分钟定时检测 MySQL 进程服务,妈妈再也不用担心我的网站会宕掉啦。 如有问题,欢迎留言交流,谢谢啦。

C/C++

最近在准备C语言的上级考试,之前对C接触不多,在练习过程中把一些小知识点记录下来。

1.字符串的截取

利用strncpy函数,传入三个参数,分别为目标字符串,起始位置,长度。 例如将日期字符串转化为数字,如20120112

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main() {

char date1[20],date2[20];
scanf("%s",&date1);
scanf("%s",&date2);
char temp[4];
int year1 = atoi(strncpy(temp,date1,4));
int year2 = atoi(strncpy(temp,date2,4));
printf("year1:%d\n",year1);
printf("year2:%d\n",year2);
char temp2[2];
int month1 = atoi(strncpy(temp2,date1+4,2));
int month2 = atoi(strncpy(temp2,date2+4,2));
printf("month1:%d\n",month1);
printf("month2:%d\n",month2);
int day1 = atoi(strncpy(temp2,date1+6,2));
int day2 = atoi(strncpy(temp2,date2+6,2));
printf("day1:%d\n",day1);
printf("day2:%d\n",day2);
return 0;

}

以上便实现了输入一个日期然后对其进行分割的操作。

2.二维数组的动态声明

利用malloc可以实现数组的动态声明

1
2
3
4
5
6
7
8
9
int **a;
a = (int **)malloc(2*sizeof(int *));
int i,j;
for (i = 0; i < 2; i ++) {
a[i] = (int *)malloc(3*sizeof(int));
for (j = 0; j < 3; j++) {
scanf("%d",&a[i][j]);
}
}

以上便实现了动态数组的分配,利用scanf为数组赋值

3.二维数组的声明和初始化

头文件

1
#include <memory.h>

初始化和测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int result[2][2];
for (i = 0; i < 2; i ++) {
for (j = 0; j < 2; j++) {
printf("%d ",result[i][j]);
}
printf("\n");
}
memset(result,0,sizeof(int)*4);
for (i = 0; i < 2; i ++) {
for (j = 0; j < 2; j++) {
printf("%d ",result[i][j]);
}
printf("\n");
}

结果

1
2
3
4
4196944 0 
4195696 0
0 0
0 0

上述是数组的非动态声明

4.快速排序

假设要排序的数组是A[1]……A[N],首先任意选取一个数据(通常选用第一个数据)作为关键数据,然后将所有比它的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一趟快速排序。一趟快速排序的算法是: 1)设置两个变量I、J,排序开始的时候 I=0,J=N-1; 2)以第一个数组元素作为关键数据,赋值给X,即X=A[0]; 3)从J开始向前搜索,即由后开始向前搜索,找到第一个小于X的值,两者交换; 4)从I开始向后搜索,即由前开始向后搜索,找到第一个大于X的值,两者交换; 5)重复第3、4步,直到I=J; 例如:待排序的数组A的值分别是:(初始关键数据X:=49) A[0] A[1] A[2] A[3] A[4] A[5] A[6] 49 38 65 97 76 13 27 进行第一次交换后: 27 38 65 97 76 13 49 ( 按照算法的第三步从后面开始找 ) 进行第二次交换后: 27 38 49 97 76 13 65 ( 按照算法的第四步从前面开始找>X的值,65>49,两者交换,此时I=3 ) 进行第三次交换后: 27 38 13 97 76 49 65 ( 按照算法的第五步将又一次执行算法的第三步从后开始找) 进行第四次交换后: 27 38 13 49 76 97 65 ( 按照算法的第四步从前面开始找大于X的值,97>49,两者交换,此时J=4 ) 此时再执行第三不的时候就发现I=J,从而结束一躺快速排序,那么经过一躺快速排序之后: 27 38 13 49 76 97 65 即所有大于49的数全部在49的后面,所有小于49的数全部在49的前面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <stdio.h>

void quiksort(int a[],int low,int high)
{
int i = low;
int j = high;
int temp = a[i];

if( low < high)
{
while(i < j)
{
while((a[j] >= temp) && (i < j))
{
j--;
}
a[i] = a[j];
while((a[i] <= temp) && (i < j))
{
i++;
}
a[j]= a[i];
}
a[i] = temp;
quiksort(a,low,i-1);
quiksort(a,j+1,high);
}
else
{
return;
}
}

int main()
{
int arr[5] = {23,1,21,4,19};
quiksort(arr,0,4);
int i;
for(i=0;i<5;i++)
{
printf("%d ",arr[i]);
}
printf("\n");
return 0;
}

快速排序代码如上

5.字符串拷贝

小例子如下

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(){
char str[20] = "hello";
char *a = "world";
strcpy(str,a);
printf("%s",str);
printf("\n");
system("pause");
return 0;
}

运行结果:world,即把后者完全覆盖前者。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(){
char *str = new char[6];
char *a = "world";
strcpy(str,a);
printf("%s", str);
printf("\n");
system("pause");
return 0;
}

运行结果一致 某一长度的字符串截取

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(){
char *str = new char[6];
char *a = "world";
strncpy(str,a+1,5);
printf("%s", str);
printf("\n");
system("pause");
return 0;
}

运行结果:orld

6.字符串的拼接

小例子如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(){
char str[20] = "hello ";
char *a = "world";
char *x = strcat(str,a);
printf("%s\n", x);
printf("%s\n", x);
printf("\n");
system("pause");
return 0;
}

运行结果: hello world hello world 此函数既返回结果,又将目标字符串赋值

7.字符串查找匹配

例子如下

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(){
char str[20] = "hello ";
char *x = strchr(str,'e');
printf("%d\n", x - str);
printf("%s\n",x);
printf("\n");
system("pause");
return 0;
}

运行结果: 1 ello

8.字符串比较

例子如下

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(){
char str[20] = "hello ";
char str2[20] = "hello2";
int x = strcmp(str, str2);
printf("%d\n",x);
printf("\n");
system("pause");
return 0;
}

运行结果-1 忽略大小写

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(){
char str[20] = "hello";
char str2[20] = "Hello";
int x = stricmp(str, str2);
printf("%d\n",x);
printf("\n");
system("pause");
return 0;
}

运行结果0

9.字符串分割

示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main()
{
char test1[] = "feng,ke,wei";
char x[3][30];
char *p;
p = strtok(test1, ",");
int count = 0;
while (p) {
printf("%s\n", p);
strcpy(x[count],p);
count++;
p = strtok(NULL, ",");
}
for (int i = 0; i<count; i ++) {
printf("%s ", x[i]);
}
system("pause");
return 0;
}

运行结果 feng ke wei feng ke wei

10.格式化输出几位小数

例如

1
printf("%.5f",18.223);

则是输出5位小数 又如

1
printf("%5.1f",1.2345);

则是控制总位数为5,小数点后为1位,不够的在前面补空格

C/C++

本节任务

本次我们需要完成的任务是 完成两台主机通过中间主机的数据通信(网络层)

  • 增加基于IP地址的转发功能
  • 增加网络层封装

其实最主要的就是基于IP地址的转发功能,网络层的封装其实我们在初级功能中就已经做好了。

原理

首先,实验的思路是A通过中间主机B向C发送数据。那么B则作为一个路由器,B要监听两个网卡,一个网卡发来的数据通过另一个网卡发出去。 示意图如下: A————->B1===B2——————>C 从图上可以看出,B主机的两个网卡数据互通,A和B1则处于一个局域网内,B2和C处于另一个局域网内。 就比如这样,现在室友A在用有线上网,我的电脑B也在用有线上网,我们的有线处在同一局域网,我的电脑B同时散着一个无线网,我的手机C又连接到了这个无线上。 那么要实现A到C的数据传送,即模拟室友A要发送数据到我的手机C,那么流程则是这样的: 室友A在有线局域网发送数据到我的网卡B1,B1将数据通过网卡B2转发到无线局域网,通过无线局域网到达我的手机C。 A的发送要构建一个帧,目的MAC地址为B1,目的IP为C。B则要开启两个网卡,B1监听接收数据,B2网卡则要用ARP协议扫描所在无线局域网内的IP和MAC,B获取到了A发来的帧之后,解析它的IP地址和MAC地址,匹配刚才扫描得到的IP和MAC对应表,将源MAC换成B2网卡MAC,目的MAC换成C的MAC,IP不变,数据data不变。构建新帧之后发送出去。 好啦,思路大体就是这样。

实战

需要三个程序,一个是发送,一个路由,一个接收。所以一共三个程序要同时运行起来执行。 以上是我的大体思路,如有错误,还请指正。现已用代码实现完毕。 代码暂不公开,只提供部分重点代码解析:

一、发送端

其实发送端和初级功能的发送差不多 个人编写的交互流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
IP地址:121.250.216.221   MAC地址:3c970e4b56d6con:127

-------------------------------------------
IP地址:121.250.216.227 MAC地址:089e01b948f4con:128

-------------------------------------------
IP地址:121.250.216.228 MAC地址:10bf48705aeecon:129

获取MAC地址完毕,请输入你要发送对方的IP地址:
192.168.1.3
请输入你要发送的内容:
im cqc
要发送的内容:im cqc

具体代码不再解析,同上一篇初级功能。

二、路由端

首先要开启两个网卡,声明两个网卡对象和处理器

1
2
pcap_if_t  *d,*d2;					//选中的网络适配器
pcap_t *adhandle,*adhandle2; //捕捉实例,是pcap_open返回的对象,adhandle是用来发送数据,adhandle2是用来接收数据

一个用来接收一个用来发送,这里定义了adhandle是用来发送,adhandle2是用来接收数据。 那么打开适配器就在main方法中,提前打开两个网卡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
int num;
printf("请输入你要转发数据的网卡代号:\n");
//让用户选择选择哪个适配器进行转发
scanf_s("%d",&num);

//跳转到选中的适配器
for(d=alldevs, i=0; i< num-1 ; d=d->next, i++);

//运行到此处说明用户的输入是合法的,找到发送数据网卡
if((adhandle = pcap_open(d->name, //设备名称
65535, //存放数据包的内容长度
PCAP_OPENFLAG_PROMISCUOUS, //混杂模式
1000, //超时时间
NULL, //远程验证
errbuf //错误缓冲
)) == NULL){
//打开适配器失败,打印错误并释放适配器列表
fprintf(stderr,"\nUnable to open the adapter. %s is not supported by WinPcap\n", d->name);
// 释放设备列表
pcap_freealldevs(alldevs);
return -1;
}

int num2;
printf("请输入你要接收数据的网卡代号:");
//让用户选择用哪个网卡来收数据
scanf_s("%d",&num2);
//用户输入的数字超出合理范围


//跳转到选中的适配器
for(d2=alldevs, i=0; i< num2-1 ; d2=d2->next, i++);

//运行到此处说明用户的输入是合法的
if((adhandle2 = pcap_open(d2->name, //设备名称
65535, //存放数据包的内容长度
PCAP_OPENFLAG_PROMISCUOUS, //混杂模式
1000, //超时时间
NULL, //远程验证
errbuf //错误缓冲
)) == NULL){
//打开适配器失败,打印错误并释放适配器列表
fprintf(stderr,"\nUnable to open the adapter. %s is not supported by WinPcap\n", d2->name);

接下来用用于发送的handle处理器来扫描它的局域网IP,获得局域网内的MAC地址,记录在一个表中,存放IP和MAC的对应关系。 这个表可以用结构体数组来保存,比如可以这样:

1
2
3
4
struct ip_mac_list{
IpAddress ip;
unsigned char mac[6];
};
1
ip_mac_list  list[256];                       //存储IP和MAC地址的对应表

那么以上便是准备工作,我们完成了两个网卡的打开,发送网卡扫描获取局域网MAC,接下来便是最重要的监听加转发。 那么这个怎办?那就开一个新线程。 让我们声明一个新的路由线程。

1
DWORD WINAPI RouteThread(LPVOID lpParameter);

那么线程要接收进来什么参数呢? 首先必须要的是两个网卡处理器,在main方法中已经做好初始化的adhandle和adhandle2,另外还有alldevs,可以持有这个指针来释放设备列表,出现错误时释放资源并退出。 初级功能中声明过了 struct sparam sp; struct gparam gp; 这两个就是发送ARP线程和接收ARP线程中的两个参数,那么仿照这个功能,我们定义一个新的结构体

1
2
3
4
5
struct rparam{
pcap_t *adhandle_rec;
pcap_t *adhandle_send;
pcap_if_t * alldevs; //所有网络适配器
};

在main方法中把它来初始化赋值

1
2
3
rp.adhandle_send = adhandle;
rp.adhandle_rec = adhandle2;
rp.alldevs = alldevs;

当做参数传入这个线程

1
2
routethread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) RouteThread, &rp,
0, NULL);

其中第四个参数就是传递了这个结构体进去。注意这个语句最好不要直接放在main方法中直接调用,可以在全部获取完MAC地址之后再开启这个线程。 那么接下来就说一下这个线程都干了些什么,只简略说一下核心部分。 首先开启了这个线程之后会一直都在执行,那么就可以加入

1
while((res = pcap_next_ex(adhandle2,&header,&pkt_data))>=0)

这样的while判断语句来一直监听数据包的接收,然后解析数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
ethernet =  (EthernetHeader *)(pkt_data);
for(int i=0;i<6;i++){
sou_mac[i] = ethernet->SourMAC[i];
}
for(int i=0;i<6;i++){
des_mac[i] = ethernet->DestMAC[i];
}
// 获得IP数据包头部的位置
ip = (IpHeader *) (pkt_data +14); //14为以太网帧头部长度
//获得TCP头部的位置
ip_len = (ip->Version_HLen & 0xf) *4;
tcp = (TcpHeader *)((u_char *)ip+ip_len);
data = (char *)((u_char *)tcp+20);
printf("data:%s\n",data);
printf("ip:");
printf("%d.%d.%d.%d -> %d.%d.%d.%d\n",
ip->SourceAddr.byte1,
ip->SourceAddr.byte2,
ip->SourceAddr.byte3,
ip->SourceAddr.byte4,
ip->DestinationAddr.byte1,
ip->DestinationAddr.byte2,
ip->DestinationAddr.byte3,
ip->DestinationAddr.byte4);
printf("sou_mac:%02x-%02x-%02x-%02x-%02x-%02x\n", sou_mac[0], sou_mac[1], sou_mac[2],
sou_mac[3], sou_mac[4], sou_mac[5]);
printf("des_mac:%02x-%02x-%02x-%02x-%02x-%02x\n", des_mac[0], des_mac[1], des_mac[2],
des_mac[3], des_mac[4], des_mac[5]);

然后接下来每接收到一个数据,就进行构建新的帧转发出去,目的MAC先匹配list表,如果list没有找到,那么我让他指定了一个mac,比如广播MAC。源MAC地址则赋值网卡的MAC地址。 注意,传统以太网中数据长度为45-1500,那么我在构建前把解析出的data作了下判断长度再构建,因为我已经把sendbuffer声明为一个固定长度了,为了防止越界,我先进行一个长度判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//以下开始构建帧发送
//首先判断data最大值小于1500
if(strlen(data)<1500){
//目的MAC
BYTE send_destmac[6];
bool findMac = false;
for(int c = 0;c<con;c++){
if(ip->DestinationAddr.byte1 == list[c].ip.byte1&&
ip->DestinationAddr.byte2 == list[c].ip.byte2&&
ip->DestinationAddr.byte3 == list[c].ip.byte3&&
ip->DestinationAddr.byte4 == list[c].ip.byte4)
{
printf("Find its MAC!\n");
findMac = true;
send_destmac[0] = list[c].mac[0];
send_destmac[1] = list[c].mac[1];
send_destmac[2] = list[c].mac[2];
send_destmac[3] = list[c].mac[3];
send_destmac[4] = list[c].mac[4];
send_destmac[5] = list[c].mac[5];
}
}
if(!findMac){
send_destmac[0] = 0xff;
send_destmac[1] = 0xff;
send_destmac[2] = 0xff;
send_destmac[3] = 0xff;
send_destmac[4] = 0xff;
send_destmac[5] = 0xff;
}
printf("destmac:%02x-%02x-%02x-%02x-%02x-%02x\n",
send_destmac[0],send_destmac[1],send_destmac[2],
send_destmac[3],send_destmac[4],send_destmac[5]
);
memcpy(send_ethernet.DestMAC, send_destmac, 6);
//源MAC地址
BYTE send_hostmac[6];
//源MAC地址
send_hostmac[0] = local_mac[0]; //赋值本地MAC地址
send_hostmac[1] = local_mac[1];
send_hostmac[2] = local_mac[2];
send_hostmac[3] = local_mac[3];
send_hostmac[4] = local_mac[4];
send_hostmac[5] = local_mac[5];
//赋值源MAC地址
memcpy(send_ethernet.SourMAC, send_hostmac, 6);
send_ethernet.EthType = htons(0x0800);
//赋值SendBuffer
memcpy(&SendBuffer, &send_ethernet, sizeof(struct EthernetHeader));

以上只是赋值了帧头,至于IP头,TCP头,数据的赋值就参照初级功能的来赋值吧,不要忘了校验和的检验。好,大体上就是这样,接受来数据包并转发出去的原理就是这样。

三、接收

不用多改,就是初级功能中的接收,在此写一写小小的优化措施,防止接收到过多的数据帧而造成不断乱蹦,导致你看不到接收的东西。 在打印的时候加一个过滤就好了。部分代码如下: 在main方法中提示用户输入要接收的IP地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
printf("请输入要接收的IP地址,输入0.0.0.0代表全部接收,请输入\n");
bool receiveAll = false;
u_int ip1,ip2,ip3,ip4;
bool legal = false;
while(!legal){
scanf_s("%d.%d.%d.%d",&ip1,&ip2,&ip3,&ip4);
if(ip1==0&&ip2==0&&ip3==0&&ip4==0){
receiveAll = true;
legal = true;
break;
}
if(ip1<0||ip1>255||ip2<0||ip2>255||ip3<0||ip3>255||ip4<1||ip4>254){
legal = false;
printf("对不起,IP输入不合法,请重新输入:\n");
}else{
legal = true;
}
}

打印时的判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if(receiveAll||(ip->SourceAddr.byte1==ip1&&
ip->SourceAddr.byte2==ip2&&
ip->SourceAddr.byte3==ip3&&
ip->SourceAddr.byte4==ip4)){
printf("%d.%d.%d.%d.%d -> %d.%d.%d.%d.%d\n",
ip->SourceAddr.byte1,
ip->SourceAddr.byte2,
ip->SourceAddr.byte3,
ip->SourceAddr.byte4,
sport,
ip->DestinationAddr.byte1,
ip->DestinationAddr.byte2,
ip->DestinationAddr.byte3,
ip->DestinationAddr.byte4,
dport);
printf("sou_mac:%02x-%02x-%02x-%02x-%02x-%02x\n", sou_mac[0], sou_mac[1], sou_mac[2],
sou_mac[3], sou_mac[4], sou_mac[5]);
printf("des_mac:%02x-%02x-%02x-%02x-%02x-%02x\n", des_mac[0], des_mac[1], des_mac[2],
des_mac[3], des_mac[4], des_mac[5]);
printf("%s\n",data);
printf("-----------------------------------------------------\n");
}

好,代码就先放送这么多,具体的实现只要有了思路我相信肯定不难,如有问题,欢迎与我交流。

C/C++

学习文档

这里我们直接进入正题吧,关于Winpcap的基础知识讲解这里就不再赘述了。在这里给大家提供一些学习网址 Winpcap网络编程及通信教程Winpcap中文技术文档

学习内容

  • 获取设备列表
  • 获取已安装设备的高级信息
  • 打开适配器并捕获数据包
  • 不用回调方法捕获数据包
  • 过滤数据包
  • 分析数据包
  • 处理脱机堆文件
  • 发送数据包
  • 收集并统计网络流量

这些内容,在上述两个链接中均已经有了比较详细的讲解,希望对大家有帮助。

两台主机通信实战

完成两台主机之间的数据通信(数据链路层)

  • 仿真ARP协议获得网段内主机的MAC表
  • 使用帧完成两台主机的通信(Hello! I’m …)

首先我们要理解ARP是干嘛的,ARP主要作用就是通过IP地址来获取MAC地址。那么怎样获取呢?本机向局域网内主机发送ARP包,ARP包内包含了目的IP,源IP,目的MAC,源MAC,其中目的MAC地址为广播地址,FF-FF-FF-FF-FF-FF,即向局域网内所有主机发送一个ARP请求,那么其他主机收到这个请求之后则会向请求来源返回一个数据包。在这个返回的数据包中包含了自身的MAC地址。那么本机收到这些返回的数据包进行解析之后便会得到局域网内所有主机的MAC地址了.. 编程开始: 新建一个C++项目,配好环境,引入Winpcap相关的库,这些不再赘述。 头文件引入

1
2
3
4
5
#define HAVE_REMOTE
#define WPCAP
#include <stdio.h>
#include <stdlib.h>
#include <pcap.h>

在main函数中首先声明一系列变量如下

1
2
3
char *ip_addr;                                    //IP地址
char *ip_netmask; //子网掩码
unsigned char *ip_mac; //本机MAC地址

为这三个变量分配地址空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ip_addr = (char *) malloc(sizeof(char) * 16); //申请内存存放IP地址
if (ip_addr == NULL)
{
printf("申请内存存放IP地址失败!\n");
return -1;
}
ip_netmask = (char *) malloc(sizeof(char) * 16); //申请内存存放NETMASK地址
if (ip_netmask == NULL)
{
printf("申请内存存放NETMASK地址失败!\n");
return -1;
}
ip_mac = (unsigned char *) malloc(sizeof(unsigned char) * 6); //申请内存存放MAC地址
if (ip_mac == NULL)
{
printf("申请内存存放MAC地址失败!\n");
return -1;
}

接下来就是烂大街的程序,获取适配器列表并选中相应的适配器,注释已经在代码中了,如果还有不明白的请参照前几次的讲解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
//获取本地适配器列表
if(pcap_findalldevs_ex(PCAP_SRC_IF_STRING,NULL,&alldevs,errbuf) == -1){
//结果为-1代表出现获取适配器列表失败
fprintf(stderr,"Error in pcap_findalldevs_ex:\n",errbuf);
//exit(0)代表正常退出,exit(other)为非正常退出,这个值会传给操作系统
exit(1);
}


for(d = alldevs;d !=NULL;d = d->next){
printf("-----------------------------------------------------------------\nnumber:%d\nname:%s\n",++i,d->name);
if(d->description){
//打印适配器的描述信息
printf("description:%s\n",d->description);
}else{
//适配器不存在描述信息
printf("description:%s","no description\n");
}
//打印本地环回地址
printf("\tLoopback: %s\n",(d->flags & PCAP_IF_LOOPBACK)?"yes":"no");
/**
pcap_addr * next 指向下一个地址的指针
sockaddr * addr IP地址
sockaddr * netmask 子网掩码
sockaddr * broadaddr 广播地址
sockaddr * dstaddr 目的地址
*/
pcap_addr_t *a; //网络适配器的地址用来存储变量
for(a = d->addresses;a;a = a->next){
//sa_family代表了地址的类型,是IPV4地址类型还是IPV6地址类型
switch (a->addr->sa_family)
{
case AF_INET: //代表IPV4类型地址
printf("Address Family Name:AF_INET\n");
if(a->addr){
//->的优先级等同于括号,高于强制类型转换,因为addr为sockaddr类型,对其进行操作须转换为sockaddr_in类型
printf("Address:%s\n",iptos(((struct sockaddr_in *)a->addr)->sin_addr.s_addr));
}
if (a->netmask){
printf("\tNetmask: %s\n",iptos(((struct sockaddr_in *)a->netmask)->sin_addr.s_addr));
}
if (a->broadaddr){
printf("\tBroadcast Address: %s\n",iptos(((struct sockaddr_in *)a->broadaddr)->sin_addr.s_addr));
}
if (a->dstaddr){
printf("\tDestination Address: %s\n",iptos(((struct sockaddr_in *)a->dstaddr)->sin_addr.s_addr));
}
break;
case AF_INET6: //代表IPV6类型地址
printf("Address Family Name:AF_INET6\n");
printf("this is an IPV6 address\n");
break;
default:
break;
}
}
}
//i为0代表上述循环未进入,即没有找到适配器,可能的原因为Winpcap没有安装导致未扫描到
if(i == 0){
printf("interface not found,please check winpcap installation");
}

int num;
printf("Enter the interface number(1-%d):",i);
//让用户选择选择哪个适配器进行抓包
scanf_s("%d",&num);
printf("\n");

//用户输入的数字超出合理范围
if(num<1||num>i){
printf("number out of range\n");
pcap_freealldevs(alldevs);
return -1;
}
//跳转到选中的适配器
for(d=alldevs, i=0; i< num-1 ; d=d->next, i++);

//运行到此处说明用户的输入是合法的
if((adhandle = pcap_open(d->name, //设备名称
65535, //存放数据包的内容长度
PCAP_OPENFLAG_PROMISCUOUS, //混杂模式
1000, //超时时间
NULL, //远程验证
errbuf //错误缓冲
)) == NULL){
//打开适配器失败,打印错误并释放适配器列表
fprintf(stderr,"\nUnable to open the adapter. %s is not supported by WinPcap\n", d->name);
// 释放设备列表
pcap_freealldevs(alldevs);
return -1;
}

上述代码中需要另外声明的有:

1
2
3
4
5
pcap_if_t  * alldevs;       //所有网络适配器
pcap_if_t *d; //选中的网络适配器
char errbuf[PCAP_ERRBUF_SIZE]; //错误缓冲区,大小为256
pcap_t *adhandle; //捕捉实例,是pcap_open返回的对象
int i = 0; //适配器计数变量
1
char *iptos(u_long in);       //u_long即为 unsigned long
1
2
3
4
5
6
7
8
9
10
11
12
13
/* 将数字类型的IP地址转换成字符串类型的 */
#define IPTOSBUFFERS 12
char *iptos(u_long in)
{
static char output[IPTOSBUFFERS][3*4+3+1];
static short which;
u_char *p;

p = (u_char *)&in;
which = (which + 1 == IPTOSBUFFERS ? 0 : which + 1);
sprintf_s(output[which], "%d.%d.%d.%d", p[0], p[1], p[2], p[3]);
return output[which];
}

到此程序应该会编译通过,可以试着编译一下运行。 GO ON… 接下来我们首先要用ifget方法获取自身的IP和子网掩码 函数声明:

1
void ifget(pcap_if_t *d, char *ip_addr, char *ip_netmask);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//获取IP和子网掩码赋值为ip_addr和ip_netmask
void ifget(pcap_if_t *d, char *ip_addr, char *ip_netmask) {
pcap_addr_t *a;
//遍历所有的地址,a代表一个pcap_addr
for (a = d->addresses; a; a = a->next) {
switch (a->addr->sa_family) {
case AF_INET: //sa_family :是2字节的地址家族,一般都是“AF_xxx”的形式。通常用的都是AF_INET。代表IPV4
if (a->addr) {
char *ipstr;
//将地址转化为字符串
ipstr = iptos(((struct sockaddr_in *) a->addr)->sin_addr.s_addr); //*ip_addr
printf("ipstr:%s\n",ipstr);
memcpy(ip_addr, ipstr, 16);
}
if (a->netmask) {
char *netmaskstr;
netmaskstr = iptos(((struct sockaddr_in *) a->netmask)->sin_addr.s_addr);
printf("netmask:%s\n",netmaskstr);
memcpy(ip_netmask, netmaskstr, 16);
}
case AF_INET6:
break;
}
}
}

main函数继续写,如下调用,之前声明的ip_addr和ip_netmask就已经被赋值了

1
ifget(d, ip_addr, ip_netmask); //获取所选网卡的基本信息--掩码--IP地址

到现在我们已经获取到了本机的IP和子网掩码,下一步发送一个ARP请求来获取自身的MAC地址 这个ARP请求的源IP地址就随便指定了,就相当于你构造了一个外来的ARP请求,本机捕获到了请求,然后发送回应给对方的数据包也被本机捕获到了并解析出来了。解析了自己发出去的数据包而已。 那么我们就声明一个函数并实现:

1
int GetSelfMac(pcap_t *adhandle, const char *ip_addr, unsigned char *ip_mac);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 获取自己主机的MAC地址
int GetSelfMac(pcap_t *adhandle, const char *ip_addr, unsigned char *ip_mac) {
unsigned char sendbuf[42]; //arp包结构大小
int i = -1;
int res;
EthernetHeader eh; //以太网帧头
Arpheader ah; //ARP帧头
struct pcap_pkthdr * pkt_header;
const u_char * pkt_data;
//将已开辟内存空间 eh.dest_mac_add 的首 6个字节的值设为值 0xff。
memset(eh.DestMAC, 0xff, 6); //目的地址为全为广播地址
memset(eh.SourMAC, 0x0f, 6);
memset(ah.DestMacAdd, 0x0f, 6);
memset(ah.SourceMacAdd, 0x00, 6);
//htons将一个无符号短整型的主机数值转换为网络字节顺序
eh.EthType = htons(ETH_ARP);
ah.HardwareType= htons(ARP_HARDWARE);
ah.ProtocolType = htons(ETH_IP);
ah.HardwareAddLen = 6;
ah.ProtocolAddLen = 4;
ah.SourceIpAdd = inet_addr("100.100.100.100"); //随便设的请求方ip
ah.OperationField = htons(ARP_REQUEST);
ah.DestIpAdd = inet_addr(ip_addr);
memset(sendbuf, 0, sizeof(sendbuf));
memcpy(sendbuf, &eh, sizeof(eh));
memcpy(sendbuf + sizeof(eh), &ah, sizeof(ah));
printf("%s",sendbuf);
if (pcap_sendpacket(adhandle, sendbuf, 42) == 0) {
printf("\nPacketSend succeed\n");
} else {
printf("PacketSendPacket in getmine Error: %d\n", GetLastError());
return 0;
}
//从interface或离线记录文件获取一个报文
//pcap_next_ex(pcap_t* p,struct pcap_pkthdr** pkt_header,const u_char** pkt_data)
while ((res = pcap_next_ex(adhandle, &pkt_header, &pkt_data)) >= 0) {
if (*(unsigned short *) (pkt_data + 12) == htons(ETH_ARP)
&& *(unsigned short*) (pkt_data + 20) == htons(ARP_REPLY)
&& *(unsigned long*) (pkt_data + 38)
== inet_addr("100.100.100.100")) {
for (i = 0; i < 6; i++) {
ip_mac[i] = *(unsigned char *) (pkt_data + 22 + i);
}
printf("获取自己主机的MAC地址成功!\n");
break;
}
}
if (i == 6) {
return 1;
} else {
return 0;
}
}

其中我们需要定义一下常量如下

1
2
3
4
5
6
#define ETH_ARP         0x0806  //以太网帧类型表示后面数据的类型,对于ARP请求或应答来说,该字段的值为x0806
#define ARP_HARDWARE 1 //硬件类型字段值为表示以太网地址
#define ETH_IP 0x0800 //协议类型字段表示要映射的协议地址类型值为x0800表示IP地址
#define ARP_REQUEST 1 //ARP请求
#define ARP_REPLY 2 //ARP应答
#define HOSTNUM 255 //主机数量

另外发送ARP请求少不了帧头和ARP头的结构,我们需要声明出来,另外我们构建发送包需要再声明两个结构体sparam和gparam

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//帧头部结构体,共14字节
struct EthernetHeader
{
u_char DestMAC[6]; //目的MAC地址 6字节
u_char SourMAC[6]; //源MAC地址 6字节
u_short EthType; //上一层协议类型,如0x0800代表上一层是IP协议,0x0806为arp 2字节
};

//28字节ARP帧结构
struct Arpheader {
unsigned short HardwareType; //硬件类型
unsigned short ProtocolType; //协议类型
unsigned char HardwareAddLen; //硬件地址长度
unsigned char ProtocolAddLen; //协议地址长度
unsigned short OperationField; //操作字段
unsigned char SourceMacAdd[6]; //源mac地址
unsigned long SourceIpAdd; //源ip地址
unsigned char DestMacAdd[6]; //目的mac地址
unsigned long DestIpAdd; //目的ip地址
};

//arp包结构
struct ArpPacket {
EthernetHeader ed;
Arpheader ah;
};

struct sparam {
pcap_t *adhandle;
char *ip;
unsigned char *mac;
char *netmask;
};
struct gparam {
pcap_t *adhandle;
};

struct sparam sp;
struct gparam gp;

到现在代码也是完整可以运行的,如果有问题请检查上述代码完整性和位置。 可能出现的BUG: 只显示ARP发送成功,没有接受到并解析打印。可能的原因是帧构造有问题,字节没有对齐,有偏差,像#define一样 写入如下代码:

1
#pragma pack(1)  //按一个字节内存对齐

GO ON.. 获取到了自身的MAC地址之后,就可以在本机上构建ARP广播请求,向局域网内的所有主机发送ARP请求,得到回应之后解析回应的数据包并进行解析,得到对方的MAC地址。在这里我们需要开启两个线程,一个用来发送一个用来接收。好,我们继续.. 先声明两个线程

1
2
HANDLE sendthread;      //发送ARP包线程
HANDLE recvthread; //接受ARP包线程

在main方法中继续写,对sp和gp两个ARP请求所需要的结构体进行赋值。赋值什么?就是你之前用ifget获取来的IP地址和子网掩码以及用getSelfMac获取来的MAC地址。

1
2
3
4
5
sp.adhandle = adhandle;
sp.ip = ip_addr;
sp.mac = ip_mac;
sp.netmask = ip_netmask;
gp.adhandle = adhandle;

接下来直接创建两个线程,一个是发送一个接受,分别调用两个方法。

1
2
3
4
5
sendthread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) SendArpPacket,
&sp, 0, NULL);
recvthread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) GetLivePC, &gp,
0, NULL);
printf("\nlistening on 网卡%d ...\n", i);

那么发送数据包的方法和接收解析数据包的方法怎样实现呢? 发送数据包,发送数据包先对结构体数据进行赋值,就像getSelfMac方法一样,然后声明了一个buffer用来存储每一个字节内容。 利用memset方法对buffer进行赋值。再利用一个for循环对255个主机进行发送,指定他们的IP地址。另外定义了一个flag,当发送成功之后将flag设置为1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/* 向局域网内所有可能的IP地址发送ARP请求包线程 */
DWORD WINAPI SendArpPacket(LPVOID lpParameter) //(pcap_t *adhandle,char *ip,unsigned char *mac,char *netmask)
{
sparam *spara = (sparam *) lpParameter;
pcap_t *adhandle = spara->adhandle;
char *ip = spara->ip;
unsigned char *mac = spara->mac;
char *netmask = spara->netmask;
printf("ip_mac:%02x-%02x-%02x-%02x-%02x-%02x\n", mac[0], mac[1], mac[2],
mac[3], mac[4], mac[5]);
printf("自身的IP地址为:%s\n", ip);
printf("地址掩码NETMASK为:%s\n", netmask);
printf("\n");
unsigned char sendbuf[42]; //arp包结构大小
EthernetHeader eh;
Arpheader ah;
//赋值MAC地址
memset(eh.DestMAC, 0xff, 6); //目的地址为全为广播地址
memcpy(eh.SourMAC, mac, 6);
memcpy(ah.SourceMacAdd, mac, 6);
memset(ah.DestMacAdd, 0x00, 6);
eh.EthType = htons(ETH_ARP);
ah.HardwareType = htons(ARP_HARDWARE);
ah.ProtocolType = htons(ETH_IP);
ah.HardwareAddLen = 6;
ah.ProtocolAddLen = 4;
ah.SourceIpAdd = inet_addr(ip); //请求方的IP地址为自身的IP地址
ah.OperationField = htons(ARP_REQUEST);
//向局域网内广播发送arp包
unsigned long myip = inet_addr(ip);
unsigned long mynetmask = inet_addr(netmask);
unsigned long hisip = htonl((myip & mynetmask));
//向255个主机发送
for (int i = 0; i < HOSTNUM; i++) {
ah.DestIpAdd = htonl(hisip + i);
//构造一个ARP请求
memset(sendbuf, 0, sizeof(sendbuf));
memcpy(sendbuf, &eh, sizeof(eh));
memcpy(sendbuf + sizeof(eh), &ah, sizeof(ah));
//如果发送成功
if (pcap_sendpacket(adhandle, sendbuf, 42) == 0) {
//printf("\nPacketSend succeed\n");
} else {
printf("PacketSendPacket in getmine Error: %d\n", GetLastError());
}
Sleep(50);
}
Sleep(1000);
flag = TRUE;
return 0;
}

注: 此函数和flag变量在前面别忘了声明一下… 然后是接收数据包并打印MAC地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/* 分析截留的数据包获取活动的主机IP地址 */
DWORD WINAPI GetLivePC(LPVOID lpParameter) //(pcap_t *adhandle)
{
gparam *gpara = (gparam *) lpParameter;
pcap_t *adhandle = gpara->adhandle;
int res;
unsigned char Mac[6];
struct pcap_pkthdr * pkt_header;
const u_char * pkt_data;
while (true) {
if (flag) {
printf("获取MAC地址完毕,请输入你要发送对方的IP地址:\n");
break;
}
if ((res = pcap_next_ex(adhandle, &pkt_header, &pkt_data)) >= 0) {
if (*(unsigned short *) (pkt_data + 12) == htons(ETH_ARP)) {
ArpPacket *recv = (ArpPacket *) pkt_data;
if (*(unsigned short *) (pkt_data + 20) == htons(ARP_REPLY)) {
printf("-------------------------------------------\n");
printf("IP地址:%d.%d.%d.%d MAC地址:",
recv->ah.SourceIpAdd & 255,
recv->ah.SourceIpAdd >> 8 & 255,
recv->ah.SourceIpAdd >> 16 & 255,
recv->ah.SourceIpAdd >> 24 & 255);
for (int i = 0; i < 6; i++) {
Mac[i] = *(unsigned char *) (pkt_data + 22 + i);
printf("%02x", Mac[i]);
}
printf("\n");
}
}
}
Sleep(10);
}
return 0;
}

以上暂告一段落,通过整合以上代码,我们可以得到如下运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
--------------------------------------------------
number:1
name:rpcap://\Device\NPF_{5AC72F8D-019C-4003-B51B-
description:Network adapter 'Microsoft' on local h
Loopback: no
Address Family Name:AF_INET6
this is an IPV6 address
Address Family Name:AF_INET6
this is an IPV6 address
--------------------------------------------------
number:2
name:rpcap://\Device\NPF_{C17EB3F6-1E86-40E5-8790-
description:Network adapter 'Microsoft' on local h
Loopback: no
Address Family Name:AF_INET6
this is an IPV6 address
Address Family Name:AF_INET
Address:192.168.95.1
Netmask: 255.255.255.0
Broadcast Address: 255.255.255.255
--------------------------------------------------
number:3
name:rpcap://\Device\NPF_{33E23A2F-F791-409B-8452-
description:Network adapter 'Qualcomm Atheros Ar81
oller' on local host
Loopback: no
Address Family Name:AF_INET6
this is an IPV6 address
Address Family Name:AF_INET6
this is an IPV6 address
Address Family Name:AF_INET6
this is an IPV6 address
Address Family Name:AF_INET
Address:121.250.216.237
Netmask: 255.255.255.0
Broadcast Address: 255.255.255.255
--------------------------------------------------
number:4
name:rpcap://\Device\NPF_{DCCF036F-A9A8-4225-B980-
description:Network adapter 'Microsoft' on local h
Loopback: no
Address Family Name:AF_INET6
this is an IPV6 address
Address Family Name:AF_INET6
this is an IPV6 address
--------------------------------------------------
number:5
name:rpcap://\Device\NPF_{D62A0060-F424-46FC-83A5-
description:Network adapter 'Microsoft' on local h
Loopback: no
Address Family Name:AF_INET6
this is an IPV6 address
Address Family Name:AF_INET
Address:192.168.191.1
Netmask: 255.255.255.0
Broadcast Address: 255.255.255.255
--------------------------------------------------
number:6
name:rpcap://\Device\NPF_{B5224A53-8450-4537-AB3B-
description:Network adapter 'Microsoft' on local h
Loopback: no
Address Family Name:AF_INET6
this is an IPV6 address
Address Family Name:AF_INET
Address:192.168.191.2
Netmask: 255.255.255.0
Broadcast Address: 255.255.255.255
Enter the interface number(1-6):3

ipstr:121.250.216.237
netmask:255.255.255.0

PacketSend succeed
获取自己主机的MAC地址成功!

listening on 网卡2 ...
ip_mac:dc-0e-a1-ec-53-c3
自身的IP地址为:121.250.216.237
地址掩码NETMASK为:255.255.255.0

请按任意键继续. . . ------------------------------
IP地址:121.250.216.1 MAC地址:000fe28e6100
-------------------------------------------
IP地址:121.250.216.3 MAC地址:089e012d20d5
-------------------------------------------
IP地址:121.250.216.5 MAC地址:5404a6af5f2d
-------------------------------------------
IP地址:121.250.216.6 MAC地址:28d244248d81
-------------------------------------------
IP地址:121.250.216.7 MAC地址:80fa5b0283f3
-------------------------------------------
IP地址:121.250.216.8 MAC地址:14dae9005b9e
-------------------------------------------
IP地址:121.250.216.9 MAC地址:b82a72bf8bce
-------------------------------------------
IP地址:121.250.216.12 MAC地址:84c9b2fefeed
-------------------------------------------
IP地址:121.250.216.15 MAC地址:28d2440b4b1b
-------------------------------------------
IP地址:121.250.216.16 MAC地址:bcee7b969beb
-------------------------------------------
........此处省略一万字....

接下来我们让用户输入要发送的IP地址和要发送的数据

1
2
3
4
5
6
u_int ip1,ip2,ip3,ip4;
scanf_s("%d.%d.%d.%d",&ip1,&ip2,&ip3,&ip4);
printf("请输入你要发送的内容:\n");
getchar();
gets_s(TcpData);
printf("要发送的内容:%s\n",TcpData);

声明一下TcpData

1
char TcpData[20];   //发送内容

接下来就是重头戏了,需要声明各种结构体,我们发送的是TCP数据,这样,TCP的TcpData 就作为真正的内容,然后在前面加上TCP头,IP头,帧头,还有校验和要正确。 最后构成一个完整的帧,那么另外声明的结构体如下,前面代码声明过的帧头部结构体就去掉了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//IP地址格式
struct IpAddress
{
u_char byte1;
u_char byte2;
u_char byte3;
u_char byte4;
};

//帧头部结构体,共14字节
struct EthernetHeader
{
u_char DestMAC[6]; //目的MAC地址 6字节
u_char SourMAC[6]; //源MAC地址 6字节
u_short EthType; //上一层协议类型,如0x0800代表上一层是IP协议,0x0806为arp 2字节
};

//IP头部结构体,共20字节
struct IpHeader
{
unsigned char Version_HLen; //版本信息4位 ,头长度4位 1字节
unsigned char TOS; //服务类型 1字节
short Length; //数据包长度 2字节
short Ident; //数据包标识 2字节
short Flags_Offset; //标志3位,片偏移13位 2字节
unsigned char TTL; //存活时间 1字节
unsigned char Protocol; //协议类型 1字节
short Checksum; //首部校验和 2字节
IpAddress SourceAddr; //源IP地址 4字节
IpAddress DestinationAddr; //目的IP地址 4字节
};

//TCP头部结构体,共20字节
struct TcpHeader
{
unsigned short SrcPort; //源端口号 2字节
unsigned short DstPort; //目的端口号 2字节
unsigned int SequenceNum; //序号 4字节
unsigned int Acknowledgment; //确认号 4字节
unsigned char HdrLen; //首部长度4位,保留位6位 共10位
unsigned char Flags; //标志位6位
unsigned short AdvertisedWindow; //窗口大小16位 2字节
unsigned short Checksum; //校验和16位 2字节
unsigned short UrgPtr; //紧急指针16位 2字节
};

//TCP伪首部结构体 12字节
struct PsdTcpHeader
{
IpAddress SourceAddr; //源IP地址 4字节
IpAddress DestinationAddr; //目的IP地址 4字节
char Zero; //填充位 1字节
char Protcol; //协议号 1字节
unsigned short TcpLen; //TCP包长度 2字节
};

继续main函数中对各种结构体的数据进行初始化赋值,并计算校验和。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
//结构体初始化为0序列
memset(&ethernet, 0, sizeof(ethernet));
BYTE destmac[8];
//目的MAC地址,此处没有对帧的MAC地址进行赋值,因为网卡设置的混杂模式,可以接受经过该网卡的所有帧。当然最好的方法是赋值为ARP刚才获取到的MAC地址,当然不赋值也可以捕捉到并解析,在此处仅做下说明。
destmac[0] = 0x00;
destmac[1] = 0x11;
destmac[2] = 0x22;
destmac[3] = 0x33;
destmac[4] = 0x44;
destmac[5] = 0x55;
//赋值目的MAC地址
memcpy(ethernet.DestMAC, destmac, 6);
BYTE hostmac[8];
//源MAC地址
hostmac[0] = 0x00;
hostmac[1] = 0x1a;
hostmac[2] = 0x4d;
hostmac[3] = 0x70;
hostmac[4] = 0xa3;
hostmac[5] = 0x89;
//赋值源MAC地址
memcpy(ethernet.SourMAC, hostmac, 6);
//上层协议类型,0x0800代表IP协议
ethernet.EthType = htons(0x0800);
//赋值SendBuffer
memcpy(&SendBuffer, ðernet, sizeof(struct EthernetHeader));
//赋值IP头部信息
ip.Version_HLen = 0x45;
ip.TOS = 0;
ip.Length = htons(sizeof(struct IpHeader) + sizeof(struct TcpHeader) + strlen(TcpData));
ip.Ident = htons(1);
ip.Flags_Offset = 0;
ip.TTL = 128;
ip.Protocol = 6;
ip.Checksum = 0;
//源IP地址
ip.SourceAddr.byte1 = 127;
ip.SourceAddr.byte2 = 0;
ip.SourceAddr.byte3 = 0;
ip.SourceAddr.byte4 = 1;
//目的IP地址
ip.DestinationAddr.byte1 = ip1;
ip.DestinationAddr.byte2 = ip2;
ip.DestinationAddr.byte3 = ip3;
ip.DestinationAddr.byte4 = ip4;
//赋值SendBuffer
memcpy(&SendBuffer[sizeof(struct EthernetHeader)], &ip, 20);
//赋值TCP头部内容
tcp.DstPort = htons(102);
tcp.SrcPort = htons(1000);
tcp.SequenceNum = htonl(11);
tcp.Acknowledgment = 0;
tcp.HdrLen = 0x50;
tcp.Flags = 0x18;
tcp.AdvertisedWindow = htons(512);
tcp.UrgPtr = 0;
tcp.Checksum = 0;
//赋值SendBuffer
memcpy(&SendBuffer[sizeof(struct EthernetHeader) + 20], &tcp, 20);
//赋值伪首部
ptcp.SourceAddr = ip.SourceAddr;
ptcp.DestinationAddr = ip.DestinationAddr;
ptcp.Zero = 0;
ptcp.Protcol = 6;
ptcp.TcpLen = htons(sizeof(struct TcpHeader) + strlen(TcpData));
//声明临时存储变量,用来计算校验和
char TempBuffer[65535];
memcpy(TempBuffer, &ptcp, sizeof(struct PsdTcpHeader));
memcpy(TempBuffer + sizeof(struct PsdTcpHeader), &tcp, sizeof(struct TcpHeader));
memcpy(TempBuffer + sizeof(struct PsdTcpHeader) + sizeof(struct TcpHeader), TcpData, strlen(TcpData));
//计算TCP的校验和
tcp.Checksum = checksum((USHORT*)(TempBuffer), sizeof(struct PsdTcpHeader) + sizeof(struct TcpHeader) + strlen(TcpData));
//重新把SendBuffer赋值,因为此时校验和已经改变,赋值新的
memcpy(SendBuffer + sizeof(struct EthernetHeader) + sizeof(struct IpHeader), &tcp, sizeof(struct TcpHeader));
memcpy(SendBuffer + sizeof(struct EthernetHeader) + sizeof(struct IpHeader) + sizeof(struct TcpHeader), TcpData, strlen(TcpData));
//初始化TempBuffer为0序列,存储变量来计算IP校验和
memset(TempBuffer, 0, sizeof(TempBuffer));
memcpy(TempBuffer, &ip, sizeof(struct IpHeader));
//计算IP校验和
ip.Checksum = checksum((USHORT*)(TempBuffer), sizeof(struct IpHeader));
//重新把SendBuffer赋值,IP校验和已经改变
memcpy(SendBuffer + sizeof(struct EthernetHeader), &ip, sizeof(struct IpHeader));
//发送序列的长度
int size = sizeof(struct EthernetHeader) + sizeof(struct IpHeader) + sizeof(struct TcpHeader) + strlen(TcpData);
int result = pcap_sendpacket(adhandle, SendBuffer,size);
if (result != 0)
{
printf("Send Error!\n");
}
else
{
printf("Send TCP Packet.\n");
printf("Dstination Port:%d\n", ntohs(tcp.DstPort));
printf("Source Port:%d\n", ntohs(tcp.SrcPort));
printf("Sequence:%d\n", ntohl(tcp.SequenceNum));
printf("Acknowledgment:%d\n", ntohl(tcp.Acknowledgment));
printf("Header Length:%d*4\n", tcp.HdrLen >> 4);
printf("Flags:0x%0x\n", tcp.Flags);
printf("AdvertiseWindow:%d\n", ntohs(tcp.AdvertisedWindow));
printf("UrgPtr:%d\n", ntohs(tcp.UrgPtr));
printf("Checksum:%u\n", ntohs(tcp.Checksum));
printf("Send Successfully!\n");
}

校验和方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//获得校验和的方法
unsigned short checksum(unsigned short *data, int length)
{
unsigned long temp = 0;
while (length > 1)
{
temp += *data++;
length -= sizeof(unsigned short);
}
if (length)
{
temp += *(unsigned short*)data;
}
temp = (temp >> 16) + (temp &0xffff);
temp += (temp >> 16);
return (unsigned short)(~temp);
}

记得在声明一下这个方法。如果放在main函数前当然就不用声明啦。 另外需要声明的变量有

1
2
3
4
struct EthernetHeader ethernet;    //以太网帧头
struct IpHeader ip; //IP头
struct TcpHeader tcp; //TCP头
struct PsdTcpHeader ptcp; //TCP伪首部
1
unsigned char SendBuffer[200];       //发送队列

接下来的运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
获取MAC地址完毕,请输
121.250.216.112
请输入你要发送的内容
what is tcp
要发送的内容:what i
Send TCP Packet.
Dstination Port:102
Source Port:1000
Sequence:11
Acknowledgment:0
Header Length:5*4
Flags:0x18
AdvertiseWindow:512
UrgPtr:0
Checksum:17149
Send Successfully!

截图如下: 好啦,发送帧到此就告一段落啦!如果有疑问请留言。 帧的接收很简单,直接贴源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
#include <stdio.h>
#include <stdlib.h>
#include <pcap.h>


char *iptos(u_long in); //u_long即为 unsigned long
void packet_handler(u_char *param, const struct pcap_pkthdr *header, const u_char *pkt_data);
//struct tm *ltime; //和时间处理有关的变量

struct IpAddress
{
u_char byte1;
u_char byte2;
u_char byte3;
u_char byte4;
};

//帧头部结构体,共14字节
struct EthernetHeader
{
u_char DestMAC[6]; //目的MAC地址 6字节
u_char SourMAC[6]; //源MAC地址 6字节
u_short EthType; //上一层协议类型,如0x0800代表上一层是IP协议,0x0806为arp 2字节
};

//IP头部结构体,共20字节
struct IpHeader
{
unsigned char Version_HLen; //版本信息4位 ,头长度4位 1字节
unsigned char TOS; //服务类型 1字节
short Length; //数据包长度 2字节
short Ident; //数据包标识 2字节
short Flags_Offset; //标志3位,片偏移13位 2字节
unsigned char TTL; //存活时间 1字节
unsigned char Protocol; //协议类型 1字节
short Checksum; //首部校验和 2字节
IpAddress SourceAddr; //源IP地址 4字节
IpAddress DestinationAddr; //目的IP地址 4字节
};

//TCP头部结构体,共20字节
struct TcpHeader
{
unsigned short SrcPort; //源端口号 2字节
unsigned short DstPort; //目的端口号 2字节
unsigned int SequenceNum; //序号 4字节
unsigned int Acknowledgment; //确认号 4字节
unsigned char HdrLen; //首部长度4位,保留位6位 共10位
unsigned char Flags; //标志位6位
unsigned short AdvertisedWindow; //窗口大小16位 2字节
unsigned short Checksum; //校验和16位 2字节
unsigned short UrgPtr; //紧急指针16位 2字节
};

//TCP伪首部结构体 12字节
struct PsdTcpHeader
{
unsigned long SourceAddr; //源IP地址 4字节
unsigned long DestinationAddr; //目的IP地址 4字节
char Zero; //填充位 1字节
char Protcol; //协议号 1字节
unsigned short TcpLen; //TCP包长度 2字节
};


int main(){

EthernetHeader *ethernet; //以太网帧头
IpHeader *ip; //IP头
TcpHeader *tcp; //TCP头
PsdTcpHeader *ptcp; //TCP伪首部

pcap_if_t * alldevs; //所有网络适配器
pcap_if_t *d; //选中的网络适配器
char errbuf[PCAP_ERRBUF_SIZE]; //错误缓冲区,大小为256
char source[PCAP_ERRBUF_SIZE];
pcap_t *adhandle; //捕捉实例,是pcap_open返回的对象
int i = 0; //适配器计数变量
struct pcap_pkthdr *header; //接收到的数据包的头部
const u_char *pkt_data; //接收到的数据包的内容
int res; //表示是否接收到了数据包
u_int netmask; //过滤时用的子网掩码
char packet_filter[] = "tcp"; //过滤字符
struct bpf_program fcode; //pcap_compile所调用的结构体

u_int ip_len; //ip地址有效长度
u_short sport,dport; //主机字节序列
u_char packet[100]; //发送数据包目的地址
pcap_dumper_t *dumpfile; //堆文件

//time_t local_tv_sec; //和时间处理有关的变量
//char timestr[16]; //和时间处理有关的变量


//获取本地适配器列表
if(pcap_findalldevs_ex(PCAP_SRC_IF_STRING,NULL,&alldevs,errbuf) == -1){
//结果为-1代表出现获取适配器列表失败
fprintf(stderr,"Error in pcap_findalldevs_ex:\n",errbuf);
//exit(0)代表正常退出,exit(other)为非正常退出,这个值会传给操作系统
exit(1);
}
//打印设备列表信息
for(d = alldevs;d !=NULL;d = d->next){
printf("-----------------------------------------------------------------\nnumber:%d\nname:%s\n",++i,d->name);
if(d->description){
//打印适配器的描述信息
printf("description:%s\n",d->description);
}else{
//适配器不存在描述信息
printf("description:%s","no description\n");
}
//打印本地环回地址
printf("\tLoopback: %s\n",(d->flags & PCAP_IF_LOOPBACK)?"yes":"no");

pcap_addr_t *a; //网络适配器的地址用来存储变量
for(a = d->addresses;a;a = a->next){
//sa_family代表了地址的类型,是IPV4地址类型还是IPV6地址类型
switch (a->addr->sa_family)
{
case AF_INET: //代表IPV4类型地址
printf("Address Family Name:AF_INET\n");
if(a->addr){
//->的优先级等同于括号,高于强制类型转换,因为addr为sockaddr类型,对其进行操作须转换为sockaddr_in类型
printf("Address:%s\n",iptos(((struct sockaddr_in *)a->addr)->sin_addr.s_addr));
}
if (a->netmask){
printf("\tNetmask: %s\n",iptos(((struct sockaddr_in *)a->netmask)->sin_addr.s_addr));
}
if (a->broadaddr){
printf("\tBroadcast Address: %s\n",iptos(((struct sockaddr_in *)a->broadaddr)->sin_addr.s_addr));
}
if (a->dstaddr){
printf("\tDestination Address: %s\n",iptos(((struct sockaddr_in *)a->dstaddr)->sin_addr.s_addr));
}
break;
case AF_INET6: //代表IPV6类型地址
printf("Address Family Name:AF_INET6\n");
printf("this is an IPV6 address\n");
break;
default:
break;
}
}
}
//i为0代表上述循环未进入,即没有找到适配器,可能的原因为Winpcap没有安装导致未扫描到
if(i == 0){
printf("interface not found,please check winpcap installation");
}

int num;
printf("Enter the interface number(1-%d):",i);
//让用户选择选择哪个适配器进行抓包
scanf_s("%d",&num);
printf("\n");

//用户输入的数字超出合理范围
if(num<1||num>i){
printf("number out of range\n");
pcap_freealldevs(alldevs);
return -1;
}
//跳转到选中的适配器
for(d=alldevs, i=0; i< num-1 ; d=d->next, i++);

//运行到此处说明用户的输入是合法的
if((adhandle = pcap_open(d->name, //设备名称
65535, //存放数据包的内容长度
PCAP_OPENFLAG_PROMISCUOUS, //混杂模式
1000, //超时时间
NULL, //远程验证
errbuf //错误缓冲
)) == NULL){
//打开适配器失败,打印错误并释放适配器列表
fprintf(stderr,"\nUnable to open the adapter. %s is not supported by WinPcap\n", d->name);
// 释放设备列表
pcap_freealldevs(alldevs);
return -1;
}


//打印输出,正在监听中
printf("\nlistening on %s...\n", d->description);

//所在网络不是以太网,此处只取这种情况
if(pcap_datalink(adhandle) != DLT_EN10MB)
{
fprintf(stderr,"\nThis program works only on Ethernet networks.\n");
//释放列表
pcap_freealldevs(alldevs);
return -1;
}

//先获得地址的子网掩码
if(d->addresses != NULL)
//获得接口第一个地址的掩码
netmask=((struct sockaddr_in *)(d->addresses->netmask))->sin_addr.S_un.S_addr;
else
// 如果接口没有地址,那么我们假设一个C类的掩码
netmask=0xffffff;

//pcap_compile()的原理是将高层的布尔过滤表
//达式编译成能够被过滤引擎所解释的低层的字节码
if(pcap_compile(adhandle, //适配器处理对象
&fcode,
packet_filter, //过滤ip和UDP
1, //优化标志
netmask //子网掩码
)<0)
{
//过滤出现问题
fprintf(stderr,"\nUnable to compile the packet filter. Check the syntax.\n");
// 释放设备列表
pcap_freealldevs(alldevs);
return -1;
}

//设置过滤器
if (pcap_setfilter(adhandle, &fcode)<0)
{
fprintf(stderr,"\nError setting the filter.\n");
//释放设备列表
pcap_freealldevs(alldevs);
return -1;
}


//利用pcap_next_ex来接受数据包
while((res = pcap_next_ex(adhandle,&header,&pkt_data))>=0)
{
if(res ==0){
//返回值为0代表接受数据包超时,重新循环继续接收
continue;
}else{
//运行到此处代表接受到正常从数据包
//header为帧的头部
printf("%.6ld len:%d ", header->ts.tv_usec, header->len);
// 获得IP数据包头部的位置
ip = (IpHeader *) (pkt_data +14); //14为以太网帧头部长度
//获得TCP头部的位置
ip_len = (ip->Version_HLen & 0xf) *4;
printf("ip_length:%d ",ip_len);
tcp = (TcpHeader *)((u_char *)ip+ip_len);
char * data;
data = (char *)((u_char *)tcp+20);
//将网络字节序列转换成主机字节序列
sport = ntohs( tcp->SrcPort );
dport = ntohs( tcp->DstPort );
printf("srcport:%d desport:%d\n",sport,dport);
printf("%d.%d.%d.%d.%d -> %d.%d.%d.%d.%d\n",
ip->SourceAddr.byte1,
ip->SourceAddr.byte2,
ip->SourceAddr.byte3,
ip->SourceAddr.byte4,
sport,
ip->DestinationAddr.byte1,
ip->DestinationAddr.byte2,
ip->DestinationAddr.byte3,
ip->DestinationAddr.byte4,
dport);
printf("%s\n",data);
}

}


//释放网络适配器列表
pcap_freealldevs(alldevs);

/**
int pcap_loop ( pcap_t * p,
int cnt,
pcap_handler callback,
u_char * user
);
typedef void (*pcap_handler)(u_char *, const struct pcap_pkthdr *,
const u_char *);
*/
//开始捕获信息,当捕获到数据包时,会自动调用这个函数
//pcap_loop(adhandle,0,packet_handler,NULL);

int inum;
scanf_s("%d", &inum);

return 0;

}

/* 每次捕获到数据包时,libpcap都会自动调用这个回调函数 */
/**
pcap_loop()函数是基于回调的原理来进行数据捕获的,如技术文档所说,这是一种精妙的方法,并且在某些场合下,
它是一种很好的选择。但是在处理回调有时候会并不实用,它会增加程序的复杂度,特别是在多线程的C++程序中
*/
/*
void packet_handler(u_char *param, const struct pcap_pkthdr *header, const u_char *pkt_data)
{
struct tm *ltime = NULL;
char timestr[16];
time_t local_tv_sec;

// 将时间戳转换成可识别的格式
local_tv_sec = header->ts.tv_sec;
localtime_s(ltime,&local_tv_sec);
strftime( timestr, sizeof timestr, "%H:%M:%S", ltime);

printf("%s,%.6ld len:%d\n", timestr, header->ts.tv_usec, header->len);

}
*/
/* 将数字类型的IP地址转换成字符串类型的 */
#define IPTOSBUFFERS 12
char *iptos(u_long in)
{
static char output[IPTOSBUFFERS][3*4+3+1];
static short which;
u_char *p;

p = (u_char *)&in;
which = (which + 1 == IPTOSBUFFERS ? 0 : which + 1);
sprintf_s(output[which], "%d.%d.%d.%d", p[0], p[1], p[2], p[3]);
return output[which];
}

运行截图如下 Thank You 如有问题,欢迎留言~

C/C++

上一篇我们介绍了Eclipse配置Winpcap环境。但是美中不足的是Eclipse编写Winpcap会出现各种各样神奇的错误… 本文原文地址: 我的CSDN博客 其实也不是不能用,就是总是报一些函数不能识别的错误,每次不能识别的函数就是因为缺少了各种各样的头文件或者宏定义…让我比较纠结,心中不由得萌生了一个想法,换个环境试试吧,于是VS环境配置就这么神奇的产生了…. 好,接下来我以VS2012为例为大家讲解Winpcap环境配置,其实VS2010,VS2013等等的版本都是通用哒.. 首先你要有一个VS…这不是废话么… 首先,按照 Eclipse配置Winpcap环境 配置好Winpcap和下载好相关的SDK,这里就不再赘述了 好,文件-新建-项目-Win32控制台应用程序,可以修改下面的路径和项目名称,然后确定 接下来就会蹦出一个窗口..点击下一步 下一步,勾选空项目,然后点击确定,空项目一定要勾选,不然会生成一些不必要的文件。 点击完成,现在看到的界面如下: 在项目名上右键单击,选择属性 链接器选项卡—输入—附加依赖项中在前面输入ws2_32.lib;wpcap.lib; 两个类库,截图如下 C/C++选项卡—-预处理器—-预处理器定义键入HAVE_REMOTE;WPCAP; 如图所示 VC++目录,加入你的Winpcap类库路径,文件夹应该叫WpdPack,一个是Include,一个是lib,这是绝对路径,你的可能与我的不一样,截图如下 好了。点击确定,环境就配好啦。 接下来新建一个cpp文件,运行程序就OK了… 如有问题,欢迎留言交流

C/C++

大家好,本节为大家带来在Eclipse下配置Winpcap环境,欢迎大家收看。 本文原文地址来自: 我的CSDN博客 首先,配置Winpcap环境的前提是你必须配置好了Eclipse下的C/C++环境。如果你还没有配置,欢迎大家收看上节内容进行配置。 链接地址: Eclipse配置C/C++环境 若链接失效,请自行查看上一篇文章或者百度其他文章。 废话不多说啦,开始我们的Winpcap的配置。

Winpcap下载

Winpcap官网: Winpcap官网 Winpcap目前最新版为4.1.3,首先你要下载exe文件并安装,直接双击运行安装即可。 下载地址: Winpcap4.1.3.exe 然后你需要下载开发包,首先必须注意的是,目前最新版本是没有开发包的,最新的开发包为4.1.2,先见下图 下面的红色框说明了目前没有提供Winpcap4.1.3的开发包,最新版本的开发包是4.1.2,他可以与4.1.3的Winpcap配套使用。 所以下载4.1.2的开发包。 下载地址: Winpcap Developer’s Pack 4.1.2

Eclipse相关配置

首先新建一个C 的项目,具体的建立过程可以参见上一节的内容。 我们加入一个测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
#define WIN32
#define HAVE_REMOTE

#include <stdio.h>
#include "pcap.h"
#include "Win32-Extensions.h"

void gen_packet(unsigned char *buf,int len);

/*生成数据包*/
void gen_packet(unsigned char *buf,int len)
{
int i=0;

//设置目标MAC地址为01:01:01:01:01:01
for (i=0;i<6;i++)
{
buf[i]=0x01;
}

//设置源MAC地址为02:02:02:02:02:02
for (i=6;i<12;i++)
{
buf[i]=0x02;
}

//设置协议标识为0xc0xd,无任何实际意义
buf[12]=0xc;
buf[13]=0xd;

//填充数据包的内容
for(i=14;i<len;i++)
{
buf[i]=i-14;
}
}


int main()
{
pcap_if_t *alldevs;
pcap_if_t *d;
int inum;
int i=0;
pcap_t *adhandle;
char errbuf[PCAP_ERRBUF_SIZE];
int ret=-1;

/* 获取本机网络设备列表 */
if (pcap_findalldevs_ex(PCAP_SRC_IF_STRING, NULL, &alldevs, errbuf) == -1)
{
fprintf(stderr,"Error in pcap_findalldevs: %s\n", errbuf);
exit(1);
}

/* 打印网络设备列表 */
for(d=alldevs; d; d=d->next)
{
printf("%d. %s", ++i, d->name);
if (d->description)
printf(" (%s)\n", d->description);
else
printf(" (No description available)\n");
}

if(i==0)
{
printf("\nNo interfaces found! Make sure WinPcap is installed.\n");
return -1;
}

/*选择网络设备接口*/
printf("Enter the interface number (1-%d):",i);
scanf("%d", &inum);

if(inum < 1 || inum > i)
{
printf("\nInterface number out of range.\n");
/* 释放设备列表 */
pcap_freealldevs(alldevs);
return -1;
}

/* 跳转到选中的适配器 */
for(d=alldevs, i=0; i< inum-1 ;d=d->next, i++);

/* 打开设备 */
if ( (adhandle= pcap_open(d->name, // 设备名
65536, // 65535保证能捕获到数据链路层上每个数据包的全部内容
PCAP_OPENFLAG_PROMISCUOUS, // 混杂模式
1000, // 读取超时时间
NULL, // 远程机器验证
errbuf // 错误缓冲池
) ) == NULL)
{
fprintf(stderr,"\nUnable to open the adapter. %s is not supported by WinPcap\n", d->name);
/* 释放设备列表 */
pcap_freealldevs(alldevs);
return -1;
}

/*在选中的设备接口上发送数据*/
printf("\nsending on %s...\n", d->description);

/* 发送数据包*/
//生成数据报
int packetlen=100;
unsigned char *buf= (unsigned char *)malloc(packetlen);
memset(buf,0x0,packetlen);
gen_packet(buf,packetlen); //获得生成的数据包,长度为packetlen
//开始数据包发送
if ( (ret=pcap_sendpacket(adhandle,buf,packetlen))==-1)
{
printf("发送失败\n");
free(buf);
pcap_close(adhandle);
pcap_freealldevs(alldevs);
return -1;
}

/*释放资源*/
free(buf);
pcap_close(adhandle);
pcap_freealldevs(alldevs);

return 0;
}

把代码拷贝到你的项目程序里面,可以发现现在是编译错误,有些对象根本无法识别,截图如下: 接下来就需要我们对类库进行配置啦。 首先解压你下载的开发包,随便放硬盘的某个位置,我放在了D盘的eclipse_plugins文件夹中,当然你可以随便放哪里都行。 接下来配置Eclipse,右键项目->属性->C/C++常规->项目和符号。 首先添加你的include库,在包含这个选项卡中添加你的库,点击添加->选择文件系统->选择你刚才的开发库的include文件夹,按照图中的顺序来 点击确定添加,同理在库路径选项卡中进行库路径的配置,这次添加的是lib文件夹。按照图片中的顺序来做 点击确定添加。 然后在库的选项卡中添加wpcap和Packet两个库,注意这次不能选择文件系统了,因为你指定了库路径之后它会自动搜索路径中库的名字,这次你只需要指定库的名字就好了。我之前添加的是文件系统,然后它总是提示找不到这个库,所以一定要直接填写这两个库的名字。如图所示: 添加完毕之后,出现这个样子: 好啦,点击确定,全部配置已经完毕啦。 重新构建项目,运行即可。 注意,构建过程可能出现如下问题:

  • error C2065: “PCAP_SRC_IF_STRING”: 未声明的标识符
  • error C3861: “pcap_findalldevs_ex”: 找不到标识符
  • error C2065: “PCAP_OPENFLAG_PROMISCUOUS”: 未声明的标识符
  • error C3861: “pcap_open”: 找不到标识符

因为新的版本里WinPcap支持远程数据包获取,所以还应当添加一个头文件remote-ext.h ,即#include “remote-ext.h”(记住这条语句要放在#include “pcap.h”之后,否则会出错!) 好了,一切问题都解决了,运行成功啦! 运行结果如下: 再附测试代码一例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
#define WIN32
#define HAVE_REMOTE

#include <stdio.h>
#include "pcap.h"
#include "Win32-Extensions.h"

void gen_packet(unsigned char *buf,int len);

/*生成数据包*/
void gen_packet(unsigned char *buf,int len)
{
int i=0;

//设置目标MAC地址为01:01:01:01:01:01
for (i=0;i<6;i++)
{
buf[i]=0x01;
}

//设置源MAC地址为02:02:02:02:02:02
for (i=6;i<12;i++)
{
buf[i]=0x02;
}

//设置协议标识为0xc0xd,无任何实际意义
buf[12]=0xc;
buf[13]=0xd;

//填充数据包的内容
for(i=14;i<len;i++)
{
buf[i]=i-14;
}
}


int main()
{
pcap_if_t *alldevs;
pcap_if_t *d;
int inum;
int i=0;
pcap_t *adhandle;
char errbuf[PCAP_ERRBUF_SIZE];
int ret=-1;

/* 获取本机网络设备列表 */
if (pcap_findalldevs_ex(PCAP_SRC_IF_STRING, NULL, &alldevs, errbuf) == -1)
{
fprintf(stderr,"Error in pcap_findalldevs: %s\n", errbuf);
exit(1);
}

/* 打印网络设备列表 */
for(d=alldevs; d; d=d->next)
{
printf("%d. %s", ++i, d->name);
if (d->description)
printf(" (%s)\n", d->description);
else
printf(" (No description available)\n");
}

if(i==0)
{
printf("\nNo interfaces found! Make sure WinPcap is installed.\n");
return -1;
}

/*选择网络设备接口*/
printf("Enter the interface number (1-%d):",i);
scanf("%d", &inum);

if(inum < 1 || inum > i)
{
printf("\nInterface number out of range.\n");
/* 释放设备列表 */
pcap_freealldevs(alldevs);
return -1;
}

/* 跳转到选中的适配器 */
for(d=alldevs, i=0; i< inum-1 ;d=d->next, i++);

/* 打开设备 */
if ( (adhandle= pcap_open(d->name, // 设备名
65536, // 65535保证能捕获到数据链路层上每个数据包的全部内容
PCAP_OPENFLAG_PROMISCUOUS, // 混杂模式
1000, // 读取超时时间
NULL, // 远程机器验证
errbuf // 错误缓冲池
) ) == NULL)
{
fprintf(stderr,"\nUnable to open the adapter. %s is not supported by WinPcap\n", d->name);
/* 释放设备列表 */
pcap_freealldevs(alldevs);
return -1;
}

/*在选中的设备接口上发送数据*/
printf("\nsending on %s...\n", d->description);

/* 发送数据包*/
//生成数据报
int packetlen=100;
unsigned char *buf= (unsigned char *)malloc(packetlen);
memset(buf,0x0,packetlen);
gen_packet(buf,packetlen); //获得生成的数据包,长度为packetlen
//开始数据包发送
if ( (ret=pcap_sendpacket(adhandle,buf,packetlen))==-1)
{
printf("发送失败\n");
free(buf);
pcap_close(adhandle);
pcap_freealldevs(alldevs);
return -1;
}

/*释放资源*/
free(buf);
pcap_close(adhandle);
pcap_freealldevs(alldevs);

return 0;
}

运行结果:

1
2
3
4
5
1. rpcap://\Device\NPF_{5AC72F8D-019C-4003-B51B-7ABB67AF392A} (Network adapter 'Microsoft' on local host)
2. rpcap://\Device\NPF_{33E23A2F-F791-409B-8452-A3FB5A78B73E} (Network adapter 'Qualcomm Atheros Ar81xx series PCI-E Ethernet Controller' on local host)
3. rpcap://\Device\NPF_{DCCF036F-A9A8-4225-B980-D3A3F0575F5B} (Network adapter 'Microsoft' on local host)
4. rpcap://\Device\NPF_{D62A0060-F424-46FC-83A5-3394081685FD} (Network adapter 'Microsoft' on local host)
5. rpcap://\Device\NPF_{B5224A53-8450-4537-AB3B-9869158121CD} (Network adapter 'Microsoft' on local host)

那么环境配好了,我们接下来就要进行计算机网络编程学习阶段了,后期的文章将会记录我计算机网络编程学习的一些内容,欢迎大家收看。

C/C++

本次为大家带来的是用Eclipse配置C/C++环境的具体步骤,希望对大家有帮助哦。 声明:如果你希望用Eclipse开发,可以参考本文章配置环境,如若想用VS开发,请看后面的文章。 本文原文地址来自 我的CSDN博客 Eclipse 配置C/C++开发环境讲解

JDK

如果没有安装JDK环境的小伙伴请自行去官网下载安装啦,安装之后的就可以跳过此步. 下载地址: JDK下载JDK环境配置

Eclipse

当然,下载JDK的目的是在Eclipse下先运行起Java程序,这样才能方便我们下一步的操作,没有Eclipse的也去下载。 下载地址: Eclipse下载 上面两步我想难度都很小,而且大部分同学已经配置了,我就不一一讲解了。

CDT

CDT是什么?它就是Eclipse的一个插件,我们需要安装了之后才可以新建C/C++项目。 注意:下载的CDT必须和Eclipse的版本对应。 下载地址: CDT下载 上面下载的Eclipse是3.7版本,在此给出适配CDT 8.0的下载链接。 下载地址: CDT 8.0下载 如果上述链接已失效,请自行百度适配你Eclipse版本的CDT进行下载。 下载之后它是一个压缩包zip格式。 不要解压直接安装即可,安装讲解如下: 打开Eclipse,进入菜单Help,选择Install New Software…,点击右边Add按钮,在Add Repository对话框中点击右下角的Archive…,浏览到你之前下载保存路径,选中cdt-master-8.0.0.zip并双击,勾选所有CDT部件,然后点击Next>,继续Next>,选中“I accept the terms of the license agreement – Finish”,点击Finish开始安装CDT。 安装完CDT后重启Eclipse。 这样我们的Eclipse的CDT插件就安装好啦,这时你会发现可以新建C/C++项目了,但是还是不能编译运行,为什么?因为没有安装编译器。接下来我们需要下载mingw编译器。

MinGW

这是一个配套Eclipse的C/C++编译器,首先我们下载下来。 官网下载地址: mingw官网下载 现在发现最新版本下载是: mingw-get-setup.exe 双击运行,选择安装路径,我的路径为:D:\mingw,安装完成之后你会发现出现一个下载库的窗口,叫Mingw Installation Manager 注意: 1. 我的类库已经安装完毕,所以每个类库前面都显示为绿色的方框,没安装之前是白色的,你需要都勾选上,然后下载安装。在每个类库上邮件单击选择make for installation即可勾选。再点击菜单中的Installation中的Update Catalogue即可安装你所勾选的类库了。 2. 安装下载可能花费不少时间,请耐心等待。 大家可以发现最左边有Basic Steup和All Packages两个分类。 第一个Basic Steup即为必须要安装的编译类库,All Packages即为所有的类库。为了保险起见,我把所有的类库都安装了。全部安装完之后即会显示绿色。 安装完之后的目录结构如下:

环境变量配置

环境变量配置: 计算机– 属性 – 高级系统设置 – 环境变量 在上方的用户变量中进行如下操作: 注意:mingw的位置不同,环境变量的配置不同,可以看最底层目录比对着来配置。我的环境变量配置如下:

(1)编辑PATH变量,在最后面加入 D:\mingw\bin D:\mingw\msys\1.0\bin D:\mingw\mingw32\bin (2)添加LIBRARY_PATH变量,内容为: D:\mingw\lib (3)添加C_INCLUDE_PATH变量,内容为: D:\mingw\include (4)添加CPLUS_INCLUDE_PATH变量,内容为: D:\mingw\lib\gcc\mingw32\4.8.1\include\c++

修改:

进入D:\mingw\bin下将mingw32-make.exe复制成make.exe。因为Eclipse使用时预设是用系统里的”make”这个文件名,而不是”mingw32-make”。

Eclipse中的配置: 窗口 -> 首选项 -> C/C++ -> New CDT Project Wizard 在右边的首选工具链的右边,工具链栏目内选择MinGW GCC,确定,如图所示: 还是在原来的 New CDT Project Wizard选项卡中,展开,Makefile项目,二进制解析器,勾选PE windows解析器,确定,如图所示。 好啦,配置工作完成,我们来测试一下新C语言项目。

测试项目

右键新建->项目->C项目 下一步,选择可执行文件,Hello World ANCI C Project 编译器选择 MinGw GCC 完成,如图所示: 完成之后的界面应该是如下所示,点击构建项目,图中的红色圆圈,构建完成之后,右键运行程序 运行结果: 欢呼吧小伙伴们 ! Eclipse 配置C/C++环境到此完全结束,如果有问题欢迎交流。

C/C++

Hi,大家好,现在计算机网络课程设计开始啦,本次为大家带来计算机网络实验的系列讲解,希望对于小伙伴们的计网课设有一定帮助哦。

写在前面

首先,博主来自山东大学,这次计网课程设计的实验学校为山东大学,年级为2012级,请对号入座。 另外,对于上学期学的计算机网络课程知识,可以说我就是为了应付考试最后狂背下来的,感觉对于计算机网络里的一些原理真的没有理解。纯粹看一些概念性的东西真的是太抽象啦,这次计网课设的目的就是让我们用编程实现网络分析技术和网络功能仿真,来深入了解计算机网络知识。可以说这些才是学习计算机网络最重要的一环,大家加油。

实验要求

1、基本任务(达标任务) 完成两台主机之间的数据通信(数据链路层)

  • 仿真ARP协议获得网段内主机的MAC表
  • 使用帧完成两台主机的通信(Hello! I’m …)

2、高端任务(优秀任务) 完成两台主机通过中间主机的数据通信(网络层)

  • 增加基于IP地址的转发功能
  • 增加网络层封装

本次任务呢分为基本任务和高端任务,当然我们的目标当然不仅限于基本任务咯。

实验概述

一句话,我们需要利用winpcap这个库用C语言编程来实现以上的任务。

winpcap讲解

那么winpcap是什么? 大多数网络应用程序通过被广泛使用的操作系统原件来访问网络,如socket。由于操作系统已经处理了底层的细节问题(如协议的处理、数据包的封装等),并提供了与读写文件类似的函数接口,因此使用该方法可以很容易的访问网络中的数据 然而有些时候,这种简单的方式并不能满足任务要求,有些应用程序需要直接访问网络中的数据包,也就是说,需要访问哪些没有被操作系统处理过的数据包 WinPcap就是为Windows平台的应用程序提供这种访问方式,提供下列功能:

  • 捕获原始数据包。无论是发送到运行WinPcap的机器上的数据包,还是在其它主机(存在网络共享介质上的主机)上进行交换的数据包,都可以被捕获
  • 在数据包传递给应用程序之前,根据用户指定的规则过滤数据包
  • 将原始数据包发送到网络上
  • 收集网络流量与网络状态的统计信息

它提供给我们什么呢?

它包含一个内核空间数据包过滤器(Netgroup Packet Filter, NPF),一个底层动态链接库(Packet.dll)和一个高层独立于操作系统的动态链接库(wpcap.dll)。各部分关系见下图

NPF:为了能够访问网络上传输的原始数据,数据包捕获系统需要绕过操作系统的协议栈。这就需要有一部分程序运行在操作系统的内核中,只有这样才能与网络接口驱动直接交互。在WinPcap中,与操作系统密切相关的是一个名为NPF的设备驱动程序,同时对不同版本的操作系统提供了不同版本的驱动程序。这些驱动程序提供了数据包捕获与发送的基本功能,同时提供了一个可编程的数据包过滤系统、一个网络监视引擎等高级功能。 动态链接库:为了让应用程序能够使用内核驱动提供的功能,数据包捕获系统必须导出相关的接口。对此,WinPcap提供两个不同层次的动态链接库:Packet.dll和wpcap.dll。 Packet.dll提供底层的API,用来直接访问驱动程序的函数,用来提供独立于微软公司不同操作系统的编程接口。 wpcap.dll库导出了更强大、更高层的捕获函数接口,具有与UNIX捕获库libpcap的兼容性。这两个库可使数据包的捕获独立于底层的网络硬件与操作系统。

实验流程

了解了winpcap之后,我们要做的就是利用它提供的类库来实现网络传输,网络解析等一系列的功能。

需要我们做好的准备有:

  • C语言的相关基础
  • 计算机网络的基础知识
  • 对开发环境的相关了解

如果对于上述中任何一个不熟悉,请先去补习一下基础知识吧。

参考书目

  • 吴功宜等,《计算机网络高级软件编程技术》(第2版),清华大学出版社
  • 徐恪等, 《高级计算机网络》,清华大学出版社
  • A.S. Tannenbaum,《计算机网络》(第5版),清华大学出版社
  • 吕雪峰等, 《网络分析技术揭秘(原理、实践与WinPcap深入解析》,机械工业出版社

结语

下一节,我们将讲解在Eclipse环境下搭建winpcap环境的具体步骤。本次讲解到此结束,谢谢大家。

个人日记

二零一五年七月九日,伴随着最后一门图形学交卷铃声的想起,我的大三正式结束了。首先很抱歉,最近一段时间一直在忙考试复习,时间也比较紧张,另外就是整理各种材料,还有准备一些面试,也算是忙得不亦乐乎,所以,这段时间也没有去编代码,大约也有一个月的时间没有更新博客了,有好多热心的朋友在我博客留言评论,也没有及时回复,在这里向大家说一声抱歉。放假回家之后,我会好好研究一下大家提出的意见和建议,给予大家回复的。 恩,接下来的日子应该会稍微轻松那么一点,自己的研究生路大体有了轮廓,有了规划。将来想从事的方向为信息安全方向,自己也做过一些项目和网站,对一些安全性问题要求也是比较高的,自己也对安全问题比较感兴趣,嗯。 现在时间是二零一五年七月十二日,昨天回家,收拾整顿一番,今天假期正式开始啦! 上学期是我大学的第六个学期了,对于保研来说,也着实是最后的一个重要的学期了,学习上也要更加重视。仔细回想起来,这个学期实际的项目开发没有做过多少,和同学开发了一个演唱会支付系统,又完善了一下自己之前的项目。学期后期,基本上就是准备一些面试和靠前复习,另外还有一些研究生招生的面试咨询等等,结果就不透露啦,自我感觉还是不错。 本来有好多话想说,然而这个时间,却不想多言了。 心里的石头还没落地,最近的日子,好好准备即将到来的面试。暑假到了,想要学习的东西还有很多,重新开启我的代码之路!Come On!

崔庆才

2015/7/12

C/C++

综述

通过上一节说的绘制 3D 图形基础,我们应该对绘制 3D 图形有了基本的认识,接下来我们就进行一个实例,绘制一个 3D 机器人。 本节我们要完成的任务有:

1.绘制一个仿真 3D 机器人(样式自选,参考例图),至少包含头、躯干、四肢三个部分. 2.对机器人填充颜色。 3.增加点光源,使得机器人更加真实。 4.实现交互,使得能够控制机器人进行旋转、前进、后退等动作。 键盘”w”:前进 键盘”s”:后退 键盘”a”:顺时针旋转 键盘”d”:逆时针旋转

接下来我们就一步步实现机器人的绘制吧。

绘制球体

绘制球体我们有两种方法,一个叫 glutSolidSphere,另一个叫 glutWireSphere,这两个的区别在于,一个绘制的是实心的球体,另一个绘制的是现状描绘而成的球体,在绘制过程中,我们可以通过一个参数来对其进行控制。 glutSolidSphere 是 GLUT 工具包中的一个函数,该函数用于渲染一个球体。球体球心位于原点。在 OpenGL 中默认的原点就是窗口客户区的中心。 函数原型

1
2
void glutSolidSphere(GLdouble radius , GLint slices , GLint stacks);
void glutWireSphere(GLdouble radius, GLint slices, GLint stacks );

radius,球体的半径 slices,以 Z 轴上线段为直径分布的圆周线的条数(将 Z 轴看成地球的地轴,类似于经线) stacks,围绕在 Z 轴周围的线的条数(类似于地球上纬线) 一般而言, 后两个参数赋予较大的值, 渲染花费的时间要长, 效果更逼真。 然而我们可以发现,这里并没有定义球中心的参数,所以,我们可以利用平移函数组合实现。即利用 glTranslated 函数来实现。最后,我们定义的画球体的方法如下

1
2
3
4
5
6
7
8
9
10
11
//画球
void drawBall(double R, double x, double y,double z, int MODE) {
glPushMatrix();
glTranslated(x,y,z);
if (MODE == SOLID) {
glutSolidSphere(R,20,20);
} else if (MODE ==WIRE) {
glutWireSphere(R,20,20);
}
glPopMatrix();
}

其中两个常量定义如下

1
2
#define SOLID 1
#define WIRE 2

在这里我们还用到了上一节所说的 PushMatrix 和 PopMatrix 方法。如果不熟悉,请查看上一节的内容。

绘制长方体

同样地,利用变换平移放缩的方法,再加上类库的绘制正方体的方法,我们也可以轻松地实现绘制长方体的方法。 正方体怎样变成长方体,很简单,拉伸一下就好了。所以,我们用到了 glScaled 方法。 绘制长方体的方法如下

1
2
3
4
5
6
7
8
9
10
11
12
//画长方体
void drawSkewed(double l, double w, double h, double x, double y, double z, int MODE) {
glPushMatrix();
glScaled(l, w, h);
glTranslated(x, y, z);
if (MODE == SOLID) {
glutSolidCube(1);
} else if (MODE == WIRE) {
glutWireCube(1);
}
glPopMatrix();
}

这里仍然还是定义了绘图模式,是线条还是实体。

定义光照

在这里提供一篇博文,讲光照讲得比较细致 OPENGL 光照 那么在这里我就直接贴上光照设置的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void init() {
//定义光源的颜色和位置
GLfloat ambient[] = { 0.5, 0.8, 0.1, 0.1 };
GLfloat diffuse[] = { 1.0, 1.0, 1.0, 1.0 };
GLfloat position[] = { -80.0, 50.0, 25.0, 1.0 };
//选择光照模型
GLfloat lmodel_ambient[] = { 0.4, 0.4, 0.4, 1.0 };
GLfloat local_view[] = { 0.0 };
glClearColor(0.0, 0.0, 0.0, 0.0);
glShadeModel(GL_SMOOTH);
//设置环境光
glLightfv(GL_LIGHT0, GL_AMBIENT, ambient);
//设置漫射光
glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuse);
//设置光源位置
glLightfv(GL_LIGHT0, GL_POSITION, position);
glLightModelfv(GL_LIGHT_MODEL_AMBIENT, lmodel_ambient);
glLightModelfv(GL_LIGHT_MODEL_LOCAL_VIEWER, local_view);
//启动光照
glEnable(GL_LIGHTING);
//启用光源
glEnable(GL_LIGHT0);
}

其中设置了光源颜色,位置等参数。

图形的移动

在这里我们实现了鼠标的监听旋转和键盘的监听旋转,在这里分别描述如下

1.鼠标监听

对于鼠标监听事件,在前面的文章中已经做了说明,如果大家不熟悉可以看下面这篇文章 鼠标监听 我们要实现的就是在拖动鼠标的时候实现图形的旋转功能。 在点击鼠标时,我们记录下来点击的位置,然后在鼠标移动的时候记录下当前坐标与上一个位置的坐标之差。通过定义一个角度的变量,每次鼠标移动的时候让整个图形旋转角度加上这个差值,然后重新绘制图形,就可以实现整个图形的 旋转了。 鼠标监听的两个方法如下,分别是鼠标点击和鼠标移动。 定义两个变量,旋转角度

1
2
int spinX = 0;
int spinY = 0;

然后定义两个鼠标事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 鼠标移动事件
void mouseMove(int x, int y) {
int dx = x - moveX;
int dy = y - moveY;
printf("dx;%dx,dy:%dy\n",dx,dy);
spinX += dx;
spinY += dy;
glutPostRedisplay();
moveX = x;
moveY = y;
}
//鼠标点击事件
void mouseClick(int btn, int state, int x, int y) {
moveX = x;
moveY = y;
}

恩,通过定义上面的方法,然后在 main 中加入监听。

1
2
3
4
//鼠标点击事件,鼠标点击或者松开时调用
glutMouseFunc(mouseClick);
//鼠标移动事件,鼠标按下并移动时调用
glutMotionFunc(mouseMove);

display 函数中,在绘图前调用该方法即可

1
2
glRotated(spinX, 0, 1, 0);
glRotated(spinY, 1, 0, 0);

分别是绕 y 轴和 x 轴旋转一定的角度。这样就可以实现鼠标的监听了。

2.键盘监听

键盘事件也很简单,同样是监听按键的按下。其中 w、s 键是用来控制机器人的远近的。 定义一个变量叫 dis,代表远近 键盘事件函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//键盘事件
void keyPressed(unsigned char key, int x, int y) {
switch (key) {
case 'a':
spinX -= 2;
break;
case 'd':
spinX += 2;
break;
case 'w':
des += 2;
break;
case 's':
des -= 2;
break;
}
glutPostRedisplay();
}

在 main 函数中加入监听

1
2
//键盘事件
glutKeyboardFunc(keyPressed);

display 函数中加入如下的变换

1
2
3
glRotated(spinX, 0, 1, 0);
glRotated(spinY, 1, 0, 0);
glTranslated(0, 0, des);

这样通过上述方法,我们便可以实现鼠标和键盘的监听了。

裁切物体

在这里我们可能要画一个半圆,那么最方便的方法就是裁切了,利用下面的函数,我们可以方便地实现。

void glClipPlane(GLenum plane, const GLdouble *equation);

定义一个裁剪平面。equation 参数指向平面方程 Ax + By + Cz + D = 0 的 4 个系数。 equation=(0,-1,0,0),前三个参数(0,-1,0)可以理解为法线向下,只有向下的,即 Y<0 的才能显示,最后一个参数 0 表示从 z=0 平面开始。这样就是裁剪掉上半平面。 equation=(0,1,0,0)表示裁剪掉下半平面, equation=(1,0,0,0)表示裁剪掉左半平面, equation=(-1,0,0,0)表示裁剪掉右半平面, equation=(0,0,-1,0)表示裁剪掉前半平面, equation=(0,0,1,0)表示裁剪掉后半平面 代码示例如下

1
2
3
4
5
GLdouble eqn[4]={0.0,0.0,-1.0,0.0};
glClipPlane(GL_CLIP_PLANE0,eqn);
glEnable(GL_CLIP_PLANE0);
glutSolidSphere(headR,slices,slices);
glDisable(GL_CLIP_PLANE0);

首先我们必须要定义一个 GLdouble 数组,然后利用 glClipPlane 方法来设置,然后开启裁切,最后关闭裁切。 利用类似的方法我们可以写出绘制半球的方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//画半球
void drawHalfBall(double R, double x, double y,double z, int MODE) {
glPushMatrix();
glTranslated(x,y,z);
GLdouble eqn[4]={0.0, 1.0, 0.0, 0.0};
glClipPlane(GL_CLIP_PLANE0,eqn);
glEnable(GL_CLIP_PLANE0);
if (MODE == SOLID) {
glutSolidSphere(R,20,20);
} else if (MODE ==WIRE) {
glutWireSphere(R,20,20);
}
glDisable(GL_CLIP_PLANE0);
glPopMatrix();
}

恩,通过上述方法,我们可以方便地绘制出一个半球体。

绘制机器人

有了上述的铺垫,我们绘制机器人简直易如反掌,同样还可以实现各式各样的监听。 主要就是位置的确定了。 display 函数如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void display(void) {
//清除缓冲区颜色
glClear(GL_COLOR_BUFFER_BIT);
//定义白色
glColor3f(1.0, 1.0, 1.0);
//圆点放坐标中心
glLoadIdentity();
//从哪个地方看
gluLookAt(-2.0, -1.0, 20.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
glPushMatrix();
glRotated(spinX, 0, 1, 0);
glRotated(spinY, 1, 0, 0);
glTranslated(0, 0, des);
//头
drawBall(2, 0, 1, 0, SOLID);
//身体
drawSkewed(5, 4.4, 4, 0, -0.75, 0, SOLID);
//肩膀
drawHalfBall(1, 3.5, -2.1, 0, SOLID);
drawHalfBall(1, -3.5, -2.1, 0, SOLID);
//胳膊
drawSkewed(1, 3, 1, 3.5, -1.3, 0, SOLID);
drawSkewed(1, 3, 1, -3.5, -1.3, 0, SOLID);
//手
drawBall(1, 3.5, -6.4, 0, SOLID);
drawBall(1, -3.5, -6.4, 0, SOLID);
//腿
drawSkewed(1.2, 3, 2, 1, -2.4, 0, SOLID);
drawSkewed(1.2, 3, 2, -1, -2.4, 0, SOLID);
//脚
drawSkewed(1.5, 1, 3, 0.9, -9.2, 0, SOLID);
drawSkewed(1.5, 1, 3, -0.9, -9.2, 0, SOLID);
glPopMatrix();
glutSwapBuffers();
}

恩,通过调用这个函数我们便可以完成机器人的绘制了。

完整代码

在这里提供完整代码示例,仅供参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
#include <glut.h>
#include <stdlib.h>
#include <stdio.h>

#define SOLID 1
#define WIRE 2

int moveX,moveY;
int spinX = 0;
int spinY = 0;
int des = 0;


void init() {
//定义光源的颜色和位置
GLfloat ambient[] = { 0.5, 0.8, 0.1, 0.1 };
GLfloat diffuse[] = { 1.0, 1.0, 1.0, 1.0 };
GLfloat position[] = { -80.0, 50.0, 25.0, 1.0 };
//选择光照模型
GLfloat lmodel_ambient[] = { 0.4, 0.4, 0.4, 1.0 };
GLfloat local_view[] = { 0.0 };
glClearColor(0.0, 0.0, 0.0, 0.0);
glShadeModel(GL_SMOOTH);
//设置环境光
glLightfv(GL_LIGHT0, GL_AMBIENT, ambient);
//设置漫射光
glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuse);
//设置光源位置
glLightfv(GL_LIGHT0, GL_POSITION, position);
glLightModelfv(GL_LIGHT_MODEL_AMBIENT, lmodel_ambient);
glLightModelfv(GL_LIGHT_MODEL_LOCAL_VIEWER, local_view);
//启动光照
glEnable(GL_LIGHTING);
//启用光源
glEnable(GL_LIGHT0);
}

//画球
void drawBall(double R, double x, double y,double z, int MODE) {
glPushMatrix();
glTranslated(x,y,z);
if (MODE == SOLID) {
glutSolidSphere(R,20,20);
} else if (MODE ==WIRE) {
glutWireSphere(R,20,20);
}
glPopMatrix();
}

//画半球
void drawHalfBall(double R, double x, double y,double z, int MODE) {
glPushMatrix();
glTranslated(x,y,z);
GLdouble eqn[4]={0.0, 1.0, 0.0, 0.0};
glClipPlane(GL_CLIP_PLANE0,eqn);
glEnable(GL_CLIP_PLANE0);
if (MODE == SOLID) {
glutSolidSphere(R,20,20);
} else if (MODE ==WIRE) {
glutWireSphere(R,20,20);
}
glDisable(GL_CLIP_PLANE0);
glPopMatrix();
}

//画长方体
void drawSkewed(double l, double w, double h, double x, double y, double z, int MODE) {
glPushMatrix();
glScaled(l, w, h);
glTranslated(x, y, z);
if (MODE == SOLID) {
glutSolidCube(1);
} else if (MODE ==WIRE) {
glutWireCube(1);
}
glPopMatrix();
}

void display(void) {
//清除缓冲区颜色
glClear(GL_COLOR_BUFFER_BIT);
//定义白色
glColor3f(1.0, 1.0, 1.0);
//圆点放坐标中心
glLoadIdentity();
//从哪个地方看
gluLookAt(-2.0, -1.0, 20.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
glPushMatrix();
glRotated(spinX, 0, 1, 0);
glRotated(spinY, 1, 0, 0);
glTranslated(0, 0, des);
//头
drawBall(2, 0, 1, 0, SOLID);
//身体
drawSkewed(5, 4.4, 4, 0, -0.75, 0, SOLID);
//肩膀
drawHalfBall(1, 3.5, -2.1, 0, SOLID);
drawHalfBall(1, -3.5, -2.1, 0, SOLID);
//胳膊
drawSkewed(1, 3, 1, 3.5, -1.3, 0, SOLID);
drawSkewed(1, 3, 1, -3.5, -1.3, 0, SOLID);
//手
drawBall(1, 3.5, -6.4, 0, SOLID);
drawBall(1, -3.5, -6.4, 0, SOLID);
//腿
drawSkewed(1.2, 3, 2, 1, -2.4, 0, SOLID);
drawSkewed(1.2, 3, 2, -1, -2.4, 0, SOLID);
//脚
drawSkewed(1.5, 1, 3, 0.9, -9.2, 0, SOLID);
drawSkewed(1.5, 1, 3, -0.9, -9.2, 0, SOLID);
glPopMatrix();
glutSwapBuffers();
}
//鼠标点击事件
void mouseClick(int btn, int state, int x, int y) {
moveX = x;
moveY = y;
GLfloat ambient[] = { (float)rand() / RAND_MAX, (float)rand() / RAND_MAX, (float)rand() / RAND_MAX, 0.1 };
//设置环境光
glLightfv(GL_LIGHT0, GL_AMBIENT, ambient);
//启用光源
glEnable(GL_LIGHT0);
}

//键盘事件
void keyPressed(unsigned char key, int x, int y) {
switch (key) {
case 'a':
spinX -= 2;
break;
case 'd':
spinX += 2;
break;
case 'w':
des += 2;
break;
case 's':
des -= 2;
break;
}
glutPostRedisplay();
}
// 鼠标移动事件
void mouseMove(int x, int y) {
int dx = x - moveX;
int dy = y - moveY;
printf("dx;%dx,dy:%dy\n",dx,dy);
spinX += dx;
spinY += dy;
glutPostRedisplay();
moveX = x;
moveY = y;
}

void reshape(int w, int h) {
//定义视口大小
glViewport(0, 0, (GLsizei) w, (GLsizei) h);
//投影显示
glMatrixMode(GL_PROJECTION);
//坐标原点在屏幕中心
glLoadIdentity();
//操作模型视景
gluPerspective(60.0, (GLfloat) w/(GLfloat) h, 1.0, 20.0);
glMatrixMode(GL_MODELVIEW);
}

int main(int argc, char** argv) {
//初始化
glutInit(&argc, argv);
//设置显示模式
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB);
//初始化窗口大小
glutInitWindowSize(500, 500);
//定义左上角窗口位置
glutInitWindowPosition(100, 100);
//创建窗口
glutCreateWindow(argv[0]);
//初始化
init();
//显示函数
glutDisplayFunc(display);
//窗口大小改变时的响应
glutReshapeFunc(reshape);
//鼠标点击事件,鼠标点击或者松开时调用
glutMouseFunc(mouseClick);
//鼠标移动事件,鼠标按下并移动时调用
glutMotionFunc(mouseMove);
//键盘事件
glutKeyboardFunc(keyPressed);
//循环
glutMainLoop();
return 0;
}

仅供参考,如有问题,敬请指正。 运行结果如下 QQ截图20150607013139 恩,大体就是这样。

总结

本次实验做的比较匆忙,只研究了一个晚上的时间,所以有些地方还是不太完善,希望发出来对小伙伴们有所启发,有所帮助。如果有问题,欢迎同我交流。谢大家!

C/C++

综述

在前面我们进行了 2D 图形的绘制,接下来,我们将步入 3D 图形的世界,绘制出一个 3D 机器人,好,废话不多说,让我们一起来迈入 3D 绘图之旅吧。

基本函数

那么在绘图之前呢,我们首先要介绍几个新的函数,弄懂了这几个函数我们才能方便地绘制出我们的 3D 机器人。 那么我们介绍一下gluPerspectivegluLookAt、glPushMatrix、glPopMatrix()函数

1.void gluPerspective(GLdouble fovy,GLdouble aspect,GLdouble zNear,GLdouble zFar)

参数说明: fovy,视角的大小,如果设置为 0,相当闭上眼睛,什么也看不到,如果为 180,则可以看到几乎所有的内容。一般设置为 60 即可。 aspect,就是实际窗口的纵横比,即 x/y,一般都是要按照窗口比例来,否则看到的图形和实际图形就会不一样了。 zNear,表示近处的裁面。 zFar,表示远处的裁面。 gluPerspective 这个函数指定了观察的视景体在世界坐标系中的具体大小,一般而言,其中的参数 aspect 应该与窗口的宽高比大小相同。比如说,aspect=2.0 表示在观察者的角度中物体的宽度是高度的两倍,在视口中宽度也是高度的两倍,这样显示出的物体才不会被扭曲。 详细说明 03087bf40ad162d9f337731d11dfa9ec8b13cdd1

2.void gluLookAt(GLdouble eyex,GLdouble eyey,GLdouble eyez,GLdouble centerx,GLdouble centery,GLdouble centerz,GLdouble upx,GLdouble upy,GLdouble upz)

该函数定义一个视图矩阵,并与当前矩阵相乘。

第一组 eyex, eyey,eyez 相机在世界坐标的位置

第二组 centerx,centery,centerz 相机镜头对准的物体在世界坐标的位置

第三组 upx,upy,upz 相机向上的方向在世界坐标中的方向

你把相机想象成为你自己的脑袋:

第一组数据就是脑袋的位置

第二组数据就是眼睛看的物体的位置

第三组就是头顶朝向的方向(因为你可以歪着头看同一个物体)

在这里,我们就涉及到一个十分重要的概念,坐标系。 在 3D 绘图中,坐标系是右手系,如果 x 轴指向我们的右侧,那么 y 则指向上侧,z 轴指向屏幕外。如下图所示。 20150606234046 理解了上述两个函数,下面我们来用一个小例子感受一下 运行如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <glut.h>
#include <stdlib.h>


void display(void) {
//清除缓冲区颜色
glClear(GL_COLOR_BUFFER_BIT);
//定义白色
glColor3f(1.0, 1.0, 1.0);
//圆点放坐标中心
glLoadIdentity();
//从哪个地方看
gluLookAt(0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
glutWireTeapot(2);
//清除缓冲区刷新
glutSwapBuffers();
}

void reshape(int w, int h) {
//定义视口大小
glViewport(0, 0, (GLsizei) w, (GLsizei) h);
//投影显示
glMatrixMode(GL_PROJECTION);
//坐标原点在屏幕中心
glLoadIdentity();
//操作模型视景
gluPerspective(60.0, (GLfloat) w/(GLfloat) h, 1.0, 20.0);
glMatrixMode(GL_MODELVIEW);
}

int main(int argc, char** argv) {
//初始化
glutInit(&argc, argv);
//设置显示模式
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB);
//初始化窗口大小
glutInitWindowSize(500, 500);
//定义左上角窗口位置
glutInitWindowPosition(100, 100);
//创建窗口
glutCreateWindow(argv[0]);
//清除颜色缓冲区
glClearColor(0.0, 0.0, 0.0, 0.0);
//显示函数
glutDisplayFunc(display);
//窗口大小改变时的响应
glutReshapeFunc(reshape);
//循环
glutMainLoop();
return 0;
}

运行结果如下 QQ截图20150606234448 在这里,我们定义了

1
gluLookAt(0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);

意思就是把我们的眼睛放到了(0,0,5)的位置,茶壶放到了原点,脑袋顶朝上,即正视茶壶。 如果我们改变了几个参数的值,例如

1
gluLookAt(0.0, 0.0, 5.0, 4.0, 1.0, 0.0, 0.0, 0.0, 1.0);

眼睛位置不变,茶壶位置变为(4,1,0),脑袋顶的方向朝屏幕外侧,那么看到的效果如下。 QQ截图20150606234708 大家可以通过更改这些参数的值来对视图进行变换。

3.glPushMatrix()、glPopMatrix()

这个方法在绘制 3D 图形时依然十分有用,因为我们需要绘制好多个图形,绘制多个不一样的图形必然要经过各种矩阵变换,所以,少不了的利用平移变换放缩的功能,所以为了保证每个图形不受影响,在绘制一个新图形前我们只要调用一下 glPushMatrix 函数,保存当前的变换矩阵,然后绘制完一个图形之后再调用 glPopMatrix 函数来恢复之前的变换矩阵就可以了。 比如,绘制球体

1
2
3
4
glPushMatrix();
glTranslated(x,y,z);
glutWireSphere(R,20,20);
glPopMatrix();

在两个函数中间我们可以进行任意的变换放缩等操作,然后绘制想要的图形即可。

绘图函数

接下来我们说一下一些常用的绘图函数,直接用类库函数来绘制即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void glutWireSphere(GLdouble radius, GLint slices, GLint stacks); 线框球
void glutSolidSphere(GLdouble radius, GLint slices, GLint stacks); 实心球
void glutWireCube(GLdouble size); 线框立方体
void glutSolidCube(GLdouble size); 实心立方体
void glutWireTorus(GLdouble innerRadius, GLdouble outerRadius, GLint nsides, GLint rings); 线框圆环
void glutSolidTorus(GLdouble innerRadius, GLdouble outerRadius, GLint nsides, GLint rings); 实心圆环
void glutWireIcosahedron(void); 线框20面体
void glutSolidIcosahedron(void); 实心20面体
void glutWireOctahedron(void); 线框8面体
void glutSolidOctahedron(void); 实心8面体
void glutWireTetrahedron(void); 线框4面体
void glutSolidTetrahedron(void); 实心4面体
void glutWireDodecahedron(GLdouble radius); 线框12面体
void glutSolidDodecahedron(GLdouble radius); 实心12面体
void glutWireCone(GLdouble radius, GLdouble height, GLint slices, GLint stacks); 线框圆锥体
void glutSolidCone(GLdouble radius, GLdouble height, GLint slices, GLint stacks); 实心圆锥体
void glutWireTeapot(GLdouble size); 线框茶壶
void glutSolidTeapot(GLdouble size); 实心茶壶

利用上面的绘图函数我们便可以方便地实现各种图形的绘制。

结语

那么本节就先告一段落,本节介绍了相关的绘图函数以及几个重要的函数,为接下来我们绘制机器人做下铺垫,希望小伙伴们可以好好理解。

PHP

综述

有时我们在textarea中输入了正常的文本内容,然而如果直接将文本取出来呈现在网页中,原本的换行和回车功能便会失效,完全丢失了原来的样式,这是因为在HTML中的空格和换行分别为 和
,所以我们需要将其替换才能变成我们想要的样式。 注:本篇内容非常基础,仅作参考,不喜勿喷。

替换函数

我们可以定义这样的一个函数,将原来的字符串替换为HTML文本。即把空格和换行替换掉即可

1
2
3
4
5
6
function htmtocode($content) { 
$content = htmlspecialchars($content, ENT_QUOTES);
$content = str_replace(" ", "&nbsp;", $content);
$content = str_replace("\n", "<br>",$content);
return $content;
}

通过上述方法,我们首先将HTML的特殊标记转化,然后替换掉空格和换行即可。

用法

写个最简单的小例子,在textarea中输入任意的文本,存储到数据库,然后再取出来,可以实现textarea输入的效果,完全一样。 数据库名称:demo 数据库表:

1
2
3
4
5
CREATE TABLE IF NOT EXISTS `article` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`text` text NOT NULL,
PRIMARY KEY (`id`)
)

插入数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<head>
<meta charset="utf-8">
</head>
<body>
<form action="" method="post">
<textarea name="text"></textarea>
<input type="submit" name="sub">
</form>
</body>
<?php
$con = mysql_connect("localhost", "root", "");
if (!$con) {
die("数据库连接失败");
}
mysql_select_db("demo",$con);
mysql_query("set names utf8");
$text = @$_POST['text'];
$sql = "insert into article(text) values ('$text')";
echo $sql;
$result = mysql_query($sql,$con);
if ($result) {
echo "录入成功";
} else {
echo "录入失败";
}

?>

查询数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<head>
<meta charset="utf-8">
</head>
<body>
<form action="" method="post">
<textarea name="text"></textarea>
<input type="submit" name="sub">
</form>
</body>
<?php
$con = mysql_connect("localhost", "root", "");
if (!$con) {
die("数据库连接失败");
}
mysql_select_db("demo",$con);
mysql_query("set names utf8");
$text = @$_POST['text'];
$sql = "select * from article";

$result = mysql_query($sql,$con);
if ($result) {
while ($row = mysql_fetch_array($result)) {
echo htmtocode($row['text']);
}
} else {
echo "录入失败";
}



function htmtocode($content) {
$content = htmlspecialchars($content, ENT_QUOTES);
$content = str_replace(" ", "&nbsp;", $content);
$content = str_replace("\n", "<br>",$content);
return $content;
}
?>

嗯,通过上面的方法我们便可以正常地实现输入内容和输出内容的匹配呈现。

JavaScript

前言

今天发现博客评论的默认头像不显示了,博客使用的是多说评论,不知道啥情况,之前设置过一个默认头像,然而今天图片都加载不出来了,可对于我这种强迫症来说,这能忍吗?你能忍我也不能忍啊,这必须不能忍啊,看到一个个的红叉号简直刺透了心。于是,我决定,必须要干掉它!

初步尝试

默认头像不行了,我就重新上传个默认头像对吧,简单粗暴易行。来来来,说试就试! 20150525114559 然而上传完毕之后,它是这个样子滴,依然不给我面子! 20150525114655 你在逗我吗?是不是多说服务器哪里出毛病了?

究其原因

然而,美好的事情总是意想不到的发生,当我知道真相的一刻,整个人都惊呆了。 浏览器里审查一下元素,看它到底引用了神马鬼。 20150525115038 好,我就打开这个链接,惊呆了。简直棒到无极限! 域名已过期!域名已过期!域名已过期!重要的话说三遍!简直溜到不行! 20150525115447 好吧,这真是太棒了!!我还能说些什么!!!

峰回路转

行,既然这样,那我自己提供 CDN,用我自己的七牛云存储! 那就用 jQuery 好了,方便简单,在</head>标签前面加上这么个代码,来试试。

1
2
3
4
5
$(document).ready(function(){
$('.ds-avatar img[src*="cdncache"]').each(function(){
$(this).attr("src",'http://cdn.cuiqingcai.com/wp-content/uploads/2015/05/20150525112155.jpg');
})
});

意思是,在页面加载完成之后,把 src 里面包含 cdncache 的多说头像 img 替换掉。 这句话最好加在所有 JS 的最后面,要不然可能不会生效。JS 执行是有先后顺序的。 这样,我们就会发现,头像已经奇迹般地发生了变化。 20150525123151 本身有头像的仍然是那个头像,使用默认头像的已经替换了新头像。

更上一层

然而这并不能满足我的愿望啊,我想要多变的头像样式,就是这么任性! 来来来,改代码,我再传上几个头像,让默认头像的孩纸们随机地使用我的头像,一定是炫爆了!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script type="text/javascript">
$(document).ready(function(){
var imgs = new Array();
imgs[0] = 'http://cdn.cuiqingcai.com/wp-content/uploads/2015/05/20150525111154.jpg';
imgs[1] = 'http://cdn.cuiqingcai.com/wp-content/uploads/2015/05/20150525111447.jpg';
imgs[2] = 'http://cdn.cuiqingcai.com/wp-content/uploads/2015/05/20150525112058.jpg';
imgs[3] = 'http://cdn.cuiqingcai.com/wp-content/uploads/2015/05/20150525112112.jpg';
imgs[4] = 'http://cdn.cuiqingcai.com/wp-content/uploads/2015/05/20150525112129.jpg';
imgs[5] = 'http://cdn.cuiqingcai.com/wp-content/uploads/2015/05/20150525112155.jpg';
$('.ds-avatar img[src*="cdncache"]').each(function(){
var rand = Math.floor(Math.random()*imgs.length);
$(this).attr("src",imgs[rand]);
})
});
</script>

嗯,把刚才代码优化成这样,我们就可以发现,整个网站的头像就被替换成我们想要的效果了! 20150525123537 简直酷炫!小伙伴们可以尝试一下!

C/C++

综述

在上一篇文章中我们已经实现了图形的取点绘制,接下来我们还要实现的功能有图形的删除、移动以及存盘读盘功能。 概述如下

  • 鼠标点击某个绘制的图形,将其删除
  • 鼠标拖动某个图形,可以对其随意拖拽改变位置
  • 点击存盘,将图形的位置及颜色保存到文件
  • 点击读盘,将图形的位置颜色从文件中读取出来并重新绘制

好,接下来我们在上一篇的基础上进行一步步的说明。

图形删除

首先像之前一样,我们定义一个删除的按钮,指定好颜色。

1
GLubyte delBtn[3] = {30, 30, 30};

在 display 函数中绘制一个删除的按钮

1
glColorCircle(220, 280, 10, delBtn);

顶部变变成了这个样子,其中最左边的那个按钮便是删除按钮 QQ截图20150511124225 接下来我们想一下删除的实现。 首先我们点击删除按钮之后要设置一下 drawStatus,比如设置为 2 代表删除。 然后鼠标点击下方任意位置时,如果当前 drawStatus 为 2,如果鼠标处在一个多边形内部,我们就把它删除掉。 那么就会出现两个问题了,怎样判断点击的点处在多边形的内部?怎样执行删除操作? 下面我们分别来研究一下

1. 判断点在多边形内部

思路:求解通过该点的水平线与多边形各边的交点 ,如果交点为奇数,那么就判定为在多边形内部,否则在外部。 因此,我的算法实现如下,传入的参数一个是 ploygon 结构体,结构体中包含了顶点坐标,另外就是这个点的 x,y 坐标。 思路参考来源: 判断点在多边形内的多种写法 经过我的改写,变成了如下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 判断一个点是否在多边形内部 */
bool pointInPoly(polygon poly, int x, int y){
int nCross = 0;
for (int i = 0; i < poly.verNum; i++) {
int p1x = poly.x[i];
int p1y = poly.y[i];
int p2x = poly.x[(i + 1) % poly.verNum];
int p2y = poly.y[(i + 1) % poly.verNum];
/* 平行或在延长线上,忽略 */
if (p1y == p2y || y < min(p1y, p2y) || y > max(p1y, p2y)) {
continue;
}
/* 计算交点横坐标 */
double m = (double)(y - p1y) * (double)(p2x - p1x) / (double)(p2y - p1y) + p1x;
if ( m > x ) nCross++;
}
return (nCross % 2 == 1);
}

以上便是判断在多边形内部的算法。

2.删除图形实现

好了,有了上面的算法,我们就依次查找我们声明的 polygons 数组,找到第一个符合要求的多边形结构体 polygon 返回它的索引。 这个方法就是传入了点击点的位置 x,y 坐标,然后判断是在哪个多边形内,然后移除之。如果没有找到,则不执行删除操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 删除多边形 */
void deletePoly(int x, int y) {
int delenum = -1;
for (int i = 0; i< con; i++) {
if (pointInPoly(polygons[i], x, y)) {
delenum = i;
}
}
if (delenum != -1) {
removePoly(delenum);
}
/* 重新绘制 */
glutPostRedisplay();
}

其中 removePoly(delenum) 就是通过传入这个结构体的索引,然后将它从 polygons 里面删除即可,方法实现如下

1
2
3
4
5
6
7
8
9
10
11
/* 删除多边形 */
void removePoly(int index)
{
if (index>con) {
printf("Out Of Index\n");
}
for (int i = index; i < con; i++) {
polygons[i] = polygons[i+1];
}
con--;
}

其实就是相当于从数组中移除一个元素,然后后面的元素依次前移。 恩好了,准备工作都做好了,我们只需要在点击了删除按钮之后,调用一下 deletePoly 函数即可。 把原来的 mouseClick 方法改写如下即可,代码较长,不完全展开,双击查看完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/*鼠标点击事件 */
void mouseClick(int btn, int state, int x, int y)
{
/* 选项卡的分界范围 */
int optionHeight = 40;
int optionWidth = 250;
if (btn == GLUT_LEFT_BUTTON && state == GLUT_DOWN)
{
/* 如果在颜色选项卡中点击 */
if (y < optionHeight && x < optionWidth)
{
glReadPixels(x, 2 * halfHeight - y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &nowColor);
printf("r:%d,g:%d,b:%d\n", nowColor[0], nowColor[1], nowColor[2]);
/* 设置绘图颜色并显示当前取色板颜色 */
glColorCircle(-180, 280, 10, nowColor);
/* 如果点击的是右侧选项按钮 */
}else if (y < optionHeight && x > optionWidth) {
//取当前的点
GLubyte color[3];
glReadPixels(x, 2 * halfHeight - y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &color);
printf("r:%d,g:%d,b:%d\n", color[0], color[1], color[2]);
/* 如果点击了开始绘制的按钮 */
if (sameColor(color, startBtn)) {
drawStatus = 0;
/* 开始画一个图形,顶点个数置零 */
polygons[con].verNum = 0;
printf("drawStatus:%d\n", drawStatus);
/* 如果点击了结束绘制的按钮 */
} else if (sameColor(color, endBtn)) {
glutPostRedisplay();
/* 画的图形个数加一 */
con++;
printf("drawStatus:%d\n", drawStatus);
} else if (sameColor(color, delBtn)) {
/* 删除时设置drawStatus为2 */
drawStatus = 2;
printf("drawStatus:%d\n", drawStatus);
}

/* 如果点击的是下方的绘图页面 */
}else{
/* 如果当前是正在取点状态 */
if (drawStatus == 0)
{
/* 保存每个点,然后该图形顶点个数加一 */
polygons[con].x[polygons[con].verNum] = x;
polygons[con].y[polygons[con].verNum] = y;
/* 画点 */
glPoints(x, y);
/* 设置当前颜色RGB,如果取色有变动,以最后的取色为准 */
polygons[con].color[0] = nowColor[0];
polygons[con].color[1] = nowColor[1];
polygons[con].color[2] = nowColor[2];
printf("polyColor%d%d%d\n", polygons[con].color[0],polygons[con].color[1],polygons[con].color[2]);
polygons[con].verNum++;
printf("con:%d,verNum:%d\n", con, polygons[con].verNum);
}else if (drawStatus == 2) {
/* 删除图形 */
deletePoly(x, y);
}
}
}
}

经过上面的步骤,我们便可以很方便地实现图形的删除了。运行结果如下

[embed]http://cdn.cuiqingcai.com/robot2.mp4[/embed]

嗯,就是这样,现在删除就大功告成了。

移动图形

像之前一样,定义一个移动图形的按钮。在此不再赘述

1
GLubyte moveBtn[3] = {40, 40, 40};
1
glColorCircle(190, 280, 10, moveBtn);

那么图形移动怎么实现?思路如下 首先利用判断点击的点是否在多边形内部的算法,找到需要移动的多边形。然后在监听鼠标移动的事件中设置这个多边形各个顶点的相对移动距离等于鼠标相对移动距离即可。 首先,移动的 drawSatus 我们设置为 3,在 mouseClick 方法中加入如下部分,在哪加你应该已经知道了。

1
2
3
4
5
else if (sameColor(color, moveBtn)) {
/* 移动时设置drawStatus为3 */
drawStatus = 3;
printf("drawStatus:%d\n", drawStatus);
}

在 mouseMove 方法中(另一个监听鼠标移动的事件)中首先判断当前的 drawStatus 如果为 3,那么就执行移动操作。 加入如下的方法即可。

1
2
3
4
5
6
7
8
/* 鼠标移动事件 */
void mouseMove(int x, int y)
{
if(drawStatus == 3 && moveNum != -1){
printf("move x:%d,y:%dmoveNum:%d\n", x, y,moveNum);
movePoly(x, y);
}
}

这里两点需要说明,moveNum 就是我们需要移动的图形的索引,定义为了全局变量。在鼠标点击时,首先判定点击点在哪个图形中,然而因为有两个鼠标事件(一个是点击,一个是移动),所以就把这个变量定义为全局变量,来标记该移动哪个图形。moveNum 的初始值为-1,如果为-1,那么就代表没有选中任何图形,不移动。如果不是-1,就进行移动。 其次,movePloy 方法实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 移动多边形 */
void movePoly(int x, int y) {
/* 取到要移动的图形 */
int verNum = polygons[moveNum].verNum;
printf("verNum%d,x%d,y%d,clickX%d,clickY%d\n",verNum,x,y,clickX,clickY);
for (int i = 0; i < verNum; i++) {
polygons[moveNum].x[i] += (x - clickX);
polygons[moveNum].y[i] += (y - clickY);
printf("polyx%d\n",polygons[moveNum].x[i]);
}
/* 移动完毕之后重新记录当前的点 */
clickX = x;
clickY = y;
/* 重新绘制 */
glutPostRedisplay();
}

这个方法的实现有一部分需要说明,首先是 clickX,clickY。 因为在移动时移动的是相对距离。所以,鼠标每移动一个像素,我们就要计算下一步鼠标移动的位置和上一个位置的差值,所以每次移动我们都需要标记一下点击的位置。所以上面的方法中才有了

1
2
3
/* 移动完毕之后重新记录当前的点 */
clickX = x;
clickY = y;

然后,在图形移动时,我们需要移动的是相对位置。所以我们将一个图形的所有顶点进行遍历,加上鼠标移动的相对位置即可。就是如下的实现

1
2
3
4
for (int i = 0; i < verNum; i++) {
polygons[moveNum].x[i] += (x - clickX);
polygons[moveNum].y[i] += (y - clickY);
}

另外,mouseClick 方法中我们需要调用一下即可。mouseClick 方法现在改写如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
/*鼠标点击事件 */
void mouseClick(int btn, int state, int x, int y)
{
/* 赋值全局变量 */
clickX = x;
clickY = y;
/* 选项卡的分界范围 */
int optionHeight = 40;
int optionWidth = 250;
if (btn == GLUT_LEFT_BUTTON && state == GLUT_DOWN)
{
/* 如果在颜色选项卡中点击 */
if (y < optionHeight && x < optionWidth)
{
glReadPixels(x, 2 * halfHeight - y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &nowColor);
printf("r:%d,g:%d,b:%d\n", nowColor[0], nowColor[1], nowColor[2]);
/* 设置绘图颜色并显示当前取色板颜色 */
glColorCircle(-180, 280, 10, nowColor);
/* 如果点击的是右侧选项按钮 */
}else if (y < optionHeight && x > optionWidth) {
//取当前的点
GLubyte color[3];
glReadPixels(x, 2 * halfHeight - y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &color);
printf("r:%d,g:%d,b:%d\n", color[0], color[1], color[2]);
/* 如果点击了开始绘制的按钮 */
if (sameColor(color, startBtn)) {
drawStatus = 0;
/* 开始画一个图形,顶点个数置零 */
polygons[con].verNum = 0;
printf("drawStatus:%d\n", drawStatus);
/* 如果点击了结束绘制的按钮 */
} else if (sameColor(color, endBtn)) {
glutPostRedisplay();
/* 画的图形个数加一 */
con++;
printf("drawStatus:%d\n", drawStatus);
} else if (sameColor(color, delBtn)) {
/* 删除时设置drawStatus为2 */
drawStatus = 2;
printf("drawStatus:%d\n", drawStatus);
} else if (sameColor(color, moveBtn)) {
/* 移动时设置drawStatus为3 */
drawStatus = 3;
printf("drawStatus:%d\n", drawStatus);
}

/* 如果点击的是下方的绘图页面 */
}else{
/* 如果当前是正在取点状态 */
if (drawStatus == 0)
{
/* 保存每个点,然后该图形顶点个数加一 */
polygons[con].x[polygons[con].verNum] = x;
polygons[con].y[polygons[con].verNum] = y;
/* 画点 */
glPoints(x, y);
/* 设置当前颜色RGB,如果取色有变动,以最后的取色为准 */
polygons[con].color[0] = nowColor[0];
polygons[con].color[1] = nowColor[1];
polygons[con].color[2] = nowColor[2];
printf("polyColor%d%d%d\n", polygons[con].color[0],polygons[con].color[1],polygons[con].color[2]);
polygons[con].verNum++;
printf("con:%d,verNum:%d\n", con, polygons[con].verNum);
}else if (drawStatus == 2) {
/* 删除图形 */
deletePoly(x, y);
} else if(drawStatus == 3) {
moveNum = -1;
for (int i = 0; i< con; i++) {
if (pointInPoly(polygons[i], x, y)) {
moveNum = i;
}
}
}
}
}
}

好,现在就大功告成了,看一下运行结果吧

[embed]http://cdn.cuiqingcai.com/robot3.mp4[/embed]

现在完整的代码如下,代码过长,不完全显示,双击查看完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
#include <glut.h>
#include <math.h>
#include <stdio.h>
#include <iostream>

using namespace std;

#define PI 3.1415926
/* 每个图形最多的顶点数 */
#define MAX_VERTEX 10
/* 画的图形最多的个数 */
#define MAX_PLOY 10
/* 窗口长宽的一半 */
int halfWidth, halfHeight;
/* 绘制多边形的起始标志,0是开始绘制,1是结束绘制,初始为-1 */
int drawStatus = -1;
int moveNum = -1;
/* 定义点击点的x,y*/
int clickX, clickY;
/* 多边形结构体 */
struct polygon {
/* 顶点坐标 */
int x[MAX_VERTEX];
int y[MAX_VERTEX];
/* 定义第几个顶点 */
int verNum;
GLubyte color[3];
};
/* 各种颜色 */
GLubyte border[3] = { 0, 0, 0 };
GLubyte grey[3] = { 195, 195, 195 };
GLubyte yellow[3] = { 255, 243, 0 };
GLubyte red[3] = { 237, 28, 36 };
GLubyte darkGrey[3] = { 126, 126, 126 };
GLubyte white[3] = { 255, 255, 255 };
GLubyte startBtn[3] = { 10, 10, 10 };
GLubyte endBtn[3] = { 20, 20, 20 };
GLubyte delBtn[3] = { 30, 30, 30 };
GLubyte moveBtn[3] = { 40, 40, 40 };
/* 当前颜色 */
GLubyte nowColor[3] = { 0, 0, 0 };
/* 声明多边形数组 */
polygon polygons[MAX_PLOY];
/* 记录画了几个多边形 */
int con = 0;

/* 绘制多边形 */
void glPolygons()
{
if ( con >= 0 )
{
for ( int i = 0; i <= con; i++ )
{
/* 取到这个多边形 */
polygon poly = polygons[i];
/* 画封闭线 */
glBegin( GL_LINE_LOOP );
int num = poly.verNum;
printf( "num:%d\n", num );
for ( int j = 0; j < num; j++ )
{
glColor3ub( poly.color[0], poly.color[1], poly.color[2] );
glVertex2d( poly.x[j] - halfWidth, halfHeight - poly.y[j] );
printf( "polyx:%d,polyy:%d", poly.x[j], poly.y[j] );
}
/* 结束画线 */
glEnd();
/* 刷新 */
glFlush();
}
}
}


/* 绘制填充的圆形 */
void glColorCircle( int x, int y, int R, GLubyte color[3] )
{
/* 开始绘制曲线 */
glBegin( GL_POLYGON );
/* 设置颜色 */
glColor3ub( color[0], color[1], color[2] );
/* 每次画增加的弧度 */
double delta_angle = PI / 180;
/* 画圆弧 */
for ( double i = 0; i <= 2 * PI; i += delta_angle )
{
/* 绝对定位加三角函数值 */
double vx = x + R * cos( i );
double vy = y + R*sin( i );
glVertex2d( vx, vy );
}
/* 结束绘画 */
glEnd();
glFlush();
}


/* 画矩形,传入的是左下角XY坐标和右上角XY坐标 */
void glRect( int leftX, int leftY, int rightX, int rightY )
{
/* 画封闭曲线 */
glBegin( GL_LINE_LOOP );
/* 左下角 */
glVertex2d( leftX, leftY );
/* 右下角 */
glVertex2d( rightX, leftY );
/* 右上角 */
glVertex2d( rightX, rightY );
/* 左上角 */
glVertex2d( leftX, rightY );
/* 结束画线 */
glEnd();
}


/* 画圆角矩形,传入矩形宽高,角半径,矩形中心点坐标 */
void glRoundRec( int centerX, int centerY, int width, int height, float cirR )
{
/* 二分之PI,一个象限的角度 */
float PI_HALF = PI / 2;
/* 划分程度,值越大画得越精细 */
float divide = 20.0;
/* 圆角矩形的坐标 */
float tx, ty;
/* 画封闭曲线 */
glBegin( GL_LINE_LOOP );
/* 四个象限不同的操作符 */
int opX[4] = { 1, -1, -1, 1 };
int opY[4] = { 1, 1, -1, -1 };
/* 用来计数,从第一象限到第四象限 */
float x = 0;
/* x自增时加的值 */
float part = 1 / divide;
/* 计算内矩形宽高一半的数值 */
int w = width / 2 - cirR;
int h = height / 2 - cirR;
/* 循环画线 */
for ( x = 0; x < 4; x += part )
{
/* 求出弧度 */
float rad = PI_HALF * x;
/* 计算坐标值 */
tx = cirR * cos( rad ) + opX[(int) x] * w + centerX;
ty = cirR * sin( rad ) + opY[(int) x] * h + centerY;
/*传入坐标画线 */
glVertex2f( tx, ty );
}
/* 结束画线 */
glEnd();
}


/* 画弧线,相对偏移量XY,开始的弧度,结束的弧度,半径 */
void glArc( double x, double y, double start_angle, double end_angle, double radius )
{
/* 开始绘制曲线 */
glBegin( GL_LINE_STRIP );
/* 每次画增加的弧度 */
double delta_angle = PI / 180;
/* 画圆弧 */
for ( double i = start_angle; i <= end_angle; i += delta_angle )
{
/* 绝对定位加三角函数值 */
double vx = x + radius * cos( i );
double vy = y + radius*sin( i );
glVertex2d( vx, vy );
}
/* 结束绘画 */
glEnd();
}


/* 画圆 */
void glCircle( double x, double y, double radius )
{
/* 画全圆 */
glArc( x, y, 0, 2 * PI, radius );
}


/* 画三角形,传入三个点的坐标 */
void glTri( int x1, int y1, int x2, int y2, int x3, int y3 )
{
/* 画封闭线 */
glBegin( GL_LINE_LOOP );
/* 一点 */
glVertex2d( x1, y1 );
/* 二点 */
glVertex2d( x2, y2 );
/*三点 */
glVertex2d( x3, y3 );
/* 结束画线 */
glEnd();
}


/* 画线,传入两点坐标 */
void glLine( int x1, int y1, int x2, int y2 )
{
/* 画封闭线 */
glBegin( GL_LINE_STRIP );
/* 一点 */
glVertex2d( x1, y1 );
/* 二点 */
glVertex2d( x2, y2 );
/* 结束画线 */
glEnd();
}


/* 函数用来画图 */
void display( void )
{
/* GL_COLOR_BUFFER_BIT表示清除颜色 */
glClear( GL_COLOR_BUFFER_BIT );
/* 设置画线颜色 */
glColor3f( 0.5, 0.5, 0.5 );
/* 画点大小 */
glPointSize( 2 );
/* 画圆角矩形,大肚子 */
glRoundRec( 0, 0, 146, 120, 15 );
/* 画圆,中间小圈 */
glCircle( 0, 0, 10 );
/* 画矩形,脖子 */
glRect( -25, 60, 25, 76 );
/* 画圆角矩形,大脸 */
glRoundRec( 0, 113, 128, 74, 10 );
/* 两个眼睛 */
glCircle( -30, 111, 10 );
glCircle( 30, 111, 10 );
/* 两条天线 */
glLine( -35, 150, -35, 173 );
glLine( 35, 150, 35, 173 );
/* 圆角矩形,两个耳朵 */
glRoundRec( 81, 115, 20, 34, 5 );
glRoundRec( -81, 115, 20, 34, 5 );
/* 圆弧,画嘴 */
glArc( 0, 133, 11 * PI / 8, 13 * PI / 8, 45 );
/* 画三角,肚子里的三角 */
glTri( -30, -15, 30, -15, 0, 28 );
/* 画矩形,胳膊连接处 */
glRect( -81, 43, -73, 25 );
glRect( 81, 43, 73, 25 );
/* 画矩形,上臂 */
glRect( -108, 45, -81, 0 );
glRect( 108, 45, 81, 0 );
/* 画矩形,中臂 */
glRect( -101, 0, -88, -4 );
glRect( 101, 0, 88, -4 );
/* 画矩形,下臂 */
glRect( -108, -4, -81, -37 );
glRect( 108, -4, 81, -37 );
/* 画圆形,手掌 */
glCircle( -95, -47, 10 );
glCircle( 95, -47, 10 );

/* 绘制取色盘 */
glColorCircle( -280, 280, 10, red );
glColorCircle( -250, 280, 10, yellow );
glColorCircle( -220, 280, 10, grey );
glColorCircle( -180, 280, 10, nowColor );
glColorCircle( 250, 280, 10, startBtn );
glColorCircle( 280, 280, 10, endBtn );
glColorCircle( 220, 280, 10, delBtn );
glColorCircle( 190, 280, 10, moveBtn );
/* 保证前面的OpenGL命令立即执行,而不是让它们在缓冲区中等待 */
/* 绘制多边形 */
glPolygons();
glFlush();
}


/* 窗口大小变化时调用的函数 */
void ChangeSize( GLsizei w, GLsizei h )
{
/* 避免高度为0 */
if ( h == 0 )
{
h = 1;
}
/* 定义视口大小,宽高一致 */
glViewport( 0, 0, w, h );
int half = 300;
/* 定义宽高的一半 */
halfHeight = 300;
halfWidth = 300;
/* 重置坐标系统,使投影变换复位 */
glMatrixMode( GL_PROJECTION );
/* 将当前的用户坐标系的原点移到了屏幕中心 */
glLoadIdentity();
/* 定义正交视域体 */
if ( w < h )
{
/* 如果高度大于宽度,则将高度视角扩大,图形显示居中 */
glOrtho( -half, half, -half * h / w, half * h / w, -half, half );
} else {
/* 如果宽度大于高度,则将宽度视角扩大,图形显示居中 */
glOrtho( -half * w / h, half * w / h, -half, half, -half, half );
}
}


/* 判断两个颜色是否相等 */
bool sameColor( GLubyte color1[3], GLubyte color2[3] )
{
if ( color1[0] - color2[0] < 5 && color1[1] - color2[1] < 5 && color1[2] - color2[1] < 5 )
{
return(true);
} else {
return(false);
}
}


/* 画点 */
void glPoints( int x, int y )
{
glBegin( GL_POINTS );
/* 点直接设置为黑色 */
glColor3ub( 0, 0, 0 );
glVertex2d( x - halfWidth, halfHeight - y );
glEnd();
glFlush();
}


/* 判断一个点是否在多边形内部 */
bool pointInPoly( polygon poly, int x, int y )
{
int nCross = 0;
for ( int i = 0; i < poly.verNum; i++ )
{
int p1x = poly.x[i];
int p1y = poly.y[i];
int p2x = poly.x[(i + 1) % poly.verNum];
int p2y = poly.y[(i + 1) % poly.verNum];
/* 平行或在延长线上,忽略 */
if ( p1y == p2y || y < min( p1y, p2y ) || y > max( p1y, p2y ) )
{
continue;
}
/* 计算交点横坐标 */
double m = (double) (y - p1y) * (double) (p2x - p1x) / (double) (p2y - p1y) + p1x;
if ( m > x )
nCross++;
}
return(nCross % 2 == 1);
}


/* 移动多边形 */
void movePoly( int x, int y )
{
/* 取到要移动的图形 */
int verNum = polygons[moveNum].verNum;
printf( "verNum%d,x%d,y%d,clickX%d,clickY%d\n", verNum, x, y, clickX, clickY );
for ( int i = 0; i < verNum; i++ )
{
polygons[moveNum].x[i] += (x - clickX);
polygons[moveNum].y[i] += (y - clickY);
printf( "polyx%d\n", polygons[moveNum].x[i] );
}
/* 移动完毕之后重新记录当前的点 */
clickX = x;
clickY = y;
/* 重新绘制 */
glutPostRedisplay();
}


/* 删除多边形 */
void removePoly( int index )
{
if ( index > con )
{
printf( "Out Of Index\n" );
}
for ( int i = index; i < con; i++ )
{
polygons[i] = polygons[i + 1];
}
con--;
}


/* 删除多边形 */
void deletePoly( int x, int y )
{
int delenum = -1;
for ( int i = 0; i < con; i++ )
{
if ( pointInPoly( polygons[i], x, y ) )
{
delenum = i;
}
}
if ( delenum != -1 )
{
removePoly( delenum );
}
/* 重新绘制 */
glutPostRedisplay();
}


/*鼠标点击事件 */
void mouseClick( int btn, int state, int x, int y )
{
/* 赋值全局变量 */
clickX = x;
clickY = y;
/* 选项卡的分界范围 */
int optionHeight = 40;
int optionWidth = 250;
if ( btn == GLUT_LEFT_BUTTON && state == GLUT_DOWN )
{
/* 如果在颜色选项卡中点击 */
if ( y < optionHeight && x < optionWidth )
{
glReadPixels( x, 2 * halfHeight - y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &nowColor );
printf( "r:%d,g:%d,b:%d\n", nowColor[0], nowColor[1], nowColor[2] );
/* 设置绘图颜色并显示当前取色板颜色 */
glColorCircle( -180, 280, 10, nowColor );
/* 如果点击的是右侧选项按钮 */
}else if ( y < optionHeight && x > optionWidth )
{
/* 取当前的点 */
GLubyte color[3];
glReadPixels( x, 2 * halfHeight - y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &color );
printf( "r:%d,g:%d,b:%d\n", color[0], color[1], color[2] );
/* 如果点击了开始绘制的按钮 */
if ( sameColor( color, startBtn ) )
{
drawStatus = 0;
/* 开始画一个图形,顶点个数置零 */
polygons[con].verNum = 0;
printf( "drawStatus:%d\n", drawStatus );
/* 如果点击了结束绘制的按钮 */
} else if ( sameColor( color, endBtn ) )
{
glutPostRedisplay();
/* 画的图形个数加一 */
con++;
printf( "drawStatus:%d\n", drawStatus );
} else if ( sameColor( color, delBtn ) )
{
/* 删除时设置drawStatus为2 */
drawStatus = 2;
printf( "drawStatus:%d\n", drawStatus );
} else if ( sameColor( color, moveBtn ) )
{
/* 移动时设置drawStatus为3 */
drawStatus = 3;
printf( "drawStatus:%d\n", drawStatus );
}

/* 如果点击的是下方的绘图页面 */
}else{
/* 如果当前是正在取点状态 */
if ( drawStatus == 0 )
{
/* 保存每个点,然后该图形顶点个数加一 */
polygons[con].x[polygons[con].verNum] = x;
polygons[con].y[polygons[con].verNum] = y;
/* 画点 */
glPoints( x, y );
/* 设置当前颜色RGB,如果取色有变动,以最后的取色为准 */
polygons[con].color[0] = nowColor[0];
polygons[con].color[1] = nowColor[1];
polygons[con].color[2] = nowColor[2];
printf( "polyColor%d%d%d\n", polygons[con].color[0], polygons[con].color[1], polygons[con].color[2] );
polygons[con].verNum++;
printf( "con:%d,verNum:%d\n", con, polygons[con].verNum );
}else if ( drawStatus == 2 )
{
/* 删除图形 */
deletePoly( x, y );
} else if ( drawStatus == 3 )
{
moveNum = -1;
for ( int i = 0; i < con; i++ )
{
if ( pointInPoly( polygons[i], x, y ) )
{
moveNum = i;
}
}
}
}
}
}


/* 鼠标移动事件 */
void mouseMove( int x, int y )
{
if ( drawStatus == 3 && moveNum != -1 )
{
printf( "move x:%d,y:%dmoveNum:%d\n", x, y, moveNum );
movePoly( x, y );
}
}


/*程序入口 */
int main( int argc, char *argv[] )
{
/* 对GLUT进行初始化,并处理所有的命令行参数 */
glutInit( &argc, argv );
/* 指定RGB颜色模式和单缓冲窗口 */
glutInitDisplayMode( GLUT_RGB | GLUT_SINGLE );
/* 定义窗口的位置 */
glutInitWindowPosition( 100, 100 );
/* 定义窗口的大小 */
glutInitWindowSize( 600, 600 );
/* 创建窗口,同时为之命名 */
glutCreateWindow( "OpenGL" );
/* 设置窗口清除颜色为白色 */
glClearColor( 1.0f, 1.0f, 1.0f, 1.0f );
/* 参数为一个函数,绘图时这个函数就会被调用 */
glutDisplayFunc( &display );
/* 参数为一个函数,当窗口大小改变时会被调用 */
glutReshapeFunc( ChangeSize );
/*鼠标点击事件,鼠标点击或者松开时调用 */
glutMouseFunc( mouseClick );
/*鼠标移动事件,鼠标按下并移动时调用 */
glutMotionFunc( mouseMove );
/* 该函数让GLUT框架开始运行,所有设置的回调函数开始工作,直到用户终止程序为止 */
glutMainLoop();
/*程序返回 */
return(0);
}

以上,就可以实现图形的移动和删除了。

存盘和读盘

这个很简单,存盘我们只需要把所有图形的位置以及颜色输出到一个文本中,然后在读盘时,把文本中所有内容读入并存储到多边形结构体数组中,然后重新绘制即可。 代码的讲解直接写在注释中,比较好理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/* 保存所有的图形 */
void savePolygons()
{
FILE *fp;
int i;
if ((fp = fopen("shapes.txt", "w")) == NULL)
{
printf("不能打开文件!");
return;
}
/* 保存一共多少个图形 */
fprintf(fp, "%d", con);
fprintf(fp, "\n");
for (i = 0; i < con; i++){
/* 保存颜色值 */
fprintf(fp, "%d %d %d ", polygons[i].color[0], polygons[i].color[1], polygons[i].color[2]);
/* 保存顶点数量 */
fprintf(fp, "%d ", polygons[i].verNum);
/* 保存顶点坐标 */
for (int j = 0; j < polygons[i].verNum; j++)
fprintf(fp, "%d ", polygons[i].x[j]);
for (int j = 0; j < polygons[i].verNum; j++)
fprintf(fp, "%d ", polygons[i].y[j]);
fprintf(fp, "\n");
}
fclose(fp);
printf("保存成功。\n");
}

/* 读取存档 */
void readPolygons()
{
FILE *fp;
int i;
if ((fp = fopen("shapes.txt", "r")) == NULL)
{
printf("不能打开文件!");
return;
}
fscanf(fp, "%d", &con);
printf("count%d",con);
fscanf(fp, "\n");
for (i = 0; i < con; i++){
/* 读取颜色值 */
fscanf(fp, "%d %d %d ", &polygons[i].color[0], &polygons[i].color[1], &polygons[i].color[2]);
/* 读取顶点数量 */
fscanf(fp, "%d ", &polygons[i].verNum);
/* 读取顶点坐标 */
for (int j = 0; j < polygons[i].verNum; j++)
fscanf(fp, "%d ", &polygons[i].x[j]);
for (int j = 0; j < polygons[i].verNum; j++)
fscanf(fp, "%d ", &polygons[i].y[j]);
fscanf(fp, "\n");
}
fclose(fp);
printf("读取成功。\n");

}

我们只需要再定义两个按钮,再 display 中画上这俩按钮

1
2
GLubyte storeBtn[3] = {50, 50, 50};
GLubyte loadBtn[3] = {55, 55, 55};

然后把 mouseClick 方法改写一下就好了。最终 mouseClick 方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/* 鼠标点击事件 */
void mouseClick( int btn, int state, int x, int y )
{
/* 赋值全局变量 */
clickX = x;
clickY = y;
/* 选项卡的分界范围 */
int optionHeight = 40;
int optionWidth = 250;
if ( btn == GLUT_LEFT_BUTTON && state == GLUT_DOWN )
{
/* 如果在颜色选项卡中点击 */
if ( y < optionHeight && x < optionWidth )
{
glReadPixels( x, 2 * halfHeight - y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &nowColor );
/* 设置绘图颜色并显示当前取色板颜色 */
glColorCircle( -180, 280, 10, nowColor );
/* 如果点击的是右侧选项按钮 */
}else if ( y < optionHeight && x > optionWidth )
{
/* 取当前的点 */
GLubyte color[3];
glReadPixels( x, 2 * halfHeight - y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &color );
/* 如果点击了开始绘制的按钮 */
if ( sameColor( color, startBtn ) )
{
drawStatus = 0;
/* 开始画一个图形,顶点个数置零 */
polygons[con].verNum = 0;
/* 如果点击了结束绘制的按钮 */
} else if ( sameColor( color, endBtn ) )
{
glutPostRedisplay();
/* 画的图形个数加一 */
con++;
} else if ( sameColor( color, delBtn ) )
{
/* 删除时设置drawStatus为2 */
drawStatus = 2;
} else if ( sameColor( color, moveBtn ) )
{
/* 移动时设置drawStatus为3 */
drawStatus = 3;
} else if ( sameColor( color, storeBtn ) )
{
/* 存盘时设置drawStatus为4 */
drawStatus = 4;
glutPostRedisplay();
savePolygons();
} else if ( sameColor( color, loadBtn ) )
{
/* 读盘时设置drawStatus为5 */
drawStatus = 5;
readPolygons();
glutPostRedisplay();
}

/* 如果点击的是下方的绘图页面 */
}else{
/* 如果当前是正在取点状态 */
if ( drawStatus == 0 )
{
/* 保存每个点,然后该图形顶点个数加一 */
polygons[con].x[polygons[con].verNum] = x;
polygons[con].y[polygons[con].verNum] = y;
/* 画点 */
glPoints( x, y );
/* 设置当前颜色RGB,如果取色有变动,以最后的取色为准 */
polygons[con].color[0] = nowColor[0];
polygons[con].color[1] = nowColor[1];
polygons[con].color[2] = nowColor[2];
polygons[con].verNum++;
}else if ( drawStatus == 2 )
{
/* 删除图形 */
deletePoly( x, y );
} else if ( drawStatus == 3 )
{
moveNum = -1;
for ( int i = 0; i < con; i++ )
{
if ( pointInPoly( polygons[i], x, y ) )
{
moveNum = i;
}
}
}
}
}
}

大功告成!现在只要你存盘成功,不管对图形进行怎样的移动,删除操作。只要再点击读盘,就可以恢复到存盘时的状态,是不是非常酷炫。 大家可以尝试一下!

总结

本篇文章说明了图形的删除,移动以及存盘读盘的功能。代码实现仅仅是我个人的思路,当然实现方式不止这一种,欢迎大家创新。 部分思路如果大家有更好的解决方法,欢迎同我交流,非常感谢。 总之希望对大家有帮助!小伙伴们加油!

C/C++

综述

在上一节我们学习了鼠标监听事件,在这里我们就利用它来做一个实例,对鼠标监听事件进行一个综合的应用。

要求

1. 绘制如下的机器人,并在此基础上进行创作 图片1 2. 对象创建:支持用户利用鼠标指定各个顶点位置,补充完整机器人的腿部。 3. 对象删除:支持用户选择一个腿部的多边形(与你的多边形保存的数据结构有关)并删除。 4. 对象存储:设计一种数据结构存储每个多边形的顶点与边,支持文件存盘,并支持读盘复原。 5. 支持用户选择多边形的颜色,支持用户移动多边形

成果

在这里为了动态演示,将我的成果以视频形式演示如下

[embed]http://cdn.cuiqingcai.com/robot.mp4[/embed]

好,欣赏完视频之后是不是非常想知道是怎样实现的呢?下面我们来一步一步进行讲解。

绘制半机器人

在前面的几篇中,已经详细描述了绘制机器人的方法,在这里我们简单贴一下代码来绘制一个半机器人。 直接上代码吧,不多说了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
#include <glut.h>
#include <math.h>
#define PI 3.1415926

/* 画矩形,传入的是左下角XY坐标和右上角XY坐标 */
void glRect( int leftX, int leftY, int rightX, int rightY )
{
/* 画封闭曲线 */
glBegin( GL_LINE_LOOP );
/* 左下角 */
glVertex2d( leftX, leftY );
/* 右下角 */
glVertex2d( rightX, leftY );
/* 右上角 */
glVertex2d( rightX, rightY );
/* 左上角 */
glVertex2d( leftX, rightY );
/* 结束画线 */
glEnd();
}


/* 画圆角矩形,传入矩形宽高,角半径,矩形中心点坐标 */
void glRoundRec( int centerX, int centerY, int width, int height, float cirR )
{
/* 二分之PI,一个象限的角度 */
float PI_HALF = PI / 2;
/* 划分程度,值越大画得越精细 */
float divide = 20.0;
/* 圆角矩形的坐标 */
float tx, ty;
/* 画封闭曲线 */
glBegin( GL_LINE_LOOP );
/* 四个象限不同的操作符 */
int opX[4] = { 1, -1, -1, 1 };
int opY[4] = { 1, 1, -1, -1 };
/* 用来计数,从第一象限到第四象限 */
float x = 0;
/* x自增时加的值 */
float part = 1 / divide;
/* 计算内矩形宽高一半的数值 */
int w = width / 2 - cirR;
int h = height / 2 - cirR;
/* 循环画线 */
for ( x = 0; x < 4; x += part )
{
/* 求出弧度 */
float rad = PI_HALF * x;
/* 计算坐标值 */
tx = cirR * cos( rad ) + opX[(int) x] * w + centerX;
ty = cirR * sin( rad ) + opY[(int) x] * h + centerY;
/*传入坐标画线 */
glVertex2f( tx, ty );
}
/* 结束画线 */
glEnd();
}


/* 画弧线,相对偏移量XY,开始的弧度,结束的弧度,半径 */
void glArc( double x, double y, double start_angle, double end_angle, double radius )
{
/* 开始绘制曲线 */
glBegin( GL_LINE_STRIP );
/* 每次画增加的弧度 */
double delta_angle = PI / 180;
/* 画圆弧 */
for ( double i = start_angle; i <= end_angle; i += delta_angle )
{
/* 绝对定位加三角函数值 */
double vx = x + radius * cos( i );
double vy = y + radius*sin( i );
glVertex2d( vx, vy );
}
/* 结束绘画 */
glEnd();
}


/* 画圆 */
void glCircle( double x, double y, double radius )
{
/* 画全圆 */
glArc( x, y, 0, 2 * PI, radius );
}


/* 画三角形,传入三个点的坐标 */
void glTri( int x1, int y1, int x2, int y2, int x3, int y3 )
{
/* 画封闭线 */
glBegin( GL_LINE_LOOP );
/* 一点 */
glVertex2d( x1, y1 );
/* 二点 */
glVertex2d( x2, y2 );
/*三点 */
glVertex2d( x3, y3 );
/* 结束画线 */
glEnd();
}


/* 画线,传入两点坐标 */
void glLine( int x1, int y1, int x2, int y2 )
{
/* 画封闭线 */
glBegin( GL_LINE_STRIP );
/* 一点 */
glVertex2d( x1, y1 );
/* 二点 */
glVertex2d( x2, y2 );
/* 结束画线 */
glEnd();
}


/* 函数用来画图 */
void display( void )
{
/* GL_COLOR_BUFFER_BIT表示清除颜色 */
glClear( GL_COLOR_BUFFER_BIT );
/* 设置画线颜色 */
glColor3f( 0.5, 0.5, 0.5 );
/* 画点大小 */
glPointSize( 2 );
/* 画圆角矩形,大肚子 */
glRoundRec( 0, 0, 146, 120, 15 );
/* 画圆,中间小圈 */
glCircle( 0, 0, 10 );
/* 画矩形,脖子 */
glRect( -25, 60, 25, 76 );
/* 画圆角矩形,大脸 */
glRoundRec( 0, 113, 128, 74, 10 );
/* 两个眼睛 */
glCircle( -30, 111, 10 );
glCircle( 30, 111, 10 );
/* 两条天线 */
glLine( -35, 150, -35, 173 );
glLine( 35, 150, 35, 173 );
/* 圆角矩形,两个耳朵 */
glRoundRec( 81, 115, 20, 34, 5 );
glRoundRec( -81, 115, 20, 34, 5 );
/* 圆弧,画嘴 */
glArc( 0, 133, 11 * PI / 8, 13 * PI / 8, 45 );
/* 画三角,肚子里的三角 */
glTri( -30, -15, 30, -15, 0, 28 );
/* 画矩形,胳膊连接处 */
glRect( -81, 43, -73, 25 );
glRect( 81, 43, 73, 25 );
/* 画矩形,上臂 */
glRect( -108, 45, -81, 0 );
glRect( 108, 45, 81, 0 );
/* 画矩形,中臂 */
glRect( -101, 0, -88, -4 );
glRect( 101, 0, 88, -4 );
/* 画矩形,下臂 */
glRect( -108, -4, -81, -37 );
glRect( 108, -4, 81, -37 );
/* 画圆形,手掌 */
glCircle( -95, -47, 10 );
glCircle( 95, -47, 10 );
/* 保证前面的OpenGL命令立即执行,而不是让它们在缓冲区中等待 */
glFlush();
}


/* 窗口大小变化时调用的函数 */
void ChangeSize( GLsizei w, GLsizei h )
{
/* 避免高度为0 */
if ( h == 0 )
{
h = 1;
}
/* 定义视口大小,宽高一致 */
glViewport( 0, 0, w, h );
int half = 300;
/* 重置坐标系统,使投影变换复位 */
glMatrixMode( GL_PROJECTION );
/* 将当前的用户坐标系的原点移到了屏幕中心 */
glLoadIdentity();
/* 定义正交视域体 */
if ( w < h )
{
/* 如果高度大于宽度,则将高度视角扩大,图形显示居中 */
glOrtho( -half, half, -half * h / w, half * h / w, -half, half );
} else {
/* 如果宽度大于高度,则将宽度视角扩大,图形显示居中 */
glOrtho( -half * w / h, half * w / h, -half, half, -half, half );
}
}


/*程序入口 */
int main( int argc, char *argv[] )
{
/* 对GLUT进行初始化,并处理所有的命令行参数 */
glutInit( &argc, argv );
/* 指定RGB颜色模式和单缓冲窗口 */
glutInitDisplayMode( GLUT_RGB | GLUT_SINGLE );
/* 定义窗口的位置 */
glutInitWindowPosition( 100, 100 );
/* 定义窗口的大小 */
glutInitWindowSize( 600, 600 );
/* 创建窗口,同时为之命名 */
glutCreateWindow( "OpenGL" );
/* 设置窗口清除颜色为白色 */
glClearColor( 1.0f, 1.0f, 1.0f, 1.0f );
/* 参数为一个函数,绘图时这个函数就会被调用 */
glutDisplayFunc( &display );
/* 参数为一个函数,当窗口大小改变时会被调用 */
glutReshapeFunc( ChangeSize );
/* 该函数让GLUT框架开始运行,所有设置的回调函数开始工作,直到用户终止程序为止 */
glutMainLoop();
/*程序返回 */
return(0);
}

运行结果如下 20150511002257 这一步非常非常简单,如果有不懂的小伙伴请参见 此文章

对象创建

通过上面的一步,我们已经可以轻松地绘制出一个半身机器人,接下来我们就要通过监听鼠标事件来实现动态多边形的绘制了。 因为实验要求中要求了多边形的颜色绘制,所以颜色怎么选取呢?我是这么想的,在面板的左上角绘制几个色块,通过鼠标点击这几个色块,读取出 RGB 值,然后相应地设置当前绘图的颜色即可。 所以首先我在左上角定义了几个颜色色块,同时监听鼠标事件。 首先定义几个颜色的 RGB 值

1
2
3
4
5
6
7
8
9
/* 各种颜色 */
GLubyte border[3] = {0, 0, 0};
GLubyte grey[3] = {195, 195, 195};
GLubyte yellow[3] = {255, 243, 0};
GLubyte red[3] = {237, 28, 36};
GLubyte darkGrey[3] = {126, 126, 126};
GLubyte white[3] = {255, 255, 255};
/* 当前颜色 */
GLubyte nowColor[3] = {0, 0, 0};

然后定义一个绘制填充色的圆形的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* 绘制填充的圆形 */
void glColorCircle(int x, int y, int R, GLubyte color[3])
{
/* 开始绘制曲线 */
glBegin(GL_POLYGON);
/* 设置颜色 */
glColor3ub(color[0], color[1], color[2]);
/* 每次画增加的弧度 */
double delta_angle = PI / 180;
/* 画圆弧 */
for (double i = 0; i <= 2 * PI; i += delta_angle)
{
/* 绝对定位加三角函数值 */
double vx = x + R * cos(i);
double vy = y + R*sin(i);
glVertex2d(vx, vy);
}
/* 结束绘画 */
glEnd();
glFlush();
}

那么在 display 函数中,我们就可以调用下面的方法来绘制三个取色盘拾取颜色,其中最后一个取色盘保存了当前的颜色。

1
2
3
4
5
/* 绘制取色盘 */
glColorCircle(-280, 280, 10, red);
glColorCircle(-250, 280, 10, yellow);
glColorCircle(-220, 280, 10, grey);
glColorCircle(-180, 280, 10, nowColor);

这样光有了取色盘可不行,需要有鼠标点击事动态地改变取色的色值。现在我们可以加入鼠标监听方法如下 通过上一节的说明,我们定义一个 mouseClick 事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*鼠标点击事件 */
void mouseClick(int btn, int state, int x, int y)
{
/* 选项卡的分界范围 */
int optionHeight = 40;
int optionWidth = 250;
if (btn == GLUT_LEFT_BUTTON && state == GLUT_DOWN)
{
/* 如果在颜色选项卡中点击 */
if (y < optionHeight && x < optionWidth)
{
glReadPixels(x, 2 * halfHeight - y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &nowColor);
printf("r:%d,g:%d,b:%d\n", nowColor[0], nowColor[1], nowColor[2]);
/* 设置绘图颜色并显示当前取色板颜色 */
glColorCircle(-180, 280, 10, nowColor);
/* 如果点击的是右侧选项按钮 */
}else if (y < optionHeight && x > optionWidth) {

}else{

}
}
}

另外加入监听方法

1
2
/*鼠标点击事件,鼠标点击或者松开时调用 */
glutMouseFunc(mouseClick);

在这里有几个地方需要说明一下。

1.选项卡的分界范围是什么?

其实我是把整个画面分割成了三部分,其中最上面 40 像素的一个长条就是选项卡,选项卡又分为了两部分。左边是取色板,右边是绘制的选项,比如取点,绘制,删除等等的操作。所以,optionHeight 就指的是上面的 40 像素的分割线,optionWidth 是中间的分割线,即上方调色板和绘制选项点的分割线。 通过鼠标点击判断是在哪个范围内从而响应不同的事件。 从而鼠标监听事件就是下面的结构了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 如果在颜色选项卡中点击 */
if (y < optionHeight && x < optionWidth)
{
glReadPixels(x, 2 * halfHeight - y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &nowColor);
printf("r:%d,g:%d,b:%d\n", nowColor[0], nowColor[1], nowColor[2]);
/* 设置绘图颜色并显示当前取色板颜色 */
glColorCircle(-180, 280, 10, nowColor);
/* 如果点击的是右侧选项按钮 */
}else if (y < optionHeight && x > optionWidth) {

/* 如果点击的是下方的绘图页面 */
}else{

}

其中第四个色板就是显示了当前的颜色,在-180,280 的位置绘制了当前的颜色色盘。并同时颜色值保存在了 nowColor 中。

2. halfHeight 和 halfWidth

这里定义了 halfHeight 和 halfWdith,在后面的绘图中会用到,同时在取色时也会用到,实际就是整个窗口宽高的一半,定义为全局变量即可。如当前的窗口是 600x600,所以现在二者就都是 300,300. 到现在,如果你按照流程走下来,代码是可以正常运行的,现在可以通过点击左上角的色盘进行取色,同时第四个色盘显示了当前的颜色。运行结果截图如下 20150511005807 好,接下来我们就进行多边形的定义了,多边形的定义我们利用结构体的形式,假设我们绘制的边数最大是 10,可以定义如下的多边形结构体,结构体中包含了顶点坐标(数组形式),顶点数量,颜色(RGB)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 每个图形最多的顶点数 */
#define MAX_VERTEX 10
/* 画的图形最多的个数 */
#define MAX_PLOY 10
/* 多边形结构体 */
struct polygon {
/* 顶点坐标 */
int x[MAX_VERTEX];
int y[MAX_VERTEX];
/* 定义第几个顶点 */
int verNum;
GLubyte color[3];
};
/* 声明多边形数组 */
polygon polygons[MAX_PLOY];

为了便于记录,我们定义一个 con 变量来记录当前绘制到了哪个多边形

1
2
/* 记录画了几个多边形 */
int con = 0;

好,定义好结构体之后,我们就可以先选取点,然后绘制多边形了。所以在这里我们定义两个按钮,取点和绘制。 定义两个颜色

1
2
GLubyte startBtn[3] = {10, 10, 10};
GLubyte endBtn[3] = {20, 20, 20};

我们可以用颜色来区分不同的按钮,现在原来的 display 函数中画上这俩按钮。

1
2
glColorCircle(250, 280, 10, startBtn);
glColorCircle(280, 280, 10, endBtn);

如图所示,当我们要取点的时候就可以点击一下左边的按钮,取完点后就可以点击右边的按钮完成绘制。 QQ截图20150511010802 在鼠标监听事件中,当点击了左边的按钮,便开始拾取几个顶点,然后点击右边的按钮之后,会将图形绘制出来。所以为了区分当前是在拾取点还是完成绘制,我们定义一个状态变量叫 drawStatus

1
2
/* 绘制多边形的起始标志,0是开始绘制,1是结束绘制,初始为-1 */
int drawStatus = -1;

在原来的 mouseClick 函数中,对于判断点击右上方选项卡的响应就可以改写如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/*鼠标点击事件 */
void mouseClick(int btn, int state, int x, int y)
{
/* 选项卡的分界范围 */
int optionHeight = 40;
int optionWidth = 250;
if (btn == GLUT_LEFT_BUTTON && state == GLUT_DOWN)
{
/* 如果在颜色选项卡中点击 */
if (y < optionHeight && x < optionWidth)
{
glReadPixels(x, 2 * halfHeight - y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &nowColor);
printf("r:%d,g:%d,b:%d\n", nowColor[0], nowColor[1], nowColor[2]);
/* 设置绘图颜色并显示当前取色板颜色 */
glColorCircle(-180, 280, 10, nowColor);
/* 如果点击的是右侧选项按钮 */
}else if (y < optionHeight && x > optionWidth) {
//取当前的点
GLubyte color[3];
glReadPixels(x, 2 * halfHeight - y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &color);
printf("r:%d,g:%d,b:%d\n", color[0], color[1], color[2]);
/* 如果点击了开始绘制的按钮 */
if (sameColor(color, startBtn)) {
drawStatus = 0;
/* 开始画一个图形,顶点个数置零 */
polygons[con].verNum = 0;
printf("drawStatus:%d\n", drawStatus);
/* 如果点击了结束绘制的按钮 */
} else if (sameColor(color, endBtn)) {
glutPostRedisplay();
/* 画的图形个数加一 */
con++;
printf("drawStatus:%d\n", drawStatus);
}
/* 如果点击的是下方的绘图页面 */
}else{

}
}
}

其中 sameColor 方法就是来判断当前点击的位置是否和按钮颜色相等,在这里为了避免误差,我们像往常一样设置一个容差。

1
2
3
4
5
6
7
8
9
10
/* 判断两个颜色是否相等 */
bool sameColor(GLubyte color1[3], GLubyte color2[3])
{
if (color1[0] - color2[0] < 5 && color1[1] - color2[1] < 5 && color1[2] - color2[1] < 5)
{
return(true);
} else {
return(false);
}
}

好,当点击了第一个黑色按钮(取点的按钮)时,我们便可以在下方拾取点来保存到数组中了。 在最后的 else 代码块中加入如下的方法,用来取点。 这里的取点是取到了点击的坐标值,当前的 RGB 值,每取一个点,响应的 verNum(顶点数目)就加一。 而且当前保存到的多边形是 polygons[con],con 初始值为 0,也就是保存到第一个多边形结构体中。 当我们点击了开始绘制按钮时,con 会加 1,并且将保存的多边形绘制出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 如果当前是正在取点状态 */
if (drawStatus == 0)
{
/* 保存每个点,然后该图形顶点个数加一 */
polygons[con].x[polygons[con].verNum] = x;
polygons[con].y[polygons[con].verNum] = y;
/* 画点 */
glPoints(x, y);
/* 设置当前颜色RGB,如果取色有变动,以最后的取色为准 */
polygons[con].color[0] = nowColor[0];
polygons[con].color[1] = nowColor[1];
polygons[con].color[2] = nowColor[2];
printf("polyColor%d%d%d\n", polygons[con].color[0],polygons[con].color[1],polygons[con].color[2]);
polygons[con].verNum++;
printf("con:%d,verNum:%d\n", con, polygons[con].verNum);
}

到现在上面的 mouseClick 方法就变成了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/*鼠标点击事件 */
void mouseClick(int btn, int state, int x, int y)
{
/* 选项卡的分界范围 */
int optionHeight = 40;
int optionWidth = 250;
if (btn == GLUT_LEFT_BUTTON && state == GLUT_DOWN)
{
/* 如果在颜色选项卡中点击 */
if (y < optionHeight && x < optionWidth)
{
glReadPixels(x, 2 * halfHeight - y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &nowColor);
printf("r:%d,g:%d,b:%d\n", nowColor[0], nowColor[1], nowColor[2]);
/* 设置绘图颜色并显示当前取色板颜色 */
glColorCircle(-180, 280, 10, nowColor);
/* 如果点击的是右侧选项按钮 */
}else if (y < optionHeight && x > optionWidth) {
//取当前的点
GLubyte color[3];
glReadPixels(x, 2 * halfHeight - y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &color);
printf("r:%d,g:%d,b:%d\n", color[0], color[1], color[2]);
/* 如果点击了开始绘制的按钮 */
if (sameColor(color, startBtn)) {
drawStatus = 0;
/* 开始画一个图形,顶点个数置零 */
polygons[con].verNum = 0;
printf("drawStatus:%d\n", drawStatus);
/* 如果点击了结束绘制的按钮 */
} else if (sameColor(color, endBtn)) {
glutPostRedisplay();
/* 画的图形个数加一 */
con++;
printf("drawStatus:%d\n", drawStatus);
}
/* 如果点击的是下方的绘图页面 */
}else{
/* 如果当前是正在取点状态 */
if (drawStatus == 0)
{
/* 保存每个点,然后该图形顶点个数加一 */
polygons[con].x[polygons[con].verNum] = x;
polygons[con].y[polygons[con].verNum] = y;
/* 画点 */
glPoints(x, y);
/* 设置当前颜色RGB,如果取色有变动,以最后的取色为准 */
polygons[con].color[0] = nowColor[0];
polygons[con].color[1] = nowColor[1];
polygons[con].color[2] = nowColor[2];
printf("polyColor%d%d%d\n", polygons[con].color[0],polygons[con].color[1],polygons[con].color[2]);
polygons[con].verNum++;
printf("con:%d,verNum:%d\n", con, polygons[con].verNum);
}
}
}
}

在这里有几点说明如下

1.glPoints 方法

即画点方法,为了直观地显示用户选取了哪个点,我们设置了这个画点的方法,每点一个点,就在屏幕上显示出来。 方法定义如下

1
2
3
4
5
6
7
8
9
/* 画点 */
void glPoints(int x, int y) {
glBegin(GL_POINTS);
/* 点直接设置为黑色 */
glColor3ub(0, 0, 0);
glVertex2d(x - halfWidth, halfHeight - y);
glEnd();
glFlush();
}

在这里,glVertex2d 方法的参数为什么不是 x,y 呢?很简单,因为我们绘画时的坐标原点是窗口的中心点,而鼠标点击的坐标原点是左上角,为了做一个映射变换,x 需要减去 halfWidth,y 需要取反并加 halfWidth。总之定义这两个 half 变量还是用处多多的。

2.glutPostRedisplay

这个方法就是重新绘制的方法,我们看到在结束绘制的地方调用了这个方法,其实就是重新调用了 display 方法,所以为了让我们绘制的图形正常显示,我们需要在 display 方法中来绘制我们已经存储好的多边形。我们可以把绘制多边形定义为一个方法,然后在 display 中调用即可。 绘制多边形的方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* 绘制多边形 */
void glPolygons()
{
if (con>=0) {
for (int i = 0; i <= con; i++) {
/* 取到这个多边形 */
polygon poly = polygons[i];
/* 画封闭线 */
glBegin(GL_LINE_LOOP);
int num = poly.verNum;
printf("num:%d\n",num);
for (int j = 0; j < num; j++)
{
glColor3ub(poly.color[0], poly.color[1], poly.color[2]);
glVertex2d(poly.x[j] - halfWidth, halfHeight - poly.y[j]);
printf("polyx:%d,polyy:%d",poly.x[j],poly.y[j]);
}
/* 结束画线 */
glEnd();
/* 刷新 */
glFlush();
}
}
}

在 display 函数中调用一下即可,方法的最后面调用一下。

1
2
/* 绘制多边形 */
glPolygons();

好,现在我们点击第一个黑色圆点,然后在屏幕上取点,取完点之后,然后点击右上角的黑色圆点,即可完成绘制 运行结果如下 QQ截图20150511013259 在此先告一段落,下一篇我们接着这里来实现图形的删除,移动以及存盘,读盘等操作。

本节代码

到这里,提供如下代码,代码运行结果即为上图所示,由于代码太长,不完全展开显示,双击查看全部代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
#include <glut.h>
#include <math.h>
#include <stdio.h>

#define PI 3.1415926
/* 每个图形最多的顶点数 */
#define MAX_VERTEX 10
/* 画的图形最多的个数 */
#define MAX_PLOY 10
/* 窗口长宽的一半 */
int halfWidth, halfHeight;
/* 绘制多边形的起始标志,0是开始绘制,1是结束绘制,初始为-1 */
int drawStatus = -1;
/* 多边形结构体 */
struct polygon {
/* 顶点坐标 */
int x[MAX_VERTEX];
int y[MAX_VERTEX];
/* 定义第几个顶点 */
int verNum;
GLubyte color[3];
};
/* 各种颜色 */
GLubyte border[3] = {0, 0, 0};
GLubyte grey[3] = {195, 195, 195};
GLubyte yellow[3] = {255, 243, 0};
GLubyte red[3] = {237, 28, 36};
GLubyte darkGrey[3] = {126, 126, 126};
GLubyte white[3] = {255, 255, 255};
GLubyte startBtn[3] = {10, 10, 10};
GLubyte endBtn[3] = {20, 20, 20};
/* 当前颜色 */
GLubyte nowColor[3] = {0, 0, 0};
/* 声明多边形数组 */
polygon polygons[MAX_PLOY];
/* 记录画了几个多边形 */
int con = 0;

/* 绘制多边形 */
void glPolygons()
{
if (con>=0) {
for (int i = 0; i <= con; i++) {
/* 取到这个多边形 */
polygon poly = polygons[i];
/* 画封闭线 */
glBegin(GL_LINE_LOOP);
int num = poly.verNum;
printf("num:%d\n",num);
for (int j = 0; j < num; j++)
{
glColor3ub(poly.color[0], poly.color[1], poly.color[2]);
glVertex2d(poly.x[j] - halfWidth, halfHeight - poly.y[j]);
printf("polyx:%d,polyy:%d",poly.x[j],poly.y[j]);
}
/* 结束画线 */
glEnd();
/* 刷新 */
glFlush();
}
}
}

/* 绘制填充的圆形 */
void glColorCircle(int x, int y, int R, GLubyte color[3])
{
/* 开始绘制曲线 */
glBegin(GL_POLYGON);
/* 设置颜色 */
glColor3ub(color[0], color[1], color[2]);
/* 每次画增加的弧度 */
double delta_angle = PI / 180;
/* 画圆弧 */
for (double i = 0; i <= 2 * PI; i += delta_angle)
{
/* 绝对定位加三角函数值 */
double vx = x + R * cos(i);
double vy = y + R*sin(i);
glVertex2d(vx, vy);
}
/* 结束绘画 */
glEnd();
glFlush();
}

//画矩形,传入的是左下角XY坐标和右上角XY坐标
void glRect(int leftX,int leftY,int rightX,int rightY){
//画封闭曲线
glBegin(GL_LINE_LOOP);
//左下角
glVertex2d(leftX,leftY);
//右下角
glVertex2d(rightX,leftY);
//右上角
glVertex2d(rightX,rightY);
//左上角
glVertex2d(leftX,rightY);
//结束画线
glEnd();
}

//画圆角矩形,传入矩形宽高,角半径,矩形中心点坐标
void glRoundRec(int centerX,int centerY,int width,int height,float cirR){
//二分之PI,一个象限的角度
float PI_HALF = PI/2;
//划分程度,值越大画得越精细
float divide=20.0;
//圆角矩形的坐标
float tx,ty;
//画封闭曲线
glBegin(GL_LINE_LOOP);
//四个象限不同的操作符
int opX[4]={1,-1,-1,1};
int opY[4]={1,1,-1,-1};
//用来计数,从第一象限到第四象限
float x=0;
//x自增时加的值
float part=1/divide;
//计算内矩形宽高一半的数值
int w=width/2-cirR;
int h=height/2-cirR;
//循环画线
for(x=0;x<4;x+=part){
//求出弧度
float rad = PI_HALF*x;
//计算坐标值
tx=cirR*cos(rad)+opX[(int)x]*w+centerX;
ty=cirR*sin(rad)+opY[(int)x]*h+centerY;
//传入坐标画线
glVertex2f(tx,ty);
}
//结束画线
glEnd();
}

//画弧线,相对偏移量XY,开始的弧度,结束的弧度,半径
void glArc(double x,double y,double start_angle,double end_angle,double radius)
{
//开始绘制曲线
glBegin(GL_LINE_STRIP);
//每次画增加的弧度
double delta_angle=PI/180;
//画圆弧
for (double i=start_angle;i<=end_angle;i+=delta_angle)
{
//绝对定位加三角函数值
double vx=x+radius * cos(i);
double vy=y+radius*sin(i);
glVertex2d(vx,vy);
}
//结束绘画
glEnd();
}


//画圆
void glCircle(double x, double y, double radius)
{
//画全圆
glArc(x,y,0,2*PI,radius);
}

//画三角形,传入三个点的坐标
void glTri(int x1,int y1,int x2,int y2,int x3,int y3){
//画封闭线
glBegin(GL_LINE_LOOP);
//一点
glVertex2d(x1,y1);
//二点
glVertex2d(x2,y2);
//三点
glVertex2d(x3,y3);
//结束画线
glEnd();
}

//画线,传入两点坐标
void glLine(int x1,int y1,int x2,int y2){
//画封闭线
glBegin(GL_LINE_STRIP);
//一点
glVertex2d(x1,y1);
//二点
glVertex2d(x2,y2);
//结束画线
glEnd();
}

//函数用来画图
void display(void)
{
//GL_COLOR_BUFFER_BIT表示清除颜色
glClear(GL_COLOR_BUFFER_BIT);
//设置画线颜色
glColor3f(0.5,0.5,0.5);
//画点大小
glPointSize(2);
//画圆角矩形,大肚子
glRoundRec(0,0,146,120,15);
//画圆,中间小圈
glCircle(0,0,10);
//画矩形,脖子
glRect(-25,60,25,76);
//画圆角矩形,大脸
glRoundRec(0,113,128,74,10);
//两个眼睛
glCircle(-30,111,10);
glCircle(30,111,10);
//两条天线
glLine(-35,150,-35,173);
glLine(35,150,35,173);
//圆角矩形,两个耳朵
glRoundRec(81,115,20,34,5);
glRoundRec(-81,115,20,34,5);
//圆弧,画嘴
glArc(0,133,11*PI/8,13*PI/8,45);
//画三角,肚子里的三角
glTri(-30,-15,30,-15,0,28);
//画矩形,胳膊连接处
glRect(-81,43,-73,25);
glRect(81,43,73,25);
//画矩形,上臂
glRect(-108,45,-81,0);
glRect(108,45,81,0);
//画矩形,中臂
glRect(-101,0,-88,-4);
glRect(101,0,88,-4);
//画矩形,下臂
glRect(-108,-4,-81,-37);
glRect(108,-4,81,-37);
//画圆形,手掌
glCircle(-95,-47,10);
glCircle(95,-47,10);

/* 绘制取色盘 */
glColorCircle(-280, 280, 10, red);
glColorCircle(-250, 280, 10, yellow);
glColorCircle(-220, 280, 10, grey);
glColorCircle(-180, 280, 10, nowColor);
glColorCircle(250, 280, 10, startBtn);
glColorCircle(280, 280, 10, endBtn);
//保证前面的OpenGL命令立即执行,而不是让它们在缓冲区中等待
/* 绘制多边形 */
glPolygons();
glFlush();
}


//窗口大小变化时调用的函数
void ChangeSize(GLsizei w,GLsizei h)
{
//避免高度为0
if(h==0) {
h=1;
}
//定义视口大小,宽高一致
glViewport(0,0,w,h);
int half = 300;
/* 定义宽高的一半 */
halfHeight = 300;
halfWidth = 300;
//重置坐标系统,使投影变换复位
glMatrixMode(GL_PROJECTION);
//将当前的用户坐标系的原点移到了屏幕中心
glLoadIdentity();
//定义正交视域体
if(w<h) {
//如果高度大于宽度,则将高度视角扩大,图形显示居中
glOrtho(-half,half,-half*h/w,half*h/w,-half,half);
} else {
//如果宽度大于高度,则将宽度视角扩大,图形显示居中
glOrtho(-half*w/h,half*w/h,-half,half,-half,half);
}

}
/* 判断两个颜色是否相等 */
bool sameColor(GLubyte color1[3], GLubyte color2[3])
{
if (color1[0] - color2[0] < 5 && color1[1] - color2[1] < 5 && color1[2] - color2[1] < 5)
{
return(true);
} else {
return(false);
}
}

/* 画点 */
void glPoints(int x, int y) {
glBegin(GL_POINTS);
/* 点直接设置为黑色 */
glColor3ub(0, 0, 0);
glVertex2d(x - halfWidth, halfHeight - y);
glEnd();
glFlush();
}

/*鼠标点击事件 */
void mouseClick(int btn, int state, int x, int y)
{
/* 选项卡的分界范围 */
int optionHeight = 40;
int optionWidth = 250;
if (btn == GLUT_LEFT_BUTTON && state == GLUT_DOWN)
{
/* 如果在颜色选项卡中点击 */
if (y < optionHeight && x < optionWidth)
{
glReadPixels(x, 2 * halfHeight - y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &nowColor);
printf("r:%d,g:%d,b:%d\n", nowColor[0], nowColor[1], nowColor[2]);
/* 设置绘图颜色并显示当前取色板颜色 */
glColorCircle(-180, 280, 10, nowColor);
/* 如果点击的是右侧选项按钮 */
}else if (y < optionHeight && x > optionWidth) {
//取当前的点
GLubyte color[3];
glReadPixels(x, 2 * halfHeight - y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &color);
printf("r:%d,g:%d,b:%d\n", color[0], color[1], color[2]);
/* 如果点击了开始绘制的按钮 */
if (sameColor(color, startBtn)) {
drawStatus = 0;
/* 开始画一个图形,顶点个数置零 */
polygons[con].verNum = 0;
printf("drawStatus:%d\n", drawStatus);
/* 如果点击了结束绘制的按钮 */
} else if (sameColor(color, endBtn)) {
glutPostRedisplay();
/* 画的图形个数加一 */
con++;
printf("drawStatus:%d\n", drawStatus);
}
/* 如果点击的是下方的绘图页面 */
}else{
/* 如果当前是正在取点状态 */
if (drawStatus == 0)
{
/* 保存每个点,然后该图形顶点个数加一 */
polygons[con].x[polygons[con].verNum] = x;
polygons[con].y[polygons[con].verNum] = y;
/* 画点 */
glPoints(x, y);
/* 设置当前颜色RGB,如果取色有变动,以最后的取色为准 */
polygons[con].color[0] = nowColor[0];
polygons[con].color[1] = nowColor[1];
polygons[con].color[2] = nowColor[2];
printf("polyColor%d%d%d\n", polygons[con].color[0],polygons[con].color[1],polygons[con].color[2]);
polygons[con].verNum++;
printf("con:%d,verNum:%d\n", con, polygons[con].verNum);
}
}
}
}




//程序入口
int main(int argc, char *argv[]){
//对GLUT进行初始化,并处理所有的命令行参数
glutInit(&argc, argv);
//指定RGB颜色模式和单缓冲窗口
glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE);
//定义窗口的位置
glutInitWindowPosition(100, 100);
//定义窗口的大小
glutInitWindowSize(600, 600);
//创建窗口,同时为之命名
glutCreateWindow("OpenGL");
//设置窗口清除颜色为白色
glClearColor(1.0f,1.0f,1.0f,1.0f);
//参数为一个函数,绘图时这个函数就会被调用
glutDisplayFunc(&display);
//参数为一个函数,当窗口大小改变时会被调用
glutReshapeFunc(ChangeSize);
/*鼠标点击事件,鼠标点击或者松开时调用 */
glutMouseFunc(mouseClick);
//该函数让GLUT框架开始运行,所有设置的回调函数开始工作,直到用户终止程序为止
glutMainLoop();
//程序返回
return 0;
}

小伙伴们可以运行试试看。

综述

本节我们主要利用了鼠标时间来实现了取色,设置颜色,并成功通过取点绘制出了各种形状的多边形。

C/C++

综述

在前面的例子中我们绘制了 2D 机器人以及颜色的填充,另外还有平移放缩变换等等。不过这些绘图方式都有一个特点,那就是无法动态地响应事件,如果我们在例子中加入鼠标点击的响应时间,让图形根据鼠标点击的变化而变化,那将会是非常友好的。 那么这一篇我们就来看一下鼠标响应的实现。

鼠标点击

对于鼠标点击,无非是左右键,滚轮的点击以及区分按下还是按上。 监听方法如下: void mouseClick(int btn, int state, int x, int y) 方法名是任意的,参数有四个,分别是左右键,点击状态(按下还是松开),点击的 x,y 坐标。 那么我们在 main 函数中怎么调用,代码如下

1
2
/* 鼠标点击事件,鼠标点击或者松开时调用 */
glutMouseFunc(mouseClick);

我们利用了类库中提供的方法 glutMouseFunc() 这个方法用来监听鼠标事件,我们传入刚才定义的 mouseClick 函数,即可完成调用,同时 mouseClick 函数的参数都会被赋值。 通过上面的代码,在鼠标点击窗口内部时,会自动调用 mouseClick 方法,而且四个参数都会有赋值。可以直接使用这些值来操作。

鼠标移动

void mouseMove(int x, int y) 方法名我们也可以任意指定,参数有 2 个,分别是 x,y 在 main 函数中可以这样调用

1
2
/*鼠标移动事件,鼠标按下并移动时调用 */
glutMotionFunc(mouseMove);

我们利用了类库中提供的方法 glutMotionFunc() 同上面的原理,传入了 mouseMove 方法,在鼠标移动时,就可以对 mouseMove 方法的参数进行赋值。

实例

好了,上面我们说了两个鼠标事件的方法,下面我们用一个例子来感受一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <glut.h>
#include <math.h>
#include <stdio.h>


/* 函数用来画图 */
void display(void)
{

}


/*鼠标点击事件 */
void mouseClick(int btn, int state, int x, int y)
{
printf("点击了鼠标,btn:%d,state:%d,x:%d,y:%d\n", btn, state, x, y);
if(btn == GLUT_LEFT_BUTTON && state == GLUT_DOWN){
printf("按下了左边按钮\n");
}
if(btn == GLUT_LEFT_BUTTON && state == GLUT_UP){
printf("松开了左边按钮\n");
}
if(btn == GLUT_RIGHT_BUTTON && state == GLUT_DOWN){
printf("按下了右边按钮\n");
}
if(btn == GLUT_RIGHT_BUTTON && state == GLUT_UP){
printf("松开了右边按钮\n");
}
}



/* 鼠标移动事件 */
void mouseMove(int x, int y)
{
printf("移动鼠标中,x:%d,y%d\n", x, y);
}


/* 程序入口 */
int main(int argc, char *argv[])
{
/* 对GLUT进行初始化,并处理所有的命令行参数 */
glutInit(&argc, argv);
/* 指定RGB颜色模式和单缓冲窗口 */
glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE);
/* 定义窗口的位置 */
glutInitWindowPosition(100, 100);
/* 定义窗口的大小 */
glutInitWindowSize(400, 400);
/* 创建窗口,同时为之命名 */
glutCreateWindow("OpenGL");
/* 设置窗口清除颜色为白色 */
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
/* 参数为一个函数,绘图时这个函数就会被调用 */
glutDisplayFunc(&display);
/*鼠标点击事件,鼠标点击或者松开时调用 */
glutMouseFunc(mouseClick);
/*鼠标移动事件,鼠标按下并移动时调用 */
glutMotionFunc(mouseMove);
/* 该函数让GLUT框架开始运行,所有设置的回调函数开始工作,直到用户终止程序为止 */
glutMainLoop();
/*程序返回 */
return(0);
}

在上面的代码中,我们监听了鼠标点击和鼠标移动事件,并打印输出了鼠标点击事件的响应。 比如我先点击了鼠标左键,然后点击了鼠标右键。然后移动了鼠标左键,然后移动了鼠标右键。运行结果如下 0150511000009 可以发现它打印输出了鼠标的按钮代号,点击状态,点击的位置。 另外我们发现鼠标点击的坐标原点是左上角,这跟我们之前读取色值的坐标原点(左下角)不一样,也跟我们绘图的坐标原点(中心点)不一样。编程时一定要注意。 获取到了点击点的位置,我们就可以利用这些值来进行相应的作图处理了。 小伙伴们可以尝试一下。

综述

本节说的不多,主要是说明了鼠标监听事件的用法。glutMouseFunc 以及 glutMotionFunc,另外通过一个实例来演示了鼠标监听事件的用法,下一篇我们就利用这个知识来进行一个实例演示。大家加油!

HTML

综述

LESS 包含一套自定义的语法及一个解析器,用户根据这些语法定义自己的样式规则,这些规则最终会通过解析器,编译生成对应的 CSS 文件。LESS 并没有裁剪 CSS 原有的特性,更不是用来取代 CSS 的,而是在现有 CSS 语法的基础上,为 CSS 加入程序式语言的特性。 本文部分内容参考 官方文档

编译器

建议使用Koala编译器 下载地址

LESS 原理及使用方式

本质上,LESS 包含一套自定义的语法及一个解析器,用户根据这些语法定义自己的样式规则,这些规则最终会通过解析器,编译生成对应的 CSS 文件。LESS 并没有裁剪 CSS 原有的特性,更不是用来取代 CSS 的,而是在现有 CSS 语法的基础上,为 CSS 加入程序式语言的特性。下面是一个简单的例子:

清单 1. LESS 文件
1
2
3
4
5
6
7
8
@color: #4D926F; 

#header {
color: @color;
}
h2 {
color: @color;
}

经过编译生成的 CSS 文件如下:

清单 2. CSS 文件
1
2
3
4
5
6
#header { 
color: #4D926F;
}
h2 {
color: #4D926F;
}

从上面的例子可以看出,学习 LESS 非常容易,只要你了解 CSS 基础就可以很容易上手。 LESS 可以直接在客户端使用,也可以在服务器端使用。在实际项目开发中,我们更推荐使用第三种方式,将 LESS 文件编译生成静态 CSS 文件,并在 HTML 文档中应用。

客户端

我们可以直接在客户端使用 .less(LESS 源文件),只需要从 LESSCSS 下载 less.js 文件,然后在我们需要引入 LESS 源文件的 HTML 中加入如下代码:

1
<link rel="stylesheet/less" type="text/css" href="styles.less">

LESS 源文件的引入方式与标准 CSS 文件引入方式一样:

1
<link rel="stylesheet/less" type="text/css" href="styles.less">

需要注意的是:在引入 .less 文件时,rel 属性要设置为“stylesheet/less”。还有更重要的一点需要注意的是:LESS 源文件一定要在 less.js 引入之前引入,这样才能保证 LESS 源文件正确编译解析。

使用编译生成的静态 CSS 文件

我们可以通过 LESS 的编译器,将 LESS 文件编译成为 CSS 文件,在 HTML 文章中引入使用。这里要强调的一点,LESS 是完全兼容 CSS 语法的,也就是说,我们可以将标准的 CSS 文件直接改成 .less 格式,LESS 编译器可以完全识别。

语法

变量

LESS 允许开发者自定义变量,变量可以在全局样式中使用,变量使得样式修改起来更加简单。 我们可以从下面的代码了解变量的使用及作用:

清单 3. LESS 文件
1
2
3
4
5
@border-color : #b5bcc7; 

.mythemes tableBorder{
border : 1px solid @border-color;
}

经过编译生成的 CSS 文件如下:

清单 4. CSS 文件
1
2
3
.mythemes tableBorder { 
border: 1px solid #b5bcc7;
}

从上面的代码中我们可以看出,变量是 VALUE(值)级别的复用,可以将相同的值定义成变量统一管理起来。 该特性适用于定义主题,我们可以将背景颜色、字体颜色、边框属性等常规样式进行统一定义,这样不同的主题只需要定义不同的变量文件就可以了。当然该特性也同样适用于 CSS RESET(重置样式表),在 Web 开发中,我们往往需要屏蔽浏览器默认的样式行为而需要重新定义样式表来覆盖浏览器的默认行为,这里可以使用 LESS 的变量特性,这样就可以在不同的项目间重用样式表,我们仅需要在不同的项目样式表中,根据需求重新给变量赋值即可。 LESS 中的变量和其他编程语言一样,可以实现值的复用,同样它也有生命周期,也就是 Scope(变量范围,开发人员惯称之为作用域),简单的讲就是局部变量还是全局变量的概念,查找变量的顺序是先在局部定义中找,如果找不到,则查找上级定义,直至全局。下面我们通过一个简单的例子来解释 Scope。

清单 5. LESS 文件
1
2
3
4
5
6
7
8
9
10
11
@width : 20px; 
#homeDiv {
@width : 30px;
#centerDiv{
width : @width;// 此处应该取最近定义的变量 width 的值 30px
}
}
#leftDiv {
width : @width; // 此处应该取最上面定义的变量 width 的值 20px

}

经过编译生成的 CSS 文件如下:

清单 6. CSS 文件
1
2
3
4
5
6
#homeDiv #centerDiv { 
width: 30px;
}
#leftDiv {
width: 20px;
}

Mixins(混入)

Mixins(混入)功能对用开发者来说并不陌生,很多动态语言都支持 Mixins(混入)特性,它是多重继承的一种实现,在 LESS 中,混入是指在一个 CLASS 中引入另外一个已经定义的 CLASS,就像在当前 CLASS 中增加一个属性一样。 我们先简单看一下 Mixins 在 LESS 中的使用:

清单 7. LESS 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义一个样式选择器
.roundedCorners(@radius:5px) {
-moz-border-radius: @radius;
-webkit-border-radius: @radius;
border-radius: @radius;
}
// 在另外的样式选择器中使用
#header {
.roundedCorners;
}
#footer {
.roundedCorners(10px);
}

经过编译生成的 CSS 文件如下:

清单 8. CSS 文件
1
2
3
4
5
6
7
8
9
10
#header { 
-moz-border-radius:5px;
-webkit-border-radius:5px;
border-radius:5px;
}
#footer {
-moz-border-radius:10px;
-webkit-border-radius:10px;
border-radius:10px;
}

从上面的代码我们可以看出:Mixins 其实是一种嵌套,它允许将一个类嵌入到另外一个类中使用,被嵌入的类也可以称作变量,简单的讲,Mixins 其实是规则级别的复用。 Mixins 还有一种形式叫做 Parametric Mixins(混入参数),LESS 也支持这一特性:

清单 9. LESS 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定义一个样式选择器
.borderRadius(@radius){
-moz-border-radius: @radius;
-webkit-border-radius: @radius;
border-radius: @radius;
}
// 使用已定义的样式选择器
#header {
.borderRadius(10px); // 把 10px 作为参数传递给样式选择器
}
.btn {
.borderRadius(3px);// // 把 3px 作为参数传递给样式选择器

}

经过编译生成的 CSS 文件如下:

清单 10. CSS 文件
1
2
3
4
5
6
7
8
9
10
#header { 
-moz-border-radius: 10px;
-webkit-border-radius: 10px;
border-radius: 10px;
}
.btn {
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
}

我们还可以给 Mixins 的参数定义一人默认值,如

清单 11. LESS 文件
1
2
3
4
5
6
7
8
.borderRadius(@radius:5px){ 
-moz-border-radius: @radius;
-webkit-border-radius: @radius;
border-radius: @radius;
}
.btn {
.borderRadius;
}

经过编译生成的 CSS 文件如下:

清单 12. CSS 文件
1
2
3
4
5
.btn { 
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
border-radius: 5px;
}

像 JavaScript 中 arguments一样,Mixins 也有这样一个变量:@arguments。@arguments 在 Mixins 中具是一个很特别的参数,当 Mixins 引用这个参数时,该参数表示所有的变量,很多情况下,这个参数可以省去你很多代码。

清单 13. LESS 文件
1
2
3
4
5
6
7
8
.boxShadow(@x:0,@y:0,@blur:1px,@color:#000){ 
-moz-box-shadow: @arguments;
-webkit-box-shadow: @arguments;
box-shadow: @arguments;
}
#header {
.boxShadow(2px,2px,3px,#f36);
}

经过编译生成的 CSS 文件如下:

清单 14. CSS 文件
1
2
3
4
5
#header { 
-moz-box-shadow: 2px 2px 3px #FF36;
-webkit-box-shadow: 2px 2px 3px #FF36;
box-shadow: 2px 2px 3px #FF36;
}

Mixins 是 LESS 中很重要的特性之一,我们这里也写了很多例子,看到这些例子你是否会有这样的疑问:当我们拥有了大量选择器的时候,特别是团队协同开发时,如何保证选择器之间重名问题?如果你是 java 程序员或 C++ 程序员,我猜你肯定会想到命名空间 Namespaces,LESS 也采用了命名空间的方法来避免重名问题,于是乎 LESS 在 mixins 的基础上扩展了一下,看下面这样一段代码:

清单 15. LESS 文件
1
2
3
4
#mynamespace { 
.home {...}
.user {...}
}

这样我们就定义了一个名为 mynamespace 的命名空间,如果我们要复用 user 这个选择器的时候,我们只需要在需要混入这个选择器的地方这样使用就可以了。#mynamespace > .user。

嵌套的规则

在我们书写标准 CSS 的时候,遇到多层的元素嵌套这种情况时,我们要么采用从外到内的选择器嵌套定义,要么采用给特定元素加 CLASS 或 ID 的方式。在 LESS 中我们可以这样写:

清单 16. HTML 片段
1
2
3
4
5
6
7
<div id="home"> 
<div id="top">top</div>
<div id="center">
<div id="left">left</div>
<div id="right">right</div>
</div>
</div>
清单 17. LESS 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#home{ 
color : blue;
width : 600px;
height : 500px;
border:outset;
#top{
border:outset;
width : 90%;
}
#center{
border:outset;
height : 300px;
width : 90%;
#left{
border:outset;
float : left;
width : 40%;
}
#right{
border:outset;
float : left;
width : 40%;
}
}
}

经过编译生成的 CSS 文件如下:

清单 18. CSS 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#home { 
color: blue;
width: 600px;
height: 500px;
border: outset;
}
#home #top {
border: outset;
width: 90%;
}
#home #center {
border: outset;
height: 300px;
width: 90%;
}
#home #center #left {
border: outset;
float: left;
width: 40%;
}
#home #center #right {
border: outset;
float: left;
width: 40%;
}

从上面的代码中我们可以看出,LESS 的嵌套规则的写法是 HTML 中的 DOM 结构相对应的,这样使我们的样式表书写更加简洁和更好的可读性。同时,嵌套规则使得对伪元素的操作更为方便。

清单 19. LESS 文件
1
2
3
4
5
6
7
8
a { 
color: red;
text-decoration: none;
&:hover {// 有 & 时解析的是同一个元素或此元素的伪类,没有 & 解析是后代元素
color: black;
text-decoration: underline;
}
}

经过编译生成的 CSS 文件如下:

清单 20. CSS 文件
1
2
3
4
5
6
7
8
a { 
color: red;
text-decoration: none;
}
a:hover {
color: black;
text-decoration: underline;
}

运算及函数

在我们的 CSS 中充斥着大量的数值型的 value,比如 color、padding、margin 等,这些数值之间在某些情况下是有着一定关系的,那么我们怎样利用 LESS 来组织我们这些数值之间的关系呢?我们来看这段代码:

清单 21 . LESS 文件
1
2
3
4
5
@init: #111111; 
@transition: @init*2;
.switchColor {
color: @transition;
}

经过编译生成的 CSS 文件如下:

清单 22. CSS 文件
1
2
3
.switchColor { 
color: #222222;
}

上面的例子中使用 LESS 的 operation 是 特性,其实简单的讲,就是对数值型的 value(数字、颜色、变量等)进行加减乘除四则运算。同时 LESS 还有一个专门针对 color 的操作提供一组函数。下面是 LESS 提供的针对颜色操作的函数列表:

1
2
3
4
5
6
7
8
lighten(@color, 10%); // return a color which is 10% *lighter* than @color 
darken(@color, 10%); // return a color which is 10% *darker* than @color
saturate(@color, 10%); // return a color 10% *more* saturated than @color
desaturate(@color, 10%);// return a color 10% *less* saturated than @color
fadein(@color, 10%); // return a color 10% *less* transparent than @color
fadeout(@color, 10%); // return a color 10% *more* transparent than @color
spin(@color, 10); // return a color with a 10 degree larger in hue than @color
spin(@color, -10); // return a color with a 10 degree smaller hue than @color

PS: 上述代码引自 LESS CSS 官方网站,详情请见 http://lesscss.org/#-color-functions 使用这些函数和 JavaScript 中使用函数一样。

清单 23. LESS 文件
1
2
3
4
init: #f04615; 
#body {
background-color: fadein(@init, 10%);
}

经过编译生成的 CSS 文件如下:

清单 24. CSS 文件
1
2
3
#body { 
background-color: #f04615;
}

从上面的例子我们可以发现,这组函数像极了 JavaScript 中的函数,它可以被调用和传递参数。这些函数的主要作用是提供颜色变换的功能,先把颜色转换成 HSL 色,然后在此基础上进行操作,LESS 还提供了获取颜色值的方法,在这里就不举例说明了。 LESS 提供的运算及函数特性适用于实现页面组件特性,比如组件切换时的渐入渐出。

模式匹配

清单 25. LESS文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.tri{
width: 0;
height: 0;
overflow: hidden;
.triangle(bottom);
}

.triangle(top,@w:5px,@c:#CCC){
border-width: @w;
border-color: transparent transparent red transparent;
border-style: dashed dashed solid dashed;
}
.triangle(bottom,@w:5px,@c:#CCC){
border-width: @w;
border-color: red transparent transparent transparent;
border-style: solid dashed dashed dashed;
}
清单 26. CSS文件
1
2
3
4
5
6
7
8
.tri {
width: 0;
height: 0;
overflow: hidden;
border-width: 5px;
border-color: red transparent transparent transparent;
border-style: solid dashed dashed dashed;
}

通过以上可以看出,我们可以在LESS中定义相同名字的样式,第一个参数不同,在调用时可以通过传入第一个参数进行匹配,得到不同的样式即可。

避免编译

清单27. LESS文件
1
2
3
.test{
width:calc(300px-30px);
}
清单28. CSS文件
1
2
3
.test {
width: calc(270px);
}

有时候一些样式如果编译了之后就会产生不必要的麻烦,如果我们不想要编译,可以改写如下

清单29. LESS文件
1
2
3
.test{
width:~'calc(300px-30px)';
}
清单30. CSS文件
1
2
3
.test {
width: calc(300px-30px);
}

通过在前面加上一个 ~然后将内容用引号引起来,就可以避免编译了。

总结

本文提到的只是 LESS 的基本功能,更高级的功能如:字符串插值,服务器端使用配置,JavaScript 表达式,避免编译等可以参看 LESS 的官方网站。 LESS 以 CSS 语法为基础,又借用了很多我们熟知编程式语言的特性,这对于我们开发人员来讲学习成本几乎可以忽略,它在保留 CSS 语法的基础上扩展了更多实用的功能,LESS 为我们提供了一种新的编写样式表的方法,我们可以根据我们的项目特性选择使用 LESS 的部分特性,我们只需用很少的成本就可以换了很大的回报,一句话,Less is more,借助 LESS 可以更便捷的进行 Web 开发。

个人日记

等啊等,终于等到五一放假,这次必须要出去好好玩一番,解脱一下,释放一下。经过一系列思想斗争,最终决定去泰安方特了!昨天方特之旅结束之后,甚是爽快,在此记流水账一篇,不喜勿喷哈! 话说回来还是要先说五月二号啊,简直了,本来打算是在五月二号去的,带上室友小周周和隔壁的花花学长。一号那天我定了第二天的火车,早上 7 点半的火车,室友小周周也提前约好了出租车,准备早上六点出发去火车站。万事俱备只欠东风了,当天晚上早早滚去睡觉,定好五点半的闹钟,准备静静等待闹钟的响起。可是,可是,一切都是来得那么突然,第二天一大早起来竟然下雨了!泰安那边下雨了!天哪,睡之前我还特意看了看二号是多云啊!这不是坑爹吗?下雨就下雨吧,可是你不知道下雨方特室外项目是不开放的好嘛!顿时心都凉了。╮(╯▽╰)╭ 哎,改签吧,小周周给那位出租司机打电话,我去重新改签票。嗯,又一个坑爹的事情发生了,7 点之前网上购票是不开放的好嘛?发车前两小时是不能在网上改签和退票的好嘛?哎,算了,还好票便宜,才十二块五,索性我不要了!不要了!默默去买了三号的车票,特么的还是无座啊,无座。哎,多么痛的领悟,╮(╯▽╰)╭。 然后,然后,就默默等到了三号,还是五点半的闹钟,这次终于不下雨了!苍天啊!带上手机,耳机,money,学僧证,充电宝,六点出门坐上 TAXI,朝火车站奔去,这次是真快啊这速度,路上都没车的,一路狂奔,竟然六点半多点就到了火车站了,可是我们七点半的火车啊,这不是坑爹吗?于是乎,我们决定改签一下提前一点,有个七点稍多的一趟车,于是乎我们兴冲冲地跑向了改签大厅,惊呆了!怎么它就那么长的队伍,哦你,那么长,么长,长。试着排了排,二十分钟往前走了不到十步,也是醉了。哎,默默走掉,去候车室等着吧,七点半就七点半! 呼呼呼,检票咯,K771,我们走上站台,看到一条漫长的绿皮车,不是说好的无座吗?怎么这么多空座,我们三个找到一个就坐下来,不得不再次吐槽一下这种座位真难受。早上好困,睡觉睡觉,带上耳机听着歌,像打坐一样睡着了,就那么睡着了,我好佩服我自己。睡了一个小时,一睁眼,泰山站到了,不错不错,下车,出发。 出站了,果然不出我所料,好多人都拿着牌子,问去方特吗?免费接送,立即出发,同时卖门票,220 一位,其实方特门票就是 220 的,学生票可能就便宜 10 块钱。哎,这样就这样吧,他们是打着免费接送的名义,实质钱都包含在门票里面了。我们也担心方特人特别多,到时候买票排个时候,就直接跟着他走吧,反正也贵不了多少。 车是一个公交车,等人来齐了一起发车出发,我们几个竟然赶到了第一波,进去的时候车是空的,要等到其他人来才发车,这不是坑爹吗?坑爹吗?说好的立马出发的呢?算了,不计较这些了,其实等了也没多久就走了。在车上交了钱买了门票,那个收钱的人(姑且叫导游吧)跟我们讲了方特的各个注意事项,还是挺贴心的嘛!在这里透露给大家一点,记不住的我就记不住了。 包包最好要寄存起来,最好寄存在方特外面,比较便宜,当然也可以背着如果你不嫌沉的话。 贵重物品一定要看管好,方特里面丢的最多的就是数码相机,然后是手机,保管好了就行咯。 如果你不想一开始就玩惊险刺激的,建议从右边开始玩,如果你一开始就想玩刺激的,建议还是从右边开始玩哈哈哈。 项链啊首饰啊最好要收起来,要不然你想体验在空中翻滚的时候项链打到牙的感受吗? 哦,其他的忘了,我也不知道他说了多少了,可能比较健忘,记不起来还有啥了。后面的我就根据我的亲身体验来说了,哈哈。 走了半个多小时,终于到了!我们去存上包,买好雨衣以防神秘河谷的时候淋湿。正式进门!进门!冲冲冲!方特之旅正式开始!差不多九点半! 放个地图先 20150504150649 恩,进去看过去就是一个方特城堡,好有艺术气息的,旁边还放着欢乐的音乐,走在小路上,还有阳光照射着,哈哈不错。而且关键是路上人好少啊,哈哈,是不是五月三号外地的都回家了呢?是吗?后面你就知道了。 4d6577b5ga2451346b0fc&690&690 恩,我们没有进城堡玩,直接奔向右边的飞跃极限。噢丫,谁说的人少的?谁说的人少的?人都跑进来了,排队啊,一层、两层、三层…不数了,心好累。也就十来层吧。排个毛线,我们直接冲向了下一站,神秘河谷。PO 个图先 25540577,1681257058& 简直是人品好,我们进去的时候竟然排到了前面,是不是刚开放就被我们赶上了呢?而且,我们赶上了第一波。这次在外面买的雨衣派上用场了,穿上我们的半透明红色雨衣,带上蓝色高筒鞋套,上船。船一排四个人,一共好像是五排吧,我们很果断地选择了第一排,雨衣全部裹起来。准备冲,看到图了吗?这是一个小船从河谷里面二十米高空俯冲下来的样子,看那浪花。这次我们真正地感受到了它的魅力。河谷里面其实是有两个小下坡的,第一次比较小,杀杀你的锐气,第二次就是图中这个超大的下坡,从楼顶的那个位置冲下来,自己想象!第一次下坡还好,等到第二次下坡的时候,啊啊啊啊,二三十米高空俯冲,那种朝下的重力加速度深深牵引着我们,俯冲的时候我们叫喊着,咆哮着,嘶吼着,身心全部都感受着重力加速,咣!超大的浪花被我们激起来了,溅起十多米,丫的坑爹的是浪花不只是超前喷的啊,特么的它在空中划了一道优美的弧线直接想瓢泼一样泼到了我们身上啊,尤其是第一排啊第一排,最惨烈啊,尼玛我当初怎么就选了第一排啊,这不是坑爹吗?我真的是穿了雨衣,但是我屁股底下的雨衣不够长啊,水落到了座位上,然后水就浸到我的裤子里面去了啊,一阵凉意袭来,嗖嗖嗖,我屁股湿了,湿了。坑爹啊,这雨衣抵了个毛用啊。随后船缓缓划回去,出来的时候我走出来,屁股底下湿了一片,像尿了裤子一样,怎么能这样啊?我才玩了一个就湿了,湿了啊,╭(╯^╰)╮ 怎么这样。 还好太阳好,我就叉开腿走着,自己想象!赶紧的太阳把我的裤子晒干吧。接下来我们去了维苏威火山,听说这个还不错。果然,走到旁边就听到了火山里面一群人的嘶吼,开始排队吧,人也不大多啊,应该还是人品好的缘故吧,嗖嗖就排上了。PO 个图先 3e4c18d1g778440eee0a9&690 对,就是这么酷炫,这就是传说中的过山车,一个个小匣子接起来,我和花花坐在了一个匣子里,抓紧扶手,小车就开动了,呼呼呼,伴随着轮子的摩擦,伴随着我们的尖叫,在急转弯的地方,车呼啸而过,整个人有种呼之欲出的感觉,巨大的离心力简直爽到爆。一个又一个急转,一次又一次的钻洞,一声又一声的呐喊,一下又一下的心跳。自行想象! 出来之后旁边有个火流星,可是排队的人太多了,机智的我们就继续绕,看到有一个影视特技演出室刚开放,就跑进去了。尼玛一进去就是一个人在表演剁手,当然那刀是假的,duang,duang,duang,加了特效而已,后来欣赏了一点电影特效制作的过程,整体不算经验刺激,还说得过去吧。 后来继续绕,到了未来警察和生命之光那里,可惜的是都没有到开放的点,就去了星际航班,这下排队的人真多,到底什么鬼,干嘛的。我先 PO 个图 别看这个外面有个火箭什么的,这么酷炫,其实进去啊,我总算见识到了,排了半个小时队,最后二十个人坐在一个舱里,这个舱是可以动的,上下左右倾斜,可以模拟一下加速度什么的,然后整个前面就是一个超大的圆形幕布,像放电影一样,来给你模拟一个飞行的过程。随着画面的切换,舱左右摆动,也算是逼真。出来之后看到外面这个火箭,我本来还真以为要飞天呢,结果给我整这么一出,我也是醉醉的。 接下来就是恐龙危机了,一走过去吓我一跳啊,这是什么玩意翻了啊,上面的字怎么还是倒着的?好吧,经过缓过神来,不得不佩服这建筑真是溜啊,真是设计师逼死建筑师的典范啊,来来来,看图你就知道我为啥这么说了,尼玛这整个建筑都被恐龙掀翻了好嘛? XocG88iM553THvvg 虽然外面看着是倒着的,不过进去之后是正着的。呸,我这不废话么,倒着谁能走啊?进去也是想刚才那样,几个人坐在一个小车里,有个跑道,小车会在跑道上走,有时候转来转去。而四周当然就是恐龙咯,带上 4D 眼镜,能够看到周围银幕上一只只恐龙扑过来,小车上还有各种音效。小车有时候没油了,有时候加速了,有时候甩尾了,有时候弹飞了,模拟得非常逼真。特么的走到一个地方上面突然出现一个霸王龙头啊,这次不是屏幕出来的啊,是实物啊,一动一动的,它的牙像魔鬼的爪牙,还迈着魔鬼的步伐,在我不经意间粗线了啊,不能忍,特么的还长着嘴朝着我吼啊,吓尿了有木有。晃来晃去,这破小车终于抵达了传说中的安全地带,我也解脱出来了,╭(╯^╰)╮。臭霸王龙。 哎呦终于出来了,这也不能光玩室内的啊,来点室外的体验一把。于是乎,我们走到了一个叫波浪翻滚的东西那里,我了个去,这不就是济南泉城公园里面的遨游太空加强版吗? 遨游太空是这样子的 20150504202138 这里的波浪翻滚是这样子的,这何止是一样啊,这简直就是一样啊! 1278644413015_1_o 坐在那上面,就尽情地摇摆摇摆吧,飞上来,飞上去,有时候这玩意还是倾斜的啊,哦飞天的感觉真爽哈哈哈! 接下来就是传说中的大摆锤了,据说好多人都在这一关不行不行的了,上吐下泻的统统地都有。我记得还有人说买了一张票一开始就坐了个大摆锤,直接不行了,后面的项目都玩不了了,合着 220 块钱就坐了个这个,真值!我还是准备挑战一下,会会它,看看这玩意到底多么厉害。先 PO 图一张。 20131119GAig3 嗯,就是这个样子滴,它一边摆,一边转啊,最开始摆的角度是很小的,后来最高的时候都有 90 度了吧,我一边承受着离心力,一边又在半空中飘荡,简直有一种超凡脱俗的感觉。啊,当到达最高点的时候,我整个人横在了半空中同时围绕着中间的转轴在转啊,转啊。神马姿态,多么优美啊!我当时脑子里在想神马?我还活着吗?神呐。 最终,我还是活着下来了,然后又来了一个刺激的,体验蹦极的感觉。PO 图一张,先提前感受一下。 624188preview4 对,就是它!太空飞梭,哈哈哈哈,体验一下从 100 米高空坠落的感觉吧!上去坐在座位上绑好了,深呼吸一下,就在那一瞬间,就是那一瞬间,座位以一个超快的速度飞到架子的顶端啊!注意它不是缓缓上去的啊,是突然地一下,升到最上面,只需要短短几秒,那一瞬间啊,我的天,周围的人的呐喊,小周周的咆哮,花花的哭喊混成一团,就像火箭升空的感觉一样。然后,座位在空中停住了,尼玛这时候我们已经升到了最高的地方了啊。还不够形象,再来一张图!QQ截图20150504211315 将近一百米的高空,我们坐在座位上,腿是悬空的,朝下一瞅你可能就会昏死过去。然后,然后!这座位突然不受力了,直接自由落体!我们也跟随着座位飞下来,呐喊声,咆哮声混作一团,而我也在撕吼,啊啊啊啊。真正的自由落体,感受到了吗?而且它接下来还没完啊,又升上去,然后又来一次降下来。呼呼呼呼呼,简直了!!嗯,最后我还是活着下来了。 其实,这个还真的不够刺激,然后我们又来了一个刺激的,空中飞舞,哈哈哈,堪称一绝了。先 PO 个图感受一下。 简直了,这个巨大的翅上都坐着人,翅子这玩意不仅是自己在转,而且这个长臂在左右上下摇摆,尼玛可想而知我们的人在天上是波浪翻滚啊,朝上甩,朝下甩,朝左甩,朝右甩,有时倒这甩,有时正着甩。有没有玩过小时候的波浪鼓,我们这些人就想波浪鼓上面的两个圆球,各种甩,各种浪,在上面想不喊都不行,在上面我们的撕吼声简直是无与伦比,有时真的感觉自己就要被甩出去的样子,自己感受一下吧。这个简直是爽哭我了,花花直接吓得没敢坐,我和小周周真是感受到了这玩意的魅力,太浪了,也是浪到不行不行的。 接下来坐了坐勇敢者转盘,就是它!就是它!罪该万死的家伙!直接把我转晕了! 68a8f2b5-02d5-4fda-87cb-ae5fca294f04 别看他只是个转盘拉着一个个小车在那里转,其实它离心力特别大,我的腿在小车里简直都抬不起来。看到这个转盘下面的支撑臂了没?一开始它是平的啊,也就是说我们最开始是在水平面上转的,随着时间推移,这个臂慢慢往上升,升到将近 90 度的样子,尼玛我就简直被离心力牵引着在转啊,你可以想想我在最高处就是脚朝上头朝下在转啊转啊转啊转。而且离心力没那么大的话我还是斜着的好嘛?我的脖子也在不由自主地斜着,出来的时候真是转得我晕死了,脖子差点没正过来啊尼玛。 就是因为这个破玩意,我走路都开始晕了,趴桌子上一会,碎一觉,就没事了,起来接着浪! 恩,然后接下来就是飞越极限了,这个效果简直赞啊。排队的人虽然多,但是一次性进去 50 多个人一起飞。坐在椅子上,脚是悬空的,在眼前会有一个超大超大的银幕,先 PO 图一张。 W020140505433553648700 应该就是这种感觉吧,一排排人坐着,座位可以模拟飞行时的感受,比如模拟加速,左右转等等的动作,相当逼真,而且还吹着风,模拟一下风中飞行的感觉。当然最享受的还是眼前的超大的幕布,一幅幅 3D 效果的画面,有艾菲尔铁塔,金字塔,故宫,长城,布达拉宫,大峡谷,珠穆朗玛峰,东方明珠等等,风景特别多。在飞行的过程中带你穿越世界各地,游遍千山万水。实在是太棒,不得不赞一个。像的看了一个超大的电影偶也! 恩,最最最重头戏来了,火流星!号称是最惊险刺激的一个项目,如果你一开始坐了这个,其他的真的一点意思都没有,所以我们把这个放在了最后。来张图先感受一下。 20131216191455574 哈哈哈,害怕了吧,整个人都是坐在座位上脚是完全悬空的,图上的人在以一个超快的速度在做空中大反转,我估计他们差点就要被甩出去了。来张全图感受一下。 QQ截图20150504215457 恩,这跑道有正的有倒的,最高处差不多有三十四米。花花没有一起坐这个,我和小周周去尝试了。绑好之后,我们在那条长直道上慢慢地往上升,尼玛在升的过程中其实是比较慢的,十米,二十米,三十米,升的过程中我的脚下是悬空的,没有任何踩踏的地方,朝下一看,整个就是一大悬崖啊,啊啊啊。我想,一声我爱过这个世界足以表达我当时的感受。接着升到最高地方,就立即向下俯冲了,尼玛啊,简直就是自由落体一般的速度啊,而且这轨道它还不是一个平稳的正圆啊,跑道就像麻花一样向前延伸着,而我们就一边在自转一边向下俯冲,我的妈啊,全车的人都在喊叫,整个在穿梭的过程中肩膀被死死压着,一个巨大的扭转力扭动着我的身体,身体向前前进的同时在做大回环运动。这还不算完,降落到最低点之后接着又是一个上升,一个弧形的 360 度大回环跑道,那时我们的脚完全是在上方,头在下方,死死地卡在座位上,巨大的离心力给我的身体难以承受之重。图中那么长的跑道,跑完需要多久?2 分钟?1 分钟?不,只需要 30 秒!30 秒啊!那速度简直太快了,一个劲的酸爽,真能到达极限了。下来之后,感觉整个人都接受了全身的按摩,浑身的酥麻感觉,也是爽到不行不行的! 火流星,真是名副其实,就像太空穿梭一遍回来了一样,无法比拟的刺激。嗯,坐完这个心里就踏实了。哈哈。 后来就是几个比较放松的项目了,海螺湾,生命之光,转转杯,放松体验一下,最后爬了一下方特城堡,也算是圆满了。 晚上在方特门口拍了拍照,买了个西部牛仔帽子,留作纪念。方特之旅正式圆满结束!偶也! 最后 PO 图几张,留作纪念!真是开心!愉快! 再见!方特!再见!泰安!么么哒! QQ截图20150504221857 QQ截图20150504222004 QQ截图20150504222028QQ截图20150504221741

Other

综述

优秀的代码风格如同一身得体的打扮,能够给人以良好的印象。初学程序设计,首先必须建立良好的编程习惯,这其中就包括代码风格。本文就代码风格中的几个重点问题进行了讨论,并在文后给出了一份优秀的代码作为风格模板。代码风格不必花费太多专门的时间研究,在使用中不断模仿模板代码,轻轻松松就能写出“专业的代码”。

80字符,代码行极限

无论时空怎么转变,世界怎样改变,一行80字符应始终铭记心间。古老的Unix终端以80列的格式显示文本,为了让源代码与手册具有最佳的可读性, Unix系统始终坚持着80列的传统。80列不多不少,足够写出一行有意义的代码,同时也足够显示在终端屏幕,足够打印在A4纸上。虽然时至今日,我们的屏幕分辨率早已足够高,一行能够显示的内容远超超过80字符,但我们的优秀传统已经形成──几乎所有的Unix/Linux内核源代码以及联机用户手册都严格地遵守着80列极限。如果你正好在使用Windows平台下的Dev C++,你是否有注意到代码编辑框里那条细细的灰色竖线?不错,那正是代码行极限。除了HTML、XML等冗长繁复的标记式语言,几乎所有的语言都需要严格遵守代码行极限,这包括C、C++、Java、C#、Python、PHP等等。不过有时,比如当PHP跟HTML打交道的时候,这个限制是可以暂时放松的。过长的代码行总是不好的,好的代码要始终保持苗条的身材。

Tab还是Space,众说纷纭的缩进方式

代码离不开缩进,关于缩进主要有两个争论,一个是该用空格(Space)还是用制表符(Tab),另外一个是该用4格缩进还是8格缩进甚至都不是。 先来谈谈Space与Tab的问题。坚持用Space的程序员会告诉你,如果你从来都不用Tab,那么你的代码放到所有的地方看都是一样的。没错,这是用Space缩进的优点,可惜的是,这是它唯一的优点。代码层次越多,内层代码最前面的缩进便越多,这意味着你需要敲很多很多次空格。即使你能忍受不厌其烦地按空格键直到它坏掉,你也一定会被IDE总是自作聪明地插入一些Tab字符的行为烦恼不已。建议总是使用Tab缩进,因为几乎所有的代码(不仅仅是C 代码)都在使用Tab缩进。 Tab到底是4格还是8格?这是Tab缩进会被某些人诟病的根源。当你写程序时使用的Tab大小与别人读程序时使用的Tab大小不同时,再漂亮的排版也会变得杂乱无章。标准的Tab是8格的,而不幸的是,几乎所有的Windows平台下的IDE,包括 Visual Studio、Dev C++,甚至跨平台的Eclipse等,都默认使用4格Tab。我使用的FreeBSD系统的所有的内核源代码都采用8格缩进,所以我一直坚持使用8格缩进。也许你不习惯太大的间距,如果不是在Unix平台下,或者不是C语言,那就采用4格Tab吧。如果你在Unix下编写C代码,使用8格的标准Tab是更好的习惯。

折行原则,容易被忽略的角落

既然有代码行极限,很多情况下我们不得不断开一个完整的代码行,这就带来了一个问题:折行后应该如何缩进?好的做法是,第一次折行后,在原来缩进的基础上增加1/2的Tab大小的空格,之后的折行全部对齐第二行。可能这样的文字描述过于晦涩了,还是举个例子罢(以8格缩进为例):

1
2
3
4
5
6
7
if (value > a && value > b && value > c && value < d && value < e && value < f
value < h && value < h) { /* 注意折行后的缩进 */
value = value + 1;
value = value * value * value * value * value * value * value * value
* value * value + value * value * value * value * value * value
* value * value; /* 注意再次折行后的缩进 */
}

显然这个段代码没有任何实际用处,只是为了说明折行缩进而编造的。

无处不在的空格,无处不在的空行

需要空格的位置有:

  • if、while、switch等关键字与之后的左括号(之间。
  • 左花括号{之前。
  • 双目运算符两侧,例如p == NULL。
  • 逗号,与分号;之后,例如for (i = 0; i < 10; i++)。

不要空格的位置有:

  • 函数名与之后的左括号(,包括带参数的宏与之后的左括号(,例如max(a, b)。
  • 分号;与冒号:之前。
  • 左括号(右边,右括号)左边,例如if (p == NULL)。

需要空行的位置有:

  • 函数的定义之前、函数的定义之后
  • 一组联系紧密的代码段之前和之后

这些规则并不完全,当你碰到上面没有列举出来的情况时,请参考本文提供的模板代码。

左花括号的争议──换行乎?不换乎?

这又是一个仁者见仁智者见智的问题了。从使代码更清晰的角度看,作为代码段开头标识的左花括号{应该另起一行:

1
2
3
4
5
if (p == NULL)
{
printf("error!\n");
exit(0);
}

可是,这看起来实在不够紧凑,所以大部分的C代码(至少Unix上如此)都采用了这样的方式:

1
2
3
4
if (p == NULL) {
printf("error!\n");
exit(0);
}

我的建议是采用后者,这会使你的代码显得更加紧凑,也更加专业。需要说明一个特例,在定义函数时,我们总是要给左花括号 { 换行:

1
2
3
4
static int maxint(int a, int b)
{
return a > b ? a : b;
}

额,不过我还是不太习惯额

总结

本文介绍了编程的代码风格,希望对大家有帮助 本文内容为转载,原文出处

HTML

目前响应式布局和扁平化越来越风靡,前端 UI 框架也是风生水起,在这里分享几款优秀的前端 UI 框架

BootStrap

相比这个大家都听说过吧,Bootstrap,来自 Twitter,是目前最受欢迎的前端框架。Bootstrap 是基于 HTML、CSS、JAVASCRIPT 的,它简洁灵活,使得 Web 开发更加快捷。 20150427144535 官网链接 中文链接

Foundation

与 Bootstrap 相比,Foundation 屈居第二,不过这并不能说明它不受欢迎。 ZURB 在后面强有力的支持,使得 Foundation 更加强大!Foundation 被各大网站使用,如 Facebook、Mozilla、Ebay、 Yahoo、美国国家地理网站等等。 20150427154906 官网链接 中文链接

Semantic Ui

语义化设计的前端框架,为攻城师而制作的可复用的开源前端框架。 20150427155432 官网链接 中文链接

Amaze UI

Amaze UI 是一个移动优先的跨屏前端框架。… Amaze UI 以移动优先(Mobile first)为理念,从小屏逐步扩展到大屏,最终实现所有屏幕适配,适应移动互联潮流。 20150427155625 官网链接

Purecss

免费响应式 CSS 前端框架是有 Yahoo 发布的一个可以应用到所有 web 项目的小巧的、响应式的 CSS 模块集,Pure 是建立在 Normalize.css 的基础上的,对 HTML 元素提供了布局和样式,增加了一些公共的并且是你需要的 UI 组件。 20150427160204 官网链接 中文链接

JavaScript

综述

最近做项目的时候发现了一个非常奇怪的问题,就是对于click事件的响应。经过测试发现,对于IOS平台,直接监听click事件可能是没有响应的,而在Android和PC上则完全没有问题。所以通过获取设备信息实现了不同平台的不同监听。

IOS监听

对于IOS设备,只监听click方法可能是没有响应的。解决方法就是监听 “touchend click”事件。 而对于Android和PC,则只监听click事件即可。

平台检测

我们利用userAgent来检测平台

1
2
3
4
5
6
7
8
9
if (/(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent)) {
//alert(navigator.userAgent);
alert("iOS");
} else if (/(Android)/i.test(navigator.userAgent)) {
//alert(navigator.userAgent);
alert("Android");
} else {
alert("PC");
};

上面的JS代码可以检测三个平台。

实现监听

我们可以把方法自定义名字,比如

1
2
3
4
5
function back_click(){
$(".group-names").show();
$(".groups:visible").hide();
$(this).hide();
}

然后跨平台实现监听

1
2
3
4
5
6
/* bind the event */
if (/(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent)) {
$(".back").bind({"touchend click":back_click});
}else{
$(".back").bind({"click":back_click});
}

通过以上监听便没有问题了。

总结

通过以上方法便可以实现不同平台的监听。

JavaScript

综述

这里我们来利用jQuery实现瀑布流效果。

在线演示

我们首先来在线演示一下效果,然后我们说一下是怎样的实现,点开链接进行预览吧 在线预览

代码分析

首先我们在页面中加入了一些图片元素,都加到id为container的元素中,每一张图片都在一个class为box的容器里,然后里面是一张图片。

1
2
3
4
5
6
7
<div id="container">
<div class="box">
<div class="content">
<img src="img/1.jpg">
</div>
</div>
</div>

在JS中,我们遍历了每一个box,实现box的位置为上一行高度最小的box的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function imgLocation(){
var box = $(".box");
var boxWidth = box.eq(0).width();
var num = Math.floor($(window).width()/boxWidth);
var boxArr=[];
box.each(function(index,value){
var boxHeight = box.eq(index).height();
if(index<num){
boxArr[index]= boxHeight;
}else{
var minboxHeight = Math.min.apply(null,boxArr);
var minboxIndex = $.inArray(minboxHeight,boxArr);
$(value).css({
"position":"absolute",
"top":minboxHeight,
"left":box.eq(minboxIndex).position().left
});
boxArr[minboxIndex]+=box.eq(index).height();
}
});
}

最后实现了滚动条滑动时的动态加载

1
2
3
4
5
6
7
function scrollside(){
var box = $(".box");
var lastboxHeight = box.last().get(0).offsetTop+Math.floor(box.last().height()/2);
var documentHeight = $(document).width();
var scrollHeight = $(window).scrollTop();
return (lastboxHeight<scrollHeight+documentHeight)?true:false;
}

调用如下

1
2
3
4
5
6
7
8
9
10
window.onscroll = function(){
if(scrollside()){
$.each(dataImg.data,function(index,value){
var box = $("<div>").addClass("box").appendTo($("#container"));
var content = $("<div>").addClass("content").appendTo(box);
$("<img>").attr("src","./img/"+$(value).attr("src")).appendTo(content);
});
imgLocation();
}
};

通过上述简单的设置我们就实现了瀑布流的效果。

源码下载

源码下载

综述

以上便是瀑布流效果的实现,希望对大家有一定帮助!

C/C++

综述

OpenGL 中的变换可以分为下面的三种: 即模型变换、投影变换、视口变换。 模型变换其实就相当于图形的几何变换,包括平移、缩放、旋转等操作,下面我们来详细研究一下 OpenGL 中三种操作的函数应用。

准备工作

在变换前我们首先要做一下准备工作,首先我们需要调用 glMatrixMode 函数来设置变换模式。 该方法介绍如下 glMatrixMode(Glenum mode),设置当前矩阵模式,它具有三个参数,分别为GL_MODELVIEWGL_PROJECTIONGL_TEXTURE。三个参数的含义为:

GL_MODELVIEW,对模型视景矩阵堆栈应用随后的矩阵操作,也就是针对本节的模型几何变换设置的模式,本节所有内容均为此模式。 GL_PROJECTION,对投影矩阵应用随后的矩阵操作,在投影变换中设置的模式。 GL_TEXTURE,对纹理矩阵堆栈应用随后的矩阵操作,在设置动态纹理的过程中设置的模式。

所以,在本节我们就需要设置如下的模式

1
glMatrixMode(GL_MODELVIEW);

接下来,我们需要设置

1
glLoadIdentity();

这个方法的作用是设置将当前的用户坐标系的原点移到了屏幕中心,类似于一个复位操作。 好,设置好以上两个条件之后我们就可以来进行变换啦。主要有以下三个函数:

1
2
3
glRotatef(angle,vx,vy,vz) //绕向量(vx,vy,vz)为轴旋转角度angle
glTranslate(dx,dy,dz) //平移,位移量为向量(dx,dy,dz)
glScalef(sx,sy,sz) //比例缩放,x,y,z 方向的缩放因子分别为sx,sy,sz

下面我们来详细说明这三个函数的用法。

平移

三种变换中,平移变换是最简单的。 类库中提供了下面两个方法:

glTranslated(GLdouble x,GLdouble y,GLdouble z) glTranslatef(GLfloat x,GLfloat y,GLfloat z)

这两种方法是基本是等价的,一种方法是传入 double 类型的参数,另一个是传入 float 类型的参数。 方法的作用是将物体分别沿 x,y,z 轴平移 x,y,z 的单位长度。 利用上一节的机器人我们来感受一下

1.向右平移 100 像素

1
glTranslated(100,0,0);

运行结果 20150422142910 可以看到,机器人向右平移了 100 像素

2.向左下分别平移 50,50 像素

1
glTranslated(-50,-50,0);

运行结果 20150422143102 可以看到,机器人分别向左和向下平移了 50 像素

3.向左下分别平移 50,50,向前平移 100

1
glTranslated(-50,-50,100);

运行结果 20150422143102 哦,和上一个类似,也就是说在 z 轴上平移没有什么效果吗?难道因为我画的是二维平面图形?哦不,那不应该是距离我的视线更近了所以看到的更大了吗?好吧,事实好像不是这样的,我就姑且认为二维平面在 z 轴上的平移是没有影响的吧。 恩,这个函数基本用法就是这样,通过传入不同的 x,y,z 值实现平移即可。

旋转

旋转的方法有下面两个:

glRotatef(GLdouble angle,GLdouble x,GLdouble y,GLdouble z) glRotatef(GLfloat angle,GLfloat x,GLfloat y,GLfloat z)

其中第一个参数是旋转的角度,后面三个参数 x,yz 参数确立了旋转轴,旋转轴是原点(0,0,0)与(x,y,z)的连线。 下面我们用几个例子来感受一下

1.绕 x 轴旋转 60 度

1
glRotated(60,10,0,0);

运行结果 20150422144433 恩,它变扁了,旋转中心在机器人的中心,也就是中间的小红点,设想一下,绕 x 轴旋转 60 度,的确我们看到的应该就是变扁的机器人

2.绕 z 轴旋转 90 度

1
glRotated(90,0,0,10);

20150422144851 可以看到,机器人绕中心点旋转了 90 度,是逆时针方向。

3.旋转平移结合

感受完上面的两个例子之后,重头戏来了,上面的例子中旋转轴是(0,0,0)和(x,y,z)两点的连线,也就是旋转轴总是通过坐标原点的。 那如果我们要让物体绕特定的旋转轴来旋转,比如绕(1,2,3)和(4,5,6)来旋转怎么办? 这里就要用到平移和旋转的组合了。

如果旋转轴通过(xp,yp,zp),而不通过坐标原点。假如旋转轴为(xp,yp,zp)和(xq,yq,zq)确立的。我们首先要将图形沿 x,y,z 方向分别平移-xp,-yp,-zp,然后旋转参数(x,y,z)分别传入(xq-xp,yq-yp,zq-zp)三个值,旋转完毕之后再把图形沿 x,y,z 方向分别平移 xp,yp,zp 个单位长度即可。

在这里,我的教材和指导书中的内容又发生了冲突,教材中说的是先沿 x,y,z 方向分别平移-xp,-yp,-zp,再沿 x,y,z 方向分别平移xp,yp,zp还原。而指导书中则是先沿 x,y,z 方向分别平移xp,yp,zp,再沿 x,y,z 方向分别平移-xp,-yp,-zp还原,到底谁对谁错呢?我们来一个小例子验证一下。 如果觉得无聊,可以自行忽略下面小段内容。 为了直观地表示,我们统一设 z 坐标为 0,在 xy 平面中观察变换过程。 假设我们有一个点(2,2,0),旋转的轴我们设为(1,2,0)和(4,5,0)的连线,那么该点绕轴旋转 180 度之后应该会是(1,3,0),从图上可以很直观地看出 111 那么在调用类库的时候,我们首先就要对这个点进行平移,然后旋转,然后再反平移恢复。

首先,我们按照第一种说法,即先沿 x,y,z 方向分别平移-xp,-yp,-zp,即(2,2,0)减去(1,2,0),变为了(1,0,0),然后传入(xq-xp,yq-yp,zq-zp)实现绕(0,0,0)与(xq-xp,yq-yp,zq-zp)两点连线为旋转轴来旋转 180 度,显然是绕(0,0,0)与(3,3,0)的连线来旋转的,这个点变为了(0,1,0),然后再沿 x,y,z 方向分别平移 xp,yp,zp 还原,即加(1,2,0),即加得到的结果是(1,3,0)。确实与图中点的符合,所以这一种说法验证正确。 另一种说法,即先沿 x,y,z 方向分别平移 xp,yp,zp,即(2,2,0)加上(1,2,0),变为了(3,4,0),然后传入(xq-xp,yq-yp,zq-zp)实现绕(0,0,0)与(xq-xp,yq-yp,zq-zp)两点连线为旋转轴来旋转 180 度,显然是绕(0,0,0)与(3,3,0)的连线来旋转的,这个点变为了(4,3,0),然后再沿 x,y,z 方向分别平移-xp,-yp,-zp 还原,即减去(1,2,0),即加得到的结果是(3,1,0)。与图中的点不符,所以这一种说法验证失败。

可见,我们得到的结果是

如果旋转轴通过(xp,yp,zp),而不通过坐标原点。假如旋转轴为(xp,yp,zp)和(xq,yq,zq)确立的。我们首先要将图形沿 x,y,z 方向分别平移-xp,-yp,-zp,然后旋转参数(x,y,z)分别传入(xq-xp,yq-yp,zq-zp)三个值,旋转完毕之后再把图形沿 x,y,z 方向分别平移 xp,yp,zp 个单位长度即可。

所以,我们要实现机器人绕两点(10,20,30)和(40,50,60)定义的轴线旋转 45 度,代码实现如下

1
2
3
4
glTranslatef(-10,-20,-30);
//(40,50,60)-(10,20,30)=(30,30,30)
glRotatef(45,30,30,30);
glTranslatef(10,20,30);

运行结果 0150422151947 运行结果可能不太直观,可以直接参考代码实现

缩放

对于缩放,我们的方法也是有两个

glScaled(GLdouble x,GLdouble y,GLdouble z) glScalef(GLfloat x,GLfloat y,GLfloat z)

区别也就是一个参数为 double 类型,一个为 float 类型 函数的作用是将物体分别在 x,y,z 轴方向缩放为 x,y,z 倍,缩放中心为原点 下面我们来几个小例子感受一下

1.沿 x 轴放大 2 倍

1
glScaled(2,1,1);

运行结果 20150422153045 画面好美!

2.沿 y 轴 z 轴分别放大 2 倍

1
glScaled(1,2,2);

运行结果 20150422153914 因为整个图形是一个平面图,所以我们只可以看出 z 轴放缩的效果

3.放缩和平移的变换

和旋转的原理一样,默认的放缩中心是原点,在这里,如果我们的放缩中心如果不是原点,我们该怎样设置呢? 假设放缩中心为(xp,yp,zp),方法如下:

如果我们的放缩中心设置为(xp,yp,zp),而不是坐标原点。我们首先要将图形沿 x,y,z 方向分别平移-xp,-yp,-zp,然后放缩参数(x,y,z)分别传入三个值代表放缩比例,放缩完毕之后再把图形沿 x,y,z 方向分别平移 xp,yp,zp 个单位长度还原。

下面,我们以(50,50,50)为放缩中心来对机器人的 xy 方向分别放缩 1.5 倍

1
2
3
glTranslated(-50,-50,0);
glScaled(1.5,1.5,0);
glTranslated(50,50,0);

运行结果 20150422155459 这样,我们就实现了以(50,50,0)为放缩原点来进行放大 1.5 倍的操作。

综述

在本篇我们描述了 OpenGL 中的平移旋转放缩变换操作,以及一些实例演示,希望对大家有帮助!

C/C++

综述

上一节我们利用种子填充算法实现了机器人的区域填充,我们可以发现,种子填充需要一定是时间,并不能在第一时间填充完毕。这里我们介绍一种更加简单的方法,我们利用 glPolygonMode 来绘制机器人,感受一下。

注意

我们曾经分别使用过类库和直线圆弧生成算法来绘制过机器人的线条。 类库绘制法:OpenGL 绘图实例一之机器人的绘制 算法绘制法:OpenGL 绘图实例二之直线和圆弧的绘制 填充算法也有两种,一种是种子填充算法,另一种就是本篇说的方法。 在这里我们可以归一下类,算法绘制线条方法和种子填充算法中的每一个点都是我们一点点绘制的,都是我们利用算法一点点计算的坐标来绘制,因此,可以把他俩归为一对。而类库绘制线条方法和本篇说的填充方法都是指定一部分点,类库为我们实现了填充,因此可以把他俩归为一对。下面就让我们来感受一下类库填充的魅力吧!

函数说明

1.函数原型

函数原型:void glPolygonMode(GLenum face, GLenum mode); 说明:控制一个多边形的正面和背面的绘图模式。表示多边形应该被画成点、轮廓还是填充形式。在默认情况下,多边形的正面和背面都被画成填充形式。 函数 glPolygonMode()用于改变当前的多边形绘制模式,它有两个参数。第一个参数指定哪些面将被这个调用,可能的值为 GL_FRONT、GL_BACK 或 GL_FRONT_AND_BACK,分别对应于前向多边形、后向多边形或两者。第二个参数指定将要使用的模式,可以为 GL_POINT、GL_LINE 或 GL_FILL。 下面我们来用一个小 Demo 来感受一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
glBegin(GL_TRIANGLES);
glVertex3f(‐0.8f,0.0f,0.0f);
glVertex3f(‐0.6f,0.0f,0.0f);
glVertex3f(‐0.7f,0.2f,0.0f);
glEnd();
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
glBegin(GL_TRIANGLES);
glVertex3f(0.1f,0.0f,0.0f);
glVertex3f(‐0.1f,0.0f,0.0f);
glVertex3f(0.0f,0.2f,0.0f);
glEnd();
glPolygonMode(GL_FRONT_AND_BACK, GL_POINT);
glBegin(GL_TRIANGLES);
glVertex3f(0.6f,0.0f,0.0f);
glVertex3f(0.8f,0.0f,0.0f);
glVertex3f(0.7f,0.2f,0.0f);
glEnd();

以上三种模式分别是绘制了填充的三角形,三角形的边界,三角形的三个点。

2.画多边形

在 glBegin 的参数传入 GL_POLYGON,绘制的时候所有的点就会连接而成一个多边形。

3.线条的粗细

1
glLineWidth(lineWidth);

利用这个方法,我们可以传入像素值大小,比如传入 2,画线的时候就是 2 像素的粗细。

绘制机器人

接下来我们利用这个方法来绘制机器人吧!我们可以注意到,定义了 glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)之后, 如果 glBegin 传入的是 GL_LINE_LOOP,绘制的仍然是封闭的曲线,如果 glBegin 传入的是 GL_POLYGON,绘制的则是封闭的填充的图形。 图片1 如果要实现上图的机器人,我们既要绘制黑色的边线,又要绘制带颜色的区块。所以我们就需要分别调用两种绘制模式,绘制的过程是完全相同的。所以,我们可以把模式当做一个变量来传入参数中改变绘制模式。比如之前的绘制三角形的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
//画三角形,传入三个点的坐标
void glTri(int x1,int y1,int x2,int y2,int x3,int y3){
//画封闭线
glBegin(GL_LINE_LOOP);
//一点
glVertex2d(x1,y1);
//二点
glVertex2d(x2,y2);
//三点
glVertex2d(x3,y3);
//结束画线
glEnd();
}

就可以改写为

1
2
3
4
5
6
7
8
9
10
11
12
13
//画三角形,传入三个点的坐标
void glTri(int x1,int y1,int x2,int y2,int x3,int y3,int MODE){
//画封闭线
glBegin(MODE);
//一点
glVertex2d(x1,y1);
//二点
glVertex2d(x2,y2);
//三点
glVertex2d(x3,y3);
//结束画线
glEnd();
}

在调用的时候我们只需要分别传入 GL_LINE_LOOP 和 GL_POLYGON 就可以了。 所有之前的方法改写如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
//画矩形,传入的是左下角XY坐标和右上角XY坐标
void glRect(int leftX,int leftY,int rightX,int rightY,int MODE){
//画封闭曲线
glBegin(MODE);
//左下角
glVertex2d(leftX,leftY);
//右下角
glVertex2d(rightX,leftY);
//右上角
glVertex2d(rightX,rightY);
//左上角
glVertex2d(leftX,rightY);
//结束画线
glEnd();
}

//画圆角矩形,传入矩形宽高,角半径,矩形中心点坐标
void glRoundRec(int centerX,int centerY,int width,int height,float cirR,int MODE){
//二分之PI,一个象限的角度
float PI_HALF = PI/2;
//划分程度,值越大画得越精细
float divide=20.0;
//圆角矩形的坐标
float tx,ty;
//画封闭曲线
glBegin(MODE);
//四个象限不同的操作符
int opX[4]={1,-1,-1,1};
int opY[4]={1,1,-1,-1};
//用来计数,从第一象限到第四象限
float x=0;
//x自增时加的值
float part=1/divide;
//计算内矩形宽高一半的数值
int w=width/2-cirR;
int h=height/2-cirR;
//循环画线
for(x=0;x<4;x+=part){
//求出弧度
float rad = PI_HALF*x;
//计算坐标值
tx=cirR*cos(rad)+opX[(int)x]*w+centerX;
ty=cirR*sin(rad)+opY[(int)x]*h+centerY;
//传入坐标画线
glVertex2f(tx,ty);
}
//结束画线
glEnd();
}

//画弧线,相对偏移量XY,开始的弧度,结束的弧度,半径
void glArc(double x,double y,double start_angle,double end_angle,double radius,int MODE)
{
//开始绘制曲线
glBegin(MODE);
//每次画增加的弧度
double delta_angle=PI/180;
//画圆弧
for (double i=start_angle;i<=end_angle;i+=delta_angle)
{
//绝对定位加三角函数值
double vx=x+radius * cos(i);
double vy=y+radius*sin(i);
glVertex2d(vx,vy);
}
//结束绘画
glEnd();
}


//画圆
void glCircle(double x, double y, double radius,int MODE)
{
//画全圆
glArc(x,y,0,2*PI,radius,MODE);
}

//画三角形,传入三个点的坐标
void glTri(int x1,int y1,int x2,int y2,int x3,int y3,int MODE){
//画封闭线
glBegin(MODE);
//一点
glVertex2d(x1,y1);
//二点
glVertex2d(x2,y2);
//三点
glVertex2d(x3,y3);
//结束画线
glEnd();
}

//画线,传入两点坐标
void glLine(int x1,int y1,int x2,int y2){
//画封闭线
glBegin(GL_LINE_STRIP);
//一点
glVertex2d(x1,y1);
//二点
glVertex2d(x2,y2);
//结束画线
glEnd();
}

那么我们实现了上面的方法之后。比如我现在要画一个矩形,就要传入 GL_LINE_LOOP 绘制一个边缘,然后再传入 GL_POLYGON 绘制一个填充的矩形,其中这个矩形的长宽等于边缘的长宽减去线的宽度(经过探索发现,应该减去线宽的一半才对,因为线的基准点是在线宽的中心的) 所以,我们可以定义一个画带边矩形的方法,比如

1
2
3
4
5
6
7
//画填充的矩形,传入左上角和右下角的坐标
void glFillRect(int leftX,int leftY,int rightX,int rightY,int color[3]){
glColor3ub(border[0],border[1],border[2]);
glRect(leftX,leftY,rightX,rightY,GL_LINE_LOOP);
glColor3ub(color[0],color[1],color[2]);
glRect(leftX+lineWidth/2,leftY-lineWidth/2,rightX-lineWidth/2,rightY+lineWidth/2,GL_POLYGON);
}

我们先定义了边界颜色绘制了边界,然后定义了图形的颜色来绘制填充的图形。其中 color 作为参数,可以传入不同的 RGB 值。 定义颜色 RGB 数组即可

1
2
3
4
5
6
int border[3]={0,0,0};
int grey[3]={195,195,195};
int yellow[3]={255,243,0};
int red[3]={237,28,36};
int darkGrey[3]={126,126,126};
int white[3]={255,255,255};

调用的时候我们只需要

1
glFillRect(-108,45,-81,0,darkGrey);

就可以绘制一个黑色边界的矩形了,填充色为暗灰色。 其他类似的方法实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//画填充的三角形,传值为逆时针,此法针对于机器人的正三角形
void glFillTri(int x1,int y1,int x2,int y2,int x3,int y3,int color[3]){
glColor3ub(border[0],border[1],border[2]);
glTri(x1,y1,x2,y2,x3,y3,GL_LINE_LOOP);
glColor3ub(color[0],color[1],color[2]);
glTri(x1+lineWidth/2,y1+lineWidth/2,x2-lineWidth/2,y2+lineWidth/2,x3,y3-lineWidth/2,GL_POLYGON);
}

//画填充的圆角矩形
void glFillRoundRec(int centerX,int centerY,int width,int height,float cirR,int color[3]){
glColor3ub(border[0],border[1],border[2]);
glRoundRec(centerX,centerY,width,height,cirR,GL_LINE_LOOP);
glColor3ub(color[0],color[1],color[2]);
glRoundRec(centerX,centerY,width-lineWidth/2,height-lineWidth/2,cirR,GL_POLYGON);
}

//画填充的圆形
void glFillCircle(double x, double y, double radius,int color[3]){
glColor3ub(border[0],border[1],border[2]);
glCircle(x,y,radius,GL_LINE_LOOP);
glColor3ub(color[0],color[1],color[2]);
glCircle(x,y,radius-lineWidth/2,GL_POLYGON);
}

恩,万事俱备只欠东风了,下面我们调用一下这几个函数直接进行绘制就好啦。 display 中的绘图调用如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
//函数用来画图
void display(void)
{
//GL_COLOR_BUFFER_BIT表示清除颜色
glClear(GL_COLOR_BUFFER_BIT);
glLineWidth(lineWidth);

//设置画线颜色
glColor3ub(0,0,0);
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
//画圆角矩形,大脸
glFillRoundRec(0,113,128,74,10,grey);
//两个眼睛
glFillCircle(-30,111,10,white);
glFillCircle(30,111,10,white);
//黑色线
glColor3ub(0,0,0);
//两条天线
glLine(-35,150,-35,173);
glLine(35,150,35,173);
//圆弧,画嘴
glArc(0,133,11*PI/8,13*PI/8,45,GL_LINE_STRIP);
//画矩形,脖子
glFillRect(-25,76,25,60,grey);
//圆角矩形,两个耳朵
glFillRoundRec(81,115,20,34,5,darkGrey);
glFillRoundRec(-81,115,20,34,5,darkGrey);
//画圆角矩形,大肚子
glFillRoundRec(0,0,146,120,15,grey);
//画三角,肚子里的三角
glFillTri(-30,-15,30,-15,0,28,yellow);
//画圆,中间小圈
glFillCircle(0,0,10,red);
//画矩形,胳膊连接处
glFillRect(-81,43,-73,25,grey);
glFillRect(73,43,81,25,grey);
//画矩形,上臂
glFillRect(-108,45,-81,0,darkGrey);
glFillRect(81,45,108,0,darkGrey);
//画矩形,中臂
glFillRect(-101,0,-88,-4,grey);
glFillRect(88,0,101,-4,grey);
//画矩形,下臂
glFillRect(-108,-4,-81,-37,darkGrey);
glFillRect(81,-4,108,-37,darkGrey);
//画圆形,手掌
glFillCircle(-95,-47,10,grey);
glFillCircle(95,-47,10,grey);
//画腿连接处
glFillRect(-41,-62,-21,-66,grey);
glFillRect(21,-62,41,-66,grey);
//画圆角矩形,大长腿
glFillRoundRec(-32,-92,38,52,10,darkGrey);
glFillRoundRec(32,-92,38,52,10,darkGrey);
//画矩形,脚踝
glFillRect(-41,-117,-21,-125,grey);
glFillRect(21,-117,41,-125,grey);
//画矩形,大脚掌
glFillRect(-59,-125,-8,-137,darkGrey);
glFillRect(8,-125,59,-137,darkGrey);
glFlush();
}

恩,这样就大功告成啦,是不是比种子填充算法简单多了?

运行结果

QQ截图20150421000536 恩,那些小白点就忽略吧!!这叫不拘小节!!哈哈哈!!

总结

本节我们利用了类库的方法来实现了颜色填充,是不是很简单?

C/C++

综述

博主研究了一下午加一晚上,终于把种子填充算法实现出来并把机器人填充完毕,路途很艰辛,不过也学到了很多,在此和大家一起分享。

吐槽

与我不是同学的小伙伴,请自动忽略,我是来吐槽教材的。 在此不得不吐槽一下,不得不说教材实在太坑爹了。对于种子填充算法的后半部分,下一个种子点的寻找过程中,从 while(x<=xright)开始,我实在无法搞懂它里面的神逻辑,最初我认为它是对的,后来按照它的思路实现之后,填充基本上是错误的,比如圆角矩形下方的部分,它就无法正常填充。根本原因还是它的下一步种子点找错了,而博主依然在固执地 DeBug,看看是不是我哪里编码有问题。后来,干脆放弃了书上的逻辑了,自己改写了搜寻下一个种子点的算法,最后终于成功。 另外,教材上的这些伪代码写得也是太伪,算了,这不是重点,言归正传。

基本梳理

在博主的研究过程中,遇到了许许多多的小问题,在这里统一做一下总结,也希望大家少走弯路,吸取我的经验教训。

1.点的定义

在这里我们避免不了要使用点,一个点包括了 2 个元素,一个是横坐标一个是纵坐标,所以我们可以直接把它定义为一个结构体。

1
2
3
4
5
struct Point
{
int x;
int y;
};

这样的话,我们就可以直接声明一个 Point 类型的变量使用了,既方便又直观。

2.栈的使用

对于种子填充算法,肯定避免不了使用栈的,在这里博主分享一下一些使用心得。 栈的引入 C++代码中,可以直接用下面的代码来导入

1
2
#include <stack>
using namespace std;

注意,这里一定记得加上 using namespace std 这句话,否则会出现 stack 未定义的错误,哈哈哈,深有体会。 栈的定义 引入了栈之后,我们就可以直接来声明一个栈了

1
stack<Point> pixelStack;

其中,需要加一个尖括号,尖括号中声明了 Point 类型,这样我们就可以使用它了 取栈顶元素 C++中取栈顶元素是很坑的,有一个 top 方法,还有一个 pop 方法。 其中 top 方法是只取得栈顶的元素而不移除它,pop 方法是直接移除栈顶元素,没有返回值。 所以我们要想取出栈顶元素并移除的话,就要分别调用这两个方法

1
2
3
4
//获取最顶端的元素
Point tempPoint=pixelStack.top();
//删除最顶端的元素
pixelStack.pop();

是不是不友好?不友好的话,那就自己去定义一个新方法吧,我就先不这么干啦。 判断栈非空 判断当前的栈是否已经为空,只需要调用 empty 方法就可以了

1
2
3
while(!pixelStack.empty()){
//code
}

这里是一个 while 循环,如果栈为非空的话不断循环。 关于栈置空 教材中的种子填充算法中用到了栈置空,不过我感觉没有必要这么做,因为在方法最前面是新声明的栈变量,它一定是空的。不过如果非要置空的话,可以利用下面的代码

1
2
3
while(!pixelStack.empty()){
pixelStack.pop();
}

如果栈不为空,就一直取元素,就可以把它置空啦。

3.关于 glColor3b 和 glColor3ub

这的确也是坑得博主不浅,之前一直在用 glColor3b 这个方法来定义颜色,奇怪的是 glColor3b(255,0,0) 竟然不是红色,而是黑色!就是因为这个颜色问题,导致我在比对颜色的过程中走了很多弯路。在这里做一下说明 glColor3b()需要传入的是 byte 类型,它的数值范围是-128-127,也就是有符号数,我传入 255,由于越界了,255 这个数就相当于-128,难怪不变红啊。 glColor3ub()需要传入的是 unsigned byte 类型,范围是 0-255,无符号数,那么在这里我们传入 255,0,0 这三个数,就变成红色了。

4.取得某像素颜色

我想说的是,这也是个深坑啊,一下午的 Debug 全归它身上了。 获取某个像素的这个函数是

1
void glReadPixels(GLint x,GLint y,GLsizesi width,GLsizei height,GLenum format,GLenum type,GLvoid *pixel);

函数说明如下:

该函数总共有七个参数。前四个参数可以得到一个矩形,该矩形所包括的像素都会被读取出来。(第一、二个参数表示了矩形的左下角横、纵坐标,坐标以窗口最左下角为零,最右上角为最大值;第三、四个参数表示了矩形的宽度和高度) 第五个参数表示读取的内容,例如:GL_RGB 就会依次读取像素的红、绿、蓝三种数据,GL_RGBA 则会依次读取像素的红、绿、蓝、alpha 四种数据,GL_RED 则只读取像素的红色数据(类似的还有 GL_GREEN,GL_BLUE,以及 GL_ALPHA)。如果采用的不是 RGBA 颜色模式,而是采用颜色索引模式,则也可以使用 GL_COLOR_INDEX 来读取像素的颜色索引。目前仅需要知道这些,但实际上还可以读取其它内容,例如深度缓冲区的深度数据等。 第六个参数表示读取的内容保存到内存时所使用的格式,例如:GL_UNSIGNED_BYTE 会把各种数据保存为 GLubyte,GL_FLOAT 会把各种数据保存为 GLfloat 等。 第七个参数表示一个指针,像素数据被读取后,将被保存到这个指针所表示的地址。注意,需要保证该地址有足够的可以使用的空间,以容纳读取的像素数据。例如一幅大小为 256*256 的图象,如果读取其 RGB 数据,且每一数据被保存为 GLubyte。

好了,那么重点来了,这个方法的坐标基准点是在画布的左下角!!而我们绘图的基准点是在画布的正中心!!所以我在获取某个点的颜色的时候一直都是错误的结果,这样的话在使用的时候我们的 xy 坐标值就要加上画布宽高的一半才能正常获取到像素的颜色,希望大家一定注意!! 那么我们如何来使用呢?实例如下,首先定义 GLByte 的数组

1
GLubyte iPixel[3];

另外还有画布的宽度高度的一半变量

1
int halfWidth,halfHeight;

我们可以调用如下的方法来获取(x,y)这个点像素的值

1
glReadPixels(x+halfWidth,y+halfHeight,1,1,GL_RGB,GL_UNSIGNED_BYTE,&iPixel);

在这里第五个参数我们定义了 GL_RGB,第六个参数我们定义了 GL_UNSIGNED_BYTE,最后是传入了数组的引用。 所以在调用这个方法之后,iPixel 数组里面的三个值就已经赋值为了该点的 RGB 值,可以拿来做下一步的判断。 比如我们边界可以定义为

1
GLubyte oldColor[3]={255,255,255};

在比较的时候就可以用下面的判别式

1
iPixel[0]!=borderColor[0]&&iPixel[1]!=borderColor[1]&&iPixel[2]!=borderColor[2]

这里我们是依次比较了三个 RGB 值是否与边界的 RGB 值相等,不过,有意思的是,识别颜色的这个方法,黑色的 RGB 值会识别成 1,1,1,而有时候在我调试的时候会识别为 0,1,1。我在想是不是系统计算误差问题,如果真是的话,因为这个小小的误差就影响了我们的判别条件岂不是亏大了?那么在这里我就定义了一个方法,允许一定的误差,这个误差姑且就称为 PS 里面的容差吧。

1
2
3
4
5
6
7
8
9
10
//传入两个颜色的RGB值,比较是否相同,容差为dis
bool sameColor(int r1,int g1,int b1,int r2,int g2,int b2){
//容差度
int dis = 10;
if(abs(r1-r2)<=dis&&abs(g1-g2)<=dis&&abs(b1-b2)<=dis){
return true;
}else{
return false;
}
}

那么我们的判定条件就改为了

1
!sameColor(iPixel[0],iPixel[1],iPixel[2],borderColor[0],borderColor[1],borderColor[2])

这样系统误差便不会影响了。

5.下一个种子点的选取

教材上的种子点选取算法有点搞不懂,我按照上面的思路实现出来,在填充的时候出现了一系列问题。后来干脆放弃了教材中的方法,自己改写了一下。 思路大体上是这样的。

在填充完一行后,这一行最左边的像素点我们定义为(xLeft,y),最右边的像素我们定义为(xRight,y),扫描上一行找寻下一个种子点,这里 y 就要增加 1,如果(xRight,y+1)这个点不是边界不是已经填充的点,那么这个点就可以作为种子点压入堆栈。如果这个点是边界或者是已经填充的点,那么就继续往左搜索,如果找到既不是边界又未填充的点,那么这个点就是种子点,压入堆栈。如果一直往左找到 xLeft 还是没有找到的话,就不存在下一个种子点了。下一行扫描线也是同样的原理,y 要在这个基础上减去 2 即可。

恩,不知道大家有没有看懂,这是我自己想出来的方法,不敢保证完全正确,在此仅供参考。 如果大家真的可以按照教材中的方法实现成功的话,希望告诉我一下,感激不尽。

方法实现

恩,重要的地方都已经点明了,下面就直接附上我的种子填充算法吧!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
//种子填充算法
void zzFill(int startX,int startY,int r,int g,int b){
stack<Point> pixelStack;
//x,y是给定的种子像素点,rgb就是要填充的颜色的RGB值
Point point = {startX,startY};
pixelStack.push(point);
int saveX;
int xRight,xLeft;
int x,y;
//如果栈不为空
while(!pixelStack.empty()){
//获取最顶端的元素
Point tempPoint=pixelStack.top();
//删除最顶端的元素
pixelStack.pop();
saveX=tempPoint.x;
x=tempPoint.x;
y=tempPoint.y;
glReadPixels(x+halfWidth,y+halfHeight,1,1,GL_RGB,GL_UNSIGNED_BYTE,&iPixel);
//如果没有到达右边界,就填充
while(!sameColor(iPixel[0],iPixel[1],iPixel[2],borderColor[0],borderColor[1],borderColor[2])){
glPoint(x,y,r,g,b);
x=x+1;
glReadPixels(x+halfWidth,y+halfHeight,1,1,GL_RGB,GL_UNSIGNED_BYTE,&iPixel);
}
xRight=x-1;
x=saveX-1;
glReadPixels(x+halfWidth,y+halfWidth,1,1,GL_RGB,GL_UNSIGNED_BYTE,&iPixel);
//如果没有到达左边界,就填充
while(!sameColor(iPixel[0],iPixel[1],iPixel[2],borderColor[0],borderColor[1],borderColor[2])){
glPoint(x,y,r,g,b);
x=x-1;
glReadPixels(x+halfWidth,y+halfWidth,1,1,GL_RGB,GL_UNSIGNED_BYTE,&iPixel);
}
//保存左端点
xLeft=x+1;
//从右边的点开始
x=xRight;
//检查上端的扫描线
y=y+1;
while(x>=xLeft){
glReadPixels(x+halfWidth,y+halfWidth,1,1,GL_RGB,GL_UNSIGNED_BYTE,&iPixel);
if(!sameColor(iPixel[0],iPixel[1],iPixel[2],borderColor[0],borderColor[1],borderColor[2])&&!sameColor(iPixel[0],iPixel[1],iPixel[2],r,g,b)){
//如果上方的点不是边界点,直接压入
Point p={x,y};
pixelStack.push(p);
//压入之后停止循环
break;
}else{
x--;
glReadPixels(x+halfWidth,y+halfWidth,1,1,GL_RGB,GL_UNSIGNED_BYTE,&iPixel);
}
}
//检查下端的扫描线
y=y-2;
//从右边的点开始
x=xRight;
while(x>=xLeft){
glReadPixels(x+halfWidth,y+halfWidth,1,1,GL_RGB,GL_UNSIGNED_BYTE,&iPixel);
if(!sameColor(iPixel[0],iPixel[1],iPixel[2],borderColor[0],borderColor[1],borderColor[2])&&!sameColor(iPixel[0],iPixel[1],iPixel[2],r,g,b)){
//如果上方的点不是边界点,直接压入
Point p={x,y};
//压入之后停止循环
pixelStack.push(p);
break;
}else{
x--;
glReadPixels(x+halfWidth,y+halfWidth,1,1,GL_RGB,GL_UNSIGNED_BYTE,&iPixel);
}
}
}
}

以上便是我实现的种子填充算法,仅供参考 在这里我们用到了 glPoint 画点的方法,这是我们定义的,方法如下,为了便于调试,每画一个点刷新一下,这样我们就可以看到绘制的全部动态效果。

1
2
3
4
5
6
7
8
9
//画点
void glPoint(int x,int y,int r,int g,int b){
glColor3ub (r,g,b);
glPointSize(1);
glBegin(GL_POINTS);
glVertex2i(x,y);
glEnd();
glFlush();
}

以上便是画点的函数

方法使用

种子填充算法肯定要在我们绘制完机器人之后使用,任意选取某个四连通区域的点,传入 xy 坐标值还有要填充的颜色的 RGB 值,就可以成功实现填充。 在上一篇机器人的基础上,我们在画机器人的方法最后加入下面的代码,即可实现填充。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//灰色:195,195,195
//黄色:255,243,0
//红色:237,28,36
//深灰色:126,126,126
//脖子
zzFill(0,70,195,195,195);
//头
zzFill(-50,110,195,195,195);
zzFill(0,93,195,195,195);
//肚子
zzFill(-50,0,195,195,195);
//耳朵
zzFill(-80,115,126,126,126);
zzFill(80,115,126,126,126);
//肚子三角
zzFill(-20,-10,255,243,0);
//肚子红色圆
zzFill(0,0,237,28,36);
//zzFill(-50,0,128,255,33);
//大臂
zzFill(-90,30,126,126,126);
zzFill(90,30,126,126,126);
//小臂
zzFill(-90,-20,126,126,126);
zzFill(90,-20,126,126,126);
//手
zzFill(-75,40,195,195,195);
zzFill(75,40,195,195,195);
//手
zzFill(-95,-47,195,195,195);
zzFill(95,-47,195,195,195);
//大腿连接处
zzFill(-40,-64,195,195,195);
zzFill(40,-64,195,195,195);
//大腿
zzFill(-40,-100,126,126,126);
zzFill(40,-100,126,126,126);
//脚踝
zzFill(-40,-121,195,195,195);
zzFill(40,-121,195,195,195);
//脚掌
zzFill(-40,-130,126,126,126);
zzFill(40,-130,126,126,126);
system("pause");

注意,有个很奇怪地方是绘制完了之后机器人就不见了,所以在这里加入了 system(“pause”)方法来暂停一下就好啦。 其他的代码基本都是上一篇中的了,大家自行整理。

运行结果

运行结果截图如下 20150419015002 恩,就是这样!

总结

以上便是博主利用种子填充算法来实现的机器人的颜色填充,在此分享给大家,希望对大家有帮助! 如有问题和错误,欢迎大家给予我批评和指正,谢谢!

福利专区

综述

小伙伴们总要有一些秘密是不能让别人知道的,之前我们使用的设置隐藏文件夹然后在控制面板设置不显示隐藏文件夹的方式都弱爆了,下面我们来用一种更高级的办法来设置隐藏文件夹,感受一下。

设置隐藏

首先我们创建一个文件夹,比如名字叫 SECRET,如图所示 20150415114405 接下来我们打开命令行,输入如下命令

1
attrib +s +h e:/SECRET

20150415114811 输入命令之后,我们再查看一下 E 盘的内容,刷新一下,记得刷新!20150415120601 恩,那个文件夹已经不见了,有人说设置一下控制面板就显示出来了。 好,我们试试,控制面板设置显示隐藏的文件夹 20150415120426 恩,我们再看看,它依然是找不到的,就是这样! 20150415120601 那么我们怎么找到它?很简单,访问的时候只要在输入文件夹名称就会找到它了。 比如,我在地址栏中输入 E:/SECRET,就可以找到它了 20150415115101 下面我们来介绍一下这个命令的用法

attrib 命令用来显示或更改文件属性。

ATTRIB [+R | -R] [+A | -A ] [+S | -S] [+H | -H] [[drive:] [path] filename] [/S [/D]] + 设置属性。 - 清除属性。 R 只读文件属性。 A 存档文件属性。 S 系统文件属性。 H 隐藏文件属性。 [drive:][path][filename] 指定要处理的文件属性。 /S 处理当前文件夹及其子文件夹中的匹配文件。 /D 也处理文件夹。

比如

1
attrib +a +s +r +h e:/SECRET

这句命令就是设置 E 盘的 SECRET 文件夹为存档文件、系统文件、只读文件、隐藏文件。

在这里我们只要设置为隐藏文件盒系统文件属性就可以完成文件夹的隐藏,是不是很酷炫?

取消隐藏

了解了上面的命令,取消隐藏就很简单啦,我们只需要输入下面的命令

1
attrib -h -s e:/SECRET

再刷新一下目录,SECRET 目录就又出现了。

20150415115831

恩,会了吧,小伙伴们还不赶紧试试。不用谢我,请叫我雷锋!