一直试图来说说TCP/IP协议栈,结合python中的网络编程,然后来使用tcpdump命令来进行抓包,来分析三次握手,连接的建立和四次断开。
涉及的概念太多,从而此文可能略长,试图用简单的说法来讲述这个复杂的协议,也算是一个小小的总结。
人人都说上网,那么上网的目的是为了啥?是为了查看某些信息浏览网页等内容,换句话说就是查看网上分享的资源,这个也就是俗称为web资源,那么web资源又划分为两类,一种是静态资源,一种是动态资源。
静态资源,随处可见,也就是一些html网页,js脚本,css样式或者是一些图片,或者是一些视频,这些都称之为静态资源,永恒不变的资源。
动态资源,一般就是后缀为.JSP,.php,.py等文件,这些文件必须经过解释器编译之后,才能返回给客户端,动态生成的资源也就称之为动态资源。
访问这些资源,我们一般都是通过一个url地址来进行访问,这个唯一的地址就是标识这个资源的路径,也就是各种域名地址,并且在访问资源的时候,我们一般都不是访问本地的资源,也就是这些资源是存放在别人的电脑上或者是互联网上的一台主机上。
那么问题来了,如何两台主机是如何通信并且交互呢?
在程序的实现中,一般是先是有参考的协议,然后再有相关的实现,在上图中,OSI七层模型也就是一种参考的协议,分成了七个层次,而在具体的实现TCP网络模型中,分成了四层,主要是用户空间的上三层对应于tcp协议栈的应用层,传输层和网络层不变,而数据链路层和物理层则对应于tcp协议栈的驱动程序和物理硬件。
一种称之为参考模型,一种是具体的实现,相当于在进行连接服务器的时候,我们使用的是ssh程序,在这里使用的协议是ssh协议,而具体的开源实现的软件程序为openssh,从而在数据传输的时候,也就是将OSI的七层参考模型的具体实现为TCP网络模型。
应用层,主要是关注于应用层的细节,也就是用户空间,user space,在通信细节方面,也就是在linux的内核中实现,也就是内核空间。应用层完全由用户掌控,可以写各种应用程序,相当于我们访问的各种网址,各种手机上的app;而对于内核层面的东西,由于都是需要相互之间进行通信的,那么就直接在内核中实现,从而实现共用。
主机于主机之间如何进行通信?在进行访问一个资源的时候,传输层主要用来提供端口号,相当于一个进程的地址,也就是应用程序监听的端口号,用来区别不同的应用程序;在网络层,也就是提供了IP地址,从而确定了哪个主机和哪个主机进行通信;而物理层,主要是提供MAC地址,从而标识了设备到设备之间的通信,设备到设备之间的通信和主机到主机之间的通信不同,主机和主机必须是两个服务器,而设备到设备,可能是服务器,也有可能是路由器,也有可能是其他的设备。
在进行传输的时候,那么是怎么传输的呢 ?在应用层中,假如使用的http协议,那么应用层发送的数据就包括http首部和相关的数据信息,到了传输层,就会添加tcp首部,在这里的数据称之为段,segment,到了网络层,就会添加ip首部,这里的数据称之为packet包, 到了物理层,就会添加mac首部,也就是以太网的首部了,这里的数据称之为frame,帧,其实。。。为啥分这么多的类型,主要是便于区分,另外就是在不同的层中每个层里面的首部信息有相关的标注,从而会划分为不同的类型。
主机于主机之间通信,其实本质上来说,就是进程与进程之间的通信,socket是IPC机制的一种实现,(inter process communication)在tcp的实现之中,使用的是套接字,也就是socket,socket使用的是一个四元组,分别为源IP,源port,目标IP,目标port。
网络层的IP地址分为几种类型,现在使用的一般是IPV4的地址,使用的是32bit来进行存储,还有一种IP为IPV6地址。IP地址分为五中类型,非别为ABCDE,A类地址的范围为1-127,在其中私有地址为10.0.0.0,B类地址的为128-191,其中私有地址为172.16.0.0./16-172.31.0.0/16,C类地址为192-223,其中私有地址为192.168.0.0/24-192.168.255.0/24,D类地址为224-239,为组播地址,E类地址为240-254。在网络层中,python使用socket模块中的AF_INET表示ipv4,使用AF_INET6表示ipv6,使用AF_UNIX表示为同一主机上的不同进程之间的通信。
传输层分为三种,一种为tcp,一种为udp,一种为裸,从而在python中分别表示为,SOCK_STREAM流式套接字,SOCK_DGRAM,数据包套接字,SOCK_RAW,裸套接字,此种主要表示为网络层直接和应用层进行通信。
传输层主要提供的是进程的地址,也就是进程的端口号,在linux系统中,端口号可以划分为三种,一种是特权端口,也就是0-1023端口,这些端口都是有特定的服务所使用,例如ssh服务使用端口为22,http使用的端口为80,telnet使用的端口为23,ftp使用的端口为21,这些端口服务的启动必须使用root来启动;另外一种端口号为1024-41952,这些是表示一些特定服务的端口,可以供应用程序使用,例如端口3306表示mysql的服务,6379表示为redis服务,最后一类端口就是4193-65535表示为随机端口,主要用于客户端端口,也就是此主机作为客户端连接远程主机的时候使用的端口,在linux中,默认的范围如下:
[root@mogilenode1 ~]# cat /proc/sys/net/ipv4/ip_local_port_range
32768 61000
[root@mogilenode1 ~]# echo "1025 65535">/proc/sys/net/ipv4/ip_local_port_range (此端口范围可以进行修改)
[root@mogilenode1 ~]# cat /proc/sys/net/ipv4/ip_local_port_range
1025 65535
在使用端口的时候,端口号是有限的,每个客户端连接进来,那么总会分配一个随机的端口进行响应客户端的连接,从而当端口都分配完毕的时候,那么主机就不能提供服务了,从而在tcp连接的时候,可以对服务器进行DDOS攻击,从而让服务状态变为不可用。
在传输层中,分为tcp和udp,对于两种协议来说,提供的端口号都是相同的数量,都是65535,并且在端口分配的时候,也会收到上面特权端口的限制,这也就是在查看某些服务的时候,同时监听在相同的端口号上,但是使用的协议不同,例如ftp,会同时监听udp的21端口和tcp的21端口。
tcp协议,transport control protocol,传输控制协议,是一种面向连接的协议,也就是说,在通信的时候,必须首先建立连接,也就是常说的三次握手的过程,然后才能建立一个通信的管道进行传输数据;可靠的连接,在每次发送完数据包之后,都会进行一个确认,也就是会收到一个响应报文,其中包含ack的标识;无边界的协议,在tcp传输报文的时候,是一种流式报文的传输,就像在打电话的时候,是一直说的,而不用标注我开始说话了,结束说话了。
udp协议,user datagram protocol,用户数据包协议,udp协议相对于tcp协议来说,udp是一种无连接的协议,也就是说,在通信的时候不需要建立连接;不可靠的连接,也就是没有确认,发出了报文了就不会再管了;是一种有边界的协议,会标识每个报文的开始和结束,就像邮寄信封的时候,会标注从哪里开始,哪里结束。
tcp和udp的使用场景不同,tcp主要是为了数据的可靠而诞生,而udp则是为了速度而诞生,tcp在建立连接和断开连接的时候,都要花费时间,但是可以保证数据的完整到达,而udp是直接进行发送和接受,从而速度比较快,但是就不那么可靠了。
在使用tcp的时候,为了保证数据报文的可靠到达,采用了各种算法,例如滑动窗口算法,主要是为了保证接收方能够接受到传送的报文,这种算法可以进行流量控制;例如拥塞控制算法,这种主要是为了防止网络传输的情况,使用慢启动的方式,也就是开始发送一个包,当网络情况好的时候,每次发送多个包,从而得到响应,当网络变得拥挤的时候,那么会发送少量的包,从而防止网络阻塞;另外当包发送超时的时候,会经过确认,然后进行重传,从而保证报文有效到达;在每次发送报文的时候,会有一个校验和,从而保证了报文的完整性;当报文进行拆包解包之后,其中首部中包含了包的序列号,从而能有效的保证包的顺序。
在linux系统中,我们可以使用命令行工具tcpdump进行抓包,从而分析数据的流向,传输的数据,并且用来理解相关的协议。
在使用抓包tcpdump结果如下所示:
[root@mogilenode1 ~]# tcpdump -i eth1 tcp port 8888 (抓取网卡接口eth1,并且协议为tcp和端口为8888的数据报文)
18:24:55.476709 IP 192.168.249.10.32806 > 192.168.249.236.ddi-tcp-1: Flags [S], seq 3268496798, win 14600, options [mss 1460,sackOK,TS val 354009 ecr 0,nop,wscale 6], length 0(第一次握手,客户端向服务段发送一个SYN包,包的初始序列号为3268496798,窗口大小为14600,最大包的大小为1460字节,这个数据包的长度为0,只是为了建立建立,客户端状态修改SYN_SENT,服务端状态修改为SYN_RECIVE)
18:24:55.476768 IP 192.168.249.236.ddi-tcp-1 > 192.168.249.10.32806: Flags [S.], seq 587382068, ack 3268496799, win 14480, options [mss 1460,sackOK,TS val 101739285 ecr 354009,nop,wscale 5], length 0(服务端向客户端发送一个SYN包,带上初始化序列号,并且附带了确认号,表示第一次握手的包已经收到,窗口大小为14480,最大包的大小为1460个字节,此时客户端的状态修改为established)
18:24:55.477676 IP 192.168.249.10.32806 > 192.168.249.236.ddi-tcp-1: Flags [.], ack 1, win 229, options [nop,nop,TS val 354010 ecr 101739285], length 0(客户端想服务端发送一个确认包,也就是发送了一个ack包,从而服务端的状态修改为establised)
18:24:55.478488 IP 192.168.249.236.ddi-tcp-1 > 192.168.249.10.32806: Flags [P.], seq 1:6, ack 1, win 453, options [nop,nop,TS val 101739286 ecr 354010], length 5(服务端向客户端发送报文)
18:24:55.478955 IP 192.168.249.10.32806 > 192.168.249.236.ddi-tcp-1: Flags [.], ack 6, win 229, options [nop,nop,TS val 354011 ecr 101739286], length 0(客户端确认接受到报文)
18:24:55.479288 IP 192.168.249.10.32806 > 192.168.249.236.ddi-tcp-1: Flags [P.], seq 1:4, ack 6, win 229, options [nop,nop,TS val 354012 ecr 101739286], length 3(客户端向服务端发送报文,长度为3)
18:24:55.479508 IP 192.168.249.10.32806 > 192.168.249.236.ddi-tcp-1: Flags [F.], seq 4, ack 6, win 229, options [nop,nop,TS val 354012 ecr 101739286], length 0(四次断开的第一次,客户端向服务段发送FIN报文,表示此次结束,客户端状态修改为FIN_WAIT1)
18:24:55.479909 IP 192.168.249.236.ddi-tcp-1 > 192.168.249.10.32806: Flags [.], ack 4, win 453, options [nop,nop,TS val 101739288 ecr 354012], length 0(四次断开的第二次,服务端向客户端发送确认报文,表示断开,服务端状态修改为CLOSE_WAIT)
18:24:55.480162 IP 192.168.249.236.ddi-tcp-1 > 192.168.249.10.32806: Flags [F.], seq 6, ack 5, win 453, options [nop,nop,TS val 101739288 ecr 354012], length 0(四次断开的第三次,服务端向客户端发送FIN报文,表示断开连接,客户端状态修改为FIN_WAIT2)
18:24:55.480455 IP 192.168.249.10.32806 > 192.168.249.236.ddi-tcp-1: Flags [.], ack 7, win 229, options [nop,nop,TS val 354013 ecr 101739288], length 0(四次断开的第四次,客户端确认断开,客户端,服务端状态均修改为TIME_WAIT)
tcp状态变化图如下所示:
在状态变化的时候,主要注意几个地方:
在客户端发送请求到服务端,然后服务端进行确认的时候,此时的超时时间大约在一分钟左右,也就是说,此时可以进行DDOS攻击(损耗大量的CPU时间和内存)。
在服务器中,经常可以看到大量的TIME_WAIT状态,这是属于正常状态,大约时间为一分钟左右。
在使用tcpdump的时候,可以用的选项很多,常用选项如下:
-i:表示接口,也就网卡的名称,例如eth0,eth1等
tcp/udp:表示协议类型
host:表示主机的ip
port:表示端口号
nn:表示直接先是ip地址和端口
-w filename:表示将结果保存在某个文件中
-Ss0:表示显示所有包的信息
and or not:组合条件
src表示源,dst表示目标
更多信息,可以直接使用man来进行查看。
在使用套接字的时候,分为两种,一种是监听套接字,也就是listen的端口,一种是连接套接字,也就是出现各种tcp状态的端口,可以使用命令netstat来进行查看,如下:
[root@mogilenode1 ~]# netstat -tnlp|grep python(监听套接字,主要使用参数l,表示listen的端口,此处监听的端口为8888)
tcp 0 0 192.168.249.236:8888 0.0.0.0:* LISTEN 1960/python
[root@mogilenode1 ~]# netstat -anp|grep 8888(服务端套接字的tcp状态)
tcp 0 0 192.168.249.236:8888 192.168.249.10:32807 TIME_WAIT
[root@OMS ~]# netstat -anp|grep 8888(客户端套接字的tcp状态)
tcp 0 0 192.168.249.10:32807 192.168.249.236:8888 TIME_WAIT
python的客户端程序如下所示:
[root@OMS ~]# cat cli.py
#!/usr/bin/env python
import socket
import commands
HOST = "192.168.249.236"
PORT = 8888
BUFSIZE = 1024
cliSock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
cliSock.connect((HOST,PORT))
print cliSock.recv(BUFSIZE)
cliSock.send("end")
cliSock.close()
客户端的流程大致如下:建立tcp套接字,连接服务器端,接收服务端发送的数据,向服务端发送数据,关闭连接。
python的服务端程序如下所示:
[root@mogilenode1 ~]# cat ser.py
#!/usr/bin/env python
import socket
HOST = "192.168.249.236"
PORT = 8888
BUFSIZE = 1024
serSock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
serSock.bind((HOST,PORT))
serSock.listen(5)
while True:
conn,addr = serSock.accept()
conn.send("hello")
print conn.recv(BUFSIZE)
conn.close()
serSock.close()
服务端的流程大致如下:建立tcp套接字,向内核注册绑定使用的端口,进行监听,接收客户端连接,向客户端发送数据,接受客户端数据,关闭连接。
在使用socket的时候,必须注意几个问题:
单纯的socket连接为单进程模式,也就是只能接受一个客户端的请求,不能支持并发,其他的客户端请求可以建立,但是不能发送数据,处于阻塞状态
在服务端和客户端发送数据的时候,必须是一个收一个发,否则的话会被阻塞
在应用程序的BUFSIZE设置的时候,如果设置的过小,可能会导致缓冲区的数据无法完全接收,可以使用循环来进行接收
在发送数据的一方过快,接收数据的一方过慢,可能会导致数据错乱,从而在有的时候,要人为等进行sleep一段时间
缓冲区的大小设置和MTU无关,只是和应用程序的接受能力有关
在使用socket的时候,可以模拟进行命令的执行,例如执行相关的shell,并且能模拟ftp进行传输文件
可能出现几个错误:broken pipe,表示缓冲区设置的过小,数据无法接收;connetion reset by peer,表示一方强制中断连接;address already use,监听端口还未释放,等待大约一分钟即可。