Web服务器小项目(Linux / C / epoll)

news2024/10/12 14:25:16

注意:前置知识:
HTTP: https://xingzhu.top/archives/web-fu-wu-qi
Linux 多线程: https://xingzhu.top/archives/duo-xian-cheng

源码放github上了,欢迎star: https://github.com/xingzhuz/webServer

思路

实现代码

server.h

#pragma once
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/stat.h>
#include <assert.h>
#include <sys/sendfile.h>
#include <dirent.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <stdlib.h>
#include <ctype.h>
#include <pthread.h>

// 子线程执行动作函数的参数结构体
struct FdInfo
{
    int fd;
    int epfd;
    pthread_t tid;
};

// 初始化监听的套接字
int initListenFd(unsigned short port);

// 启动 epoll
int epollRun(int lfd);

// 和客户端建立连接
// int acceptClient(int lfd, int epfd);
void *acceptClient(void *arg);

// 接收http请求
// int recvHttpRequest(int cfd, int epfd);
void *recvHttpRequest(void *arg);

// 解析请求行
int parseRequestLine(const char *line, int cfd);

// 发送文件
int sendFile(const char *fileName, int cfd);

// 发送响应头(状态行+响应头)
int sendHeadMsg(int cfd, int status, const char *descr, const char *type, int length);

// 根据文件名字或者后缀获取 HTTP 格式响应的数据类型
const char *getFileType(const char *name);

// 发送目录
int sendDir(const char *dirName, int cfd);

// 将字符转换为整形数
int hexToDec(char c);

// 解码
// to 存储解码之后的数据,传出参数,from为被解码的数据,传入参数
void decodeMsg(char *to, char *from);

main.c

#include "server.h"

int main(int argc, char *argv[])
{
    if (argc < 3)
    {
        printf("./a.out port path\n");
        return -1;
    }
    unsigned short port = atoi(argv[1]);

    // 切换服务器的工作路径
    chdir(argv[2]);

    // 初始化用于监听的套接字
    int lfd = initListenFd(port);

    // 启动服务器程序
    epollRun(lfd);
    return 0;
}

initListenFd

// 初始化监听的套接字
int initListenFd(unsigned short port)
{
    // 1.创建监听的fd
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if (lfd == -1)
    {
        perror("socket");
        return -1;
    }

    // 2. 设置端口复用
    int opt = 1;
    int ret = setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof opt);
    if (ret == -1)
    {
        perror("setsocket");
        return -1;
    }

    // 3. 绑定
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = INADDR_ANY;
    ret = bind(lfd, (struct sockaddr *)&addr, sizeof addr);
    if (ret == -1)
    {
        perror("bind");
        return -1;
    }

    // 4. 设置监听
    ret = listen(lfd, 128);
    if (ret == -1)
    {
        perror("listen");
        return -1;
    }

    // 返回 fd
    return lfd;
}
  • 这些步骤都是基础的 Socket 网络通信部分,不再赘述
  • 解释端口复用:因为存在服务器端主动断开连接的情况,如果是服务器端主动断开连接,主动断开的一方存在一个等待时长,也就是在这个等待时长内,端口还是没有被释放,时长结束后才会释放
  • 如果不想等待这个时长或者由于这个时长而换端口,就需要设置这个端口复用,设置后即使即使是在等待时长时间段内,仍可使用该端口
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
  • sockfd: 套接字的文件描述符,通常是通过 socket() 函数创建的
  • level: 选项所在的协议层,通常为 SOL_SOCKET,表示通用的套接字选项。也可以是特定协议的层,例如 IPPROTO_TCP
  • optname: 需要设置的选项的名称。可以是多种选项,如:
    • SO_REUSEADDR: 允许重用本地地址
    • SO_KEEPALIVE: 启用 TCP 的保活机制
    • SO_BROADCAST: 允许发送广播消息
  • optval: 指向要设置的选项值的指针。这个值的类型取决于选项的类型
  • optlen: optval 所指向的值的大小,通常使用 sizeof() 来获取

epollRun

int epollRun(int lfd)
{
    // 1. 创建 epoll 实例
    int epfd = epoll_create(1); // 1 这个参数已被弃用,随便写一个大于 0 的数即可
    if (epfd == -1)
    {
        perror("epoll_create");
        return -1;
    }

    // 2. lfd 上树
    struct epoll_event ev;
    ev.data.fd = lfd;    // data 是一个联合体,只能使用一个成员,这里使用 fd
    ev.events = EPOLLIN; // 委托内核需要检测的事件: 检测连接请求,对于服务器而言就是读事件 EPOLLIN

    // 参数: epoll id, 操作的动作, 操作的文件描述符, 事件结构体
    // 可以做三件事,增加、修改、删除
    int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
    if (ret == -1)
    {
        perror("epoll_ctl");
        return -1;
    }

    // 3. 检测
    struct epoll_event evs[1024];
    int size = sizeof(evs) / sizeof(struct epoll_event);
    while (1)
    {
        // 检测添加到 epoll 树上的文件描述符事件是否被激活,也就是是否有事件到达
        // 第四个参数是阻塞时长,如果为 -1 就是一直阻塞
        int num = epoll_wait(epfd, evs, size, -1);
        for (int i = 0; i < num; i++)
        {
            struct FdInfo *info = (struct FdInfo *)malloc(sizeof(struct FdInfo));
            int fd = evs[i].data.fd;
            info->epfd = epfd;
            info->fd = fd;
            if (fd == lfd)
            {
                // 如果是监听的文件描述符,建立新连接 accept
                // 注意这里的 accept 是不会阻塞的,因为 epoll 已经检测了,只有触发了,才会在 evs 数组中
                // 这里创建多线程处理,效率更高
                pthread_create(&info->tid, NULL, acceptClient, info);
            }
            else
            {
                // 响应客户端请求,接收客户端请求
                pthread_create(&info->tid, NULL, recvHttpRequest, info);
            }
        }
    }
}
  • epoll 是 IO 多路转接 / 复用中的一个实现,可以大大提高效率,IO 多路转接/复用可以实现一个线程就监视多个文件描述符,其实现机制是在内核去中监视的,也就是可以大大减小开销,不用手动创建线程阻塞等待连接了,内核区监视是否有连接请求
  • epoll 是实现方式中效率较高的,是基于红黑树实现的,搜索起来快速

acceptClient

void *acceptClient(void *arg)
{
    struct FdInfo *info = (struct FdInfo *)arg;

    // 1. 建立连接
    // 第二三个参数都是客户端相关信息,不需要知道,直接指定为 NULL
    int cfd = accept(info->fd, NULL, NULL);
    if (cfd == -1)
    {
        perror("accept");
        return NULL;
    }

    // 2. 设置非阻塞
    int flag = fcntl(cfd, F_GETFL); // 第二个参数表示得到当前文件描述符属性
    flag |= O_NONBLOCK;             // 将非阻塞属性 O_NONBLOCK 追加进去
    fcntl(cfd, F_SETFL, flag);      // 重新设置文件描述符的属性,即 flag

    // 3. cfd 添加到 epoll 中
    struct epoll_event ev;
    ev.data.fd = cfd;
    // 这个加的属性 EPOLLET 表示设置这个通信的文件描述符对应的处理事件为边沿触发模式
    ev.events = EPOLLIN | EPOLLET;
    int ret = epoll_ctl(info->epfd, EPOLL_CTL_ADD, cfd, &ev);
    if (ret == -1)
    {
        perror("epoll_ctl");
        return NULL;
    }

    printf("acceptClient threadId: %ld\n", info->tid);
    free(info);
    return 0;
}
  • epoll 工作模式中,边缘非阻塞模式效率最高,因此采用这个,所以设置了文件描述符为非阻塞模式(默认为阻塞)
  • 这里的连接和接收数据用多线程处理效率更高,即使之前已经实现了多个客户端和多个服务器端通信

recvHttpRequest

void *recvHttpRequest(void *arg)
{
    struct FdInfo *info = (struct FdInfo *)arg;

    int len = 0, total = 0;
    char tmp[1024] = {0};
    char buf[4096] = {0};

    while ((len = recv(info->fd, tmp, sizeof tmp, 0)) > 0)
    {
        if (total + len < sizeof buf)
        {
            memcpy(buf + total, tmp, len);
        }
        total += len;
    }
    // 判断数据是否被接收完毕
    if (len == -1 && errno == EAGAIN)
    {
        // 解析请求行
        char *pt = strstr(buf, "\r\n");
        int reLen = pt - buf;
        buf[reLen] = '\0';
        parseRequestLine(buf, info->fd);
    }
    else if (len == 0)
    {
        // 客户端断开了连接
        // 删除在 epoll 树上的文件描述符,因为不需要检测这个文件描述符了
        epoll_ctl(info->epfd, EPOLL_CTL_DEL, info->fd, NULL);
        close(info->fd);
    }
    else
        perror("recv");

    printf("resvMsg threadId: %ld\n", info->tid);
    free(info);
    return NULL;
}
  • 上述 total 是偏移量,因为 memcpy 是从起始位置开始复制
  • 虽然 buf 只有 4096 字节,存在读不完所有的请求数据,但是这也是没问题的,有用的数据 4096 已经够了,因为请求行最重要,只需要知道客户端向服务器请求的静态资源是什么,即便后面没读完,也不影响
  • 由于这个套接字是非阻塞,所以当数据读完后,不阻塞,但是返回 -1,但是读取数据失败也是返回 -1,这就无法判断是否是读取完数据了,此时再用到 errno == EAGAIN 就能判断成功
  • 如果套接字是阻塞的,当读取完数据后,会一直阻塞,所以书写逻辑需要更改,内部判断是否读取完毕,然后 break 循环

parseRequestLine

// 解析请求行
int parseRequestLine(const char *line, int cfd)
{
    // 解析请求行 get /xxx/1.jpg http/1.1
    char method[12];
    char path[1024];
    sscanf(line, "%[^ ] %[^ ]", method, path);
    if (strcasecmp(method, "get") != 0) // 这个比较忽略大小写
    {
        // 这里只处理 get 请求
        return -1;
    }
    
    // 处理中文编码问题
    decodeMsg(path, path);

    // 处理客户端请求的静态资源(目录或者文件)
    char *file = NULL;
    if (strcmp(path, "/") == 0)
    {
        // 说明只有当前资源目录
        file = "./";
    }
    else
    {
        // 说明目录中存在当前资源目录中的子目录
        // 去掉 '/' 就能是相对路径了,就成功了,或者在开头加个 '.' 也行
        file = path + 1;
    }

    // printf("%s\n", file);
    // 获取文件属性
    struct stat st;
    int ret = stat(file, &st);
    if (ret == -1)
    {
        // 文件不存在 -- 回复 404
        // 最后一个参数设置为 -1,让浏览器自己计算长度
        sendHeadMsg(cfd, 404, "Not Found", getFileType(".html"), -1);
        sendFile("404.html", cfd); // 这个 html 需要当前资源目录下的 html文件(自己部署)
        return 0;
    }
    // 判断文件类型
    if (S_ISDIR(st.st_mode))
    {
        // 把这个目录中的内容发送给客户端
        sendHeadMsg(cfd, 200, "OK", getFileType(".html"), -1);
        sendDir(file, cfd);
    }
    else
    {
        // 把文件的内容发送给客户端
        sendHeadMsg(cfd, 200, "OK", getFileType(file), st.st_size);
        sendFile(file, cfd);
    }
}

sendFile

// 发送的数据部分
int sendFile(const char *fileName, int cfd)
{
    // 1.打开文件
    int fd = open(fileName, O_RDONLY);
    // 断言判断文件是否打开成功,如果打开失败,程序直接挂在这里,或者抛出异常
    assert(fd > 0);  
#if 0
    while (1)
    {
        char buf[1024];
        int len = read(fd, buf, sizeof buf);
        if (len > 0)
        {
            send(cfd, buf, len, 0);
            usleep(10); // 这非常重要
        }
        else if (len == 0)   // 文件内容读取完毕
            break;
        else
            perror("read");
    }
#else
    off_t offset = 0;
    int size = lseek(fd, 0, SEEK_END);
    lseek(fd, 0, SEEK_SET);

    while (offset < size)
    {
        int ret = sendfile(cfd, fd, &offset, size - offset);
        printf("ret value: %d\n", ret);
        if (ret == -1 && errno != EAGAIN)
        {
            perror("sendfile");
        }
    }

#endif
    return 0;
}
  • 上述是发送文件的两种方式
  • 第一种方式的 usleep(10) 很重要,发送数据很快,但是客户端读数据不一定这么快,客户端需要读取数据,然后进行解析,然后呈现出,这都需要耗时间的,不休眠一会儿,会存在接收数据不一致的问题(我遭受过…)
  • 第二种方式使用库函数 sendfile ,通过这个函数发送,比手写的发送文件代码效率高,因为会减少拷贝次数,第四个参数是发送的大小,size - offset 的原因是 offset 这个参数是传入传出参数,会偏移到发送的位置,由于多次发送,前面发送了数据之后,就不是 size 了,就需要减去发送的字节数,也就是传出的偏移量 offset
  • 注意 lseek 函数计算文件大小,会移动文件的指针,且 sendfile 也是有内部也是有缓存大小的,因此需要循环读取发送
  • if 判断是因为文件描述符改为了非阻塞模式,会一直读取数据,如果数据读完,也会返回 -1 ,所以就需要再加个判断

sendHeadMsg

// 发送响应头
int sendHeadMsg(int cfd, int status, const char *descr, const char *type, int length)
{
    // 状态行
    char buf[4096] = {0};
    sprintf(buf, "http/1.1 %d %s\r\n", status, descr);

    // 响应头
    sprintf(buf + strlen(buf), "content-type: %s\r\n", type);
    sprintf(buf + strlen(buf), "content-length: %d\r\n\r\n", length); // 注意两个\r\n

    send(cfd, buf, strlen(buf), 0);
    return 0;
}

getFileType

// 根据文件名字或者后缀获取 HTTP 格式响应的数据类型
const char *getFileType(const char *name)
{
    // a.jpg a.mp4 a.html
    // 自右向左查找 '.' 字符,如不存在返回 NULL
    const char *dot = strrchr(name, '.');

    if (dot == NULL)
        return "text/plain;charset=utf-8"; // 纯文本

    if (strcmp(dot, ".html") == 0 || strcmp(dot, ".htm") == 0)
        return "text/html; charset=utf-8";

    if (strcmp(dot, ".jpg") == 0 || strcmp(dot, ".jpeg") == 0)
        return "image/jpeg";

    if (strcmp(dot, ".gif") == 0)
        return "image/gif";

    if (strcmp(dot, ".png") == 0)
        return "image/png";

    if (strcmp(dot, ".css") == 0)
        return "text/css";

    if (strcmp(dot, ".au") == 0)
        return "audio/basic";

    if (strcmp(dot, ".wav") == 0)
        return "audio/wav";

    if (strcmp(dot, ".mp3"))
        return "audio/mp3";

    // 还有一些未写

    return "text/plain; charset = utf-8";
}

sendDir

下述拼接是这样的

<html>
    <head>
        <title>test</title>
    </head>
    <body>
        <table>    <!--- 开头拼接到这 --->
            <tr>            <!--- 中间部分的拼接 --->
                <td></td>
                <td></td>
            </tr>
            <tr>
                <td></td>
                <td></td>
            </tr>
        </table>   <!--- 尾巴从这开始拼接  -->
    </body>
</html>
// 发送目录
int sendDir(const char *dirName, int cfd)
{
    char buf[8192] = {0};
    sprintf(buf,
            "<html>"
            "<head>"
            "<title>%s</title>"
            "<style>"
            "body { font-family: Arial, sans-serif; margin: 20px; background-color: #f4f4f4; }"
            "h1 { color: #2c3e50; text-align: center; }"
            "table { width: 100%%; border-collapse: collapse; margin-top: 20px; }"
            "th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }"
            "th { background-color: #3498db; color: white; }"
            "tr:hover { background-color: #e7f3ff; }"
            "a { text-decoration: none; color: #3498db; transition: color 0.3s; }"
            "a:hover { color: #2980b9; text-decoration: underline; }"
            "</style>"
            "</head>"
            "<body><h1>%s</h1><table><tr><th>名称</th><th>大小 (字节)</th></tr>",
            dirName, dirName);

    struct dirent **namelist;
    // 第三个参数是回调函数,表示遍历时过滤的规则, 第四个参数是排序的方式
    int num = scandir(dirName, &namelist, NULL, alphasort);
    // 虽然 namelist 定义时没有分配地址,但是在函数调用后就分配了地址,所以后续要释放内存

    for (int i = 0; i < num; i++)
    {
        // 取出文件名,namelist 指向的是一个指针数组
        char *name = namelist[i]->d_name;
        struct stat st;
        char subPath[1024] = {0};
        sprintf(subPath, "%s/%s", dirName, name);
        stat(subPath, &st);

        if (S_ISDIR(st.st_mode))
        {
            sprintf(buf + strlen(buf),
                    "<tr><td><a href=\"%s/\">%s</a></td><td>%ld</td></tr>",
                    name, name, st.st_size);
        }
        else
        {
            sprintf(buf + strlen(buf),
                    "<tr><td><a href=\"%s\">%s</a></td><td>%ld</td></tr>",
                    name, name, st.st_size);
        }

        send(cfd, buf, strlen(buf), 0);
        memset(buf, 0, sizeof buf);
        free(namelist[i]);
    }

    sprintf(buf, "</table></body></html>");
    send(cfd, buf, strlen(buf), 0);
    free(namelist);
    return 0;
}
  • 拼接 html 网页元素,是因为需要一个网页形式发送给浏览器
  • 可以拼一份,发一份,因为底层使用的 TCP 协议
  • 注意上述 a 标签那儿 \"%s/\" 需要 \ 转义,因为前面已经有 " 了,所以需要用 \ 转义,%s 后面加 / 是因为可能需要点击进入这个子目录,所以必须要这个 /

注意: 中文乱码问题

  • HTTP 协议中,不支持特殊字符 (如中文),会自动转义为 utf-8 编码,也就是如果当前文件名为中文,那么 linux 会将这个特殊字符转换为 utf-8 编码
  • /Linux%E5%86%85%E6%A0%B8.jpg 原本是 /Linux内核.jpg ,这样之后发送信息时就打不开了,报错 Not Found ,因为本地文件名是带有中文,但是经过代码处理后,程序读出的文件名没有中文,就找不到了
  • 因此需要转换一下

decodeMsg

// 将字符转换为整形数
int hexToDec(char c)
{
    if (c >= '0' && c <= '9')
        return c - '0';
    if (c >= 'a' && c <= 'f')
        return c - 'a' + 10;
    if (c >= 'A' && c <= 'F')
        return c - 'A' + 10;

    return 0;
}

// 解码
// to 存储解码之后的数据,传出参数,from被解码的数据,传入参数
void decodeMsg(char *to, char *from)
{
    for (; *from != '\0';)
    {
        // isxdigit -> 判断字符是不是16进制格式,取值在 0-f
        if (from[0] == '%' && isxdigit(from[1]) && isxdigit(from[2]))
        {
            // 将 16进制的数 -> 十进制 将这个数值赋值给了字符 int -> char
            *to = hexToDec(from[1]) * 16 + hexToDec(from[2]);

            // 跳过 '%' 和后面的两个字符
            to++;
            from += 3; // 修改为3
        }
        else
        {
            // 字符拷贝,赋值
            *to = *from;
            to++;
            from++;
        }
    }
    *to = '\0'; // 添加字符串结束符
}
  • 这个没必要理解,直接网上搜索即可,这里让 GPT 润色修改成功的

404.html

<!DOCTYPE html>
<html lang="zh">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>404 页面未找到</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            background-color: #f4f4f4;
            margin: 0;
            padding: 0;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            text-align: center;
        }

        .container {
            background-color: #fff;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
            padding: 40px;
            max-width: 400px;
            width: 100%;
        }

        h1 {
            font-size: 72px;
            color: #e74c3c;
            margin: 0;
        }

        h2 {
            color: #333;
            margin: 20px 0;
        }

        p {
            color: #666;
            margin-bottom: 20px;
        }

        a {
            text-decoration: none;
            background-color: #3498db;
            color: #fff;
            padding: 10px 20px;
            border-radius: 5px;
            transition: background-color 0.3s;
        }

        a:hover {
            background-color: #2980b9;
        }
    </style>
</head>

<body>
    <div class="container">
        <h1>404</h1>
        <h2>页面未找到</h2>
        <p>抱歉,我们找不到您请求的页面。</p>
        <a href="/">返回首页</a>
    </div>
</body>

</html>

效果展示

# 编译
gcc *.c -o server
./server
  1. 刚登上服务器页面

QQ_1727247435027

  1. 点击 code 文件目录中的 cal.c 代码文件

QQ_1727247468878

QQ_1727247496701

  1. 点击 html 目录中的一个 html 文件

QQ_1727181780278

  1. 点击 Image 目录中的一个图片

在这里插入图片描述

  1. 点击 mp3 文件,开始播放

在这里插入图片描述

  1. 如果是一个不存在的文件

QQ_1727247655101


说明:参考学习:https://subingwen.cn/

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

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

相关文章

毕设成品 基于深度学习二维码检测识别系统

文章目录 0 简介1 二维码基础概念1.1 二维码介绍1.2 QRCode1.3 QRCode 特点 2 机器视觉二维码识别技术2.1 二维码的识别流程2.2 二维码定位2.3 常用的扫描方法 4 深度学习二维码识别4.1 部分关键代码 最后 0 简介 今天学长向大家分享一个毕业设计项目 **毕业设计 基于深度学习…

【最新华为OD机试E卷-支持在线评测】第K个排列(100分)多语言题解-(Python/C/JavaScript/Java/Cpp)

🍭 大家好这里是春秋招笔试突围 ,一枚热爱算法的程序员 💻 ACM金牌🏅️团队 | 大厂实习经历 | 多年算法竞赛经历 ✨ 本系列打算持续跟新华为OD-E/D卷的多语言AC题解 🧩 大部分包含 Python / C / Javascript / Java / Cpp 多语言代码 👏 感谢大家的订阅➕ 和 喜欢�…

【MySQL 保姆级教学】在Linux(CentoS 7)中安装MySQL(1)

目录 1. 卸载linux&#xff08;Centos7&#xff09; 中不要的环境2. 获取MySQL官方yum源2.1 获取yum源前先查看自己 linux&#xff08;Centos&#xff09;的版本2.2 获取官方yum源 3. 安装xftp和连接4. 开放连接端口5. 上传文件到Centos76. 安装MySQL6.1 顺利安装6.2 查询是否安…

Terminus ssh key 登陆

生成key 一、添加 KEY 配置 电脑: Terminus > Preferences&#xff0c;或 ⌘,。选择左侧 Keychain 标签。 手机: Terminus > Keychain 电脑: 点击右侧上方的 NEW KEY 按钮, 手机: 点加号 电脑: 在最右侧弹出的页面中填写 Label 和 Private key&#xff0c;Private ke…

电脑怎么录屏?探索屏幕捕捉的奥秘,新手也能成为录屏高手!

在数字时代&#xff0c;无论是制作教学视频、分享游戏精彩瞬间还是展示软件操作流程&#xff0c;屏幕录制都成了一项必不可少的技能。然而&#xff0c;对于许多初次接触录屏的新手来说&#xff0c;如何开始这一过程似乎充满了挑战。本文将为你揭开录屏的神秘面纱&#xff0c;带…

golang-基础知识(流程控制)

1 条件判断if和switch 所有的编程语言都有这个if&#xff0c;表示如果满足条件就做某事&#xff0c;不满足就做另一件事&#xff0c;go中的if判断和其它语言的区别主要有以下两点 1. go里面if条件判断不需要括号 2. go的条件判断语句中允许声明一个变量&#xff0c;这个变量…

MySQL8.0环境部署+Navicat17激活教程

安装MySQL 下载MySQL MySQL官网下载当前最新版本&#xff0c;当前是8.0.39。 选择No thanks, just start my download等待下载即可。 安装MySQL 下载完成后&#xff0c;双击安装进入安装引导页面。选择Custom自定义安装。 选择MySQL Server 8.0.39 - X64安装。 点击Execute执…

基于BeautyEye开发Java程序用户界面

文章目录 I idea引入jar包添加本地jar包maven方式引入本地包方式1:将第三方JAR包安装到本地仓库maven方式引入本地包方式2:引用本地路径将本地jar包打进war包Maven内置变量说明II BeautyEye Swing外观实现方案案例III 知识扩展Swing常用的顶级容器BeautyEye SwingI idea引入j…

在python中如何判断回文串(二)?

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;开发者-曼亿点 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 曼亿点 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a…

一次性解决vue3引入@jiaminghi/data-view需要手动修改node_modules下文件

修改文件1&#xff1a;node_modules\jiaminghi\data-view\lib\components\decoration6\src\main.vue 修改文件2&#xff1a; node_modules\jiaminghi\data-view\lib\components\decoration3\src\main.vue 修改前&#xff1a; 修改后&#xff1a; 通过打补丁的方式对引用库进行…

SpringBoot购物推荐网站:从零到一的构建过程

1系统概述 1.1 研究背景 如今互联网高速发展&#xff0c;网络遍布全球&#xff0c;通过互联网发布的消息能快而方便的传播到世界每个角落&#xff0c;并且互联网上能传播的信息也很广&#xff0c;比如文字、图片、声音、视频等。从而&#xff0c;这种种好处使得互联网成了信息传…

如何使用ipopt进行非线性约束求目标函数最小值(NLP非线性规划)内点法(Interior point method)

非线性规划,一般用matlab调用cplex和gurobi了,但这两个一般用于线性规划和二次规划 线性规划LP,二次规划(quadratic programming),如果要求更一般的非线性规划IPOT是个很好的选择,求解器很多,根据情况自己选择 非线性 具体的,这篇文章介绍的很清楚了https://blog.csd…

Javascript笔试题目(一)

1.JS查找文章中出现频率最高的单词? 要在JavaScript中查找文章中出现频率最高的单词&#xff0c;你可以按照以下步骤进行操作&#xff1a; 将文章转换为小写&#xff1a;这可以确保单词的比较是大小写不敏感的。移除标点符号&#xff1a;标点符号会干扰单词的计数。将文章拆…

SpringBoot环境下的电商推荐网站开发全攻略

1系统概述 1.1 研究背景 如今互联网高速发展&#xff0c;网络遍布全球&#xff0c;通过互联网发布的消息能快而方便的传播到世界每个角落&#xff0c;并且互联网上能传播的信息也很广&#xff0c;比如文字、图片、声音、视频等。从而&#xff0c;这种种好处使得互联网成了信息传…

题目:1297. 子串的最大出现次数

> Problem: 1297. 子串的最大出现次数 题目&#xff1a;1297. 子串的最大出现次数 题目描述 给定一个字符串 s&#xff0c;要求找到满足以下条件的任意子串的出现次数&#xff0c;并返回该子串的最大出现次数&#xff1a; 子串中不同字母的数目必须小于等于 maxLetters。…

使用3080ti配置安装blip2

使用3080ti运行blip2的案例 本机环境&#xff08;大家主要看GPU&#xff0c;ubuntu版本和cuda版本即可&#xff09;&#xff1a;安装流程我最后安装的所有包的信息&#xff08;python 3.9 &#xff09;以供参考&#xff08;environment.yml&#xff09;&#xff1a; 本机环境&a…

Git:LF will be replaced by CRLF、pytest PermissionError以及Git应用中的一些问题解决及一些使用技巧

一、Git:LF will be replaced by CRLF和pytest: --cov NTERNALERROR PermissionError 1. git warning: LF will be replaced by CRLF in ***file 偶然git add在进行代码提交的时候碰到警告warning: LF will be replaced by CRLF in ***file&#xff0c;原因是编辑的代码内容中…

java抽象类和接口-cnblog

java抽象类和接口 1 抽象类 在解决实际问题时,一般将父类作为抽象类&#xff0c;子类继承父类&#xff0c;并且实例化对象 在一个类中&#xff0c;只要有有一个方法是抽象的&#xff0c;类就是抽象的 抽象类被继承后需要实现所有的抽象方法&#xff0c;抽象类的关键词是abst…

entity,pojo,vo,dto 详解

在Java项目中&#xff0c;包名通常用于组织代码&#xff0c;使其更加清晰和易于维护。entity、pojo、vo和dto是常见的包名&#xff0c;它们各自有不同的含义和用途。下面将详细解释这些包名的含义&#xff0c;并提供一个示例&#xff0c;帮助你更好地理解它们在项目中的应用。 …

第二届 龙信杯 电子数据取证竞赛部分Writeup

大佬文章&#xff1a; 龙信杯复现&#xff08;23、24&#xff09; | BthclsBlog 手机部分 资料&#xff1a;2024年第二届龙信杯 WP_2024龙信杯wp-CSDN博客 1.分析手机检材&#xff0c;请问此手机共通过adb连接过几个设备&#xff1f;[标准格式&#xff1a;3] 2 /data/a…