Linux socket编程(10):UDP详解、聊天室实现及进阶知识

news2024/9/24 21:29:03

首先来回顾以下TCP的知识,TCP是一种面向连接的、可靠的传输协议,具有以下特点:

  • TCP通过三次握手建立连接,确保通信的可靠性和完整性
  • 使用流控制和拥塞控制机制,有效地调整数据传输的速率,防止网络拥塞
  • TCP提供错误检测和重传机制,以确保数据在传输过程中不会丢失或损坏
  • TCP支持全双工通信,允许双方同时发送和接收数据,从而提高通信效率

然而,与TCP不同,UDP是一种无连接的、不可靠的传输协议。相比于TCP,UDP更加轻量级,没有连接的建立和断开过程,也没有复杂的流控制和拥塞控制机制。UDP直接将数据包发送到目标地址,不保证数据的顺序和可靠性,因此在某些实时性要求较高、可以容忍少量数据丢失的应用场景中表现得更为适用。

文章目录

  • 1 相关函数
    • 1.1 socket
    • 1.2 bind
    • 1.3 sendto
    • 1.4 recvfrom
  • 2 UDP聊天室
    • 2.1 客户端
    • 2.2 服务端
    • 2.3 实验结果
    • 2.4 完整代码
  • 3 UDP进阶注意事项
    • 3.1 如何判断对端断开
    • 3.2 UDP报文丢失、重复、乱序、流量控制
    • 3.3 UDP数据被丢弃
    • 3.4 connect判断对端是否存在

1 相关函数

首先来看一下UDP的流程图:
在这里插入图片描述

对于服务端来说,UDP无需像TCP一样listen,因为UDP是无连接的。同样地,对于客户端来说,UDP也不需要调用connect建立连接。对于socket/bind等函数可以参考之前TCP的文章socket函数介绍及C/S模型代码实现中的介绍。这里就来介绍一下recvfromsendto

1.1 socket

socket()函数用于创建套接字,即通信的端点。

int socket(int domain, int type, int protocol);
  • 参数
    • domain:协议族,常用的是 AF_INET 表示IPv4协议族。
    • type:套接字类型,常用的是 SOCK_DGRAM 表示UDP套接字。
    • protocol:协议,通常设置为 0 表示使用默认协议。
  • 示例
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);

1.2 bind

bind()函数用于将套接字绑定到特定的地址和端口。

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 参数
    • sockfd:套接字文件描述符。
    • addr:指向包含地址信息的结构体的指针,通常是struct sockaddr_in
    • addrlen:结构体的长度,使用sizeof(struct sockaddr_in)
  • 示例
struct sockaddr_in server_address;
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = INADDR_ANY;
server_address.sin_port = htons(8080);

bind(udp_socket, (struct sockaddr*)&server_address, sizeof(server_address));

1.3 sendto

sendto()函数用于向指定的目标地址发送数据。

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
  • 参数
    • sockfd:套接字文件描述符。
    • buf:要发送的数据的指针。
    • len:要发送的数据的字节数。
    • flags:发送标志,通常设置为 0
    • dest_addr:指向目标地址信息的结构体的指针,通常是struct sockaddr_in
    • addrlen:结构体的长度,使用sizeof(struct sockaddr_in)
  • 示例
char message[] = "Hello,world";
struct sockaddr_in client_address;
client_address.sin_family = AF_INET;
client_address.sin_addr.s_addr = inet_addr("127.0.0.1");
client_address.sin_port = htons(8081);

sendto(udp_socket, message, sizeof(message), 0, (struct sockaddr*)&client_address, sizeof(client_address));

1.4 recvfrom

recvfrom()函数用于接收数据以及发送方的地址。

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
  • 参数
    • sockfd:套接字文件描述符。
    • buf:用于存放接收数据的缓冲区的指针。
    • len:缓冲区的大小。
    • flags:接收标志,通常设置为 0
    • src_addr:指向发送方地址信息的结构体的指针,通常是struct sockaddr_in
    • addrlen:指向存储发送方地址结构体长度的变量的指针。
  • 示例:
char buffer[1024];
struct sockaddr_in client_address;
socklen_t client_address_len = sizeof(client_address);

recvfrom(udp_socket, buffer, sizeof(buffer), 0, (struct sockaddr*)&client_address, &client_address_len);

2 UDP聊天室

现在来实现一个UDP聊天室,在代码编写的过程中,我们能够学到一些细节。

  • 目的:服务端能够将收到的客户端的消息转发给其它的所有客户端,从而实现多个客户端之间的通信。

2.1 客户端

1、创建UDP套接字

int udp_socket;
udp_socket = socket(AF_INET, SOCK_DGRAM, 0));

2、服务端地址配置

这里使用本地环回地址127.0.0.1作为服务端IP:

#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8080
memset(&server_address, 0, sizeof(server_address));
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = inet_addr(SERVER_IP);
server_address.sin_port = htons(SERVER_PORT);

3、使用select监听stdin和服务端消息

现在需要使用select一边从stdin获取用户输入的数据然后发送给服务端,一边从服务端获取消息。

fd_set read_fds;
while (1)
{
    FD_ZERO(&read_fds);
    FD_SET(STDIN_FILENO, &read_fds);
    FD_SET(udp_socket, &read_fds);

    int activity = select(udp_socket + 1, &read_fds, NULL, NULL, NULL);

    if (FD_ISSET(STDIN_FILENO, &read_fds))
    {
        fgets(buffer, sizeof(buffer), stdin);
        buffer[strlen(buffer) - 1] = '\0';  //换行符替换为结束符
        // 发送消息给服务器
        sendto(udp_socket, buffer, strlen(buffer), 0, (struct sockaddr*)&server_address, sizeof(server_address));
    }

    if (FD_ISSET(udp_socket, &read_fds))
    {
        ssize_t bytes_received = recvfrom(udp_socket, buffer, sizeof(buffer), 0, NULL, NULL);
        buffer[bytes_received] = '\0';
        printf("%s\n", buffer);
    }
}

客户端在调用sendto的时候指定了服务端的地址,服务端收到消息后通过recvfromsrc_addr参数知道客户端的地址并保存起来,接着就可以通过这个地址发送消息给客户端。

2.2 服务端

1、创建并绑定套接字

udp_socket = socket(AF_INET, SOCK_DGRAM, 0);

memset(&server_address, 0, sizeof(server_address));
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = INADDR_ANY;//监听所有地址
server_address.sin_port = htons(PORT);

bind(udp_socket, (struct sockaddr*)&server_address, sizeof(server_address));

2、接收并转发客户端的消息

由于服务端需要接收多个客户端的消息,所以需要创建一个数组来保存已经连接上的客户端的IP和端口号信息。

struct Client {
    struct sockaddr_in address;
    socklen_t len;
    int socket;
};
#define MAX_CLIENTS 10
struct Client clients[MAX_CLIENTS];

然后是调用recvfrom来监听新的客户端消息并创建连接:

ssize_t bytes_received = recvfrom(udp_socket, buffer, sizeof(buffer), 0, (struct sockaddr*)&client_address, &client_address_len);

if (bytes_received == -1)
{
    perror("Error receiving data");
    continue;
}

// 查找客户端是否已存在
int client_index = -1;
for (int i = 0; i < client_count; ++i)
{
    if (memcmp(&client_address, &clients[i].address, sizeof(struct sockaddr_in)) == 0)
    {
        client_index = i;
        break;
    }
}

// 如果是新客户端,则添加到客户端列表
if (client_index == -1)
{
    if (client_count < MAX_CLIENTS)
    {
        clients[client_count].address = client_address;
        clients[client_count].len = client_address_len;
        clients[client_count].socket = udp_socket;
        client_index = client_count;
        printf("new client:(%s:%d)\n", inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));
        client_count++;
    }
    else
    {
        printf("聊天室已满,拒绝新连接\n");
    }
}

最后将其中一个客户端发来的消息转发给其它所有已经建立连接的客户端:

buffer[bytes_received] = '\0';
for (int i = 0; i < client_count; ++i)
{
    if (i != client_index)
    {
        sendto(udp_socket, buffer, bytes_received, 0, (struct sockaddr*)&clients[i].address, clients[i].len);
    }
}

2.3 实验结果

在这里插入图片描述

可以看到,客户端通过发送一个消息与服务端建立连接,建立连接后的所有客户端发送的消息,在服务端收到后会广播给所有的客户端,这样就相当于多个客户端之间的一个聊天室。

2.4 完整代码

1.客户端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/select.h>

#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8080
#define MAX_BUFFER_SIZE 1024

int main() {
    int udp_socket;
    struct sockaddr_in server_address;
    char buffer[MAX_BUFFER_SIZE];

    // 创建UDP套接字
    if ((udp_socket = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
        perror("Error creating socket");
        return EXIT_FAILURE;
    }

    // 设置服务器地址结构
    memset(&server_address, 0, sizeof(server_address));
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = inet_addr(SERVER_IP);
    server_address.sin_port = htons(SERVER_PORT);

    printf("UDP聊天室客户端启动,连接到服务器 %s:%d\n", SERVER_IP, SERVER_PORT);

    fd_set read_fds;
    struct timeval timeout;

    while (1) {
        FD_ZERO(&read_fds);
        FD_SET(STDIN_FILENO, &read_fds);
        FD_SET(udp_socket, &read_fds);
        // 使用 select 监听 stdin 和 udp_socket
        int activity = select(udp_socket + 1, &read_fds, NULL, NULL, NULL);

        if (activity < 0) {
            perror("Error in select");
            break;
        }

        if (FD_ISSET(STDIN_FILENO, &read_fds)) {
            // 从 stdin 读取用户输入
            fgets(buffer, sizeof(buffer), stdin);
            buffer[strlen(buffer) - 1] = '\0';  // 移除末尾的换行符
            // 发送消息给服务器
            sendto(udp_socket, buffer, strlen(buffer), 0, (struct sockaddr*)&server_address, sizeof(server_address));
        }

        if (FD_ISSET(udp_socket, &read_fds)) {
            // 接收服务器消息
            ssize_t bytes_received = recvfrom(udp_socket, buffer, sizeof(buffer), 0, NULL, NULL);

            if (bytes_received == -1) {
                perror("Error receiving data");
                break;
            } else if (bytes_received == 0) {
                // 服务器断开连接
                printf("server closed\n");
                break;
            }

            buffer[bytes_received] = '\0';
            printf("%s\n", buffer);
        }
    }

    close(udp_socket);
    return EXIT_SUCCESS;
}

2.服务端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 8080
#define MAX_CLIENTS 10
#define MAX_BUFFER_SIZE 1024

struct Client {
    struct sockaddr_in address;
    socklen_t len;
    int socket;
};

int main() {
    int udp_socket;
    struct sockaddr_in server_address, client_address;
    socklen_t client_address_len = sizeof(client_address);
    char buffer[MAX_BUFFER_SIZE];

    struct Client clients[MAX_CLIENTS];
    int client_count = 0;

    // 创建UDP套接字
    if ((udp_socket = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
        perror("Error creating socket");
        return EXIT_FAILURE;
    }

    // 设置服务器地址结构
    memset(&server_address, 0, sizeof(server_address));
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = INADDR_ANY;
    server_address.sin_port = htons(PORT);

    // 将套接字绑定到服务器地址和端口
    if (bind(udp_socket, (struct sockaddr*)&server_address, sizeof(server_address)) == -1) {
        perror("Error binding socket");
        close(udp_socket);
        return EXIT_FAILURE;
    }

    printf("UDP聊天室服务端启动,监听端口 %d\n", PORT);

    while (1) {
        // 接收客户端消息
        ssize_t bytes_received = recvfrom(udp_socket, buffer, sizeof(buffer), 0, (struct sockaddr*)&client_address, &client_address_len);

        if (bytes_received == -1) {
            perror("Error receiving data");
            continue;
        }

        // 查找客户端是否已存在
        int client_index = -1;
        for (int i = 0; i < client_count; ++i) {
            if (memcmp(&client_address, &clients[i].address, sizeof(struct sockaddr_in)) == 0) {
                client_index = i;
                break;
            }
        }

        // 如果是新客户端,则添加到客户端列表
        if (client_index == -1) {
            if (client_count < MAX_CLIENTS) {
                clients[client_count].address = client_address;
                clients[client_count].len = client_address_len;
                clients[client_count].socket = udp_socket;
				client_index = client_count;
                printf("new client:(%s:%d)\n", inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));
                client_count++;
            } else {
                printf("聊天室已满,拒绝新连接\n");
            }
        }
        // 转发消息给所有其他客户端
        buffer[bytes_received] = '\0';
        for (int i = 0; i < client_count; ++i) {
            if (i != client_index) {
                sendto(udp_socket, buffer, bytes_received, 0, (struct sockaddr*)&clients[i].address, clients[i].len);
            }
        }
        printf("来自 (%s:%d) 的消息:%s\n", inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port), buffer);
    }

    close(udp_socket);
    return EXIT_SUCCESS;
}

3 UDP进阶注意事项

3.1 如何判断对端断开

在UDP中,由于其面向无连接的特性,没有明确的连接状态,因此在传统的意义上无法像TCP一样直接判断连接是否断开。UDP是无连接、无状态的协议,它不维护连接状态,也不会报告连接断开的事件。

我们可以通过收发数据的过程中发生的错误来判断连接状态。如果你在发送数据时遇到错误(如sendto返回 -1),这可能意味着对方无法接收数据,但这并不一定意味着连接已经断开,只是当前无法发送数据而已。

一个常见的方法是在应用层定义一种心跳机制,定期向对端发送一些特殊的数据包,如果长时间没有收到对端的响应,可以认为连接可能已经断开。

  • 在TCP中readrecv返回0的时候表示连接断开,但在UDP中recvfrom返回0时并不代表连接断开,可能只是对端发了一个空的数据

3.2 UDP报文丢失、重复、乱序、流量控制

在UDP协议下,确实可能会发生报文丢失、重复和乱序的情况。UDP是一种无连接的协议,它不提供可靠性,因此没有内建的机制来保证数据的完整性、顺序性和不重复性。

  1. 报文丢失:

    • 原因: UDP不保证报文的可靠传输,因此报文可能在传输过程中丢失。
    • 解决方案: 应用层可以通过在协议中实现重传机制或者使用应用层的确认机制来处理丢失的报文。
  2. 报文重复:

    • 原因: 在网络中,可能由于网络拥塞、重传机制等原因导致报文重复。
    • 解决方案: 应用层可以通过在报文中添加唯一标识符,并在接收端进行去重处理来避免重复的问题。
  3. 报文乱序:

    • 原因: 报文在传输过程中可能会因为经过不同的网络路径而导致乱序。

    • 解决方案: 在报文中添加序列号,并在接收端进行排序操作,将乱序的报文按序组装成完整的数据。

  4. 无流量控制:

    流量控制通常用于防止发送方发送速率过快,导致网络拥塞或接收方无法及时处理数据的情况。TCP通过滑动窗口机制,发送方可以根据接收方的处理能力来动态调整发送速率。

    在UDP中,由于缺少流量控制机制,发送方会以最大速率发送数据,而无法感知网络的拥塞状况。这可能导致一些问题,例如在网络状况不佳时,UDP发送的数据可能会导致丢包,而且无法自动适应网络状况。

应用层可以实现自定义的协议,通过添加额外的信息,如确认机制、重传机制、序列号等来确保数据的可靠性和顺序性。当然,UDP并不适合所有场景的协议,如果对数据的可靠性要求较高,建议TCP。

3.3 UDP数据被丢弃

在UDP中,如果接收端的缓冲区大小无法容纳整个UDP数据包,多出来的数据将被丢弃。修改一下前面的客户端代码的recvfrom中的参数,每次只接收8个字节:

在这里插入图片描述

来看看效果:

在这里插入图片描述

3.4 connect判断对端是否存在

在我们调用sendto发送数据的时候,即使对端不存在,sendto也会返回成功,这是因为这个函数仅仅是完成了将数据拷贝到套接字的过程。**那如果对端不存在的话,我们有没有什么方法可以判断呢?**我们可以使用connect函数。

在UDP通信中,connect函数通常用于绑定本地UDP套接字到指定的远程地址和端口。这与TCP中的connect 有所不同,因为UDP是无连接的协议,connect并不会像在TCP中一样建立连接。在UDP中,connect主要有两个作用:

  1. 指定默认的目标地址和端口: 通过使用 connect函数,你可以在UDP套接字上指定一个默认的目标地址和端口。这样,在使用 send 函数时,就不需要每次都指定目标地址和端口了。这对于一直与同一个目标通信的UDP套接字而言是方便的。
  2. 用于错误检测和筛选数据包: 虽然connect并不会建立连接,但它可以用于检测目标是否可达。当你使用 connect后,如果目标地址不可达,后续的send操作可能会返回错误。这在某种程度上可以用来判断对端是否存在。但需要注意,UDP的特性仍然存在,即使使用了connectsend仍然是无阻塞的,而不会等待连接的建立。

下面来更改一下代码:

在这里插入图片描述

增加connect,然后sendto可以改为send进行发送,此时如果服务端不存在的话,recvfrom会返回-1,errno的值为ECONNREFUSED,表示连接被拒绝。这样我们就知道对端不存在了。

在这里插入图片描述

这个的原理实际上是当数据无法到达的时候,TCP协议栈会产生一个ICMP错误发给客户端,客户端需要在recvfrom中处理。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用 PyTorch 进行 K 折交叉验证

一、说明 中号机器学习模型在训练后必须使用测试集进行评估。我们这样做是为了确保模型不会过度拟合&#xff0c;并确保它们适用于现实生活中的数据集&#xff0c;与训练集相比&#xff0c;现实数据集的分布可能略有偏差。 但为了使您的模型真正稳健&#xff0c;仅仅通过训练/测…

OneNote for Windows10 彻底删除笔记本

找了超多方法&#xff0c;都没有用&#xff0c;我的OneNote都没有文件选项&#xff0c;要在OneDrive中删除&#xff0c;但是一直登不进&#xff0c;然后又找到一个方法&#xff1a; 在网页中打开Office的控制面板 "Sign in to your Microsoft account" 在“最近”一…

k8s volumes and data

Overview 传统上&#xff0c;容器引擎(Container Engine)不提供比容器寿命更长的存储。由于容器被认为是瞬态(transient)的&#xff0c;这可能会导致数据丢失或复杂的外部存储选项。Kubernetes卷共享 Pod 生命周期&#xff0c;而不是其中的容器。如果容器终止&#xff0c;数据…

ctfhub技能树_web_信息泄露

目录 二、信息泄露 2.1、目录遍历 2.2、Phpinfo 2.3、备份文件下载 2.3.1、网站源码 2.3.2、bak文件 2.3.3、vim缓存 2.3.4、.DS_Store 2.4、Git泄露 2.4.1、log 2.4.2、stash 2.4.3、index 2.5、SVN泄露 2.6、HG泄露 二、信息泄露 2.1、目录遍历 注&#xff1…

POI Excel导入导出(下)

作者简介&#xff1a;大家好&#xff0c;我是smart哥&#xff0c;前中兴通讯、美团架构师&#xff0c;现某互联网公司CTO 联系qq&#xff1a;184480602&#xff0c;加我进群&#xff0c;大家一起学习&#xff0c;一起进步&#xff0c;一起对抗互联网寒冬 上一篇通过四个简单的小…

力扣刷题day1(两数相加,回文数,罗马数转整数)

题目1&#xff1a;1.两数之和 思路1和解析&#xff1a; //1.暴力枚举解法(历遍两次数组&#xff0c;时间复杂度O&#xff08;N^2)&#xff0c;空间复杂度O&#xff08;1&#xff09; int* twoSum(int* nums, int numsSize, int target, int* returnSize) {for (int i 0; i &…

短波红外相机的原理及应用场景

短波红外 (简称SWIR&#xff0c;通常指0.9~1.7μm波长的光线) 是一种比可见光波长更长的光。这些光不能通过“肉眼”看到&#xff0c;也不能用“普通相机”检测到。由于被检测物体的材料特性&#xff0c;一些在可见光下无法看到的特性&#xff0c;却能在近红外光下呈现出来&…

使用Python Flask搭建Web问答应用程序并发布到公网远程访问

使用Python Flask搭建web问答应用程序框架&#xff0c;并发布到公网上访问 文章目录 使用Python Flask搭建web问答应用程序框架&#xff0c;并发布到公网上访问前言1. 安装部署Flask并制作SayHello问答界面2. 安装Cpolar内网穿透3. 配置Flask的问答界面公网访问地址4. 公网远程…

AI Agent 结构与分类

一、什么是AI agent 在人工智能中&#xff0c;智能代理AI Agent是以智能方式行事的代理&#xff1b;它感知环境&#xff0c;自主采取行动以实现目标&#xff0c;并可以通过学习或获取知识来提高其性能。人其实就是一种复杂代理。 为了理解智能代理的结构&#xff0c;我们应该熟…

go写文件后出现大量NUL字符问题记录

目录 背景 看看修改前 修改后 原因 背景 写文件完成后发现&#xff1a; size明显也和正常的不相等。 看看修改前 buf : make([]byte, 64) buffer : bytes.NewBuffer(buf)// ...其它逻辑使得buffer有值// 打开即将要写入的文件&#xff0c;不存在则创建 f, err : os.Open…

Ruby和HTTParty库下载代码示例

ruby require httparty require nokogiri # 设置服务器 proxy_host "" proxy_port "" # 定义URL url "" # 创建HTTParty对象&#xff0c;并设置服务器 httparty HTTParty.new( :proxy > "#{proxy_host}:#{proxy_port}" ) …

不止有console.log()可以打印日志

1.带错误的打印 //1.醒目的打印 2.方便筛选器筛选 console.log("正常打印") console.warn("警告打印") console.error("错误打印") console.info("信息打印") console.log("%c带样式的打印", "color: red; font-size…

Nginx(十二) gzip gzip_static sendfile directio aio 组合使用测试(2)

测试10&#xff1a;开启gzip、sendfile、aio、directio1m&#xff0c;关闭gzip_static&#xff0c;请求/index.js {"time_iso8601":"2023-11-30T17:20:5508:00","request_uri":"/index.js","status":"200","…

论文阅读[2022sigcomm]GSO-Simulcast Global Stream Orchestration in Simulcast Video

GSO-Simulcast Global Stream Orchestration in Simulcast Video 作者&#xff1a; 1 背景 1视频会议成为全球数十亿人远程协作、学习和个人互动的核心&#xff0c;这些不断增长的虚拟连接需求推动视频会议服务的蓬勃发展 2当前用户越来越希望在低延迟下看到更高质量的视频…

【C/PTA —— 14.结构体1(课外实践)】

C/PTA —— 14.结构体1&#xff08;课外实践&#xff09; 一.函数题6-1 选队长6-2 按等级统计学生成绩6-3 学生成绩比高低6-4 综合成绩6-5 利用“选择排序算法“对结构体数组进行排序6-6 结构体的最值6-7 复数相乘运算 二.编程题7-5 一帮一7-6 考试座位号 一.函数题 6-1 选队长…

【Selenium+Webmagic】基于JAVA语言实现爬取js渲染后的页面,附有代码

事先声明 笔者最近需要查看一些数据&#xff0c;自己挨个找太麻烦了&#xff0c;于是简单的学了一下爬虫。笔者在这里声明&#xff0c;爬的数据只为学术用&#xff0c;没有其他用途&#xff0c;希望来这篇文章学习的同学能抱有同样的目的。 枪本身不坏&#xff0c;坏的是使用枪…

20:kotlin 类和对象 --泛型(Generics)

类可以有类型参数 class Box<T>(t: T) {var value t }要创建类实例&#xff0c;需提供类型参数 val box: Box<Int> Box<Int>(1)如果类型可以被推断出来&#xff0c;可以省略 val box Box(1)通配符 在JAVA泛型中有通配符?、? extends E、? super E&…

peft / bitsandbytes包windows安装问题

peft / bitsandbytes包windows安装问题 环境版本安装peftCUDA Setup failed despite GPU being available报错信息解决方法 ImportError: cannot import name is_npu_available from accelerate.utils报错信息解决方法 AttributeError: NoneType object has no attribute cuDev…

linux部署前端静态页面(实战)

Linux基本命令&#xff08;学习笔记&#xff09;零基础入门linux系统运维_linux find exec rm_Z_Xshan的博客-CSDN博客 如果linux不熟可以看我之前写的入门教程 感谢支持&#xff01;&#xff01; 一、服务器 这里去购买云服务器&#xff0c;如果是练习可以用虚拟机&#xff…

内网穿透的应用-公网环境下移动端通过群晖管家+cpolar远程管理家中本地局域网内黑群晖设备

白嫖怪狂喜&#xff01;黑群晖也能使用群晖管家啦&#xff01; 文章目录 白嫖怪狂喜&#xff01;黑群晖也能使用群晖管家啦&#xff01;1.使用环境要求&#xff1a;2.下载安装群晖管家app3.随机地址登陆群晖管家app4.固定地址登陆群晖管家app 自己组装nas的白嫖怪们虽然也可以通…