2023年10月

不知道有多少小伙伴还在头疼支付宝权限的问题,这边汇总了下目前对接支付宝可能会出现的权限问题,总有一篇能解决。

前期准备:支付宝赋权要求

工欲善其事,必先利其器。这里先介绍下支付宝目前的
赋权要求:

只要
满足了上面三个条件,支付宝大部分的服务端接口你都能走通了
~(๑•̀ㅂ•́)و✧

自研开发场景|常见问题汇总

自研开发场景比较简单,就是拿着 个人/公司 的账号去走上面三步。

目前
服务端接口常见返回的报错为

除了服务端的接口之外,还有一些
小程序的接口报错
如下:

服务商开发场景|常见问题汇总

如果是服务商开发的场景的话,操作可能会有点不一样。

区别在于,签约这部分需要
商家拿自己的支付宝账号
去操作,商家账号签约完成之后,再
授权给服务商
的支付宝账号进行开发。

授权相关的常见问题为:

以上就是支付宝权限问题的汇总啦,欢迎小伙伴们一起补充交流 (~ ̄▽ ̄)~

如果还有其他问题,可以留言讨论哦~

1. 异步编程概述

异步编程是一种编程范式,用于处理那些需要等待I/O操作完成或者耗时任务的情况。在传统的同步编程中,代码会按照顺序逐行执行,直到遇到一个耗时操作,它会阻塞程序的执行直到操作完成。这种阻塞式的模型在某些场景下效率低下,因为代码在等待操作完成时无法执行其他任务。

异步编程通过使用非阻塞I/O和协程(coroutine)来提高效率。协程是一种特殊的函数,可以在执行过程中暂停和恢复。当一个协程遇到一个耗时操作时,它会暂停自己的执行,让出控制权给其他协程,从而实现并发执行。async/await关键字是Python中处理协程的语法工具

2. async/await关键字

async关键字

async是一个关键字,用于定义一个协程函数。协程函数可以通过使用await关键字来暂停自身的执行,等待其他协程或异步操作完成。

以下是一个简单的示例,展示了如何定义一个协程函数:

import asyncio

async def my_coroutine():
    print("Coroutine started")
    await asyncio.sleep(1)
    print("Coroutine resumed")
    return "Result"

my_coroutine
是一个协程函数。它使用了
async
关键字进行定义,并包含了一个
await
语句来暂停执行。

await关键字

await是另一个关键字,用于暂停协程函数的执行,等待另一个协程、异步操作或者Future对象完成。

以下是一个使用await的示例:

import asyncio

async def my_coroutine():
    print("Coroutine started")
    await asyncio.sleep(1)
    print("Coroutine resumed")
    return "Result"

async def main():
    result = await my_coroutine()
    print(f"Result: {result}")

asyncio.run(main())

在上面的示例中,
main
函数是一个协程函数,它使用
await
关键字来等待
my_coroutine
协程函数的执行结果。当
await
语句执行时,
main
函数会暂停自身的执行,直到
my_coroutine
协程函数完成并返回结果。

需要注意的是,
await
关键字只能在协程函数中使用。如果你在普通的同步函数中使用
await
,会导致语法错误。

3. 异步事件循环

异步编程的核心是事件循环(event loop)。事件循环负责调度和执行协程,确保它们按照正确的顺序执行。

在Python中,可以使用
asyncio
模块提供的事件循环来创建和管理协程。

以下是一个使用事件循环的示例:

import asyncio

async def my_coroutine():
    print("Coroutine started")
    await asyncio.sleep(1)
    print("Coroutine resumed")
    return "Result"

async def main():
    result = await my_coroutine()
    print(f"Result: {result}")

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

在上面的示例中,
asyncio.get_event_loop()
用于获取默认的事件循环对象。然后,通过调用
run_until_complete
方法来运行
main
协程函数,直到它完成

异步编程最常见的用例是处理I/O操作,例如读写文件或与网络通信。在传统的同步编程中,这些操作会阻塞程序的执行,直到操作完成。而在异步编程中,可以使用异步IO操作来实现非阻塞的并发执行。

4. 异步IO操作

Python提供了
asyncio
模块来处理异步IO操作。
asyncio
中的一些常用函数和类包括:

  • asyncio.sleep(delay)
    : 创建一个休眠指定时间的协程。
  • asyncio.open_connection(host, port)
    : 创建一个协程,用于与指定的主机和端口建立网络连接。
  • asyncio.open_unix_connection(path)
    : 创建一个协程,用于与指定路径的UNIX域套接字建立连接。
  • asyncio.start_server(client_connected_cb, host, port)
    : 创建一个协程,用于监听指定主机和端口的连接请求,并在每次连接时调用
    client_connected_cb
    回调函数。

以下是一个使用异步IO操作的示例:

import asyncio

async def read_data():
    # 模拟异步IO读取操作
    await asyncio.sleep(1)
    return "Data"

async def write_data(data):
    # 模拟异步IO写入操作
    await asyncio.sleep(1)
    print(f"Data written: {data}")

async def main():
    data = await read_data()
    await write_data(data)

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

在上面的示例中,
read_data

write_data
函数模拟了异步的IO读取和写入操作。在
main
函数中,我们使用
await
关键字等待读取操作完成,然后将结果传递给写入操作。

执行步骤如下:

  1. 首先,创建一个事件循环(Event Loop)对象,使用
    asyncio.get_event_loop()
    获取默认的事件循环。

  2. 定义了三个协程函数:
    read_data()

    write_data()

    main()

  3. 调用
    loop.run_until_complete(main())
    ,将
    main()
    协程任务提交给事件循环并运行,直到
    main()
    协程完成。


  4. main()
    协程中,首先调用
    read_data()
    协程函数。这会启动
    read_data()
    协程,并在
    await asyncio.sleep(1)
    处暂停执行,等待1秒钟。

  5. 在暂停执行的同时,事件循环可以切换到其他可运行的协程,例如
    write_data()
    协程。

  6. write_data()
    协程同样会启动,并在
    await asyncio.sleep(1)
    处暂停执行,等待1秒钟。


  7. write_data()
    协程暂停执行时,事件循环没有其他可运行的协程,因此它会等待,直到有其他协程可运行。

  8. 在等待1秒钟后,
    read_data()
    协程恢复执行。它完成后,返回结果"Data"。

  9. main()
    协程接收到
    read_data()
    协程的返回结果,将其赋值给
    data
    变量。

  10. main()
    协程继续执行,调用
    write_data(data)
    协程。

  11. write_data()
    协程恢复执行,打印出"data"的值。

  12. main()
    协程完成,事件循环结束。

在这个过程中,通过使用
await
关键字,协程能够在等待IO操作完成时暂停执行,并允许事件循环切换到其他协程。这种方式下,IO操作可以以异步的方式执行,而不会阻塞整个程序的执行流程。

5. 并发执行多个协程

异步编程的一个关键优势是能够并发执行多个协程,以提高程序的性能。

asyncio
提供了多种方式来实现协程的并发执行,其中最常用的方式是使用
asyncio.gather
函数。

以下是一个并发执行多个协程的示例:

import asyncio

async def coroutine1():
    await asyncio.sleep(1)
    print("Coroutine 1 completed")

async def coroutine2():
    await asyncio.sleep(2)
    print("Coroutine 2 completed")

async def coroutine3():
    await asyncio.sleep(0.5)
    print("Coroutine 3 completed")

async def main():
    await asyncio.gather(coroutine1(), coroutine2(), coroutine3())

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

在上面的示例中,
coroutine1

coroutine2

coroutine3
是三个协程函数。在
main
函数中,我们使用
asyncio.gather
函数来并发执行这三个协程。
asyncio.gather
接受一个可变数量的协程参数,并返回一个新的协程,该协程在所有给定的协程完成后完成。执行循序为当执行到 coroutine1中的await时,此协程会挂起,执行权交给新的协程 coroutine2开始执行,以此类推。当 coroutine3 等待0.5s执行完毕后,执行权重新回到coroutine3 ,继续执行一下语句,其他同理。

需要注意的是,Python的协程是单线程的,通过事件循环来实现并发执行。当一个协程遇到阻塞的IO操作时,它会暂停自身的执行,并切换到下一个可执行的协程。这种切换是由事件循环调度

1、前言
《中庸》有:“九层之台,起于垒土” 之说,那么对于我们搞技术的人,同样如此!
对于
Linux
内存管理,你可以说没有留意过,但是它存在于我们日常开发的方方面面,你所打开的文件,你所创建的变量,你所运行的程序,无不以此为基础,它可以说是操作系统的基石;只是底层被封装的太好了,以至于我们在做开发的过程中,不需要关心的太多,哪有什么岁月静好,只是有人在负重前行罢了。
虽然日常开发中涉及的比较少,但是作为一个合格的
Linux
开发者,搞懂内存管理,又显得至关重要,同时也会对嵌入式开发大有脾益,今天我们就来详细聊聊内存管理的那点事。
该方面的文章,网上也有很多写的非常不错,但是100个人有100种理解方式,并且不同的人,基础不同,理解能力也不同,所以我写这系列的文章,也更有了意义。
2、内存管理的由来
为什么要有这个概念呢?
  • 首先,内存管理,管理的是个什么东西?
管理的其实是我们的物理内存,也就是我们的
RAM
空间,在电脑上,表现为我们安装的内存条,有的人装个
4G
的、
8G
的、甚至
64G
的,这些就是实打实的物理空间大小,也就是我们的实际的硬件资源。

  • 为什么要进行管理?
做嵌入式的都知道,像我们刚开始玩的
C51
单片机、
STM32
单片机,我们将程序烧录到
Flash
中后,开机启动后,然后
CPU
会将
Flash
程序加载到
RAM
中,也就是我们的物理内存,随后我们的所有操作都是基于这一个物理内存所进行的。

那么此时
  1. 我们想再次运行一个一模一样的程序怎么办?
  2. 即使运行了,那两个程序同时操作了同一个变量,值被错误修改了怎么办?
这些就是
Linux
内存管理要做的事情。
顺便介绍一下 我的圈子:
高级工程师聚集地
,期待大家的加入。
3、Linux内存管理思想
为了解决上面的一些问题,
Linux
采用虚拟内存管理技术。
  1. Linux
    操作系统抽象出来一个
    虚拟地址空间
    的概念,供上层用户使用,这么做的目的是为了让多个用户进程,都以为自己独享了内存空间。
  2. 而虚拟地址空间与物理地址空间的对应关系,就交给了一个
    MMU(Memory Managerment Unit)
    的家伙来管理,其主要负责将虚拟内存空间映射到真实的物理地址空间。

添加图片注释,不超过 140 字(可选)
这么做的主要目的在于:
  1. 让每个进程都拥有相同大小的虚拟地址空间
  2. 避免用户直接访问物理内存,导致系统崩溃
这样,我们同时执行多个进程,虽然看起来虚拟地址操作都是相同的,但是通过
MMU
之后,就被映射到了不同的物理地址空间,这样就解决了以上的问题。
4、总结
熟悉了内存管理由来以及其思想,我们可以看出,操作系统的内存管理,主要分为以下几个方面:
  1. 虚拟内存空间管理
    :我们抽象出来的虚拟地址空间,该怎么使用,该怎么管理?
  2. 物理内存空间管理
    :虚拟地址映射到物理内存空间后,该如何管理,如何分配?
  3. 如何映射
    :虚拟内存如何映射到物理内存,是怎么操作的,映射方法有哪些?
下面我们来一一详细探究。

热力图,是一种通过对色块着色来显示数据的统计图表。
它通过使用颜色编码来表示数据的值,并在二维平面上呈现出来。
热力图通常用于显示大量数据点的密度、热点区域和趋势。

绘图时,一般较大的值由较深的颜色表示,较小的值由较浅的颜色表示;较大的值由偏暖的颜色表示,较小的值由较冷的颜色表示,等等。

热力图适合用于查看总体的情况、发现异常值、显示多个变量之间的差异,以及检测它们之间是否存在任何相关性。

1. 主要元素

热力图的主要元素如下:

  1. 矩形块
    :每个矩形块都有一个对应的位置。表示某种属性、频率、密度等。
  2. 颜色映射
    :通常使用渐变色带来表示数值的大小或密度。常见的颜色映射包括从冷色调(如蓝色)到热色调(如红色)的渐变,表示数值从低到高的变化。
  3. 热力密度
    :通过颜色的深浅来表示数据的密度或频率。较浅的颜色表示较低的密度或频率,而较深的颜色表示较高的密度或频率。
  4. 坐标轴
    :热力图通常在二维平面上显示,因此会有两个坐标轴,分别表示水平和垂直方向上的位置。

2. 适用的场景

热力图适用于以下分析场景:

  • 数据密度分析
    :显示数据点的密度分布情况。它可以帮助用户观察数据的聚集区域和稀疏区域,从而揭示数据的分布模式和趋势。
  • 热点区域识别
    :识别数据中的热点区域,即数据密度较高的区域。对于发现热门地区、热门产品或热门事件等具有重要意义。
  • 趋势分析
    :通过观察颜色的变化,可以分析数据在不同区域或时间段的变化趋势。
  • 空间数据分析
    :在地理信息系统(GIS)和位置数据分析中,可以显示地理空间上的数据分布和密度,帮助用户理解地理区域的特征和差异。
  • 网站流量分析
    :显示用户在网页上的点击热度和浏览热度。这有助于优化网站布局、改进用户体验和提高转化率。

3. 不适用的场景

热力图在以下分析场景中可能不适用:

  • 无序数据
    :对于无序的数据,热力图可能无法提供有意义的分析结果。
  • 数据缺失
    :如果数据中存在大量缺失值或空白区域,可能无法准确地反映数据的密度和分布情况。
  • 多个并行路径
    :通常用于展示单一维度的数据分布情况。如果需要同时比较多个维度或路径的数据,热力图可能不是最合适的选择。

4. 分析实战

本次分析今年上半年南京主要的几个区二手房的成交数量情况。

4.1. 数据来源

数据来自链家网南京地区的二手房成交的页面。
整理好的数据可以从下面的地址下载:
https://databook.top/lianjia/nj

各个区的二手房交易数据已经整理成
csv
格式。

import os

df_dict = {}
#数据解压的地址
fp = "d:/share/data/南京二手房交易"
for f in os.listdir(fp):
    df = pd.read_csv(os.path.join(fp, f))
    df_dict[f] = df

df_dict  #合并所有区的数据

4.2. 数据清理

清理数据的主要几个步骤:


  1. dealDate
    列转换为 日期(datetime)格式
  2. 按周统计的交易数量
  3. 统计结果保存到新的字典中(
    df_stat
    ),取日期最近的10条
df_stat = {}

for k, df in df_dict.items():
    df["dealDate"] = pd.to_datetime(df["dealDate"])
    # 最近10周的交易量
    week_sum = df.resample("W", on="dealDate").name.count()
    week_sum = week_sum.sort_index(ascending=False)

    df_stat[k.replace(".csv", "")] = week_sum.head(10)

df_stat

4.3. 分析结果可视化

更加各个区的成交数量绘制最近10周的交易热力图。

x_labels = []
y_labels = df_stat.keys() # Y周的标签
data = []
for _, v in df_stat.items():
    if len(x_labels) == 0: # X轴的日期标签
        x_labels = v.index.strftime("%Y-%m-%d").tolist()
        x_labels.reverse()

    v = v.sort_index()
    data.append(v.tolist())

plt.xticks(ticks=np.arange(len(x_labels)), 
           labels=x_labels,
           rotation=45)
plt.yticks(ticks=np.arange(len(y_labels)), 
           labels=y_labels)

plt.imshow(data, cmap=plt.cm.hot_r)
plt.colorbar()
plt.show()

image.png

从热力图中可以看出,
江宁区

浦口区
的成交数量明显多于其他区,尤其是
江宁区

其次是
鼓楼区

秦淮区
稍好一些(可能和这2个区学区房比较多有关),而
溧水区

六合区
明显交易量不行。

原生套接字抓包的实现原理依赖于
Windows
系统中提供的
ioctlsocket
函数,该函数可将指定的网卡设置为混杂模式,网卡混杂模式(
Promiscuous Mode
)是常用于计算机网络抓包的一种模式,也称为监听模式。在混杂模式下,网卡可以收到经过主机的所有数据包,而非只接收它所对应的
MAC
地址的数据包。

一般情况下,网卡会根据
MAC
地址过滤数据包,只有
MAC
地址与网卡所对应的设备的通信数据包才会被接收和处理,其他数据包则会被忽略。但在混杂模式下,网卡会接收经过它所连接的网络中所有的数据包,这些数据包可以是面向其他设备的通信数据包、广播数据包或多播数据包等。

混杂模式可以通过软件驱动程序或网卡硬件实现。启用混杂模式的主要用途之一是网络抓包分析,使用混杂模式可以捕获网络中所有的数据包,且不仅仅是它所连接的设备的通信数据包。因此,可以完整获取网络中的通信内容,便于进行网络监控、安全风险感知、漏洞检测等操作。


Windows
系统下,开启混杂模式可以使用
ioctlsocket()
函数,该函数原型定义如下:

int ioctlsocket (
   SOCKET s,        //要操作的套接字
   long cmd,        //操作代码
   u_long *argp     //指向操作参数的指针
);

其中,参数说明如下:

  • s: 要执行
    I/O
    控制操作的套接字。
  • cmd: 操作代码,用于控制对套接字的特定操作。
  • argp: 与特定请求代码相关联的参数指针。此参数的具体含义取决于请求代码。

在该函数中,参数
cmd
指定了
I/O
控制操作代码,是一个整数值,用于控制对套接字的特定操作。
argp
是一个指向特定请求代码相关联的参数的指针,它的具体含义将取决于请求代码。函数返回值为
int
类型,表示函数执行结果的状态码,若函数执行成功,则其返回值为0,否则返回一个错误代码,并将错误原因存入
errno
变量中。

要实现抓包前提是需要先选中绑定到那个网卡,如下
InitAndSelectNetworkRawSocket
函数则是实现绑定套接字到特定网卡的实现流程,在代码中首先初始化并使用
gethostname
函数获取到当前主机的主机名,主机IP地址等基本信息,接着通过循环的方式将自身网卡信息追加到
g_HostIp
全局结构体内进行存储,通过使用一个交互式选择菜单让用户可以选中需要绑定的网卡名称,当用户选中后则下一步是绑定套接字,并通过调用
ioctlsocket
函数将网卡设置为混杂模式,至此网卡的绑定工作就算结束了,当读者需要操作时只需要对全局变量进行操作即可,而选择函数仅仅只是获取到网卡信息而已并没有实际的作用。

#include <iostream>
#include <WinSock2.h>
#include <ws2tcpip.h>
#include <mstcpip.h>

#pragma comment(lib, "ws2_32.lib")

// 全局结构
typedef struct
{
  int iLen;
  char szIPArray[10][50];
}HOSTIP;

// 全局变量
SOCKET g_RawSocket = 0;
HOSTIP g_HostIp;

// -------------------------------------------------------
// 初始化与选择套接字
// -------------------------------------------------------
BOOL InitAndSelectNetworkRawSocket()
{
  // 设置套接字版本
  WSADATA wsaData = { 0 };
  if (0 != WSAStartup(MAKEWORD(2, 2), &wsaData))
  {
    return FALSE;
  }
  // 创建原始套接字
  // Windows无法抓取RawSocket MAC层的数据包,只能抓到IP层及以上的数据包
  g_RawSocket = socket(AF_INET, SOCK_RAW, IPPROTO_IP);
  // g_RawSocket = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
  if (INVALID_SOCKET == g_RawSocket)
  {
    WSACleanup();
    return FALSE;
  }

  // 绑定到接口 获取本机名
  char szHostName[MAX_PATH] = { 0 };
  if (SOCKET_ERROR == ::gethostname(szHostName, MAX_PATH))
  {
    closesocket(g_RawSocket);
    WSACleanup();
    return FALSE;
  }

  // 根据本机名获取本机IP地址
  hostent* lpHostent = ::gethostbyname(szHostName);
  if (NULL == lpHostent)
  {
    closesocket(g_RawSocket);
    WSACleanup();
    return FALSE;
  }

  // IP地址转换并保存IP地址
  g_HostIp.iLen = 0;
  strcpy(g_HostIp.szIPArray[g_HostIp.iLen], "127.0.0.1");
  g_HostIp.iLen++;
  char* lpszHostIP = NULL;

  while (NULL != (lpHostent->h_addr_list[(g_HostIp.iLen - 1)]))
  {
    lpszHostIP = inet_ntoa(*(in_addr*)lpHostent->h_addr_list[(g_HostIp.iLen - 1)]);
    strcpy(g_HostIp.szIPArray[g_HostIp.iLen], lpszHostIP);
    g_HostIp.iLen++;
  }

  // 选择IP地址对应的网卡来嗅探
  printf("选择侦听网卡 \n\n");
  for (int i = 0; i < g_HostIp.iLen; i++)
  {
    printf("\t [*] 序号: %d \t IP地址: %s \n", i, g_HostIp.szIPArray[i]);
  }

  printf("\n 选择网卡序号: ");
  int iChoose = 0;
  scanf("%d", &iChoose);

  // 如果选择超出范围则直接终止
  if ((0 > iChoose) || (iChoose >= g_HostIp.iLen))
  {
    exit(0);
  }
  if ((0 <= iChoose) && (iChoose < g_HostIp.iLen))
  {
    lpszHostIP = g_HostIp.szIPArray[iChoose];
  }

  // 构造地址结构
  sockaddr_in SockAddr = { 0 };
  RtlZeroMemory(&SockAddr, sizeof(sockaddr_in));
  SockAddr.sin_addr.S_un.S_addr = inet_addr(lpszHostIP);
  SockAddr.sin_family = AF_INET;
  SockAddr.sin_port = htons(0);

  // 绑定套接字
  if (SOCKET_ERROR == bind(g_RawSocket, (sockaddr*)(&SockAddr), sizeof(sockaddr_in)))
  {
    closesocket(g_RawSocket);
    WSACleanup();
    return FALSE;
  }

  // 设置混杂模式 抓取所有经过网卡的数据包
  DWORD dwSetVal = 1;
  if (SOCKET_ERROR == ioctlsocket(g_RawSocket, SIO_RCVALL, &dwSetVal))
  {
    closesocket(g_RawSocket);
    WSACleanup();
    return FALSE;
  }
  return TRUE;
}

int main(int argc, char *argv[])
{
  // 选择网卡并设置网络为非阻塞模式
  BOOL SelectFlag = InitAndSelectNetworkRawSocket();
  if (SelectFlag == TRUE)
  {
    printf("[*] 网卡已被选中 套接字ID = %d | 套接字IP = %s \n", g_RawSocket,g_HostIp.szIPArray);
  }

  system("pause");
  return 0;
}

读者可自行编译并以管理员身份运行上述代码片段,当读者运行后会看到如下图所示的代码片段,此处笔者就选择三号网卡进行绑定操作,当绑定后此时套接字ID对应的则是特定的网卡,后续的操作均可针对此套接字ID进行,如下图所示;

当读者有了设置混杂模式的功能则下一步就是抓包了,抓包的实现很简单,只需要在开启了非阻塞混杂模式的网卡上使用
recvfrom
函数循环进行监听即可,当有数据包产生时则直接输出
iRecvBytes
中所存储的数据即可,这段代码的实现如下所示;

int main(int argc, char *argv[])
{
  // 选择网卡并设置网络为非阻塞模式
  BOOL init_flag = InitAndSelectNetworkRawSocket();
  if (init_flag == FALSE)
  {
    return 0;
  }

  sockaddr_in RecvAddr = { 0 };
  int iRecvBytes = 0;
  int iRecvAddrLen = sizeof(sockaddr_in);

  // 定义缓冲区长度
  DWORD dwBufSize = 12000;
  BYTE* lpRecvBuf = new BYTE[dwBufSize];

  // 循环接收接收
  while (1)
  {
    RtlZeroMemory(&RecvAddr, iRecvAddrLen);
    iRecvBytes = recvfrom(g_RawSocket, (char*)lpRecvBuf, dwBufSize, 0, (sockaddr*)(&RecvAddr), &iRecvAddrLen);
    if (0 < iRecvBytes)
    {
      // 接收数据包并输出
      printf("[接收数据包] %s \n", lpRecvBuf);
    }
  }

  // 释放内存
  delete[]lpRecvBuf;
  lpRecvBuf = NULL;

  // 关闭套接字
  Sleep(500);
  closesocket(g_RawSocket);
  WSACleanup();
  return 0;
}

当读者选择网卡后即可看到如下所示的输出结果,这些数据则是经过网卡
192.168.9.125
的所有数据,由于此处没有解码和区分数据包类型所以显示出的字符串并没有任何意义,如下图所示;

接下来我们就需要根据不同的数据包类型对这些数据进行解包操作,在解包之前我们需要先来定义几个关键的数据包结构体,如下代码中
ether_header
代表的是以太网包头结构,该结构占用
14
个字节的存储空间,
arp_header
则是
ARP
结构体,该结构体占用
28
个字节,ARK结构中还存在一个
ARK
报文结构,该结构占用
42
字节的内存长度,接着分别顶一个
ipv4_header

ipv6_header

tcp_header

udp_header
等结构体,这些结构体的完整定义如下所示;

#pragma pack(1)

/*以太网帧头格式结构体 14个字节*/
typedef struct ether_header
{
    unsigned char ether_dhost[6];  // 目的MAC地址
    unsigned char ether_shost[6];  // 源MAC地址
    unsigned short ether_type;     // eh_type 的值需要考察上一层的协议,如果为ip则为0x0800
}ETHERHEADER, * PETHERHEADER;

/*以ARP字段结构体 28个字节*/
typedef struct arp_header
{
    unsigned short arp_hrd;
    unsigned short arp_pro;
    unsigned char arp_hln;
    unsigned char arp_pln;
    unsigned short arp_op;
    unsigned char arp_sourha[6];
    unsigned long arp_sourpa;
    unsigned char arp_destha[6];
    unsigned long arp_destpa;
}ARPHEADER, * PARPHEADER;

/*ARP报文结构体 42个字节*/
typedef struct arp_packet
{
    ETHERHEADER etherHeader;
    ARPHEADER   arpHeader;
}ARPPACKET, * PARPPACKET;

/*IPv4报头结构体 20个字节*/
typedef struct ipv4_header
{
    unsigned char ipv4_ver_hl;        // Version(4 bits) + Internet Header Length(4 bits)长度按4字节对齐
    unsigned char ipv4_stype;         // 服务类型
    unsigned short ipv4_plen;         // 总长度(包含IP数据头,TCP数据头以及数据)
    unsigned short ipv4_pidentify;    // ID定义单独IP
    unsigned short ipv4_flag_offset;  // 标志位偏移量
    unsigned char ipv4_ttl;           // 生存时间
    unsigned char ipv4_pro;           // 协议类型
    unsigned short ipv4_crc;          // 校验和
    unsigned long  ipv4_sourpa;       // 源IP地址
    unsigned long  ipv4_destpa;       // 目的IP地址
}IPV4HEADER, * PIPV4HEADER;

/*IPv6报头结构体 40个字节*/
typedef struct ipv6_header
{
    unsigned char ipv6_ver_hl;
    unsigned char ipv6_priority;
    unsigned short ipv6_lable;
    unsigned short ipv6_plen;
    unsigned char  ipv6_nextheader;
    unsigned char  ipv6_limits;
    unsigned char ipv6_sourpa[16];
    unsigned char ipv6_destpa[16];
}IPV6HEADER, * PIPV6HEADER;

/*TCP报头结构体 20个字节*/
typedef struct tcp_header
{
    unsigned short tcp_sourport;     // 源端口
    unsigned short tcp_destport;     // 目的端口
    unsigned long  tcp_seqnu;        // 序列号
    unsigned long  tcp_acknu;        // 确认号
    unsigned char  tcp_hlen;         // 4位首部长度
    unsigned char  tcp_reserved;     // 标志位
    unsigned short tcp_window;       // 窗口大小
    unsigned short tcp_chksum;       // 检验和
    unsigned short tcp_urgpoint;     // 紧急指针
}TCPHEADER, * PTCPHEADER;

/*UDP报头结构体 8个字节*/
typedef struct udp_header
{
    unsigned short udp_sourport;   // 源端口 
    unsigned short udp_destport;   // 目的端口
    unsigned short udp_hlen;       // 长度
    unsigned short udp_crc;        // 校验和
}UDPHEADER, * PUDPHEADER;
#pragma pack()

当有了结构体的定义部分,则实现对数据包的解析只需要判断数据包的类型并使用不同的结构体对数据包进行解包打印即可,如下是实现数据包解析的完整代码,在代码中分别实现了几个核心函数,其中
printData
函数可以实现对特定内存数据的十六进制格式输出方便检查输出效果,函数
AnalyseRecvPacket_All
用于解析除去
TCP/UDP
格式的其他数据包,
AnalyseRecvPacket_TCP
用于解析
TCP
数据,
AnalyseRecvPacket_UDP
用于解析
UDP
数据,在主函数中通过使用
ip->ipv4_pro
判断数据包的具体类型,并根据类型的不同依次调用不同的函数实现数据包解析。

// 输出数据包
void PrintData(BYTE* lpBuf, int iLen, int iPrintType)
{
  // 16进制
  if (0 == iPrintType)
  {
    for (int i = 0; i < iLen; i++)
    {
      if ((0 == (i % 8)) && (0 != i))
      {
        printf("  ");
      }
      if ((0 == (i % 16)) && (0 != i))
      {
        printf("\n");
      }
      printf("%02x ", lpBuf[i]);

    }
    printf("\n");
  }
  // ASCII编码
  else if (1 == iPrintType)
  {
    for (int i = 0; i < iLen; i++)
    {
      printf("%c", lpBuf[i]);
    }
    printf("\n");
  }
}

// 解析所有其他数据包
void AnalyseRecvPacket_All(BYTE* lpBuf)
{
  struct sockaddr_in saddr, daddr;
  PIPV4HEADER ip = (PIPV4HEADER)lpBuf;
  saddr.sin_addr.s_addr = ip->ipv4_sourpa;
  daddr.sin_addr.s_addr = ip->ipv4_destpa;

  printf("From:%s --> ", inet_ntoa(saddr.sin_addr));
  printf("To:%s\n", inet_ntoa(daddr.sin_addr));
}

// 解析TCP数据包
void AnalyseRecvPacket_TCP(BYTE* lpBuf)
{
  struct sockaddr_in saddr, daddr;
  PIPV4HEADER ip = (PIPV4HEADER)lpBuf;
  PTCPHEADER tcp = (PTCPHEADER)(lpBuf + (ip->ipv4_ver_hl & 0x0F) * 4);
  int hlen = (ip->ipv4_ver_hl & 0x0F) * 4 + tcp->tcp_hlen * 4;

  // 这里要将网络字节序转换为本地字节序
  int dlen = ntohs(ip->ipv4_plen) - hlen;
  saddr.sin_addr.s_addr = ip->ipv4_sourpa;
  daddr.sin_addr.s_addr = ip->ipv4_destpa;

  printf("From:%s:%d --> ", inet_ntoa(saddr.sin_addr), ntohs(tcp->tcp_sourport));
  printf("To:%s:%d  ", inet_ntoa(daddr.sin_addr), ntohs(tcp->tcp_destport));
  printf("ack:%u  syn:%u length=%d\n", tcp->tcp_acknu, tcp->tcp_seqnu, dlen);

  PrintData((lpBuf + hlen), dlen, 0);
}

// 解析UDP数据包
void AnalyseRecvPacket_UDP(BYTE* lpBuf)
{
  struct sockaddr_in saddr, daddr;
  PIPV4HEADER ip = (PIPV4HEADER)lpBuf;
  PUDPHEADER udp = (PUDPHEADER)(lpBuf + (ip->ipv4_ver_hl & 0x0F) * 4);
  int hlen = (int)((ip->ipv4_ver_hl & 0x0F) * 4 + sizeof(UDPHEADER));
  int dlen = (int)(ntohs(udp->udp_hlen) - 8);

  //  int dlen = (int)(udp->udp_hlen - 8);
  saddr.sin_addr.s_addr = ip->ipv4_sourpa;
  daddr.sin_addr.s_addr = ip->ipv4_destpa;
  printf("Protocol:UDP  ");
  printf("From:%s:%d -->", inet_ntoa(saddr.sin_addr), ntohs(udp->udp_sourport));
  printf("To:%s:%d\n", inet_ntoa(daddr.sin_addr), ntohs(udp->udp_destport));

  PrintData((lpBuf + hlen), dlen, 0);
}

int main(int argc, char* argv[])
{
  // 选择网卡,并设置网络为非阻塞模式
  InitAndSelectNetworkRawSocket();

  sockaddr_in RecvAddr = { 0 };
  int iRecvBytes = 0;
  int iRecvAddrLen = sizeof(sockaddr_in);
  DWORD dwBufSize = 12000;
  BYTE* lpRecvBuf = new BYTE[dwBufSize];

  // 循环接收接收
  while (1)
  {
    RtlZeroMemory(&RecvAddr, iRecvAddrLen);
    iRecvBytes = recvfrom(g_RawSocket, (char*)lpRecvBuf, dwBufSize, 0, (sockaddr*)(&RecvAddr), &iRecvAddrLen);
    if (0 < iRecvBytes)
    {
      // 接收数据包解码输出
      // 分析IP包的协议类型
      PIPV4HEADER ip = (PIPV4HEADER)lpRecvBuf;
      switch (ip->ipv4_pro)
      {
      case IPPROTO_ICMP:
      {
                // 分析ICMP
        printf("[ICMP]\n");
        AnalyseRecvPacket_All(lpRecvBuf);
        break;
      }
      case IPPROTO_IGMP:
      {
                // 分析IGMP
        printf("[IGMP]\n");
        AnalyseRecvPacket_All(lpRecvBuf);
        break;
      }
      case IPPROTO_TCP:
      {
        // 分析tcp协议
        printf("[TCP]\n");
        AnalyseRecvPacket_TCP(lpRecvBuf);
        break;
      }
      case IPPROTO_UDP:
      {
        // 分析udp协议
        printf("[UDP]\n");
        AnalyseRecvPacket_UDP(lpRecvBuf);
        break;
      }
      default:
      {
                // 其他数据包
        printf("[OTHER IP]\n");
        AnalyseRecvPacket_All(lpRecvBuf);
        break;
      }
      }
    }
  }

  // 释放内存
  delete[]lpRecvBuf;
  lpRecvBuf = NULL;

  // 关闭套接字
  Sleep(500);
  closesocket(g_RawSocket);
  WSACleanup();
  return 0;
}

读者可自行编译并运行上述代码片段,当程序运行后可自行选择希望监控的网卡,当程序中检测到TCP数据包后会输出如下图所示的提示信息,在图中我们可以清晰的看出数据包的流向信息,以及数据包长度数据包内的数据等;

当读者通过使用
Ping
命令探测目标主机时,此时同样可以抓取到
ICMP
相关的数据流,只是在数据解析时并没有太规范导致只能看到简单的流向,当然读者也可以自行完善这段代码,让其能够解析更多参数。

本文作者: 王瑞
本文链接:
https://www.lyshark.com/post/8e15eea.html
版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!