十、网络编程
一、socket是什么?
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个 门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让 Socket去组织数据,以符合指定的协议 所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出
的程序自然就是遵循tcp/udp标准的
有人将socket说成ip+port,ip是用来标识互联网中的一台主机的位置,而port是用来标识这台机器上的一个应用程序,
ip地址是配置到网卡上的,而port是应用程序开启的,ip与port的绑定就标识了互联网中独一无二的一个应用程序
而程序的pid是同一台机器上不同进程或者线程的标识
二、socket套接字工作流程
三、实现一个TCP简单的C/S架构
#服务端: import socket obj=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #指定网络协议AF_INET里的TCP协议SOCK_STREAM obj.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #重用端口,防止端口被占用,占用要么是其他app要么是上次非正常退出 obj.bind(('127.0.0.1',8080)) #绑定ip和端口,端口范围0-65535 obj.listen(5) #监听最大5个客户端来连接,依次执行 print ('starting....') conn,address=obj.accept() #返回(conn对象,address客户端地址),阻塞等待客户端来连接 print ('====') data=conn.recv(1024) #1024Byte,也具有阻塞作用 print(data) conn.send(data.upper()) conn.close() #关闭连接会话 obj.close() #关闭socket对象 #客户端: import socket obj=socket.socket(socket.AF_INET,socket.SOCK_STREAM) obj.connect(('127.0.0.1',8080)) #连接服务端 obj.send(b'hello')#基于Bytes类型发送 data=obj.recv(1024) print (data) obj.close()
#实现一个客户端能够一直发送信息,服务端能正常接收 #服务端 import socket obj=socket.socket(socket.AF_INET,socket.SOCK_STREAM) obj.bind(('127.0.0.1',8080)) obj.listen(5) print ('starting....') conn,address=obj.accept() while True: #通信循环 data=conn.recv(1024) print('客户端消息:',data) conn.send(data.upper()) conn.close() obj.close() #客户端 import socket obj=socket.socket(socket.AF_INET,socket.SOCK_STREAM) obj.connect(('127.0.0.1',8080)) while True: content=input('输入发送消息:').strip() if not content:continue #禁止发送空字符串 obj.send(content.encode('utf-8')) data=obj.recv(1024) print ('服务器端回复的消息:',data) obj.close()
#需求:多个客户端串行(等上一个客户端连接完,在建新的连接)连接服务器端 #windows下服务器端: import socket obj=socket.socket(socket.AF_INET,socket.SOCK_STREAM) obj.bind(('127.0.0.1',8080)) obj.listen(5) print ('starting....') while True: #连接循环 conn,address=obj.accept() while True: #通信循环 try: data=conn.recv(1024) print('客户端消息:',data) conn.send(data.upper()) except ConnectionError: #捕捉客户端单方面终止链接,windows系统下的服务器端会报错 break conn.close() obj.close() #客户端: import socket obj=socket.socket(socket.AF_INET,socket.SOCK_STREAM) obj.connect(('127.0.0.1',8080)) while True: content=input('输入发送消息:').strip() if not content:continue #禁止发送空字符串 obj.send(content.encode('utf-8')) data=obj.recv(1024) print ('服务器端回复的消息:',data) obj.close() #============================================== #在Linux下服务器端: import socket obj=socket.socket(socket.AF_INET,socket.SOCK_STREAM) obj.bind(('127.0.0.1',8080)) obj.listen(5) print ('starting....') while True: #连接循环 conn,address=obj.accept() while True: #通信循环 data=conn.recv(1024) if not data:break #linux下服务器端,如果客户端单方面中断连接,服务端会一直接收为空,因此收空结束此次连接 print('客户端消息:',data) conn.send(data.upper()) conn.close() obj.close() #客户端: import socket obj=socket.socket(socket.AF_INET,socket.SOCK_STREAM) obj.connect(('127.0.0.1',8080)) while True: content=input('输入发送消息:').strip() if not content:continue #禁止发送空字符串 obj.send(content.encode('utf-8')) data=obj.recv(1024) print ('服务器端回复的消息:',data) obj.close() C/S架构增强版v2.0
四、常遇到问题解析
4.1服务器端口被占用
当遇到这个问题的时候,可能是上次程序非法中断,或者被其他app占用了端口
解决方法:
#方法一:服务端 obj=socket.socket(socket.AF_INET,socket.SOCK_STREAM) obj.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #重用端口,防止端口被占用 obj.bind(('127.0.0.1',8080))
发现系统存在大量TIME_WAIT状态的连接,通过调整linux内核参数解决, vi /etc/sysctl.conf 编辑文件,加入以下内容: net.ipv4.tcp_syncookies = 1 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_tw_recycle = 1 net.ipv4.tcp_fin_timeout = 30 然后执行 /sbin/sysctl -p 让参数生效。 net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭; net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭; net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。 net.ipv4.tcp_fin_timeout 修改系統默认的 TIMEOUT 时间
#在windows系统下,杀死端口8080 1.找到端口占用进程的PID:netstat -ano | findstr 8080,最后的数字为PID号 2.查看运行此PID的程序名:tasklist | findstr 15572 3.根据PID号杀死程序端口占用:taskkill -F -PID 15572 4.tasklist | findstr 15572,再次查看不存在了
4.2 避免发送端发送空字符串
问题表述:客户端如果发送空字符串,服务器的conn.recr(1024)没有收到字符串,就会一直处于阻塞状态
解决办法,在客户端将用户发空情况过滤点,如
#客户端
import socket obj=socket.socket(socket.AF_INET,socket.SOCK_STREAM) obj.connect(('127.0.0.1',8080)) while True: content=input('输入发送消息:').strip() # if not content:continue #禁止发送空字符串 obj.send(content.encode('utf-8')) data=obj.recv(1024) print ('服务器端回复的消息:',data) obj.close()
五、什么是粘包
5.1 粘包只出现TCP协议
只有TCP有粘包现象:tcp是面向连接的,发送端两K两K发送数据,接收端可以三K提走数据,或一K提走数据。应用程序所看到的数据是一个整体流,一条消息多少字节对应用程序是不可见的。因此容易出现粘包
UDP永远不会粘包:udp是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,否则如下错误:显示一次接收不完全
5.2 引出粘包现象
#服务器端 import socket import subprocess server=socket.socket(socket.AF_INET,socket.SOCK_STREAM) server.bind(('127.0.0.1',8080)) server.listen(5) while True: conn,address=server.accept() while True: try: cmd=conn.recv(1024) if not cmd:break res=subprocess.Popen(cmd.decode('utf-8'),shell=True,\ stdout=subprocess.PIPE,\ stderr=subprocess.PIPE) #服务端执行客户端传过来的命令 stdout=res.stdout.read() stderr=res.stderr.read() data=stdout+stderr print ('返回结果长度:',len(data)) conn.send(data) #将命令执行结果返回 except ConnectionResetError: break conn.close() server.close() #客户端代码 import socket client=socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect(('127.0.0.1',8080)) while True: try: cmd=input('excute cmd:').strip() if not cmd:continue client.send(cmd.encode('utf-8')) data=client.recv(1024) print (data.decode('gbk')) except ConnectionResetError: break client.close() #客户端上执行:tasklist #服务器端要返回的结果带下超过了1024,而客户端一次性只接受1024Byte的长度,因此出现了粘包现象 (即客户端的内存里还有遗存着剩下的没接收完的结果)
5.3 引发粘包的原因
#1.发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间很短,数据小,会合到一起,产生粘包) #2.接收方不及时接收缓冲区的包,造成过个包一起接收(客户端发送了一段数据,服务端只接收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
5.4 解决粘包
#方法一:借助struct(制作固定包长度) #服务端 import socket import subprocess import struct server=socket.socket(socket.AF_INET,socket.SOCK_STREAM) server.bind(('127.0.0.1',8080)) server.listen(5) while True: conn, address = server.accept() try: cmd=conn.recv(1024) except ConnectionResetError: break if not cmd:break result = subprocess.Popen(cmd.decode('utf-8'),shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout=result.stdout.read() stderr=result.stderr.read() total_size=len(stdout)+len(stderr) #1.将报文封装成固定长度 header=struct.pack('i',total_size) #i为int类型,默认是4个字节 #2.发送命令执行结果长度,封装在固定4字节的报头里 conn.send(header) #3.发送命令执行结果 conn.send(stdout) #发送命令执行的正常结果 conn.send(stderr) #发送命令执行的错误结果 conn.close() server.close() #客户端 import socket import struct client=socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect(('127.0.0.1',8080)) while True: send_cmd=input('>>:').strip() if not send_cmd:continue client.send(send_cmd.encode('utf-8')) #1.接收报头长度 header=client.recv(4) #2.获得命令执行结果长度 total_size=struct.unpack('i',header)[0] recv_size=0 data=b'' #3.取回完整命令执行结果 while recv_size <total_size: recv_data=client.recv(1024) data +=recv_data recv_size +=len(recv_data) print (data.decode('gbk')) client.close()
#总结:struct打包,对于第二个形参即文件内容有长度要求,有局限性。如下图,struct对于第一个形参不同类型支持的长度
5.5 终极解决粘包问题
#以上方法一解决的场景是一些远程执行命令,返回执行结果的。不能适合于文件传输,缺少文件的名,文件MD5等等描述信息 #终结解决粘包问题方法 #服务端 ##关键点讲述 #1.定制文件描述信息字典 #2.字典序列化(为了客户端反序列化能得到对应的字典内容)(json) #3.序列化后字符A转化成字节B传输 #4.将字节B长度大小打包发送告诉终端(struct) #5.将字节B内容发送给终端 import socket import json import struct import os server=socket.socket(socket.AF_INET,socket.SOCK_STREAM) server.bind(('127.0.0.1',8081)) server.listen(5) print('服务启动...') while True: conn, client_addr = server.accept() while True: try: data=conn.recv(1024) if not data:break
#1.定制文件描述信息字典,即制作报头 head_dic={ 'filename':'Readme', 'total_size':os.path.getsize('Readme'), 'md5':'xxxx', } # 2.字典序列化(为了客户端反序列化能得到对应的字典内容)(json) header_json=json.dumps(head_dic) # 3.序列化后字符A转化成字节B传输 header_bytes=header_json.encode('utf-8') # 4.将字节B长度大小打包发送告诉终端(struct) conn.send(struct.pack('i',len(header_bytes))) #5.将字节B内容发送给终端. conn.send(header_bytes) #6.发送文件内容给终端 with open('Readme','rb') as f: for line in f: conn.send(line) except ConnectionResetError: break conn.close() server.close() #客户端 #1.接收固定报头长度A #2.对报头长度A进行解包(struct)成报头内容大小B个字节串 #3.接收B个字节串的数据C(recv(B)) #4.将文件描述信息数据C转成字符串D,在反序列化(json)成字典E #5.去除字典E里面的文件长度F #6.循环接收文件数据,直到收完为止 import socket import struct import json client=socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect(('127.0.0.1',8081)) while True: cmd=input('>>:').strip() if not cmd:continue client.send(cmd.encode('utf-8')) # 1.接收固定报头长度A obj=client.recv(4) # 2.对报头长度A进行解包(struct)成报头内容大小B个字节串 header_size=struct.unpack('i',obj)[0] # 3.接收B个字节串的数据C(recv(B)) header_bytes=client.recv(header_size) # 4.将文件描述信息数据C转成字符串D,在反序列化(json)成字典E header_json=header_bytes.decode('utf-8') header_dic=json.loads(header_json) # 5.取出字典E里面的文件长度F total_size=header_dic['total_size'] # 6.循环接收文件数据,直到收完为止 recv_size=0 res=b'' with open('b.txt','wb') as f: while recv_size < total_size: recv_data=client.recv(1024) f.write(recv_data) recv_size +=len(recv_data) print ('文件接收完成') client.close()
六、UDP通信
6.1 udp特点
#1.基于udp协议发送的每一条数据都自带边界,即udp协议没有粘包问题 #2.基于udp协议的通信,一定是一发对应一收
6.2 udp应用
#一般用于查询请求,如dns查询 ps:dsn查询分为递归查询(一问其他人再代问)和迭代查询(一问一答)
6.3 UDP简单通信示例
# udp协议 ##server服务端 from socket import * ip_port = ("127.0.0.1", 8081) server = socket(AF_INET, SOCK_DGRAM) # 数据报协议 server.bind(ip_port) while True: msg, client_addr = server.recvfrom(1024) server.sendto(msg.upper(), client_addr) ##客户端 from socket import * ip_port = ("127.0.0.1", 8081) client = socket(AF_INET, SOCK_DGRAM) # 数据报协议 while True: msg = input('>>:').strip() client.sendto(msg.encode("utf-8"),("127.0.0.1",8080)) data, server_ip = client.recvfrom(1024) print(data)
6.4 udp注意事项
#1.当发的数据量大于收的数据量 接收方如果是windows下回报错: OSError: [WinError 10040] 一个在数据报套接字上发送的消息大于内部消息缓冲区或其他一些网络限制, 或该用户用于接收数据报的缓冲区比数据报小。 接收方如果是在linux下不会报错: 值会接收对应长度的数据 #2.UDP不用于发送文件或者大数据量的,要保证稳定udp传输的最大为512字节
七、socketserver模拟并发套接字通信
实现服务端并发效果
import socketserver class MyTCPHandler(socketserver.BaseRequestHandler): #继承 def handle(self): while True: try: data=self.request.recv(1024) if not data:break self.request.send(data.upper()) except ConnectionRefusedError: break self.request.close() if __name__ == '__main__': #连接循环 server=socketserver.ThreadingTCPServer(('127.0.0.1',8080),MyTCPHandler) #实现TCP并发 server.serve_forever() #客户端 import socket client=socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect(('127.0.0.1',8080)) while True: msg=input('>>>:').strip() client.send(msg.encode('utf-8')) data=client.recv(1024) print(data.decode('utf-8')) client.close()
八、协程
8.1 什么是协程
协程是想要在单线程下实现并发,所谓的并发=切换+保存状态,如果我们能把原本是操作系统实现的切换+保存状态
在应用程序中实现,即在我们的线程中实现,那么单线程也能实现并发
8.2 引入协程的目的
#单线程下实现并发是为了提升单线程的运行效率: 单线程下的多个人一定要实现遇到IO切换,才能降低单线程的IO阻塞,增多就绪状态,操作系统就会尽可能多的为该线程分配CPU
8.3 实现单线程并发的解决方案
#1.能够在多个任务之间实现切换,并且在切走之前保存好任务的执行状态 #2.监听多个任务的IO行为,实现只有遇到IO操作时才切换
8.4 串行执行效率
#串行执行 import time def consumer(res): '''任务1:接收数据,处理数据''' pass def producer(): '''任务2:生产数据''' for i in range(10000000): pass start=time.time() #串行执行 res=producer() consumer(res) #写成consumer(producer())会降低执行效率 stop=time.time() print(stop-start) #0.3930225372314453
8.5 yield切换模拟多任务执行效率
#基于yield并发执行 import time def consumer(): '''任务1:接收数据,处理数据''' while True: x=yield def producer(): '''任务2:生产数据''' g=consumer() next(g) for i in range(10000000): g.send(i) start=time.time() producer() stop=time.time() print(stop-start) #1.6560947895050049
8.6协程示例
#pip3 install gevent #安装gevent模块 from threading import current_thread from gevent import monkey;monkey.patch_all() # 在整个文件的开头加上这一行,表示可识别非本模块IO操作 import gevent import time def eat(name): # print(current_thread().name) #表示在同一个线程下 print("%s eat 1" %name) time.sleep(5) print("%s eat 2" %name) def play(name): # print(current_thread().name) print("%s play 1" %name) time.sleep(2) print("%s play 2" %name) # 异步提交任务: g1=gevent.spawn(eat,'小舞') g2=gevent.spawn(play,'唐三') # gevent.sleep(4) #睡4秒,与time.sleep(4)一样 # g1.join() # g2.join() # print(current_thread().name) #异步执行的子程序与主程序属于同一线程 gevent.joinall([g1,g2]) #等待异步执行完,主线程才结束
8.7 服务端采用协程,实现单线程下高并发示例
#服务端 from gevent import monkey,spawn;monkey.patch_all() from socket import * def talk(conn): while True: try: data=conn.recv(1024) if not data:break conn.send(data.upper()) except ConnectionResetError: break conn.close() def server(ip,port,backlog=5): server=socket(AF_INET,SOCK_STREAM) server.bind((ip,port)) server.listen(backlog) while True: conn,addr=server.accept() # t=Thread(target=talk,args=(conn,)) # t.start() spawn(talk,conn) if __name__ == '__main__': server('127.0.0.1',8080) #客户端,模拟500用户同时来请求 from socket import * from threading import Thread,current_thread def client(): client = socket(AF_INET, SOCK_STREAM) client.connect(('127.0.0.1', 8080)) while True: msg = '%s say hello' % current_thread().name client.send(msg.encode('utf-8')) data = client.recv(1024) print(data.decode('utf-8')) if __name__ == '__main__': for i in range(500): t=Thread(target=client) t.start()