# 网络架构

# 网络体系结构

1.OSI 协议是由 ISO (国际标准化组织) 制定的,用于提供给开发者一个必须的、通用的概念以便开发完善、可以用来解释连接不同系统的框架。OSI 协议将网络体系结构划分为 7 层:应用层、表示层、会话层、传输层、网络层、数据链路层、物理层。
2.TCP/IP (Transmission Control Protocol/Internet Protocol,传输控制协议 / 网际协议) 体系结构是指能够在多个不同网络间实现的协议簇。TCP/IP 传输协议包含 4 层体系结构,应用层、传输层、网络层和网络接口层。

主要分层级如下图所示:

image-20230131173017627

分成设计的好处:

各层之间可以独立设计和实现,只要保证相邻层之间的调用规范和接口不变,便可以非常方便的灵活的改变各层内部实现方式,以进行优化和完成需求

# 应用层

应用层定义了运行在不同系统上的应用程序如何相互传递报文

  • DNS: 域名系统 (Domain Name System),用来实现域名与 IP 地址的转换,运行在 UDP 之上,默认使用 53 端口:
  • FTP: 文件传输协议 (File Transfer Protocol),可以通过网络在不同平台之间实现文件的传输,是一种基于 TCP 的明文传输协议,默认工作在 21 端口;
  • HTTP: 超文本传输协议 (HyperText Transfer Protocol),运行于 TCP 之上,默认端口为 80:
  • SMTP: 简单邮件传输协议 (Simple Mail Transfer Protoco),建立在 TCP 的基础上,使用明文传递邮件和发送命令,默认使用 25 端口。
  • TELNET: 远程登陆协议,运行于 TCP 之上,默认使用 23 端口。

# 传输层

主要负责向两个主机中进程之间的通信提供服务。

在网络中,每台主机系统都拥有一个唯一的 IP 地址,发送方根据接收方的 IP 地址,将消息发送到接收方。每个程序的运行在主机系统中都有唯一的端口号。只需要通过 IP 进行寻址确定的目标主机,再根据目标系统的端口号,就能够正确的将消息进行传递。

网络层是根据网络地址将源结点发出的数据包传送到目的结点,而传输层则负责将数据可靠地传送到目标系统对应的端口。

  • TCP: 为两台计算机之间提供面向连接、可靠的字节流服务。一台计算机发出的字节流无差错地发往网络上的其他计算机,由于其可靠的传输方式,故传输速度较慢。
  • UDP: 是一个简单的面向数据报的传输层协议。提供的是非面向连接的、不可靠的数据流传输。UDP 在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,故传输速度很快。

# 三次握手协议

image-20230131212555640

  1. 客户端送一个 SYN 包作为建立连接的请求等待确认应答。
  2. 服务器接收到请求数据包后,发送 ACK 确认应答,发送 SYN 包请求连接。
  3. 客户端针对 SYN 包发送 ACK 包确认应答。

# 网络层

ip 运行与互联网,是网络互联的重要基础,通过 ip 地址来标识网络上的主机,在公开网络上或者局域网内部,必须使用不同的 ip 地址

由于网络地址转换(NAT)和代理服务器等技术的广泛应用,不同内网之间的主机可以使用相同的公网 ip 地址。IP 地址与端口来共同标识网络上特定应用程序,俗称 Socket

# 网络接口层

MAC 地址也称为网卡物理地址,具有唯一性,是一个 48 为的 2 进制

# Socket 使用

# Socket 的编程简介

套接字 (Socket) 是计算机之间进行通信的一种约定。通过 Socket,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据

市面上多数的远程管理软件,大多数依赖 Socket 来实现特定功能的,其包括两个部分:运行在客户端的称为 ClientSocket,运行在服务端的称为 ServerSocket,其实现通信的过程如下所示:

  1. 服务端先初始化 Socket,然后与端口绑定(bind),对端口进行监听(listen),等待客户端连接。
  2. 客户端初始化一个 Socket,客户端的套接字必须首先指出服务端套接字的地址和端口号,然后向服务器端套接字提出连接请求(connect)。
  3. 当服务器端套接字接受到客户端套接字的连接请求,响应客户端套戒字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,由此连接成功

image-20230131215351633

# UDP 编程

UDP 属于无连接协议,编程的时候不需要建立连接,而是直接向接收方发送信息。UDP 不提供应答重传机制,无法保证数据一定能够到达目的地,下面是三种 UDP 编程常用的 socket 模块方法

  1. socket (family [,type [,proto]]):创建一个 Socket 对象,family 为 socket.AF_INET 表示使用 IPV4,socket.AF_INET6 表示使用 IPV6;type 为 SOCK_STREAM 表示使用 TCP,SOCK_DGRAM 表示使用 UDP
  2. sendto (string,address):把 string 指定的内容发送给 adress 指定的地址(这里的 adress 是指 ip + 端口号)
  3. recvfrom (bufsize [,flags]):接受数据
#server
import socket
s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)#调用 socket 函数初始化
#print(s)
s.bind(("",8001)) #指定 IP 和端口
while True:
    data,addr=s.recvfrom(1024)#不断的接收数据
    print(data.decode())
    if data.decode=='exit':
        break
s.close()
#client
import socket
import uuid
import sys
s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
def get_mac_adress():
    mac=uuid.UUID(int=uuid.getnode()).hex[-12:]
    return ":".join([mac[e:e+2] for e in range(0,11,2)])
ip=socket.gethostbyname(socket.gethostname())
#print ("ip 的值:",ip)
mac=get_mac_adress()
#print ("mac 的值",mac)
info="ip addr:"+ip+"\n"+"mac addr:"+mac
#print(info)
s.sendto(info.encode(),("127.0.0.1",8001))
s.sendto(sys.argv[1].encode(),("127.0.0.1",8001))
s.close()

image-20230201195704235

# TCP 编程

通常用于可靠的数据传输场合,TCP 通常使用的 socket 模块如下:

  • socket (family [,type [,proto]]): 创建一个 Socket 对象,fiamily 为 socket.AF INET 表示使用 IPV4,socket.AF_INET6 表示使用 IPV6; type 为 SOCK STREAM 表示使用 TCP,SOCK DGRAM 表示使用 UDP。
  • connect (address): 连接远程主机:
  • send (bytes [,flags]): 发送数据:
  • recv (bufsizelflags]): 接收数据:
  • bind (address): 绑定地址:
  • listen (backlog): 开始监听,等待客户端连接,blacklog 排队数,backlog+1 表示允许的最大连接数:
  • accept (): 响应客户端的请求,接收一个连接
#Server
import socket
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.bind(("",8001))
s.listen(1)
print("Listen at port: 8001")
conn,addr = s.accept()
print(conn)
print(addr)
while True:
    data = conn.recv(1024)
    data = data.decode()
    print('Recv: ',data)
    c = input ('Input:')
    conn.sendall(c.encode())
    if c.lower()=='bye':
        break
conn.close()
s.close()
#Client
import socket
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
try:
    s.connect(('127.0.0.1',8001))
except Exception as q:
    print("Error!!!")
while True:
    c = input("input: ")
    s.sendall(c.encode())
    data = s.recv(1024).decode()
    print('Recv:',data)
    if c.lower() == 'bye':
        break
s.close()

image-20230201202218353

image-20230201202227841

# 扩展

下表还列举了一些常用函数

函数描述
服务器端套接字
s.bind()绑定地址(host,port)到套接字, 在 AF_INET 下,以元组(host,port)的形式表示地址。
s.listen()开始 TCP 监听。backlog 指定在拒绝连接之前,操作系统可以挂起的最大连接数量。该值至少为 1,大部分应用程序设为 5 就可以了。
s.accept()被动接受 TCP 客户端连接,(阻塞式) 等待连接的到来
客户端套接字
s.connect()主动初始化 TCP 服务器连接,。一般 address 的格式为元组(hostname,port),如果连接出错,返回 socket.error 错误。
s.connect_ex()connect () 函数的扩展版本,出错时返回出错码,而不是抛出异常
公共用途的套接字函数
s.recv()接收 TCP 数据,数据以字符串形式返回,bufsize 指定要接收的最大数据量。flag 提供有关消息的其他信息,通常可以忽略。
s.send()发送 TCP 数据,将 string 中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于 string 的字节大小。
s.sendall()完整发送 TCP 数据。将 string 中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回 None,失败则抛出异常。
s.recvfrom()接收 UDP 数据,与 recv () 类似,但返回值是(data,address)。其中 data 是包含接收数据的字符串,address 是发送数据的套接字地址。
s.sendto()发送 UDP 数据,将数据发送到套接字,address 是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。
s.close()关闭套接字
s.getpeername()返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)。
s.getsockname()返回套接字自己的地址。通常是一个元组 (ipaddr,port)
s.setsockopt(level,optname,value)设置给定套接字选项的值。
s.getsockopt(level,optname[.buflen])返回套接字选项的值。
s.settimeout(timeout)设置套接字操作的超时期,timeout 是一个浮点数,单位是秒。值为 None 表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如 connect ())
s.gettimeout()返回当前超时期的值,单位是秒,如果没有设置超时期,则返回 None。
s.fileno()返回套接字的文件描述符。
s.setblocking(flag)如果 flag 为 0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)。非阻塞模式下,如果调用 recv () 没有发现任何数据,或 send () 调用无法立即发送数据,那么将引起 socket.error 异常。
s.makefile()创建一个与该套接字相关连的文件

# Socket 文件传输

#Server
import socketserver
import struct
import os
import re
import json
import struct
from optparse import OptionParser
def sendFile(conn, head_info,head_info_len,filename):
    try:
        conn.send(head_info_len)
        conn.send(head_info.encode('utf-8'))
        with open(filename, 'rb') as f:
            conn.sendall(f.read())
        print('[+]send success! '+filename)
    except:
        print('[-]send fail! ' + filename)
def operafile(filename):
    filesize_bytes = os.path.getsize(filename)
    pattern = re.compile(r'([^<>/\\\|:""\*\?]+\.\w+$)')
    data = pattern.findall(filename)
    head_dir = {
        'filename': data,
        'filesize_bytes': filesize_bytes,
    }
    head_info = json.dumps(head_dir)
    head_info_len = struct.pack('i', len(head_info))
    return head_info_len, head_info
class MyServer(socketserver.BaseRequestHandler):
    buffsize = 1024
    def handle(self):
        print('[+]远程客户端ip地址:', self.client_address[0],'\n')
        while True:
            filename = input('请输入要发送的文件名>>>').strip() #移除字符串头尾指定的字符(默认为空格或换行符)或字符序列
            if(filename == "exit"):
                break
            head_info_len, head_info = operafile(filename)
            sendFile(self.request,head_info,head_info_len,filename)
        self.request.close()
def main():
    parser = OptionParser("Usage:%prog -p <port> ")   # 输出帮助信息
    parser.add_option('-p',type='string',dest='port',help='specify targer port')   # 获取 ip 地址参数
    options,args = parser.parse_args()
    port = int(options.port)
    print("[+]listening at " + str(port))
    s = socketserver.ThreadingTCPServer(('0.0.0.0', port), MyServer)  #
    s.serve_forever()
if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        print("interrupted by user, killing all threads...")
#client
from socket import *
import os
import sys
import json
import struct
from optparse import OptionParser
def recv_file(head_dir, tcp_client):
    filename = head_dir['filename']
    filesize = head_dir['filesize_bytes']
    print("[+]filename: "+filename[0])
    print("[+]filesize: "+ str(filesize))
    recv_len = 0
    f = open(filename[0], 'wb')
    while recv_len < filesize:
        if(filesize > 1024):
            recv_mesg = tcp_client.recv(1024)
            recv_len += len(recv_mesg)
            f.write(recv_mesg)
        else:
            recv_mesg = tcp_client.recv(filesize)
            recv_len += len(recv_mesg)
            f.write(recv_mesg)
    f.close()
    print('[+]文件传输完成!')
def main():
    parser = OptionParser("Usage:%prog -u <target address> -p <port> ")   # 输出帮助信息
    parser.add_option('-u', type='string', dest='ip', help='specify targer ip')  # 获取 ip 地址参数
    parser.add_option('-p',type='string',dest='port',help='specify targer port')   # 获取 ip 地址参数
    options,args = parser.parse_args()
    target_port = int(options.port)
    target_ip = options.ip
    tcp_client = socket(AF_INET, SOCK_STREAM)  # socket 初始化
    ip_port = ((target_ip, target_port))
    tcp_client.connect_ex(ip_port)
    print('[+]等待服务端应答数据....')
    struct_len = tcp_client.recv(4)  # 接收报头长度
    struct_info_len = struct.unpack('i', struct_len)[0]  # 解析得到报头信息的长度
    print("[+]接收头信息长度:" + str(struct_info_len))
    head_info = tcp_client.recv(struct_info_len)
    head_dir = json.loads(head_info.decode('utf-8'))  # 将报头的内容反序列化
    print("[+]输出头部信息内容:" + str(head_dir))
    recv_file(head_dir, tcp_client)
if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        print("interrupted by user, killing all threads...")

# 可执行文件的转换

当开发者向普通用户分享程序的时候,为了方便用户在未安装 Python 环境的情况下能够正常运行,需要将为开发号的程序进行打包,转换成用户可执行的文件类型

  • PyInstaller
  • Py2exe

上面是两种常见的打包工具,PyInstaller 打包的执行文件,只能在与执行打包操作系统的同类型环境下运行

# PyInstaller 安装

pip install pyinstaller

# win 系统下的转换

pyinstaller -F -i ico文件 py文件

# Linux 下的转换

pyinstaller -F py文件