TFTP网络编程
TFTP介绍
TFTP(Trivial File Transfer Protocol,简单⽂件传输协议)是TCP/IP协议簇中的⼀个⽤来在客户端与服务器之间进⾏简单⽂件传输的协议
使用tftp这个协议,就可以实现简单文件的下载
特点:
简单、占⽤资源⼩、适合传递⼩⽂件、适合在局域⽹进⾏传递、端⼝号为69、基于UDP实现
网络调试助手工具下载
链接:https://pan.baidu.com/s/14qwiErUKlOfnCcvx9Edoeg
提取码:1122
Tftpd32:共享服务器(可以从本机共享文件)
Tftpd32工具下载
链接:https://pan.baidu.com/s/1VlTXpETIKSumsBttJ9i5dA
提取码:1122
browse:选择一个文件夹,确定给客户端文件时的搜索路径
客户端:数据接收方
服务器:数据发送方
代码实现客户端TFTP下载器
流程图
格式
#构造下载请求数据:“1test.jpg0octet0”
import struct
cmb_buf = struct.pack(“!H8sb5sb”,1,b“test.jpg”,0,b“octet”,0)
#如何保证操作码(1/2/3/4/5)占两个字节?如何保证0占一个字节?
#!H8sb5sb: ! 表示按照网络传输数据要求的形式来组织数据(占位的格式)
#!固定写法
#H 表示将后面的 1 替换成占两个字节
#8s 相当于8个s(ssssssss)占8个字节;s:1字节,字符型数组存字符串
#b 占一个字节
struct模块使用
struct模块可以按照指定格式将Python数据转换为字符串,该字符串为字节流
struct模块中最重要的三个函数是pack(), unpack(), calcsize()
# 按照给定的格式(fmt),把数据封装成字符串(实际上是类似于c结构体的字节流)pack(fmt, v1, v2, ...) 将v1,v2等参数的值进行一层包装,包装的方法由fmt指定。被包装的参数必须严格符合fmt。最后返回一个包装后的字符串。
# 按照给定的格式(fmt)解析字节流string,返回解析出来的元组unpack(fmt, string)
# 计算给定的格式(fmt)占用多少字节的内存calcsize(fmt)
struct.pack(“!H8sb5sb”,1,“test.jpg”,0,“octet”,0)
struct.pack("!HH",4,p_num)
cmdTuple = struct.unpack("!HH", recvData[:4])
TFTP客户端编程(下载)
import struct
from socket import *
filename = 'vvue.jpg'
server_ip = '192.168.0.5'
send_data = struct.pack('!H%dsb5sb' % len(filename), 1, filename.encode(), 0, 'octet'.encode(), 0)
s = socket(AF_INET, SOCK_DGRAM)
s.sendto(send_data, (server_ip, 69)) # 第一次发送, 连接服务器69端口
f = open(filename, 'ab') #a:以追加模式打开(必要时可以创建)append;b:表示二进制
while True:
recv_data = s.recvfrom(1024) # 接收数据
print(recv_data)
caozuoma, ack_num = struct.unpack('!HH', recv_data[0][:4]) # 获取数据块编号
rand_port = recv_data[1][1] # 获取服务器的随机端口
if int(caozuoma) == 5:
print('文件不存在...')
break
print("操作码:%d,ACK:%d,服务器随机端口:%d,数据长度:%d"%(caozuoma, ack_num, rand_port, len(recv_data[0])))
f.write(recv_data[0][4:])#将数据写入
if len(recv_data[0]) < 516:
f.close()
break
ack_data = struct.pack("!HH", 4, ack_num)
s.sendto(ack_data, (server_ip, rand_port)) # 回复ACK确认包
TFTP客户端编程(上传)
import struct
from socket import *
filename = "1.jpg"
data = struct.pack(f"!H{len(filename.encode('gb2312'))}sb5sb",2,filename.encode("gb2312"),0,b"octet",0)
s = socket(type=SOCK_DGRAM)
s.sendto(data,("127.0.0.1",69))
f = open(filename,"rb")
while True:
ret = s.recvfrom(1024)
addr = ret[1]
data1,data2 = struct.unpack("!HH",ret[0][:4])
if data1 == 4:
data = f.read(512)
dabao = struct.pack(f"!HH{len(data)}s",3,data2+1,data)
s.sendto(dabao,addr)
if len(data) < 512:
break
UDP广播
发送端
import socket
dest = ('<broadcast>',7788)#<broadcast>自动识别当前网络的广播地址
#创建udp套接字
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
#对这个需要发送广播数据的套接字进行修改设置,否则不能发送广播数据
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) #允许s发送广播数据
#setsocketopt 设置套接字选项
#以广播形式发送数据到本网络的所有电脑中
s.sendto(b'Hi',dest)
print("等待回复")
while True:
(buf, address) = s.recvfrom(2048)
print(address,buf.decode('GB2312'))
接收端
# udp广播案例
from socket import *
s=socket(AF_INET,SOCK_DGRAM)
# 设置套接字
s.setsockopt(SOL_SOCKET,SO_BROADCAST,1)
# 选择一个接收地址
s.bind(('0.0.0.0',7788))
while True:
try:
msg,addr=s.recvfrom(1024)
print('from %s bg %s'% (addr,msg.decode()))
s.sendto(b'ku',addr)
except KeyboardInterrupt:
break
except Exception as e:
print(e)
s.close()
TCP
TCP:传输控制协议(使用情况多于udp)
稳定:保证数据一定能收到
相对UDP会慢一点 web服务器一般都使用TCP(银行转账,稳定比快要重要)
TCP通信模型:
在通信之前,必须先等待建立链接
TCP的三次握手
TCP四次挥手
第一次挥手:主动关闭方调用close,会发送一个长度为0的数据包以及FIN(结束标志)用来关闭主动方到被动关闭方的数据传送,这表示Client没有数据要发送给Server了,但是,此时主动关闭方还可以接受数据
第二次挥手:被动关闭方收到FIN包后,发送一个ACK给对方,确认序号为收到序号+1
第三次挥手:被动关闭方发送一个FIN,用来关闭被动关闭方到主动关闭方的数据传送,也就是告诉主动关闭方,我的数据也发送完了,不会再给你发数据了
第四次挥手:主动关闭方收到FIN后,发送一个ACK给被动关闭方,确认序号为收到序号+1,至此,完成四次挥手
常见问题
【问题1】为什么连接的时候是三次握手,关闭的时候却是四次握手?
答:因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
【问题2】什么是TCP半连接?
在三次握手过程中,服务器端发送SYN、ACK后,收到客户端的ACK之前的TCP连接称为半连接,此时服务器端处于SYN_RCVD状态,当收到ACK之后,服务器端进入Established状态。
【问题3】为什么不能用两次握手进行连接?
答:3次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认。
现在把三次握手改成仅需要两次握手,死锁是可能发生的。作为例子,考虑计算机S和C之间的通信,假定C给S发送一个连接请求分组,S收到了这个分组,并发 送了确认应答分组。按照两次握手的协定,S认为连接已经成功地建立了,可以开始发送数据分组。可是,C在S的应答分组在传输中被丢失的情况下,将不知道S 是否已准备好,不知道S建立什么样的序列号,C甚至怀疑S是否收到自己的连接请求分组。在这种情况下,C认为连接还未建立成功,将忽略S发来的任何数据分 组,只等待连接确认应答分组。而S在发出的分组超时后,重复发送同样的分组。这样就形成了死锁。
【问题4】如果已经建立了连接,但是客户端突然出现故障了怎么办?
TCP还设有一个计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。
【问题5】为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?
答:虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。在Client发送出最后的ACK回复,但该ACK可能丢失。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK。Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。所谓的2MSL是两倍的MSL(Maximum Segment Lifetime)。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。
TCP服务器
长连接:
三次握手四次挥手之间分多次传递完所有数据(优酷看视频、在线游戏),长时间占用某个套接字
短连接:
三次握手四次挥手之间传递少部分数据,多次握手挥手才传递完所有数据(浏览器),短时间占用
tcp服务器流程如下:
- socket创建⼀个套接字
- bind绑定ip和port
- listen设置最大连接数,收到连接请求后,这些请求需要排队,如果队列满,就拒绝请求
- accept等待客户端的链接、接收连接请求
- recv/send接收发送数据
简易TCP服务器代码
from socket import *
tcpSerSocket = socket(AF_INET, SOCK_STREAM)
address = ('', 7788)
tcpSerSocket.bind(address)
tcpSerSocket.listen(5)#设置最大连接数
newSocket, clientAddr = tcpSerSocket.accept()
# 如果有新的客户端来链接服务器, 那么就产⽣⼀个新的套接字
# newSocket⽤来为这个客户端服务
# tcpSerSocket就可以省下来等待其他新客户端的链接
# 接收对⽅发送过来的数据, 最⼤接收1024个字节
recvData = newSocket.recv(1024) #接收tcp数据
# 发送⼀些数据到客户端
newSocket.send(b"thank you !") #发送tcp数据
# 关闭为这个客户端服务的套接字, 只要关闭了, 就意味着为不能再为这个客户端服务
newSocket.close()
# 关闭监听套接字, 只要这个套接字关闭了, 就意味着整个程序不能再接收任何新的客户端的连接
tcpSerSocket.close()
TCP客户端
from socket import *
clientSocket = socket(AF_INET, SOCK_STREAM)
serAddr = ('192.168.0.5', 7788)
#链接服务器
clientSocket.connect(serAddr)
clientSocket.send(b"hello")
recvData = clientSocket.recv(1024)
print("接收到的数据为:", recvData)
clientSocket.close()
TCP服务器(单进程)
每次只能服务一个客户端
from socket import *
serSocket = socket(AF_INET, SOCK_STREAM)
localAddr = ('',7788)
serSocket.bind(localAddr)
serSocket.listen(5)
while True:
print("主进程等待新客户端")
newSocket,destAddr = serSocket.accept()
print("主进程接下来负责处理",str(destAddr))
try:
while True:
recvData = newSocket.recv(1024)
if len(recvData)>0: #如果收到的客户端数据长度为0,代表客户端已经调用close()下线
print("接收到", str(destAddr),recvData)
else:
print("%s-客户端已关闭" %str(destAddr))
break
finally:
newSocket.close()
serSocket.close()
TCP并发服务器(多进程)
serSocket.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
重新设置套接字选项,重复使用绑定的信息
当有一个有相同本地地址和端口的socket1处于TIME_WAIT状态时,而你启动的程序的socket2要占用该地址和端口,你的程序就要用到SO_REUSEADDR选项。
from socket import *
from multiprocessing import *
from time import sleep
# 处理客户端的请求并为其服务
def dealWithClient(newSocket,destAddr):
while True:
recvData = newSocket.recv(1024)
if len(recvData)>0:
print('recv[%s]:%s'%(str(destAddr), recvData))
else:
print('[%s]客户端已经关闭'%str(destAddr))
break
newSocket.close()
def main():
serSocket = socket(AF_INET, SOCK_STREAM)
serSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR , 1)
localAddr = ('', 7788)
serSocket.bind(localAddr)
serSocket.listen(5)
try:
while True:
print('-----主进程,等待新客户端的到来------')
newSocket,destAddr = serSocket.accept()
print('-----主进程,接下来创建⼀个新的进程负责数据处理')
client = Process(target=dealWithClient, args=(newSocket,destAddr))
client.start()
#因为已经向⼦进程中copy了⼀份(引⽤) ,并且⽗进程中这个套接字也没有用处了
#所以关闭
newSocket.close()
finally:
#当为所有的客户端服务完之后再进⾏关闭,表示不再接收新的客户端的链接
serSocket.close()
if __name__ == '__main__':
main()
TCP服务器(多线程)
耗费的资源比多进程小一些
from socket import *
from threading import Thread
# 处理客户端的请求并执⾏事情
def dealWithClient(newSocket,destAddr):
while True:
recvData = newSocket.recv(1024)
if len(recvData)>0:
print('recv[%s]:%s'%(str(destAddr), recvData))
else:
print('[%s]客户端已经关闭'%str(destAddr))
break
newSocket.close()
def main():
serSocket = socket(AF_INET, SOCK_STREAM)
serSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR , 1)
localAddr = ('', 7788)
serSocket.bind(localAddr)
serSocket.listen(5)
try:
while True:
print('-----主进程, , 等待新客户端的到来------')
newSocket,destAddr = serSocket.accept()
print('主进程接下来创建⼀个新的线程负责处理 ', str(destAddr))
client = Thread(target=dealWithClient, args=(newSocket,destAddr))
client.start()
#因为线程中共享这个套接字, 如果关闭了会导致这个套接字不可⽤,
#但是此时在线程中这个套接字可能还在收数据, 因此不能关闭
finally:
serSocket.close()
if __name__ == '__main__':
main()
socketsever
可以使用socketserver来创建socket用来简化并发服务器
socketserver可以实现和多个客户端通信(实现并发处理多个客户端请求的Socket服务端)
它是在socket的基础上进行了一层封装,也就是说底层还是调用的socket
服务器接受客户端连接请求——》实例化一个请求处理程序——》根据服务器类和请求处理程序类,调用处理方法。
例如:
基本请求程序类(BaseRequestHandler)调用方法 handle 。此方法通过属性 self.request 来访问客户端套接字
#创建socketserver的基本步骤:
#首先import socketserver
#创建一个请求处理类,继承 BaseRequestHandler 并且重写父类中的 handle()
#在handle()中处理和客户端所有的交互,建立链接时会自动执行handle方法
socketserver.TCPServer.allow_reuse_address = True # 允许地址(端口)重用
#实例化 TCPServer对象 ,将服务器IP/端口号和请求处理类传给 TCPServer
server = socketserver.ThreadingTCPServer(ip_port,MyServer)
#对socketserver.ThreadingTCPServer 类实例化对象,将ip地址,端口号以及自己定义的类名传入,并返回一个对象
#多线程:ThreadingTCPServer
#多进程:ForkingTCPServer -only in Linux
#对象执行serve_forever方法,开启服务端(handle_request()只处理一个请求)
server.serve_forever() #处理多个请求,永远执行
socketsever服务端
import socketserver
import time
# 自定义类来实现通信循环
class MyServer(socketserver.BaseRequestHandler):
# 必须写入handle方法,建立链接时会自动执行handle方法
def handle(self):
while True:
data = self.request.recv(1024)
# handle 方法通过属性 self.request 来访问客户端套接字
print('->client:', data)
self.request.send(data.upper())
time.sleep(2)
socketserver.TCPServer.allow_reuse_address = True
server = socketserver.ThreadingTCPServer(('', 8080), MyServer)
server.serve_forever()
socketsever客户端
import socket
import time
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('192.168.0.5', 8080))
while True:
time.sleep(2)
client.send('hello'.encode('utf-8'))
data = client.recv(1024)
print(data)
subprocess远程执行命令
Python可以使用subprocess下的Popen类中的封装的方法来执行命令
构造方法 popen() 创建popen类的实例化对象
obj = Subprocess.Popen(data,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
data 命令内容
shell = True 命令解释器,相当于调用cmd 执行指定的命令
stdout 正确结果丢到管道中
stderr 错了丢到另一个管道中
PIPE 将结果转移到当前进程
stdout.read() 可以获取命令执行的结果
指定结果后会将执行结果封装到指定的对象中
然后通过对象.stdout.read()获取执行命令的结果,如果不定义stdout会将结果进行标准输出
import subprocess
obj = subprocess.Popen('net user',shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
print(obj.stdout.read().decode('gbk')) # 正确命令
print(obj.stderr.read().decode('gbk')) #错误命令
subprocess服务端
import socket
import subprocess
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.bind(('127.0.0.1', 8080))
phone.listen(5)
while 1:
conn, client_addr = phone.accept()
print(client_addr)
while 1:
try:
cmd = conn.recv(1024)
ret = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
correct_msg = ret.stdout.read()
error_msg = ret.stderr.read()
conn.send(correct_msg + error_msg)
except ConnectionResetError:
print("服务结束")
break
conn.close()
phone.close()
subprocess客户端
import socket
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.connect(('127.0.0.1', 8080))
while 1:
cmd = input('>>>')
if cmd == "zaijian":
break
phone.send(cmd.encode('utf-8'))
from_server_data = phone.recv(1024)
print(from_server_data.decode('gbk'))
phone.close()
解决沾包问题
TCP协议是面向流的协议,容易出现粘包问题
不管是recv还是send都不是直接接收对方的数据(不是一个send一定对应一个recv),而是操作自己的缓存区(产生沾包的根本原因)
例如基于tcp的套接字客户端往服务端上传数据,发送时数据内容是按照一段一段的字节流发送的,
在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,
不能一次提取任意字节的数据,这一点和TCP是很不同的
只有TCP有粘包现象,UDP永远不会粘包
粘包不一定会发生
如果发生了:1.可能是在客户端已经粘了
2.客户端没有粘,可能是在服务端粘了
客户端粘包
发送端需要等缓冲区满才发送出去,造成粘包
(发送数据时间间隔很短,数据量很小,TCP优化算法会当做一个包发出去,产生粘包)
服务端粘包
接收方没能及时接收缓冲区的包(或没有接收完),造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,
服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
客户端沾包
server端
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 9904))
server.listen(5)
conn, addr = server.accept()
res1 = conn.recv(1024)
print('第一次', res1)
res2 = conn.recv(1024)
print('第二次', res2)
client端
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 9904))
client.send('hello'.encode('utf-8'))
client.send('world'.encode('utf-8'))
打印结果:
第一次 b'helloworld'
第二次 b''
不合适的解决方案:
send时加上时间间隔,虽然可以解决,但是会影响效率。不可取。
服务端沾包
server端
import socket
server =socket.socket(socket.AF_INET ,socket.SOCK_STREAM)
server.bind(('127.0.0.1' ,9904))
server.listen(5)
conn,addr =server.accept()
res1 = conn.recv(2) # 第一没有接收完整
print('第一次' ,res1)
res2 =conn.recv(10)# 第二次会接收旧数据,再收取新的
print('第二次', res2)
client端
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 9904))
client.send('hello'.encode('utf-8'))
client.send('world'.encode('utf-8'))
打印结果:
第一次 b'he'
第二次 b'lloworld'
不合适的解决方案:
提升recv的接收数量的上限。 不可取
因为没有上限。8G数据,一次接收8G撑爆内存了
如何解决
问题的根源在于:接收端不知道发送端将要传送的字节流的长度
所以解决粘包的方法就是发送端在发送数据前,发一个头文件包,告诉发送的字节流总大小,然后接收端来一个死循环接收完所有数据
使用struct模块可以用于将Python的值根据格式符,转换为固定长度的字符串(byte类型)
struct模块中最重要的三个函数是pack(), unpack(), calcsize()
pack(fmt, v1, v2, ...) 按照给定的格式(fmt),把数据封装成字符串(实际上是类似于c结构体的字节流)
unpack(fmt, string) 按照给定的格式(fmt)解析字节流string,返回解析出来的tuple
calcsize(fmt) 计算给定的格式(fmt)占用多少字节的内存
server端
import socket,struct,json
import subprocess
gd_server=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
gd_server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #在bind前加
gd_server.bind(('127.0.0.1',9991))
gd_server.listen(5)
while True:
conn, addr = gd_server.accept()
while True:
cmd = conn.recv(1024)
if not cmd:
break
print('cmd: %s' % cmd)
# 命令操作
res=subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
err=res.stderr.read()
print(err)
if err:
back_msg=err
else:
back_msg=res.stdout.read()
headers = {'data_size': len(back_msg)}
head_json = json.dumps(headers) # 字典转换成字符串
head_json_bytes = bytes(head_json, encoding='utf-8')
conn.send(struct.pack('i', len(head_json_bytes))) # 先发报头长度
conn.send(head_json_bytes) # 再发报头
conn.sendall(back_msg) # 再发正式内容
conn.close()
client端
import socket, json, struct
ip_port = ('127.0.0.1',9991)
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(ip_port)
while True:
cmd=input('>>: ').strip()
if not cmd :
continue
client.send(bytes(cmd,encoding='utf-8'))
head = client.recv(4) # 先收4个bytes,这里4个bytes里包含了报头的长度
head_json_len = struct.unpack('i', head)[0] # 解出报头的长度
head_json = json.loads(client.recv(head_json_len).decode('utf-8')) # 拿到报头
data_len = head_json['data_size'] # 取出报头内包含的信息
# 开始接收数据
recv_size = 0
recv_data = b''
while recv_size < data_len:
recv_data += client.recv(1024)
recv_size += len(recv_data)
print(recv_data.decode('gbk'))
# 命令结果的编码是以当前所在的系统为准的,如果是windows,那么res.stdout.read()读出的就是GBK编码的,mac 默认 utf-8 编码