0%

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

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

  • 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://qiniu.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://qiniu.cuiqingcai.com/wp-content/uploads/2015/05/20150525111154.jpg';
imgs[1] = 'http://qiniu.cuiqingcai.com/wp-content/uploads/2015/05/20150525111447.jpg';
imgs[2] = 'http://qiniu.cuiqingcai.com/wp-content/uploads/2015/05/20150525112058.jpg';
imgs[3] = 'http://qiniu.cuiqingcai.com/wp-content/uploads/2015/05/20150525112112.jpg';
imgs[4] = 'http://qiniu.cuiqingcai.com/wp-content/uploads/2015/05/20150525112129.jpg';
imgs[5] = 'http://qiniu.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://qiniu.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://qiniu.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://qiniu.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,另外通过一个实例来演示了鼠标监听事件的用法,下一篇我们就利用这个知识来进行一个实例演示。大家加油!