python之socket编程
第一、 客户端/服务器架构
C/S架构:
1.硬件C/S架构(打印机)
2.软件C/S架构(web服务)
学习socket也是为了写一款C/S架构的软件
第二、 socket介绍
前言:我们要开发一套C/S架构,首先要了解osi七层,简单说osi七层包括 : 应,表,会,传,网,数,物
主要侧重点,我们开发的基于C/S架构的一款应用程序(属于应用层),其是通过网络进行通信的,而网络的核心在于一大堆的协议,TCP/IP协议,以太网协议,如果开发一款C/S架构的软件,必须基于这些标准。现在简单介绍一下这些标准
只介绍与socket的相关的TCP/IP层:TCP/IP协议族包括运输层、网络层、链路层。
现在你会疑问,socket在工作在那一层?详见下图:
综合上图:
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
第三、 套接字
套接字发展史及分类:
套接字起源于 20 世纪 70 年代加利福尼亚大学伯克利分校版本的 Unix,即人们所说的 BSD Unix。 因此,有时人们也把套接字称为“伯克利套接字”或“BSD 套接字”。一开始,套接字被设计用在同 一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或 IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。
基于文件类型的套接字家族
套接字家族的名字:AF_UNIX
unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
基于网络类型的套接字家族
套接字家族的名字:AF_INET
(还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET)
套接字工作流程:
面向连接套接字:
1.服务器先用 socket 函数来建立一个套接字,用这个套接字完成通信的监听。
2.用 bind 函数来绑定一个端口号和 IP 地址。因为本地计算机可能有多个网址和 IP,每一个 IP 和端口有多个端口。需要指定一个 IP 和端口进行监听。
3.服务器调用 listen 函数,使服务器的这个端口和 IP 处于监听状态,等待客户机的连接。
4.客户机用 socket 函数建立一个套接字,设定远程 IP 和端口。
5.客户机调用 connect 函数连接远程计算机指定的端口。
6.服务器用 accept 函数来接受远程计算机的连接,建立起与客户机之间的通信。
7.建立连接以后,客户机用 write 函数向 socket 中写入数据。也可以用 read 函数读取服务器发送来的数据。
8.服务器用 read 函数读取客户机发送来的数据,也可以用 write 函数来发送数据。
9.完成通信以后,用 close 函数关闭 socket 连接。
面向无连接套接字
无连接的通信不需要建立起客户机与服务器之间的连接,因此在程序中没有建立连接的过程。进行通信之前,需要建立网络套接字。服务器需要绑定一个端口,在这个端口上监听接收到的信息。客户机需要设置远程 IP 和端口,需要传递的信息需要发送到这个 IP 和端口上。
详解:
服务端套接字函数
s.bind() 绑定(主机,端口号)到套接字
s.listen() 开始TCP监听
s.accept() 被动接受TCP客户的连接,(阻塞式)等待连接的到来
客户端套接字函数
s.connect() 主动初始化TCP服务器连接
s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
公共用途的套接字函数
s.recv() 接收TCP数据
s.send() 发送TCP数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完)
s.sendall() 发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完)
s.recvfrom() 接收UDP数据
s.sendto() 发送UDP数据
s.getpeername() 连接到当前套接字的远端的地址
s.getsockname() 当前套接字的地址
s.getsockopt() 返回指定套接字的参数
s.setsockopt() 设置指定套接字的参数
s.close() 关闭套接字
面向锁的套接字方法
s.setblocking() 设置套接字的阻塞与非阻塞模式
s.settimeout() 设置阻塞套接字操作的超时时间
s.gettimeout() 得到阻塞套接字操作的超时时间
面向文件的套接字的函数
s.fileno() 套接字的文件描述符
s.makefile() 创建一个与该套接字相关的文件
基于TCP的套接字
socket服务端:
#!/usr/bin/env python #-*-coding:utf-8-*- import socket server=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #创建服务端套接字 server.bind(('127.0.0.1',8080)) #绑定ip+port server.listen(5) #监听连接 conn,clinet_addr=server.accept() #接受客户端连接 print(conn,clinet_addr) client_data=conn.recv(1024) #接受客户端升级 print('client data is %s'%client_data) conn.send(client_data.upper()) #发送数据给客户端 conn.close() #关闭客户端套接字连接
server.close() #关闭服务端连接
socket客户端:
#!/usr/bin/env python #-*-coding:utf-8-*- import socket client=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #创建客户端套接字 client.connect(('127.0.0.1',8080)) #连接服务端 client.send('hello'.encode('utf-8')) #发送byte格式数据 client_date=client.recv(1024) #接收服务端数据 print(client_date) client.close() #关闭连接
进化版:
server端
#!/usr/bin/env python #-*-coding:utf-8-*- import socket server=socket.socket(socket.AF_INET,socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #如果服务端仍然存在四次挥手的time_wait状态在占用地址,需要用这个释放 server.bind(('127.0.0.1',8080)) server.listen(5) #backlog:最大能挂起几个连接 while True: #连接循环 conn,clinet_addr=server.accept() print(conn,clinet_addr) while True: #通信循环 try: #异常处理是解决,如果有一个客户端断开连接,服务端崩溃的问题 client_data=conn.recv(1024) #收多少个字节 print('client data is %s'%client_data) conn.send(client_data.upper()) except Exception: break conn.close() server.close()
客户端:
#!/usr/bin/env python #-*-coding:utf-8-*- import socket client=socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect(('127.0.0.1',8080)) while True: msg=input('>>').strip() if len(msg) == 0:continue client.send(msg.encode('utf-8')) client_date=client.recv(1024) #收取1024个字节 print(client_date) client.close()
基于UDP的套接字
server端:
#!/usr/bin/env python #-*-coding:utf-8-*- import socket server=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) server.bind(('127.0.0.1',8080)) while True: data,client_addr_port=server.recvfrom(1024) print(data.decode('utf-8')) msg=input('>>').strip() server.sendto(msg.encode('utf-8'),client_addr_port) server.close()
client:
#!/usr/bin/env python #-*-coding:utf-8-*- import socket client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) while True: msg=input('>>').strip() client.sendto(msg.encode('utf-8'),('127.0.0.1',8080)) data,server_addr_port=client.recvfrom(1024) print(data.decode('utf-8')) client.close()
第四、 recv与recvfrom&&send与sendto的区别
1.声明:发消息,都是将数据发送到己端的发送缓冲中,收消息都是从己端的缓冲区中收
1.1: tcp:send发消息,recv收消息
1.2: udp:sendto发消息,recvfrom收消息
2.send与sendto
tcp是基于数据流的,而udp是基于数据报的:
- send(bytes_data):发送数据流,数据流bytes_data若为空,自己这段的缓冲区也为空,操作系统不会控制tcp协议发空包
- sendto(bytes_data,ip_port):发送数据报,bytes_data为空,还有ip_port,所有即便是发送空的bytes_data,数据报其实也不是空的,自己这端的缓冲区收到内容,操作系统就会控制udp协议发包。
3.recv与recvfrom
1.tcp协议:
(1)如果收消息缓冲区里的数据为空,那么recv就会阻塞(阻塞很简单,就是一直在等着收)
(2)只不过tcp协议的客户端send一个空数据就是真的空数据,客户端即使有无穷个send空,也跟没有一个样。
(3)tcp基于链接通信
- 基于链接,则需要listen(backlog),指定半连接池的大小
- 基于链接,必须先运行的服务端,然后客户端发起链接请求
- 对于mac系统:如果一端断开了链接,那另外一端的链接也跟着完蛋recv将不会阻塞,收到的是空(解决方法是:服务端在收消息后加上if判断,空消息就break掉通信循环)
- 对于windows/linux系统:如果一端断开了链接,那另外一端的链接也跟着完蛋recv将不会阻塞,收到的是空(解决方法是:服务端通信循环内加异常处理,捕捉到异常后就break掉通讯循环)
2.udp协议
(1)如果如果收消息缓冲区里的数据为“空”,recvfrom也会阻塞
(2)只不过udp协议的客户端sendinto一个空数据并不是真的空数据(包含:空数据+地址信息,得到的报仍然不会为空),所以客户端只要有一个sendinto(不管是否发送空数据,都不是真的空数据),服务端就可以recvfrom到数据。
(3)udp无链接
- 无链接,因而无需listen(backlog),更加没有什么连接池之说了
- 无链接,udp的sendinto不用管是否有一个正在运行的服务端,可以己端一直的发消息,只不过数据丢失
- recvfrom收的数据小于sendinto发送的数据时,在mac和linux系统上数据直接丢失,在windows系统上发送的比接收的大直接报错
- 只有sendinto发送数据没有recvfrom收数据,数据丢失
总结:你单独运行udp的客户端,并不会报错,相反tcp却会报错,因为udp协议只负责把包发出去,对方收不收,我根本不管,而tcp是基于链接的,必须有一个服务端先运行着,客户端去跟服务端建立链接然后依托于链接才能传递消息,任何一方试图把链接摧毁都会导致对方程序的崩溃。
第五、 粘包现象
通过远程执行命令,查看粘包现象
server端
#!/usr/bin/env python #-*-coding:utf-8-*- import socket import subprocess server=socket.socket(socket.AF_INET,socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #如果服务端仍然存在四次挥手的time_wait状态在占用地址,需要用这个释放 server.bind(('127.0.0.1',8080)) server.listen(5) while True: #连接循环 conn,clinet_addr=server.accept() print(conn,clinet_addr) while True: #通信循环 try: #异常处理是解决,如果有一个客户端断开连接,服务端崩溃的问题 cmd=conn.recv(1024) if not cmd:break cmd=cmd.decode('utf-8') cmd_res=subprocess.Popen(cmd,shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE) err=cmd_res.stderr.read() if err: res=err else: res=cmd_res.stdout.read() conn.send(res) except Exception: break conn.close() server.close()
client端:
#!/usr/bin/env python #-*-coding:utf-8-*- import socket client=socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect(('127.0.0.1',8080)) while True: cmd=input('>>').strip() if len(cmd) == 0:continue client.send(bytes(cmd,encoding='utf-8')) client_date=client.recv(1024) print(client_date.decode('gbk')) client.close()
仔细看执行结果:
看执行结果可知:当输入ipconfig的时候,执行的任然是dir命令的执行结果,则这就是发生粘包现象的结果,为什么发生这种现象呢?是因为client端接受执行结果的时候,接受的1024字节的数据,而执行结果的数据大于1024个字节,所以发生这种现象
第六、 什么是粘包
声明:第五中看到的是tcp发生粘包的现象,并且粘包的现象只会发生在tcp连接中,但是udp中会丢弃1024以后的字节数据,不详细说了这个,但是tcp为什么呢?请看下面分解
发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
- TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
- UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
第七、 解决粘包的方法
方法一:先把数据的长度大小由client端发送server端
server端:
#!/usr/bin/env python #-*-coding:utf-8-*- import socket import subprocess server=socket.socket(socket.AF_INET,socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #如果服务端仍然存在四次挥手的time_wait状态在占用地址,需要用这个释放 server.bind(('127.0.0.1',8080)) server.listen(5) while True: #连接循环 conn,clinet_addr=server.accept() print(conn,clinet_addr) while True: #通信循环 try: #异常处理是解决,如果有一个客户端断开连接,服务端崩溃的问题 cmd=conn.recv(1024) if not cmd:break cmd=cmd.decode('utf-8') cmd_res=subprocess.Popen(cmd,shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE) err=cmd_res.stderr.read() if err: res=err else: res=cmd_res.stdout.read() res_len=len(res) conn.send(str(res_len).encode('utf-8')) data=conn.recv(1024).decode('utf-8') if data == 'recv_ready': conn.sendall(res) except Exception: break conn.close() server.close()
client端:
#!/usr/bin/env python #-*-coding:utf-8-*- import socket client=socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect(('127.0.0.1',8080)) while True: cmd=input('>>').strip() if len(cmd) == 0:continue client.send(cmd.encode('utf-8')) length=int(client.recv(1024).decode('utf-8')) client.send('recv_ready'.encode('utf-8')) send_size=0 recv_size=0 data=b'' while recv_size < length: data+=client.recv(1024) recv_size+=len(data) print(data.decode('gbk')) client.close()
执行结果:
粘包的问题现在确实解决了,但是由于程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗,并且如果发送字节的长度超过了你定义的长度,也会出现粘包,但是会大大降低。所以推荐使用第二种解决粘包的方式
方法二:自定义报头:
为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据
server端:
#!/usr/bin/env python #-*-coding:utf-8-*- import socket import subprocess import struct import json server=socket.socket(socket.AF_INET,socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #如果服务端仍然存在四次挥手的time_wait状态在占用地址,需要用这个释放 server.bind(('127.0.0.1',8080)) server.listen(5) while True: #连接循环 conn,clinet_addr=server.accept() print(conn,clinet_addr) while True: #通信循环 try: #异常处理是解决,如果有一个客户端断开连接,服务端崩溃的问题 cmd=conn.recv(1024) print(cmd) if not cmd:break cmd=cmd.decode('utf-8') cmd_res=subprocess.Popen(cmd,shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE) err=cmd_res.stderr.read() if err: res=err else: res=cmd_res.stdout.read() head_dic={'filename':'a.txt','size':len(res)} head_json=json.dumps(head_dic) head_bytes=head_json.encode('utf-8') conn.send(struct.pack('i',len(head_bytes))) conn.send(head_bytes) conn.sendall(res) except Exception: break conn.close() server.close()
client端:
#!/usr/bin/env python #-*-coding:utf-8-*- import socket import json import struct client=socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect(('127.0.0.1',8080)) while True: cmd=input('>>').strip() if len(cmd) == 0:continue client.send(bytes(cmd,encoding='utf-8')) #第一阶段:收数据的长度 x=client.recv(4) #报头的长度 print(x) baotou_len=struct.unpack('i',x)[0] print(baotou_len) head_byte=client.recv(baotou_len) head_json=head_byte.decode('utf-8') print(head_json) head_dic=json.loads(head_json) data_len=head_dic['size'] # 第二阶段:根据数据的长度,收数据 recv_size=0 res=b'' while recv_size < data_len: recv_data=client.recv(1024) res+=recv_data recv_size+=len(recv_data) print(res.decode('gbk')) client.close()
完美解决:
第八、socketserver实现并发
基于tcp的套接字,关键就是两个循环,一个链接循环,一个通信循环
socketserver模块中分两大类:server类(解决链接问题)和request类(解决通信问题)
server类:
与连接相关:BaseServer,TcpServer,UdpServer,UnixSteamServer,UnixDategramServer
基于多线程实现并发:ThreadingMinIn,ThreadingTCPServer,ThreadingUCPServer
基于多进程实现并发:ForkingMinIn,ForkingTCPServer,ForkingUDPServer
继承关系:
以下述代码为例,分析socketserver源码:
ftpserver=socketserver.ThreadingTCPServer(('127.0.0.1',8080),FtpServer)
ftpserver.serve_forever()
查找属性的顺序:ThreadingTCPServer->ThreadingMixIn->TCPServer->BaseServer
- 实例化得到ftpserver,先找类ThreadingTCPServer的__init__,在TCPServer中找到,进而执行server_bind,server_active
- 找ftpserver下的serve_forever,在BaseServer中找到,进而执行self._handle_request_noblock(),该方法同样是在BaseServer中
- 执行self._handle_request_noblock()进而执行request, client_address = self.get_request()(就是TCPServer中的self.socket.accept()),然后执行self.process_request(request, client_address)
- 在ThreadingMixIn中找到process_request,开启多线程应对并发,进而执行process_request_thread,执行self.finish_request(request, client_address)
- 上述四部分完成了链接循环,本部分开始进入处理通讯部分,在BaseServer中找到finish_request,触发我们自己定义的类的实例化,去找__init__方法,而我们自己定义的类没有该方法,则去它的父类也就是BaseRequestHandler中找....
源码分析总结:
基于tcp的socketserver我们自己定义的类中的
- self.server即套接字对象
- self.request即一个链接
- self.client_address即客户端地址
基于udp的socketserver我们自己定义的类中的
- self.request是一个元组(第一个元素是客户端发来的数据,第二部分是服务端的udp套接字对象),如(b'adsf', <socket.socket fd=200, family=AddressFamily.AF_INET, type=SocketKind.SOCK_DGRAM, proto=0, laddr=('127.0.0.1', 8080)>)
- self.client_address即客户端地址
示例:
基于tcp的并发:
server端
#!/usr/bin/env python #-*-coding:utf-8-*- import socketserver class MyServer(socketserver.BaseRequestHandler): def handle(self): print(self.request) while True: data=self.request.recv(1024) self.request.send(data.upper()) if __name__ == '__main__': s=socketserver.ThreadingTCPServer(('127.0.0.1',8080),MyServer) s.serve_forever()
client端:
#!/usr/bin/env python #-*-coding:utf-8-*- import socket c=socket.socket(socket.AF_INET,socket.SOCK_STREAM) c.connect(('127.0.0.1',8080)) while True: inp=input('>>:').strip() if not inp: continue c.send(inp.encode('utf-8')) data=c.recv(1024) print(data)
有多个client端
看server端执行连接接入的过程,即可知道是并发执行
基于udp的并发:
server端:
#!/usr/bin/env python #-*-coding:utf-8-*- import socketserver class MyServer(socketserver.BaseRequestHandler): def handle(self): print(self.request) self.request[1].sendto(self.request[0].upper(),self.client_address) if __name__ == '__main__': s=socketserver.ThreadingUDPServer(('127.0.0.1',8080),MyServer) s.serve_forever()
client端:
#!/usr/bin/env python #-*-coding:utf-8-*- import socket c=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) server_ip_port=('127.0.0.1',8080) while True: inp=input(">>: ").strip() c.sendto(inp.encode('utf-8'),server_ip_port) data=c.recvfrom(1024) print(data)
运行结果:
由上图可知是并发执行