什么是网络
大家天天网上冲浪,对网络这个词肯定不会陌生,我们现在的生活中离不开网络,我们用微信等聊天软件,如果没有网络那只能自己跟自己聊天了......
如何能大家一起愉快的聊天呢?那就是联网了,即internet
然而internet为何物?
举一个简单的例子:如果把一个人和这个人的手机比作一台计算机,那这个人怎么与其他人进行沟通呢?两个人之间想要打电话首先一点必须是接电话线,这就好比是计算机之间的通信首先要有物理链接介质,比如网线,交换机,路由器等网络设备。
通信的线路建好之后,只是物理层面有了可以承载数据的介质,要想通信,还需要我们按照某种规则组织我们的数据,这样对方在接收到数据后就可以按照相同的规则去解析出数据,这里说的规则指的就是:中国有很多地区,不同的地区有不同的方言,为了全中国人都可以听懂,大家统一讲普通话
普通话属于中国国内人与人之间通信的标准,那如果是两个国家的人交流呢?
我们是不是也应该让两个国家的人说一种语言,于是有了世界统一的通信标准:英语
英语成为世界上所有人通信的统一标准,计算机之间的通信也应该有一个像英语一样的通信标准,这个标准称之为互联网协议, 可以很明确地说:互联网协议就是计算机界的英语,网络就是物理链接介质+互联网协议。
互联网协议
协议这个名词不仅局限于互联网范畴,也体现在日常生活中,比如情侣双方约定好在哪个地点吃饭,这个约定也是一种协议,比如你应聘成功了,企业会和你签订劳动合同,这种双方的雇佣关系也是一种 协议。注意自己一个人对自己的约定不能成为协议,协议的前提条件必须是多人约定。
那么网络协议是什么呢?
网络协议就是网络中(包括互联网)传递、管理信息的一些规范。如同人与人之间相互交流是需要遵循一定的规矩一样,计算机之间的相互通信需要共同遵守一定的规则,这些规则就称为网络协议。
没有网络协议的互联网是混乱的,就和人类社会一样,人不能想怎么样就怎么样,你的行为约束是受到法律的约束的;那么互联网中的端系统也不能自己想发什么发什么,也是需要受到通信协议约束的。
按照功能不同,人们将互联网协议分为OSI七层或TCP/IP五层或TCP/IP四层(我们只需要掌握tcp/ip五层协议即可),这种分层就好比是学习的几个阶段,每个阶段应该掌握专门的技能或者说完成特定的任
什么是TCP/IP?
TCP/IP 协议是我们程序员接触最多的协议,实际上,TCP/IP 又被称为 TCP/IP 协议簇,它并不特指单纯的 TCP 和 IP 协议,而是容纳了许许多多的网络协议。
然而,TCP/IP协议并不是国际官方组织制定的标准,而是民间组织(一些大型国际厂商、高等院校)自行商定的标准,因为更简便,推广力度更大,而成为了事实上的标准。
TCP/IP五层模型讲解
为了给网络协议的设计提供一个结构,网络设计者以分层的方式组织协议,每个协议属于层次模型之一,每一层都是向上提供服务。
OSI 模型共有七层,从下到上分别是物理层、数据链路层、网络层、运输层、会话层、表示层和应用层。但是这显然是有些复杂的。
我们将应用层,表示层,会话层并作应用层,从TCP/IP五层协议的角度来阐述每层的由来与功能,搞清楚了每层的主要协议,就理解了整个互联网通信的原理。
物理层
一台计算机与另一台计算机要进行通信,第一件要做的事是什么?当然是要把这台计算机与另外的其他计算机连起来啊,这样,我们才能把数据传输过去。例如可以通过光纤啊,电缆啊,双绞线啊等介质把他们连接起来,然后才能进行通信。
物理层的协议使用链路层协议,这些协议与实际的物理传输介质有关
物理层功能:主要是基于电器特性发送高低电压(电信号),高电压对应数字1,低电压对应数字0
数据链路层
数据链路层由来:单纯的电信号0和1没有任何意义,必须规定电信号多少位一组,每组什么意思
数据链路层的功能:定义了电信号的分组方式
以太网协议:早期的时候各个公司都有自己的分组方式,后来形成了统一的标准,即以太网协议ethernet
ethernet规定
- 一组电信号构成一个数据包,叫做‘帧’
- 每一数据帧分成:报头head和数据data两部分
- head包含:(固定18个字节)
- 发送者/源地址,6个字节
- 接收者/目标地址,6个字节
- 数据类型,6个字节
- data包含:(最短46字节,最长1500字节)
- head包含:(固定18个字节)
head中包含的源和目标地址由来:ethernet规定接入internet的设备都必须具备网卡,发送端和接收端的地址便是指网卡的地址,即mac地址
mac地址:每块网卡出厂时都被烧制上一个世界唯一的mac地址
PC1想要给PC4发送数据,是以广播的方式发送以太网包给PC4,然而PC3,PC2都会收到,拆开数据包后发现目标mac地址如果不是自己就丢弃,如果是自己就响应
广播:有了mac地址,同一网络内的两台主机就可以通信了
网络层
网络层由来:有了ethernet、mac地址、广播的发送方式,世界上的计算机就可以彼此通信了,问题是世界范围的互联网是由
一个个彼此隔离的小的局域网组成的,那么如果所有的通信都采用以太网的广播方式,那么一台机器发送的包全世界都会收到,这就不仅仅是效率低的问题了,这会是一种灾难
必须找出一种方法来区分哪些计算机属于同一广播域,哪些不是,如果是就采用广播的方式发送,如果不是,就采用路由的方式
网络层功能:引入一套新的地址用来区分不同的广播域/子网,这套地址即网络地址
IP协议:
规定网络地址的协议叫ip协议,它定义的地址称之为ip地址,广泛采用的v4版本即ipv4,它规定网络地址由32位2进制表示,范围0.0.0.0-255.255.255.255
IP地址分类:IP地址根据网络ID的不同分为5种类型,A类地址、B类地址、C类地址、D类地址和E类地址。
子网掩码
所谓”子网掩码”,就是表示子网络特征的一个参数。它在形式上等同于IP地址,也是一个32位二进制数字,它的网络部分全部为1,主机部分全部为0。
子网掩码是用来标识一个IP地址的哪些位是代表网络位,以及哪些位是代表主机位。子网掩码不能单独存在,它必须结合IP地址一起使用。子网掩码只有一个作用,就是将某个IP地址划分成网络地址和主机地址两部分。
我要区分网络位和主机位做什么?
这就像寄信,你给你的南方姑娘寄信,她肉身在厦门,详细地址是厦门鼓浪屿三街27号,那网络位就相当于城市,详细地址就是主机位,网络位帮你定位到城市,主机位帮你找到你的南方姑娘。 路由器通过子网掩码来确定哪些是网络位,哪些是主机位
IP地址分类
IP地址根据网络ID的不同分为5种类型,A类地址、B类地址、C类地址、D类地址和E类地址。
A类IP地址:一个A类IP地址由1字节的网络地址和3字节主机地址组成,网络地址的最高位必须是“0”, 地址范围从1.0.0.0 到126.0.0.0。可用的A类网络有126个,每个网络能容纳1亿多个主机。
B类IP地址 :一个B类IP地址由2个字节的网络地址和2个字节的主机地址组成,网络地址的最高位必须是“10”,地址范围从128.0.0.0到191.255.255.255。可用的B类网络有16382个,每个网络能容纳6万多个主机 。
C类IP地址:一个C类IP地址由3字节的网络地址和1字节的主机地址组成,网络地址的最高位必须是“110”。范围从192.0.0.0到223.255.255.255。C类网络可达209万余个,每个网络能容纳254个主机。
D类地址用于多点广播(Multicast): D类IP地址第一个字节以“lll0”开始,它是一个专门保留的地址。 它并不指向特定的网络,目前这一类地址被用在多点广播(Multicast)中。多点广播地址用来一次寻址一组计算机,它标识共享同一协议的一组计算机。
E类IP地址 以“llll0”开始,为将来使用保留。
传输层
传输层的由来:网络层的ip帮我们区分子网,以太网层的mac帮我们找到主机,然后大家使用的都是应用程序,你的电脑上可能同时开启qq,暴风影音,迅雷等多个应用程序。
那么我们通过ip和mac找到了一台特定的主机,如何标识这台主机上的应用程序呢?答案就是端口,端口即应用程序与网卡关联的编号。
传输层功能:建立端口到端口的通信
补充:端口范围0-65535,0-1023为系统占用端口
传输层有两种协议,TCP和UDP
- TCP 是面向连接的、可靠的流协议。流就是指不间断的数据结构,当应用程序采用 TCP 发送消息时,虽然可以保证发送的顺序,但还是犹如没有任何间隔的数据流发送给接收端。TCP 为提供可靠性传输,实行“顺序控制”或“重发控制”机制。此外还具备“流控制(流量控制)”、“拥塞控制”、提高网络利用率等众多功能。
- UDP 是不具有可靠性的数据报协议。细微的处理它会交给上层的应用去完成。在 UDP 的情况下,虽然可以确保发送消息的大小,却不能保证消息一定会到达。因此,应用有时会根据自己的需要进行重发处理。
- TCP 和 UDP 的优缺点无法简单地、绝对地去做比较:TCP 用于在传输层有必要实现可靠传输的情况;而在一方面,UDP 主要用于那些对高速传输和实时性有较高要求的通信或广播通信。TCP 和 UDP 应该根据应用的目的按需使用
TCP/IP五层模型讲解:参考文章
Socket介绍
通过前面的介绍我们已经知道,假设我现在要写一个程序,给另一台计算机发数据,必须通过tcp/ip协议 ,但具体的实现过程是什么呢?我应该怎么操作才能把数据封装成tcp/ip的包,又执行什么指令才能把数据发到对端机器上呢?
此时,socket隆重登场,socket是一种抽象层,应用程序通过它来发送和接受数据,使用Socket可以将应用程序添加到网络中,与处于同一网络中的其他应用程序进行通信。
简单来说,Socket提供了程序内部与外界通信的端口并为通信双方提供数据传输通道
什么是socket?
socket又称"套接字",应用程序通常通过"套接字"向网络发出请求或者应答网络请求,使主机间或者一台计算机上的进程间可以通讯。白话说,socket就是两个节点为了互相通信,而在各自家里装的一部'电话'。
我们知道两台计算机之间数据传输,要通过网卡。网卡归谁管?操作系统。 所以说我写的程序,只要把数据给操作系统,操作系统调用网卡去发就好了。
我的程序怎么把数据给操作系统呢?
回想一下,我程序要打开存于硬盘上的文件,是不是调用file就可以了,file是不是就是我程序和操作系统之间的一个接口呢?
同样的道理,socket也是这么一个接口,用于程序和操作系统之间,进行网络数据收发的接口。在面向过程的语言中,socket是一个函数,在面向对象的语言中,socket是一个class,无论哪样,都是程序和操作系统之间的一个接口。
在调用socket时,我们是需要指定协议的,如果指定tcp,那么这个socket就用tcp跟对方通信,如果指定udp,那么socket就用udp跟对方通信
UDP发送与接收程序
UDP发送数据
import socket
def main():
# 创建一个udp套接字
udp_socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
# 可以使用socket收发数据
# 发送的内容(是字节不是字符串)
udp_socket.sendto(b'nihao',('192.168.0.162',8080))
# 关闭socket
udp_socket.close()
if __name__ == '__main__':
main()
网络调试助手接收数据
发送任意的数据给网络调试助手,优化程序
import socket
def main():
udp_socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
while True:
send_data = input("请输入要发送的数据:")
if send_data == 'q':
break
# 字符串转字节
send_data = send_data.encode('utf-8')
udp_socket.sendto(send_data,('192.168.0.162',8080))
# 关闭连接
udp_socket.close()
if __name__ == '__main__':
main()
UDP接收数据
- 1 创建套接字
- 2 绑定本地信息(IP和端口)
- 3 接受数据
- 4 打印数据
- 5 关闭套接字
import socket
def main():
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 绑定本地的相关信息,如果一个网络程序不绑定,则系统会随机分配
local_addr = ('', 7789) # ip地址和端口号,ip一般不用写,表示本机的任何一个ip
udp_socket.bind(local_addr)
while True:
# recvfrom 表示接收数据
recv_data = udp_socket.recvfrom(1024) # 1024表示本次接收的最大字节数
# 是一个元组(接收到的数据,(发送方的ip,port))
print(recv_data)
recv = recv_data[0]
send_addr = recv_data[1]
# 如果用utf-8程序报错,Windows里面默认编码是gbk
# print("%s:%s"%(str(send_addr),recv.decode('utf-8')))
print("%s:%s"%(str(send_addr),recv.decode('gbk')))
udp_socket.close()
if __name__ == '__main__':
main()
UDP聊天器
import socket
def send_data(udp_socket):
""" 先完成发送数据,测试 """
send_data = input("请输入要发送的数据:")
dest_ip = input("请输入要发送的IP:")
dest_port = int(input("请输入要发送的端口:"))
# 发送数据
udp_socket.sendto(send_data.encode('utf-8'), (dest_ip, dest_port))
def recv_data(udp_socket):
""" 接收数据 """
recv_data = udp_socket.recvfrom(1024) # 程序到这里会阻塞
print("%s:%s" % (recv_data[1], recv_data[0].decode()))
def main():
# 创建套接字 套接字是可以同时收发数据的
udp_socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
udp_socket.bind(("",7788))
while True:
send_data(udp_socket)
recv_data(udp_socket)
if __name__ == '__main__':
main()
TCP发送接收数据
TCP分为客户端和服务端
- 客户端指运行在用户设备上的程序
- 服务端指运行在服务器设备上的程序,专门为客户端提供数据服务
TCP客户端构建流程
- 1.创建socket
- 2.链接服务器
- 3.接收/发送数据(最大接收1024个字节)
- 4.关闭套接字
import socket
def main():
tcp_client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# server_ip = input("server ip:")
# server_port = int(input("server port:"))
# server_addr = (server_ip,server_port)
# tcp_client.connect(server_addr)
tcp_client.connect(("192.168.0.162",8080))
send_data = input("发送的数据:")
# 这里用send,因为已经连接过服务器
tcp_client.send(send_data.encode())
recvData = tcp_client.recv(1024)
print('接收到的数据为:', recvData.decode('gbk')) # 这里也可以改成utf-8
tcp_client.close()
if __name__ == '__main__':
main()
TCP服务端
如果想让别人能更够打通咱们的电话获取相应服务的话,需要做以下几件事情:
- 1 买个手机
- 2 插上手机卡
- 3 设计手机为正常接听状态
- 4 等待别人拨打电话
对应socket流程
- 1 socket创建套接字
- 2 bind绑定IP和port (不能让客户端,每次都去问)
- 3 listen使套接字变为可以被动链接
- 4 accept等待客户端的链接
- 5 recv/send接收发送数据
import socket
def main():
# 创建套接字
tcp_server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# 绑定本地信息
tcp_server.bind(("",7890))
# 让默认的套接字由主动变为被动,这样就可以接收别人的链接了 创建出来套接字默认是链接别人,而不是别人链接你
tcp_server.listen(128)
# 等待客户端的链接 程序会阻塞
# new_client_socket 用来为这个客户端服务
# tcp_server就可以省下来服务其他新客户端的链接
new_client_socket,client_addr = tcp_server.accept()
# 客户端的IP和端口
print(client_addr)
# 接受客户端发送过来的数据
# 对于recvfrom 可同时应用于面向连接的和无连接的套接字。
# recv一般只用在面向连接的套接字,几乎等同于recvfrom,
recv_data = new_client_socket.recv(1024)
print(recv_data)
# 回数据给客户端
new_client_socket.send("haha".encode('utf-8'))
# 关闭客户端
new_client_socket.close()
tcp_server.close()
if __name__ == '__main__':
main()
循环为多个客户端服务
import socket
def main():
# 创建套接字
tcp_server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# 绑定本地信息
tcp_server.bind(("",7891))
# 1.为多个客户端服务
while True:
print("等待新的客户端来连接.....")
# 让默认的套接字由主动变为被动
tcp_server.listen(128)
# 等待客户端的链接
new_client_socket,client_addr = tcp_server.accept()
# 客户端的IP和端口
print(client_addr)
# 2.循环为一个客户端服务多次
while True:
# 接受客户端发送过来的数据
recv_data = new_client_socket.recv(1024)
print(recv_data.decode('gbk'))
# 如果recv解堵塞,2种方式
# 1.客户端发送过来数据
# 2.客户端调用close,导致recv解堵塞
if recv_data:
# 回数据给客户端
new_client_socket.send("haha".encode('utf-8'))
else:
break
# 关闭客户端 意味着不会为这个客户端服务
new_client_socket.close()
print("已经服务完毕")
# 如果将监听套接字关闭了,那么会导致 不能再次等待新客户端的到来
tcp_server.close()
if __name__ == '__main__':
main()
socketserver
我们上面的案例虽然可以为多个客户端服务,但是不能同时为多个客户端服务,那有没有什么本法可以解决呢?
是可以通过socketserver
模块来实现并发,同时为多个客户端服务器
import socketserver
class MyServer(socketserver.BaseRequestHandler):
def handle(self):
print(self.request)
conn = self.request
while True:
conn.send(b'hello')
print(conn.recv(1024))
server = socketserver.ThreadingTCPServer(('192.168.100.158', 9003), MyServer)
server.serve_forever()
这是一个简易版的socketserver
并发,我们可以稍微完善一下代码
import socketserver
# 自定义一个处理通信的类,继承BaseRequestHandler
class MyRequestHandle(socketserver.BaseRequestHandler):
# 重写父类的handle方法,该方法实现通信循环
def handle(self):
# 如果tcp协议,self.request => conn
# 如果tcp协议,self.client_address => client_addr
while True:
try:
msg = self.request.recv(1024)
if len(msg) == 0: break
self.request.send(msg.upper())
except Exception:
break
self.request.close()
s = socketserver.ThreadingTCPServer(('127.0.0.1',8889),MyRequestHandle)
# server_forever() 不停的循环等到那客户端请求连接
s.serve_forever()
开发自己的静态Web服务器
实现步骤:
- 编写一个TCP服务端程序
- 获取浏览器发送的http请求报文数据
- 读取固定页面数据,把页面数据组装成HTTP响应报文数据发送给浏览器。
- HTTP响应报文数据发送完成以后,关闭服务于客户端的套接字。
import socket
if __name__ == '__main__':
# 创建tcp服务端套接字
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置端口号复用, 程序退出端口立即释放
tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# 绑定端口号
tcp_server_socket.bind(("192.168.100.79", 9000))
# 设置监听
tcp_server_socket.listen(128)
while True:
# 等待接受客户端的连接请求
new_socket, ip_port = tcp_server_socket.accept()
# 代码执行到此,说明连接建立成功
recv_client_data = new_socket.recv(4096)
# 对二进制数据进行解码
recv_client_content = recv_client_data.decode("utf-8")
print(recv_client_content)
with open("static/index.html", "r") as file:
# 读取文件数据
file_data = file.read()
# 把数据封装成http 响应报文格式的数据
# 响应报文
# 响应行
response_line = "HTTP/1.1 200 OK\r\n"
# 响应头
response_header = "Server: PWS1.0\r\n"
# 空行
# 响应体
response_body = file_data
# 拼接响应报文
response_data = response_line + response_header + "\r\n" + response_body
# 发送数据
new_socket.send(response_data.encode('utf-8'))
# 关闭服务与客户端的套接字
new_socket.close()
返回指定页面数据
目前的Web服务器,不管用户访问什么页面,返回的都是固定页面的数据,接下来需要根据用户的请求返回指定页面的数据
返回指定页面数据的实现步骤:
- 获取用户请求资源的路径
- 根据请求资源的路径,读取指定文件的数据
- 组装指定文件数据的响应报文,发送给浏览器
- 判断请求的文件在服务端不存在,组装404状态的响应报文,发送给浏览器
import socket
def main():
# 创建tcp服务端套接字
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置端口号复用, 程序退出端口立即释放
tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# 绑定端口号
tcp_server_socket.bind(("192.168.100.79", 9000))
# 设置监听
tcp_server_socket.listen(128)
while True:
# 等待接受客户端的连接请求
new_socket, ip_port = tcp_server_socket.accept()
# 代码执行到此,说明连接建立成功
recv_client_data = new_socket.recv(4096)
# 用网络调试助手,连接上,关闭 此时收不到数据,下面的切割就会报错
if len(recv_client_data) == 0:
print("关闭浏览器了")
new_socket.close()
# return
continue
# 对二进制数据进行解码
recv_client_content = recv_client_data.decode("utf-8")
# print(recv_client_content)
# 根据指定字符串进行分割, 最大分割次数指定2 , maxsplit=2
request_list = recv_client_content.split(" ")
print(request_list)
# 获取请求资源路径
request_path = request_list[1]
# 判断请求的是否是根目录,如果条件成立,指定首页数据返回
if request_path == "/":
request_path = "/index.html"
try:
# 动态打开指定文件, 使用rb模式, 兼容图片文件
with open("static" + request_path, "rb") as file:
# 读取文件数据
file_data = file.read()
except Exception as e:
# 请求资源不存在,返回404数据
# 响应行
response_line = "HTTP/1.1 404 Not Found\r\n"
# 响应头
response_header = "Server: PWS1.0\r\n"
with open("static/error.html", "rb") as file:
file_data = file.read()
# 响应体
response_body = file_data
# 拼接响应报文 \r\n 是空行
response_data = (response_line + response_header + "\r\n").encode("utf-8") + response_body
# 发送数据
new_socket.send(response_data)
else:
# 没有异常
# 响应行
response_line = "HTTP/1.1 200 OK\r\n"
# 响应头
response_header = "Server: PWS1.0\r\n"
# 响应体
response_body = file_data
# 拼接响应报文
response_data = (response_line + response_header + "\r\n").encode("utf-8") + response_body
# 发送数据
new_socket.send(response_data)
finally:
# 无论是否有异常,都会执行的代码
# 关闭服务与客户端的套接字
new_socket.close()
if __name__ == '__main__':
main()