# 网络架构
# 网络体系结构
1.OSI 协议是由 ISO (国际标准化组织) 制定的,用于提供给开发者一个必须的、通用的概念以便开发完善、可以用来解释连接不同系统的框架。OSI 协议将网络体系结构划分为 7 层:应用层、表示层、会话层、传输层、网络层、数据链路层、物理层。
2.TCP/IP (Transmission Control Protocol/Internet Protocol,传输控制协议 / 网际协议) 体系结构是指能够在多个不同网络间实现的协议簇。TCP/IP 传输协议包含 4 层体系结构,应用层、传输层、网络层和网络接口层。
主要分层级如下图所示:
分成设计的好处:
各层之间可以独立设计和实现,只要保证相邻层之间的调用规范和接口不变,便可以非常方便的灵活的改变各层内部实现方式,以进行优化和完成需求
# 应用层
应用层定义了运行在不同系统上的应用程序如何相互传递报文
- 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 在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,故传输速度很快。
# 三次握手协议
- 客户端送一个 SYN 包作为建立连接的请求等待确认应答。
- 服务器接收到请求数据包后,发送 ACK 确认应答,发送 SYN 包请求连接。
- 客户端针对 SYN 包发送 ACK 包确认应答。
# 网络层
ip 运行与互联网,是网络互联的重要基础,通过 ip 地址来标识网络上的主机,在公开网络上或者局域网内部,必须使用不同的 ip 地址
由于网络地址转换(NAT)和代理服务器等技术的广泛应用,不同内网之间的主机可以使用相同的公网 ip 地址。IP 地址与端口来共同标识网络上特定应用程序,俗称 Socket
# 网络接口层
MAC 地址也称为网卡物理地址,具有唯一性,是一个 48 为的 2 进制
# Socket 使用
# Socket 的编程简介
套接字 (Socket) 是计算机之间进行通信的一种约定。通过 Socket,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据
市面上多数的远程管理软件,大多数依赖 Socket 来实现特定功能的,其包括两个部分:运行在客户端的称为 ClientSocket,运行在服务端的称为 ServerSocket,其实现通信的过程如下所示:
- 服务端先初始化 Socket,然后与端口绑定(bind),对端口进行监听(listen),等待客户端连接。
- 客户端初始化一个 Socket,客户端的套接字必须首先指出服务端套接字的地址和端口号,然后向服务器端套接字提出连接请求(connect)。
- 当服务器端套接字接受到客户端套接字的连接请求,响应客户端套戒字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,由此连接成功
# UDP 编程
UDP 属于无连接协议,编程的时候不需要建立连接,而是直接向接收方发送信息。UDP 不提供应答重传机制,无法保证数据一定能够到达目的地,下面是三种 UDP 编程常用的 socket 模块方法
- socket (family [,type [,proto]]):创建一个 Socket 对象,family 为 socket.AF_INET 表示使用 IPV4,socket.AF_INET6 表示使用 IPV6;type 为 SOCK_STREAM 表示使用 TCP,SOCK_DGRAM 表示使用 UDP
- sendto (string,address):把 string 指定的内容发送给 adress 指定的地址(这里的 adress 是指 ip + 端口号)
- 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() |
# 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() |
# 扩展
下表还列举了一些常用函数
函数 | 描述 |
---|---|
服务器端套接字 | |
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文件