操作系统(简称OS)基础:
应用软件不能直接操作硬件,能直接操作硬件的只有操作系统;所以,应用软件可以通过操作系统来间接操作硬件
网络基础之网络协议:
网络通讯原理:
连接两台计算机之间的Internet实际上就是一系列统一的标准,这些标准称之为互联网协议;互联网的本质就是一系列的协议,总称为“互联网协议” (Internet Protocol Suite)
互联网协议的功能:定义计算机何如接入Internet,以及接入Internet的计算机通信的标准。
osi七层协议: 互联网协议按照功能不同分为OSI七层或TCP/IP五层或TCP/IP四层
用户感知到的只是最上面的一层应用层,自上而下每层都依赖于下一层;每层都运行特定的协议,越往上越靠近用户,越往下越靠近硬件
物理层功能:主要是基于电气特性发送高低电压(电信号),高电压对应的数字为1,低电压对应数字0
数据链路层:
数据链路层的由来: 单纯的电信号0、1没有任何意义,必须要规定电信号多少位一组,每组什么意思
数据链路层的功能:定义了电信号的分组方式
以太网协议(Ethernet):Ethernet协议规定了:1. 一组电信号构成一个数据包,叫做“帧”;2. 每一数据帧分成“报头”head和数据data两部分
head包含(固定18个字节):1. 发送者/原地址,6个字节; 2. 数据类型,6个字节; 3. 接受者/目标地址,6个字节
data包含:数据包的具体内容
Mac地址:head中包含的源、目标地址的由来:Ethernet规定接入Internet的设备都必须具备网卡,发送端和接收端的地址便是指网卡的地址,即Mac地址;
(每块网卡出厂时都被烧制上一个世界唯一的Mac地址,长度为48位2进制,通常由12位16进制数表示(前六位是厂商编号,后六位是流水线号))
广播: 有了Mac地址,同一网络内的两台主机就可以通信了;Ethernet采用最原始的广播的方式进行通信,即计算机通信基本靠吼
网络层:有了Ethernet、Mac地址、广播的发送方式,同一个局域网内的计算机就可以彼此通讯了,但世界范围内的互联网是由一个个彼此隔离的小的局域网(子网)组成的,所以不能所有的通信都采用以太网的广播方式
从上图可以看出:必须找出一种方法来区分哪些计算机属于同一广播域、哪些不是,如果是就采用广播的方式发送;如果不是就采用路由的方式(向不同广播域/子网分发数据包),Mac地址是无法区分的,它只跟厂商有关
网络层功能:引入一套新的地址来区分不同的广播域(子网),这套地址即网络地址
IP协议:1. 规定网络地址的协议叫IP协议,它定义的地址称为IP地址,广泛采用的v4版本即ipv4,它规定网咯地址由32位2进制表示;
2. 范围0.0.0.0-255.255.255.255
3. 一个IP地址通常写成四段十进制数,例如:172.16.10.1
IP地址分成两部分: 1. 网络部分:标识子网; 2. 主机部分:标识主机
注:单纯的IP地址段只是标识了IP地址的种类,从网络部分或主机部分都无法辨识一个IP所处的子网
子网掩码:表示子网络特征的一个参数;知道了“子网掩码”,我们就能判断任意两个IP地址是否处在同一个子网络。
网络层作用总结:IP协议的主要作用有两个:1. 为每台计算机分配IP地址;2.确定哪些地址在同一个子网络
IP数据包:分为head和data两个部分,然后直接放入以太包的data部分,如下所示:
ARP协议:由来:计算机通信基本靠吼,即广播的方式,所有上层的包到最后都要封装上以太网头,然后通过以太网协议发送;通信是基于Mac的广播方式实现,计算机在发包时获取自身的Mac容易,如何获取目标主机的Mac就需要通过ARP协议。
ARP协议功能:广播的方式发送数据包,获取目标主机的Mac地址
协议工作方式: 每台主机IP都是已知的
1. 首先通过IP地址和子网掩码区分出自己所处的子网
2. 分析是否处于同一网络(如果不是同一网络。通过ARP获取的是网关的Mac)
3. 这个包以广播的方式在发送端所处的子网内传输,所有主机接收后拆开包,发现目标IP是自己的就响应返回自己的Mac(这点还不是很理解,发送端所处的子网??)
传输层:由来:网络层的IP帮我们区分子网,以太网层的Mac帮我们找到主机,然后大家使用的都是应用程序,那么我们通过IP和Mac找到了一台特定的主机;然后,标识这台主机上的应用程序就是端口,端口即应用程序和网卡关联的编号。
传输层功能:建立端口到端口的通信
补充:端口范围0-65535,0-1023为操作系统占用端口
TCP协议: 可靠传输,需要挖双向“通道“,””3次“握手”和4次“挥手”;流式协议
UDP协议:不可靠传输,不需要挖“通道”;又称“数据报协议”
应用层:由来:用户使用的都是应用程序,均工作于应用层,互联网是开发的,大家都可以开发自己的应用程序,数据多种多样,必须规定好数据的组织形式
应用层功能: 规定应用程序的数据格式
例如: TCP协议可以为各种各样的程序传递数据,比如Email、www、FTP等;那么必须有不同协议规定电子邮件、网页、发图片数据的格式,这些应用程序协议就构成了“应用层”
Socket:
socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,socket是一个门面模式,他把复杂的TCP/IP协议族隐藏在socket接口的后面,对于用户来说,一组简单的接口就是全部,让socket去组织数据,以符合指定的协议。
所以我们无需深入理解TCP/UDP协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循TCP、UDP标准的
附:也有人将socket说成IP+port,IP是用来标识互联网中的一台主机的位置,而port是用来标识这台机器上的一个应用程序,IP地址是配置到网卡上的,而port是应用程序开启的,IP与port的绑定就标识了互联网中独一无二的一个应用程序; 而程序的pid是同一台机器上不同进程或线程的标识
套接字: 套接字有两种(或者说两个种族),分别是基于文件型的和基于网络型的。
基于文件类型的套接字家族: 套接字家族的名字是 : AF_UNIX
基于网络类型的套接字家族: 套接字家族的名字是:AF_INET
还有AF_INET6被用于ivp6;AF_INET是使用最广泛的一个,python支持很多地址家族,但是由于我们只关心网络编程,所以大部分时候我们只是用AF_INET
套接字(socket) 工作流程:以打电话为例说明:
客户端代码如下:
import socket # 1. 买“手机” phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) print(phone) # 打印结果: # <socket.socket fd=300, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0> # 2. “拨号” (客户端不需要绑定IP和端口) phone.connect(("127.0.0.1",8080)) """ # connect 发起连接, ("127.0.0.1",8080)是服务端的IP和端口; # 客户端的connect对应服务端的accept(),connect()和服务端的accept()底层进行的就是TCP的“三次握手” # 服务端accept()之后,客户端的phone就相当于服务端的那个 conn,就是那根“电话线” """ print(phone) # 运行结果: # 服务端accept()之后客户端的phone就发生了变化,变得和服务端中的conn对应 # <socket.socket fd=300, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 62064), raddr=('127.0.0.1', 8080)> # 发、收消息;发收的消息都是bytes类型 phone.send("hello".encode("utf-8")) # 发 """ # 不能直接发字符串, 物理层传输的0101,这一步需要发送bytes类型; # 字符串转bytes: string.encode(编码格式) # phone.send() 对应服务端的 conn.recv() """ data = phone.recv(1024) # 收 print(data) # 关闭 phone.close() # 先启动服务端,再启动客户端,运行结果如下: # b'HELLO'
服务端代码如下:
import socket # 1. 买“手机” phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 基于网络通讯的、基于TCP协议的套接字;phone就是一个套接字对象 # 这一步得到一个服务端的套接字phone """ 全称是: phone = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM) socket.socket() # socket下面的socket类; family=socket.AF_INET # 地址家族(socket的类型)是基于网络通讯的AF_INET type=socket.SOCK_STREAM # 用的是流式的协议,即 TCP协议 """ # print(phone) # 打印结果 # <socket.socket fd=316, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0> # 2. 绑定“手机卡”(服务端的IP地址需要固定下来、并公示给别人;客户端虽然有IP和端口但是不需要绑定) phone.bind(("127.0.0.1",8080)) """ # 服务端需要绑定IP和Port(IP和端口),ip和端口需要以元祖的形式传进来; 其中第一个参数是字符串形式的IP地址; 127.0.0.1是指本机,专门用于测试的,IP写成这个就意味着服务端和客户端都必须在同一台主机上; 第二个参数是端口;端口范围是0-65535,其中0-1023是给操作系统使用的,2014以后的你可以使用 """ # 3. “开机” phone.listen(5) # 开始TCP监听 """ # 5代表最大挂起的链接数; 通常这个数写在配置文件中 # Enable a server to accept connections. If backlog is specified, it must be at least 0 (if it is lower, it is set to 0); it specifies the number of unaccepted connections that the system will allow before refusing new connections. If not specified, a default reasonable value is chosen. """ # 4. 等电话 # res = phone.accept() # print(res) # print(phone) """ # 等待链接; 等待的结果赋值给一个变量 # 服务端程序启动后,程序会停在这一步; # 服务端的accept()对应客户端的connect() # accept()底层建立的就是TCP的“三次握手”,“三次握手”之后会建成一个双向的链接(下面的conn),然后客户端得到一个对象(新的phone)、服务端得到一个对象(conn),这两个对象都可以收、发消息 """ # 客户端的程序启动后,服务端的程序也从 res = phone.accept()这一步接着往下运行 # 其中一次的运行结果: # (<socket.socket fd=356, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 56572)>, ('127.0.0.1', 56572)) # 元祖的形式,元祖里面有2个元素,第一个元素是发送端的链接对象(套接字对象),第二个元素是客户端的IP和端口 # <socket.socket fd=312, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080)> """ 由于phone.accept()得到的结果是元祖的形式,里面有两个元素:第一个、客户端的链接对象(相当于拨号人的电话线);第二个、客户端的IP和端口,所以phone.accept()可以写成如下形式 """ conn,client_addr = phone.accept() print(conn) # 打印结果: # <socket.socket fd=328, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 62064)> # 5. 收、发消息(基于刚刚建好的那根“电话线”(conn)收发消息),收发的消息都是bytes类型 data = conn.recv(1024) # 收 print("客户端的数据",data) """ # conn.recv(1024):接收conn这个发送端对象发来的数据(或者理解成沿着conn这根“电话线”接收消息) # 括号内的数字需要注意两个地方: 1. 数字单位:bytes;2. 数字2014代表最大接收1024个bytes # conn.recv(1024)接收到的数据赋值给变量 data """ conn.send(data.upper()) # 发 """ # conn.send(data.upper()):给conn的客户端发送消息(沿着conn这个“电话线”发送消息) # .upper() # 把字符串里面的都变成大写 """ # 挂电话(关闭) conn.close() # 关机 phone.close() # 运行结果: # 客户端的数据 b'hello' """ 1. 服务端有两种套接字对象:服务端的phone和conn 服务端的phone用于:绑定(IP和端口)、监听TCP和最重要的接收接收客户端的链接和客户端的IP、端口 conn用于收发消息 2. 客户端有一种套接字对象:客户端的phone(其实客户端的phone在服务端accept之后也发生了变化),它的作用是:发起建链接请求(.connect())和发、收消息 """
简单套接字加上通信循环:
把上面的代码加上 while True 就变成了循环通信,如下所示
客户端代码:
import socket phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.connect(("127.0.0.1",8080)) while True: msg = input(">>>").strip() phone.send(msg.encode("utf-8")) data = phone.recv(1024)
print(data) # 也是bytes形式
"""
如果想要打印正常的形式,可利用利用:
print(data.decode("utf-8)) """ phone.close()
服务端代码:
import socket phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.bind(("127.0.0.1",8080)) phone.listen(5) conn,client_addr = phone.accept() print(client_addr) while True: data = conn.recv(1024) # 在收到消息之前,程序也“卡”在这一步; 所以,recv()的具体含义是“等待接收消息” print("客户端的数据",data) conn.send(data.upper()) conn.close() phone.close()
重启服务端的时候可能出现端口仍然被占用的情况,原因是端口被操作系统回收需要时间,解决办法如下:
为服务端加一句代码:
import socket phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) # 在绑定之前加上这句代码; # reuseaddr表示重新用该端口 phone.bind(("127.0.0.1",8081)) phone.listen(5) conn,client_addr = phone.accept() data = conn.recv(1024) print("客户端的数据",data) conn.send(data.upper()) conn.close() print(phone) phone.close()
客户端和服务端代码bug修复:
客户端可以发空消息,但服务端却收不到空消息,如下代码:
客户端:
import socket phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.connect(("127.0.0.1",8080)) while True: msg = input(">>>").strip() """ 客户端可以发空数据,但是服务端却收不到空数据 解决客户端发空消息可以用如下代码: """ if not msg:continue # 如果发的消息为空,则重新发 phone.send(msg.encode("utf-8")) data = phone.recv(1024) print(data.decode("utf-8"))
"""
recv和send都是python(应该说是应用程序)发给操作系统的命令
收发消息需要通过Internet进行传输,而Internet需要通过网卡去发送、接收数据,只有操作系统才能调用网卡这个硬件
所以,具体执行发送、接收消息动作的是操作系统(就如文件处理中的open(file)一样),python(应用程序)把发送的消息的内存原封不动地复制给操作系统,然后操作系统去发送消息;
当客户端发送空消息时,应用程序会把这个空消息复制给操作系统,正常情况下操作系统会根据TCP协议调用网卡,但由于操作系统收到的是空消息,所以操作系统没有调用任何硬件,
也就是说,python(应用程序)发送的空消息只发到了客户端操作系统这一步,然后客户端的操作系统并没有接着往下发这个空消息;所以客户端的程序就卡在了这一步
"""
phone.close()
服务端:
import socket phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) phone.bind(("127.0.0.1",8080)) phone.listen(5) conn,client_addr = phone.accept() print(client_addr) while True: data = conn.recv(1024) print("客户端的数据",data) conn.send(data.upper()) conn.close() phone.close()
还有一种情况:以上面的代码为例, 由于conn是基于客户端和服务端建立起来的一个双向通道,假如客户端被强行终止掉了,那么这个双向通道conn就没有意义了;在Windows系统下,假如客户端被强行终止,那么服务端就会报错,但在Linux系统下,服务端不会报错,而是进入了while的死循环,为了防止Linux的这个死循环,可以利用如下方法解决:
import socket phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) phone.bind(("127.0.0.1",8080)) phone.listen(5) conn,client_addr = phone.accept() print(client_addr) while True: data = conn.recv(1024) if not data:break """ 由上面的分析可知:正常情况下服务端不可能收到空消息,因为假如客户端发了空消息,那么客户端的操作系统根本不会把这个空消息发出去; 所以,假如data变成了空消息,那一定是因为conn这个双向通道少了一方,也就是客户端单方面终止了; 所以 if not data:break # 就是说,假如客户端已经终止了,那就结束服务端的这个while True循环 """ print("客户端的数据",data) conn.send(data.upper()) conn.close() phone.close()
上述方法是针对Linux的;Windows下客户端当方面终止程序,服务端直接报错,所以应该用 try...except...去解决:
while True: try: data = conn.recv(1024) print("客户端的数据",data) conn.send(data.upper()) except ConnectionResetError: break conn.close() phone.close()
服务端为多个客户端提供服务:
服务端代码如下:
import socket phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) phone.bind(("127.0.0.1",8080)) phone.listen(5) """ 这个服务端可以为多个服务端服务,但同一时间只能服务于一个客户端; 当有其他服务端发来建链接请求时就挂起,当正在被服务的服务端退出后,挂起的其他服务端建链接的请求就会执行; 5为最大的挂起链接数 """ while True: # 链接循环 conn,client_addr = phone.accept() print(client_addr) while True: # 通讯循环 try: data = conn.recv(1024) print("客户端的数据",data) conn.send(data.upper()) except ConnectionResetError: break conn.close() phone.close()
模拟ssh远程执行命令:
关于系统命令的知识点补充:
# 一、系统命令: # windows: # dir # 查看某个文件夹下的子文件名和子文件夹名 # ipconfig # 查看本地网卡的IP信息 # tasklist # 查看运行的进程 # Linux系统对应的是: # ls # ifconfig # ps aux """ 系统命令不能直接在pycharm上写,而应该在cmd上输入(Windows系统); cmd也是一个程序,它的功能非常单一,就是来接收你输入的有特殊意义的单词(命令),然后把你输入的有特殊意义的单词(命令)解析成操作系统认识的指令去执行;所以这个程序称之为“命令解释器” 如: dir f;learning Linux系统中: / 代表c盘 """ # 二、执行系统命令: # 1、考虑使用os模块 # import os # os.system("dir f;learning") # 字符串形式的命令 # 但是这种方法是在服务端的终端上打印了dir f:learning 的子文件和子文件夹名;而我们想要的结果是把命令结果拿到客户端然后再客户端打印 """ res = os.system("dir f;learning") # res 只是 os.system("dir f:learning") 的执行状态结果:0或者非0(0代表命令执行成功),并不是命令的查看结果 """ # 执行系统命令,并拿到命令的结果 # 2. subprocess模块的Popen import subprocess obj = subprocess.Popen("dir f:learning",shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) # 命令的结果赋值给obj """ # 第一个参数是字符串格式的命令 要写: shell = True # shell是指命令解释器 # 启动一个程序来解析前面的字符串,把这个字符串解析成相应的命令去执行 # 相当于起了一个cmd 这个事例中,不管执行结果正确与否,命令的结果都只有一个,你没告诉subprocess把命令的结果给谁,它就把结果默认给了终端;但我们想要的是把命令的结果给客户端,而不是终端 所以我们需要通过某种手段告诉subprocess不要把结果给终端,而是把结果先存到一个地方,等我调用的时候发送给客户端,所以就用到了“管道”的概念; 把命令的结果放到一个管道里面(操作系统的内存),等你需要的时候再去管道里面取 让subprocess把结果放到管道里的方法: stdout = subprocess.PIPE # stdout是命令的正确执行结果 # 命令的正确执行结果放到一个管道里面 stderr = subprocess.PIPE # stderr是命令的错误执行结果 # 每次的 .PIPE都触发一次PIPE的功能,从而产生一个新的管道;so 这两个 PIPE是不一样的 """ print(obj) # 打印结果: # <subprocess.Popen object at 0x0000007994CEA8D0> print("stdout---->",obj.stdout.read()) # obj从stdout(正确结果)这个管道里面读 (从管道读取一次之后再取就没有了) # 打印结果:(bytes格式)(不管服务端还是客户端,收、发消息都得是bytes形式) # stdout----> b' xc7xfdxb6xafxc6xf7 F xd6xd0xb5xc4xbexedxc3xbbxd3xd0xb1xeaxc7xa9xa1xa3 xbexedxb5xc4xd0xf2xc1xd0xbaxc5xcaxc7 BCA5-0E10 f:\learning xb5xc4xc4xbfxc2xbc 2018/01/17 16:19 <DIR> . 2018/01/17 16:19 <DIR> .. 2018/01/12 01:04 <DIR> funny 2018/01/15 14:12 <DIR> IDLExd7xf7xd2xb5xb2xe2xcaxd4 2018/02/05 12:04 <DIR> pycharm_pro 2018/01/19 09:16 <DIR> pythontest 2018/03/10 11:05 <DIR> xd7xf7xd2xb5xccxe1xbdxbb 2018/03/08 11:45 <DIR> xb2xa9xbfxcdxa1xa2xb4xedxcexf3xa1xa2xd2xc9xcexcaxbdxd8xcdxbc 2018/01/27 18:01 <DIR> xbdxd8xcdxbc 2018/01/16 10:33 <DIR> xd7xd4xd1xa7 2018/01/11 14:53 <DIR> xc4xacxd0xb4 0 xb8xf6xcexc4xbcxfe 0 xd7xd6xbdxda 11 xb8xf6xc4xbfxc2xbc 115,108,585,472 xbfxc9xd3xc3xd7xd6xbdxda ' # obj.stdout.read()是bytes格式,如果想看bytes格式里面具体是什么内容,则需要 decode(); print("stdout---->",obj.stdout.read().decode("gbk")) """ encode()是按照什么编码,decode()也需要按照相应的编码; subprocess.Popen("dir f;learning")执行的是系统命令,这个命令是提交给操作系统的,由操作系统执行完后拿到一个结果; 由于没告诉操作系统命令的结果用什么格式编码,所以系统会用它默认的编码格式;所以: obj.stdout.read().decode("gbk") """ print("stderr--->",obj.stderr.read().decode("gbk")) # 执行结果不一定正确,所以也要从obj.stderr 读取 # 打印结果: # stdout----> # stderr--->
模拟ssh远程执行命令具体代码:
客户端:
import socket phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.connect(("127.0.0.1",8080)) while True: # 1. 发命令 cmd = input(">>>").strip() # 客户端在这行代码输入一条命令 if not cmd:continue phone.send(cmd.encode("utf-8")) # 2. 得到命令的结果,并打印 data = phone.recv(1024) # data是bytes格式,打印需要解码 # 1024是个坑,待优化 print(data.decode("gbk")) """ data 解码需要是gbk,因为:data是由服务端传来的(stdout+stderr),而stdout和stderr是由 subprocess.Popen()得到的 subprocess.Popen("命令")是把命令交给了操作系统去处理,操作系统处理命令后会按照自己默认的编码把处理结果encode,而Windows的默认编码是 gbk """ phone.close()
服务端:
import subprocess import socket phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) phone.bind(("127.0.0.1",8080)) phone.listen(5) while True: # 链接循环 conn,client_addr = phone.accept() while True: # 通讯循环 try: # 1. 接收命令 cmd = conn.recv(1024) # cmd是bytes格式 # 2. 执行命令,拿到执行后的结果 obj = subprocess.Popen(cmd.decode("utf-8"), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # subprocess.Popen()中需要的是字符串格式的命令,所以需要把cmd decode;由于客户端是按照utf-8进行的encode,所以这步需要decode("utf-8") stdout = obj.stdout.read() # obj.stdout需要read stderr = obj.stderr.read() # stderr和stdout都是bytes格式的 # 3. 把命令的结果返回给客户端 conn.send(stdout+stderr) # + 会影响效率;因为 + 是重新创建了一份stdout和stderr的新的内存空间(把stdout和stderr的内存空间重新copy了一遍)# 所以+是一个可以优化的点 except ConnectionResetError: break conn.close() phone.close()