-
tcp/ip协议学习 第六章 ICMP:Internet控制报文协议
干嘛的
在我看来, ICMP协议主要就是为了互相传递/查询一些基本信息, 大部分是传递一些错误信息.
比如A发送UDP信息到B的10000端口, 但B的10000端口并没有开放, 就会回一个ICMP包给A, 告诉A10000端口未开放.
基本的查询信息, 比如最常用的ping命令, 就是发送ICMP包到目的主机, 然后等待目的主机的响应(响应也是ICMP包).
协议
协议定义的非常简单. ICMP在IP层上面一层. 前面是20个字节的IP头, 然后就是ICMP头.
ICMP头, 截图来自TCP/IP协议详解卷一
类型和代码两个字段的组合决定了这个ICMP包的用途, 比如我们常用的ping就是0,0组合和8,0组合. 具体如下:
各种类型的ICMP报文, 截图来自TCP/IP协议详解卷一
代码放上
好像没有什么好说的. 直接代码放上吧. 实现了一下书中的例子, 一个是查询子网掩码, 一个是查询时间. github地址, 点我点我
端口不可达
这也是书中的一个例子. 比如A发送UDP信息到B的10000端口, 但B的10000端口并没有开放, 就会回一个ICMP包给A, 告诉A10000端口未开放.
来看一下效果.用瑞士军刀发送个UDP消息到192.168.0.108的10000端口.
% nc -u 192.168.0.108 10000 ✭ abcd
同时开一个Tcpdump监听看看:
# tcpdump -vvv -x -nn icmp ✭ tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes 01:48:01.420363 IP (tos 0x0, ttl 64, id 45430, offset 0, flags [DF], proto ICMP (1), length 56) 192.168.0.108 > 192.168.0.104: ICMP 192.168.0.108 udp port 10000 unreachable, length 36 IP (tos 0x0, ttl 64, id 42558, offset 0, flags [DF], proto UDP (17), length 33) 192.168.0.104.60181 > 192.168.0.108.10000: [no cksum] UDP, length 5 0x0000: 4500 0038 b176 4000 4001 072a c0a8 006c 0x0010: c0a8 0068 0303 eac9 0000 0000 4500 0021 0x0020: a63e 4000 4011 1269 c0a8 0068 c0a8 006c 0x0030: eb15 2710 000d 0000
ping
-
kibana4打包和安装
对于Kibana4,官方只提供了一个打包好的JAVA的包(现在已经是用nodejs了,使用很方便,修改源码也很方便,所以后面这些话忽略吧). 如果想自己修改一些代码添加一些自定义功能, impossible. 至少我还是希望能像Kibana3一样,就是一普通的hmlt静态网站,放在nginx下面跑. 可以添加一些自己的panel. 好吧, 虽然Kibana4好像已经不需要添加什么panel了,但改改css, html总行吧. 而且还可以利用nginx做一些权限控制什么的.
虽然github有源码了,但做为一个新手, 对于grunt这些东西只是有最最最基本的一些了解,还是折腾了一会才搞定. 纪录一下.
以下操作都是基于8cef836c61b5749164156e19accb2a0881e902e4这个commit
-
从github下载kibana4代码.
git clone [email protected]:elasticsearch/kibana.git
-
默认你已经有node了, 如果没有, 用apt-get 或者 yum 或者brew等工具装上. 然后先把bower grunt装好, 接下来就要用.
npm install -g bower grunt-cli
-
进入kibana目录. 我没有在master分支, 我是切换到了v4.0.0-beta2分支上面.
cd kibana git checkout v4.0.0-beta2
-
安装需要的npm包.
其实, 我不确定是不是所有的包都要装, 我只是要用grunt build一下.甚至只是生成css文件而已.
这里要注意, 如果是用官方源, 而你身在大陆的话, 那就等死吧. 淘宝源在此npm install
-
bower install
安装需要的js css包. 从github下载. 对于大陆电信30M用户来说, 也是极其痛苦, 一个400K的包死活就是下载不下来.
osx系统的bower cache文件夹好像在这里/private/var/folders/j9/37cyszz92cg1xkfc46cl5w5r0000gn/T/yourusername/bower 浏览器明明能下载下来, bower就是死活不行. 没办法, 先用浏览器下载之后放在cache文件夹里面bower install
- grunt
可以看到默认是跑了两个任务.
grunt.registerTask(‘default’, [‘jshint:source’, ‘less’]); less生成css文件. jshint大概就是检查一下JS语法错误吧.
grunt
-
grunt build
其实生成css之后, 把src/kibana放在nginx下面就可以跑了.
build, 就是把一些文件合并压缩一下, 加速用户访问. -
config 配置.
grunt之后, 会在目录下生成一个config文件, 这是一个json文件, 里面是kibana4的一些配置. 里面的kibanaIndex需要改成kibana_index这种格式. 这个生成的原始配置文件应该是nodejs用的, nodejs应该会转成kibana_index这种格式, 但我们现在不用Nodejs做后端服务器了, 所以需要用人改一下.
{ "request_timeout": 60, "kibana_index": ".kibana", "bundled_plugin_ids": [ "plugins/dashboard/index", "plugins/discover/index", "plugins/settings/index", "plugins/table_vis/index", "plugins/vis_types/index", "plugins/visualize/index" ], "default_app_id": "discover", "host": "0.0.0.0", "shard_timeout": 30000, "plugins": [ "plugins/dashboard/index", "plugins/discover/index", "plugins/settings/index", "plugins/table_vis/index", "plugins/vis_types/index", "plugins/visualize/index" ], "port": 5601, "verifySSL": false }
-
还差一点点了.
到这里…居然还不行…
打开components/config/config.js, 可以看到:var configFile = JSON.parse(require('text!config')); configFile.elasticsearch = (function () { var a = document.createElement('a'); a.href = 'elasticsearch'; return a.href; }());
最重要的elasticsearch的配置, 不是放在配置文件里, 而是写在代码里面了. 这里改成真正的elasticsearch的地址就可以了.
- k4要求ES版本至少也是1.4. 但其实1.3版本的也可以正常使用. 我们就是1.3. 暂时也没打算升级. 限制的代码在index.js里面, 改成
constant('minimumElasticsearchVersion', '1.3.0')
就OK了.
OVER.
补充 (曾经的某个版本下还需要):
-
script_fileds
安全起见, 我们的ES禁用script.
但是k4有些请求, 至少在找index pattern的时候, 使用了script_fileds.
我简单粗暴的把components/courier/data_source/_abstract.js里面带script_fileds的代码注释了…
目前还没有发现副作用, 先这样用吧. -
要设置一个默认index pattern
新建index pattern之后, 要设置一个为默认. 否则k4有些2去找名叫.kibana这个index pattern, 当然找不到, 就报错了.
beta2还没这个要求.
我也不是完全确认就是这个原因造成的, 但的确设置一个默认之后就好了.
-
-
tcp/ip协议学习 第四章 ARP:地址解析协议
ARP干嘛的
曾经有段时间, 六七年前了吧. 本科的时候, 流行了一阵子ARP病毒攻击, 导致整个局域网都不能上网了. 当时只听说这个东西防不住, 只要有一个人中毒, 就导致所有人上不了网. 现在也终于知道这是怎么回事了, 也能手工让某个同学上不了网了, 咳咳.
大家应该也都知道ARP是干嘛的, 我再啰嗦一下.. 比如我访问了百度, 百度回了包给我, 百度只知道我的IP是什么,不知道我的MAC地址. 这个包到网关的时候, IP这一层再把数据交给下一层的链路层, 链路层不知道IP是什么东西的, 它只认MAC地址. 所以就需要把IP转成MAC地址, ARP请求就是做这个的.
就是说, 我可以通过这个协议广播问一下所有机器 , 谁的IP是XXX.XXX.XXX.XXX, 请把你的MAC地址告诉我. 这个IP是XXX.XXX.XXX.XXX的机器收到请求之后, 就会告诉我, XXX.XXX.XXX.XXX的MAC地址是啥啥啥.
不幸这个是基于互相信任的, 理论上大家都会相信别人说的是正确的. 但是, 我可以撒谎说, XXX.XXX.XXX.XXX是我, 我的MAC是啥啥啥. 然后本来应该到 XXX.XXX.XXX.XXX那里的数据包就到我这里来了. XXX.XXX.XXX.XXX不仅仅是断网了, 我还能窃听他的数据.
协议格式
要想伪造, 啊,不, 发送一个ARP请求或者应答, 一定要了解协议格式. 从RFC文件抄了一份.
To communicate mappings from <protocol, address> pairs to 48.bit Ethernet addresses, a packet format that embodies the Address Resolution protocol is needed. The format of the packet follows. Ethernet transmission layer (not necessarily accessible to the user): 48.bit: Ethernet address of destination 48.bit: Ethernet address of sender 16.bit: Protocol type = ether_type$ADDRESS_RESOLUTION Ethernet packet data: 16.bit: (ar$hrd) Hardware address space (e.g., Ethernet, Packet Radio Net.) 16.bit: (ar$pro) Protocol address space. For Ethernet hardware, this is from the set of type fields ether_typ$<protocol>. 8.bit: (ar$hln) byte length of each hardware address 8.bit: (ar$pln) byte length of each protocol address 16.bit: (ar$op) opcode (ares_op$REQUEST | ares_op$REPLY) nbytes: (ar$sha) Hardware address of sender of this packet, n from the ar$hln field. mbytes: (ar$spa) Protocol address of sender of this packet, m from the ar$pln field. nbytes: (ar$tha) Hardware address of target of this packet (if known). mbytes: (ar$tpa) Protocol address of target.
这个还是TCP/IP协议详解书里面的图好看一些, 且等我截个图来, 呃,截歪了.
其实里面有些值, 像”硬件地址长度”什么的, 是个变量, 后面的”发送端以太网地址”就是根据这个而变化 .就我们目前的应用和环境来说, 拿这个图就可以啦.
这里的长度单位是字节, 不是上一篇IP协议里面的bit了.详细解释我也copy一下吧. 也是出自TCP/IP协议详解一书.
以太网报头中的前两个字段是以太网的源地址和目的地址。目的地址为全 1的特殊地址是 广播地址。电缆上的所有以太网接口都要接收广播的数据帧。
两个字节长的以太网帧类型表示后面数据的类型。对于 ARP请求或应答来说,该字段的 值为0x0806。
形容词hardware(硬件)和protocol(协议)用来描述 ARP分组中的各个字段。例如,一个 ARP 请求分组询问协议地址(这里是 IP地址)对应的硬件地址(这里是以太网地址)。
硬件类型字段表示硬件地址的类型。它的值为 1即表示以太网地址。协议类型字段表示要 映射的协议地址类型。它的值为 0x0800即表示 IP地址。它的值与包含 IP数据报的以太网数据 帧中的类型字段的值相同,这是有意设计的(参见图 2-1)。
接下来的两个 1字节的字段,硬件地址长度和协议地址长度分别指出硬件地址和协议地址 的长度,以字节为单位。对于以太网上 IP地址的 ARP请求或应答来说,它们的值分别为 6和4。 操作字段指出四种操作类型,它们是 ARP请求(值
(值为3)和RARP应答(值为4)(我们在第5章讨论RARP)。这个字段必需的,因为 ARP请求 和ARP应答的帧类型字段值是相同的。
接下来的四个字段是发送端的硬件地址(在本例中是以太网地址)、发送端的协议地址 (IP地址)、目的端的硬件地址和目的端的协议地址。注意,这里有一些重复信息:在以太网的数据帧报头中和 ARP请求数据帧中都有发送端的硬件地址。
对于一个 ARP请求来说,除目的端硬件地址外的所有其他的字段都有填充值。当系统收到一份目的端为本机的 ARP请求报文后,它就把硬件地址填进去,然后用两个目的端地址分 别替换两个发送端地址,并把操作字段置为 2,最后把它发送回去。
来段程序吧
其它也没啥好说的了, 先来段程序吧.
广播一个ARP请求, 问一下网关的MAC地址是多少. 运行环境是ubuntu12.04. osx上面是不行的. 据说windows也不行. 搜索了好久, 也不知道mac上面怎么搞.
#!/usr/bin/env python # -*- coding: utf-8 -*- ''' 广播一个ARP请求, 问一下网关的MAC地址是多少. 运行环境是ubuntu12.04. osx上面是不行的. 据说windows也不行. 搜索了好久, 也不知道mac上面怎么搞. ''' import socket import struct def main(): st = struct.Struct('!6s 6s h h h b b h 6s 4s 6s 4s') GATEWAY = '192.168.1.1' MYIP = '192.168.1.8' MYMAC = '20:c9:d0:88:96:3f' dst_ethernet_addr = ''.join( [chr (int(e, 16)) for e in 'FF:FF:FF:FF:FF:FF'.split(':')]) protocol_type = 0x0806 hw_addr_space = 1 protocol_addr_space = 0x800 hw_addr_length = 6 protocol_addr_length = 4 op = 1 my_mac = ''.join([chr(int(e, 16)) for e in MYMAC.split(':')]) my_ip = socket.inet_aton(MYIP) target_hw_addr = ''.join( [chr (int(e, 16)) for e in '00:00:00:00:00:00'.split(':')]) des_ip = socket.inet_aton(GATEWAY) data = ( dst_ethernet_addr, my_mac, protocol_type, hw_addr_space, protocol_addr_space, hw_addr_length, protocol_addr_length, op, my_mac, my_ip, target_hw_addr, des_ip, ) packed_data = st.pack(*data) s = socket.socket(socket.PF_PACKET, socket.SOCK_RAW, socket.SOCK_RAW) s.bind(('eth0', socket.SOCK_RAW)) # 下面这样也行, 不知道区别. #http://sock-raw.org/papers/sock_raw 这个应该可以参考 #s = socket.socket(socket.PF_PACKET, socket.SOCK_RAW) #s.bind(('eth0',0)) r = s.send(packed_data) print r return if __name__ == '__main__': main()
抓包结果, 这里已经忽略了以太网首部的14个字节. 是从上图中的”硬件类型”开始的.
% sudo tcpdump -nn -vvv -x -c1 arp tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes 19:28:43.209235 ARP, Ethernet (len 6), IPv4 (len 4), Request who-has 192.168.1.1 tell 192.168.1.8, length 28 0x0000: 0001 0800 0604 0001 20c9 d088 963f c0a8 0x0010: 0108 0000 0000 0000 c0a8 0101 1 packet captured 1 packet received by filter 0 packets dropped by kernel
ARP攻击
上面的代码简单改一下就可以做ARP攻击了.
对网关做一个ARP应答.
op改为2, 代表ARP应答. 然后把 “以太网源地址”和”发送端以太网地址”都写自己的. IP地址写要攻击的人. OVER -
tcp/ip协议学习 第三章 IP协议
IP的头文件还是先贴一下, 总是记不住.
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |Version| IHL |Type of Service| Total Length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Identification |Flags| Fragment Offset | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Time to Live | Protocol | Header Checksum | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Source Address | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Destination Address | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Options | Padding | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
网络和子网划分
不太明白ABCD四类地址划分的意义,除了在路由选择的匹配中会配合子网划分用到.
路由表, 路由选择
以前本科的时候, IP配置好之后, 学校内网是可以访问的. 如果想访问外网, 需要拨号.但拨号之后内网就不方便上了.
学校网站有个小程序, 可以傻瓜式的修改系统路由. 拨号之后内网外网一起上.
当时不明白这是个什么东西, 其实现在也想不太明白怎么回事, 也没条件回学校试验下了. 但至少现在也算大概明白路由表以及路由选择了.
之前为了学习, 拿公司的网络实验了一把. osx下面的双网卡路由配置
来段程序吧
python
用程序发送raw IP包的时候, Total Length 和 Checksum 会由内核自动生成. 至于为啥,可能看到tcpip协议详解下(实现)的时候就知道了吧.
Checksum 算法在模拟后面协议的时候就要自己实现了.ICMP、IGMP、UDP和TCP都采用相同的检验和算法.
有个不同点是: 首部检验和字段是根据IP头计算的检验和码。它不对body进行计算。 ICMP、 IGMP、UDP和TCP的checksum覆盖首部和数据。
#!/usr/bin/env python # -*- coding: utf-8 -*- '''发送一个裸的IP包, 20字节的IP头, 后面跟一个随便写的字符串. 还不知道IP包的ID应该根据什么生成, 就随便写了一个54321 IP头里面: IP包总长度属性和checksum属性都是内核自动生成的. 协议是用的socket.IPPROTO_TCP,也就是6.但没什么用,IP包里面就随便的字符串,不是按TCP协议来的. ''' import socket from struct import pack import sys def main(): try: s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW) except socket.error as msg: print 'Socket could not be created. Error Code : ' + str(msg[0]) + ' Message ' + msg[1] return packet = '' source_ip = '127.0.0.1' dest_ip = '127.0.0.1' # ip header fields ip_ver = 4 ip_ihl = 5 ip_tos = 0 ip_tot_len = 0 # kernel will fill the correct total length ip_id = 54321 ip_frag_off = 0 ip_ttl = 32 ip_proto = socket.IPPROTO_TCP # no use in this case ip_checksum = 0 # kernel will fill the correct checksum ip_saddr = socket.inet_aton(source_ip) ip_daddr = socket.inet_aton(dest_ip) ip_ihl_ver = (ip_ver << 4) + ip_ihl # the ! in the pack format string means network order ip_header = pack( '!BBHHHBBH4s4s', ip_ihl_ver, ip_tos, ip_tot_len, ip_id, ip_frag_off, ip_ttl, ip_proto, ip_checksum, ip_saddr, ip_daddr) user_data = sys.argv[1] if sys.argv[1:] else '0123456789' packet = ip_header + user_data # the port specified has no effect r = s.sendto(packet, (dest_ip, 0)) # result is the length of packet sent out print r if __name__ == '__main__': main() ip_check = 1231 # kernel will fill the correct checksum
抓包. 运行环境, ubuntu12.04
% sudo tcpdump -i lo -x -vvv -t -c 1 ✭ tcpdump: listening on lo, link-type EN10MB (Ethernet), capture size 65535 bytes IP (tos 0x0, ttl 32, id 54321, offset 0, flags [none], proto TCP (6), length 30) localhost.12337 > localhost.12851: tcp 10 [bad hdr length 0 - too short, < 20] 0x0000: 4500 001e d431 0000 2006 c8a6 7f00 0001 0x0010: 7f00 0001 3031 3233 3435 3637 3839 1 packet captured 2 packets received by filter 0 packets dropped by kernel
tcp 10 [bad hdr length 0 - too short, < 20] 是说IP body里面的字节数太少, 只有10, 小于TCP header应有的20字节.
-
1字节, 45, 4代表Version, 5代表Header Length, 单位是4Byte.
- 2字节, 00, OTS(Type of Service), 代表此IP包的最小时延,最大吞吐量,最高可靠性, 最小费用等特征. 比如用于FTP的控制的IP包, 应该是最小时延, 用于FTP的数据的IP包, 则应该是最大吞吐量.书中提到:
现在大多数的TCP/IP实现都不支持TOS特性,但是自4.3BSD Reno以后的新版系统都对它 进行了设置。另外,新的路由协议如 OSPF和IS-IS都能根据这些字段的值进行路由决策。
-
3,4字节, 001e, Total Length. 0x1e = 30; 发送的01234567890+20字节的IP Header.
因为一些数据链路(如以太网)需要填充一些数据以达到最小长度。尽管以太网的最小帧长为 46字节,但是IP数据可能会更短,像我们就是30。如果没有总长度字段,那么IP层就不知道46字节中有多少是IP数据报的内容。所以IP包总长度这个字段是一定需要的. -
5,6字节, Identification. 0xd431=54321.
-
7,8字节, 0000.
-
9字节, 20. TTL. 路由转发此IP包的时候就把这个数值减1, 减到0的时候就直接丢弃, 并发送ICMP通知主机. trouceroute即利用了这个.
-
10字节, 06, 代表TCP协议.
-
11,12字节 c8a6. Checksum.
-
13-16字节, 7f00 0001. Source Address. 代表127.0.0.1
- 17-20字节, 7f00 0001. Destination Address. 代表127.0.0.1
Checksum是内核帮我们算好的, 但以后的TCP等就要自己算了, 这里先看一下算法.
首先把检验和字段置为 0。然后,对首部中每个 16 bit 进行二进制反码求和(整个首部看成是由一串 16 bit的字组成)
def checksum(ip_header): ip_header += chr(0x00)*(len(ip_header)%2) r = 0 while ip_header: # 因为ip_header已经是网络序了, 所以这里用!H. 否则需要用H r += unpack('H',ip_header[:2])[0] ip_header = ip_header[2:] if r > 0xffff: r = r&0xffff + (r>>16); return ~r
-
-
OSX下面的双网卡路由配置
曾经的公司网络
公司网络现在已经好多了, 以前的时候挺奇葩的:无线网络可以连外网, 但不能连公办网络, 有线的正好反过来.
想工作的时候吧,就把无线停了,或者把有线的优先级设高. 遇到问题想google一下呢,就要再反过来把有线停了,无线打开.
有同事说osx设置好有线和无线的优先级,不会有这个问题,系统会自己尝试,测试下来不可以.
当前配置
这种配置下, 只能走有线到办公网络. 不能通过wifi连外网.
无线网卡
% ifconfig en0 en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500 ether 20:c9:d0:88:96:4f inet6 fe80::22c9:d0ff:fe88:963f%en0 prefixlen 64 scopeid 0x5 inet 172.16.27.226 netmask 0xfffff800 broadcast 172.16.31.255 nd6 options=1<PERFORMNUD> media: autoselect status: active
有线网卡
% ifconfig en2 en2: flags=8963<UP,BROADCAST,SMART,RUNNING,PROMISC,SIMPLEX,MULTICAST> mtu 1500 options=4<VLAN_MTU> ether 00:8a:8d:8a:12:2a inet6 fe80::28a:8dff:fe8a:121a%en2 prefixlen 64 scopeid 0x4 inet 172.16.142.76 netmask 0xffffff00 broadcast 172.16.142.255 nd6 options=1<PERFORMNUD> media: autoselect (100baseTX <full-duplex>) status: active
路由表 osx下面要用netstat查路由表
% netstat -rl -f inet Routing tables Internet: Destination Gateway Flags Refs Use Mtu Netif Expire default 172.16.142.251 UGSc 31 0 1500 en2 default 172.16.24.1 UGScI 3 0 1500 en0 127 localhost UCS 0 0 16384 lo0 localhost localhost UH 8 17513401 16384 lo0 169.254 link#4 UCS 0 0 1500 en2 172.16.24/21 link#5 UCS 8 0 1500 en0 172.16.24.1 0:1a:e3:5b:f:d7 UHLWIir 4 43 1500 en0 1119 172.16.142.251 0:1a:e3:5b:1c:44 UHLWIir 32 14 1500 en2 1200
先看一下系统是怎么找一个IP包应该走哪个路由的:
IP路由选择主要完成以下这些功能: 1)搜索路由表,寻找能与目的IP地址完全匹配的表目(网络号和主机号都要匹配)。如果 找到,则把报文发送给该表目指定的下一站路由器或直接连接的网络接口(取决于标 志字段的值)。
2)搜索路由表,寻找能与目的网络号相匹配的表目。如果找到,则把报文发送给该表目 指定的下一站路由器或直接连接的网络接口(取决于标志字段的值)。目的网络上的所有主机都可以通过这个表目来处置。例如,一个以太网上的所有主机都是通过这种表目进行寻径的。这种搜索网络的匹配方法必须考虑可能的子网掩码。关于这一点我们在下一节中进行讨论。
3)搜索路由表,寻找标为“默认(default)”的表目。如果找到,则把报文发送给该表目指定的下一站路由器。
from TCP/IP 协议卷1 3.3节
可以看到我这里路由默认走的是有线的, 毕竟在上班, 办公环境要优先, 然后是wifi. 但实际上路由表搜索下来, 除了172.16.24, 其他是不会走到wifi的. 就是说wifi其实是废的.
添加路由
思路就是:
-
把wifi设置成优先的default路由
-
配置内网的网段走有线的网关
-
除了有线的, 自然就是走wifi default了
走起.
-
网络偏好设置里面, 把wifi调整为最高优先级
-
添加路由
route add -net 192.168.0.0/16 172.16.142.251 route add -net 10.0.0.0/8 172.16.142.251
就这么简单, 搞定! 来Ping 一下
% ping -c 1 www.baidu.com PING www.a.shifen.com (61.135.169.105): 56 data bytes 64 bytes from 61.135.169.105: icmp_seq=0 ttl=50 time=23.199 ms --- www.a.shifen.com ping statistics --- 1 packets transmitted, 1 packets received, 0.0% packet loss round-trip min/avg/max/stddev = 23.199/23.199/23.199/0.000 ms % ping -c 1 10.8.84.90 64 ↵ PING 10.8.84.90 (10.8.84.90): 56 data bytes 64 bytes from 10.8.84.90: icmp_seq=0 ttl=58 time=1.286 ms --- 10.8.84.90 ping statistics --- 1 packets transmitted, 1 packets received, 0.0% packet loss round-trip min/avg/max/stddev = 1.286/1.286/1.286/0.000 ms
赞, 都是通的.
再来一个内网的域名
% ping es.XXXcorp.com ping: cannot resolve es.XXXcorp.com: Unknown host
擦, 解析不了域名!
更改DNS
域名解析不了嘛, 肯定是dns的问题. 手动更改一下/etc/resolv.conf嘛. 看一下有线的dns配置, 找到有线的dns服务器加进去.
再来一次!
% ping es.XXXcorp.com ping: cannot resolve es.XXXcorp.com: Unknown host
还是不行…
清理一下dns再来一次. 不同版本的osx不一样的命令, 咱保险起见, 都来一次.
dscacheutil -flushcache killall -HUP mDNSResponder
还是不行…
nslookup手工指定server是没问题的.
% nslookup > server 192.168.102.20 Default server: 192.168.102.20 Address: 192.168.102.20#53 > es.XXXcorp.com Server: 192.168.102.20 Address: 192.168.102.20#53 Non-authoritative answer: Name: es.XXXcorp.com Address: 10.8.81.10 >
总之, 一翻测试下来, 才发现
nslookup, host命令是可以的, ping 浏览器都解析不了.然后又一翻搜索, 原来还要到网络偏好设置里面,去那里改dns才行.
nslookup和浏览器是去不同地方找配置的.做如上修改之后, 整个网络都畅通了.
-
-
python里多线程的写法
今天用到python多线程的时候, 发现不知道如何正确的等待所有线程结束后再结束主线程.
其实到最后我才知道这都是杞人忧天, Thread()出来的实例本来就是等到主进程结束后才结束.
官方解释:
daemon A boolean value indicating whether this thread is a daemon thread (True) or not (False). This must be set before start() is called, otherwise RuntimeError is raised. Its initial value is inherited from the creating thread; the main thread is not a daemon thread and therefore all threads created in the main thread default to daemon = False.
The entire Python program exits when no alive non-daemon threads are left.
默认daemon是false, start之后也不会阻塞在那里(废话, 否则要多线程干嘛, 不过总会想到join会阻塞这点). 对于我的需要来说简直完美,其实什么都不用做嘛.
可以用setDaemon(True)改变daemon值. 这样的话, 就需要调用join等待子线程结束了. (好多此一举..)
for t in threads: t.join()
可以把所有要起的线程放到一个队列里面, 然后对每一个线程join. 这样的确是可以实现, 但看起来太丑, 用着实在别扭.
像如下这样, 其实for循环里面, 进程等待在第一个join那里:
#!/usr/bin/env python # -*- coding: utf-8 -*- from threading import Thread import time def target(i): time.sleep(10-i) def main(): threads = [] for i in range(10): t = Thread(target=target,args=(i,)) threads.append(t) for t in threads: t.start() for idx,t in enumerate(threads): print idx t.join() if __name__ == '__main__': main()
multiprocessing.Pool
又用Queue实现了一下, 但还是太丑陋, 封装不好. 然后才搜索到Python其实有个multiprocessing.Pool, 可以很好看的实现这个功能:
#!/usr/bin/env python # -*- coding: utf-8 -*- from multiprocessing import Pool import time def work(): print time.time() time.sleep(4) def main(): pool = Pool(processes=4) for i in range(4): pool.apply_async(work) pool.close() pool.join() if __name__ == '__main__': main()
借鉴官方的Pool, 尝试自己实现一下最简单的Multiprocessing Pool
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
#!/usr/bin/env python # -*- coding: utf-8 -*- from threading import Thread from Queue import Queue import time def hello(msg=None): print time.time(),msg time.sleep(5) class MPool(object): def posttask(self,func): def inner(*args, **kwargs): q = args[0] real_args = args[1:] func(*real_args, **kwargs) q.task_done() return inner def __init__(self): self._queue = Queue() #简单起见没有设置队列上限 def run_task(self, target, *args, **kwargs): task = Thread(target=self.posttask(target), args=(self._queue,)+args, kwargs=kwargs) self._queue.put(task) task.setDaemon(True) task.start() def join(self): self._queue.join() def main(): p = MPool() for i in range(10): p.run_task(hello,i) p.join() if __name__ == '__main__': main()
最后再提一下, 我的初衷只是想等所有线程结束后再退出. 默认就是这样的, join什么的都不用.
-
rabbitmq 学习记录 -- 权限控制
好懂? 不好懂?
看懂之后, 觉得官网上的文档还是写的挺全面的. 但没看懂的时候, 觉得这写的啥啊…看不懂啊..
简单说下权限控制的层次
首先, 权限都是针对virtual host设置的. 毕竟virtual host也是个host.. 当一个用户建立连接的时候, 就先要判断这个用户对这个virtual host有没有权限. 没有的话, 连接也建立不了.
在一个virtual host里面, 对一个资源(原文是resource, 我理解大概就是指的amqp command, 比如说basic_consume), rabbitmq有三个方面的权限控制, 分别是configure, write, read. 对于一个amqp command, 需要对这三个方面(中的一个或者多个)设置相应的权限. 权限的表现形式就是一个正则表达式, 匹配queue或者是exchange的名字
preprocess是我们的一个用户, preprocessed rawevent是两个virtual host.
在下面的命令中, 可以看到一个用户的权限都是针对virtual host来的.
针对一个 virtual host, 按顺序分别列出了configure, write, read的权限对于rawevent这个virtual host, 我们的configure权限是^$, write是^$, read是^rawevent$ , 没错, 就是普通的正则表达式. # rabbitmqctl list_user_permissions preprocess Listing permissions for user "preprocess" ... preprocessed ^$ ^amq\\.default$ ^$ rawevent ^$ ^$ ^rawevent$ ...done
来几个例子
官网文档这个表格里面, 第一列就相当于权限啦, 一共有16个权限可以用.
如果想拥有声明queue的权限, 找到queue.declare这行, 可以看到需要在拥有configure的权限, 权限是一个针对queue的正则表达式.
反过来说, 如果我们的configure权限是 .* , 就代表我们可以声明任何名字的queue
如果我们限制他声明的队列只能以abcd开头, 就给他configure权限是 ^abcd.*$
如果还想要queue.bind的权限, 同样找到queue.bind这一行. 看到需要read write两个权限, read权限是queue的正则, write是exchange的正则.
比如, 某用户要订阅消息, 我们限制用户不能自己给queue取名字,只能用服务器生成的队列名字, 而且只能绑定在amq.fanout这个exchange.
比对这张表格,
声明queue需要:
configure权限应该是^mq.gen.*$绑定到amq.fanout需要:
write是^mq.gen.*$
read是 ^amq.fanout$basic_consume需要:
read是^mq.gen.*$结合起来! 就是:(configure write read的顺序)
^mq.gen.$ ^mq.gen.$ ^(amq.fanout) (mq.gen.*)$ -
rabbitmq 学习记录 -- TTL
queue本身的TTL
注意, 这里说的是queue本身的TTL. 不是说里面的消息
声明一个队列的时候, 可以用x-expires指定队列的TTL值. 过期之后, 这个队列就被删掉了.
不管里面是不是还有消息没有消费
#!/usr/bin/env python # -*- coding: utf-8 -*- ''' the queue exists for only 5 seconds, whether there is messages! ''' import pika import sys def main(): body = ' '.join(sys.argv[1:]) or 'Hello World' connection = pika.BlockingConnection( pika.ConnectionParameters( host='localhost')) channel = connection.channel() channel.queue_declare(queue='ttlhello',arguments={"x-expires":5000}) # ttl 5 second channel.basic_publish(exchange='', routing_key='ttlhello', body=body, ) connection.close() if __name__ == '__main__': main()
跑一下看看效果
# python queueTTL.py ;sleep 1; rabbitmqctl list_queues; sleep 6;rabbitmqctl list_queues Listing queues ... ttlhello 1 Listing queues ...
Per-Queue Message TTL
这个也是队列的属性, 而不是消息的. 队列中的所有消息过了TTL就会被删除.
#!/usr/bin/env python # -*- coding: utf-8 -*- ''' the messages in the queue exist for only 5 seconds ''' import pika import sys def main(): body = ' '.join(sys.argv[1:]) or 'Hello World' connection = pika.BlockingConnection( pika.ConnectionParameters( host='localhost')) channel = connection.channel() channel.queue_declare(queue='ttlmessagehello',arguments={"x-message-ttl":5000}) # ttl 5 second channel.basic_publish(exchange='', routing_key='ttlmessagehello', body=body, ) connection.close() if __name__ == '__main__': main()
跑一下看看效果
# python queueMessageTTL.py; rabbitmqctl list_queues; sleep 6; rabbitmqctl list_queues Listing queues ... ttlmessagehello 1 Listing queues ... ttlmessagehello 0
#Per-Message TTL
发送消息的时候, 也可以给每条消息一个TTL的属性.
messageTTLSend.py
#!/usr/bin/env python # -*- coding: utf-8 -*- import pika import sys def main(): body = ' '.join(sys.argv[1:]) or 'Hello World' connection = pika.BlockingConnection( pika.ConnectionParameters( host='localhost')) channel = connection.channel() channel.queue_declare(queue='hello') channel.basic_publish(exchange='', routing_key='hello', body=body, properties=pika.BasicProperties( expiration="5000" ) ) connection.close() if __name__ == '__main__': main()
跑一下看看效果
# python messageTTLSend.py; rabbitmqctl list_queues; sleep 6; rabbitmqctl list_queues; Listing queues ... hello 1 Listing queues ... hello 0
最后, 如per-message ttl 和 per-queue message ttl不一样, 按小的来.
-
rabbitmq 学习记录 -- ACK和数据持久化
为了数据不丢失, 需要在两个层面上做一些配置. 一个是ACK, 一个是数据持久化.
ACK
如果没有启用的话, 消费者拿走消息的时候, queue就把它删除了.
消费者拿走一条消息之后, 还没有处理完就crash了. 那么这条消息就丢失了. 为了保证消息一定被处理完了才从queue中被删掉, 就要启用Message acknowledgment .
启用之后, queue会在收到ack之后把消息删掉.
在这里没有timeout的概念, 哪怕这个任务执行很久, 不管多久, 会一直等ack. 或者是tcp链接断了, 才会把消息再给另外一个消费者.
ack默认是开启的, 也可以显示显示地关闭
channel.basic_consume(callback, queue=queue_name, no_ack=True)
callbak里面要记得发送ack,否则消息要被一次又一次的处理,然后再次回到队列 … …
def callback(ch, method, properties, body): print " [x] Received %r" % (body,) time.sleep( 10 ) raise SystemExit(1) # message will put back to the original queue ch.basic_ack(delivery_tag = method.delivery_tag) print " [x] Done"
来跑几个例子测试一下
生产者:
#!/usr/bin/env python # -*- coding: utf-8 -*- import pika import sys def main(): body = ' '.join(sys.argv[1:]) or 'Hello World' connection = pika.BlockingConnection( pika.ConnectionParameters( host='localhost')) channel = connection.channel() channel.queue_declare(queue='hello') channel.basic_publish(exchange='', routing_key='hello', body=body, ) connection.close() if __name__ == '__main__': main()
消费者:
#!/usr/bin/env python # -*- coding: utf-8 -*- import pika import time def callback(ch, method, properties, body): print " [x] Received %r" % (body,) time.sleep(10) raise SystemExit(1) ch.basic_ack(delivery_tag = method.delivery_tag) print " [x] Done" def main(): connection = pika.BlockingConnection( pika.ConnectionParameters( host='localhost')) channel = connection.channel() channel.queue_declare(queue='hello') channel.basic_consume(callback, queue='hello', ) channel.start_consuming() if __name__ == '__main__': main()
发送一条消息到队列 , 然后消费. 观察一下状态
# rabbitmqctl list_queues name messages_ready messages_unacknowledged Listing queues ... hello 0 1
等10秒, 再看, 消息没有被消费成功, 再次回到队列中.
# rabbitmqctl list_queues name messages_ready messages_unacknowledged Listing queues ... hello 1 0
REJECT
可以用ACK告诉rabbitmq任务处理完了, 但是如果没有成功的话, 也可以再把消息塞回队列. 就是Negative Acknowledgement. pika对应的方法是basic_reject
但可得注意, 不要搞成死循环了
数据持久化
启用ack之后, 消费者死掉不会丢失数据, 但rabbitmq进程死掉的话, 消息就丢掉了. 为保证数据不丢失, 还需要启动数据持久化. 需要在两个层面上做持久化:
- 队列的持久化
- 消息的持久化
channel.queue_declare(queue='hello', durable=True)
这样就申明了一个持久化的队列, durable的属性是不会变的, 如果之前hello队列已经申明过且不是持久化的, 这个再次申明会失败. 这个队列不会因为rabbitmq重启而丢失, 接下来还要继续做消息的持久化.
channel.basic_publish(exchange='', routing_key="task_queue", body=message, properties=pika.BasicProperties( delivery_mode = 2, # make message persistent ))
Q: 如果在一个非持久化的队列上发送数据时, 指明要持久化, 为发生什么情况?
A: 可以正常发送, 但重启rabbitmq之后, 队列丢失, 当然消息找不到了.
-
rabbitmq 学习记录 -- 基本概念
messag queue嘛, 就是生产者往里扔东西, 消费者取走. 但是要涉及到细节,还是有些多的.
#基本概念 其实就是官网文档的搬运工.
路由模型
先来看看一条消息的生命线, 生产者把消息发送到exchange, 然后根据exchange的类型和routing key(消息发送时的一个参数), 把这条消息路由到不同的队列中去, 图片中是发到了一个列队, 其实也可以到多个. 然后消费都从队列中把消息取走. 和kafka有些不同, rabbitmq里面的一个队列里面一条消息被一个消费者拿走之后, 就不可能再被其他人取到了(不考虑ACK的时候).
queue
队列, 就是存储消息的容器. 有些属性
- Name
- Durable (the queue will survive a broker restart)
- Exclusive (used by only one connection and the queue will be deleted when that connection closes)
- Auto-delete (queue is deleted when last consumer unsubscribes)
- Arguments (some brokers use it to implement additional features like message TTL)
使用queue之前, 需要先声明. 生产者和消费者都可以申明声明. 声明的时候如果队列已经存在了, 也没啥事, 但是如果再次声明的时候, 已经存在的队列参数和当时申明的参数不一样, 是会报错的.
exchange
生产者不和queue接触, 消息全部是通过exchange转到对应的queue的. 每一个队列都和一个或者多个exchange绑定在一起. 声明一个queue的时候, 它已经和default exchange绑定在一起了.
exchange有四种类型, 每种有不同的路由方式~
先看一下exchange的属性吧
- Name
- Durability (exchanges survive broker restart)
- Auto-delete (exchange is deleted when all queues have finished using it)
- Arguments (these are broker-dependent)
说exchange类型之前, 要先知道routing key的概念.
- queue和exchange绑定的时候, 是有一个routing key的, 为了和下面的routing key区别开来, 还是叫做binding key吧.
- 发送一条消息时, 有个参数, 叫routing key
- exchange type, bind key, routing key这三个东西结合在一起, 就决定了这条消息最终被路由到哪个/哪几个queue里面.
-
direct exchange
发送到direct的消息, 会找到和routing key一样的binding key的队列, 发过去. 一般来说, 这种exchange就是为了点对点的发消息, 一个消息就是固定发到一个特定的queue中. 但是一定要用来发到多个queue也是可以的.
#!/usr/bin/env python # -*- coding: utf-8 -*- import pika import sys def main(): body = ' '.join(sys.argv[1:]) or 'Hello World' connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost')) channel = connection.channel() channel.queue_declare(queue='hello1') channel.queue_bind(exchange='amq.direct', queue='hello1',routing_key="hello") channel.queue_declare(queue='hello2') channel.queue_bind(exchange='amq.direct', queue='hello2',routing_key="hello") channel.basic_publish(exchange='amq.direct', routing_key='hello', body=body) connection.close() if __name__ == '__main__': main()
-
fanout
这种类型的exchange会把消息发到每一个和他绑定的队列, routing/binding key被忽略. 适合用广播(和简单订阅?)
-
topic
比较灵活的路由方式, routing key可以用通配符.
* (star) can substitute for exactly one word.
# (hash) can substitute for zero or more words.直接看图吧
-
headers 这种路由的方式还是很灵活的.
如果需要绑定的不是一个特定的字符串, 而多个属性. 比如一条服务器的消息, 有OS, 有CPU核数, 有内存大小, 希望这些全部都匹配成功时,也可以是有一条匹配成功时,发到你的队列中来. 这个时候用headers exchange就比较方便.
绑定的时候,一个重要的参数是x-match. 如果是all,就是说所有属性匹配成功才发到这个队列. 如果是any,就是任意一个属性匹配成功啦.
这里也有个python的例子 Using pika to create headers exchanges with RabbitMQ in python
-
default 官网上把default exchange单独列了出来, 不过在我看来, default exchange就是一个direct exchange. 只是有些特殊的地方:
- 创建一个queue的时候, 自动绑定到default exchange. binding key就是队列名字
- 一个queue不能和default exchange解除绑定 (这点我不100%确定)
我只是大概翻译一下官网文档,记录一下. 这里对exchange介绍的更加详细. Working with RabbitMQ exchanges 这里面介绍比较详细.