day30 网络通信过程和TFTP协议

day30 网络通信过程和TFTP协议

今日内容概要

  1. 网络通信过程
  2. TFTP协议

昨日内容回顾

  1. 网络基础
  2. socket
  3. UDP

今日内容详细

网络通信过程

今天,我们要研究一下网络通信过程的细节。

UDP广播

在学习UDP广播之前,我们先要了解一下什么是广播。

网络节点之间的通信,根据接收方数目的不同,可分为单播、多播和广播。

  1. 单播:点对点,一对一
  2. 多播:一对多
  3. 广播:一对所有接收方

广播是针对网络中所有的接收方而言的,因此,只能使用UDP,而不能使用TCP。因为TCP需要用户之间进行连接。但是广播只是一方发送,多方接受,并不需要建立连接。

1571748286404

UDP的socket默认是不支持广播的,我们需要设置它的setsockopt的方法,让其允许发送广播数据。具体的为:

import socket
addr = ('<broadcast>', 8888)
# <broadcast>自动识别为当前网络的广播地址,一般为“网络号.255”,如192.168.34.255
skt = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
skt.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) # 允许skt发送广播数据
# 对需要发送广播数据的套接字对象修改设置,否则不能发送广播数据
# setsockopt 设置套接字选项
skt.sendto(b'Hello World!', addr)
print('发送成功!')

上面的代码会对整个网段中,所有IP的7788端口发送消息Hello World!。我们可以编写一个简单的接收程序来接收广播的内容:

from socket import socket, AF_INET, SOCK_DGRAM
addr = ('', 8888)
skt = socket(AF_INET, SOCK_DGRAM)
skt.bind(addr)
while True:
    data = skt.recvfrom(1024)
    print(data[0].decode())

这样我们就能收到广播的内容了。

我们可以把发出的内容改成让用户输入的东西,我们就可以随意广播想要的内容了:

import socket
addr = ('<broadcast>', 8888)
skt = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
skt.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
while True:
    msg = input('请输入要广播的内容:')
    skt.sendto(msg.encode(), addr)    # 注意编码方式要和解码方式相同
    print('发送成功!')
网络连接方式
Cisco Packet Tracer的安装和使用

Packet Tracer是由Cisco(思科)公司发布的一款辅助学习工具,用来给思科网络课程的初学者提供一个方便的设计、配置、排除网络故障的网络虚拟环境。有了这个工具,用户可以不必购买硬件就能学习网络连接和交互的方式了。

用户可以在软件的图形用户界面上直接使用拖曳方法建立网络拓扑,并可提供数据包在网络中行进的详细处理过程,观察网络实时运行情况。

思科官网上我并没有顺利找到Packet Tracer的下载地址,但是百度有很多第三方软件下载网站都可以免费下载。甚至也能下载到汉化版本。

安装也没有太多需要注意的,一路点Next就可以。

2台电脑联网

首先,我们创建两台电脑

1571750996572

然后,将两台电脑相互连接:

1571751338862

然后,单击一台电脑,在弹出窗口中选择conf选项卡。在Interface标签中选择FastEthernet0选项,设置一个IP地址,并补全子网掩码。IP地址最好使用私有IP,最后一位不要是1(1留给网关使用)。

1571751765091

同时,要确定端口状态为开启。

1571752086303

同样地,设置另一台电脑的IP地址和子网掩码。注意这个电脑的IP要和前一个电脑在同一个局域网中,且不能重复。

1571751899723

单击随意一台电脑,选择到Desktop选项,打开命令行。输入命令ping 192.168.1.3来测试连接状态,其中192.168.1.3是刚刚设置的另一台电脑的IP。

1571752690804

成功收到对方的相应,连接成功,而且很轻松。

通过集线器联网

通过网线的方式虽然能实现两台计算机相互连接,但是对于多台计算机之间组网就不十分现实——一台终端计算机只能连接一个设备。

于是,人们创造出了集线器,用来实现多台计算机之间的联网通信。

1571790664153

集线器的缺点是每次发送消息,都要由集线器以广播的形式发送给其他全部计算机。这样会占用很多资源,速度比较慢。

例如我们现在有一个8口hub, 要使用端口1上的机器要给端口8上的机器发数据。

  1. 端口1先检查hub上有没有数据在传输,如果没有,端口1就跳出来向hub上喊:“我有数据包要给端口8,请端口8听到后回话” 。
  2. 这个数据被以广播的方式发送到hub上的其余7个口上,每端口都会接到这样的数据包,然后端口2---端口7会发一则消息给1:“我不是端口8” 。
  3. 与此同时端口8会发消息给1:“我是端口8,找我吗?”端口1收到上述消息后,会和端口8进行确认,然后他们建立传输链接,完成数据转发。
  4. 等如果端口1在发送寻找断口8的消息后,没有得到相应,那它还会接着发这个消息,直到收到端口8的回答。等端口1和端口8完整数据转发后,假设他们还要进行通讯,那么hub上还会重复以上的过程。
  5. 一个数据,需要送达所有的端口,这不但增加了数据转发的时间,而且hub往往会给网络带来广播风暴(广播数据充斥网络无法处理,并占用大量网络带宽,导致正常业务不能运行)。
通过交换机连接

如果我们把集线器换成交换机,就可以实现通过MAC地址,一对一设备的网络通信了。

使用Cisco PT的时候,要注意切换到实时模式,等到连接线两端的状态点都编程绿色之后,再用ping测试网络联通状态。

交换机收发

**集线器(Hub)**是计算机网络中连接多个计算机或其他设备的连接设备,是对网络进行集中管理的最小单元。英文Hub就是中心的意思,像树的主干一样,它是各分支的汇集点。Hub是一个共享设备,主要提供信号放大和中转的功能,它把一个端口接收的所有信号向所有端口分发出去

**交换机(Switch)**是一种基于MAC(网卡的硬件地址)识别,能完成封装转发数据包功能的网络设备。交换机可以“学习”MAC地址,并把其存放在内部地址表中,通过在数据帧的始发者和目标接收者之间建立临时的交换路径,使数据帧直接由源地址到达目的地址

我们在集线器中的例子,如果换成交换机来做,就会容易很多:

  1. 假设端口1和端口8从没有通信过,那么开始的时候,他们的工作和hub一样,端口1要在交换机上找端口8,一旦端口8返回确认信息,那再端口1上就会生成1个和端口8的地址对应表。
  2. 这个表里面有所有和端口1通过信的端口,一旦有了这地址对应表,那在以后端口1要和端口8通讯,可以直接送达,而且其他的端口也不会知道他们之间正在转发数据,这样加快了数据转发时间,并且避免了广播风暴。

MAC地址:

  • 物理地址(实际地址):由网络设备制造商生产时写在硬件内部
  • IP地址与MAC地址在计算机里都是以二进制表示的,IP地址是32位的,而MAC地址则是48位的(6个字节)
    • 如:08:00:20:0A:8C:6D就是一个MAC地址,其中前3组16进制数08:00:20代表网络硬件制造商的编号,它由IEEE(电气与电子工程师协会)分配
    • 而后3组16进制数0A:8C:6D代表该制造商所制造的某个网络产品(如网卡)的系列号
  • MAC地址在世界是惟一的(可以直接理解为网卡的序列号)
  • 通过IP地址、端口号、MAC地址 保证了数据的稳定传输
通过路由器联网

两台电脑能进行通信的前提是它们处在同一网段。集线器和交换机都要求两台电脑处在统一网段中。

为什么我们要使用集线器或交换机来实现多态计算机之间的通信,而不是像电工一样,把网线剪开,直接分成多叉,连接多台电脑呢?

那是因为网络中传输的不是简简单单的能量,而是电压时高时低的电信号。如果多台电脑同时向同一条网线中传输信号,电压的高低会互相影响而无法传输信息。这是我们使用工具来协调多台计算机之间通信的原因。

集线器和交换机的作用就是组成一个小型的局域网。

集线器和交换机之间的区别在于:

  • 集线器收到的所有数据包都会以广播的形式发送
  • 交换机可以智能学习,如果对于已经通信过的设备,可以直接通信。

那么什么是路由器呢?

路由器是确定一条路径的设备。路由器是因特网中用来链接网络号不同的网络。相当于中间人,连接各局域网、广域网的设备。它会根据信道的情况自动选择和设定路由,以最佳路径,按前后顺序发送信号。

路由器的一个作用是连通不同的网络,另一个作用是选择信息传送的线路。

选择通畅快捷的近路,能大大提高通信速度,减轻网络系统通信负荷,节约网络系统资源,提高网络系统畅通率

1571821927241

不同网段间计算机之间的通信,就要用到路由器。一个路由器只有两个端口,所以同一网段中的计算机还是需要用交换机连接。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-psGC6oBt-1571837169464)(https://gitee.com/oldboy-python-full-stack-26/19083026021/raw/master/week08/day30/day30%20网络通信过程和TFTP协议.assets/路由器.gif)]

尝试着从左侧的终端电脑连接右侧的电脑,似乎并没有成功。

路由器_测试

这或许是我们没有给各个终端指定网管导致的。给每个终端计算机指定它们对应的路由器端口的IP作为网关。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wPHhGaJ5-1571837169470)(https://gitee.com/oldboy-python-full-stack-26/19083026021/raw/master/week08/day30/day30%20网络通信过程和TFTP协议.assets/路由器_修改网关.gif)]

再ping一下,成功连接。第一次连接时间延迟会长一点。之后的链接都会很顺畅。

路由器_连接成功

如果连接不同,可以尝试交换一下路由器中两个网卡的IP地址。

连通之后,我们可以查看不同网络中的通信顺序。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E6DwSZTV-1571837169478)(https://gitee.com/oldboy-python-full-stack-26/19083026021/raw/master/week08/day30/day30%20网络通信过程和TFTP协议.assets/路由器_连接测试.gif)]

第一次的时候需要寻找对方的MAC地址,以后的通信可以实现一对一。

TFTP协议

TFTP介绍

TFTP(Trivial File Transfer Protocol,简单文件传输协议)是TCP/IP协议簇中的一个用来在客户端与服务器之间进行简单文件传输的协议。

使用TFTP协议,可以实现简单的文件上传和下载。

TFTP协议的特点有:

  • 简单
  • 占用资源少
  • 适合传递小文件
  • 适合在局域网中进行传输
  • 端口号为69
  • 基于UDP实现

我们可以使用Tftpd32共享服务器作为TFTP的服务器端。其下载安装方法也极简单,一路点击下一步即可。

1571823605951

有了服务器,我们还需要一个下载器(客户端)。这个下载器就要由我们自己来编写代码了。

下载,也就是从服务器上将一个文件复制到本机上。

下载一共分三步:

  1. 在本地创建一个空文件
  2. 向里面写入数据(接收到一点就向空文件里写一点)
  3. 接收完数据后关闭文件

在写入数据时,我们需要从服务器上接收数据。

对于TFTP协议来说,首先,我们要向服务器发出下载的请求。服务器会先检查要下载的文件是否存在于共享文件夹中。如果不存在,会范围一个表示文件不存在的数据。如果文件存在,服务器会相应给我们一个确认的数据包。我们回复服务器一个ACK确认包,随后服务器就会在一个新端口将数据逐个传给我们,每次传递512个字节的数据。

我们收到的数据包的前四位为固定格式,前两位表示操作码,后两位表示块编号。每次我们收到数据后,都要将块编号封装到ACK数据中返回给服务器,确认我们已经收到。

如果数据包丢失,我们没有发送确认包,服务器隔一段时间就会再发送一个相同的数据包过来。

如果我们的ACK文件丢失,服务器还是会给我们发送一个相同的数据包。我们看到里面的块编号跟上一个数据包相同,就不进行写入操作,而是重新发送一个ACK文件,让服务器给我们发送下一个数据包。

当数据包中数据的长度小于512(或总长度小于516)时,说明这个包是最后一个包(如果文件的大小刚好是512的倍数,最后一个数据包的数据长度将会是0)。我们就可以终止接受了。

1571828297847

struct模块的使用

我们给TFTP服务段发送请求时,发送的数据需要是按照TFTP协议的标准封装:

  • 数据的前两个字节为操作码,其中,1代表下载请求,2代表上传请求,3代表本数据传递的是数据,4代表本数据为ACK确认包,5表示出现错误。
  • 读写请求的第二部分是字节形式的文件名,Windows系统需要使用gb2312编码;第三部分是占一个字节的数字0;第四部分是传输数据的模式,一般写成octet,表示使用八位二进制数读写;第五部分也是一个占一字节的数字0;
  • 数据包的第二部分是块编号,占两个字节,用来表示当前数据在整个文档中的位置;第三部分是512字节的二进制文件数据;
  • ACK的第二部分是确认的块编号,占两个字节;
  • ERROR的第二部分是占两个字节的差错码;第三部分是差错信息,也是字节形式;第三部分是占一位字节的数字0。

1571828410069

TFTP传输的数据需要符合协议规范,我们需要确保操作码(1、2、3、4、5)占两个字节,0占一个字节。如果我们自己封装,一来操作十分繁琐,二来这些设计底层的操作很容易出现错误。

好在Python中封装的struct包可以帮助我们更加容易地组织数据。

例如,我们想要构造下载请求数据1test.jpg0octet0,可以这样编写代码:

import struct
data = struct('!H8sb5sb', 1, b'test.jpg', 0, b'octet', 0)    # 注意传入的字符串需要提前转成字节

其中,!H8sb5sb是封装的格式,最开头的!用来表示按照网络传输数据要求的形式来组织数据,其他的位置的含义与正则表达式,用来指代不同的数据类型,我们稍后会有详细讨论。

在这里,H表示将后面的1替换成两个字节;8s相当于ssssssss,每一个s用来表示一个字符,因为test.jpg一共有8个字符,所以是8s;b表示后面的0替换成一个字节;5s的含义同8s类似,用来代表后面的octet;b则是用来替换后面的0。

具体的格式含义,可以参考下面的表格:

FormatC TypePython字节数
xpad byteno value
ccharstring of length 11
bsigned charinteger1
Bunsigned charinteger1
?_Boolbool1
hshortinteger2
Hunsigned shortinteger2
iintinteger4
Iunsigned intinteger or long4
llonginteger4
Lunsigned longlong4
qlong longlong8
Qunsigned long longlong8
ffloatfloat4
ddoublefloat8
schar[]string
pchar[]string
Pvoid *long

对于这个表格,有两点需要说明:

  1. q和Q只适用于64位机器;
  2. 每个格式前可以有一个数字,表示这个类型的个数。比如s表示字符串,4s表示长度为4个字节的字符串

为了同c中的结构体交换数据,还要考虑有的c或c++编译器使用了字节对齐,通常是以4个字节为单位的32位系统,故而struct根据本地机器字节顺序转换。可以用格式中的第一个字符来改变对齐方式.定义如下:

CharacterByte orderSize and alignment
@nativenative 凑够4个字节
=nativestandard 按原字节数
<little-endianstandard 按原字节数
>big-endianstandard 按原字节数
!network (= big-endian)standard 按原字节数

使用方法是放在fmt的第一个位置,就像@5s6sif

了解了如何使用struct模块的格式,我们就可以深入了解struct了。

struct模块可以按照指定格式,将Python数据转换为字符串,该字符串为字节流。

struct模块中最重要的三个方法是pack()unpack()calcsize()

  • pack()函数用来按照指定的格式(fmt),把数据封装成字符串(实际上是类似于C结构体的字节流)

    data = struct.pack('!H8sb5sb', 1, b'test.jpg', 0, b'obtet', 0)
    block_num = 1
    ack = struct.padk('!HH', 4, block_num)
    
  • unpack()函数可以按照给定的格式(fmt)解析字节流字符串,返回解析出来的元组

    recv_data = skt.recvfrom(1024)
    operation_code, block_num = struct.unpack('!HH', recv_data[:4])
    
  • calcsize()用来计算给定的格式(fmt)会占用多少字节

    struct.calcsize(fmt)
    

struct模块配合socket就可以尝试向服务器发送请求了,其基本用法为:

import struct
from socket import socket, AF_INET, SOCK_DGRAM
acquire_data = struct.pack('!H8sb5sb', 1, b'test.jpg', 0, b'octet', 0)
skt = socket(AF_INET, SOCK_DGRAM)
addr = ('192.168.34.93', 69)
skt.sendto(acquire_data, addr)
print(skt.recvfrom(1024))
skt.close()

上面的代码运行后的结果为:

(b'\x00\x03\x00\x01\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x02\x00\x00\x01\x00\x01\x00\x00\xff\xdb\x00C\x00\x08\x06\x06\x07\x06\x05\x08\x07\x07\x07\t\t\x08\n\x0c\x14\r\x0c\x0b\x0b\x0c\x19\x12\x13\x0f\x14\x1d\x1a\x1f\x1e\x1d\x1a\x1c\x1c $.\' ",#\x1c\x1c(7),01444\x1f\'9=82<.342\xff\xdb\x00C\x01\t\t\t\x0c\x0b\x0c\x18\r\r\x182!\x1c!22222222222222222222222222222222222222222222222222\xff\xc0\x00\x11\x08\t#\x06v\x03\x01"\x00\x02\x11\x01\x03\x11\x01\xff\xc4\x00\x1f\x00\x00\x01\x05\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\xff\xc4\x00\xb5\x10\x00\x02\x01\x03\x03\x02\x04\x03\x05\x05\x04\x04\x00\x00\x01}\x01\x02\x03\x00\x04\x11\x05\x12!1A\x06\x13Qa\x07"q\x142\x81\x91\xa1\x08#B\xb1\xc1\x15R\xd1\xf0$3br\x82\t\n\x16\x17\x18\x19\x1a%&\'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz\x83\x84\x85\x86\x87\x88\x89\x8a\x92\x93\x94\x95\x96\x97\x98\x99\x9a\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xff\xc4\x00\x1f\x01\x00\x03\x01\x01\x01\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\xff\xc4\x00\xb5\x11\x00\x02\x01\x02\x04\x04\x03\x04\x07\x05\x04\x04\x00\x01\x02w\x00\x01\x02\x03\x11\x04\x05!1\x06\x12AQ\x07aq\x13"2\x81\x08\x14B\x91\xa1\xb1\xc1\t#3R\xf0\x15br\xd1\n\x16$4\xe1%\xf1\x17\x18\x19\x1a&\'()*56789:CDEFGHI', ('192.168.34.93', 57918))

返回值是一个元组,元组有两个元素,第二个元素很显而易见,是服务器的地址元组。第一个元素是一坨二进制码。虽然我们不能完全解读,但是前四位还是能看出来,是两个数字:3和1。其中,数字3表示这是一串数据;数字1表示这串数据的块编号是1。

TFTP客户端编写

直到TFTP的协议规则,也知道struct模块的使用方法,我们就可以写一个用来下载数据的客户端了:

import struct
from socket import socket, AF_INET, SOCK_DGRAM
file_name = input('请输入要下载的文件名:').strip().encode('gbk')
service_ip = input('请输入服务器的IP地址:').strip()
acquire_data = struct.pack(f'!H{len(file_name)}sb5sb', 1, file_name, 0, b'octet', 0)
skt = socket(AF_INET, SOCK_DGRAM)
skt.sendto(acquire_data, (service_ip, 69))
f = open(file_name, 'ab')
count = 1
while True:
    data, addr = skt.recvfrom(1024)
    operation_code, block_num = struct.unpack('!HH', data[:4])
    if count == block_num:
        if operation_code == 3:
            f.write(data[4:])
            f.flush()
            ack = struct.pack('!HH', 4, block_num)
            skt.sendto(ack, addr)
            count += 1
        else:
            print('出现错误!')
            break
        if len(data) < 516:
            print('传输完成!')
            break
f.close()

同样道理,上传数据的客户端可以这样写:

from socket import socket, AF_INET, SOCK_DGRAM
import struct

file_name = input('请输入要上传的文件:').strip()
server_ip = input('请输入服务器IP地址:').strip()
upload_acquire = struct.pack(f'!H{len(file_name.encode("gbk"))}sb5sb', 2, file_name.encode('gbk'), 0, b'octet', 0)
skt = socket(AF_INET, SOCK_DGRAM)
skt.sendto(upload_acquire, (server_ip, 69))
f = open(file_name, 'rb')
count = 0

while True:
    data, addr = skt.recvfrom(1024)
    operation_code, block_num = struct.unpack('!HH', data)
    if block_num == count:
        if operation_code == 4:
            data_read = f.read(512)
            count += 1
            data_send = struct.pack(f'!HH{len(data_read)}s', 3, count, data_read)
            skt.sendto(data_send, addr)
            if len(data_read) < 512:
                print('上传完毕!')
                break
        else:
            print('出现错误!')
            break
f.close()

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值