协程
协程,又称微线程,纤程。英文名Coroutine。
协程其实可以认为是比线程更小的执行单元。 为啥说他是一个执行单元,因为他自带CPU上下文。这样只要在合适的时机, 我们可以把一个协程 切换到另一个协程。 只要这个过程中保存或恢复 CPU上下文那么程序还是可以运行的。
通俗的理解:在一个线程中的某个函数,可以在任何地方保存当前函数的一些临时变量等信息,然后切换到另外一个函数中执行,注意不是通过调用函数的方式做到的,并且切换的次数以及什么时候再切换到原来的函数都由开发者自己确定
协程和线程差异
线程切换从系统层面远不止保存和恢复 CPU上下文这么简单。 操作系统为了程序运行的高效性每个线程都有自己缓存Cache等等数据,操作系统还会帮你做这些数据的恢复操作。 所以线程的切换非常耗性能。但是协程的切换只是单纯的操作CPU的上下文,所以一秒钟切换个上百万次系统都抗的住。
协程问题
系统并不感知,所以操作系统不会帮你做切换。 那么谁来帮你做切换?让需要执行的协程更多的获得CPU时间才是问题的关键。
例子
目前的协程框架一般都是设计成 1:N 模式。所谓 1:N 就是一个线程作为一个容器里面放置多个协程。 那么谁来适时的切换这些协程?答案是有协程自己主动让出CPU,也就是每个协程池里面有一个调度器, 这个调度器是被动调度的。意思就是他不会主动调度。而且当一个协程发现自己执行不下去了(比如异步等待网络的数据回来,但是当前还没有数据到), 这个时候就可以由这个协程通知调度器,这个时候执行到调度器的代码,调度器根据事先设计好的调度算法找到当前最需要CPU的协程。 切换这个协程的CPU上下文把CPU的运行权交个这个协程,直到这个协程出现执行不下去需要等等的情况,或者它调用主动让出CPU的API之类,触发下一次调度。
这个实现有一个问题:假设这个线程中有一个协程是CPU密集型的他没有IO操作, 也就是自己不会主动触发调度器调度的过程,那么就会出现其他协程得不到执行的情况, 所以这种情况下需要程序员自己避免。
# 协程简单例子
from time import sleep
def A():
a = 1
while a<4:
print('A a = {}'.format(a))
yield a
a += 1
sleep(0.3)
def B(c):
while True:
try:
print('B'.center(20, '*'))
next(c)
sleep(0.3)
except StopIteration:
break
if __name__ == '__main__':
a = A()
B(a)
*********B**********
A a = 1
*********B**********
A a = 2
*********B**********
A a = 3
*********B**********
# greenlet 版协程
# -*- coding: utf-8 -*-
from greenlet import greenlet
from time import sleep
def A():
a = 1
while a<4:
print('A a = {}'.format(a).center(20, '*'))
gr2.switch() # 切换至B()
a += 1
sleep(0.3)
def B():
b = 1
while True:
print('B'.center(20, '*'))
gr1.switch()
sleep(0.3)
gr1 = greenlet(A)
gr2 = greenlet(B)
gr1.switch()
******A a = 1*******
*********B**********
******A a = 2*******
*********B**********
******A a = 3*******
*********B**********
'''grevent:当遇到IO时,自动切换'''
# 使用例子
# -*- coding: utf-8 -*-
import gevent
def test(n):
for i in range(n):
print('this is "{}" at NO.{}'.format(gevent.getcurrent(), i))
g1 = gevent.spawn(test, 3)
g2 = gevent.spawn(test, 3)
g1.join()
g2.join()
this is "" at NO.0
this is "" at NO.1
this is "" at NO.2
this is "" at NO.0
this is "" at NO.1
this is "" at NO.2
# gevent 切换执行
# -*- coding: utf-8 -*-
import gevent
def test(n):
for i in range(n):
print('this is "{}" at NO.{}'.format(gevent.getcurrent(), i))
gevent.sleep(0.3)
g1 = gevent.spawn(test, 3)
g2 = gevent.spawn(test, 3)
g1.join()
g2.join()
this is "" at NO.0
this is "" at NO.0
this is "" at NO.1
this is "" at NO.1
this is "" at NO.2
this is "" at NO.2
asyncio、async/await、aiohttp
参考自廖雪峰的Python教程,不在此赘述,下文给出参考网址
asyncio
async/await
aiohttp
网络编程
使用网络能够把多方链接在一起,然后可以进行数据传递
所谓的网络编程就是,让在不同的电脑上的软件能够进行数据传递,即进程之间的通信
Python Internet 模块
协议 | 功能用处 | 端口号 | Python模块 |
---|---|---|---|
HTTP | 网页访问 | 80 | httplib, urllib, xmlrpclib |
NNTP | 阅读和张贴新闻文章,俗称为"帖子" | 119 | nntplib |
FTP | 文件传输 | 20 | fitlib, urllib |
SMTP | 发送邮件 | 25 | smtplib |
POP3 | 接收邮件 | 110 | poplib |
IMAP4 | 获取邮件 | 143 | imaplib |
Telnet | 命令行 | 23 | telnetlib |
Gopher | 信息查找 | 70 | gopherlib,urllib |
TCP/IP协议(族)
应用层:应用层,表示层,会话层
传输层:传输层
网络层:网络层
链路层:数据链路层,物理层
对上图的说明:
网际层也称为:网络层;网络接口层也称为:链路层
端口
端口:可以认为是设备与外界通讯交流的出口。
端口号是唯一的
端口范围:0~65535
端口分类:
**公认端口(WellKnownPorts):**从0到1023,它们紧密绑定(binding)于一些服务。
**注册端口(RegisteredPorts):**从1024到49151。它们松散地绑定于一些服务。也就是说有许多服务绑定于这些端口,这些端口同样用于许多其它目的。例如:许多系统处理动态端口从1024左右开始。
**动态和/或私有端口(Dynamicand/orPrivatePorts):**从49152到65535。理论上,不应为服务分配这些端口。实际上,机器通常从1024起分配动态端口。但也有例外:SUN的RPC端口从32768开始。
查看端口:
1、netstat -an
指令查看
2、第三方扫描软件
IP地址
ip地址:用来在网络中标记一台电脑的一串数字,比如192.168.1.1;在本地局域网上是惟一的。
ip地址分类:
A类IP地址
一个A类IP地址由1字节的网络地址和3字节主机地址组成,网络地址的最高位必须是“0”,地址范围1.0.0.1-126.255.255.254二进制表示为:00000001 00000000 00000000 00000001 - 01111110 11111111 11111111 11111110
可用的A类网络有126个,每个网络能容纳1677214个主机
B类IP地址
一个B类IP地址由2个字节的网络地址和2个字节的主机地址组成,网络地址的最高位必须是“10”,地址范围128.1.0.1-191.255.255.254
二进制表示为:10000000 00000001 00000000 00000001 - 10111111 11111111 11111111 11111110
可用的B类网络有16384个,每个网络能容纳65534主机
C类IP地址
一个C类IP地址由3字节的网络地址和1字节的主机地址组成,网络地址的最高位必须是“110”
范围192.0.1.1-223.255.255.254
二进制表示为: 11000000 00000000 00000001 00000001 - 11011111 11111111 11111110 11111110
C类网络可达2097152个,每个网络能容纳254个主机
D类地址用于多点广播
D类IP地址第一个字节以“1110”开始,它是一个专门保留的地址。
它并不指向特定的网络,目前这一类地址被用在多点广播(Multicast)中
多点广播地址用来一次寻址一组计算机
地址范围224.0.0.1-239.255.255.254
- E类IP地址*
以“1111”开始,为将来使用保留
E类地址保留,仅作实验和开发用
- 私有ip*
在这么多网络IP中,国际规定有一部分IP地址是用于我们的局域网使用,也就
是属于私网IP,不在公网中使用的,它们的范围是:10.0.0.0~10.255.255.255
172.16.0.0~172.31.255.255
192.168.0.0~192.168.255.255
注意
P地址127.0.0.1~127.255.255.255用于回路测试,
如:127.0.0.1可以代表本机IP地址,用[http://127.0.0.1]
就可以测试本机中配置的Web服务器。
子网掩码
子网掩码(subnet mask)又叫网络掩码、地址掩码、子网络遮罩,
它是一种用来指明一个IP地址的哪些位标识的是主机所在的子网,
以及哪些位标识的是主机的位掩码。
子网掩码不能单独存在,它必须结合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协议的网络来说非常重要,只有通过子网掩码,才能表明一台主机所在的子网与其他子网的关系,使网络正常工作。
子网掩码是“255.255.255.0”的网络:
最后面一个数字可以在0~255范围内任意变化,因此可以提供256个IP地址。
但是实际可用的IP地址数量是256-2,即254个,因为主机号不能全是“0”或全是“1”。
主机号全为0,表示网络号
主机号全为1,表示网络广播
注意:
如果将子网掩码设置过大,也就是说子网范围扩大,根据子网寻径规则,很可能发往和本地主机不在同一子网内的目标主机的数据,会因为错误的判断而认为目标主机是在同一子网内,导致数据包将在本子网内循环,直到超时并抛弃,使数据不能正确到达目标主机,导致网络传输错误;如果将子网掩码设置得过小,那么就会将本来属于同一子网内的机器之间的通信当做是跨子网传输,数据包都交给缺省网关处理,这样势必增加缺省网关的负担,造成网络效率下降。因此,子网掩码应该根据网络的规模进行设置。如果一个网络的规模不超过254台电脑,采用“255.255.255.0”作为子网掩码就可以了,现在大多数局域网都不会超过这个数字,因此“255.255.255.0”是最常用的IP地址子网掩码;假如在一所大学具有1500多台电脑,这种规模的局域网可以使用“255.255.0.0”。
socket
本地(一台机器上)的进程间通信(IPC)有很多种方式,例如
队列
同步(互斥锁、条件变量等)
网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket(简称:套接字)。
Python提供了两个级别访问的网络服务:
低级别的网络服务支持基本的 Socket,它提供了标准的 BSD Sockets API,可以访问底层操作系统Socket接口的全部方法。
高级别的网络服务模块 SocketServer, 它提供了服务器中心类,可以简化网络服务器的开发。
Python创建socket:
import socket
socket.socket(family[, type[, proto]])
参数解释:
family:套接字家族可以使AF_UNIX或者AF_INET
type:套接字类型可以根据是面向连接的还是非连接分为SOCK_STREAM或SOCK_DGRAM
protocol:一般不填默认为0.
Socket 对象(内建)方法
服务器端套接字
s.bind()
绑定地址(host, port)到socket, 在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.getsocketopt(level, optname, buflen)
返回套接字选项的值。
s.settimeout(timeout)
设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如connect())
s.gettimeout()
返回当前超时期的值,单位是秒,如果没有设置超时期,则返回None。
s.gileno()
返回套接字的文件描述符。
s.setbolcking(flag)
如果flag为0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)。非阻塞模式下,如果调用recv()没有发现任何数据,或send()调用无法立即发送数据,那么将引起socket.error异常
s.makefile()
创建一个与该套接字相关连的文件
# 创建socket 例子
import socket
# 创建一个tcp socket
tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print('tcp socket created!')
# 创建一个udp socket
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
print('udp socket created!')
tcp socket created!
udp socket created!
# socket 简单服务器
# 导入socket, sys 模块
import socket
import sys
# 创建socket对象
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 获取本地主机名
host = socket.gethostname()
# 是指端口
port = 6666
# 绑定端口号
server_socket.bind((host, port))
# 设置最大连接数,超过后阻塞
server_socket.listen(5)
while True: # 死循环,实际应用不可用
# 建立客户端连接
client_socket, addr = server_socket.accept()
print('连接地址:addr = {}'.format(addr))
msg = "这是一个socket 超简易服务器!\r\n"
client_socket.send(msg.encode('utf-8'))
client_socket.close()
# 因为是死循环,未设置终止,所以手动终止程序,会出现下方错误
---------------------------------------------------------------------------
KeyboardInterrupt Traceback (most recent call last)
in
19 while True: # 死循环,实际应用不可用
20 # 建立客户端连接
---> 21 client_socket, addr = server_socket.accept()
22
23 print('连接地址:addr = {}'.format(addr))
~/WorkStations/anaconda3/lib/python3.7/socket.py in accept(self)
210 For IP sockets, the address info is a pair (hostaddr, port).
211 """
--> 212 fd, addr = self._accept()
213 sock = socket(self.family, self.type, self.proto, fileno=fd)
214 # Issue #7995: if no default timeout is set and the listening
KeyboardInterrupt:
# socket 客户端例子
# 导入socket, sys 模块
import socket
import sys
# 创建socket对象
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 获取本地主机名
host = socket.gethostname()
# 是指端口
port = 6666
# 连接服务,指定主机和端口
s.connect((host, port))
# 接收小于2048字节的数据
msg = s.recv(2048)
s.close()
print(msg.decode('utf-8'))
socket 服务器 客户端 通信 结果图
UDP
UDP --- 用户数据报协议,是一个无连接的简单的面向数据报的运输层协议。
udp是TCP/IP协议族中的一种协议能够完成不同机器上的程序间的数据通信
udp只是把应用层传给ip层的数据报发出去,不能保证到达;udp在传输数据报前不用在客户和服务器之间建立连接,没有超时重发等机制,传输速度快。
UDP是一种面向无连接的协议,每个数据报都是一个独立的信息,包括完整的源地址或目的地址,它在网络上以任何可能的路径传往目的地,因此能否到达目的地,到达目的地的时间以及内容的正确性都是不能被保证的。
UDP特点:
面向无连接的通讯协议,UDP数据包括目的端口号和源端口号信息,由于通讯不需要连接,所以可以实现广播发送。
UDP传输数据有大小限制:每个被传输的数据报必须限定在64KB内。
udp不可靠,发送方所发送的数据报并不一定以相同的次序到达接收方。
UDP一般用于多点通信和实时的数据业务:
语音广播
视频
QQ
TFTP(简单文件传送)
SNMP(简单网络管理协议)
RIP(路由信息协议,如报告股票市场,航空信息)
DNS(域名解释)
UDP操作简单,而且仅需要较少的监护,因此通常用于局域网高可靠性的分散系统中client/server应用程序。
udp端口号动态变化解释:
每次重新运行网络程序,对于未绑定端口号的程序,系统默认随机分配一个端口号来唯一标识这个程序。如果需要向此程序发送信息,只需要向这个端口标识的程序发送即可。
UDP网络通信过程
udp服务器、客户端
udp的服务器和客户端的区分:往往是通过请求服务和提供服务来进行区分
请求服务的一方称为:客户端
提供服务的一方称为:服务器
udp 发送数据:
创建一个udp客户端程序步骤:
1、创建客户端套接字
2、发送/接收数据
3、关闭套接字
UDP创建流程图
# 创建udp 客户端 例子udp
# -*- coding: utf-8 -*-
import socket
#1创建socket
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 2 准备接收方地址
sendAddr = ('192.168.1.3', 8080)
# 3 键入数据
send_data = input('input send data:')
# 4 发送到指定的地址
udp_socket.sendto(send_data.encode('utf-8'), sendAddr)
# 5 关闭socket
udp_socket.close()
# 结果:在1922.168.1.3机器上通过【网络调试助手】软件会收到发送的数据
input send data:asdf
# udp 发送、接收数据 例子
# -*- coding: utf-8 -*-
import socket
# 1、创建socket
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 2、准备接收数据的机器地址
send_addr = ('192.168.1.3', 8080)
# 3、输出要发送的数据, 注意.encode('utf-8') 转换二进制
send_data = input('请输入要发送的数据:').encode('utf-8')
# 4、发送数据到指定机器
udp_socket.sendto(send_data, send_addr)
# 5、等待接收方发送数据
recv_data = udp_socket.recvfrom(1024) # 1024表示本次接受的最大字节数
# 6、显示接收到的数据
print(recv_data)
# 7、关闭socket
udp_socket.close()
请输入要发送的数据:asdf
udp绑定
一个udp网络程序,可以不绑定,此时操作系统会随机进行分配一个端口,
如果重新运行次程序端口可能会发生变化
一个udp网络程序,也可以绑定信息(ip地址,端口号),如果绑定成功,
那么操作系统用这个端口号来进行区别收到的网络数据是否是此进程的
# udp 接收方 端口绑定 例子
# -*-coding: utf-8 -*-
import socket
# 1、创建socket
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 2、绑定本地的相关信息,若一个网络程序不绑定,则系统会随机分配端口号
bind_addr = ('',6666)# ip地址和端口号,ip一般不用写,表示本机的任何一个ip
udp_socket.bind(bind_addr)
# 3、等待接收数据
recv_data = udp_socket.recvfrom(1024)# 1204为本次接收的最大字节数
# 4、显示接收数据
# 若接收到的是bite格式,则用decode('decode-type')解码,str类型不必decode
print(recv_data.decode('utf-8'))
# 5、关闭socket
udp_socket.close()
# 因为例子是接收方的端口绑定,所以单独运行是没有显示的,故不运行
# udp echo 服务器 示例
# echo服务器:将收到的信息原封不动的返回
import socket
# 1、创建socket
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 2、绑定本地相关信息
bind_addr = ('', 6666)# ip地址和端口号,ip一般不用写,表示本机的任何一个ip
udp_socket.bind(bind_addr)
# 统计次数
num = 0
while True:
# 3、等待接收对方发送的数据
recv_data = udp_socket.recvfrom(2048)# 最大接收字节数2048
# 4、回传接收到的信息
udp_socket.sendto(recv_data[0], recv_data[1])
# 5、统计信息
num +=1
print('已完成{}次数据收发'.format(num))
# 5、关闭socket
udp_socket.close()
udp广播
# udp 广播例子
# -*- coding: utf-8 -*-
import socket, sys
dest = ('', 6666)
# 创建socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 对socket进行修改,以发送广播
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
# 以广播的形式发送数据到网络的所有电脑中
s.sendto('hello, world!', dest)
print('等待回复,Ctrl+c退出')
while True:
(buf, address) = s.recvfrom(2048)
print("received from {}:{}".format(address, buf))
TCP
tcp通信模型中,在通信开始之前,一定要先建立相关的链接,才能发送数据,类似于生活中"打电话"
TCP三次握手和四次挥手部分参考自下面的网址:
参考网址 作者:小书go
背景描述:
网络层,可以实现两个主机之间的通信。但是这并不具体,因为,真正进行通信的实体是在主机中的进程,是一个主机中的一个进程与另外一个主机中的一个进程在交换数据。IP协议虽然能把数据报文送到目的主机,但是并没有交付给主机的具体应用进程。而端到端的通信才应该是应用进程之间的通信。
UDP,在传送数据前不需要先建立连接,远地的主机在收到UDP报文后也不需要给出任何确认。虽然UDP不提供可靠交付,但是正是因为这样,省去和很多的开销,使得它的速度比较快,比如一些对实时性要求较高的服务,就常常使用的是UDP。对应的应用层的协议主要有 DNS,TFTP,DHCP,SNMP,NFS 等。
TCP,提供面向连接的服务,在传送数据之前必须先建立连接,数据传送完成后要释放连接。因此TCP是一种可靠的的运输服务,但是正因为这样,不可避免的增加了许多的开销,比如确认,流量控制等。对应的应用层的协议主要有 SMTP,TELNET,HTTP,FTP 等。
TCP报文首部
源端口和目的端口,各占2个字节,分别写入源端口和目的端口;
序号,占4个字节,TCP连接中传送的字节流中的每个字节都按顺序编号。例如,一段报文的序号字段值是 301 ,而携带的数据共有100字段,显然下一个报文段(如果还有的话)的数据序号应该从401开始;
确认号,占4个字节,是期望收到对方下一个报文的第一个数据字节的序号。例如,B收到了A发送过来的报文,其序列号字段是501,而数据长度是200字节,这表明B正确的收到了A发送的到序号700为止的数据。因此,B期望收到A的下一个数据序号是701,于是B在发送给A的确认报文段中把确认号置为701;
数据偏移,占4位,它指出TCP报文的数据距离TCP报文段的起始处有多远;
保留,占6位,保留今后使用,但目前应该都位0;
紧急URG,当URG=1,表明紧急指针字段有效。告诉系统此报文段中有紧急数据;
确认ACK,仅当ACK=1时,确认号字段才有效。TCP规定,在连接建立后所有报文的传输都必须把ACK置1;
推送PSH,当两个应用进程进行交互式通信时,有时在一端的应用进程希望在键入一个命令后立即就能收到对方的响应,这时候就将PSH=1;
复位RST,当RST=1,表明TCP连接中出现严重差错,必须释放连接,然后再重新建立连接;
同步SYN,在连接建立时用来同步序号。当SYN=1,ACK=0,表明是连接请求报文,若同意连接,则响应报文中应该使SYN=1,ACK=1;
终止FIN,用来释放连接。当FIN=1,表明此报文的发送方的数据已经发送完毕,并且要求释放;
窗口,占2字节,指的是通知接收方,发送本报文你需要有多大的空间来接受;
检验和,占2字节,校验首部和数据这两部分;
紧急指针,占2字节,指出本报文段中的紧急数据的字节数;
选项,长度可变,定义一些其他的可选的参数。
TCP通信过程
TCP三次握手
syn>>>> syn+ack<<<<<<<<<<<< ack>>>>>>>>>>>>>>>
*tcp传输前会发送ack包确认,但是udp不会进行确认,所以有tcp比udp稳定
client close()方法,会通知到server,server会回复通知信息,然后client回复,挥手完*
最开始的时候客户端和服务器都是处于CLOSED状态。主动打开连接的为客户端,被动打开连接的是服务器。
TCP服务器进程先创建传输控制块TCB,时刻准备接受客户进程的连接请求,此时服务器就进入了LISTEN(监听)状态;
TCP客户进程也是先创建传输控制块TCB,然后向服务器发出连接请求报文,这是报文首部中的同部位SYN=1,同时选择一个初始序列号 seq=x ,此时,TCP客户端进程进入了 SYN-SENT(同步已发送状态)状态。TCP规定,SYN报文段(SYN=1的报文段)不能携带数据,但需要消耗掉一个序号。
TCP服务器收到请求报文后,如果同意连接,则发出确认报文。确认报文中应该 ACK=1,SYN=1,确认号是ack=x+1,同时也要为自己初始化一个序列号 seq=y,此时,TCP服务器进程进入了SYN-RCVD(同步收到)状态。这个报文也不能携带数据,但是同样要消耗一个序号。
TCP客户进程收到确认后,还要向服务器给出确认。确认报文的ACK=1,ack=y+1,自己的序列号seq=x+1,此时,TCP连接建立,客户端进入ESTABLISHED(已建立连接)状态。TCP规定,ACK报文段可以携带数据,但是如果不携带数据则不消耗序号。
为什么TCP客户端最后还要发送一次确认
一句话,主要防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误。
如果使用两次握手建立连接,假设有这样一种场景,客户端发送了第一个请求连接并且没有丢失,只是因为在网络结点中滞留的时间太长了,由于TCP的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数据,然后关闭连接。此时此前滞留的那一次请求连接,网络通畅了到达了服务器,这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务器再次建立连接,这将导致不必要的错误和资源的浪费。
如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了确认报文,但是客户端不会再次发出确认。由于服务器收不到确认,就知道客户端并没有请求连接。
tcp四次挥手
数据传输完毕后,双方都可释放连接。最开始的时候,客户端和服务器都是处于ESTABLISHED状态,然后客户端主动关闭,服务器被动关闭。
客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。
tcp十种状态
当一端收到一个FIN,内核让read返回0来通知应用层另一端已经终止了向本端的数据传送
发送FIN通常是应用层对socket进行关闭的结果
TCP的2MSL问题
MSL(Maximum Segment Lifetime),TCP允许不同的实现可以设置不同的MSL值。
第一,保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失,站在服务器的角度看来,我已经发送了FIN+ACK报文请求断开了,客户端还没有给我回应,应该是我发送的请求断开报文它没有收到,于是服务器又会重新发送一次,而客户端就能在这个2MSL时间段内收到这个重传的报文,接着给出回应报文,并且会重启2MSL计时器。
第二,防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中。客户端发送完最后一个确认报文后,在这个2MSL时间中,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样新的连接中不会出现旧连接的请求报文。
为什么建立连接是三次握手,关闭连接确是四次挥手呢?
建立连接的时候, 服务器在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。
而关闭连接时,服务器收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,而自己也未必全部数据都发送给对方了,所以己方可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送,从而导致多了一次。
如果已经建立了连接,但是客户端突然出现故障了怎么办?
TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75分钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。
TCP长连接和短连接
TCP在真正的读写操作之前,server与client之间必须建立一个连接,
当读写操作完成后,双方不再需要这个连接时它们可以释放这个连接,
连接的建立通过三次握手,释放则需要四次握手,
所以说每个连接的建立都是需要资源消耗和时间消耗的。
TCP短连接
模拟一种TCP短连接的情况:
client 向 server 发起连接请求
server 接到请求,双方建立连接
client 向 server 发送消息
server 回应 client
一次读写完成,此时双方任何一个都可以发起 close 操作
在第 步骤5中,一般都是 client 先发起 close 操作。当然也不排除有特殊的情况。
从上面的描述看,短连接一般只会在 client/server 间传递一次读写操作!
TCP长连接
模拟一种TCP长连接的情况:
client 向 server 发起连接请求
server 接到请求,双方建立连接
client 向 server 发送消息
server 回应 client
一次读写完成,连接不关闭
后续读写操作...
长时间操作之后client发起关闭请求
TCP长/短连接操作过程
短连接:
建立连接——数据传输——关闭连接...建立连接——数据传输——关闭连接
长连接:
建立连接——数据传输...(保持连接)...数据传输——关闭连接
长短连接各自优缺点:
长连接可以省去较多的TCP建立和关闭的操作,减少浪费,节约时间。
对于频繁请求资源的客户来说,较适用长连接。
client与server之间的连接如果一直不关闭的话,会存在一个问题,
随着客户端连接越来越多,server早晚有扛不住的时候,这时候server端需要采取一些策略,
如关闭一些长时间没有读写事件发生的连接,这样可以避免一些恶意连接导致server端服务受损;
如果条件再允许就可以以客户端机器为颗粒度,限制每个客户端的最大长连接数,
这样可以完全避免某个蛋疼的客户端连累后端服务。
短连接对于服务器来说管理较为简单,存在的连接都是有用的连接,不需要额外的控制手段。
但如果客户请求频繁,将在TCP的建立和关闭操作上浪费时间和带宽。
TCP长/短连接的应用场景:
长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况。
而像WEB网站的http服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源,
常见网络攻击案例
tcp半链接攻击
tcp半链接攻击也称为:SYN Flood (SYN洪水)
是种典型的DoS (Denial of Service,拒绝服务) 攻击
效果就是服务器TCP连接资源耗尽,停止响应正常的TCP连接请求
dns攻击
dns服务器被劫持
域名服务器对其区域内的用户解析请求负责,但是并没有一个机制去监督它有没有真地负责。
将用户引向一个错误的目标地址。这就叫作 DNS 劫持,主要用来阻止用户访问某些特定的网站,或者是将用户引导到广告页面。
dns欺骗
DNS 欺骗简单来说就是用一个假的 DNS 应答来欺骗用户计算机,让其相信这个假的地址,并且抛弃真正的 DNS 应答。
arp攻击
TCP服务器
创建TCP服务器过程:
1、创建socket
2、bind绑定ip和port
3、listen使套接字变为可以被动链接
4accept等待客户端的链接
5、recv/send接收发送数据
TCP服务器创建流程:socket(), bind(), listen(), accept(), read(), write(), close()
# tcp服务器 示例代码
# -*- coding: utf -8 -*-
import socket
# 创建socket
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定本地信息
address = ("", 6666)
tcp_server_socket.bind(address)
# 使用socket创建的套接字默认的属性是主动的,使用listen变为被动
# 变为被动后,就可以接收别人的链接了
tcp_server_socket.listen(5)
# 有行的客户端链接服务器,就为这个客户端生成一个新的套接字
# new_socket用来为这个客户端服务
# tcp_server_socket可以省下等待其他新的客户端链接
new_socket, client_addr = tcp_server_socket.accept()
# 接收对方发送的数据,最大为1024字节
recv_data = new_socket.recv(1024)
print('接收到的数据为:{}'.format(recv_data))
# 发送数据到客户端
new_socket.send('send from server!')
# 关闭为此客户端创建的socket
# 关闭后就不再服务,要想继续服务只能重连
new_socket.close()
# 关闭监听socket,此socket关闭表示不再接收客户端链接
tcp_server_socket.close()
TCP客户端
# tcp 客户端 示例代码
# -*- coding: utf-8 -*-
import socket
# 创建socket
tcp_client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 链接服务器
server_addr = ('192.168.1.1', 6666)
tcp_client_socket.connect(server_addr)
# 提示输入数据
send_data = input('请输入要发送的数据:')
# 接收服务器发送来的数据,最大接收1024字节
recv_data = tcp_client_socket.recv(1024)
print('接收到的数据:{}'.format(recv_data))
# 关闭socket
tcp_client_socket.close()
'''tcp 模拟qq'''
# 客户端代码
# -*- coding: utf-8 -*-
import socket
# 创建secket
tcp_client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 连接服务器
server_addr = ('192.168.1.1', 6666)
tcp_client_socket.connect(server_addr)
while True:
# 提示用户输入数据
send_data = input('send: ')
if len(send_data)>0:
tcp_client_socket.send(send_data)
else:
break
# 接收对方发来的数据,最大接收1024字节
recv_data = tcp_client_socket.recv(1024)
print('received:', recv_data)
# 关闭套接字
tcp_client_socket.close()
# 服务器 实例代码
#-*- coding: utf-8 -*-
import socket
# create socket
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定本地信息
address = ('', 6666)
tcp_server_socket.bind(address)
#用listen变socket为被动
tcp_server_socket.listen(5)
while True:
# new socket
new_socket, client_addr = tcp_server_socket.accept()
while True:
# 接收数据,最大接收1024字节
recv_data = new_socket.recv(1024)
# 接收到的数据长度为0,表示客户端关闭链接
if len(recv_data)>0:
print('recv: ', recvData)
else:
break
# 发送数据到客户端
send_data = input('send: ')
new_socket.send(send_data)
# 关闭此客户端的套接字,不再为此客户端服务
new_socket.close()
# 关闭监听socket
tcp_server_socket.close()
网络编程 应用
单全双工信息收发
单工:只能接收,半双工:同一时刻只能接收或是发送,全双工:可接可发
socket(套接字)是全双工的
编码:encode("utf-8")
解码:decode("utf-8")
# 简单全双工信息收发 例子
#-*-coding: utf-8 -*-
from threading import Thread
import socket
# 接收数据,打印
def recv_data():
while True:
recv_msg = udp_socket.recvfrom(4096)
print('收到>:{}:()'.str(recv_msg[1], recv_msg[0]))
# 发送数据
def send_data():
while True:
send_data = input('发送\r<:')
print('>:')
udp_socket.sendto(send_data.encode('utf-8'), (which_ip, which_port))
# socket, ip, port
udp_socket = None
which_ip = ''
which_port = 0
def main():
global udp_socket
global which_ip
global which_port
# 输入ip:port
which_ip = input('请输入对方IP:\t')
which_port = input("请输入对方的port:\t")
# create socket
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# bind local info
udp_socket.bind('', 6666)
# create threading
tr = Thread(target=recv_data)
ts = Thread(target=send_data)
# start threading
tr.start()
ts.start()
# blocking
tr.join()
ts.join()
if __name__=='__main__':
main()
TFTP客户端
TFTP 协议介绍
TFTP(Trivial File Transfer Protocol,简单文件传输协议)是TCP/IP协议族中的一个用来在客户端与服务器之间进行简单文件传输的协议
TFTP特点:
简单
占用资源小
适合传递小文件
适合在局域网进行传递
端口号为69
基于UDP实现
TFTP下载过程
TFTP服务器默认监听69号端口
当客户端发送“下载”请求(即读请求)时,需要向服务器的69端口发送
服务器若批准此请求,则使用一个新的、临时的端口进行数据传输
当服务器找到需要现在的文件后,会立刻打开文件,把文件中的数据通过TFTP协议发送给客户端
如果文件的总大小较大(比如3M),那么服务器分多次发送,每次会从文件中读取512个字节的数据发送过来
因为发送的次数有可能会很多,所以为了让客户端对接收到的数据进行排序,所以在服务器发送那512个字节数据的时候,会多发2个字节的数据,用来存放序号,并且放在512个字节数据的前面,序号是从1开始的
因为需要从服务器上下载文件时,文件可能不存在,那么此时服务器就会发送一个错误的信息过来,为了区分服务发送的是文件内容还是错误的提示信息,所以又用了2个字节 来表示这个数据包的功能(称为操作码),并且在序号的前面
操作码 | 功能 |
---|---|
1 | 读请求(下载) |
2 | 写请求(上传) |
3 | 表示数据包(DATA) |
4 | 确认码(ACK) |
5 | 错误 |
TFTP协议中规定,服务器确认客户端收到刚刚发送的数据包:当客户端接收到一个数据包的时候需要向服务器进行发送确认信息,这样的包成为ACK(应答包)
发送完:客户端接收到的数据小于516(2字节操作码+2字节序号+512字节数据)
'''
同一时刻只能为一个客户进行服务,不能同时为多个客户服务
类似于找一个“明星”签字一样,客户需要耐心等待才可以获取到服务
当服务器为一个客户端服务时,而另外的客户端发起了connect,
只要服务器listen的队列有空闲的位置,就会为这个新客户端进行连接,
并且客户端可以发送数据,但当服务器为这个新客户端服务时,
可能一次性把所有数据接收完毕
当recv接收数据时,返回值为空,即没有返回数据,
那么意味着客户端已经调用了close关闭了;
因此服务器通过判断recv接收数据是否为空 来判断客户端是否已经下线
'''
# tftp 客户端 示例代码
# -*- coding:utf-8 -*-
import struct
from socket import *
import time
import os
def main():
#0. 获取要下载的文件名字:
downloadFileName = raw_input("请输入要下载的文件名:")
#1.创建socket
udpSocket = socket(AF_INET, SOCK_DGRAM)
# 这里对下面的!H8sb5sb解释:
# !H占两个,表示操作码,8s是文件名的占位长度,b是0的占位长度,5sb同理
requestFileData = struct.pack("!H%dsb5sb"%len(downloadFileName), 1, downloadFileName, 0, "octet", 0)
#2. 发送下载文件的请求
udpSocket.sendto(requestFileData, ("192.168.119.215", 69))
flag = True #表示能够下载数据,即不擅长,如果是false那么就删除
num = 0
f = open(downloadFileName, "w")
while True:
#3. 接收服务发送回来的应答数据
responseData = udpSocket.recvfrom(1024)
# print(responseData)
recvData, serverInfo = responseData
opNum = struct.unpack("!H", recvData[:2])
packetNum = struct.unpack("!H", recvData[2:4])
print(packetNum[0])
# print("opNum=%d"%opNum)
# print(opNum)
# if 如果服务器发送过来的是文件的内容的话:
if opNum[0] == 3: #因为opNum此时是一个元组(3,),所以需要使用下标来提取某个数据
#计算出这次应该接收到的文件的序号值,应该是上一次接收到的值的基础上+1
num = num + 1
# 如果一个下载的文件特别大,即接收到的数据包编号超过了2个字节的大小
# 那么会从0继续开始,所以这里需要判断,如果超过了65535 那么就改为0
if num==65536:
num = 0
# 判断这次接收到的数据的包编号是否是 上一次的包编号的下一个
# 如果是才会写入到文件中,否则不能写入(因为会重复)
if num == packetNum[0]:
# 把收到的数据写入到文件中
f.write(recvData[4:])
num = packetNum[0]
#整理ACK的数据包
ackData = struct.pack("!HH", 4, packetNum[0])
udpSocket.sendto(ackData, serverInfo)
elif opNum[0] == 5:
print("sorry,没有这个文件....")
flag = False
# time.sleep(0.1)
if len(recvData)<516:
break
if flag == True:
f.close()
else:
os.unlink(downloadFileName)#如果没有要下载的文件,那么就需要把刚刚创建的文件进行删除
if __name__ == '__main__':
main()
单进程 TCP服务器
# 单进程 TCP服务器
from socket import *
server_socket = socket(AF_INET, SOCK_STREAM)
# 重用绑定的信息
server_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
local_addr = ('', 6666)
server_socket.bind(local_addr)
server_socket.listen(5)
while True:
print('主进程,等待新客户端的到来')
new_socket, dest_addr = server_socket.accept()
print('主进程, 接下来负责数据处理{}'.format(str_dest_addr))
try:
while True:
recv_data = new_socket.recv(1024)
if len(recv_data)>0:
print('recv[{}]:{}'.format(str(dest_addr), recv_data))
else:
print('{}客户端已经关闭'.format(str(dest_addr)))
finally:
new_socket.close()
server_socket.close()
主进程,等待新客户端的到来
多进程服务器
# 多进程服务器
'''
通过为每个客户端创建一个进程的方式,能够同时为多个客户端进行服务
当客户端不是特别多的时候,这种方式还行,如果有几百上千个,就不可取了,
因为每次创建进程等过程需要好较大的资源
'''
import socket
from multiprocessing import Process
from time import sleep
# 处理客户端请求,并为其服务
def deal_with_client(new_socket, dest_addr):
while True:
recv_data = enw_socket.recv(1024)
if len(recv_data)>0:
print('recv[{}]: {}'.format(str(dest_addr), recv_data))
else:
print('[{}]客户端已经关闭'.format(str(dest_addr)))
break
new_socket.close()
def main():
server_socket = socket(AF_INET, SOCK_STREAM)
server_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
local_addr = ('', 6666)
server_socket.bind(local_addr)
server_socket.listen(5)
try:
while True:
print('主进程,等待新客户端的到来')
new_socket, dest_addr = server_socket.accept()
print('主进程, 接下来创建一个新的进程负责数据处理[{}]'.format(str(dest_addr)))
client = Process(target=deal_with_client, args=(new_socket, dest_addr))
client.start()
# 因为已经向子进程copy了一份(引用),并且父进程中这个套接字也没有用处了
# 所以关闭
new_socket.close()
finally:
3 当为所有的客户端服务完后再关闭,表示不再接收新的客户端的连接
server_socket.close()
if __name__=='__main__':
main()
多线程服务器
# 多线程服务器
# -*- coding: utf-8 -*-
from socket import *
from threading import Thread
from thime import sleep
# 处理客户的请求并执行事情
def deal_with_client(new_socket, dest_addr):
while True:
recv_data = new_socket.recv(1024)
if len(recv_data)>0:
print('recv[{}]: {}'.format(str(dest_addr), recv_data))
else:
print('[{}]客户端已经关闭'.format(dest_addr))
break
new_socket.close()
def main():
server_socket = socket(AF_INET, SOCK_STREAM)
server_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
local_addr = ('',6666)
server_socket.bind(local_addr)
server_socket.listen(5)
try:
while True:
print('-----主进程,,等待新客户端的到来------')
new_socket, dest_addr = server_socket.accept()
print('主进程,接下来创捷一个新的进程负责数据处理[{}]'.format(
str(dest_addr)
))
client = Thread(target=deal_with_client, args=(new_socket, dest_addr))
client.start()
# 线程中共享socket, 若关闭会导致套接字不可用
# 但是此时在线程中这个socket可能还在收数据,因此不可关闭
# new_socket.close()
finally:
server_socket.close()
if __name__ =='__main__':
main()
单进程服务器,非阻塞模式
# 服务器
# -*- coding: utf-8 -*-
from socket import *
import time
# 存储所有的新连接的socket
g_socket_list = []
def main():
server_socket = socket(AF_INET, SOCK_STREAM)
server_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
local_addr = ('', 6666)
server_socket.bind(local_addr)
# 可以适当修改listen中的值来看看不同的现象
server_socket.listen(1000)
# 设置套接字为非阻塞
# 设置为非阻塞后,若accept时,无客户端connect,accept会抛出一个异常,
# 所以需要try处理异常
server_socket.setblocking(False)
while True:
# 测试
# time.sleep(0.5)
try:
new_client_info = server_socket.accept()
except Exception as result:
pass
else:
print('一个新的客户端到来:{}'.format(str(new_client_info))
new_client_infp[0].setblocking(False)
g_socket_list.append(new_client_info)
# 存储需要删除的客户端信息
need_del_client_info_list = []
for client_socket, client_addr in g_socket_list:
try:
recv_data = client_socket.recv(1024)
if len(recv_data)>0:
print('recv[{}]:{}'.format(str(client_addr), recv_data))
else:
print('[{}]客户端已经关闭'.format(client_addr))
client_socket.close()
g_need_del_client_info_list.append(client_socket, client_addr)
except Exception as result:
pass
for need_del_client_info in need_del_client_info_list:
g_socket_list.remove(need_del_client_info)
if __name__=='__main__':
main()
# 客户端
#coding=utf-8
from socket import *
import random
import time
serverIp = input("请输入服务器的ip:")
connNum = input("请输入要链接服务器的次数(例如1000):")
g_socketList = []
for i in range(int(connNum)):
s = socket(AF_INET, SOCK_STREAM)
s.connect((serverIp, 7788))
g_socketList.append(s)
print(i)
while True:
for s in g_socketList:
s.send(str(random.randint(0,100)))
# 用来测试用
#time.sleep(1)
select版 TCP服务器
select原理
多路复用的模型中,较常用的有select,epoll模型。这两个都是系统接口,
由操作系统提供。当然,Python的select模块进行了更高级的封装。
网络通信被Unix系统抽象为文件的读写,通常是一个设备,由设备驱动程序提供,驱动可以知道自身的数据是否可用。支持阻塞操作的设备驱动通常会实现一组自身的等待队列,如读/写等待队列用于支持上层(用户层)所需的block或non-block操作。设备的文件的资源如果可用(可读或者可写)则会通知进程,反之则会让进程睡眠,等到数据到来可用的时候,再唤醒进程。
这些设备的文件描述符被放在一个数组中,然后select调用的时候遍历这个数组,如果对于的文件描述符可读则会返回改文件描述符。当遍历结束之后,如果仍然没有一个可用设备文件描述符,select让用户进程则会睡眠,直到等待资源可用的时候在唤醒,遍历之前那个监视的数组。每次遍历都是依次进行判断的。
select 优缺点
优点
select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。
缺点
select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。
一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.
对socket进行扫描时是依次扫描的,即采用轮询的方法,效率较低。
当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。
# select 回显服务器
import select
import socket
import sys
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('', 6660))
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()
# 包含writeList服务器
# -*- coding: utf-8 -*-
import socket
import Queue
form select import select
server_ip = ('', 8888)
# 保存客户端发过来的消息,存入消息队列
message_queue = {}
input_list = []
output_list = []
if __name__ == '__main__':
server = socket.socket()
server.bind(server_ip)
server.listen(10)
# 设置为非阻塞
server.setblocking(False)
# 初始化将服务器加入监听列表
input_list.append(server)
while True:
# 开始select监听,对input_list中的服务端server进行监听
stdinput, stdoutput, stderr = select(inut_list, out_list, input_list)
# 循环判断是否有客户端连接进来,有客户端连接时select将触发
for obj in stdinput:
# 判断当前出发的是不是服务端对象,当触发的对象是服务端对象时,
# 说明有新客户连接进来了
if obj == server:
# 接收客户端的连接, 获取客户端对象和客户端地址信息
conn, addr = server.accept()
print('client {} connected!'.format(addr))
# 将客户端对象也加入到监听的列表中,当客户端发送消息时select将触发
input_list.append(conn)
# 为连接的客户端单独创建一个消息队列,用来保存客户端发送的消息
message_queue[conn] = Queue.Queue()
else:
# 将客户端加入到了监听列表(input_list),客户端发送消息就触发
try:
recv_data = obj.recv(1024)
# 客户端未断开
print('received {} from client {} '.format(recv_data, str(addr)))
# 将收到的消息放入到各客户端的消息队列中
message_queue[obj].put(recv_data)
# 将回复操作放到output列表中,让select监听
if obj not in output_list:
output_list.append(obj)
except ConnectionResetError:
# 客户端断开连接了,将客户端的监听中input列表中移除
input_list.remove(obj)
# 移除客户端的消息队列
del message_queue[obj]
print("\n[input] Client %s disconnected"%str(addr))
# 如果现在没有客户端请求,也没有客户端发送消息时,
# 开始对发送消息队列进行处理,是否需要发送消息
for sendobj in output_list:
try:
# 如果消息队列中有消息,从消息队列中获取要发送的消息
if not message_queue[sendobj].empty():
# 从该客户端对象的消息队列中获取要发送的消息
send_data = message_queue[sendobj].get()
sendobj.send(send_data)
else:
# 将监听移除等待下一次客户端发送消息
output_list.remove(sendobj)
except ConnectResetError:
# 客户端连接断开
del message_queue[sendobj]
output_list.remove(sendobj)
print("\n[output] Client %s disconnected"%str(addr))
epoll版-TCP服务器
epoll的优点
没有最大并发连接的限制,能打开的FD(指的是文件描述符,通俗的理解就是套接字对应的数字编号)的上限远大于1024
效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,epoll的效率就会远远高于select和poll。
epoll使用说明
EPOLLIN (可读)
EPOLLOUT (可写)
EPOLLET (ET模式)
epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:
LT模式:当epoll检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll时,会再次响应应用程序并通知此事件。
ET模式:当epoll检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll时,不会再次响应应用程序并通知此事件。
# epoll 参考代码
import socket
import select
# 创建socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置可以重复使用绑定的信息
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 绑定本机信息
s.bind('', 1234)
# 绑定本机信息
s.bind(10)
# 创建一个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 event
# 如果是socket创建的socket被激活
if fd == s.fileno():
conn, addr = s.accept()
print('有新的客户端到来{}'.format(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上接收
recv_data = connections[fd].recv(1024)
if len(recv_data)>0:
print('recv: {} '.format(recv_data))
else:
# 从epoll中移除该连接 fd
epoll.unregister(fd)
# server 侧主动关闭该连接 fd
connections[df].close()
print('{} ---offline---'.format(str(addresses[fd])))
# gevent版TCP服务器
import sys
import time
import gevent
from gevent import socket, monkey
monkey.patch_all()
def handle_request(conn):
data = conn.recv(1024)
if not data:
conn.close()
break
print('recv: {}'.format(data))
conn.send(data)
def server(port):
s = socket.socket()
s.bind(('', port))
s.listen(5)
while True:
cli, addr = s.accept()
gevent,spawn(handle_request, cli)
if __name__ == '__main__':
server(4567)
File "", line 17
break
^
SyntaxError: 'break' outside loop
C:\MyPrograms\Anaconda3\lib\site-packages\gevent\hub.py:154: UserWarning: libuv only supports millisecond timer resolution; all times less will be set to 1 ms
with loop.timer(seconds, ref=ref) as t:
网络分析工具
wireshark
linux 下可能出现问题:打开wireshark提示权限不足
解决方法:
参考网址
添加组,wireshark,但是安装软件时已经创建,这里可以省略
sudo groupadd wireshark
将自己添加到wireshark组
sudo usermod -a -G wireshark 'username'
newgrp wireshark
更改组别
sudo chgrp wireshark /usr/bin/dumpcap
添加权限(1-x, 2-w, 4-r)
sudo chmod 754 /usr/bin/dumpcap
这里原作者有两个方法,我选择一个简单的
sudo setcap cap_net_raw,cap_netadmin=eip /usr/bin/dumpcap
sudo reboot now
至此,问题解决
Packet Tracer
hub(集线器)能够完成多个电脑的链接
每个数据包的发送都是以广播的形式进行的,容易堵塞网络
交换机能够完成多个电脑的链接
每个数据包的发送都是以广播的形式进行的,容易堵塞网络
如果PC不知目标IP所对应的的MAC,那么可以看出,pc会先发送arp广播,得到对方的MAC然后,在进行数据的传送
当switch第一次收到arp广播数据,会把arp广播数据包转发给所有端口(除来源端口);如果以后还有pc询问此IP的MAC,那么只是向目标的端口进行转发数据
路由器(Router)又称网关设备(Gateway)是用于连接多个逻辑上分开的网络
在同一网段的pc,需要设置默认网关才能把数据传送过去 通常情况下,都会把路由器默认网关
当路由器收到一个其它网段的数据包时,会根据“路由表”来决定,把此数据包发送到哪个端口;路由表的设定有静态和动态方法
每经过一次路由器,那么TTL值就会减一
Ciso 的packet tracer
hub集线器现在基本已经废弃,现在一般用交换机
路由器:链接不同网段的网络,使不同网段的设备可以通讯
mac地址在通信传输中,是动态的,在两个设备之间会发生变话
IP地址在通信传输中,是静态的,在整个传输中不会发生变化
Django
虚拟环境
安装虚拟环境:
Python3 :pip3 install python3-venv
python2 :pip install python-virtualenvs
创建虚拟环境:
python3 -m venv DirName(这个可以自定义名称)
python -m virtualenv DirName
启动虚拟环境:
cd /home/DirName/bin/;source activate
关闭虚拟环境:
deactivate
安装django
pip install django==1.8 # 这里的==1.8表示的是要安装的版本号,可以不写
创建一个新的项目
django-admin startproject Demo # 这里的Demo是django的项目名
启动项目
python3 manage.py startapp Demo
运行服务
python3 manage.py runserver 8080 # 这里的8080是端口号,可以随意更改(>1023)
设置迁移
python3 manage.py makemigrations
应用迁移
python3 manage.py migrate
进入shell
python3 manage.py shell
视图view:接受请求,逻辑处理,调用数据,输出响应
配置url:在自己的应用中配置正则url(正则表达式,视图的名称)
DIRS:定义目录
APP_DIRS:在项目的根目录创建模板目录
建的项目不打算移植,可以用DIRS,希望以后移植,用APP_DIRS更好一些。
django模板处理
1.加载模板内容
2.根据模板内容渲染
# 加载渲染的完整代码
from django.tmplate import loader, RequestContext
from django.http import HttpResponse
def index(request):
tmp = loader.get_tmplate("Demo/demo.html")
context = RequestContext(request, {})
return HttpResponse(tmp.render(context))
# 简化代码:
from django.shortcuts import render
def index(request):
return render(request, "Demo/demo.html")
# 这里的简化代码中render_to_string("")返回一个字符串
# render(request, "模板", context)
DTL语言
'''
变量
{{ var }}:字典,属性或方法,数字索引,空字符串,
注意:模板中调用方法不能传递参数,因为不能有()出现在模板中
标签
{% 代码 %}
{%for%}
{{forloop.counter}} # 返回一个当前是第几次返回的数字
{%empty%} # 当for in 的list不存在或空时,执行这个标签
{%endfor%}
{%if%}
{%elif%}
{%else%}
{%endif%}
{%comment%}
这里是一个多行注释
{%endcomment%}
反向解析:
{%url "namespace:name" "参数1" "参数2" %} # 反向解析
{%crsf_token%} # 用于跨站请求伪造保护
过滤器
{{变量|过滤器}} # “|” 符合是管道符号,用来应用过滤器
这里用“:”来传递参数
{{name|length>10}} 选出长度大于10 的name
注释
{# 注释 #}
模板的继承
# 这是用在父模板的,base.html
{%block my_name%} # 这里的my_name只是用做变量的区分
这里是要填充的内容
{%endblock my_name%}
# 父模板中可以用很多的{%block%}
# 这是用在子模板的,demo.html
{%extends "要继承的父模板的位置"%} # 这个语句要写在子模板的首行
{%block my_name%}
这里来填充父模板的block
{%endblock my_name%}
注意:一旦使用了继承,那么要显示的内容只能写在block中,写在外边的无法显示
html转义:
1 用过滤器:
{{value|escape}} # 这里是转义,默认是转义
{{value|safe}} # 这里是不转义
2 用标签
{%autoescape off%} # 这里是关闭自动转义
{{value}} # 这里是不希望转义的后台传输过来的内容
{%endautoescape%}
'''
django高级
'''
静态文件
static
STATIC_URL = "url"
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "static")
# 注意这里的static是项目下的一个文件夹的名字,这里必须相同
]
在网页中首行添加
{%load static from staticfiles %}
在img标签中的src使用{%static "fileName"%}
中间件
MIDDLEWARE_CLASS
是一个轻量级、底层的插件系统
__init__方法:第一次访问
process_request:url匹配前
process_view:视图前
process_tmplate_response:视图后,html前,在请求上调用
process_response:HTML后,返回到浏览器之前
process_exception:视图中出错,到次方法,转到process_response
'''