前言

一、FTP具体实现代码

二、相关函数及其说明

1.建立连接函数

2.线程处理函数

3.文件下载函数

4.文件上传函数

5.哈希函数

6.文件目录函数

三.原理概述

3.1 FTP原理概述

3.2  FTP工作流程

3.3 TCP三次握手四次挥手

3.4 socket通信过程

四.重要问题及其解决方式

4.1流式协议

4.2粘包问题

4.3简单处理方法

4.4复杂处理方法


前言

  由于最近在做计算机网络课程设计,了解了一些基本的网络通信协议以及一些网络通信基本原理之后,由于又恰好在学习python,所以不妨产生了用python实现模拟FTP登录过程,类似于相同的ssh协议(远程登陆协议)与FTP实现过程如出一辙。最近也有很长时间没有继续学习了,仅以此篇作为记录。


一、FTP具体实现代码

服务端代码如下

import socket
import struct
import os
import json
from threading import Thread
import hashlib

#获取当前文件所在文件路径
def get_cur_dir():
    return os.getcwd()
#获取当前文件夹下所有文件
def get_cur_all(cur_dir):
    return os.listdir(cur_dir)

def get_hash(filename):
    md5 = hashlib.md5()
    with open(filename, 'rb') as f:
        for line in f:
            md5.update(line)
    md5_str=md5.hexdigest()
    return md5_str

def sendmsg(filename,conn,base_path):
    # 1、制作报头
    header_dict = {'filename': filename,
                   'md5': get_hash(filename),
                   'total_size': os.path.getsize('%s/%s' % (base_path, filename))
                   }
    head_json = json.dumps(header_dict)
    head_bytes = head_json.encode('utf-8')
    # 2、发送报文的长度
    conn.send(struct.pack('i', len(head_bytes)))
    # 发送报头
    conn.send(head_bytes)
    # 发送数据
    with open(filename, 'rb') as f:
        for line in f:
            conn.send(line)
        print('文件发送完毕!\n')

def download_to_file(phone,path):
    # 先收报头的长度
    obj = phone.recv(4)
    head_size = struct.unpack('i', obj)[0]
    # 从报头中解析信息
    header_bytes = phone.recv(head_size)
    head_json = header_bytes.decode('utf-8')
    head_dict = json.loads(head_json)
    print(head_dict)
    filename = head_dict['filename']
    total_size = head_dict['total_size']
    rec_hash = head_dict['md5']
    with open('%s/%s' % (path, filename), 'wb') as f:
        recv_size = 0
        while recv_size < total_size:
            line = phone.recv(1024)
            f.write(line)
            recv_size += len(line)
            print('该文件大小为  ', total_size, '\n已经写入', recv_size)
        print('文件上传完毕!\n')
    sa_md5 = get_hash(filename)
    if rec_hash == sa_md5:
        print('文件校验成功,文件无误!')

#发送当前文件夹下的目录列表
def send_sever_list(conn):
    rec_msg_list = get_cur_all(get_cur_dir())
    rec_msg_str = ' '.join(rec_msg_list)
    rec_msg_bytes = rec_msg_str.encode('utf-8')
    conn.send(rec_msg_bytes)
#发送当前文件所在路径
def send_sever_path(conn):
    cur_dir = get_cur_dir()
    cur_dir_bytes = cur_dir.encode('utf-8')
    conn.send(cur_dir_bytes)

def action_thread(conn):
    base_path = r'D:\computercode\文件传输\sever'
    while True:
        # 1、接受命令
        res = conn.recv(1024)
        if not res: break
        # 解析命令
        cmds = res.decode('utf-8').split()
        # print(cmds)
        if cmds[0] == 'get':
            filename = cmds[1]  # 文件名称,用于制作报头以及确定文件大小
            sendmsg(filename, conn,base_path)
        elif cmds[0] == 'post':
            download_to_file(conn,base_path)
        elif cmds[0] == 'exit':
            print('客户端已经断开连接!')
            break
        elif cmds[0]=='dirs':
            send_sever_list(conn)
        elif cmds[0]=='paths':
            send_sever_path(conn)
        elif cmds[0]=='chdir':
            os.chdir(cmds[1])
            base_path=os.getcwd()
            send_sever_list(conn)

def run():
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    phone.bind(('127.0.0.1',8080))
    phone.listen(5)
    print('服务器启动成功,正在等待客户连接....')
    while True:
        conn , client_adrr=phone.accept()
        print('一个客户端连接成功,其IP地址为',client_adrr[0])

        kh=Thread(target=action_thread,args=(conn,))
        kh.start()
    conn.close()
    phone.close()

if __name__=='__main__':
    run()

客户端代码如下

import json
import socket
import struct
import json
import os
import hashlib
download_path=r'D:\computercode\文件传输\user'

def get_cur_dir():
    return os.getcwd()

def get_cur_all(cur_dir):
    return os.listdir(cur_dir)

def show_info_curpath_sever(phone):
    print('正在等待服务端发送信息....')
    sv_cur_path_bytes = phone.recv(1024)
    sv_cur_path_str = sv_cur_path_bytes.decode('utf-8')
    print(sv_cur_path_str)

def show_info_curdir_sever(phone):
    print('正在等待服务器发送信息.....')
    sv_cur_all_bytes = phone.recv(1024)
    sv_cur_all_str = sv_cur_all_bytes.decode('utf-8')
    sv_cur_all_list = sv_cur_all_str.split()
    for item in sv_cur_all_list:
        print(item)

def get_hash(filename):
    md5 = hashlib.md5()
    with open(filename, 'rb') as f:
        for line in f:
            md5.update(line)
    md5_str=md5.hexdigest()
    return md5_str

def download_to_file(phone):
    # 先收报头的长度
    obj = phone.recv(4)
    head_size = struct.unpack('i', obj)[0]
    # 从报头中解析信息
    header_bytes = phone.recv(head_size)
    head_json = header_bytes.decode('utf-8')
    head_dict = json.loads(head_json)
    print(head_dict)
    filename = head_dict['filename']
    total_size = head_dict['total_size']
    rec_hash=head_dict['md5']
    with open('%s/%s' % (download_path, filename), 'wb') as f:
        recv_size = 0
        while recv_size < total_size:
            line = phone.recv(1024)
            f.write(line)
            recv_size += len(line)
            print('该文件大小为  ', total_size, '\n已经写入', recv_size)
        print('文件写入完毕!')
    sa_md5=get_hash(filename)
    if rec_hash==sa_md5:
        print('文件校验成功,文件无误!')

def sendmsg(filename,conn):
    # 1、制作报头
    header_dict = {'filename': filename,
                   'md5': get_hash(filename),
                   'total_size': os.path.getsize('%s/%s' % (download_path, filename))
                   }
    head_json = json.dumps(header_dict)
    head_bytes = head_json.encode('utf-8')
    # 2、发送报文的长度
    conn.send(struct.pack('i', len(head_bytes)))
    # 发送报头
    conn.send(head_bytes)
    # 发送数据
    with open(filename, 'rb') as f:
        for line in f:
            conn.send(line)
        print('文件上传完毕!\n')


def run():
    phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)

    phone.connect(('127.0.0.1',8080))

    while True:
        #1、发送命令
        cmd = input('请输入命令>>:')
        if not cmd:continue
        cmds=cmd.split()
        phone.send(cmd.encode('utf-8'))
        #2、解析命令结果
        if cmds[0]=='get':
            download_to_file(phone)
        elif cmds[0]=='post':
            filename = cmds[1]  # 文件名称,用于制作报头以及确定文件大小
            sendmsg(filename,phone)
        elif cmds[0]=='path':
            print(get_cur_dir())
        elif cmds[0]=='dir':
            cur_all=get_cur_all(get_cur_dir())
            for item in cur_all:
                print(item)
        elif cmds[0]=='exit':
            print('程序退出')
            break
        elif cmds[0]=='dirs':
            show_info_curdir_sever(phone)
        elif cmds[0]=='paths':
            show_info_curpath_sever(phone)
        elif cmds[0]=='chdir':
            show_info_curdir_sever(phone)

    phone.close()

if __name__=='__main__':
    run()

二、相关函数及其说明

1.建立连接函数

def run():
    phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    phone.bind(('127.0.0.1',8080))
    phone.listen(5)
    print('服务器启动成功,正在等待客户连接....')
    while True:
        conn , client_adrr=phone.accept()
        print('一个客户端连接成功,其IP地址为',client_adrr[0])

        kh=Thread(target=action_thread,args=(conn,))
        kh.start()
    conn.close()
    phone.close()

2.线程处理函数

def action_thread(conn):
    base_path = r'D:\computercode\文件传输\sever'
    while True:
        # 1、接受命令
        res = conn.recv(1024)
        if not res: break
        # 解析命令
        cmds = res.decode('utf-8').split()
        # print(cmds)
        if cmds[0] == 'get':
            filename = cmds[1]  # 文件名称,用于制作报头以及确定文件大小
            sendmsg(filename, conn,base_path)
        elif cmds[0] == 'post':
            download_to_file(conn,base_path)
        elif cmds[0] == 'exit':
            print('客户端已经断开连接!')
            break
        elif cmds[0]=='dirs':
            send_sever_list(conn)
        elif cmds[0]=='paths':
            send_sever_path(conn)
        elif cmds[0]=='chdir':
            os.chdir(cmds[1])
            base_path=os.getcwd()
            send_sever_list(conn)

3.文件下载函数

def download_to_file(phone,path):
    # 先收报头的长度
    obj = phone.recv(4)
    head_size = struct.unpack('i', obj)[0]
    # 从报头中解析信息
    header_bytes = phone.recv(head_size)
    head_json = header_bytes.decode('utf-8')
    head_dict = json.loads(head_json)
    print(head_dict)
    filename = head_dict['filename']
    total_size = head_dict['total_size']
    rec_hash = head_dict['md5']
    with open('%s/%s' % (path, filename), 'wb') as f:
        recv_size = 0
        while recv_size < total_size:
            line = phone.recv(1024)
            f.write(line)
            recv_size += len(line)
            print('该文件大小为  ', total_size, '\n已经写入', recv_size)
        print('文件上传完毕!\n')
    sa_md5 = get_hash(filename)
    if rec_hash == sa_md5:
        print('文件校验成功,文件无误!')

4.文件上传函数

def sendmsg(filename,conn,base_path):
    # 1、制作报头
    header_dict = {'filename': filename,
                   'md5': get_hash(filename),
                   'total_size': os.path.getsize('%s/%s' % (base_path, filename))
                   }
    head_json = json.dumps(header_dict)
    head_bytes = head_json.encode('utf-8')
    # 2、发送报文的长度
    conn.send(struct.pack('i', len(head_bytes)))
    # 发送报头
    conn.send(head_bytes)
    # 发送数据
    with open(filename, 'rb') as f:
        for line in f:
            conn.send(line)
        print('文件发送完毕!\n')

5.哈希函数

def get_hash(filename):
    md5 = hashlib.md5()
    with open(filename, 'rb') as f:
        for line in f:
            md5.update(line)
    md5_str=md5.hexdigest()
    return md5_str

6.文件目录函数

#获取当前文件所在文件路径
def get_cur_dir():
    return os.getcwd()
#获取当前文件夹下所有文件
def get_cur_all(cur_dir):
    return os.listdir(cur_dir)
#发送当前文件夹下的目录列表
def send_sever_list(conn):
    rec_msg_list = get_cur_all(get_cur_dir())
    rec_msg_str = ' '.join(rec_msg_list)
    rec_msg_bytes = rec_msg_str.encode('utf-8')
    conn.send(rec_msg_bytes)
#发送当前文件所在路径
def send_sever_path(conn):
    cur_dir = get_cur_dir()
    cur_dir_bytes = cur_dir.encode('utf-8')
    conn.send(cur_dir_bytes)

三.原理概述

3.1 FTP原理概述

         文件传送协议FTP(File Transfer Protocol)是TCP/IP体系的一个重要协议,它采用Internet标准文件传输协议FTP的用户界面,向用户提供了一组用来管理计算机之间文件传输的应用程序。FTP是基于客户—服务器(C/S)模型而设计的,在客户端和FTP建立两个TCP连接。FTP 的独特的优势同时也是与其它客户服务器程序最大的不同点就在于它在两台通信的主机之间使用了两条 TCP 连接,一条是数据连接,用于数据传送;另一条是控制连接,用于传送控制信息(命令和响应),这种将命令和数据分开传送的思想大大提高了 FTP 的效率,而其它客户服务器应用程序一般只有一条 TCP 连接。图 1 给出了 FTP 的基本模型。客户有三个构件:用户接口、客户控制进程和客户数据传送进程。服务器有两个构件:服务器控制进程和服务器数据传送进程。在整个交互的 FTP 会话中,控制连接始终是处于连接状态的,数据连接则在每一次文件传送时先打开后关闭。

Python上传文件到hdfs python上传本地文件到ftp_网络协议

用户通过一个支持FTP协议的客户机程序,连接在远程主机FTP服务器程序。通过在客户端向服务器端发送FTP命令,服务器执行该命令,并将执行结果返回给客户端。由于控制连接的因素,客户端发送FTP命令,服务器都会有相应的应答。

3.2  FTP工作流程

       FTP进行文件传输的基本工作流程主要分为四个阶段,即建立连接阶段,身份认证阶段、命令交互阶段和断开连接阶段。

       建立连接阶段:该阶段是由FTP客户端通过TCP三次握手与FTP服务端进行建立连接。客户端向FTP服务器发出建立连接的请求,FTP服务器对请求进行应答。如果FTP服务器上的21端口是启用的,可以接受来自其他主机的请求,服务器给出应答220,即告诉客户端需要的FTP服务器已经准备好了。返回应答后,FTP服务端需要客户端进行身份认证,向客户端发送身份认证请求。

       身份认证阶段:客户端需要向FTP服务提供登录所需的用户名和密码。FTP服务器对客户端输入的用户名和密码都会给出相应的应答。如果验证正确,将成功登录FTP服务器,此时会进入FTP会话。

       命令交互阶段:在与FTP服务器会话中,用户可以执行FTP命令进行文件传输,如查看目录,切换目录,文件上传和下载等。客户端输入想要执行的FTP命令,服务器会给出应答。输入的命令正确,服务器会将命令的执行结果返回给客户端。执行结果返回完成后,服务器会继续等待客户端发出FTP命令。

       断开连接阶段:当客户端不再与FTP服务器进行文件传输时,需要断开连接。客户端向FTP服务器发送断开连接请求,FTP服务器收到相应的请求会给出相应的应答。

       FTP工作流程如图1.2.1所示

Python上传文件到hdfs python上传本地文件到ftp_Python上传文件到hdfs_02

3.3 TCP三次握手四次挥手

在实现简单FTP客户端的实现过程当中,基于SOCKET通信进行文件传输的底层协议是TCP传输协议,故有必要对TCP进行通信的基本流程进行详细说明。

TCP提供面向有连接的通信传输。面向有连接是指在数据通信开始之前先做好通信两端之间的准备工作。准备工作即TCP服务器西安创建传输控制快TCB,时刻准备接收客户进程的连接请求,此时服务器处于监听状态,服务器处于关闭状态。

第一次握手:客户端给服务器发送一个同步报文段SYN,并指定客户端的初始序列号ISN,此时客户端处于SYN_SENT状态。

第二次握手:服务器收到来自客户端的同步报文段SYN后,会以自己的SYN报文作为应答,并且也指定了自己的初始序列号ISN。同时会把客户端的SEP+1确认序列号ack的值,表示自己收到了客户端的同步报文段SYN,此时服务器处于SYN_RECD的状态。

第三次握手: 客户端收到来自服务器的同步报文段SYN之后,会发送一个确定报文段ACK,以服务器的SEP+1作为ack的值,表明已经收到来自服务器的同步报文段ACK。客户端会进入建立状态,服务器确认报文段之后,也会进入建立状态,此时双方已经建立起连接,可以正常的发送数据。

当数据传输完成后,需要断开连接,断开连接的过程就是四次挥手。

Python上传文件到hdfs python上传本地文件到ftp_Python上传文件到hdfs_03

第一次挥手;当客户端发起中断连接请求,即发送FIN报文。服务器接收到FIN报文后,向服务器表明,客户端已经没有数据要发送给服务端,但如果服务端数据还没有发送完成,不必着急关闭socket,可以继续发送数据。

第二次挥手,服务器先发送ACK,告诉客户端,关闭连接的请求已经收到,此时客户端处于FIN_WAIT状态,继续等待服务器发送FIN报文。

第三次挥手:当服务端的数据确认发送完毕后,向客户端发送FIN报文,告诉客户端,服务器的数据已经发送完毕,准备好关闭连接了

第四次握手:当客户端收到来自服务器的FIN报文后,会向服务器发送一个ACK然后进入TIME-WAIT状态,服务器收到ACK后,就可以断开连接,客户都安等待了2MSL后依然没有收到回复,证明服务端已经正常关闭,此时客户端也关闭连接了。

在进行FTP简单客户端实现的过程当中,以python语言为基础,调用socket包实现模拟FTP文件传输,所谓socket通常也称作“套接字”,用于描述IP地址和端口,是一个通信的句柄。应用进程通过“套接字”向网络发出请求或者应答网络请求。套接字是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。可以将套接字视作不同主机进程之间进行双向通信的断电,它构成了单个主机内及整个网络间的编程界面。

3.4 socket通信过程

Socket通信的连接过程可以分为三个步骤:服务器监听、客户端请求,连接确认。

服务器监听,是指服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态。

客户端请求,是指由客户端提出请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端的套接字的地址和端口号,然后向服务器发送请求。

连接确认,是指当服务器端套接字监听或者说接收到客户端套接字的连接请求,他就响应客户端套接字的请求,建议一个新的线程,把服务器套接字的描述发给客户端,一旦客户端确认了此描述,连接就成功建立。而服务器端套接字继续处于监听状态,继续接受其他客户端套接字的连接请求。

在进行具体实现时,首先利用socket包中的socket函数,将AF_INET以及SOCK_STREAM作为参数初始化信道,然后利用bind函数,将一个本地协议地址赋予一个套接字,bind函数绑定的IP地址必须属于其所在主机的网络接口之一,服务器在绑定端口后,利用listen()函数,将一个未连接的套接字装换位一个被动套接字,指示内核应接受指向该套接字的连接请求,调用listen函数将导致套接字从CLOSE状态转换为LISTEN状态,第二个参数规定了内核应为相应套接字排队的最大连接个数,即当客户端与服务器连接进行数据交互时,新的未连接的套接字处于等待连接状态的最大个数。在执行完listen后,服务器端TCP会将到达的数据进行排队,最大数量为相应已连接套接字的接收缓冲区大小,执行完listen,而没有执行accept,客户是可以成功建立连接的,只不过该连接被加入到已连接队列中,当调用accept时会被提取出来。Accept函数有TCP服务器调用,用于从已完成队列中列头返回下一个已完成连接,如果队列为空,则进程被投入睡眠,accept成功返回值是由内核自动生成一个全新的套接字,代表与返回客户的TCP连接,函数返回的第一个参数为监听套接字,第二个为客户端的套接字号。

Python上传文件到hdfs python上传本地文件到ftp_数据分析_04

四.重要问题及其解决方式

4.1流式协议

       TCP是一种流协议,这就意味着数据是以字节流的形式传递给接受者的,没有固定的报文或报文边界的概念。所谓流式协议,即协议的内容就是像流水一样的字节流,内容与内容之间没有明确的分界标志,需要我们人为的去给这些协议划分边界。

4.2粘包问题

       在发送数据时,我们假设主机A和主机B的应用程序之间有一条TCP连接,主机A上的应用程序向主机B发送一条报文。进一步假设主机A有两条报文要发送,调用两次send函数来发送,每条报文调用一次,在理想中的模型是报文1和报文2应该是作为两个单独的实体,在各自的分组中发送的。如下图所示

Python上传文件到hdfs python上传本地文件到ftp_Python上传文件到hdfs_05

       但是在实际的数据传输模型中很可能不会遵循这个模型。主机A上的程序调用send,实际上是将数据复制到主机A的TCP/IP协议栈中,就返回了。由TCP来决定需要立即发送多少数据,而做这个决定的过程实际上很复杂,取决于很多因素,比如发送窗口(当时主机B能够接收的数据量),拥塞窗口(对网络拥塞的估计),路径上的最大传输单元(沿着主机A和B之间的网络路径一次可以传输的最大数据量),以及连接的输出队列中有多少数据。而在这个过程当中,TCP协议使用了Nagle算法。Nagle算法主要是避免发送小的数据包,要求TCP连接上最多只能有一个未被确认的小分组,在该分组的确认到达之前不能发送其他小分组。TCP收集这些少量的小分组,并在确认到来时以一个分组的方式发送出去。这样做的好处是减少了网络拥堵,但是在一些交互性很强的应用程序当中这是不允许的,因为实时性却降低了。同时,也因为使用了Nagle算法,再加上流式协议对报文界限并不明确的条件,使得在实际传输过程中,两个独立的实体最终的结果可能会出现以下情况:

(A)M1,M2作为独立的实体传输

(B)M1,M2作为一个分组传输

(C)M1整体,M2的前半部分作为一个分组传播,M2的后半部分作为一个分组进行传播

(D)M1的前半部分作为一个分组进行传播,M1的后半部分和M2整体作为一个分组传播

Python上传文件到hdfs python上传本地文件到ftp_tcp/ip_06

       B,C,D这三种情况就都出现了粘包问题,其根源是上文介绍中TCP协议是流式数据格式。解决问题的思路是想办法从收到的数据中即将包与包的边界区分出来。

4.3简单处理方法

接收固定包长的数据包

顾名思义,即每个协议包的长度都是固定的。举个例子,例如我们可以规定每个协议包的大小是 64 个字节,每次收满 64 个字节,就取出来解析(如果不够,就先存起来)。

这种通信协议的格式简单但灵活性差。如果包内容不足指定的字节数,剩余的空间需要填充特殊的信息,如 \0如果包内容超过指定字节数,又得分包分片,需要增加额外处理逻辑——在发送端进行分包分片,在接收端重新组装包片。

4.4复杂处理方法

报头+数据格式

       这种格式的包一般分为两个部分,即报头和报体,报头是固定大小的,且该报头中必须含有一个字段来说明接下来的报体有多大。在实际处理过程当中,我们在制作报头时,也会包含发送数据的一些信息,如发送数据的文件名称,加密算法的哈希值,以及发送数据文件的大小。

       实际的报头,通常采用字典的方式完成对数据信息的封装,然后将报头的长度控制在固定的长度,接收方会在一开始就接收固定长度的数据包,然后在发送报头,发送数据。其一般的传输过程如下所示:

服务器

       1.制作报头

       2.发送报头的长度

       3.发送报头

       4.发送数据

客户端

       1.接收报头长度

       2.接收报头

       3.解析报头内容

       4.接收数据

在具体实现过程当中,使用了特殊的数据结果struct,python中的struct包,将报头的长度可以转化为只有四个字节大小的数据结构,在客户端只需在接收数据前预先接收4个字节大小的数据,从此数据当中可以得到报头的长度,将此作为接收报头的长度,即可完成对报头的接收。接着提取报头内的信息,将报头内关于数据长度的信息提取出来作为接收数据长度的大小,即可完成接收数据的任务。在这个过程当中,无论是发送报头长度,发送报文,发送数据,对于服务器和客户端来说,都对发送或者接收的信息有一个明显界限,能够区分发送的数据。成功的解决了TCP作为流式协议无法确认接收多少报文的缺陷,同时也明确了包与包之间的界限。