Python菜鸟之路:Python基础-socket基础

Posted 程序员改变了世界、妹子改变了程序员!

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python菜鸟之路:Python基础-socket基础相关的知识,希望对你有一定的参考价值。

预热知识

OSI 七层模型

  谈到TCP/IP,就不得不说OSI七层模型,OSI 是国际标准化组织(ISO)和国际电报电话咨询委员会(CCITT)联合制定的开放系统互连参考模型,为开放式互连信息系统提供了一种功能结构的框架,图示如下:

   各个层次的详细说明,可以阅读百度词条http://baike.baidu.com/link?url=Agh556r2NM_rG7Yi4eV3UrxpkTBmpVU4eRrZSBpUf3HTF1xFcwkoh5AgfAyo5YqIbDosIuhhxh4v-dE4zLgC0ZqQwEn_5JGJl325CZYWYui6xdLMtrDc9b4JuXGQ_SLSjf6X4z6nzAt1uUAdrpwcbTUo_nciq4SoSPgBS__ZatfudORRkwseD_cO5CkRaqH2

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模块来实现,通常分为以下步骤:

  1. 创建socket对象
  2. 绑定本地地址+端口
  3. 监听本地端口
  4. 等待链接(阻塞的)
  5. 应答(非必须)、关闭客户端链接(非必须)
  6. 关闭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,通常包含以下步骤:

  1. 创建socket对象
  2. 连接socket server地址
  3. 数据交互
  4. 断开连接

代码如下:

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()

 

 

 

 

 

 

 

 

以上是关于Python菜鸟之路:Python基础-socket基础的主要内容,如果未能解决你的问题,请参考以下文章

菜鸟的Python之路--基础知识

菜鸟的Python之路--基础知识(列表的基本操作)

Python菜鸟之路一:Python基础-内置函数补充

Python菜鸟之路:Python基础-生成器和迭代器

我的python菜鸟之路1

我的python菜鸟之路1