网络编程
一.软件开发的架构
1.C/S架构:
即 Client与Server。中文意思:即 客户端与服务端架构
2.B/S架构:
即 Browser与Server。中文意思:即 浏览器与服务端架构
二.网络基础
1.OSI七层模型
物理层功能:主要是基于电器特性发送高低电压(电信号),高电压对应数字1,低电压对应数字0
数据链路层的功能:定义了电信号的分组方式(单纯的电信号0和1没有任何意义,必须规定电信号多少位一组,每组什么意思)
网络层功能:引入一套新的地址用来区分不同的广播域/子网,这套地址即网络地址
传输层功能:建立端口到端口的通信,TCP协议,UDP协议
会话层:建立客户端与服务端连接
表示层:对来自应用层的命令和数据进行解释,按照一定格式传给会话层。如编码、数据格式转换、加密解密、压缩解压
应用层功能:规定应用程序的数据格式。
2.简述三次握手 四次挥手的流程
三次握手: 1.客户端(Client)向服务端(Server)发送一次请求 2.服务端确认并回复客户端 3.客户端检验确认请求,建立连接 四次挥手: 1.客户端向服务端发一次请求 2.服务端回复客户端(断开客户端-->服务端) 3.服务端再次向客户端发请求(告诉客户端可以断开了) 4.客户端确认请求,连接断开
3.TCP协议和UDP协议
tcp协议:面向连接,消息可靠,相对udp来讲,传输速度慢,消息是面向流的,无消息保护边界0
udp协议:面向无连接,消息不可靠,传输速度快,消息是面向包的,有消息保护边界.
TCP协议:面向连接 - 通信之前先三次握手 - 断开之前先四次握手 - 必须先启动服务端,再启动客户端-->连接服务端 - 安全、可靠、面向连接(不会丢包) UDP协议:无连接 - 传输速度快 - 先启动哪一端都可以 - 不面向连接,不能保证数据的完整性
直接上图:
三.套接字(socket)使用
基于TCP协议的socket
server端
import socket # 创建服务端socket对象 server = socket.socket() # 绑定IP和端口 server.bind(('192.168.13.100',8000)) # 后边可以等5个人 server.listen(5) print('服务端准备开始接收客户端的连接...') # 等待客户端来连接,如果没人连接就一直等待 # conn是客户端与服务端连接的对象,服务端以后要通过该对象进行收发数据 # addr是客户端的地址信息 # 阻塞,只有客户端进行连接,则获取客户端连接然后开始进行通信 conn,addr = server.accept() print('已经有人连接上了,客户端信息:',addr) # 通过对象去获取(客户端发送的消息) # 1024表示:服务端获取数据时,一次最多拿1024字节 data = conn.recv(1024) print('收到消息:',data.decode('utf-8')) # 服务端通过连接对象给客户端恢复一个消息 conn.send('赛利亚!你好,我是凯丽'.encode('utf-8')) # 与客户端断开连接 conn.close() # 关闭服务端的服务 server.close()
client端
import socket client = socket.socket() # 客户端向服务端发起连接请求 # 阻塞,去连接,直到连接成功才会继续向下执行 client.connect(('192.168.13.100',8000)) # 连接上服务端后,向服务端发送消息 client.send('你好啊,我是赛利亚...'.encode('utf-8')) # 等待服务端回复消息 data = client.recv(1024) print(data.decode('utf-8')) # 关闭自己(客户端) client.close()
基于UDP协议的socket
server端
import socket #创建一个udp协议下的socket,需要使用参数type udp_server = socket.socket(type=socket.SOCK_DGRAM) #DGRAM : datagram 数据报 #拿到一个地址,启动程序的时候,告诉电脑,你给我这个程序分配8001端口. ip_port = ('192.168.15.91',8001) #绑定IP地址和端口 udp_server.bind(ip_port) print('准备接收消息了...') #接收消息,from_client_msg来自客户端的消息,client_addr客户端的地址('192.168.15.113', 8001) from_client_msg,client_addr = udp_server.recvfrom(1024) #阻塞住了 print(11111) print(from_client_msg) print(client_addr) #发送消息 udp_server.sendto(b'It''s Monday',client_addr) #关闭udp的socket对象 udp_server.close()
client端
import socket udp_client = socket.socket(type=socket.SOCK_DGRAM) server_ip_port = ('192.168.15.91',8001) udp_client.sendto(b'What day is today?',server_ip_port) from_server_msg,server_addr = udp_client.recvfrom(1024) print(from_server_msg) print(server_addr) udp_client.close()
服务端套接字函数
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() 创建一个与该套接字相关的文件
四.黏包
1.什么是黏包
粘包:数据粘在一起,主要因为:接收方不知道消息之间的界限,不知道一次性提取多少字节的数据造成的
数据量比较小,时间间隔比较短,就合并成了一个包,这是底层的一个优化算法(Nagle算法)
注意:只有TCP有粘包现象,UDP永远不会粘包
2.基于tcp协议的黏包现象成因
基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,
根本不知道该文件的字节流从何处开始,在何处结束
此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据
后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP
段后一次发送出去,这样接收方就收到了粘包数据。
3.会发生黏包的两种情况
情况一 发送方的缓存机制
发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据量很小,会合到一起,产生粘包)
import socket server = socket.socket() server.bind(('192.168.15.91',8005)) server.listen(5) conn,addr = server.accept() data1 = conn.recv(1024).decode('utf-8') data2 = conn.recv(1024).decode('utf-8') print('data1:',data1) print('data2:',data2) conn.close() server.close()
import socket client = socket.socket() client.connect(('192.168.15.91',8005)) client.send('你好啊'.encode('utf-8')) client.send('我是赛利亚'.encode('utf-8')) client.close()
情况二 接收方的缓存机制
接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
import socket server = socket.socket() server.bind(('192.168.15.91',8005)) server.listen(5) conn,addr = server.accept() data1 = conn.recv(10).decode('utf-8') # 一次没有接收完整数据 data2 = conn.recv(1024).decode('utf-8') # 下次接收的时候,会先取没接收完的旧数据,然后再取新数据 print('data1:',data1) print('data2:',data2) conn.close() server.close()
import socket client = socket.socket() client.connect(('192.168.15.91',8005)) client.send('你好啊! 我是赛利亚'.encode('utf-8')) client.close()
总结:
黏包现象只发生在tcp协议中:
1.从表面上看,黏包问题主要是因为发送方和接收方的缓存机制、tcp协议面向流通信的特点。
2.实际上,主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的
4.黏包的解决方案
最主要原因就是接收端不知道发送端发送的数据字节流大小,所以解决方案就围绕,如何让接收端接收数据前知道要接收数据字节流大小,然后根据数据字节流大小,循环接收完所有数据
解决方法一:
先了解一个叫:subprocess的模块
import subprocess msg = input('请输入你要执行的指令:') res=subprocess.Popen(msg, shell=True, stderr=subprocess.PIPE, # 指令错误的时候执行stderr stdout=subprocess.PIPE # 指令正确的时候执行stdout ) print(res.stdout.read().decode('gbk')) print(res.stderr.read().decode('gbk')) 请输入你要执行的指令:dir 驱动器 D 中的卷是 系统 卷的序列号是 A054-442E D:\ 2018/10/18 20:33 <DIR> . 2018/10/18 20:33 <DIR> .. 2018/10/18 20:33 350 11.py 1 个文件 350 字节 2 个目录 326,375,858,176 可用字节
import socket,subprocess ip_port=('127.0.0.1',8080) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind(ip_port) s.listen(5) while True: conn,addr=s.accept() print('客户端',addr) while True: msg=conn.recv(1024) if not msg:break res=subprocess.Popen(msg.decode('utf-8'),shell=True, stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE) err=res.stderr.read() if err: ret=err else: ret=res.stdout.read() data_length=len(ret) conn.send(str(data_length).encode('utf-8')) data=conn.recv(1024).decode('utf-8') if data == 'recv_ready': conn.sendall(ret) conn.close()
import socket s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) res=s.connect_ex(('127.0.0.1',8080)) while True: msg=input('请输入要执行的指令: ').strip() if len(msg) == 0:continue if msg == 'quit':break s.send(msg.encode('utf-8')) length=int(s.recv(1024).decode('utf-8')) s.send('recv_ready'.encode('utf-8')) send_size=0 recv_size=0 data=b'' while recv_size < length: data+=s.recv(1024) recv_size+=len(data) print(data.decode('gbk'))
解决方法二:
需要借助struct这个模块,这个模块可以把要发送的数据长度转换成固定长度的字节.
import struct # 压包 res = struct.pack('i',999999999) # 'i'是一个模式,int的意思 print(res) print(len(res)) # 解包 obj = struct.unpack('i',res) print(obj) # 打印的是一个元组 print(obj[0]) b'\xff\xc9\x9a;' 4 (999999999,) 999999999
import socket import subprocess # 傻波普润赛丝 import struct server = socket.socket() # 创建套接字 server.bind(('192.168.15.91', 8001)) # 绑定端口 server.listen(5) # 排队5人 while True: print('等待客户端连接..') conn, addr = server.accept() print('客户信息:',conn) while True: try: cmd = conn.recv(1024).decode('utf-8') if cmd == b'exit': break res = subprocess.Popen(cmd, shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE) out = res.stdout.read() # 正确结果 err = res.stderr.read() # 错误结果 print('out响应长度:',len(out)) print('err响应长度:',len(err)) if err: # 构建报头 res = struct.pack('i', len(err)) # 发送报头 conn.send(res) # 发送数据 conn.send(err) else: # 构建报头 res = struct.pack('i',len(out)) # 发送报头 conn.send(res) # 发送数据 conn.send(out) except Exception as e: break
import socket import struct sk = socket.socket() sk.connect(('192.168.15.91', 8001)) while True: cmd = input('请输入指令:') sk.send(cmd.encode('utf-8')) if cmd == "": continue if cmd == 'exit': break res = sk.recv(4) data_length = struct.unpack('i',res)[0] print('响应长度:',data_length) recv_data_length = 0 recv_data = b'' while recv_data_length < data_length: data = sk.recv(1024) recv_data_length += len(data) recv_data += data print(recv_data.decode('gbk')) sk.close()
总结:
解决黏包就是在传递数据的时候让接收端知道要接收数据字节流大小,接收端根据数据字节流大小来接收。