网络编程补充
一、OSI七层协议
1.OSI七层协议之传输层
传输层规定了数据传输所遵循的规则
1.PORT协议
2.TCP协议UDP协议
ps:数据传输数据能够遵循的协议很多 TCP和UDP协议是最常见的两种
2.TCP协议
1.三次握手
Client会向Server发送通信请求,然后Server会同意请求并向Client发送通信请求,然后Client也会向Server发送同意请求
这样它们两个就完成了双向通道,就可以通信了
ps:洪水攻击
同时让大量用户向服务端发送连理TCP链接的请求
2.四次挥手
Client会向Server发送断开请求,Server会同意断开请求,然后在向Client发送断开请求,这个时候Client就会同意断开请求
这样两个就会断开通信了
中间两步不能合并
应为中间要有检查过程
一个问题:
基于TCP传输数据非常的安全 因为有双向通道?
是错的
基于TCP传输数据是安全的,但是是因为数据不容易丢失!!! 不容易丢失的原因在于二次确认机制
每次发送数据都需要返回确认消息 否则在一定的时间会反复发送,而不是双向通道
3.UDP协议
基于UDP协议发送数据 没有任何的通道也没有任何的限制
UDP发送数据没有TCP安全(没有二次确认机制)
""" TCP协议类似于:两个人打电话 两个人你一句我一句 有来有往 UDP协议类似于:一个人向另一个发送短信 只要发送了 不给你收没收到不管了 也不需要另一个人回复 """
二、应用层
TCP协议可以为各种各样的程序传递数据,比如Email、WWW、FTP等等。那么,必须有不同协议规定电子邮件、网页、FTP数据的格式,这些应用程序协议就构成了”应用层”。
三、socket套接字
基于文件类型的套接字家族
套接字家族的名字:AF_UNIX
基于网络类型的套接字家族
套接字家族的名字:AF_INET
两个进程需要进行通讯,最基本的一个前提是能够唯一的标识一个进程,在本地进程中我们可以使用 PID 来唯一标识一个进程,但 PID 只在本地唯一,网络中的两个进程 PID 冲突的几率很大。这时候我们需要另辟它径了,我们知道IP层的ip地址可以唯一标示主机,而TCP层协议和端口号可以唯一标示主机的一个进程,这样我们可以利用ip地址+协议+端口号唯一标示网络中的一个进程 。
能够唯一标示网络中的进程后,它们就可以利用socket进行通信了,什么是socket呢?我们经常把socket翻译为套接字,socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用已实现进程在网络中通信。
socket起源于UNIX,在Unix一切皆文件哲学的思想下,socket是一种"打开—读/写—关闭"模式的实现,服务器和客户端各自维护一个"文件",在建立连接打开后,可以向自己文件写入内容供对方读取或者读取对方内容,通讯结束时关闭文件。
1.socket代码
运行程序时 首先确保先运行程序的是服务端 然后才运行客户端
服务端
# 服务端 import socket # 1.创建一个socket对象 server = socket.socket() # 括号内什么都不写 默认就是基于网络的TCP套接字 # 2.绑定一个固定的地址(ip\port) server.bind(('127.0.0.1', 8080)) # 127.0.0.1本地回环地址(只允许自己的机器访问) # 3.半连接池(暂且忽略) server.listen(5) # 4.开业 等待接客 sock, address = server.accept() print(sock, address) # sock是双向通道 address是客户端地址 # 5.数据交互 sock.send(b'hello big baby~') # 朝客户端发送数据 data = sock.recv(1024) # 接收客户端发送的数据 1024bytes print(data) # 6.断开连接 sock.close() # 断链接 server.close() # 关机
客户端
# 客户端 import socket # 1.产生一个socket对象 client = socket.socket() # 2.连接服务端(拼接服务端的ip和port) client.connect(('127.0.0.1', 8080)) # 3.数据交互 data = client.recv(1024) # 接收服务端发送的数据 print(data) client.send(b'hello sweet server') # 朝服务端发送数据 # 4.关闭 client.close()
这个时候两个页面就可以互相传输数据了 这不过真能传一次
2.代码优化
1.send与recv 客户端与服务端不能同时执行同一个 有一个收 另外一个就是发 有一个发 另外一个就是收 不能同时收或者发!!! 2.消息自定义 input获取用户数据即可(主要编码解码) 3.循环通信 给数据交互环节添加循环即可 4.服务端能够持续提供服务 不会因为客户端断开连接而报错 异常捕获 一旦客户端断开连接 服务端结束通信循环 调到连接处等待 5.消息不能为空 判断是否为空 如果是则重新输入(主要针对客户端) 6.服务端频繁重启可能会报端口被占用的错(主要针对mac电脑) from socket import SOL_SOCKET,SO_REUSEADDR server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加 7.客户端异常退出会发送空消息(针对mac linux) 针对接收的消息加判断处理即可
服务端
import socket from socket import SOL_SOCKET,SO_REUSEADDR server = socket.socket() server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加 server.bind(('127.0.0.1', 8081)) server.listen(5) while True: sock, address = server.accept() while True: msg = input('请输入发送给客户端信息>>:').strip() if len(msg) == 0: continue sock.send(msg.encode('utf8')) data = sock.recv(1024) print(data.decode('utf8'))
客户端
import socket client = socket.socket() client.connect(('127.0.0.1', 8081)) while True: data = client.recv(1024) print(data.decode('utf8')) msg = input('请输入发送给服务端的信息>>>:').strip() if len(msg) == 0: msg = '手滑了' client.send(msg.encode('utf8'))
四、半连接池
server.listen(5) ''' 其实就是限制了客户端链接服务端的个数 括号里写几个就客户端的最大个数就是 N+1 主要是为了做缓冲 避免太多无效等待 '''
五、粘包问题
# 服务端代码 sock.recv(1024) sock.recv(1024) sock.recv(1024) # 客户端代码 client.send(b'jason') client.send(b'kevin') client.send(b'tony') # 然后服务端接收的的结果为: b'jasonkevintony' b'' b'' # 全都当做一条数据发送了 这就是粘包问题 ''' 1.TCP特性 流式协议:所有的数据类似水流一样 链接一起了 ps:数据量很小 并且时间间隔很多 那么就会自动组织到一起 2.recv 我们不知道即将要接收的数据量多大 如果知道的话不会产生也不会产生黏包 '''
六、struct模块
1.struct代码演示
# 这个时候后我们介绍一个模块struct模块 ''' struct模块无论数据长度是多少 都可以帮你打包成固定长度 然后基于该固定长度 还可以反向解析出真实长度 ''' # 代码演示: import struct info = '你好美女 交个朋友' print(len(info)) # 9 数据原本的长度 res = struct.pack('i', len(info)) # 将数据原本的长度打包 print(len(res)) # 4 打包之后的长度是4 ret = struct.unpack('i', res) # 将打包之后固定长度为4的数据拆包 print(ret[0]) # 9 又得到了原本数据的长度 import struct info1 = '打起精神啊 下午也需要奋斗 也需要认真听 客服困难 你困我也困!!!' print(len(info1)) # 34 res = struct.pack('i', len(info1)) # 将数据原本的长度打包 print(len(res)) # 4 打包之后的长度是4 ret = struct.unpack('i', res) print(ret[0]) # 34 # 这样我们就可以用struct模块帮助我们解决粘报问题
# 但是会有点小问题
其实struct模块不管是什么模式他的长度是有限制的 如果我们想要传输的数据过大的话也是传输不了的
''' 其实我们可把想要的数据放在字典中 解决思路: 服务端 1.我们也把想要传输的数据都放在字典中 2.然后把字典转换成json字符串格式 3.然后用len算出json格式字符串的长度 4.然后再用strutc模块打包字典发送 5.再发送字典数据 6.然后在发送真实数据 客户端 1.接收报头长度 2.将报头长度转反解包成字典的长度 3.然后接收字典数据 4.在将字典转换成字符串格式 5.然后在接收真是数据 '''
2.解决粘报代码演示
服务端
# 服务端 import socket import struct import os import json server = socket.socket() server.bind(('127.0.0.1', 9000)) server.listen(5) while True: sock, address = server.accept() while True: dict_data = { 'file_name': 'jason 老师集合', 'file_size': os.path.getsize(r'jason 老师合集.txt'), 'file_root': 'tony' } # 1.将字典转换成字符串 dict_json = json.dumps(dict_data) # 2.计算字符串转成二进制后的长度 file_dict_len = len(dict_json.encode('utf8')) # 3.将计算后的长度转换成报头 dict_len = struct.pack('i', file_dict_len) # 4.将固定的包头发送 sock.send(dict_len) # 5.然后在将真实的字典数据发送 sock.send(dict_json.encode('utf8')) # 6.然后在发送字典中的真是数据 with open(r'jason 老师合集.txt', 'rb')as f: for line in f: sock.send(line) break
客户端
# 客户端 import socket import struct import json import os client = socket.socket() client.connect(('127.0.0.1', 9000)) while True: # 1.接收报头的长度 header_len = client.recv(4) # 2.将接收到的长度解包成字典的真是长度 dict_len = struct.unpack('i', header_len)[0] # 3.按照字典的长度接收到真是的字典数据 dict_json = client.recv(dict_len) # 现在的字典还是json格式的bytes类型 # 4.转换成真是字典 dict_data = json.loads(dict_json) # 5.然后就可以知道接收的数据的长度 total_size = dict_data.get('file_size') # 6.创建一个变量名 file_size = 0 # 7.打开文件写入数据 with open(r'%s' % dict_data.get('file_name'), 'wb')as f: # 8.判断接收的长度 while file_size < total_size: # 当变量大于真实数据的长度的时候就退出数据的接收 data = client.recv(1024) # 接收真实数据 f.write(data) # 将接受到的数据写入 file_size += len(data) # 将变量加上每次读取数据的长度,以防最后一次接收数据的时候小于1024 break
作业
1.编写一个cs架构的软件
就两个功能
一个视频下载:从服务端下载视频
一个视频上传:从客户端上传视频
服务端
import socket import struct import json from socket import SOL_SOCKET, SO_REUSEADDR server = socket.socket() server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) server.bind(('127.0.0.1', 8050)) server.listen(5) while True: sock, address = server.accept() while True: # 接收报头长度 header_len = sock.recv(4) # 将接收到的报头长度转换成字典长度 dict_len = struct.unpack('i', header_len)[0] # 然后根据字典长度接收字典数据 dict_json = sock.recv(dict_len) # 现在的字典是json二进制格式 # 将json格式转换成真实字典格式 dict_data = json.loads(dict_json) # 然后就知道电影数据的长度了 movie_len = dict_data.get('file_size') total_size = 0 with open(r'%s.mp4' % dict_data.get('file_name'), 'wb') as f: while total_size < movie_len: data = sock.recv(1024) f.write(data) total_size += len(data) break
客户端
import socket import struct import os import json client = socket.socket() client.connect(('127.0.0.1', 8050)) movie_dir = r'D:\网络编程day02\视频' while True: movie_list = os.listdir(movie_dir) for i, j in enumerate(movie_list): print(i, j) choice = input('请输入您要上传的视频编号>>>:').strip() if not choice.isdigit(): print('视频编号必须是纯数字') continue choice = int(choice) if choice not in range(len(movie_list)): print('视频编号超出范围') continue # 拿到视频名字 movie_name = movie_list[choice] # 把想要的上传的视频拼接路径 movie_name_dir = os.path.join(movie_dir, movie_name) movie_dict = { 'file_name': '06 socket代码优化', 'file_size': os.path.getsize(movie_name_dir), 'file_root': 'tony', 'file_desc': 'jason老师激情授课 速速观看' } # 将字典转成json格式 movie_json = json.dumps(movie_dict) # 将计算转成json格式的二进制的长度 movie_dict_len = len(movie_json.encode('utf8')) # 将转成的长度变成报头 file_len = struct.pack('i', movie_dict_len) # 然后将报头发送出去 client.send(file_len) # 然后发送字典数据 client.send(movie_json.encode('utf8')) # 然后发送电影数据 with open(r'%s' % movie_name_dir, 'rb') as f: for line in f: client.send(line) break