python之路——网络编程
计算机网络
ftp作业
网络编程
mac地址
arp协议:通过ip地址找mac地址
ip地址是什么——一台机器在网络上的位置
公网ip 局域网ip
端口号
网络相关的程序才需要开一个端口,为的是能找到某台计算机上唯一的一个程序
在同一台机器上,同一时间同一端口只能被一个程序占用
一般情况下使用8000之后的端口
TCP协议和UDP协议
TCP:可靠的(不会丢包),面向链接的,相对耗时长
三次握手,四次挥手
UDP:不可靠,无链接,效率高
常见问题
ip协议属于网络osi7层协议中的哪一层:网络层
tcp协议和udp协议属于传输层
arp协议属于数据链路层
应用层的协议:http(https)网页 ftp文件传输 smtp邮件相关的协议
socket
tcp协议(server端要先运行)
- 简单例子
#服务端代码
import socket
sk = socket.socket() #买手机 创建一个socket对象
sk.bind(('127.0.0.1',8080)) #给server端绑定一个ip和端口
sk.listen() #监听链接
conn,addr = sk.accept() #获取一个客户端的链接,已经完成三次握手,建立的链接就是conn
# 阻塞(等待一个客户端的链接)
msg = conn.recv(1024) #阻塞,直到收到一个客户端发的消息
print(msg)
conn.send(b'hello') #发消息
conn.close() #关闭链接
sk.close() #关闭socket对象,如果不关闭,还能继续接收
#现在有一个服务端和两个客户端
#server与client1建立长链接,此时client1与server可通信
#client2可链接,占线状态,发送的消息会被缓存,直到client1与server断开长链接
#客户端代码
import socket
sk = socket.socket()
sk.connect(('127.0.0.1',8080))
sk.send(b'hi')
ret = sk.recv(1024)
print(ret)
sk.close()
- 尬聊
#server端
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',8888))
sk.listen()
conn,addr = sk.accept()
while True:
msg = conn.recv(1024)
msg = msg.decode('utf-8')
if msg == 'bye':
break
print(msg)
info = input('请输入一句话:')
if info == 'bye':
info = info.encode('utf-8')
conn.send(info)
break
info = info.encode('utf-8')
conn.send(info)
conn.close()
sk.close()
#client端
import socket
sk = socket.socket()
sk.connect((('127.0.0.1',8888)))
while True:
msg = input('请输入一句话:')
if msg == 'bye':
msg = msg.encode('utf-8')
sk.send(msg)
break
msg = msg.encode('utf-8')
sk.send(msg)
ret = sk.recv(1024)
ret = ret.decode('utf-8')
if ret == 'bye':
break
print(ret)
sk.close()
udp协议(都可以先运行)
- 简单例子
#server端
import socket
sk = socket.socket(type=socket.SOCK_DGRAM) #DGRAM datagram
sk.bind((‘127.0.0.1’,8080))
msg, addr = sk.recvfrom(1024)
msg = msg.decode(‘utf-8’)
print(msg)
sk.sendto(b’bye’,addr)
sk.close()
#udp的server不需要进行监听也不需要建立链接
#在启动服务之后只能被动的等待客户端发送消息过来,这点与tcp不同(tcp两边都可以发送)
#客户端发送消息的同时还会自带地址信息
#消息回复的时候不仅需要发送消息,还需要把对方的地址写在后面
#client端
import socket
sk = socket.socket(type = socket.SOCK_DGRAM)
ip_port = (‘127.0.0.1’, 8080)
sk.sendto(b’hello’,ip_port)
ret, addr = sk.recvfrom(1024)
ret = ret.decode(‘utf-8’)
print(ret)
sk.close()
- 简单QQ
#server端
import socket
sk = socket.socket(type=socket.SOCK_DGRAM)
ip_port = (‘127.0.0.1’, 8080)
sk.bind(ip_port)
while True:
msg, addr = sk.recvfrom(1024)
msg = msg.decode(‘utf-8’)
print(msg)
info = input(‘>>>’).encode(‘utf-8’)
sk.sendto(info, addr)
sk.close()
#client端
import socket
sk = socket.socket(type=socket.SOCK_DGRAM)
ip_port = (‘127.0.0.1’,8080)
while True:
info = input(‘A请输入一句话:’)
info = (‘\033[32m来自A的消息:%s\033[0m’ %info).encode(‘utf-8’)
sk.sendto(info,ip_port)
msg, addr = sk.recvfrom(1024)
msg = msg.decode(‘utf-8’)
print(msg)
sk.close()
#其他客户端复制粘贴即可,只需要改为某某请输入一句话
- 作业
提供报时服务,接受一个时间格式,然后将时间转化为接收到的格式发给客户端。(用udp写)
例子(黏包现象)
- tcp
所有的客户端执行server端下发的cmd指令,将结果反馈回来。
需要用到以下工具
import subprocess
res = subprocess.Popen(‘dir’,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
print(‘strout:’,res.stdout.read().decode(‘gbk’))
print(‘stderr:’,res.stderr.read().decode(‘gbk’))
#因为windows默认为gbk编码
#stdout指的是正常输出
#stderr指的是错误输出
#基于tcp实现的远程执行命令
#在server下发命令
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',8080))
sk.listen()
conn, addr =sk.accept()
while True:
cmd = input('>>>')
if cmd == 'q':
conn.send(b'q')
break
cmd = cmd.encode('gbk')
conn.send(cmd)
res = conn.recv(1024).decode('gbk')
print(res)
conn.close()
sk.close()
#在client端接收命令并执行
import socket
import subprocess
sk = socket.socket()
ip_port =('127.0.0.1',8080)
sk.connect(ip_port)
while True:
cmd = sk.recv(1024).decode('gbk')
if cmd == 'q':
break
res = subprocess.Popen(cmd,shell= True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
sk.send(res.stdout.read())
sk.send(res.stderr.read())
sk.close()
#这里两个发对应一个收,会产生黏包现象
#不丢包
- udp
import socket
sk = socket.socket(type=socket.SOCK_DGRAM)
ip_port = (‘127.0.0.1’,8090)
sk.bind(ip_port)
msg, addr = sk.recvfrom(1024)
while True:
cmd = input(‘>>>’)
if cmd == ‘q’:
break
sk.sendto(cmd.encode(‘utf-8’),addr)
msg,addr = sk.recvfrom(1024)
print(msg.decode(‘utf-8’)
sk.close()
import socket
import subprocess
sk = socket.socket(type=socket.SOCK_DGRAM)
ip_port = (‘127.0.0.1’,8090)
sk.sendto(b’hi’,ip_port)
while True:
cmd,addr = sk.recvfrom(1024)
cmd = cmd.decode(‘utf-8’)
ret = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
std_out = ‘stdout:’+(ret.stdout.read()).decode(‘gbk’)
std_err = ‘stderr:’+(ret.stderr.read()).decode(‘gbk’)
print(std_out)
print(std_err)
sk.sendto(std_out.encode(‘utf-8’),addr)
sk.sendto(std_err.encode(‘utf-8’),addr)
sk.close()
#不会黏包,会丢包
- 作业
网盘 文件的上传下载
登陆:客户端登陆,将用户名和密码发给服务器,服务器确认信息后上传下载
选择上传/下载
上传:选择要上传的文件路径,在server创建一个同名的空文件夹
下载:选择要下载的文件路径,在client端创建一个同名的空文件
只需要实现上述的功能,真正的传输可以用几句话代替(打印几句话之类的) -
黏包问题原因
图片与程序相反,图片是服务器给客户端发文件,程序是服务端接收文件
两个recv,第一个recv特别小
#server
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',8090))
sk.listen()
conn, addr = sk.accept()
ret = conn.recv(2)
ret2 = conn.recv(10)
print(ret)
print(ret2)
conn.close()
sk.close()
#client
import socket
sk = socket.socket()
sk.connect(('127.0.0.1',8090))
sk.send(b'hello,egg')
sk.close()
黏包问题本质的问题是不知道发送数据的长度
连续send两个小数据
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',8090))
sk.listen()
conn, addr = sk.accept()
ret = conn.recv(12)
print(ret)
conn.close()
sk.close()
import socket
sk = socket.socket()
sk.connect(('127.0.0.1',8090))
sk.send(b'hello')
sk.send(b'egg')
sk.close()
#tcp协议内部的优化算法,连续的小数据包会被合并
- 黏包问题解决方法
首先,发送数据长度
按照数据的长度接收数据
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',8080))
sk.listen()
conn, addr =sk.accept()
while True:
cmd = input('>>>')
if cmd == 'q':
conn.send(b'q')
break
cmd = cmd.encode('gbk')
conn.send(cmd) #引入struct模块这一步可删除
num = conn.recv(1024).decode('utf-8')
conn.send(b'ok')
res = conn.recv(int(num)).decode('gbk')
print(res)
conn.close()
sk.close()
import socket
import subprocess
sk = socket.socket()
ip_port =('127.0.0.1',8080)
sk.connect(ip_port)
while True:
cmd = sk.recv(1024).decode('gbk')
if cmd == 'q':
break
res = subprocess.Popen(cmd,shell= True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
std_out = res.stdout.read()
std_err = res.stderr.read()
sk.send(str(len(std_out)+len(std_err)).encode('utf-8'))
#发送的长度是一个小数据,可能会发生粘包,所以要有一个接收与真正要发送的消息区别开
sk.recv(1024)#
sk.send(std_out)
sk.send(std_err)
sk.close()
#好处:确定了我到底要接收多大的数据
#要在文件中设置一个配置项:就是每一次recv的大小,一般情况下程序里不会超过4096个字节
#当我们要发送大数据的时候,要明确的告诉接收方数据的大小,
# 以便接收方能准确的接收所有数据
#多用于文件传输
#大文件的传输 一定是按照字节读 每一次读固定的字节
#传输的过程中 一边读一遍传 接收端 一边收一边写
#不好的地方:多了一次交互
#send和sendto在超过一定范围的时候都会报错
#要设计好程序的内存管理
- struct模块,进一步解决黏包问题
该模块可以把一个类型,如数字,转成固定长度的bytes
import struct
ret = struct.pack('i',80000) #'i'代表int,就是即将要把一个数字转化成固定长度(数字对应4)的bytes类型
print(ret)
print(len(ret))
num = struct.unpack('i',ret) #num是一个元组
print(num[0])
#若已知是固定长度,这样接收就有了确定长度,可在上一次的基础上继续改进程序
import struct
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',8080))
sk.listen()
conn, addr =sk.accept()
while True:
cmd = input('>>>')
if cmd == 'q':
conn.send(b'q')
break
cmd = cmd.encode('gbk')
conn.send(cmd)
num = conn.recv(4)
num = struct.unpack('i',num)[0]
res = conn.recv(int(num)).decode('gbk')
print(res)
conn.close()
sk.close()
import socket
import struct
import subprocess
sk = socket.socket()
ip_port =('127.0.0.1',8080)
sk.connect(ip_port)
while True:
cmd = sk.recv(1024).decode('gbk')
if cmd == 'q':
break
res = subprocess.Popen(cmd,shell= True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
std_out = res.stdout.read()
std_err = res.stderr.read()
len_num = len(std_out)+len(std_err)
num = struct.pack('i',len_num) #已经是bytes形式,不用decode
sk.send(num)
sk.send(std_out)
sk.send(std_err)
sk.close()
-
struct定制报头
文件传输代码
import json
import struct
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',8090))
sk.listen()
buffer = 1024
conn, addr =sk.accept()
head_len = conn.recv(4) #接收bytes长度为4(报头的长度)
head_len = struct.unpack('i',head_len)[0] #转化为int类型
json_head = conn.recv(head_len).decode('utf-8') #根据报头的长度接收报头的内容,将bytes转化为字符串
head = json.loads(json_head) #得到报头的信息
filesize = head['filesize']
with open(head['filename'],'wb') as f:
while filesize:
if filesize >= buffer:
content = conn.recv(buffer) #接收文件并写入
f.write(content)
filesize -= buffer
else:
content = conn.recv(filesize)
f.write(content)
break
conn.close()
sk.close()
#发送端
import os
import json
import struct
import socket
sk = socket.socket()
sk.connect(('127.0.0.1',8090))
buffer = 1024
#发送文件
head = {'filepath':r'D:\Python全栈9期(第一部分):基础+模块+面向对象+网络编程\day18'
,'filename':r'04 python fullstack s9day18 re模块.mp4'
,'filesize':None}
file_path = os.path.join(head['filepath'],head['filename'])
filesize = os.path.getsize(file_path)
head['filesize'] = filesize
json_head = json.dumps(head) #字典转化为字符串
bytes_head = json_head.encode('utf-8') #字符串转bytes
#计算head的长度
head_len = len(bytes_head) #报头的长度
pack_len = struct.pack('i',head_len)
sk.send(pack_len) #先发报头的长度
sk.send(bytes_head) #再发bytes类型的报头
with open(file_path,'rb') as f:
while filesize:
if filesize >= buffer:
content = f.read(buffer) #每次读出来的内容
sk.send(content) #将文件拆分为小块后发送
filesize -= buffer
else:
content = f.read(filesize)
sk.send(content)
break
8.总结
hmac的检验客户端合法性
检测一下客户端是否合法,不依靠登陆认证,也可以用hashilib来实现
import hmac
h = hmac.new() #通过密钥和你想进行加密的bytes创建一个对象
密文 = h.digest() #得到一个密文
hmac.compare_digest() #对比密文和另一个密文,返回一个True或者Flase
#服务端发送一个bytes给客户端,
#正常的客户端都有和服务端相同的key,
#所以正常的客户端得到的密文和服务端密文一致
#通过compare_digest进行对比,就可以判断客户端是否正常
import os
import hmac
import socket
secret_key = b'egg1'
sk = socket.socket()
sk.bind(('127.0.0.1',8080))
sk.listen()
def check_conn(conn):
msg = os.urandom(32)
conn.send(msg)
h = hmac.new(secret_key,msg)
digest = h.digest()
client_digest = conn.recv(1024)
return hmac.compare_digest(digest, client_digest)
conn,addr = sk.accept()
res = check_conn(conn)
if res:
print('合法的客户端')
conn.close()
else:
print('不合法的客户端')
conn.close()
sk.close()
import hmac
import socket
secret_key = b'egg'
sk = socket.socket()
sk.connect(('127.0.0.1',8080))
msg = sk.recv(1024)
h = hmac.new(secret_key,msg)
digest = h.digest()
sk.send(digest)
sk.close()
socketserver
简单实现
#socketserver可以和多个客户端通信
import socketserver
class MyServer(socketserver.BaseRequestHandler):
def handle(self): #self.request就相当于一个conn
print(self.request.recv(1024).decode('utf-8'))
if __name__ == '__main__':
server = socketserver.ThreadingTCPServer(('127.0.0.1',8080),MyServer)
#thread 线程
server.serve_forever()
import socket
sk = socket.socket()
sk.connect(('127.0.0.1',8080))
sk.send('饿了么'.encode('utf-8'))
sk.recv(1024)
sk.close()
与多个客户端通信,这段代码存在问题
疑点:#sk.send(b'q')和 self.request.close()
当sk.send(b'q')在美团客户端内不被注释掉的时候,可以正常关闭,搞清楚self.request.close()的机制
#socketserver可以和多个客户端通信
import socketserver
class MyServer(socketserver.BaseRequestHandler):
def handle(self): #self.request就相当于一个conn
while True:
msg = self.request.recv(1024).decode('utf-8')
if msg == 'q':
self.request.close()
break
print(msg)
info = input('>>>')
self.request.send(info.encode('utf-8'))
if __name__ == '__main__':
server = socketserver.ThreadingTCPServer(('127.0.0.1',8080),MyServer)
#thread 线程
server.serve_forever()
import socket
sk = socket.socket()
sk.connect(('127.0.0.1',8080))
while True:
msg = input('>>>')
if msg == 'q':
#sk.send(b'q')
break
sk.send(('饿了么:'+msg).encode('utf-8'))
ret = sk.recv(1024).decode('utf-8')
print(ret)
sk.close()
import socket
sk = socket.socket()
sk.connect(('127.0.0.1',8080))
while True:
msg = input('>>>')
if msg == 'q':
sk.send(b'q')
break
sk.send(('美团:'+msg).encode('utf-8'))
ret = sk.recv(1024).decode('utf-8')
print(ret)
sk.close()
看源码方法
- 多个类之间的继承关系要先整理
- 每个类的方法要大致列出来
- 要了解self是谁的对象
- 所有的方法要退回最子类的类中开始寻找,逐级向上
socketserver源码视频讲解较难,有时间自己去看
ftp作业
大作业,暂时不做