目录

依据TFTP协议的服务端和客户端

依据TFTP协议的服务端和客户端

今天写了下依据TFTP协议的服务端和客户端,端口号设置为2048。

实现功能:

  • 可让服务端客户端搭配使用实现上传下载功能
  • 可在服务端记录log日志
  • 客户端可单独与Windows上的TFTP程序完成文件传输

待完善:

  • 服务端无退出功能,不退出的话端口不能释放
  • 代码均尚未捕获异常
  • 服务端文件列表未实时更新
  • 服务端log日志未设保护
  • 未按照MD5校验值来判断文件

服务端代码

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
from socket import *
import struct
import os
import time

def send_file():
    global log
    '发送文件'
    if file_name in file_list: # 检测服务端是否存在客户端要下载的文件
        f = open('./%s'%file_name, 'rb')
        i = 1
        times = 0
        while True:
            content = f.read(512)
            con_len = len(content)
            pack_content = struct.pack('!HH%ds'%con_len, 3, i, content)
            tftp_socket.sendto(pack_content, addr)
            echo_msg = tftp_socket.recvfrom(1024) # 接收客户端返回值
            echo_op = struct.unpack('!HH', echo_msg[0][:4]) # 读取客户端ACK
            if echo_op == (4, i):
                times = 0 # 重置客户端无响应次数
                i += 1
                if i == 65536:
                    i = 0 # 重置块编号
            elif echo_op == (4, i-1):
                times += 1 # 客户端无响应次数统计
                f.seek(1, -512) # 调整文件读取位置
                if times > 6:
                    log = open('log.txt', 'a')
                    log.write('<time : %s>\t<ip : %s>\t<op : 请求下载文件%s,中途断开链接,下载失败!>\n'%(time.ctime(), addr[0], file_name))
                    log.close()
                    break
            if con_len < 512: # 数据长度判断
                log = open('log.txt', 'a')
                log.write('<time : %s>\t<ip : %s>\t<op : 请求下载文件%s,下载成功!>\n'%(time.ctime(), addr[0], file_name))
                log.close()
                break
    else:
        error_info = struct.pack('!HH21sb', 5, 1, 'cannot find this file'.encode('utf-8'), 0) # 返回文件未找到的错误信息
        tftp_socket.sendto(error_info, addr)
        log = open('log.txt', 'a')
        log.write('<time : %s>\t<ip : %s>\t<op : 请求下载文件%s,服务端无此文件,下载失败!>\n'%(time.ctime(), addr[0], file_name))
        log.close()

def recv_file():
    '接收文件'
    if file_name in file_list:
        error_info = struct.pack('!HH19sb', 5, 2, 'file already exists'.encode('utf-8'), 0) # 返回文件未找到的错误信息
        tftp_socket.sendto(error_info, addr)
        log = open('log.txt', 'a')
        log.write('<time : %s>\t<ip : %s>\t<op : 请求上传文件%s,服务端已有此文件,上传失败!>\n'%(time.ctime(), addr[0], file_name))
        log.close()
    else:
        ack_info = struct.pack('!HH', 4, 0)
        tftp_socket.sendto(ack_info, addr)
        recv_data = tftp_socket.recvfrom(1024) # 接收服务端信息
        f = open('./%s'%file_name, 'wb')
        i = 0
        while True:
            recv_msg = recv_data[0][4:] # 读取接收信息
            recv_addr = recv_data[1] # 读取地址
            recv_id = struct.unpack('!H', recv_data[0][2:4])[0] #读取块编号

            tftp_socket.sendto(struct.pack('!HH', 4, recv_id), recv_addr) #发送ACK

            i += 1
            if i == 65536:
                i = 0

            if i == recv_id: # 防止丢包的时候也写入了文件
                f.write(recv_msg)

            if len(recv_data[0]) < 516:
                log = open('log.txt', 'a')
                log.write('<time : %s>\t<ip : %s>\t<op : 请求上传文件%s,上传成功!>\n'%(time.ctime(), addr[0], file_name))
                log.close()
                break
            recv_data = tftp_socket.recvfrom(1024) # 接收服务端信息

def main():
    global tftp_socket
    global file_name
    global file_list
    global addr
    global log

    tftp_socket = socket(AF_INET, SOCK_DGRAM)
    tftp_socket.bind(('', 2048))
    os.chdir('./server_files')

    while True:
        file_list = os.listdir('.') # 文件列表需要不断更新
        recv_msg = tftp_socket.recvfrom(1024) # 从客户端获取信息
        msg_len = len(recv_msg[0])-9 # 获取文件名长度
        file_name = struct.unpack('%ds'%msg_len, recv_msg[0][2:-7])[0].decode('utf-8') # 解码出文件名
        addr = recv_msg[1] # 获取客户端地址信息
        op = struct.unpack('!H', recv_msg[0][:2])[0] # 获取客户端请求操作码

        if op == 1: # 客户端请求下载
            send_file()

        elif op == 2: # 客户端请求上传
            recv_file()

    log.close()

if __name__ == '__main__':
    tftp_socket = None
    file_name = ''
    file_list = []
    addr = ()
    log = None
    main()

客户端代码

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
from socket import *
import struct
import os
import time

def send_requ(io):
    '发送请求'
    name_len = len(file_name)
    send_msg = struct.pack('!H%dsb5sb'%name_len, io, file_name.encode('utf-8'), 0, 'octet'.encode('utf-8'), 0 )
    tftp_socket.sendto(send_msg, (ip_addr, port_addr)) 

def recv_file():
    '接收文件' 
    recv_data = tftp_socket.recvfrom(1024) # 接收服务端信息
    if struct.unpack('!H', recv_data[0][:2])[0] == 5: # 读取操作码
        print('-----服务端无文件 %s!-----'%file_name) 

    elif struct.unpack('!H', recv_data[0][:2])[0] == 3: # 读取操作码
        f = open('./%s'%file_name, 'wb')
        i = 0
        while True:
            recv_msg = recv_data[0][4:] # 读取接收信息
            recv_addr = recv_data[1] # 读取地址
            recv_id = struct.unpack('!H', recv_data[0][2:4])[0] #读取块编号
            print(recv_id, end = ' ')

            tftp_socket.sendto(struct.pack('!HH', 4, recv_id), recv_addr) #发送ACK

            i += 1
            if i == 65536:
                i = 0

            if i == recv_id: # 防止丢包的时候也写入了文件
                f.write(recv_msg)

            if len(recv_data[0]) < 516:
                print('\n-----文件 %s下载完成!-----'%file_name)
                break
            recv_data = tftp_socket.recvfrom(1024) # 接收服务端信息

        f.close()

def send_file():
    '发送文件'
    echo_data = tftp_socket.recvfrom(1024) # 接收服务端ACK
    if struct.unpack('!H', echo_data[0][:2])[0] == 5: # 读取操作码
        print('服务端已有文件 %s,请勿重复上传!'%file_name)

    elif struct.unpack('!HH', echo_data[0]) == (4, 0): # 读取操作码
        f = open('./%s'%file_name, 'rb')
        i = 1
        times = 0
        while True:
            send_msg = f.read(512) # 每次发送512字节
            msg_len = len(send_msg) 
            send_addr = echo_data[1] # 读取地址
            pack_data = struct.pack('!HH%ds'%msg_len, 3, i, send_msg) # 打包发送信息
            tftp_socket.sendto(pack_data, send_addr)
            echo_data = tftp_socket.recvfrom(1024) # 接收客户端返回值
            echo_op = struct.unpack('!HH', echo_data[0][:4]) # 读取客户端ACK
            if echo_op == (4, i):
                times = 0 # 重置服务端无响应次数
                print(i,end = ' ')
                i += 1
                if i == 65536:
                    i = 0 # 重置块编号
            elif echo_op == (4, i-1):
                times += 1 # 客户端无响应次数统计
                f.seek(1, -512) # 调整文件读取位置
                if times > 6:
                    print('-----服务端无响应,传输失败!-----')
                    break
            if msg_len < 512: # 数据长度判断
                print('\n-----文件 %s上传完成-----'%file_name)
                break
        f.close()

def main():
    '主函数'
    global tftp_socket
    global file_name
    global ip_addr
    global port_addr

    print('-'*50)
    print('欢迎使用下载上传工具 by Shelming.Song')
    ip_addr = input('请输入服务器IP地址:')
    port_addr = int(input('请输入服务器端口:'))
    print('-'*50)

    tftp_socket = socket(AF_INET, SOCK_DGRAM)
    os.chdir('./client_files')

    while True:
        file_list = os.listdir('.') # 文件列表需要不断更新
        op = input('请选择您要进行的操作:\n1.下载\n2.上传\n3.退出\n')

        if op == '1': # 下载
            file_name = input('请输入您要下载的文件名(含后缀名):')
            if file_name in file_list:
                confirm = input('本地已有文件 %s,是否重新下载? y/n:'%file_name)
                if confirm.lower() == 'y':
                    send_requ(1)
                    recv_file()
                else:
                    continue
            else:
                send_requ(1)
                recv_file()

        elif op == '2': # 上传
            file_name = input('请输入您要上传的文件名(含后缀名):')
            if file_name not in file_list:
                print('本地无文件 %s!:'%file_name)
            else:
                send_requ(2)
                send_file()

        elif op == '3': # 退出
            print('程序即将退出,欢迎再次使用!')
            break

        else:
            print('您的输入有误,请重新输入!')
            break

if __name__ == '__main__':
    tftp_socket = None
    file_name = ''
    ip_addr = ''
    port_addr = 0
    main()  

博客更新地址