第6章 网络与并发编程

一. 网络编程

参考: 软件开发架构, 互联网协议, socket通信

1. 简述TCP三次握手, 四次挥手的流程

  • TCP三次握手
                                                     CLOSED
CLOSED                                               LISTEN
          -------------- SYN=1 seq=x --------------> 
SYN-SEND(同步已发送)                                           
          <------- ACK=1 ack=x+1 SYN=1 syn=y -------
                                                     SYN-RECV(同步收到)
           --------- ACK=1 ack=y+1 seq=x+1 --------->
ESTAB-LISTEN(已建立连接)                              ESTAB-LISTEN(已建立连接)

简单流程: CLOSED(客户端) -> SYN-SEND(客户端) -> CLOSED+LISTEN(服务器) -> SYN-RECV(服务器) -> ESTAB-LISTEN(客户端) -> ESTAB-LISTEN(服务器)
"""
开始阶段: 服务器在客户端发送请求之前一直处于`LISTEN`状态, 等待客户端的连接请求. 如有, 则立即做出响应.

第一次握手: TCP客户端进程打算建立连接时, 向服务器发送请求报文段, 这时首部中的同步位`SYN=1`, 同时选择一个初始序列`seq=x`. 请求报文段发送出去以后, TCP客户端进入`SYN_SEND(同步已发送)`状态.(提示: TCP协议规定, SYN报文段, 即SYN=1的报文段不能携带数据, 但要消耗一个序列号. 这里我们消耗了一个初始序列号x)

第二次握手: 服务器在接收到客户端的请求报文后, 如同意连接, 则向客户端发送确认. 再确认报文中应把SYN位和ACK位都重置为1, 确认号是`ack=x+1`(提示: 这个确认号是成功收到客户端请求的序列号的基础之上加1), 同时也为自己选择一个初始序列号`seq=y`. 这时TCP服务器进程进入`SYN_RECV(同步收到)`状态.(提示: 这个报文段也不能携带数据, 但同样要消耗一个序列号.)

第三次握手: TCP客户进程收到服务器的确认后, 还要向服务器给出确认. 此时将确认报文段的ACK重置为1, 确认号`ack=y+1`, 而自己的序列号`seq=x+1`. TCP的标准规定, ACK报文段可以携带数据. 但如果不携带数据则不消耗序列号. 因此, 一个数据报文段的序号仍是`seq=x+1`(刚刚服务器反馈回来的ack=x+1). 这时客户端通往服务器的TCP连接已经建立, 客户端进入`ESTAB-LISHED(已建立连接)`状态. 接着服务器收到TCP客户端进程的确认后, 也进入了`ESTAB-LISHED`状态, 服务器通过客户端的TCP链接也几经建立. 即建立双向连接成功.
"""
  • TCP四次挥手
ESABLE-LISTEN(已建立连接)                             ESABLE-LISTEN(已建立连接)
          -------------- FIN=1 seq=u --------------> 
FIN-WAIT-1(等待终止-1)                                CLOSE-WAIT(等待关闭)
          <---------- ACK=1 ack=u+1 seq=v ----------
FIN-WAIT-2(等待终止-2)    
          <------- FIN=1 ACK=1 ack=u+1 seq=w  ------ 
TIME-WAIT(时间等待)                                   LASE-ACK(等待确认)
 ↓(2SML)  --------- ACK=1 ack=w+1 seq=u+1 --------->
CLOSED                                               CLOSED

简单流程: ESTAB-LISTEN(客户端) -> ESTAB-LISTEN+LISTEN(服务器) -> FIN-WAIT-1(客户端) -> CLOSE-WAIT(服务器) -> FIN-WAIT-2(客户端) -> LAST-ACK(服务器) -> TIME-WAIT(客户端) -> CLOSED(服务器) -> 2SML -> CLOSED(客户端)
"""
提示: 这里以客户端主动发起的FIN结束请求为例. 数据传输结束后, 通信得双方都可释放连接. 因为三次报文的成功握手, 通信双方都处于`ESTABLISTH`状态.

第一次挥手: 客户端的应用进程先向其TCP发出连接释放报文, 并停止再发送数据, 主动关闭TCP连接.客户端把连接释放报文段首部的终止控制位FIN置1, 其序号`seq=u`, 它等于前面已经传送过的数据的最后一个字节的序号加1. 这时客户端进入`FIN-WAIT-1(等待终止1)`状态, 等待服务器的确认. (注意: TCP规定, FIN报文段即使不携带数据, 它也消耗一个序号)

第二次挥手: 服务器收到连接释放报文段后立即发出确认, 确认号是`ack=u+1`, 而这个报文段自己的序号是v, 它等于前面已经传送过的数据的最后一个字节的序号加1. 然后服务器就进入`CLOSE-WAIT(关闭等待)`状态. TCP服务器进程这时应该通知高层应用进程, 因而从客户端 -> 服务器这个方向的连接就释放了, 这时TCP连接处于(half-close)半关闭状态, 即客户端已经没有数据要发送了, 但服务器若要发送数据, 客户端任然需要接收. 也就是说, 从服务器 -> 客户端这个方向的连接并没有关闭, 这个状态可能会持续一段时间. 客户端收到来自服务器的确认后, 就要进入`FIN-WAIT-2(终止等待2)`状态, 等待服务器发出的连接释放报文段.

第三次挥手: 若服务器已经没有数据发送给客户端, 其应用进程就通知TCP释放连接. 这时服务器发出的连接释放报文段必须使用`FIN=1`. 现假定服务器的序号为w(在半关闭状态服务器可能又发送了一些数据). 服务器还必须重复上次已经发送过的确认号`ack=u+1`. 这时服务器就进入`LAST-ACk(最后确认)`状态, 等待客户端的确认.

第四次挥手: 客户端在收到服务器的连接释放报文段后, 必须对此发出确认. 再确认报文段中把ACk置1, 确认ack=w+1, 而自己的序号是seq=u+1(更具TCP标准, 前面发送过的FIN报文段要消耗一个序列号). 然后进入到`TIME-WAIT(时间等待)`状态. 注意!!, 现在TCP连接还没有释放掉. 必须经过时间等待计时期(TIME-WAIT timer)设置的时间2MSL后, 客户端才进入到`CLOSED`状态. 时间MSL叫做最长报文段寿命(MaXmun Segment Lifetime), RFC793建议设置为2分钟. 但这完全是从工厂上来考虑的, 对与现在的网络, MSL=2分钟可能太长了一些. 因此TCP允许不同的实现可更具具体情况进入到`CLOSED`状态. 因此, 从客户端进入到`TIME-WAIT`状态后, 要经过4分钟才能进入到`CLOSED`状态, 才能开始建立下一个新的连接. 当客户端撤销相应的传输控制块TCB后, 就结束了这次的TCP连接. 于此同时, 服务器只要收到了客户端发出的确认, 就进入`CLOSED`状态. 同样, 服务器在撤销相应的传输控制块TCB后, 就结束了这次的TCP连接. 我们注意到. 服务器结束TCP连接的时间要比客户端早一些.
"""

2. 什么是HTTP协议

"""
超文本传输协议 用来规定服务端和浏览器之间的数据交互的格式...

该协议你可以不遵循 但是你写的服务端就不能被浏览器正常访问 你就自己跟自己玩
你就自己写客户端 用户想要使用 就下载你专门的app即可
"""

# 四大特性
    1. 基于请求响应. 向服务端发送请求, 服务端响应客户端请求.
    2. 基于TCP/IP之上的作用于应用层的协议
    3. 无状态: 不保存用户的信息
        举例: egon这个人来了一千次 你都记不住 每次都当他如初见.
        拓展: 由于HTTP协议是无状态的 所以后续出现了一些专门用来记录用户状态的技术. cookie、session、token...
    4. 无链接&短链接
  	    请求来一次我响应一次 之后我们两个就没有任何链接和关系.
        拓展: 长链接. 之后出现了websocket可以实现长链接, 可以让双方建立连接之后默认不断开. 可以实现: 群聊功能、服务端主动给客户端发送消息
            
            
# 请求数据格式
    请求首行(标识HTTP协议版本,当前请求方式)
    请求头(一大堆k,v键值对)
    \r\n
    请求体(并不是所有的请求方式都有. get没有post有, post存放的是请求提交的敏感数据)            
    
    
# 响应数据格式
    响应首行(标识HTTP协议版本,响应状态码)
    响应头(一大堆k,v键值对)
    \r\n
    响应体(返回给浏览器展示给用户看的数据)   
    
    
# 响应状态码: 用一串简单的数字来表示一些复杂的状态或者描述性信息  例如: 返回响应状态码为404, 则表示请求资源不存在.
    1XX: 信息. 服务器收到请求,需要请求者继续执行操作.
    2XX: 成功. 操作被服务器成功接收并处理.
        200 OK 表明该请求被成功地完成,所请求的资源发送回客户端.
    3XX: 重定向. 需要进一步的操作以完成请求.(比如: 当你在访问一个需要登陆之后才能看的页面 你会发现会自动跳转到登陆页面)
    4XX: 客户端请求错误. 请求包含语法错误或无法完成请求
        404: 请求资源不存在(服务器无法根据客户端的请求找到对应的网页资源)
        403: 服务器理解请求客户端的请求,但是拒绝执行此请求.(当前请求不合法或者不符合访问资源的条件. 比如: 这是千万级别的俱乐部, 只有999万的你被限制无法进入)
    5XX: 服务器内部错误 
        500: 服务器内部错误,无法完成请求
 
	补充: 上述的状态码是HTTP协议规定的,其实到了公司之后每个公司还会自己定制自己的状态及提示信息

        
# 请求方式
    1.get请求: 朝服务端要数据. 
        eg: 输入网址获取对应的内容
    2.post请求: 朝服务端提交数据
        eg: 用户登陆 输入用户名和密码之后 提交到服务端后端做身份校验

    get和post方法的区别:
        1.  GET提交的数据会放在URL之后,以?分割URL和传输数据,参数之间以&相连,如EditPosts.aspx?name=test1&id=123456.  POST方法是把提交的数据放在HTTP包的Body中.
        2. GET提交的数据大小有限制(因为浏览器对URL的长度有限制),而POST方法提交的数据没有限制.
        3. GET方式提交数据,会带来安全问题,比如一个登录页面,通过GET方式提交数据时,用户名和密码将出现在URL上,如果页面可以被缓存或者其他人可以访问这台机器,就可以从历史记录获得该用户的账号和密码.
            
# url: 统一资源定位符(大白话 网址) 
    形式: scheme:[//[user:password@]host[:port]][/]path[?query-string][#anchor]

    提示: 方框内的是可选部分。
    scheme:协议(例如:http, https, ftp)
    user : password@用户的登录名以及密码
    host:服务器的IP地址或者域名
    port:服务器的端口(如果是走协议默认端口,http 80 or https 443)
    path:访问资源的路径
    query-string:参数,它通常使用键值对来制定发送给http服务器的数据
    anchor:锚(跳转到网页的指定锚点位置)

二. 并发编程

参考: 操作系统, 多进程, 多线程, 协程, 网络IO模型

1. 什么是GIL全局解释器锁?

"""
关键字: Cpython, 解释器级别的锁, Cpyhotn解释器的内存管理不是线程安全的, 垃圾回收机制线程GC(引用计数, 标记-清除, 分代回收), 无法利用多核优势, 解释器语言的通病.

1. GIL是Cpython解释器的特点 不是python的特点!!!
2. GIL本质也是一把互斥锁, 但它是解释器级别的锁. 争对不同级别的数据需要用不同的锁处理. 比如: 争对解释器级别的数据, 使用了GIL. 争对用户级别的数据, 使用了Lock. 
3. GIL的存在是因为Cpyhotn解释器的内存管理不是线程安全的.
4. GIL的存在虽然让同一个进程下的多个线程无法同时执行,但保证了解释器级别的数据的安全. 是因为在每个开启的进程当中都包含一个垃圾回收机制(GC)线程的存在, 垃圾回收机制中的标记/清除算法, 会遍历堆区的所有对象, 
    将没有标记为0的对象清除. 因为垃圾回收机制的存在, 很可能某个线程刚开启并执行的过程中某个即将被执行的值还没有被标记, 就被垃圾回收机制线程拿到了python解释器的交给CPU执行, 执行完毕以后刚刚的值就被清除了.  ==> 垃圾回收机制
     1) 什么是垃圾回收机制?
            垃圾回收机制简称GC, 是python解释器自带的一种机制, 专门用来回收不可用的变量值所占用的内存空间.

     2) 为什么要用垃圾回收机制?
            程序的运行过程中会申请大量的内存空间, 而对于一些无用的内存空间如果不及时清理的话会导致内存空间是用殆尽, 进而造成内存溢出, 最终导致程序奔溃. 但是管理内存是一件非常繁琐且复杂的事情, python则提供了垃圾回收机制来帮我们程序从复杂的内存管理中解放出来.

     3) 垃圾回收机制的三大算法工作模式:
          1) 引用计数: 跟踪回收垃圾
                引用计数又分直接应用, 间接引用. 如果'值'引用计数为0, '值'占用的内存地址将会被垃圾回收机制回收.

          2) 标记/清除: 解决容器类型的循环引用问题(注意: 会在内存快被占满的时候生效)
                执行前提: 当应用程序可用内存空间快被耗尽.
                标记: 有根之人当活, 无根之人当死. 根指的栈区, 也就是说可以通过栈区间接或者直接可以访问到堆区的对象的数据会被保留. 否则执行清除. 
                清除: 遍历堆区中所有对象, 将没有标记的对象全部清除.

          3) 分代回收: 解决引用计数每次回收内存都需要遍历所有对象的效率问题.
                根据存活时间划分扫描频率. 刚来的数据权重最低, 扫描频率最高. 数据存活时间越长权重越高, 扫描频率越低.
            
5. GIL间接导致了在同一个进程下多个线程无法同时执行, 从而无法利用多核优势.
6. 解释器语言的通病: 同一个进程下的多个线程无法利用多核优势.
"""

2. 什么是进程,线程,协程,程序中如何依次创建/实现它们?

进程: 资源单位. 正在进行的一个程序或者说一个任务. 每个进程自带一个线程. 起一个进程仅仅只是在内存空间中开辟一块独立的空间.

线程: 执行单位. 真正被CPU执行的其实是进程里面的线程,线程指的就是代码的执行过程, 执行代码中所需要使用到的数据或者叫资源,都是去找其所在的进程索要.

协程: 单线程下实现并发.  (提示: 这个概念完全是程序员自己意淫出来的根本不存在)  
  • 创建进程
# 第一种实现方式: 直接使用Process开启
import time
import os
from multiprocessing import Process


def task(name):
    print(f'{name} is running...', os.getpid())
    time.sleep(1)
    print(f'{name} is end.......', os.getpid())

if __name__ == '__main__':
    p = Process(target=task, args=('tank',))
    p.start()
    print('主', os.getpid())



# 第二种实现方式: 定义类继承Process
import time
import os
from multiprocessing import Process


class MyProcess(Process):
    def __init__(self, name):
        super().__init__()
        self.name = name

    def run(self) -> None:
        print(f'{self.name} is running...')
        time.sleep(1)
        print(f'{self.name} is end.......')


if __name__ == '__main__':
    p = MyProcess('jsaon')
    p.run()
    print('主', os.getpid())
  • 创建线程
# 第一种实现方式: 直接使用Thread开启
import time
from threading import Thread, current_thread

def task():
    print(f'{current_thread().name} is running...')
    time.sleep(1)
    print(f'{current_thread().name} is end.......')


t = Thread(target=task)
t.start()
print('主', current_thread().name)



# 第二种实现方式: 定义类继承Thread
import time
from threading import Thread, current_thread

class MyThread(Thread):
    def __init__(self, name):
        super().__init__()
        self.name = name

    def run(self):
        print(f'{self.name} is running...')
        time.sleep(1)
        print(f'{self.name} is end.......')


t = MyThread('jason')
t.run()
print('主', current_thread().name)
  • 创建协程
# 创建协程: 使用gevent第三方模块
from gevent import monkey;monkey.patch_all()
import gevent
import time


def task1(n):
    print(f'{n} is running...')
    time.sleep(1)
    print(f'{n} is end.......')


def task2(n):
    print(f'{n} is running...')
    time.sleep(2)
    print(f'{n} is end.......')


start_time = time.time()
g1 = gevent.spawn(task1, 'egon')
g2 = gevent.spawn(task2, 'tank')
gevent.joinall([g1, g2])
print(time.time() - start_time)  # 2.0058369636535645
posted @ 2020-05-11 20:30  给你加马桶唱疏通  阅读(154)  评论(0编辑  收藏  举报