TCP/IP协议簇
OSI中的层 功能 TCP/IP协议族
应用层 文件传输,电子邮件,文件服务,虚拟终端 TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet
表示层 数据格式化,代码转换,数据加密 没有协议
会话层 解除或建立与别的接点的联系 没有协议
传输层 提供端对端的接口 TCP,UDP
网络层 为数据包选择路由 IP,ICMP,RIP,OSPF,BGP,IGMP
数据链路层 传输有地址的帧以及错误检测功能 SLIP,CSLIP,PPP,ARP,RARP,MTU
物理层 以二进制数据形式在物理媒体上传输数据 ISO2110,IEEE802,IEEE802.2
ip地址分类
以C类为例,同一网络环境下,只能有254个机器,可以看到你插网线和切换wife你的ip就会变化,其中前三个代表网关,最后一位代表你的主机
这里表示的ipv4 最多有2的32次方,4G
那么外界怎么来区分哪些是网络号,哪些是主机号,就出现了子网掩码
如果不指定,就不知道哪些位是网络号、哪些是主机号,这就需要通过子网掩码来实现。
子网掩码不能单独存在,它必须结合IP地址一起使用。
子网掩码只有一个作用,就是将某个IP地址划分成网络地址和主机地址两部分子网掩码的设定必须遵循一定的规则。
与IP地址相同,子网掩码的长度也是32位,
- 左边是网络位,用二进制数字“1”表示;
- 右边是主机位,用二进制数字“0”表示。
假设IP地址为“192.168.1.1”子网掩码为“255.255.255.0”。
其中,“1”有24个,代表与此相对应的IP地址左边24位是网络号;
“0”有8个,代表与此相对应的IP地址右边8位是主机号。
这样,子网掩码就确定了一个IP地址的32位二进制数字中哪些是网络号、哪些是主机号。
这对于采用TCP/IP协议的网络来说非常重要,只有通过子网掩码,才能表明一台主机所在的子网与其他子网的关系,使网络正常工作。
Socket介绍
https://www.cnblogs.com/ruanbl/archive/2007/10/22/933430.html
在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的。
其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。
这样利用ip地址,协议,端口
就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互
UDP Socket网络调试Demo
首先Mac下面安装一个网络调试助手
然后写个UDP测试下即可
from socket import *
import os
#1. 创建套接字a
udpSocket = socket(AF_INET, SOCK_DGRAM)
#2. 准备接收方的地址
sendAddr = ('169.254.132.159', 8080)
# 绑定端口,ip自动分配
#
bindAddr = ('', 7788)
udpSocket.bind(bindAddr)
print(os.getpid())
#3. 从键盘获取数据
sendData = bytes(input("请输入要发送的数据:"),"utf-8")
#4. 发送数据到指定的电脑上
udpSocket.sendto(sendData, sendAddr)
#5. 等待接收对方发送的数据
recvData = udpSocket.recvfrom(1024) # 1024表示本次接收的最大字节数
#6. 显示对方发送的数据
print(recvData)
#5. 关闭套接字
udpSocket.close()
Python3下面运行起来,然后输入你要发送的信息,就可以在调试助手那里接收到消息了
UDP(用户数据包协议)
UDP --- 用户数据报协议,是一个无连接的简单的面向数据报的运输层协议。UDP不提供可靠性,它只是把应用程序传给IP层的数据报发送出去,但是并不能保证它们能到达目的地。由于UDP在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,故而传输速度很快。
UDP是一种面向无连接的协议,每个数据报都是一个独立的信息,包括完整的源地址或目的地址,它在网络上以任何可能的路径传往目的地,因此能否到达目的地,到达目的地的时间以及内容的正确性都是不能被保证的。
UDP是面向无连接的通讯协议,UDP数据包括目的端口号和源端口号信息,由于通讯不需要连接,所以可以实现广播发送。 UDP传输数据时有大小限制,每个被传输的数据报必须限定在64KB之内。 UDP是一个不可靠的协议,发送方所发送的数据报并不一定以相同的次序到达接收方。
【适用情况】
UDP是面向消息的协议,通信时不需要建立连接,数据的传输自然是不可靠的,UDP一般用于多点通信和实时的数据业务,比如
- 语音广播 只有udp才有广播
- 视频
- TFTP(简单文件传送)
- SNMP(简单网络管理协议)
- RIP(路由信息协议,如报告股票市场,航空信息)
- DNS(域名解释)
注重速度流畅
UDP操作简单,而且仅需要较少的监护,因此通常用于局域网高可靠性的分散系统中client/server应用程序。例如视频会议系统,并不要求音频视频数据绝对的正确,只要保证连贯性就可以了,这种情况下显然使用UDP会更合理一些。
可以理解为写信,每次写出去都需要写地址(接受方的信息ip port和协议),没有建立连接,不能保证对方一定能收到
1.应用层发送方发送数据例如 "hello world"
2.传输层里面有udp和tcp,包装一层目的端口和源端口
3.网络层包装一层ip地址
4.到达链路层包装一层Mac地址,然后硬件进行传输
5.接受方硬件接收到,链路层解开Mac地址匹配
6.网上传递到网络层,通过ip地址解析
7.继续传递到传输层,通过端口向上传递
8.最后通过端口在接收方的应用层找到对应的进程接收数据
Mac地址和ip地址到底是什么关系 可以看看知乎上的介绍,我个人认为Mac地址就是身份证,ip就是根据你的住址动态分配给你的地址
小结:
当地址总数不是很多的情况下,有了唯一地址就可以定位相互通信的主体,然而,当地址总是非常多时,高效的找出目标地址就非常关键。类似电话区号,都是地址层次性的体现。Mac地址和IP地址的区别就是只有IP地址具备有效的层次性。虽然Mac地址根据制造商等信息也有层次性,但是对网络查找没有一点卵用,因此IP才是寻址过程中实现分层的具体体现。可以理解为Mac是身份证,IP是你现在住的地址,IP能通过省市区等层次进行快速寻址,这也是我认为这两位最大的关系和区别
TCP(传输控制协议)
TCP服务器中
1.socket() 类似买了个手机 (创建一个套接字)
2.bind() 绑定手机卡(绑定ip和port)
3.listen() 开启来电提醒业务(使套接字变为可以被动链接)
4.accept() 等待别人打电话 (等待客户端的链接)
5.recv/send() 接收发送数据
TCP客户端中
1.socket() 找个通讯工具(电话亭)
2.connect() 直接拨打对方号码
3.三次握手,电话通了,传输数据
from socket import *
# socket()
serveSocker = socket(AF_INET,SOCK_STREAM)
# bind
serveSocker.bind(("192.168.1.47",8899))
# listen() # 使用socket创建的套接字默认的属性是主动的,使用listen将其变为被动的,这样就可以接收别人的链接了
serveSocker.listen(5)
# accept()
# 如果有新的客户端来链接服务器,那么就产生一个新的套接字专门为这个客户端服务器
# newSocket用来为这个客户端服务
# serveSocker就可以省下来专门等待其他新客户端的链接
newSocket,clientAddress = serveSocker.accept()
revData1 = newSocket.recvmsg(1024)
print("接收到的数据 ")
# 元祖类型
print(revData1)
print(clientAddress)
newSocket.send("我是服务器客服,请问需要什么服务".encode("utf-8"))
newSocket.close()
serveSocker.close()
上面就是一个简单的TCP Demo。通过流程图和代码可以简单看到一次流程的流转。首先Python写的服务端代码socket()创建的是服务器socket,还是通俗的记下流程,服务端的socket类似你拨打10086,,10086设置好之后一般就是等待用户的拨打,也就是accept,当用户拨打过来,通过三次握手,10086服务器socket会单独为这个用户安排一个客服人员(新的socket)进行连接,还返回了一些客户的信息,然后客户和这个单独开设的socket连接进行通信,10086服务器继续等待其他人员的来电,整个流程的退出就需要退出对应的socket,如果10086的服务端socket关闭之后,服务就没了
客户端代码
#coding=utf-8
from socket import *
# 创建socket
tcpClientSocket = socket(AF_INET, SOCK_STREAM)
# 链接服务器
serAddr = ('192.168.1.102', 7788)
tcpClientSocket.connect(serAddr)
# 提示用户输入数据
sendData = raw_input("请输入要发送的数据:")
tcpClientSocket.send(sendData)
# 接收对方发送过来的数据,最大接收1024个字节
recvData = tcpClientSocket.recv(1024)
print '接收到的数据为:',recvData
# 关闭套接字
tcpClientSocket.close()
下面是TCP服务器和多个客服端之间的简单Demo
服务器
from socket import *
from threading import Thread
def clientServe1(newSocket,address):
while True:
revData1 = newSocket.recvmsg(1024)
if len(revData1[0]) > 0:
print("收到数据--->%s",address)
print(revData1)
else:
print("个人服务结束-->%s",address)
break
newSocket.send("我是服务器的反馈".encode("utf-8"))
newSocket.close()
return
def main():
# socket()
serveSocker = socket(AF_INET,SOCK_STREAM)
# bind
serveSocker.bind(("192.168.1.47",8899))
# listen() # 使用socket创建的套接字默认的属性是主动的,使用listen将其变为被动的,这样就可以接收别人的链接了
serveSocker.listen(5)
# accept()
# 如果有新的客户端来链接服务器,那么就产生一个新的套接字专门为这个客户端服务器
# newSocket用来为这个客户端服务
# serveSocker就可以省下来专门等待其他新客户端的链接 服务器地址中的某一个新端口未单个线程服务
#
while True:
newSocket,clientAddress = serveSocker.accept()
client_thread = Thread(target=clientServe1,args=(newSocket,clientAddress))
client_thread.start()
serveSocker.close()
if __name__=="__main__":
main()
客服端
from socket import *
# socket()
clientSocket = socket(AF_INET,SOCK_STREAM)
clientSocket.connect(("192.168.1.47",8899))
while True:
sendData = input("请输入")
if len(sendData) > 0:
clientSocket.send(sendData.encode("utf-8"))
else:
break
revData = clientSocket.recvmsg(1024)
print(revData)
clientSocket.close()
上面运行起来服务器Demo,然后在写一个客户端,然后另一个用网络调试助手来建立,就能简单实现上面的数据传输Demo,多个客户端各自不干扰
TCP和UDP的代码可以看到,TCP有个connect三次握手的过程,而UDP没有,直接发送数据,因此TCP稳定,慢一点,但是UDP不稳定,快
网络通信原理和底层介绍分析
1.网页输入访问全过程介绍()
2.IP和Mac地址的区别
3.网络掩码作用(掩码和IP与,判断是否同一网段)
4.默认网关作用(如果发送的数据包IP地址,不在当前网段,就会发送到配置的默认网关上去)
5.DNS (域名解析 UDP包)
6.TCP三次握手和四次挥手
7.DHCP(自动分配IP)
8.ARP (IP查Mac) RARP(Mac查IP)
常见网络攻击
1.TCP半连接攻击
tcp半链接攻击也称为:SYN Flood (SYN洪水)
是种典型的DoS (Denial of Service,拒绝服务) 攻击
效果就是服务器TCP连接资源耗尽,停止响应正常的TCP连接请求
2.DNS劫持
劫持DNS服务器,这个就是作死。。。
下面的是DNS欺骗,DNS是UDP协议,可以伪造一个电脑接上交换机之后通过255的广播不停的发数据
3.arp中间人攻击
小知识 nslookup差点域名对应的IP
mintoudeMacBook-Pro:~ mintou$ nslookup baidu.com
Server: 172.17.255.242
Address: 172.17.255.242#53
Non-authoritative answer:
Name: baidu.com
Address: 123.125.115.110
Name: baidu.com
Address: 220.181.57.216
mintoudeMacBook-Pro:~ mintou$
并发服务器
1.单进程服务器
A:单进程服务器堵塞(serSocket.setblocking(True))
B:单进程服务器非堵塞(serSocket.setblocking(False))
C:单进程服务器select(跨平台,最大监听1024个套接字,轮询的方式检测,消耗CPU)
D:单进程服务器pool(解决上面select最大套接字的问题,无上限,一样是轮询检测)
E:单进程服务器epool(没有1024的限制,无上限,事件通知机制,底层有个容器存储接收到数据的socket,每次只要拿走容器进行检测即可,各自的socket会根据是否接收数据放入容器用来减少轮询时间)
2.多进程服务器 (多开进程,多了耗资源)
3.多线程服务器 (网络请求,如果高并发要实现,多线程会耗费CPU,因此就有了IO多路复用)
Select Demo
Select TCP服务器 C C++中也有,能完成IO多路复用(没有开辟多线程多进程的情况下能完成并发服务器(套接字的处理的)开发)
import select
import socket
import sys
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('', 7788))
server.listen(5)
inputs = [server, sys.stdin]
running = True
while True:
# 调用 select 函数,阻塞等待
readable, writeable, exceptional = select.select(inputs, [], [])
# 数据抵达,循环
for sock in readable:
# 监听到有新的连接
if sock == server:
conn, addr = server.accept()
# select 监听的socket
inputs.append(conn)
# 监听到键盘有输入
elif sock == sys.stdin:
cmd = sys.stdin.readline()
running = False
break
# 有数据到达
else:
# 读取客户端连接发送的数据
data = sock.recv(1024)
if data:
sock.send(data)
else:
# 移除select监听的socket
inputs.remove(sock)
sock.close()
# 如果检测到用户输入敲击键盘,那么就退出
if not running:
break
server.close()
epool Demo
import socket
import select
# 创建套接字
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# 设置可以重复使用绑定的信息
s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
# 绑定本机信息
s.bind(("",7788))
# 变为被动
s.listen(10)
# 创建一个epoll对象
epoll=select.epoll()
# 测试,用来打印套接字对应的文件描述符
# print s.fileno()
# print select.EPOLLIN|select.EPOLLET
# 注册事件到epoll中
# epoll.register(fd[, eventmask])
# 注意,如果fd已经注册过,则会发生异常
# 将创建的套接字添加到epoll的事件监听中
epoll.register(s.fileno(),select.EPOLLIN|select.EPOLLET)
connections = {}
addresses = {}
# 循环等待客户端的到来或者对方发送数据
while True:
# epoll 进行 fd 扫描的地方 -- 未指定超时时间则为阻塞等待
epoll_list=epoll.poll()
# 对事件进行判断
for fd,events in epoll_list:
# print fd
# print events
# 如果是socket创建的套接字被激活
if fd == s.fileno():
conn,addr=s.accept()
print('有新的客户端到来%s'%str(addr))
# 将 conn 和 addr 信息分别保存起来
connections[conn.fileno()] = conn
addresses[conn.fileno()] = addr
# 向 epoll 中注册 连接 socket 的 可读 事件
epoll.register(conn.fileno(), select.EPOLLIN | select.EPOLLET)
elif events == select.EPOLLIN:
# 从激活 fd 上接收
recvData = connections[fd].recv(1024)
if len(recvData)>0:
print('recv:%s'%recvData)
else:
# 从 epoll 中移除该 连接 fd
epoll.unregister(fd)
# server 侧主动关闭该 连接 fd
connections[fd].close()
print("%s---offline---"%str(addresses[fd]))
进程:是资源分配的单位
线程:CPU调度的单位
协程:进程或者协程里面把任务搞成了N份 微线程
计算密集型:例如大量for循环嵌套 大部分时间占用大量CPU 多进程实现(多线程实现的话相当于一个CPU高负荷运转)
IO密集型:网络请求 大部分时间都是等待 多线程和协程
总之,计算密集型程序适合C语言多线程,I/O密集型适合脚本语言开发的多线程。参考
协程是啥
首先我们得知道协程是啥?协程其实可以认为是比线程更小的执行单元。 为啥说他是一个执行单元,因为他自带CPU上下文。这样只要在合适的时机, 我们可以把一个协程 切换到另一个协程。 只要这个过程中保存或恢复 CPU上下文那么程序还是可以运行的。
通俗的理解:在一个线程中的某个函数,可以在任何地方保存当前函数的一些临时变量等信息,然后切换到另外一个函数中执行,注意不是通过调用函数的方式做到的,并且切换的次数以及什么时候再切换到原来的函数都由开发者自己确定
协程和线程差异
那么这个过程看起来比线程差不多。其实不然, 线程切换从系统层面远不止保存和恢复 CPU上下文这么简单。 操作系统为了程序运行的高效性每个线程都有自己缓存Cache等等数据,操作系统还会帮你做这些数据的恢复操作。 所以线程的切换非常耗性能。但是协程的切换只是单纯的操作CPU的上下文,所以一秒钟切换个上百万次系统都抗的住。
协程的问题
但是协程有一个问题,就是系统并不感知,所以操作系统不会帮你做切换。 那么谁来帮你做切换?让需要执行的协程更多的获得CPU时间才是问题的关键。
例子
目前的协程框架一般都是设计成 1:N 模式。所谓 1:N 就是一个线程作为一个容器里面放置多个协程。 那么谁来适时的切换这些协程?答案是有协程自己主动让出CPU,也就是每个协程池里面有一个调度器, 这个调度器是被动调度的。意思就是他不会主动调度。而且当一个协程发现自己执行不下去了(比如异步等待网络的数据回来,但是当前还没有数据到), 这个时候就可以由这个协程通知调度器,这个时候执行到调度器的代码,调度器根据事先设计好的调度算法找到当前最需要CPU的协程。 切换这个协程的CPU上下文把CPU的运行权交个这个协程,直到这个协程出现执行不下去需要等等的情况,或者它调用主动让出CPU的API之类,触发下一次调度。
那么这个实现有没有问题?
其实是有问题的,假设这个线程中有一个协程是CPU密集型的他没有IO操作, 也就是自己不会主动触发调度器调度的过程,那么就会出现其他协程得不到执行的情况, 所以这种情况下需要程序员自己避免。这是一个问题,假设业务开发的人员并不懂这个原理的话就可能会出现问题。
协程的好处
在IO密集型的程序中由于IO操作远远慢于CPU的操作,所以往往需要CPU去等IO操作。 同步IO下系统需要切换线程,让操作系统可以在IO过程中执行其他的东西。 这样虽然代码是符合人类的思维习惯但是由于大量的线程切换带来了大量的性能的浪费,尤其是IO密集型的程序。
所以人们发明了异步IO。就是当数据到达的时候触发我的回调。来减少线程切换带来性能损失。 但是这样的坏处也是很大的,主要的坏处就是操作被 “分片” 了,代码写的不是 “一气呵成” 这种。 而是每次来段数据就要判断 数据够不够处理哇,够处理就处理吧,不够处理就在等等吧。这样代码的可读性很低,其实也不符合人类的习惯。
但是协程可以很好解决这个问题。比如 把一个IO操作 写成一个协程。当触发IO操作的时候就自动让出CPU给其他协程。要知道协程的切换很轻的。 协程通过这种对异步IO的封装 既保留了性能也保证了代码的容易编写和可读性。在高IO密集型的程序下很好。但是高CPU密集型的程序下没啥好处。
协程一个简单实现
import time
def A():
while True:
print("----A---")
yield
time.sleep(0.5)
def B(c):
while True:
print("----B---")
c.next()
time.sleep(0.5)
if __name__=='__main__':
a = A()
B(a)
Greenlet 手动版本 https://github.com/python-greenlet/greenlet
Gevent 自动版本 https://github.com/gevent/gevent
HTTP请求流程
步骤1:浏览器首先向服务器发送HTTP请求,请求包括:
方法:GET还是POST,GET仅请求资源,POST会附带用户数据;
路径:/full/url/path;
域名:由Host头指定:Host: www.sina.com
以及其他相关的Header;
如果是POST,那么请求还包括一个Body,包含用户数据
步骤2:服务器向浏览器返回HTTP响应,响应包括:
响应代码:200表示成功,3xx表示重定向,4xx表示客户端发送的请求有错误,5xx表示服务器端处理时发生了错误;
响应类型:由Content-Type指定;
以及其他相关的Header;
通常服务器的HTTP响应会携带内容,也就是有一个Body,包含响应的内容,网页的HTML源码就在Body中。
步骤3:如果浏览器还需要继续向服务器请求其他资源,比如图片,就再次发出HTTP请求,重复步骤1、2。
Web采用的HTTP协议采用了非常简单的请求-响应模式,从而大大简化了开发。当我们编写一个页面时,我们只需要在HTTP请求中把HTML发送出去,不需要考虑如何附带图片、视频等,浏览器如果需要请求图片和视频,它会发送另一个HTTP请求,因此,一个HTTP请求只处理一个资源(此时就可以理解为TCP协议中的短连接,每个链接只获取一个资源,如需要多个就需要建立多个链接)
HTTP协议同时具备极强的扩展性,虽然浏览器请求的是http://www.sina.com
的首页,但是新浪在HTML中可以链入其他服务器的资源,比如<img src="http://i1.sinaimg.cn/home/2013/1008/U8455P30DT20131008135420.png">
,从而将请求压力分散到各个服务器上,并且,一个站点可以链接到其他站点,无数个站点互相链接起来,就形成了World Wide Web,简称WWW。
HTTP格式
每个HTTP请求和响应都遵循相同的格式,一个HTTP包含Header和Body两部分,其中Body是可选的。
HTTP协议是一种文本协议,所以,它的格式也非常简单。
HTTP GET请求的格式:
GET /path HTTP/1.1
Header1: Value1
Header2: Value2
Header3: Value3
每个Header一行一个,换行符是\r\n。
HTTP POST请求的格式:
POST /path HTTP/1.1
Header1: Value1
Header2: Value2
Header3: Value3
body data goes here...
当遇到连续两个\r\n时,Header部分结束,后面的数据全部是Body。
HTTP响应的格式:
200 OK
Header1: Value1
Header2: Value2
Header3: Value3
body data goes here...
HTTP响应如果包含body,也是通过\r\n\r\n来分隔的。
请再次注意,Body的数据类型由Content-Type头来确定,如果是网页,Body就是文本,如果是图片,Body就是图片的二进制数据。
当存在Content-Encoding时,Body数据是被压缩的,最常见的压缩方式是gzip,所以,看到Content-Encoding: gzip时,需要将Body数据先解压缩,才能得到真正的数据。压缩的目的在于减少Body的大小,加快网络传输。
这里有个POST提交的四种编码方式 POST四种方式