【C/C++服务器开发】具备丰富功能和可以同时响应多个客服端的服务器

2023-05-16

文章目录

    • 一、前言
    • 二、功能更加丰富的服务器
    • 三、让服务器可以同时响应多个客户端请求
      • 1. 单线程 / 进程
      • 2. 多进程并发
        • 父进程:
        • 子进程:
      • 3. 多线程并发
        • 主线程:
        • 子线程:
    • 四、技术准备

一、前言

往期博客回顾:

C/C++ 服务器/后台开发学习路线总结及准备

【C/C++服务器开发】什么是服务器?服务器分类及构建一个简单的服务器系统

【C/C++服务器开发】socket网络编程函数接口详解

【C/C++服务器开发】socket网络编程函数接口的灵活运用

之前的服务器只能提供简单的功能,我们可以不断丰富其功能,使其可以提供更加丰富的功能。比如web服务器可以提供网页数据,文件服务器可以提供文件下载等。

此外之前的服务器只能同时响应一个客户端的请求,因此我们需要考虑使用多进程或多线程来改进服务器模型,使其可以同时接收多个客户端的请求。

二、功能更加丰富的服务器

服务器的功能需要根据我们的需求来确定,这边我们只是提供一种思路,一个方向。

比如我们在数据库端保存一份数据,然后客户端可以通过连接服务器端来查询这个数据。

server.cpp

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(){

    //创建套接字
    int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

    //将套接字和IP、端口绑定
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));  //每个字节都用0填充
    serv_addr.sin_family = AF_INET;  //使用IPv4地址
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具体的IP地址
    serv_addr.sin_port = htons(1234);  //端口
    bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
    
    //进入监听状态,等待用户发起请求
    listen(serv_sock, 20);

    char bufferrev[100];
    memset(bufferrev, 0x00, sizeof(bufferrev));
    char* buffersnd;

    while(1) {

        //接收客户端请求
        struct sockaddr_in clnt_addr;
        socklen_t clnt_addr_size = sizeof(clnt_addr);
        int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
        
        int len = read(clnt_sock, bufferrev, 100-1);
        printf("receive data: %s\n", bufferrev);

        if (strcmp(bufferrev, "wuhan") == 0) {
            strcpy(buffersnd, "you want know wuhan!");
        } else if (strcmp(bufferrev, "guangzhou") == 0) {
            strcpy(buffersnd, "you want know guangzhou!");
        } else if (strcmp(bufferrev, "shenzhen") == 0) {
            strcpy(buffersnd, "you want know shenzhen!");
        } else if (strcmp(bufferrev, "shagnhai") == 0) {
            strcpy(buffersnd, "you want know shanghai!");
        } else {strcpy(buffersnd, "no data about that!");}

        //向客户端发送数据,将从客户端接收的数据反送回去
        printf("server send data to client!\n");
        write(clnt_sock, buffersnd, sizeof(buffersnd));

        //重置缓存区
        memset(bufferrev, 0x00, sizeof(bufferrev));
        memset(buffersnd, 0x00, sizeof(buffersnd));

        //关闭客户端套接字
        close(clnt_sock);

    }
    
    //关闭服务端套接字
    close(serv_sock);
    return 0;
}

client.cpp

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

int main(){
    

    //向服务器(特定的IP和端口)发起请求
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));  //每个字节都用0填充
    serv_addr.sin_family = AF_INET;  //使用IPv4地址
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具体的IP地址
    serv_addr.sin_port = htons(1234);  //端口


    char bufSend[100] = {0};
    char bufRecv[100] = {0};

    while(1) {
        //创建套接字
        int sock = socket(AF_INET, SOCK_STREAM, 0);

        connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

        //获取用户输入的字符串并发送给服务器
        printf("please choose a city you want know!\n");
        printf("wuhan guangzhou shanghai shenzhen\n");
        printf("Input a city name: ");
        scanf("%s", bufSend);
        //gets(bufSend);

        write(sock, bufSend, sizeof(bufSend));

        //读取服务器传回的数据
        read(sock, bufRecv, sizeof(bufRecv)-1);
   
        printf("Message form server: %s\n", bufRecv);

        //关闭套接字
        close(sock);

    }
    
    return 0;
}

三、让服务器可以同时响应多个客户端请求

1. 单线程 / 进程

在 TCP 通信过程中,服务器端启动之后可以同时和多个客户端建立连接,并进行网络通信,但是在介绍 TCP 通信流程的时候,提供的服务器代码却不能完成这样的需求,先简单的看一下之前的服务器代码的处理思路,再来分析代码中的弊端:

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

int main()
{
    // 1. 创建监听的套接字
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    // 2. 将socket()返回值和本地的IP端口绑定到一起
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(10000);   // 大端端口
    // INADDR_ANY代表本机的所有IP, 假设有三个网卡就有三个IP地址
    // 这个宏可以代表任意一个IP地址
    addr.sin_addr.s_addr = INADDR_ANY;  // 这个宏的值为0 == 0.0.0.0
    int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
    // 3. 设置监听
    ret = listen(lfd, 128);
    // 4. 阻塞等待并接受客户端连接
    struct sockaddr_in cliaddr;
    int clilen = sizeof(cliaddr);
    int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &clilen);
    // 5. 和客户端通信
    while(1)
    {
        // 接收数据
        char buf[1024];
        memset(buf, 0, sizeof(buf));
        int len = read(cfd, buf, sizeof(buf));
        if(len > 0)
        {
            printf("客户端say: %s\n", buf);
            write(cfd, buf, len);
        }
        else if(len  == 0)
        {
            printf("客户端断开了连接...\n");
            break;
        }
        else
        {
            perror("read");
            break;
        }
    }
    close(cfd);
    close(lfd);
    return 0;
}

在上面的代码中用到了三个会引起程序阻塞的函数,分别是:

  • accept():如果服务器端没有新客户端连接,阻塞当前进程 / 线程,如果检测到新连接解除阻塞,建立连接
  • read():如果通信的套接字对应的读缓冲区没有数据,阻塞当前进程 / 线程,检测到数据解除阻塞,接收数据
  • write():如果通信的套接字写缓冲区被写满了,阻塞当前进程 / 线程(这种情况比较少见)

如果需要和发起新的连接请求的客户端建立连接,那么就必须在服务器端通过一个循环调用 accept() 函数,另外已经和服务器建立连接的客户端需要和服务器通信,发送数据时的阻塞可以忽略,当接收不到数据时程序也会被阻塞,这时候就会非常矛盾,被 accept() 阻塞就无法通信,被 read() 阻塞就无法和客户端建立新连接。因此得出一个结论,基于上述处理方式,在单线程 / 单进程场景下,服务器是无法处理多连接的,解决方案也有很多,常用的有四种:

  1. 使用多线程实现
  2. 使用多进程实现
  3. 使用 IO 多路转接(复用)实现
  4. 使用 IO 多路转接 + 多线程实现

2. 多进程并发

如果要编写多进程版的并发服务器程序,首先要考虑,创建出的多个进程都是什么角色,这样就可以在程序中对号入座了。在 Tcp 服务器端一共有两个角色,分别是:监听和通信,监听是一个持续的动作,如果有新连接就建立连接,如果没有新连接就阻塞。关于通信是需要和多个客户端同时进行的,因此需要多个进程,这样才能达到互不影响的效果。进程也有两大类:父进程和子进程,通过分析我们可以这样分配进程:

父进程:

  • 负责监听,处理客户端的连接请求,也就是在父进程中循环调用 accept() 函数
  • 创建子进程:建立一个新的连接,就创建一个新的子进程,让这个子进程和对应的客户端通信
  • 回收子进程资源:子进程退出回收其内核 PCB 资源,防止出现僵尸进程

子进程:

  • 负责通信,基于父进程建立新连接之后得到的文件描述符,和对应的客户端完成数据的接收和发送。
  • 发送数据:send() / write()
  • 接收数据:recv() / read()

在多进程版的服务器端程序中,多个进程是有血缘关系,对应有血缘关系的进程来说,还需要想明白他们有哪些资源是可以被继承的,哪些资源是独占的,以及一些其他细节:

  • 子进程是父进程的拷贝,在子进程的内核区 PCB 中,文件描述符也是可以被拷贝的,因此在父进程可以使用的文件描述符在子进程中也有一份,并且可以使用它们做和父进程一样的事情。
  • 父子进程有用各自的独立的虚拟地址空间,因此所有的资源都是独占的
  • 为了节省系统资源,对于只有在父进程才能用到的资源,可以在子进程中将其释放掉,父进程亦如此。
  • 由于需要在父进程中做 accept() 操作,并且要释放子进程资源,如果想要更高效一下可以使用信号的方式处理

图片

多进程版并发 TCP 服务器示例代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
#include <errno.h>

// 信号处理函数
void callback(int num)
{
    while(1)
    {
        pid_t pid = waitpid(-1, NULL, WNOHANG);
        if(pid <= 0)
        {
            printf("子进程正在运行, 或者子进程被回收完毕了\n");
            break;
        }
        printf("child die, pid = %d\n", pid);
    }
}

int childWork(int cfd);
int main()
{
    // 1. 创建监听的套接字
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if(lfd == -1)
    {
        perror("socket");
        exit(0);
    }

    // 2. 将socket()返回值和本地的IP端口绑定到一起
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(10000);   // 大端端口
    // INADDR_ANY代表本机的所有IP, 假设有三个网卡就有三个IP地址
    // 这个宏可以代表任意一个IP地址
    // 这个宏一般用于本地的绑定操作
    addr.sin_addr.s_addr = INADDR_ANY;  // 这个宏的值为0 == 0.0.0.0
    //    inet_pton(AF_INET, "192.168.237.131", &addr.sin_addr.s_addr);
    int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
    if(ret == -1)
    {
        perror("bind");
        exit(0);
    }

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

    // 注册信号的捕捉
    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = callback;
    sigemptyset(&act.sa_mask);
    sigaction(SIGCHLD, &act, NULL);

    // 接受多个客户端连接, 对需要循环调用 accept
    while(1)
    {
        // 4. 阻塞等待并接受客户端连接
        struct sockaddr_in cliaddr;
        int clilen = sizeof(cliaddr);
        int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &clilen);
        if(cfd == -1)
        {
            if(errno == EINTR)
            {
                // accept调用被信号中断了, 解除阻塞, 返回了-1
                // 重新调用一次accept
                continue;
            }
            perror("accept");
            exit(0);
 
        }
        // 打印客户端的地址信息
        char ip[24] = {0};
        printf("客户端的IP地址: %s, 端口: %d\n",
               inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, sizeof(ip)),
               ntohs(cliaddr.sin_port));
        // 新的连接已经建立了, 创建子进程, 让子进程和这个客户端通信
        pid_t pid = fork();
        if(pid == 0)
        {
            // 子进程 -> 和客户端通信
            // 通信的文件描述符cfd被拷贝到子进程中
            // 子进程不负责监听
            close(lfd);
            while(1)
            {
                int ret = childWork(cfd);
                if(ret <=0)
                {
                    break;
                }
            }
            // 退出子进程
            close(cfd);
            exit(0);
        }
        else if(pid > 0)
        {
            // 父进程不和客户端通信
            close(cfd);
        }
    }
    return 0;
}


// 5. 和客户端通信
int childWork(int cfd)
{

    // 接收数据
    char buf[1024];
    memset(buf, 0, sizeof(buf));
    int len = read(cfd, buf, sizeof(buf));
    if(len > 0)
    {
        printf("客户端say: %s\n", buf);
        write(cfd, buf, len);
    }
    else if(len  == 0)
    {
        printf("客户端断开了连接...\n");
    }
    else
    {
        perror("read");
    }

    return len;
}

在上面的示例代码中,父子进程中分别关掉了用不到的文件描述符(父进程不需要通信,子进程也不需要监听)。如果客户端主动断开连接,那么服务器端负责和客户端通信的子进程也就退出了,子进程退出之后会给父进程发送一个叫做 SIGCHLD 的信号,在父进程中通过 sigaction() 函数捕捉了该信号,通过回调函数 callback() 中的 waitpid() 对退出的子进程进行了资源回收。

另外还有一个细节要说明一下,这是父进程的处理代码:

int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &clilen);
while(1)
{
        int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &clilen);
        if(cfd == -1)
        {
            if(errno == EINTR)
            {
                // accept调用被信号中断了, 解除阻塞, 返回了-1
                // 重新调用一次accept
                continue;
            }
            perror("accept");
            exit(0);
 
        }
 }

如果父进程调用 accept() 函数没有检测到新的客户端连接,父进程就阻塞在这儿了,这时候有子进程退出了,发送信号给父进程,父进程就捕捉到了这个信号 SIGCHLD, 由于信号的优先级很高,会打断代码正常的执行流程,因此父进程的阻塞被中断,转而去处理这个信号对应的函数 callback(),处理完毕,再次回到 accept() 位置,但是这是已经无法阻塞了,函数直接返回 - 1,此时函数调用失败,错误描述为 accept: Interrupted system call,对应的错误号为 EINTR,由于代码是被信号中断导致的错误,所以可以在程序中对这个错误号进行判断,让父进程重新调用 accept(),继续阻塞或者接受客户端的新连接。

3. 多线程并发

编写多线程版的并发服务器程序和多进程思路差不多,考虑明白了对号入座即可。多线程中的线程有两大类:主线程(父线程)和子线程,他们分别要在服务器端处理监听和通信流程。根据多进程的处理思路,就可以这样设计了:

主线程:

  • 负责监听,处理客户端的连接请求,也就是在父进程中循环调用 accept() 函数
  • 创建子线程:建立一个新的连接,就创建一个新的子进程,让这个子进程和对应的客户端通信
  • 回收子线程资源:由于回收需要调用阻塞函数,这样就会影响 accept(),直接做线程分离即可。

子线程:

  • 负责通信,基于主线程建立新连接之后得到的文件描述符,和对应的客户端完成数据的接收和发送。
  • 发送数据:send() / write()
  • 接收数据:recv() / read()

在多线程版的服务器端程序中,多个线程共用同一个地址空间,有些数据是共享的,有些数据的独占的,下面来分析一些其中的一些细节:

  • 同一地址空间中的多个线程的栈空间是独占的
  • 多个线程共享全局数据区,堆区,以及内核区的文件描述符等资源,因此需要注意数据覆盖问题,并且在多个线程访问共享资源的时候,还需要进行线程同步。

图片

多线程版 Tcp 服务器示例代码如下:

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

struct SockInfo
{
    int fd;                      // 通信
    pthread_t tid;               // 线程ID
    struct sockaddr_in addr;     // 地址信息
};

struct SockInfo infos[128];

void* working(void* arg)
{
    while(1)
    {
        struct SockInfo* info = (struct SockInfo*)arg;
        // 接收数据
        char buf[1024];
        int ret = read(info->fd, buf, sizeof(buf));
        if(ret == 0)
        {
            printf("客户端已经关闭连接...\n");
            info->fd = -1;
            break;
        }
        else if(ret == -1)
        {
            printf("接收数据失败...\n");
            info->fd = -1;
            break;
        }
        else
        {
            write(info->fd, buf, strlen(buf)+1);
        }
    }
    return NULL;
}

int main()
{
    // 1. 创建用于监听的套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd == -1)
    {
        perror("socket");
        exit(0);
    }

    // 2. 绑定
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;          // ipv4
    addr.sin_port = htons(8989);        // 字节序应该是网络字节序
    addr.sin_addr.s_addr =  INADDR_ANY; // == 0, 获取IP的操作交给了内核
    int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
    if(ret == -1)
    {
        perror("bind");
        exit(0);
    }

    // 3.设置监听
    ret = listen(fd, 100);
    if(ret == -1)
    {
        perror("listen");
        exit(0);
    }

    // 4. 等待, 接受连接请求
    int len = sizeof(struct sockaddr);

    // 数据初始化
    int max = sizeof(infos) / sizeof(infos[0]);
    for(int i=0; i<max; ++i)
    {
        bzero(&infos[i], sizeof(infos[i]));
        infos[i].fd = -1;
        infos[i].tid = -1;
    }

    // 父进程监听, 子进程通信
    while(1)
    {
        // 创建子线程
        struct SockInfo* pinfo;
        for(int i=0; i<max; ++i)
        {
            if(infos[i].fd == -1)
            {
                pinfo = &infos[i];
                break;
            }
            if(i == max-1)
            {
                sleep(1);
                i--;
            }
        }

        int connfd = accept(fd, (struct sockaddr*)&pinfo->addr, &len);
        printf("parent thread, connfd: %d\n", connfd);
        if(connfd == -1)
        {
            perror("accept");
            exit(0);
        }
        pinfo->fd = connfd;
        pthread_create(&pinfo->tid, NULL, working, pinfo);
        pthread_detach(pinfo->tid);
    }

    // 释放资源
    close(fd);  // 监听

    return 0;
}

在编写多线程版并发服务器代码的时候,需要注意父子线程共用同一个地址空间中的文件描述符,因此每当在主线程中建立一个新的连接,都需要将得到文件描述符值保存起来,不能在同一变量上进行覆盖,这样做丢失了之前的文件描述符值也就不知道怎么和客户端通信了。

在上面示例代码中是将成功建立连接之后得到的用于通信的文件描述符值保存到了一个全局数组中,每个子线程需要和不同的客户端通信,需要的文件描述符值也就不一样,只要保证存储每个有效文件描述符值的变量对应不同的内存地址,在使用的时候就不会发生数据覆盖的现象,造成通信数据的混乱了。

四、技术准备

通过上面的了解我们需要对多线程编程和字符串处理需要有更加深入的理解。

多线程编程相关博客

有关C/C++字符串处理的博客应该近期会出来!


ref:https://mp.weixin.qq.com/s/EN3UiTZbvuWEHPD8fxiVgQ

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【C/C++服务器开发】具备丰富功能和可以同时响应多个客服端的服务器 的相关文章

  • PHP + Apache + Mysql集成环境部署及简要教程

    文章目录 PHP运行原理和机制PHP 的设计理念及特点PHP 的四层体系1 Zend 引擎 xff08 核心 xff09 2 Extensions xff08 扩展 xff09 3 SAPI xff08 服务器应用程序编程接口 xff09
  • 不同行业公司工资对比,计算机YYDS

    一 纳税标准 推荐一篇文章 xff1a 扣除社保 公积金年后 社保和公积金的扣除比例是22 左右 xff0c 工资在扣完社保和公积金的基础上再进行个税的扣除 税前19 2w xff0c 税前平均每月1 6w xff0c 扣除社保 公积金后年
  • 对实时操作系统多任务的一些理解

    一 什么是优先级反转 优先级反转 xff0c 是指在使用信号量时 xff0c 可能会出现的这样一种不合理的现象 xff0c 即 xff1a 优先级反转是指一个低优先级的任务持有一个被高优先级任务所需要的共享资源 高优先任务由于因资源缺乏而处
  • 【LeetCode刷题日记】数组和链表性质总结

    一 数据结构的存储方式 数据结构的存储方式只有两种 xff1a 数组 xff08 顺序存储 xff09 和链表 xff08 链式存储 xff09 这句话怎么理解 xff0c 不是还有散列表 栈 队列 堆 树 图等等各种数据结构吗 xff1f
  • C语言面向对象实现滑动均值滤波与平均值滤波

    文章目录 一 背景二 平均值滤波1 算法介绍2 代码实现3 实例 三 滑动均值滤波 xff08 Moving Average xff09 四 C语言面向面向对象实现滑动均值滤波 一 背景 在实际的数据采集中 xff0c 我们经常会取多次数据
  • 【LeetCode刷题日记】常用算法基础和理解及运用

    在我们LeetCode刷题过程中 xff0c 如果我们只是了解数据结构 xff08 数组 xff0c 链表 xff0c 数 xff09 的使用方法 xff0c 那我们在面对复杂的题目时 xff0c 是很难很好的解决问题的 xff0c 因此我
  • 【LeetCode刷题日记】数组类题目常见题型

    文章目录 303 区域和检索 数组不可变 https leetcode cn com problems range sum query immutable 304 二维区域和检索 矩阵不可变 https leetcode cn com pr
  • 【LeetCode刷题日记】队列类题目常见题型

    文章目录 225 用队列实现栈 https leetcode cn com problems implement stack using queues 剑指 Offer 09 用两个栈实现队列 https leetcode cn com p
  • 【LeetCode刷题日记】栈类题目常见题型

    文章目录 20 有效的括号 https leetcode cn com problems valid parentheses 225 用队列实现栈 https leetcode cn com problems implement stack
  • 回顾 nexus maven-snapshots 401 Unauthorized

    1 修改maven settings 文件 私库的用户名和密码 lt server gt lt id gt maven releases lt id gt lt username gt admin lt username gt lt pas
  • 【LeetCode刷题日记】树类题目常见题型

    文章目录 树基础知识 104 二叉树的最大深度 https leetcode cn com problems maximum depth of binary tree 102 二叉树的层序遍历 https leetcode cn com p
  • 【LeetCode刷题日记】常用算法基础和理解及运用

    文章目录 递归和迭代递归迭代迭代和递归的关系和区别 xff08 敲黑板 xff09 二分法典型的二分法二分法的变种找出第一个与key相等的元素的位置找出最后一个与key相等的元素的位置查找第一个等于或者大于Key的元素的位置查找第一个大于k
  • 编程常用快捷键,学完保证和大神一样件字如飞

    每次看到一下大神在编程时 xff0c 那打字的速度简直是恐怖 xff0c 而且几乎不会用到鼠标 xff0c 手不用离开键盘 xff0c 因此打字效率也高 而菜鸡的我 xff0c 每次写代码时 xff0c 有时候要在不同行之间跳动 xff0c
  • Clion中编译C/C++混合代码

    今天在Clion中编译C C 43 43 语言时 xff0c 出现了一个不容易发现的错误 项目文件包括三个文件 三个文件其实都是用C语言写的 xff0c 只是第一个文件的后缀是 cpp 如果在这种情况下直接编译 xff0c 会出现一下报错
  • 【LeetCode刷题日记】[447. 回旋镖的数量](https://leetcode-cn.com/problems/number-of-boomerangs/)

    LeetCode刷题日记 447 回旋镖的数量 题目 给定平面上 n 对 互不相同 的点 points xff0c 其中 points i 61 xi yi 回旋镖 是由点 i j k 表示的元组 xff0c 其中 i 和 j 之间的距离和
  • 【LeetCode刷题日记】[162. 寻找峰值]

    LeetCode刷题日记 162 寻找峰值 题目 峰值元素是指其值严格大于左右相邻值的元素 给你一个整数数组 nums xff0c 找到峰值元素并返回其索引 数组可能包含多个峰值 xff0c 在这种情况下 xff0c 返回 任何一个峰值 所
  • 基于Junit4+Mockito+PowerMock实现Controller+Service的单元测试

    一 导入的依赖 lt test gt lt dependency gt lt groupId gt org mockito lt groupId gt lt artifactId gt mockito core lt artifactId
  • 嵌入式编程之状态机

    文章目录 1 什么是状态机 xff1f 2 状态机编程的优点 1 提高CPU使用效率 2 逻辑完备性 3 程序结构清晰 3 状态机的三种实现方法switch case 法表格驱动法函数指针法小节 摘要 xff1a 不知道大家有没有这样一种感
  • VxWorks几种常用的延时方法

    转载于 xff1a https www vxworks net app 144 time facilities of vxworks 自用 VxWorks系统提供灵活多样的定时器机制 xff0c 有多种接口可以方便地实现延时 xff0c 文
  • 推荐一些嵌入式、C/C++的开源库和项目

    前言 想用代码实现一个功能时 xff0c 我们可以先看看是否有前辈已经实现了该功能 xff0c 并且开源分享在网上 一般我们自己造出来的轮子的可用性基本上是很难和大神们造的轮子相比的 因此多了解一下优秀的开源库的使用 xff0c 可以让我们

随机推荐