网络并发每日习题解释版

网络并发每日习题解释版

1.软件开发架构类别

软件开发架构类别:

  • 软件开发架构是指在软件设计和开发过程中,用于组织和管理软件系统的基本结构。

常见的软件开发架构类别包括:

分层架构(Layered Architecture):

  • 将软件系统划分为多个相互独立的层,每个层都有特定的功能和责任。

客户端-服务器架构(Client-Server Architecture):

  • 将软件系统划分为客户端和服务器两个部分,客户端向服务器请求服务并接收响应。

管道和过滤器架构(Pipe and Filter Architecture):

  • 将软件系统划分为一系列处理组件(过滤器),每个组件按照特定顺序进行数据处理。

MVC架构(Model-View-Controller Architecture):

  • 将软件系统划分为模型、视图和控制器三个核心组件,实现数据、界面和业务逻辑的分离。

微服务架构(Microservices Architecture):

  • 将软件系统划分为一系列独立的小型服务,每个服务都专注于完成特定的任务。

2.osi七层及各自功能

  • OSI(Open Systems Interconnection)是一个国际标准化的网络通信模型,共分为七层。

物理层(Physical Layer):

  • 负责传输比特流,管理物理设备和传输介质。
  • 通过物理层提供的传输服务,将数据转换为帧,控制与物理层的通信。

网络层(Network Layer):

  • 负责在网络中传输数据包,并进行路径选择和拆分、封装等操作。

传输层(Transport Layer):

  • 提供端到端的可靠数据传输,确保数据正确无误地到达目标。

会话层(Session Layer):

  • 负责建立、管理和结束会话,提供应用之间的数据交换服务。

表示层(Presentation Layer):

  • 处理数据的表示形式,对数据进行加密、解密、压缩和格式转换等操作。

应用层(Application Layer):

  • 为用户提供服务,实现各种应用程序的功能。

3.谈谈你对tcp三次握手和四次挥手的理解

对TCP三次握手和四次挥手的理解:

  • TCP(Transmission Control Protocol)是一种面向连接的可靠传输协议,它使用三次握手和四次挥手来建立和终止连接。

三次握手(Three-way Handshake)

是建立TCP连接的过程,包括以下步骤:

  1. 客户端发送一个SYN(同步)包给服务器,请求建立连接。
  2. 服务器收到SYN包后,回复一个SYN+ACK(同步+确认)包给客户端,表示接受连接请求。
  3. 客户端收到服务器的SYN+ACK包后,再发送一个ACK(确认)包给服务器,表示连接建立成功。

四次挥手(Four-way Handshake)

是终止TCP连接的过程,包括以下步骤:

  1. 客户端发送一个FIN(结束)包给服务器,表示不再发送数据。
  2. 服务器收到FIN包后,发送一个ACK包给客户端,确认收到FIN包。
  3. 服务器准备好关闭连接后,发送一个FIN包给客户端,表示不再发送数据。
  4. 客户端收到服务器的FIN包后,发送一个ACK包给服务器,确认收到FIN包,完成连接的关闭。

通过三次握手,客户端和服务器建立起可靠的连接;

而通过四次挥手,双方协商关闭连接并释放相关资源。

这样可以确保数据能够可靠地传输并正确关闭连接。

4.描述粘包现象

粘包现象是在计算机通信中常见的问题

它指的是数据在传输过程中由于一些原因被组合在一起形成连续的数据块,导致接收方无法正确解析和处理这些数据。

粘包现象的主要原因有以下几种:

  • 发送端数据发送速度过快:

    • 发送端在短时间内连续发送多个数据包,但接收端可能无法及时处理这些数据。当接收方在解析一个数据包时,下一个数据包已经到达,导致数据粘合在一起形成粘包现象。
  • 接收端读取数据不及时:

    • 接收端处理数据的速度没有跟上发送端的速度,导致发送的多个数据包在接收端未能及时处理,从而形成粘包现象。
  • TCP协议的滑动窗口机制:

    • TCP协议使用滑动窗口机制来提高数据传输的效率,但这也可能导致粘包现象。

    • 当发送端的滑动窗口尺寸大于接收端的缓冲区大小时,会将多个数据包发送到接收端,从而形成粘包。

  • 网络拥塞:

    • 当网络中存在拥塞时,数据包在传输过程中可能会出现延迟,导致数据粘在一起形成粘包现象。
  • 数据包大小限制:

    • 某些网络设备或协议对数据包大小有限制,当发送端发送的数据包超过了这个限制,就会发生粘包现象。

5.写出解决粘包现象的思路

  • 增加数据包边界标识:

    • 在发送端和接收端添加特定的数据包边界标识,以便接收端可以准确地将不同的数据包分开处理。
  • 使用定长数据包:

    • 发送端将数据按照固定长度进行分包,并在接收端按照相同长度进行解析,从而避免数据粘合在一起。
  • 引入消息头/消息尾:

    • 在数据包的头部或尾部添加消息头/消息尾,其中包含数据包的长度信息,接收端通过读取消息头/消息尾来准确定位每个数据包的边界。
  • 使用应用层协议:

    • 采用应用层协议可以处理粘包问题,例如在数据包中添加指定字段表示数据包长度,或者使用特定的分隔符将不同的数据包分开。
  • 控制发送速度和接收速度:

    • 通过控制发送端和接收端的数据处理速度,使得两者能够相互匹配,避免数据粘包现象的发生。

粘包现象是计算机通信中常见的问题,可以通过增加数据包边界标识、使用定长数据包、引入消息头/消息尾、使用应用层协议以及控制发送和接收速度等方法来解决。

6.struct模块如何使用

在Python中,struct模块用于处理二进制数据和C结构体之间的转换。

它提供了一组函数,可以将数据打包成二进制字符串(pack),或者从二进制字符串中解析出数据(unpack)。

【1】导入struct模块:

import struct

【2】将数据打包成二进制字符串:

  • 使用struct.pack(format, v1, v2, ...)函数将数据按照指定的格式(format)打包成二进制字符串。
  • format是一个描述打包规则的字符串,v1、v2等是要打包的数据值。

例如,将整数和浮点数打包成二进制字符串:

data = struct.pack('i f', 42, 3.14)

这里的'i'表示整数类型,'f'表示浮点数类型。pack函数返回的是打包后的二进制字符串。

【3】从二进制字符串解析出数据:

  • 使用struct.unpack(format, buffer)函数从二进制字符串中解析出数据。
  • format是与打包时使用的格式相同的字符串,buffer是包含二进制数据的字符串。

例如,从上一步得到的二进制字符串中解析出整数和浮点数:

result = struct.unpack('i f', data)

unpack函数返回一个元组,元组中包含了按照格式解析出的数据。

【4】小结

  • 需要注意的是
    • 打包和解析时使用的格式要一致,否则可能会导致数据解析错误。
  • struct模块还提供了其他函数,用于处理更复杂的数据类型、字节顺序等问题。
  • 总结起来,使用struct模块时,首先导入模块,然后使用pack函数将数据打包成二进制字符串,或使用unpack函数从二进制字符串中解析出数据。

7.如何开启进程、进程类中的参数及方法有哪些?

  • 要开启一个进程,可以使用Python中的multiprocessing模块。
  • 下面是使用multiprocessing.Process类来创建并开启一个进程的示例代码:
import multiprocessing

def my_process_func(arg1, arg2):
    # 进程要执行的任务逻辑
    print("进程参数:", arg1, arg2)

if __name__ == "__main__":
    # 创建一个进程对象,并传入要执行的函数和参数
    p = multiprocessing.Process(target=my_process_func, args=("参数1", "参数2"))
    
    # 开启进程
    p.start()
  • 在上面的示例中,my_process_func是一个自定义的函数,表示要在新进程中执行的任务逻辑。

    • arg1arg2是传递给该函数的参数。
    • multiprocessing.Process类接收一个target参数,设置要执行的函数名称,也可以通过args参数传递函数的参数。
  • 除了args参数之外

    • multiprocessing.Process类还有其他可选参数,用于控制进程的行为和属性。以下是一些常用的参数和方法:

    • name:设定进程的名称。

    • daemon:设定进程是否为守护进程。

    • is_alive():判断进程是否存活。

    • start():开启进程执行。

    • join([timeout]):等待进程执行完毕。可选的timeout参数表示最长等待时间。

    • terminate():终止进程的执行。

8.如何开启多进程,如何让所有子进程先执行完毕再执行主进程

  • 要开启多个子进程并让它们执行完毕后再执行主进程,可以使用Python中的multiprocessing模块中的Process类和join()方法。
  • 下面是一个示例代码:
import multiprocessing

def my_process_func(arg):
    # 子进程要执行的任务逻辑
    print("子进程参数:", arg)

if __name__ == "__main__":
    processes = []  # 用于存放子进程对象
    
    for i in range(5):
        # 创建一个子进程对象,并传入要的函数和参数
        p = multiprocessing.Process(target=my_process_func, args=(i,))
        processes.append(p)  # 将子进程对象添加到列表中
        p.start()  # 开启子进程执行
    
    # 等待所有子进程执行完毕
    for p in processes:
        p.join()
    
    # 主进程继续执行的逻辑...
    print("主进程执行完毕")
  • 在上面的示例中

    • 首先创建了一个空的列表processes,用于存放子进程对象。
    • 然后使用循环创建了5个子进程对象,每个子进程都执行my_process_func函数,并传递不同的参数。
    • 将每个子进程对象添加到processes列表中,并调用start()方法开启子进程执行。
  • 接下来,使用另一个循环遍历processes列表中的子进程对象,调用join()方法等待所有子进程执行完毕,这样主进程会在所有子进程执行完毕后才继续执行。

  • 最后,可以在主进程中添加需要执行的逻辑。示例代码中打印了一条消息表示主进程执行完毕。

  • 这样,当所有子进程执行完毕后,主进程才会接着执行。

9.使用进程锁的目的是什么,怎么加进程锁

  • 进程锁(Process Lock),也称为互斥锁(Mutex Lock),是一种用于控制多个进程对共享资源进行访问的机制。
  • 它的主要目的是防止多个进程同时访问临界区(Critical Section),避免出现竞态条件(Race Condition)和数据不一致的情况。
  • 当多个进程需要同时访问某个共享资源时,如果没有进程锁的保护,就可能会导致数据的不一致性或者程序的不确定行为。
  • 通过使用进程锁,可以确保同一时间只有一个进程能够进入临界区,从而保证对共享资源的安全访问。
  • 要加上进程锁,可以使用Python中的multiprocessing模块中的Lock类。下面是一个示例代码:
import multiprocessing

def my_process_func(lock, shared_data):
    lock.acquire()  # 加锁

    try:
        # 在临界区进行共享资源的操作
        shared_data.value += 1
        print("进程ID:", multiprocessing.current_process().pid, "共享数据:", shared_data.value)
    finally:
        lock.release()  # 释放锁

if __name__ == "__main__":
    lock = multiprocessing.Lock()  # 创建进程锁
    shared_data = multiprocessing.Value('i', )  # 创建共享数据
    
    processes = []
    
    for i in range(5):
        p = multiprocessing.Process(target=my_process_func, args=(lock, shared_data))
        processes.append(p)
        p.start()
    
    for p in processes:
        p.join()
  • 在上面的示例中,首先创建了一个进程锁对象lock和一个共享数据对象shared_data,通过multiprocessing.Value()创建了一个可被多个进程访问的整型共享数据。

  • 然后,在子进程的函数my_process_func中,使用lock.acquire()来请求获取锁。在临界区内,对共享数据进行操作,确保只有获得锁的进程可以访问共享数据,并按照需要更新它。最后,使用lock.release()释放锁。

  • 在主程序中,通过将进程锁对象和共享数据对象作为参数传递给子进程的函数。每个子进程都会根据需要获取并释放进程锁,并对共享数据进行安全操作。

  • 这样,通过加入进程锁的机制,可以保证同一时间只有一个进程能够进入临界区,有效地解决了多个进程对共享资源的并发访问问题。

10.查看进程号的方式有哪些?

在Windows操作系统中

  1. 任务管理器:按下“Ctrl+Shift+Esc”组合键,打开任务管理器。切换到“进程”(Processes)选项卡,可以查看所有正在运行的进程及其相应的进程号。

  2. 命令行工具:打开命令提示符窗口(CMD),输入"tasklist"命令,可以列出所有正在运行的进程及其相关信息,包括进程号。

例如:

tasklist
  1. PowerShell:打开PowerShell窗口,输入"Get-Process"命令,可以显示当前正在运行的进程及其详细信息,包括进程号。

例如:

Get-Process

以上这些方法都可以帮助您在Windows操作系统中查看进程号。

在Linux和Unix系统中

  • 可以使用命令行工具来查看进程号。

  • 常用的命令包括:

  • ps命令:

    • 用于列出当前运行的进程。可以使用ps aux命令来显示详细信息,包括进程号。
  • top命令:

    • 实时显示系统中各个进程的状态和资源占用情况。进入top后按下"Shift+P"可以按照CPU占用排序,按下"Shift+N"可以按照进程ID排序。
  • pgrep命令:

    • 用于通过进程名查找对应的进程号。例如,pgrep nginx会返回所有名为"nginx"的进程的进程号。

11.如何开启线程,线程类的参数及方法有哪些?

使用threading.Thread类:

  • 参数:Thread类的构造函数接受一个代表线程执行代码的函数或方法作为参数。这个函数或方法将作为新线程的入口点。
  • 方法:
    • start()方法:用于启动线程。
    • join([timeout])方法:用于等待线程执行完成。可选的timeout参数指定等待时间(以秒为单位)。
    • run()方法:线程的执行代码逻辑写在该方法内。

示例代码:

import threading

def do_work():
    # 线程执行的代码逻辑

new_thread = threading.Thread(target=do_work)
new_thread.start()

使用concurrent.futures模块中的ThreadPoolExecutor类(适用于Python 3及更高版本):

  • 参数:ThreadPoolExecutor类的构造函数接受一个整数参数,指定线程池中的线程数量。
  • 方法:
    • submit(fn, *args, **kwargs)方法:用于提交线程执行的函数或方法,返回一个表示异步计算结果的Future对象。
    • shutdown(wait=True)方法:用于关闭线程池。wait参数指定是否等待所有线程执行完成。

示例代码:

from concurrent.futures import ThreadPoolExecutor

def do_work():
    # 线程执行的代码逻辑

with ThreadPoolExecutor() as executor:
    future = executor.submit(do_work)
    # 访问future对象可以获取线程的返回结果

12.什么是生产者消费者模型,解决什么问题?

  • 生产者消费者模型是一种常见的并发编程模式,用于解决生产者和消费者之间的数据交互和协作问题。
    • 在该模型中,生产者负责生成数据,并将数据传递给消费者进行处理,而消费者则负责处理数据。

生产者消费者模型解决了以下问题:

  1. 生产者和消费者之间的解耦:生产者和消费者可以独立地进行操作,不需要彼此直接通信。通过引入一个缓冲区,生产者将数据放入缓冲区,而消费者从缓冲区中获取数据进行处理。

  2. 平衡生产者和消费者的速度差异:生产者和消费者可能具有不同的速度,如果没有合适的机制来平衡二者之间的速度差异,就可能导致资源浪费或者阻塞。生产者消费者模型通过引入缓冲区作为一个中间层,可以实现生产者和消费者的异步操作,使得二者的速度可以相对独立,提高系统的整体效率。

  3. 缓冲区的有界性和安全性:缓冲区可以是有界的,限制了生产者产生的数据量,也可以是无界的。在有界缓冲区中,当缓冲区已满时,生产者需要等待,直到有消费者取走了数据之后才能继续生产。同时,在多线程情况下,需要保证缓冲区的安全性,避免出现竞态条件等问题。

  • 通过使用生产者消费者模型,可以实现任务的异步处理和解耦,提高系统整体的并发性能和扩展性。该模型常见的应用场景包括消息队列、线程池、事件驱动等。

13.进程之间如何通信?

进程之间可以通过多种方式进行通信,以下是常见的几种进程间通信(IPC)的方式:

  1. 管道(Pipe):管道是一种半双工的通信机制,可以在父子进程或具有亲缘关系的进程之间进行通信。管道有两种类型:匿名管道和命名管道。匿名管道只能用于父子进程之间的通信,命名管道可以用于无亲缘关系的进程之间。

  2. 命名管道(FIFO):命名管道也被称为FIFO(First In First Out),它是一种特殊的文件,提供了一个路径名,让不相关的进程可以通过该路径来进行通信。

  3. 信号量(Semaphore):信号量是一种计数器,用于多个进程之间的同步和互斥。它可以用来解决进程竞争资源的问题,通过对信号量的P操作(申请资源)和V操作(释放资源)来实现进程间的同步和互斥。

  4. 消息队列(Message Queue):消息队列是一种在进程之间传递数据的机制,可以通过发送消息到队列中,其他进程从队列中读取消息。消息队列支持不同类型的消息,可以有优先级,简化了进程之间通信的处理。

  5. 共享内存(Shared Memory):共享内存是一种将相同的物理内存映射到不同进程的虚拟地址空间中,实现进程之间数据共享的机制。多个进程可以直接读写共享内存中的数据,而无需进行数据的复制和传输。

  6. 套接字(Socket):套接字是一种用于网络通信的IPC机制,允许不同的主机之间的进程进行通信。套接字提供了一组网络通信的API,可支持TCP/IP等协议栈,用于实现进程间的网络通信。

14.什么是GIL,有哪些主要特征

GIL(全局解释器锁)

  • GIL(全局解释器锁)是Python解释器中的一个机制
  • 它用于保证在解释器级别上同一时刻只有一个线程能够执行Python字节码。

GIL的主要特征

  • 互斥性:
    • 在解释器级别上,同一时刻只允许一个线程执行Python字节码。
  • 全局性:
    • GIL是针对整个解释器的,而不是针对单个线程或单个线程锁的。
  • 影响效率:
    • 由于GIL的存在,阻碍了多线程并行执行任务的效率提升。
    • 因为在任意时刻只有一个线程能够执行,其他线程只能等待。
    • 这意味着即使在多核CPU架构下,Python解释器无法发挥多核处理器的潜力。

15.python中开多线程是否有用,原因是

有用

  • 在Python中,开启多线程的确可以在某些情况下有用,但其好处受到GIL的影响而有限。

  • 多线程在以下几种情况下可能仍然有效:

    • IO密集型任务:

      • 如果任务主要涉及等待IO操作(如文件读写、网络请求等),多线程可以通过在等待IO过程中切换线程来提高整体效率。
    • 充分利用外部库:

      • 某些第三方库(如numpy、OpenCV等)可以绕过GIL的限制,通过底层的C代码实现并行计算,从而允许在多个线程中执行相关操作。
    • 并发处理:

      • 对于涉及到大量并行处理的任务,虽然每个线程受到GIL影响,但可以使用多个进程,在每个进程内来开启多线程,以达到整体并行处理的效果。

16.互斥锁的功能及代码使用方式

互斥锁(Mutex Lock)是用于实现临界区互斥访问的一种同步机制,功能包括:

  • 保护共享资源:当多个线程(或进程)需要访问共享的临界区资源时,互斥锁可以确保只有一个线程(或进程)能进入该临界区,避免数据竞争和不一致性问题。
  • 防止死锁:互斥锁提供了加锁(Lock)和释放锁(Unlock)两个基本操作,可以协调线程对临界区资源的请求和释放,避免死锁情况的发生。
  • 同步线程:互斥锁可以被用来同步多个线程的执行顺序,使得线程能够按照特定的顺序执行,保证数据的一致性和可靠性。

使用互斥锁的代码方式如下:

import threading

# 创建互斥锁对象
lock = threading.Lock()

def func():
    # 上锁
    lock.acquire()
    try:
        # 需要保护的临界区代码块
        # ...
    finally:
        # 释放锁
        lock.release()

# 创建线程并启动
thread = threading.Thread(target=func)
thread.start()
  • 在使用互斥锁时
    • 需要明确使用acquire()来申请锁
    • 然后在临界区代码块中进行需要保护的操作
    • 最后使用release()来释放锁。
  • 这样可以确保同一时刻只有一个线程能够进入临界区执行相关代码,实现互斥访问的效果。
posted @ 2023-07-26 10:35  Chimengmeng  阅读(10)  评论(0编辑  收藏  举报