0%

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,另外通过一个实例来演示了鼠标监听事件的用法,下一篇我们就利用这个知识来进行一个实例演示。大家加油!

HTML

综述

LESS 包含一套自定义的语法及一个解析器,用户根据这些语法定义自己的样式规则,这些规则最终会通过解析器,编译生成对应的 CSS 文件。LESS 并没有裁剪 CSS 原有的特性,更不是用来取代 CSS 的,而是在现有 CSS 语法的基础上,为 CSS 加入程序式语言的特性。 本文部分内容参考 官方文档

编译器

建议使用Koala编译器 下载地址

LESS 原理及使用方式

本质上,LESS 包含一套自定义的语法及一个解析器,用户根据这些语法定义自己的样式规则,这些规则最终会通过解析器,编译生成对应的 CSS 文件。LESS 并没有裁剪 CSS 原有的特性,更不是用来取代 CSS 的,而是在现有 CSS 语法的基础上,为 CSS 加入程序式语言的特性。下面是一个简单的例子:

清单 1. LESS 文件
1
2
3
4
5
6
7
8
@color: #4D926F; 

#header {
color: @color;
}
h2 {
color: @color;
}

经过编译生成的 CSS 文件如下:

清单 2. CSS 文件
1
2
3
4
5
6
#header { 
color: #4D926F;
}
h2 {
color: #4D926F;
}

从上面的例子可以看出,学习 LESS 非常容易,只要你了解 CSS 基础就可以很容易上手。 LESS 可以直接在客户端使用,也可以在服务器端使用。在实际项目开发中,我们更推荐使用第三种方式,将 LESS 文件编译生成静态 CSS 文件,并在 HTML 文档中应用。

客户端

我们可以直接在客户端使用 .less(LESS 源文件),只需要从 LESSCSS 下载 less.js 文件,然后在我们需要引入 LESS 源文件的 HTML 中加入如下代码:

1
<link rel="stylesheet/less" type="text/css" href="styles.less">

LESS 源文件的引入方式与标准 CSS 文件引入方式一样:

1
<link rel="stylesheet/less" type="text/css" href="styles.less">

需要注意的是:在引入 .less 文件时,rel 属性要设置为“stylesheet/less”。还有更重要的一点需要注意的是:LESS 源文件一定要在 less.js 引入之前引入,这样才能保证 LESS 源文件正确编译解析。

使用编译生成的静态 CSS 文件

我们可以通过 LESS 的编译器,将 LESS 文件编译成为 CSS 文件,在 HTML 文章中引入使用。这里要强调的一点,LESS 是完全兼容 CSS 语法的,也就是说,我们可以将标准的 CSS 文件直接改成 .less 格式,LESS 编译器可以完全识别。

语法

变量

LESS 允许开发者自定义变量,变量可以在全局样式中使用,变量使得样式修改起来更加简单。 我们可以从下面的代码了解变量的使用及作用:

清单 3. LESS 文件
1
2
3
4
5
@border-color : #b5bcc7; 

.mythemes tableBorder{
border : 1px solid @border-color;
}

经过编译生成的 CSS 文件如下:

清单 4. CSS 文件
1
2
3
.mythemes tableBorder { 
border: 1px solid #b5bcc7;
}

从上面的代码中我们可以看出,变量是 VALUE(值)级别的复用,可以将相同的值定义成变量统一管理起来。 该特性适用于定义主题,我们可以将背景颜色、字体颜色、边框属性等常规样式进行统一定义,这样不同的主题只需要定义不同的变量文件就可以了。当然该特性也同样适用于 CSS RESET(重置样式表),在 Web 开发中,我们往往需要屏蔽浏览器默认的样式行为而需要重新定义样式表来覆盖浏览器的默认行为,这里可以使用 LESS 的变量特性,这样就可以在不同的项目间重用样式表,我们仅需要在不同的项目样式表中,根据需求重新给变量赋值即可。 LESS 中的变量和其他编程语言一样,可以实现值的复用,同样它也有生命周期,也就是 Scope(变量范围,开发人员惯称之为作用域),简单的讲就是局部变量还是全局变量的概念,查找变量的顺序是先在局部定义中找,如果找不到,则查找上级定义,直至全局。下面我们通过一个简单的例子来解释 Scope。

清单 5. LESS 文件
1
2
3
4
5
6
7
8
9
10
11
@width : 20px; 
#homeDiv {
@width : 30px;
#centerDiv{
width : @width;// 此处应该取最近定义的变量 width 的值 30px
}
}
#leftDiv {
width : @width; // 此处应该取最上面定义的变量 width 的值 20px

}

经过编译生成的 CSS 文件如下:

清单 6. CSS 文件
1
2
3
4
5
6
#homeDiv #centerDiv { 
width: 30px;
}
#leftDiv {
width: 20px;
}

Mixins(混入)

Mixins(混入)功能对用开发者来说并不陌生,很多动态语言都支持 Mixins(混入)特性,它是多重继承的一种实现,在 LESS 中,混入是指在一个 CLASS 中引入另外一个已经定义的 CLASS,就像在当前 CLASS 中增加一个属性一样。 我们先简单看一下 Mixins 在 LESS 中的使用:

清单 7. LESS 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义一个样式选择器
.roundedCorners(@radius:5px) {
-moz-border-radius: @radius;
-webkit-border-radius: @radius;
border-radius: @radius;
}
// 在另外的样式选择器中使用
#header {
.roundedCorners;
}
#footer {
.roundedCorners(10px);
}

经过编译生成的 CSS 文件如下:

清单 8. CSS 文件
1
2
3
4
5
6
7
8
9
10
#header { 
-moz-border-radius:5px;
-webkit-border-radius:5px;
border-radius:5px;
}
#footer {
-moz-border-radius:10px;
-webkit-border-radius:10px;
border-radius:10px;
}

从上面的代码我们可以看出:Mixins 其实是一种嵌套,它允许将一个类嵌入到另外一个类中使用,被嵌入的类也可以称作变量,简单的讲,Mixins 其实是规则级别的复用。 Mixins 还有一种形式叫做 Parametric Mixins(混入参数),LESS 也支持这一特性:

清单 9. LESS 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定义一个样式选择器
.borderRadius(@radius){
-moz-border-radius: @radius;
-webkit-border-radius: @radius;
border-radius: @radius;
}
// 使用已定义的样式选择器
#header {
.borderRadius(10px); // 把 10px 作为参数传递给样式选择器
}
.btn {
.borderRadius(3px);// // 把 3px 作为参数传递给样式选择器

}

经过编译生成的 CSS 文件如下:

清单 10. CSS 文件
1
2
3
4
5
6
7
8
9
10
#header { 
-moz-border-radius: 10px;
-webkit-border-radius: 10px;
border-radius: 10px;
}
.btn {
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
}

我们还可以给 Mixins 的参数定义一人默认值,如

清单 11. LESS 文件
1
2
3
4
5
6
7
8
.borderRadius(@radius:5px){ 
-moz-border-radius: @radius;
-webkit-border-radius: @radius;
border-radius: @radius;
}
.btn {
.borderRadius;
}

经过编译生成的 CSS 文件如下:

清单 12. CSS 文件
1
2
3
4
5
.btn { 
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
border-radius: 5px;
}

像 JavaScript 中 arguments一样,Mixins 也有这样一个变量:@arguments。@arguments 在 Mixins 中具是一个很特别的参数,当 Mixins 引用这个参数时,该参数表示所有的变量,很多情况下,这个参数可以省去你很多代码。

清单 13. LESS 文件
1
2
3
4
5
6
7
8
.boxShadow(@x:0,@y:0,@blur:1px,@color:#000){ 
-moz-box-shadow: @arguments;
-webkit-box-shadow: @arguments;
box-shadow: @arguments;
}
#header {
.boxShadow(2px,2px,3px,#f36);
}

经过编译生成的 CSS 文件如下:

清单 14. CSS 文件
1
2
3
4
5
#header { 
-moz-box-shadow: 2px 2px 3px #FF36;
-webkit-box-shadow: 2px 2px 3px #FF36;
box-shadow: 2px 2px 3px #FF36;
}

Mixins 是 LESS 中很重要的特性之一,我们这里也写了很多例子,看到这些例子你是否会有这样的疑问:当我们拥有了大量选择器的时候,特别是团队协同开发时,如何保证选择器之间重名问题?如果你是 java 程序员或 C++ 程序员,我猜你肯定会想到命名空间 Namespaces,LESS 也采用了命名空间的方法来避免重名问题,于是乎 LESS 在 mixins 的基础上扩展了一下,看下面这样一段代码:

清单 15. LESS 文件
1
2
3
4
#mynamespace { 
.home {...}
.user {...}
}

这样我们就定义了一个名为 mynamespace 的命名空间,如果我们要复用 user 这个选择器的时候,我们只需要在需要混入这个选择器的地方这样使用就可以了。#mynamespace > .user。

嵌套的规则

在我们书写标准 CSS 的时候,遇到多层的元素嵌套这种情况时,我们要么采用从外到内的选择器嵌套定义,要么采用给特定元素加 CLASS 或 ID 的方式。在 LESS 中我们可以这样写:

清单 16. HTML 片段
1
2
3
4
5
6
7
<div id="home"> 
<div id="top">top</div>
<div id="center">
<div id="left">left</div>
<div id="right">right</div>
</div>
</div>
清单 17. LESS 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#home{ 
color : blue;
width : 600px;
height : 500px;
border:outset;
#top{
border:outset;
width : 90%;
}
#center{
border:outset;
height : 300px;
width : 90%;
#left{
border:outset;
float : left;
width : 40%;
}
#right{
border:outset;
float : left;
width : 40%;
}
}
}

经过编译生成的 CSS 文件如下:

清单 18. CSS 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#home { 
color: blue;
width: 600px;
height: 500px;
border: outset;
}
#home #top {
border: outset;
width: 90%;
}
#home #center {
border: outset;
height: 300px;
width: 90%;
}
#home #center #left {
border: outset;
float: left;
width: 40%;
}
#home #center #right {
border: outset;
float: left;
width: 40%;
}

从上面的代码中我们可以看出,LESS 的嵌套规则的写法是 HTML 中的 DOM 结构相对应的,这样使我们的样式表书写更加简洁和更好的可读性。同时,嵌套规则使得对伪元素的操作更为方便。

清单 19. LESS 文件
1
2
3
4
5
6
7
8
a { 
color: red;
text-decoration: none;
&:hover {// 有 & 时解析的是同一个元素或此元素的伪类,没有 & 解析是后代元素
color: black;
text-decoration: underline;
}
}

经过编译生成的 CSS 文件如下:

清单 20. CSS 文件
1
2
3
4
5
6
7
8
a { 
color: red;
text-decoration: none;
}
a:hover {
color: black;
text-decoration: underline;
}

运算及函数

在我们的 CSS 中充斥着大量的数值型的 value,比如 color、padding、margin 等,这些数值之间在某些情况下是有着一定关系的,那么我们怎样利用 LESS 来组织我们这些数值之间的关系呢?我们来看这段代码:

清单 21 . LESS 文件
1
2
3
4
5
@init: #111111; 
@transition: @init*2;
.switchColor {
color: @transition;
}

经过编译生成的 CSS 文件如下:

清单 22. CSS 文件
1
2
3
.switchColor { 
color: #222222;
}

上面的例子中使用 LESS 的 operation 是 特性,其实简单的讲,就是对数值型的 value(数字、颜色、变量等)进行加减乘除四则运算。同时 LESS 还有一个专门针对 color 的操作提供一组函数。下面是 LESS 提供的针对颜色操作的函数列表:

1
2
3
4
5
6
7
8
lighten(@color, 10%); // return a color which is 10% *lighter* than @color 
darken(@color, 10%); // return a color which is 10% *darker* than @color
saturate(@color, 10%); // return a color 10% *more* saturated than @color
desaturate(@color, 10%);// return a color 10% *less* saturated than @color
fadein(@color, 10%); // return a color 10% *less* transparent than @color
fadeout(@color, 10%); // return a color 10% *more* transparent than @color
spin(@color, 10); // return a color with a 10 degree larger in hue than @color
spin(@color, -10); // return a color with a 10 degree smaller hue than @color

PS: 上述代码引自 LESS CSS 官方网站,详情请见 http://lesscss.org/#-color-functions 使用这些函数和 JavaScript 中使用函数一样。

清单 23. LESS 文件
1
2
3
4
init: #f04615; 
#body {
background-color: fadein(@init, 10%);
}

经过编译生成的 CSS 文件如下:

清单 24. CSS 文件
1
2
3
#body { 
background-color: #f04615;
}

从上面的例子我们可以发现,这组函数像极了 JavaScript 中的函数,它可以被调用和传递参数。这些函数的主要作用是提供颜色变换的功能,先把颜色转换成 HSL 色,然后在此基础上进行操作,LESS 还提供了获取颜色值的方法,在这里就不举例说明了。 LESS 提供的运算及函数特性适用于实现页面组件特性,比如组件切换时的渐入渐出。

模式匹配

清单 25. LESS文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.tri{
width: 0;
height: 0;
overflow: hidden;
.triangle(bottom);
}

.triangle(top,@w:5px,@c:#CCC){
border-width: @w;
border-color: transparent transparent red transparent;
border-style: dashed dashed solid dashed;
}
.triangle(bottom,@w:5px,@c:#CCC){
border-width: @w;
border-color: red transparent transparent transparent;
border-style: solid dashed dashed dashed;
}
清单 26. CSS文件
1
2
3
4
5
6
7
8
.tri {
width: 0;
height: 0;
overflow: hidden;
border-width: 5px;
border-color: red transparent transparent transparent;
border-style: solid dashed dashed dashed;
}

通过以上可以看出,我们可以在LESS中定义相同名字的样式,第一个参数不同,在调用时可以通过传入第一个参数进行匹配,得到不同的样式即可。

避免编译

清单27. LESS文件
1
2
3
.test{
width:calc(300px-30px);
}
清单28. CSS文件
1
2
3
.test {
width: calc(270px);
}

有时候一些样式如果编译了之后就会产生不必要的麻烦,如果我们不想要编译,可以改写如下

清单29. LESS文件
1
2
3
.test{
width:~'calc(300px-30px)';
}
清单30. CSS文件
1
2
3
.test {
width: calc(300px-30px);
}

通过在前面加上一个 ~然后将内容用引号引起来,就可以避免编译了。

总结

本文提到的只是 LESS 的基本功能,更高级的功能如:字符串插值,服务器端使用配置,JavaScript 表达式,避免编译等可以参看 LESS 的官方网站。 LESS 以 CSS 语法为基础,又借用了很多我们熟知编程式语言的特性,这对于我们开发人员来讲学习成本几乎可以忽略,它在保留 CSS 语法的基础上扩展了更多实用的功能,LESS 为我们提供了一种新的编写样式表的方法,我们可以根据我们的项目特性选择使用 LESS 的部分特性,我们只需用很少的成本就可以换了很大的回报,一句话,Less is more,借助 LESS 可以更便捷的进行 Web 开发。

个人日记

等啊等,终于等到五一放假,这次必须要出去好好玩一番,解脱一下,释放一下。经过一系列思想斗争,最终决定去泰安方特了!昨天方特之旅结束之后,甚是爽快,在此记流水账一篇,不喜勿喷哈! 话说回来还是要先说五月二号啊,简直了,本来打算是在五月二号去的,带上室友小周周和隔壁的花花学长。一号那天我定了第二天的火车,早上7点半的火车,室友小周周也提前约好了出租车,准备早上六点出发去火车站。万事俱备只欠东风了,当天晚上早早滚去睡觉,定好五点半的闹钟,准备静静等待闹钟的响起。可是,可是,一切都是来得那么突然,第二天一大早起来竟然下雨了!泰安那边下雨了!天哪,睡之前我还特意看了看二号是多云啊!这不是坑爹吗?下雨就下雨吧,可是你不知道下雨方特室外项目是不开放的好嘛!顿时心都凉了。╮(╯▽╰)╭ 哎,改签吧,小周周给那位出租司机打电话,我去重新改签票。嗯,又一个坑爹的事情发生了,7点之前网上购票是不开放的好嘛?发车前两小时是不能在网上改签和退票的好嘛?哎,算了,还好票便宜,才十二块五,索性我不要了!不要了!默默去买了三号的车票,特么的还是无座啊,无座。哎,多么痛的领悟,╮(╯▽╰)╭。 然后,然后,就默默等到了三号,还是五点半的闹钟,这次终于不下雨了!苍天啊!带上手机,耳机,money,学僧证,充电宝,六点出门坐上TAXI,朝火车站奔去,这次是真快啊这速度,路上都没车的,一路狂奔,竟然六点半多点就到了火车站了,可是我们七点半的火车啊,这不是坑爹吗?于是乎,我们决定改签一下提前一点,有个七点稍多的一趟车,于是乎我们兴冲冲地跑向了改签大厅,惊呆了!怎么它就那么长的队伍,哦你,那么长,么长,长。试着排了排,二十分钟往前走了不到十步,也是醉了。哎,默默走掉,去候车室等着吧,七点半就七点半! 呼呼呼,检票咯,K771,我们走上站台,看到一条漫长的绿皮车,不是说好的无座吗?怎么这么多空座,我们三个找到一个就坐下来,不得不再次吐槽一下这种座位真难受。早上好困,睡觉睡觉,带上耳机听着歌,像打坐一样睡着了,就那么睡着了,我好佩服我自己。睡了一个小时,一睁眼,泰山站到了,不错不错,下车,出发。 出站了,果然不出我所料,好多人都拿着牌子,问去方特吗?免费接送,立即出发,同时卖门票,220一位,其实方特门票就是220的,学生票可能就便宜10块钱。哎,这样就这样吧,他们是打着免费接送的名义,实质钱都包含在门票里面了。我们也担心方特人特别多,到时候买票排个时候,就直接跟着他走吧,反正也贵不了多少。 车是一个公交车,等人来齐了一起发车出发,我们几个竟然赶到了第一波,进去的时候车是空的,要等到其他人来才发车,这不是坑爹吗?坑爹吗?说好的立马出发的呢?算了,不计较这些了,其实等了也没多久就走了。在车上交了钱买了门票,那个收钱的人(姑且叫导游吧)跟我们讲了方特的各个注意事项,还是挺贴心的嘛!在这里透露给大家一点,记不住的我就记不住了。 包包最好要寄存起来,最好寄存在方特外面,比较便宜,当然也可以背着如果你不嫌沉的话。 贵重物品一定要看管好,方特里面丢的最多的就是数码相机,然后是手机,保管好了就行咯。 如果你不想一开始就玩惊险刺激的,建议从右边开始玩,如果你一开始就想玩刺激的,建议还是从右边开始玩哈哈哈。 项链啊首饰啊最好要收起来,要不然你想体验在空中翻滚的时候项链打到牙的感受吗? 哦,其他的忘了,我也不知道他说了多少了,可能比较健忘,记不起来还有啥了。后面的我就根据我的亲身体验来说了,哈哈。 走了半个多小时,终于到了!我们去存上包,买好雨衣以防神秘河谷的时候淋湿。正式进门!进门!冲冲冲!方特之旅正式开始!差不多九点半! 放个地图先 20150504150649 恩,进去看过去就是一个方特城堡,好有艺术气息的,旁边还放着欢乐的音乐,走在小路上,还有阳光照射着,哈哈不错。而且关键是路上人好少啊,哈哈,是不是五月三号外地的都回家了呢?是吗?后面你就知道了。 4d6577b5ga2451346b0fc&690&690 恩,我们没有进城堡玩,直接奔向右边的飞跃极限。噢丫,谁说的人少的?谁说的人少的?人都跑进来了,排队啊,一层、两层、三层…不数了,心好累。也就十来层吧。排个毛线,我们直接冲向了下一站,神秘河谷。PO个图先 25540577,1681257058& 简直是人品好,我们进去的时候竟然排到了前面,是不是刚开放就被我们赶上了呢?而且,我们赶上了第一波。这次在外面买的雨衣派上用场了,穿上我们的半透明红色雨衣,带上蓝色高筒鞋套,上船。船一排四个人,一共好像是五排吧,我们很果断地选择了第一排,雨衣全部裹起来。准备冲,看到图了吗?这是一个小船从河谷里面二十米高空俯冲下来的样子,看那浪花。这次我们真正地感受到了它的魅力。河谷里面其实是有两个小下坡的,第一次比较小,杀杀你的锐气,第二次就是图中这个超大的下坡,从楼顶的那个位置冲下来,自己想象!第一次下坡还好,等到第二次下坡的时候,啊啊啊啊,二三十米高空俯冲,那种朝下的重力加速度深深牵引着我们,俯冲的时候我们叫喊着,咆哮着,嘶吼着,身心全部都感受着重力加速,咣!超大的浪花被我们激起来了,溅起十多米,丫的坑爹的是浪花不只是超前喷的啊,特么的它在空中划了一道优美的弧线直接想瓢泼一样泼到了我们身上啊,尤其是第一排啊第一排,最惨烈啊,尼玛我当初怎么就选了第一排啊,这不是坑爹吗?我真的是穿了雨衣,但是我屁股底下的雨衣不够长啊,水落到了座位上,然后水就浸到我的裤子里面去了啊,一阵凉意袭来,嗖嗖嗖,我屁股湿了,湿了。坑爹啊,这雨衣抵了个毛用啊。随后船缓缓划回去,出来的时候我走出来,屁股底下湿了一片,像尿了裤子一样,怎么能这样啊?我才玩了一个就湿了,湿了啊,╭(╯^╰)╮ 怎么这样。 还好太阳好,我就叉开腿走着,自己想象!赶紧的太阳把我的裤子晒干吧。接下来我们去了维苏威火山,听说这个还不错。果然,走到旁边就听到了火山里面一群人的嘶吼,开始排队吧,人也不大多啊,应该还是人品好的缘故吧,嗖嗖就排上了。PO个图先 3e4c18d1g778440eee0a9&690 对,就是这么酷炫,这就是传说中的过山车,一个个小匣子接起来,我和花花坐在了一个匣子里,抓紧扶手,小车就开动了,呼呼呼,伴随着轮子的摩擦,伴随着我们的尖叫,在急转弯的地方,车呼啸而过,整个人有种呼之欲出的感觉,巨大的离心力简直爽到爆。一个又一个急转,一次又一次的钻洞,一声又一声的呐喊,一下又一下的心跳。自行想象! 出来之后旁边有个火流星,可是排队的人太多了,机智的我们就继续绕,看到有一个影视特技演出室刚开放,就跑进去了。尼玛一进去就是一个人在表演剁手,当然那刀是假的,duang,duang,duang,加了特效而已,后来欣赏了一点电影特效制作的过程,整体不算经验刺激,还说得过去吧。 后来继续绕,到了未来警察和生命之光那里,可惜的是都没有到开放的点,就去了星际航班,这下排队的人真多,到底什么鬼,干嘛的。我先PO个图 别看这个外面有个火箭什么的,这么酷炫,其实进去啊,我总算见识到了,排了半个小时队,最后二十个人坐在一个舱里,这个舱是可以动的,上下左右倾斜,可以模拟一下加速度什么的,然后整个前面就是一个超大的圆形幕布,像放电影一样,来给你模拟一个飞行的过程。随着画面的切换,舱左右摆动,也算是逼真。出来之后看到外面这个火箭,我本来还真以为要飞天呢,结果给我整这么一出,我也是醉醉的。 接下来就是恐龙危机了,一走过去吓我一跳啊,这是什么玩意翻了啊,上面的字怎么还是倒着的?好吧,经过缓过神来,不得不佩服这建筑真是溜啊,真是设计师逼死建筑师的典范啊,来来来,看图你就知道我为啥这么说了,尼玛这整个建筑都被恐龙掀翻了好嘛? XocG88iM553THvvg 虽然外面看着是倒着的,不过进去之后是正着的。呸,我这不废话么,倒着谁能走啊?进去也是想刚才那样,几个人坐在一个小车里,有个跑道,小车会在跑道上走,有时候转来转去。而四周当然就是恐龙咯,带上4D眼镜,能够看到周围银幕上一只只恐龙扑过来,小车上还有各种音效。小车有时候没油了,有时候加速了,有时候甩尾了,有时候弹飞了,模拟得非常逼真。特么的走到一个地方上面突然出现一个霸王龙头啊,这次不是屏幕出来的啊,是实物啊,一动一动的,它的牙像魔鬼的爪牙,还迈着魔鬼的步伐,在我不经意间粗线了啊,不能忍,特么的还长着嘴朝着我吼啊,吓尿了有木有。晃来晃去,这破小车终于抵达了传说中的安全地带,我也解脱出来了,╭(╯^╰)╮。臭霸王龙。 哎呦终于出来了,这也不能光玩室内的啊,来点室外的体验一把。于是乎,我们走到了一个叫波浪翻滚的东西那里,我了个去,这不就是济南泉城公园里面的遨游太空加强版吗? 遨游太空是这样子的 20150504202138 这里的波浪翻滚是这样子的,这何止是一样啊,这简直就是一样啊! 1278644413015_1_o 坐在那上面,就尽情地摇摆摇摆吧,飞上来,飞上去,有时候这玩意还是倾斜的啊,哦飞天的感觉真爽哈哈哈! 接下来就是传说中的大摆锤了,据说好多人都在这一关不行不行的了,上吐下泻的统统地都有。我记得还有人说买了一张票一开始就坐了个大摆锤,直接不行了,后面的项目都玩不了了,合着220块钱就坐了个这个,真值!我还是准备挑战一下,会会它,看看这玩意到底多么厉害。先PO图一张。 20131119GAig3 嗯,就是这个样子滴,它一边摆,一边转啊,最开始摆的角度是很小的,后来最高的时候都有90度了吧,我一边承受着离心力,一边又在半空中飘荡,简直有一种超凡脱俗的感觉。啊,当到达最高点的时候,我整个人横在了半空中同时围绕着中间的转轴在转啊,转啊。神马姿态,多么优美啊!我当时脑子里在想神马?我还活着吗?神呐。 最终,我还是活着下来了,然后又来了一个刺激的,体验蹦极的感觉。PO图一张,先提前感受一下。 624188preview4 对,就是它!太空飞梭,哈哈哈哈,体验一下从100米高空坠落的感觉吧!上去坐在座位上绑好了,深呼吸一下,就在那一瞬间,就是那一瞬间,座位以一个超快的速度飞到架子的顶端啊!注意它不是缓缓上去的啊,是突然地一下,升到最上面,只需要短短几秒,那一瞬间啊,我的天,周围的人的呐喊,小周周的咆哮,花花的哭喊混成一团,就像火箭升空的感觉一样。然后,座位在空中停住了,尼玛这时候我们已经升到了最高的地方了啊。还不够形象,再来一张图!QQ截图20150504211315 将近一百米的高空,我们坐在座位上,腿是悬空的,朝下一瞅你可能就会昏死过去。然后,然后!这座位突然不受力了,直接自由落体!我们也跟随着座位飞下来,呐喊声,咆哮声混作一团,而我也在撕吼,啊啊啊啊。真正的自由落体,感受到了吗?而且它接下来还没完啊,又升上去,然后又来一次降下来。呼呼呼呼呼,简直了!!嗯,最后我还是活着下来了。 其实,这个还真的不够刺激,然后我们又来了一个刺激的,空中飞舞,哈哈哈,堪称一绝了。先PO个图感受一下。 简直了,这个巨大的翅上都坐着人,翅子这玩意不仅是自己在转,而且这个长臂在左右上下摇摆,尼玛可想而知我们的人在天上是波浪翻滚啊,朝上甩,朝下甩,朝左甩,朝右甩,有时倒这甩,有时正着甩。有没有玩过小时候的波浪鼓,我们这些人就想波浪鼓上面的两个圆球,各种甩,各种浪,在上面想不喊都不行,在上面我们的撕吼声简直是无与伦比,有时真的感觉自己就要被甩出去的样子,自己感受一下吧。这个简直是爽哭我了,花花直接吓得没敢坐,我和小周周真是感受到了这玩意的魅力,太浪了,也是浪到不行不行的。 接下来坐了坐勇敢者转盘,就是它!就是它!罪该万死的家伙!直接把我转晕了! 68a8f2b5-02d5-4fda-87cb-ae5fca294f04 别看他只是个转盘拉着一个个小车在那里转,其实它离心力特别大,我的腿在小车里简直都抬不起来。看到这个转盘下面的支撑臂了没?一开始它是平的啊,也就是说我们最开始是在水平面上转的,随着时间推移,这个臂慢慢往上升,升到将近90度的样子,尼玛我就简直被离心力牵引着在转啊,你可以想想我在最高处就是脚朝上头朝下在转啊转啊转啊转。而且离心力没那么大的话我还是斜着的好嘛?我的脖子也在不由自主地斜着,出来的时候真是转得我晕死了,脖子差点没正过来啊尼玛。 就是因为这个破玩意,我走路都开始晕了,趴桌子上一会,碎一觉,就没事了,起来接着浪! 恩,然后接下来就是飞越极限了,这个效果简直赞啊。排队的人虽然多,但是一次性进去50多个人一起飞。坐在椅子上,脚是悬空的,在眼前会有一个超大超大的银幕,先PO图一张。 W020140505433553648700 应该就是这种感觉吧,一排排人坐着,座位可以模拟飞行时的感受,比如模拟加速,左右转等等的动作,相当逼真,而且还吹着风,模拟一下风中飞行的感觉。当然最享受的还是眼前的超大的幕布,一幅幅3D效果的画面,有艾菲尔铁塔,金字塔,故宫,长城,布达拉宫,大峡谷,珠穆朗玛峰,东方明珠等等,风景特别多。在飞行的过程中带你穿越世界各地,游遍千山万水。实在是太棒,不得不赞一个。像的看了一个超大的电影偶也! 恩,最最最重头戏来了,火流星!号称是最惊险刺激的一个项目,如果你一开始坐了这个,其他的真的一点意思都没有,所以我们把这个放在了最后。来张图先感受一下。 20131216191455574 哈哈哈,害怕了吧,整个人都是坐在座位上脚是完全悬空的,图上的人在以一个超快的速度在做空中大反转,我估计他们差点就要被甩出去了。来张全图感受一下。 QQ截图20150504215457 恩,这跑道有正的有倒的,最高处差不多有三十四米。花花没有一起坐这个,我和小周周去尝试了。绑好之后,我们在那条长直道上慢慢地往上升,尼玛在升的过程中其实是比较慢的,十米,二十米,三十米,升的过程中我的脚下是悬空的,没有任何踩踏的地方,朝下一看,整个就是一大悬崖啊,啊啊啊。我想,一声我爱过这个世界足以表达我当时的感受。接着升到最高地方,就立即向下俯冲了,尼玛啊,简直就是自由落体一般的速度啊,而且这轨道它还不是一个平稳的正圆啊,跑道就像麻花一样向前延伸着,而我们就一边在自转一边向下俯冲,我的妈啊,全车的人都在喊叫,整个在穿梭的过程中肩膀被死死压着,一个巨大的扭转力扭动着我的身体,身体向前前进的同时在做大回环运动。这还不算完,降落到最低点之后接着又是一个上升,一个弧形的360度大回环跑道,那时我们的脚完全是在上方,头在下方,死死地卡在座位上,巨大的离心力给我的身体难以承受之重。图中那么长的跑道,跑完需要多久?2分钟?1分钟?不,只需要30秒!30秒啊!那速度简直太快了,一个劲的酸爽,真能到达极限了。下来之后,感觉整个人都接受了全身的按摩,浑身的酥麻感觉,也是爽到不行不行的! 火流星,真是名副其实,就像太空穿梭一遍回来了一样,无法比拟的刺激。嗯,坐完这个心里就踏实了。哈哈。 后来就是几个比较放松的项目了,海螺湾,生命之光,转转杯,放松体验一下,最后爬了一下方特城堡,也算是圆满了。 晚上在方特门口拍了拍照,买了个西部牛仔帽子,留作纪念。方特之旅正式圆满结束!偶也! 最后PO图几张,留作纪念!真是开心!愉快! 再见!方特!再见!泰安!么么哒! QQ截图20150504221857 QQ截图20150504222004 QQ截图20150504222028QQ截图20150504221741

Other

综述

优秀的代码风格如同一身得体的打扮,能够给人以良好的印象。初学程序设计,首先必须建立良好的编程习惯,这其中就包括代码风格。本文就代码风格中的几个重点问题进行了讨论,并在文后给出了一份优秀的代码作为风格模板。代码风格不必花费太多专门的时间研究,在使用中不断模仿模板代码,轻轻松松就能写出“专业的代码”。

80字符,代码行极限

无论时空怎么转变,世界怎样改变,一行80字符应始终铭记心间。古老的Unix终端以80列的格式显示文本,为了让源代码与手册具有最佳的可读性, Unix系统始终坚持着80列的传统。80列不多不少,足够写出一行有意义的代码,同时也足够显示在终端屏幕,足够打印在A4纸上。虽然时至今日,我们的屏幕分辨率早已足够高,一行能够显示的内容远超超过80字符,但我们的优秀传统已经形成──几乎所有的Unix/Linux内核源代码以及联机用户手册都严格地遵守着80列极限。如果你正好在使用Windows平台下的Dev C++,你是否有注意到代码编辑框里那条细细的灰色竖线?不错,那正是代码行极限。除了HTML、XML等冗长繁复的标记式语言,几乎所有的语言都需要严格遵守代码行极限,这包括C、C++、Java、C#、Python、PHP等等。不过有时,比如当PHP跟HTML打交道的时候,这个限制是可以暂时放松的。过长的代码行总是不好的,好的代码要始终保持苗条的身材。

Tab还是Space,众说纷纭的缩进方式

代码离不开缩进,关于缩进主要有两个争论,一个是该用空格(Space)还是用制表符(Tab),另外一个是该用4格缩进还是8格缩进甚至都不是。 先来谈谈Space与Tab的问题。坚持用Space的程序员会告诉你,如果你从来都不用Tab,那么你的代码放到所有的地方看都是一样的。没错,这是用Space缩进的优点,可惜的是,这是它唯一的优点。代码层次越多,内层代码最前面的缩进便越多,这意味着你需要敲很多很多次空格。即使你能忍受不厌其烦地按空格键直到它坏掉,你也一定会被IDE总是自作聪明地插入一些Tab字符的行为烦恼不已。建议总是使用Tab缩进,因为几乎所有的代码(不仅仅是C 代码)都在使用Tab缩进。 Tab到底是4格还是8格?这是Tab缩进会被某些人诟病的根源。当你写程序时使用的Tab大小与别人读程序时使用的Tab大小不同时,再漂亮的排版也会变得杂乱无章。标准的Tab是8格的,而不幸的是,几乎所有的Windows平台下的IDE,包括 Visual Studio、Dev C++,甚至跨平台的Eclipse等,都默认使用4格Tab。我使用的FreeBSD系统的所有的内核源代码都采用8格缩进,所以我一直坚持使用8格缩进。也许你不习惯太大的间距,如果不是在Unix平台下,或者不是C语言,那就采用4格Tab吧。如果你在Unix下编写C代码,使用8格的标准Tab是更好的习惯。

折行原则,容易被忽略的角落

既然有代码行极限,很多情况下我们不得不断开一个完整的代码行,这就带来了一个问题:折行后应该如何缩进?好的做法是,第一次折行后,在原来缩进的基础上增加1/2的Tab大小的空格,之后的折行全部对齐第二行。可能这样的文字描述过于晦涩了,还是举个例子罢(以8格缩进为例):

1
2
3
4
5
6
7
if (value > a && value > b && value > c && value < d && value < e && value < f
value < h && value < h) { /* 注意折行后的缩进 */
value = value + 1;
value = value * value * value * value * value * value * value * value
* value * value + value * value * value * value * value * value
* value * value; /* 注意再次折行后的缩进 */
}

显然这个段代码没有任何实际用处,只是为了说明折行缩进而编造的。

无处不在的空格,无处不在的空行

需要空格的位置有:

  • if、while、switch等关键字与之后的左括号(之间。
  • 左花括号{之前。
  • 双目运算符两侧,例如p == NULL。
  • 逗号,与分号;之后,例如for (i = 0; i < 10; i++)。

不要空格的位置有:

  • 函数名与之后的左括号(,包括带参数的宏与之后的左括号(,例如max(a, b)。
  • 分号;与冒号:之前。
  • 左括号(右边,右括号)左边,例如if (p == NULL)。

需要空行的位置有:

  • 函数的定义之前、函数的定义之后
  • 一组联系紧密的代码段之前和之后

这些规则并不完全,当你碰到上面没有列举出来的情况时,请参考本文提供的模板代码。

左花括号的争议──换行乎?不换乎?

这又是一个仁者见仁智者见智的问题了。从使代码更清晰的角度看,作为代码段开头标识的左花括号{应该另起一行:

1
2
3
4
5
if (p == NULL)
{
printf("error!\n");
exit(0);
}

可是,这看起来实在不够紧凑,所以大部分的C代码(至少Unix上如此)都采用了这样的方式:

1
2
3
4
if (p == NULL) {
printf("error!\n");
exit(0);
}

我的建议是采用后者,这会使你的代码显得更加紧凑,也更加专业。需要说明一个特例,在定义函数时,我们总是要给左花括号 { 换行:

1
2
3
4
static int maxint(int a, int b)
{
return a > b ? a : b;
}

额,不过我还是不太习惯额

总结

本文介绍了编程的代码风格,希望对大家有帮助 本文内容为转载,原文出处