18.1 Socket 原生套接字抓包

原生套接字抓包的实现原理依赖于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_headeripv6_headertcp_headerudp_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 许可协议。转载请注明出处!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/105314.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ThinkPHP8学习笔记

ThinkPHP8官方文档地址&#xff1a;ThinkPHP官方手册 一、composer换源 1、查看 composer 配置的命令composer config -g -l 2、禁用默认源镜像命令composer config -g secure-http false 3、修改为阿里云镜像源composer config -g repo.packagist composer https://mirror…

Ubuntu环境下DOSBOX的配置

【步骤一】 先打开命令行&#xff0c;进入root模式&#xff0c;输入如下语句&#xff1a; sudo apt-get install dosbox 该语句的作用主要是安装dosbox 【步骤二】 安装完成之后&#xff0c;命令行输入dosbox 会进入dosbox页面&#xff1a; 【步骤三】 在你的主机中&…

使用pycharm远程连接到Linux服务器进行开发

预计达到的效果 本地的 PyCharm 能达到和远程服务器之间的文件同步&#xff1b;本地的 PyCharm 能够使用远程服务器的开发环境&#xff1b; 环境配置 PyCharm&#xff1a;PyCharm 2021.3 (Professional Edition)Linux服务器&#xff1a;Ubuntu20.04 步骤 1.进入配置项 配…

p5.js 变换操作

本文简介 带尬猴&#xff0c;我嗨德育处主任 在 canvas 里&#xff0c;变换是基础功能。很多基于 canvas 封装的库都有这功能&#xff0c;比如 《Fabric.js 变换视窗》。 变换是针对画布进行全局调整的一种能力&#xff0c;它可以对画布进行全局移动、缩放、旋转等操作。 p5…

一键同步,无处不在的书签体验:探索多电脑Chrome书签同步插件

说在前面 平时大家都是怎么管理自己的浏览器书签数据的呢&#xff1f;有没有过公司和家里的电脑浏览器书签不同步的情况&#xff1f;有没有过电脑突然坏了但书签数据没有导出&#xff0c;导致书签数据丢失了&#xff1f;解决这些问题的方法有很多&#xff0c;我选择自己写个chr…

Server Name Indication(SNI),HTTP/TLS握手过程解析

Server Name Indication&#xff08;SNI&#xff09;是一种TLS扩展&#xff0c;用于在TLS握手过程中传递服务器的域名信息。在未使用SNI之前&#xff0c;客户端在建立TLS连接时只能发送单个IP地址&#xff0c;并且服务器无法知道客户端请求的具体域名。这导致服务器需要使用默认…

uniapp把文件中的内复制到另一个文件中

使用的是Html 5的plus.io.resolveLocalFileSystemURL方法&#xff0c;文档&#xff1a;HTML5 API Reference var soursePath file:///storage/emulated/0/a/;//用于读取var removePath file:///storage/emulated/0/w/;//用于移除w这个文件夹var targetPath file:///storage/…

Redis 主从

目录 ​编辑一、构建主从架构 1、集群结构 2、准备实例和配置 &#xff08;1&#xff09;创建目录 &#xff08;2&#xff09;修改原始配置 &#xff08;3&#xff09;拷贝配置文件到每个实例目录 &#xff08;4&#xff09;修改每个实例的端口&#xff0c;工作目录 &a…

【Android Studio】工程中文件Annotate with Git Blame 不能点击

问题描述 工程文件中想要查看代码提交信息但是相关按钮不可点击 解决方法 Android Studio -> Preferences -> Version Control-> 在Unregistered roots里找到你想要的工程文件 点击左上角➕号 然后右下角Apply即可

Django viewsets 视图集与 router 路由实现评论接口开发

正常来说遵循restful风格编写接口&#xff0c;定义一个类包含了 get post delete put 四种请求方式&#xff0c;这四种请求方式是不能重复的 例如:获取单条记录和多条记录使用的方式都是get&#xff0c;如果两个都要实现的话那么得定义两个类&#xff0c;因为在同一个类中不能有…

金融领域:怎么保持电力系统连续供应?

银行作为金融领域的关键机构&#xff0c;依赖于高度可靠的电力供应&#xff0c;以保持银行操作的连续性。在电力中断或电力质量问题的情况下&#xff0c;银行可能面临严重的风险&#xff0c;包括数据丢失、交易中断和客户满意度下降。 UPS监控系统在这一背景下变得至关重要&…

解决msvcp120.dll丢失的问题的5个方法,修复系统dll问题

在使用计算机的过程中&#xff0c;我们经常会遇到各种各样的动态链接库&#xff08;DLL&#xff09;文件。其中之一就是“msvcp120.dll丢失”。这个错误通常会导致某些应用程序无法正常运行。为了解决这个问题&#xff0c;我们需要找到合适的方法来修复丢失的msvcp120.dll文件。…

【单例模式】饿汉式,懒汉式?JAVA如何实现单例?线程安全吗?

个人简介&#xff1a;Java领域新星创作者&#xff1b;阿里云技术博主、星级博主、专家博主&#xff1b;正在Java学习的路上摸爬滚打&#xff0c;记录学习的过程~ 个人主页&#xff1a;.29.的博客 学习社区&#xff1a;进去逛一逛~ 单例设计模式 Java单例设计模式 Java单例设计模…

达芬奇MacOS最新中文版 DaVinci Resolve Studio 18中文注册秘钥

DaVinci Resolve Studio 18是一款专业的视频编辑软件&#xff0c;它具有多种强大的功能。首先&#xff0c;它提供了丰富的视频剪辑工具&#xff0c;如剪切、复制、粘贴、剪辑、缩放和移动等&#xff0c;使用户可以轻松地剪辑和组合视频素材。其次&#xff0c;该软件还支持多个轨…

Redis和Memcached网络模型详解

1. Redis单线程单Reactor网络模型 1.1 redis单线程里不能执行十分耗时的流程&#xff0c;不然会客户端响应不及时 解决方法一&#xff1a; beforesleep里删除过期键操作若存在大量过期键时&#xff0c;会耗费大量时间&#xff0c;redis采用的策略之一就是采用timelimit方案超过…

Spring Boot整合OAuth2实现GitHub第三方登录

Spring Boot整合OAuth2&#xff0c;实现GitHub第三方登录 1、第三方登录原理 第三方登录的原理是借助OAuth授权来实现&#xff0c;首先用户先向客户端提供第三方网站的数据证明自己的身份获取授权码&#xff0c;然后客户端拿着授权码与授权服务器建立连接获得一个Access Token…

搞定蓝牙——第四章(GATT协议)

搞定蓝牙——第四章&#xff08;GATT协议&#xff09; 原理介绍层次结构server和client端Attribute ESP32代码 文章下面用的英文表示&#xff1a; server和client&#xff1a;服务端和客户端 char.&#xff1a;characteristic缩写&#xff0c;特征 Attribute:属性 ATT:Attribut…

JavaWeb——IDEA操作:Project最终新建module

在project中创建新的module&#xff1a; 创建一个新的module很容易&#xff0c;但是它可能连接不上Tomcat&#xff0c;因此需要修改一些配置&#xff1a; 将以下地址修改为新module的地址

Spring+spring mvc+mybatis整合的框架

Spring是一个轻量级的企业级应用开发框架&#xff0c;于2004年由Rod Johnson发布了1.0版本&#xff0c;经过多年的更新迭代&#xff0c;已经逐渐成为Java开源世界的第一框架&#xff0c;Spring框架号称Java EE应用的一站式解决方案&#xff0c;与各个优秀的MVC框架如SpringMVC、…

PX30 android8.1添加RTL8723DU

将8723du复制到kernel/drivers/net/wireless/rockchip_wlan/rtl8723du 并修改makefile 修改平台 CONFIG_PLATFORM_I386_PC nCONFIG_PLATFORM_ARM_RK3188 y 修改 ifeq ($(CONFIG_PLATFORM_ARM_RK3188), y) EXTRA_CFLAGS -DCONFIG_LITTLE_ENDIAN -DCONFIG_PLATFORM_ANDRO…