python 多进程局域网扫描工具 socket应用

该程序主要使用socket、multiprocessing,利用multiprocessing中的pool进程池创建多个进程对于局域网上存活的主机端口进行扫描。程序中会用到re模块,使用正则表达式提取ip地址,也用到了os模块调用外部程序。程序运行使用的解释器版本是python 3.8,下面是python38官方文档的网址和python标准库的网址,可以利用这两个网址查询下文未介绍到的函数功能及用法

3.8.16 文档 (python.org)

Python 标准库 — Python 3.8.16 文档

一共包括五个函数 usual_ports()、ping()、host_find()、port_open()、port_scan() 下面分别介绍五个函数的构成以及作用

一、usual_ports()

def usual_ports():
    # get port name
    print('开始获取常用端口信息......')
    ports_service = dict()   #创建一个字典保存信息
    for port in list (range(1,65535)):
        try:
            ports_service[port] = socket.getservbyport(port)
        except socket.error:
            pass
    print('端口信息获取完毕......\n')
    return ports_service

TCP/IP协议规定计算机中端口号是用2个字节16bit来表示,所以取值范围是0-65535,它标志本计算机应用层中的各个进程在和运输层交互时的层间接口。getservbyport函数可以将端口号转化为端口的服务名称

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mh4K7mqK-1693013585785)(C:\Users\Charlie\AppData\Roaming\Typora\typora-user-images\image-20230214163312509.png)]

将ports_service信息打印出来如下

{7: 'echo', 9: 'discard', 11: 'systat', 13: 'daytime', 17: 'qotd', 19: 'chargen', 20: 'ftp-data', 21: 'ftp',  后面省略。。。

二、ping()

def ping(ip,type):
    output = popen('ping -%s 1 %s'%(type,ip)).readlines()
    for w in output:
        if str(w).upper().find('TTL') >= 0:
            print(ip,'  ok')
            return ip

这个函数是调用外部程序(如同进入cmd输入命令执行)对局域网内的各个ip地址发送ICMP回送请求报文,也就是常用的ping命令,output保存命令执行结果如下,readlines()方法将一行行读取下面内容并保存在一个列表中

C:\Users\Charlie>ping -n 1 192.168.1.58

正在 Ping 192.168.1.58 具有 32 字节的数据:
来自 192.168.1.58 的回复: 字节=32 时间=2ms TTL=64

192.168.1.58 的 Ping 统计信息:
    数据包: 已发送 = 1,已接收 = 1,丢失 = 0 (0% 丢失),
往返行程的估计时间(以毫秒为单位):
    最短 = 2ms,最长 = 2ms,平均 = 2ms

if判断是否含有TTL,如果含有则find函数返回值会>=0,如果不含有find返回值为-1,TTL是ICMP应答的一部分参数

只有有ICMP应答的ip地址才是存活的主机地址

三、host_find()

这个函数用来发现局域网中存活的主机,使用多进程调用外部程序执行ping命令,返回局域网中存活的主机ip地址列表

def host_find():
    host_ip = []
    local_system = system() #获取系统类型
    if local_system == 'Windows':
        type = 'n'
    elif local_system == 'Linux':
        type = 'c'
    else:
        print(system(),'是不支持的系统类型')
        sys.exit()

    net = re.findall(r'\d+\.\d+\.\d+\.', gethostbyname(gethostname()))[0]
    print('开始扫描存活主机\n网段:%s\n进程数量:%s\n--------------'%(net+'0',cpu_count()-2))
    m = multiprocessing.Manager()
    pool = multiprocessing.Pool( processes = cpu_count()-2 )
    
    for ip in [ net+str(i) for i in range(1,255) ]:
        host_ip.append( (pool.apply_async( ping , args = ( ip,type ) ) ) )  #apply_async函数返回值类型为类对象 在此处使用get函数提取的话会导致不能异步执行
    # 关闭进程池
    pool.close()
    # 等待进程全部执行结束
    pool.join()
    # apply_anyce函数返回值是applyresult对象 对于每一个返回值,需要用get方法提取 提取到的变量类型取决于进程的函数返回类型
    #print(host_ip)

    host_ip = [ip.get() for ip in host_ip if ip.get() != None]
    # host_ip.remove(None) 这个函数一次只能删除一个 可以先用count统计None的数量再循环删除

    print('--------------\n局域网扫描完成\n')
    return host_ip

1、先获取系统类型,如果是Linux系统则标记为n,如果是windows系统则标记为c,这是应为ping命令可以指定发多少个数据包,不同系统中的指令不同

2、获得本地主机的ip地址再使用正则表达式提取出ip地址的前面三位,再使用列表推导式生成ip列表,进入for循环将参数传入进程执行函数

3、创建的进程数为cpu_count()-2,进程执行的是ping()函数,进程池会创建指定数量的进程,并且保持指定数量(比如:一个进程池有八个进程,但是我们的任务没有执行完,依旧有新的参数传入apply_async()函数中,这时候只有等进程池中的进程执行完后,再创建新进程使其数量达到八。),apply_async()函数会返回进程的执行结果,但是返回的是apply_async对象类型的数据,只有使用get方法才能获取到真正的进程函数的返回值。

4、当for循环执行完后,就代表着我们的ip地址序列已经全部测试过了,这时候不需要创建新的进程,只需等待原有进程池的进程执行完毕就行

5、生成存活主机列表,先用get获得进程函数返回值,判断是否为空,如果提取到的apply_async对象为空即代表这个进程ping()函数并未通过if判断,也就是说这个地址在局域网内没有主机使用

四、port_open()

def port_open(host,ports_services):
    ports_open =[]
    print('starting ' + host + '...')
    for port in ports_services:
        try:
            #尝试连接指定端口
            sock = socket.socket()
            sock.settimeout(0.1)      #每一个端口的扫描时间100ms
            sock.connect((host,port))
            ports_open.append(port)
            sock.close()
        except socket.error:
            continue
    return ports_open

给出ip地址和常用端口信息字典作为形参传入该函数。用ports_open记录该ip地址开放的端口。

for循环中port依次取字典中键值也就是端口数字,对于每一个端口创建一个socket与之进行连接。在局域网内设置连接的时长100ms绰绰有余,一个socket超过100ms还未连接上connect方法就会报错,进入异常处理,执行下一次循环 也就说明该端口未开放。如果连接成功就会继续执行后面的代码该端口就会记录到列表中,随后测试该端口的socket关闭进入下一个循环。

所有循环执行完毕后ports_open列表保存了所有开放的端口号,并返回

五、port_scan()

def port_scan(ports_service,host_ip):

    results = dict()
    print('开始扫描存活主机端口')

    m = multiprocessing.Manager()
    #创建进程池最多14个进程同时运行
    pool = multiprocessing.Pool(processes=cpu_count()-2)
    for ip in host_ip:
        results[ip] = pool.apply_async( port_open, (ip,ports_service) )

    #关闭进程池
    pool.close()
    #等待进程全部执行结束
    pool.join()
    print('存活主机常用端口开放如下')
    for host in results:
        #print('='*30)
        #print(host,'.'*38)
        for port in results[host].get():
            print(host,'  ',port,':',ports_service[port])
    return results

cpu_count() 获取计算机核数,进程池中间同时最多有14个进程,因为我们的cpu核数大于进程池,所以最多同时有14个进程并行执行(电脑处理其他任务的进程或者后台进程不纳入考虑范围内)

一开始放14个进程到进程池中,进行执行时间不一定相同,只要有进程执行完毕进程池中的进程数量少于14,那么就会往进程池中添加新的进程,每个进程执行完毕后会将 结果返回给results字典,直到port_open用完不再添加新的进行等待进程池中的进程执行完毕后程序继续往后执行。

六、完整代码如下

import socket
import multiprocessing
import sys
import re
from platform import system
from os import popen,cpu_count
from socket import gethostname,gethostbyname

def usual_ports():
    # get port name
    print('开始获取常用端口信息......')
    ports_service = dict()
    for port in list (range(1,65535)):
        try:
            ports_service[port] = socket.getservbyport(port)
        except socket.error:
            pass
    print('端口信息获取完毕......\n')
    return ports_service

def ping(ip,type):
    output = popen('ping -%s 1 %s'%(type,ip)).readlines()
    for w in output:
        if str(w).upper().find('TTL') >= 0:
            print(ip,'  ok')
            return ip

def host_find():
    host_ip = []
    local_system = system() #获取系统类型
    if local_system == 'Windows':
        type = 'n'
    elif local_system == 'Linux':
        type = 'c'
    else:
        print(system(),'是不支持的系统类型')
        sys.exit()

    net = re.findall(r'\d+\.\d+\.\d+\.', gethostbyname(gethostname()))[0]
    print('开始扫描存活主机\n网段:%s\n进程数量:%s\n--------------'%(net+'0',cpu_count()-2))
    m = multiprocessing.Manager()
    pool = multiprocessing.Pool( processes = cpu_count()-2 )
    for ip in [ net+str(i) for i in range(1,255) ]:
        host_ip.append( (pool.apply_async( ping , args = ( ip,type ) ) ) )  #apply_async函数返回值类型为类对象 在此处使用get函数提取的话会导致不能异步执行
    # 关闭进程池
    pool.close()
    # 等待进程全部执行结束
    pool.join()
    # apply_anyce函数返回值是applyresult对象 对于每一个返回值,需要用get方法提取 提取到的变量类型取决于进程的函数返回类型
    #print(host_ip)

    host_ip = [ip.get() for ip in host_ip if ip.get() != None]
    # host_ip.remove(None) 这个函数一次只能删除一个 可以先用count统计None的数量再循环删除

    print('--------------\n局域网扫描完成\n')
    return host_ip


def port_open(host,ports_services):
    ports_open =[]
    print('starting ' + host + '...')
    for port in ports_services:
        try:
            #尝试连接指定端口
            sock = socket.socket()
            sock.settimeout(0.1)      #每一个端口的扫描时间
            sock.connect((host,port))
            ports_open.append(port)
            sock.close()
        except socket.error:
            continue
    return ports_open

def port_scan(ports_service, host_ip ):
    results = dict()
    print('开始扫描存活主机端口')

    m = multiprocessing.Manager()
    #创建进程池最多16个进程同时运行
    pool = multiprocessing.Pool(processes=cpu_count()-2)
    for ip in host_ip:
        results[ip] = pool.apply_async( port_open, (ip,ports_service) )

    #关闭进程池
    pool.close()
    #等待进程全部执行结束
    pool.join()
    print('存活主机常用端口开放如下')
    for host in results:
        #print('='*30)
        #print(host,'.'*38)
        for port in results[host].get():
            print(host,'  ',port,':',ports_service[port])
    return results


if __name__ == '__main__':
    ports_service = usual_ports()
    host_ip = host_find()
    port_scan(ports_service,host_ip)