预热知识
OSI 七层模型
谈到TCP/IP,就不得不说OSI七层模型,OSI 是国际标准化组织(ISO)和国际电报电话咨询委员会(CCITT)联合制定的开放系统互连参考模型,为开放式互连信息系统提供了一种功能结构的框架,图示如下:
TCP/IP 四层模型
应用层:应用程序间沟通的层,如简单电子邮件传输(SMTP)、文件传输协议(FTP)、网络远程访问协议(Telnet)等。
传输层:在此层中,它提供了节点间的数据传送服务,如传输控制协议(TCP)、用户数据报协议(UDP)等,TCP和UDP给数据包加入传输数据并把它传输到下一层中,这一层负责传送数据,并且确定数据已被送达并接收。
网络层:负责提供基本的数据封包传送功能,让每一块数据包都能够到达目的主机(但不检查是否被正确接收),如网际协议(IP)。
链路层:也叫网络接口层。对实际的网络媒体的管理,定义如何使用实际网络(如Ethernet、Serial Line等)来传送数据。
数据包、数据帧
“包”(Packet)是TCP/IP协议通信传输中的数据单位,一般也称“数据包”。但是TCP/IP协议是工作在OSI模型第三层(网络层)、第四层(传输层)上的,而帧是工作在第二层(数据链路层)。上一层的内容由下一层的内容来传输,所以在局域网中,“包”是包含在“帧”里的。
“帧”数据由两部分组成:帧头和帧数据。帧头包括接收方主机物理地址的定位以及其它网络信息。帧数据区含有一个数据体。为确保计算机能够解释数据帧中的数据,这两台计算机使用一种公用的通讯协议。互联网使用的通讯协议简称IP,即互联网协议。IP数据体由两部分组成:数据体头部和数据体的数据区。数据体头部包括IP源地址和IP目标地址,以及其它信息。数据体的数据区包括用户数据协议(UDP),传输控制协议(TCP),还有数据包的其他信息。这些数据包都含有附加的进程信息以及实际数据。
MTU
最大传输单元(Maximum Transmission Unit,MTU)是指一种通信协议的某一层上面所能通过的最大数据包大小(以字节为单位)。最大传输单元这个参数通常与通信接口有关(网络接口卡、串口等)。如果IP层有一个数据包要传,而且数据的长度比链路层的MTU大,那么IP层就会进行分片,把数据包分成托干片,让每一片都不超过MTU。 如下是几种常见的MTU:
TCP标志位
在TCP层,有个FLAGS字段,这个字段有以下几个标识:SYN, FIN, ACK, PSH, RST, URG.其中,对于我们日常的分析有用的就是前面的五个字段。它们的含义是:SYN表示建立连接,FIN表示关闭连接,ACK表示响应,PSH表示有 DATA数据传输,RST表示连接重置。其中,ACK是可能与SYN,FIN等同时使用的,比如SYN和ACK可能同时为1,它表示的就是建立连接之后的响应,如果只是单个的一个SYN,它表示的只是建立连接。TCP的几次握手就是通过这样的ACK表现出来的。但SYN与FIN是不会同时为1的,因为前者表示的是建立连接,而后者表示的是断开连接。RST一般是在FIN之后才会出现为1的情况,表示的是连接重置。一般地,当出现FIN包或RST包时,我们便认为客户端与服务器端断开了连接;而当出现SYN和SYN+ACK包时,我们认为客户端与服务器建立了一个连接。PSH为1的情况,一般只出现在 DATA内容不为0的包中,也就是说PSH为1表示的是有真正的TCP数据包内容被传递。TCP的连接建立和连接关闭,都是通过请求-响应的模式完成的。
TCP三次握手四次断开
本地的进程间通信(IPC)可以通过以下方式:
linux下进程间通信的几种主要手段简介:
- 管道(Pipe)及有名管道(named pipe):管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;
- 信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数);
- 报文(Message)队列(消息队列):消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
- 共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。
- 信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。
- 套接口(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。
- 远程过程调用
- 通过/proc 下的某些目录
而要实现网络中进程间通信,首先要解决的是如何标识唯一进程:网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。
Socket编程基础
什么是socket?
Socket又称"套接字",应用程序通常通过"套接字"向网络发出请求或者应答网络请求。
socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。因此可以理解为Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)
socket流程
一个完整的socket server的建立
在Python中,socket建立可以使用内置的socket模块来实现,通常分为以下步骤:
- 创建socket对象
- 绑定本地地址+端口
- 监听本地端口
- 等待链接(阻塞的)
- 应答(非必须)、关闭客户端链接(非必须)
- 关闭socket
代码如下:
import socket
# 创建socket对象
s = socket.socket()
ip_port = ('127.0.0.1', 9999)
# 绑定本地IP+端口
s.bind(ip_port)
# 监听本地地址
s.listen(5)
# 等待客户端请求
conn, addr = s.accept()
# 接收客户端请求或数据
recv_data = conn.recv(1024)
# 应答客户端(非必须)
conn.send(send_data)
# 关闭客户端链接
conn.close()
# 关闭socket
s.close()
一个socket client的建立
client去连接socket server,通常包含以下步骤:
- 创建socket对象
- 连接socket server地址
- 数据交互
- 断开连接
代码如下:
import socket
# 创建socket对象
s = socket.socket()
ip_port = ('127.0.0.1', 9999)
# 连接socket server,该过程connect 不阻塞
s.connect(ip_port)
# 数据交互(发)
s.send(bytes('请求内容'), encoding='utf8')
# 数据交互(收)
s.recv(1024)
# 断开连接
s.close()
Socket模块的用法
1 s=socket.socket() # socket.socket()创建socket
2
3 s.bind() # 绑定地址到套接字
4 s.listen() # 开始TCP监听
5 s.accept() # 被动接受TCP客户端连接,等待连接的到来
6 s.connect() # 主动初始化TCP服务器连接
7 s.connect_ex() # connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
8 s.recv() # 接收TCP数据
9 s.send() # 发送TCP数据
10 s.sendall() # 完整发送TCP数据
11 s.recvfrom() # 接收UDP数据
12 s.sendto() # 发送UDP数据
13 s.getpeername() # 连接到当前套接字的远端的地址(TCP连接)
14 s.getsockname() # 当前套接字的地址
15 s.getsockopt() # 返回指定套接字的参数
16 s.setsockopt() # 设置指定套接字的参数
17 s.close() # 关闭套接字
18 s.setblocking() # 设置套接字的阻塞与非阻塞模式
19 s.settimeout() # 设置阻塞套接字操作的超时时间
20 s.gettimeout() # 得到阻塞套接字操作的超时时间
21 s.filen0() # 套接字的文件描述符
22 s.makefile() # 创建一个与该套接字关联的文件对象
23
24 socket.AF_UNIX # 只能够用于单一的Unix系统进程间通信
25 socket.AF_INET # 服务器之间网络通信
26 socket.AF_INET6 # IPv6
27
28 socket.SOCK_STREAM # 流式socket , for TCP
29 socket.SOCK_DGRAM # 数据报式socket , for UDP
30 socket.SOCK_RAW # 原始套接字,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_RAW可以;其次,SOCK_RAW也可以处理特殊的IPv4报文;此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头。
31
32 socket.SOCK_RDM # 是一种可靠的UDP形式,即保证交付数据报但不保证顺序。SOCK_RAM用来提供对原始协议的低级访问,在需要执行某些特殊操作时使用,如发送ICMP报文。SOCK_RAM通常仅限于高级用户或管理员运行的程序使用。
33
34 socket.SOCK_SEQPACKET # 可靠的连续数据包服务
35
36 socket的方法
粘包
什么是粘包?
指TCP协议中,发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。
造成粘包的原因?
TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。即面向流的通信是无消息保护边界的。
UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。(http://zgc168.iteye.com/blog/1880620)
小结:
1 发送端需要等缓冲区满才发送出去,造成粘包
2 接收方不及时接收缓冲区的包,造成多个包接收
什么时候不需要考虑粘包?
如果利用tcp每次发送数据,就与对方建立连接,然后双方发送完一段数据后,就关闭连接,这样就不会出现粘包问题
如果发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就ok,也不用考虑粘包
如何解决粘包?
在头加一个数据长度之类的包,以确保接收。
代码案例
client端代码,核心在24-28行。
1 import socket
2 import os ,json
3 ip_port=('192.168.11.150',8009)
4 #买手机
5 s=socket.socket()
6 #拨号
7 s.connect(ip_port)
8 #发送消息
9 welcome_msg = s.recv(1024)
10 print("from server:",welcome_msg.decode())
11 while True:
12 send_data=input(">>: ").strip()
13 if len(send_data) == 0:continue
14
15 cmd_list = send_data.split()
16 if len(cmd_list) <2:continue
17 task_type = cmd_list[0]
18 if task_type == 'put':
19 abs_filepath = cmd_list[1]
20 if os.path.isfile(abs_filepath):
21 file_size = os.stat(abs_filepath).st_size
22 filename = abs_filepath.split("\\")[-1]
23 print('file:%s size:%s' %(abs_filepath,file_size))
24 msg_data = {"action":"put",
25 "filename":filename,
26 "file_size":file_size}
27 # 在发送数据之前,先发送本次发送的数据包信息,关键字是 file_size
28 s.send( bytes(json.dumps(msg_data),encoding="utf-8") )
29 server_confirmation_msg = s.recv(1024)
30 confirm_data = json.loads(server_confirmation_msg.decode())
31 if confirm_data['status'] ==200:
32
33 print("start sending file ",filename)
34 f = open(abs_filepath,'rb')
35 for line in f:
36 s.send(line)
37
38 print("send file done ")
39
40 else:
41 print("\033[31;1mfile [%s] is not exist\033[0m" % abs_filepath)
42 continue
43 else:
44 print("doesn't support task type",task_type)
45 continue
46 #s.send(bytes(send_data,encoding='utf8'))
47 #收消息
48 recv_data=s.recv(1024)
49 print(str(recv_data,encoding='utf8'))
50 #挂电话
51 s.close()
server端代码,关键点在28行。
1 import socketserver,json
2 class MyServer(socketserver.BaseRequestHandler):
3 def handle(self):
4 # print self.request,self.client_address,self.server
5 self.request.sendall(bytes('欢迎致电 10086,请输入1xxx,0转人工服务.',encoding="utf-8"))
6 while True:
7 data = self.request.recv(1024)
8 if len(data) == 0:break
9 print("data", data)
10 print("[%s] says:%s" % (self.client_address,data.decode() ))
11
12 task_data = json.loads( data.decode() )
13 task_action = task_data.get("action")
14 if hasattr(self, "task_%s"%task_action):
15 func = getattr(self,"task_%s" %task_action)
16 func(task_data)
17 else:
18 print("task action is not supported",task_action)
19
20 def task_put(self,*args,**kwargs):
21 print("---put",args,kwargs)
22 filename = args[0].get('filename')
23 filesize = args[0].get('file_size')
24 server_response = {"status":200}
25 self.request.send(bytes( json.dumps(server_response), encoding='utf-8' ))
26 f = open(filename,'wb')
27 recv_size = 0
# 接收到客户端发来的数据包文件大小,然后进行循环接收,直至数据包刚好接收完毕
28 while recv_size < filesize:
29 data = self.request.recv(4096)
30 f.write(data)
31 recv_size += len(data)
32 print('filesize: %s recvsize:%s' % (filesize,recv_size))
33 print("file recv success")
34 f.close()
35
36 if __name__ == '__main__':
37 server = socketserver.ThreadingTCPServer(('0.0.0.0',8009),MyServer)
38 server.serve_forever()