C++ 网络编程

news/2025/2/15 17:55:09

1. socket

        Socket 是一种用于网络通信的编程接口,它提供了一种类似于文件描述符的接口,允许不同计算机之间的进程进行通信。Socket 可以工作在多种协议上,最常用的是 TCP/IP 和 UDP/IP 协议

1.1 UDP

1.1.1 概念

        UDP(用户数据报协议)是一种无连接的网络协议,它允许数据在不建立可靠连接的情况下快速传输。UDP 不保证数据的可靠性、顺序性或完整性,但它的速度比 TCP 快,适合对实时性要求较高的应用,例如视频流、语音通信、游戏等。

1.1.2 UDP 的特点

  1. 无连接:UDP 不需要建立连接,发送方可以直接发送数据报,接收方接收数据报。

  2. 不可靠:UDP 不保证数据报的传输顺序、完整性或可靠性。数据可能会丢失、重复或乱序到达。

  3. 轻量级:UDP 的头部只有8字节,比 TCP 的20-60字节的头部更轻量。

  4. 速度较快:由于没有连接建立和确认机制,UDP 的传输速度比 TCP 快。

1.1.3 UDP 数据报组成:

  1. UDP 头部(8字节):

    • 源端口(16位):发送方的端口号。

    • 目的端口(16位):接收方的端口号。

    • 长度(16位):UDP 数据报的总长度(包括头部和数据部分)。

    • 校验和(16位):用于检测数据报在传输过程中是否出错。

  2. 数据部分:用户数据。

1.1.4 示例代码

服务器代码:UDP 服务器会监听特定端口,接收来自客户端的消息并打印出来

#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

int main() {
    int sockfd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    char buffer[1024];

    // 创建 UDP 套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        std::cerr << "Error creating socket" << std::endl;
        return 1;
    }

    // 配置服务器地址
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080); // 监听端口
    server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有网络接口

    // 绑定套接字到地址
    if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        std::cerr << "Error binding socket" << std::endl;
        close(sockfd);
        return 1;
    }

    std::cout << "UDP server is running and waiting for messages..." << std::endl;

    while (true) {
        // 接收客户端消息
        int bytes_received = recvfrom(sockfd, buffer, sizeof(buffer), 0,
                                      (struct sockaddr*)&client_addr, &client_addr_len);
        if (bytes_received < 0) {
            std::cerr << "Error receiving message" << std::endl;
            continue;
        }

        buffer[bytes_received] = '\0'; // 确保字符串以 null 结尾
        std::cout << "Received message from client: " << buffer << std::endl;

        // 可选:向客户端发送响应
        const char* response = "Message received";
        sendto(sockfd, response, strlen(response), 0,
               (struct sockaddr*)&client_addr, client_addr_len);
    }

    close(sockfd);
    return 0;
}

客户端代码:UDP 客户端会向服务器发送消息,并接收服务器的响应。

#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

int main() {
    int sockfd;
    struct sockaddr_in server_addr;
    char buffer[1024];

    // 创建 UDP 套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        std::cerr << "Error creating socket" << std::endl;
        return 1;
    }

    // 配置服务器地址
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080); // 服务器端口
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 服务器 IP 地址

    std::cout << "Enter a message to send to the server: ";
    std::cin.getline(buffer, sizeof(buffer));

    // 发送消息到服务器
    if (sendto(sockfd, buffer, strlen(buffer), 0,
               (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        std::cerr << "Error sending message" << std::endl;
        close(sockfd);
        return 1;
    }

    std::cout << "Waiting for server response..." << std::endl;

    // 接收服务器响应
    int bytes_received = recvfrom(sockfd, buffer, sizeof(buffer), 0,
                                  NULL, NULL);
    if (bytes_received < 0) {
        std::cerr << "Error receiving response" << std::endl;
        close(sockfd);
        return 1;
    }

    buffer[bytes_received] = '\0'; // 确保字符串以 null 结尾
    std::cout << "Server response: " << buffer << std::endl;

    close(sockfd);
    return 0;
}

基本逻辑

1.2 TCP 

1.2.1 概念 

        TCP(传输控制协议)是一种面向连接的、可靠的网络协议,用于在不可靠的网络中提供可靠的字节流服务。TCP 通过建立连接、确认机制、重传机制等技术,确保数据的完整性和顺序性,适用于对数据可靠性要求较高的应用,如文件传输、网页浏览、邮件服务等。

1.2.2 TCP 的特点

  1. 面向连接:TCP 在数据传输之前需要建立连接,确保通信双方的准备就绪。

  2. 可靠传输:TCP 提供可靠的数据传输服务,通过确认机制、重传机制和滑动窗口协议来确保数据的完整性和顺序性。

  3. 字节流:TCP 将数据视为字节流,不保留数据的边界信息。

  4. 流量控制:TCP 使用滑动窗口协议来控制发送方的发送速率,防止接收方被淹没。

  5. 拥塞控制:TCP 通过拥塞控制算法(如慢启动、拥塞避免、快速重传等)来避免网络拥塞。

1.2.3 示例代码

服务端

#include <string.h>
#include <stdio.h>
#include <sys/socket.h>
#include <errno.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <unistd.h>

int main() {
	int tcp_socket;
	int ret;
	// 1. 申请一个TCP的socket
	tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
	if (tcp_socket == -1) {
		perror("socket");
		return -1;
	}
	int opt = 1;
	ret = setsockopt(tcp_socket, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
	if (ret < 0) {
		perror("set sock opt");
		return -1;
	}

	// 2. 作为服务器,应该在某一个端口上进行监测
	struct sockaddr_in self;
	memset(&self, 0, sizeof(self));
	// 2.1 填充socket tcp/ip协议的地址
	self.sin_family = AF_INET;
	self.sin_port = htons(7788);
	self.sin_addr.s_addr = htonl(INADDR_ANY);        // 服务器是可以在任何机器上运行,监测当前系统的所有IP
	ret = bind(tcp_socket, (const struct sockaddr *) &self, sizeof(self));
	if (ret == -1) {
		perror("bind");
		return -1;
	}

	// 3. 将主动发送的socket变为被动监听的socket类型
	listen(tcp_socket, 5);
	printf("listen...\n");

	// 4. 等待新的客户端发来消息
	int new_fd;
	char buf[1024];
	ssize_t t;
	while (1) {
		// 4.1 使用accept等待listen fd是否有成功的描述
		new_fd = accept(tcp_socket, NULL, NULL);
		if (new_fd < 0) {
			perror("accept");
			break;
		}
		printf("New connection success!\n");
		// 5. 服务器等待新的客户端发来消息(请求),服务器根据请求来对客户端进行响应
		while (1) {		// 客户端发来消息,直到客户端关闭,服务器才关闭
			memset(buf, 0, sizeof(buf));
			// 5.1 接收请求
			t = recv(new_fd, buf, sizeof(buf), 0);
			if (t <= 0) {
				if (errno == 0) {
					printf("remote client closed!\n");
					break;
				}
				perror("recv");
				break;
			}
			buf[t] = 0;
			printf("receive : %s\n", buf);
			// 5.2 发送响应
			send(new_fd, "hello", 5, 0);

		}
		close(new_fd);
	}
	close(tcp_socket);
	return 0;
}

客户端

#include <string.h>
#include <stdio.h>
#include <sys/socket.h>
#include <errno.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>

int main(int argc, char *argv[]) {
	int tcp_socket;
	struct sockaddr_in dest;
	uint16_t port;
	socklen_t dest_len = sizeof(dest);
	int ret;

	if (argc < 3) {
		fprintf(stderr, "Usage: tcp_client ip port");
		exit(-1);
	}

	port = atoi(argv[2]);

	// 1. 申请一个TCP的socket
	tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
	if (tcp_socket == -1) {
		perror("socket");
		return -1;
	}

	memset(&dest, 0, sizeof(dest));
	dest.sin_family = AF_INET;
	dest.sin_port = htons(port);
	dest.sin_addr.s_addr = inet_addr(argv[1]);		// inet_aton(argv[1], &dest.sin_addr);

	// 2. 第一次握手
	ret = connect(tcp_socket, (const struct sockaddr *)&dest, dest_len);
	if (ret < 0) {
		perror("connect");
		exit(-1);
	}
	printf("connect success!\n");

	// 3. 发起请求
	char buf[1024];
	memset(buf, 0, sizeof(buf));
	printf("<input>:");
	while (fgets(buf, sizeof(buf), stdin) != NULL) {
		buf[strlen(buf) - 1] = 0;
		if (strncmp(buf, "quit", 4) == 0) {
			break;
		}
		ret = send(tcp_socket, buf, strlen(buf), 0);
		if (ret == -1) {
			perror("send to");
			break;
		}
		printf("send %ld bytes success!\n", ret);
		ret = recv(tcp_socket, buf, sizeof(buf), 0);
		buf[ret] = 0;
		printf("server: %s\n", buf);
		printf("<input>:");
	}
	close(tcp_socket);
	return 0;
}

一个进程默认能打开1024个文件描述符

在C++中,多路I/O复用是一种高效的I/O处理方式,允许程序同时监视多个文件描述符(如网络连接或文件),并等待它们中的任何一个变为可读或可写。以下是几种常见的多路I/O复用技术及其在C++中的实现方式;

2. 多路IO复用

2.1 select

2.1.1 概念

select 是一种多路I/O复用技术,用于同时监视多个文件描述符(如套接字、文件等),以确定它们是否准备好进行读、写或异常操作。

select 使用位域来表示文件描述符集合,通过位操作来快速检查和更新文件描述符的状态。内核只维护这个位域结构。

 工作流程
select 的工作流程可以分为以下几个步骤:
初始化文件描述符集合:将需要监视的文件描述符添加到 fd_set 中。
调用 select:将文件描述符集合传递给 select 函数,同时指定最大文件描述符值(fdmax)和超时时间。
等待 I/O 事件:select 函数会阻塞,直到:
至少有一个文件描述符准备好进行 I/O 操作。
超时时间到达。
检查结果:select 返回后,程序可以检查哪些文件描述符已经准备好,然后进行相应的读写操作。

1.2 函数原型及参数说明

#include <sys/select.h>

int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);

nfds
表示要监视的最大文件描述符加1。例如,如果你的文件描述符范围是0到10,nfds 应该是11。
这个参数用于指定 select 需要检查的最大文件描述符范围。
readfds
指向一个 fd_set 类型的指针,表示需要监视的可读文件描述符集合。
如果不需要监视可读文件描述符,可以传入 NULL。
writefds
指向一个 fd_set 类型的指针,表示需要监视的可写文件描述符集合。
如果不需要监视可写文件描述符,可以传入 NULL。
exceptfds
指向一个 fd_set 类型的指针,表示需要监视的异常条件文件描述符集合。
如果不需要监视异常条件,可以传入 NULL。
timeout
指向一个 struct timeval 类型的指针,表示 select 的超时时间。
如果传入 NULL,select 将会阻塞,直到有文件描述符准备好。
如果传入一个时间值,select 将在指定时间内未检测到任何准备好状态时返回。

1.3 示例代码

#include <iostream>
#include <cstring>
#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

int main() {
    int server_fd, client_fd;
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    fd_set readfds;
    struct timeval tv;

    // 创建 Socket
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 绑定地址和端口
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(8080);

    if (bind(server_fd, (struct sockaddr*)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    // 监听连接
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    std::cout << "Server is running on port 8080..." << std::endl;

    // 初始化文件描述符集合
    FD_ZERO(&readfds);
    FD_SET(server_fd, &readfds);

    // 设置超时时间
    tv.tv_sec = 5;  // 5 秒超时
    tv.tv_usec = 0;

    int max_fd = server_fd;

    while (true) {
        fd_set tempfds = readfds; // 复制文件描述符集合
        int activity = select(max_fd + 1, &tempfds, NULL, NULL, &tv);

        if ((activity < 0) && (errno != EINTR)) {
            std::cout << "Select error";
        }

        // 判断服务器的监听套接字是否有新的连接请求。
        if (FD_ISSET(server_fd, &tempfds)) {
            if ((client_fd = accept(server_fd, (struct sockaddr*)&address, (socklen_t*)&addrlen)) < 0) {
                perror("accept failed");
                exit(EXIT_FAILURE);
            }

            std::cout << "Client connected." << std::endl;
            FD_SET(client_fd, &readfds); // 将客户端文件描述符加入集合
            if (client_fd > max_fd) {
                max_fd = client_fd; // 更新最大文件描述符
            }
        }

        // 检查客户端文件描述符是否准备好
        for (int i = 0; i <= max_fd; i++) {
            if (FD_ISSET(i, &tempfds)) {
                if (i == server_fd) {
                    continue; // 跳过服务器端口
                }

                char buffer[1024] = {0};
                int valread = recv(i, buffer, 1024, 0);

                if (valread <= 0) {
                    std::cout << "Client disconnected." << std::endl;
                    close(i); // 关闭客户端连接
                    FD_CLR(i, &readfds); // 从集合中移除
                } else {
                    std::cout << "Message from client: " << buffer << std::endl;
                    send(i, buffer, strlen(buffer), 0); // 回显消息
                }
            }
        }
    }

    close(server_fd);
    return 0;
}

2.2 epoll(只支持linux)

2.2.1 概述

epollepoll 是 Linux 内核提供的一种高效的 I/O 多路复用机制。epoll 的核心在于它将文件描述符的管理与事件通知分离,通过内核维护一个事件表来高效地处理 I/O 事件。适用于需要长时间保持连接的场景,如 Web 服务器。

epoll 的工作原理基于以下三个核心系统调用:
epoll_create:
创建一个 epoll 实例,返回一个特殊的文件描述符(epfd),用于后续操作。
内部使用红黑树存储所有监听的文件描述符(FD),并维护一个就绪链表用于存储活跃事件。
epoll_ctl:
用于管理红黑树中的文件描述符,支持添加(EPOLL_CTL_ADD)、修改(EPOLL_CTL_MOD)和删除(EPOLL_CTL_DEL)操作。
每个文件描述符与一个回调函数绑定,当事件发生时,内核会将事件插入到就绪链表中。
epoll_wait:
等待就绪链表中的事件发生。如果有事件,直接返回给用户态,时间复杂度为 O(1);如果没有事件,根据超时参数阻塞或立即返回

 Epoll 的高效数据结构
红黑树(RB-Tree):
存储所有监听的文件描述符,支持高效的插入、删除和查找操作(时间复杂度为 O(log N)),适用于海量连接。
就绪链表(Ready List):
存储活跃事件,内核通过回调函数将事件加入链表,epoll_wait 只需遍历此链表即可,避免了全量扫描。

2.2.2 优势

相比 selectpollepoll 的优势在于:

  • 更高的效率epoll 不需要每次调用时都遍历所有文件描述符,而是只处理就绪的文件描述符。

  • 无文件描述符数量限制epoll 不受 FD_SETSIZE 的限制,可以处理大量并发连接。

  • 事件驱动epoll 是被动触发的,只有当文件描述符状态发生变化时,内核才会通知用户空间

2.2.3 工作模式

2.2.4 epoll 与 select/poll 的比较

特性selectpollepoll
文件描述符限制有限(默认 1024)无限制无限制
效率随文件描述符数量增加而降低随文件描述符数量增加而降低高效,不受文件描述符数量影响
事件通知轮询轮询事件驱动
跨平台性跨平台跨平台Linux 特有
应用场景小规模并发、实时性要求高中等规模并发高并发、长连接
 Epoll 的性能优势
无需遍历全部 FD:epoll_wait 只处理就绪链表中的事件,时间复杂度为 O(1),
                而 select 和 poll 的时间复杂度为 O(n)。
减少内存拷贝:通过 mmap 共享内核与用户空间内存,避免了 select 和 poll 的多次数据拷贝。
支持海量连接:红黑树结构使 epoll 可管理数十万级文件描述符,适用于现代高并发服务器。

2.2.5 示例代码

#include <iostream>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

int main() {
    int server_fd, epoll_fd;
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    struct epoll_event event, events[10];

    // 创建 TCP 套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket failed");
        return -1;
    }

    // 绑定地址和端口
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(8080);

    if (bind(server_fd, (struct sockaddr*)&address, sizeof(address)) < 0) {
        perror("bind failed");
        return -1;
    }

    // 监听连接
    if (listen(server_fd, 3) < 0) {
        perror("listen failed");
        return -1;
    }

    // 创建 epoll 实例
    epoll_fd = epoll_create(1);
    if (epoll_fd < 0) {
        perror("epoll_create failed");
        return -1;
    }

    // 将监听套接字加入 epoll
    event.data.fd = server_fd;
    event.events = EPOLLIN | EPOLLET; // 边缘触发
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) < 0) {
        perror("epoll_ctl failed");
        return -1;
    }

    std::cout << "Server is running on port 8080..." << std::endl;

    while (true) {
        int nfds = epoll_wait(epoll_fd, events, 10, -1);
        for (int n = 0; n < nfds; ++n) {
            if (events[n].data.fd == server_fd) {
                // 接受新连接
                int client_fd = accept(server_fd, (struct sockaddr*)&address, (socklen_t*)&addrlen);
                if (client_fd < 0) {
                    perror("accept failed");
                    continue;
                }

                // 将客户端套接字加入 epoll
                event.data.fd = client_fd;
                event.events = EPOLLIN | EPOLLET; // 边缘触发
                epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event);
            } else {
                // 处理客户端数据
                char buffer[1024] = {0};
                int valread = read(events[n].data.fd, buffer, sizeof(buffer));
                if (valread <= 0) {
                    close(events[n].data.fd);
                } else {
                    std::cout << "Message from client: " << buffer << std::endl;
                    write(events[n].data.fd, buffer, valread);
                }
            }
        }
    }

    close(server_fd);
    close(epoll_fd);
    return 0;
}

3. 长连接与短连接

网络编程中,长连接短连接是两种常见的连接模式,它们的区别主要在于连接的持续时间和使用场景。

3.1 长连接

        长连接是指客户端和服务器之间建立的连接在较长时间内保持打开状态,而不是在一次数据传输后立即关闭。这种连接模式适用于需要频繁交互的场景。

3.1.1 特点
  1. 持续时间长:连接在较长时间内保持打开状态,避免了频繁建立和关闭连接的开销。

  2. 高效:减少了连接建立和关闭的开销,适合高频率的数据交互。

  3. 资源占用:由于连接保持打开状态,会占用一定的系统资源(如文件描述符、内存等)。

  4. 适用场景:适用于需要实时交互的应用,如聊天应用、游戏服务器、WebSockets 等。

3.1.2 示例
  • WebSockets:用于实时通信,客户端和服务器之间保持一个持久的连接,数据可以随时发送。

  • 游戏服务器:客户端和服务器之间需要频繁交互,长连接可以减少延迟。

  • 即时通讯:如微信、QQ 等,客户端和服务器之间保持长连接,以便实时接收消息。

3.2 短连接

短连接是指客户端和服务器之间的连接在一次数据传输后立即关闭。这种连接模式适用于偶尔交互的场景。

3.2.1 特点

  1. 连接时间短:每次交互后立即关闭连接,避免了长时间占用资源。

  2. 资源占用少:由于连接时间短,系统资源占用较少。

  3. 开销较大:每次交互都需要建立和关闭连接,增加了连接的开销。

  4. 适用场景:适用于偶尔交互的应用,如传统的 HTTP 请求。

3.2.2 示例

  • HTTP/1.0:每次请求后关闭连接,适合简单的网页浏览。

  • 文件下载:客户端请求文件后,下载完成后关闭连接。


http://www.niftyadmin.cn/n/5852409.html

相关文章

2.SpringSecurity在mvc项目中的使用

SpringSecurity认证过程 参考 来源于黑马程序员&#xff1a; 手把手教你精通新版SpringSecurity 设置用户状态 用户认证业务里&#xff0c;我们封装User对象时&#xff0c;选择了三个构造参数的构造方法&#xff0c;其实还有另一个构造方法&#xff1a; public User(Strin…

AWTK-WEB 快速入门(4) - JS Http 应用程序

XMLHttpRequest 改变了 Web 应用程序与服务器交换数据的方式&#xff0c;fetch 是 XMLHttpRequest 继任者&#xff0c;具有更简洁的语法和更好的 Promise 集成。本文介绍一下如何使用 JS 语言开发 AWTK-WEB 应用程序&#xff0c;并用 fetch 访问远程数据。 用 AWTK Designer 新…

PMP冲刺每日一题(8)

试题1 您已经被委派为某项目的项目经理&#xff0c;权职范围明确界定&#xff0c;限于产品总装线的设计及建设阶段。客户组的一位成员向项目部门主管要求在项目安装阶段完成一项小工作。项目部门主管请客户询问项目经理。对这一请求的答复应包括∶ A、经修订的资源计划 B、经项…

Jenkins | Jenkins安装

Jenkins安装 一、前置准备二、启动三、登录 一、前置准备 下载安装包 war包 下载地址: https://www.jenkins.io/ 安装jdk 要求jdk11版本以上 集成maven项目的话 需要有maven 与 git 二、启动 启动命令 需要注意使用jdk11以上的版本 /usr/java/jdk17/bin/java -Xms2048m -X…

Vue笔记(九)

一、文章分类架子--PageContainer 学习PageContainer组件的封装&#xff0c;这一组件用于搭建页面基础结构&#xff0c;为后续内容展示提供统一布局。它可能包含通用的页面样式、导航栏、侧边栏等基础元素的结构搭建。 在Vue组件中&#xff0c; <template> 标签内定义基础…

小爱音箱控制手机和电视听歌的尝试

最近买了小爱音箱pro&#xff0c;老婆让我扔了&#xff0c;吃灰多年的旧音箱。当然舍不得&#xff0c;比小爱还贵&#xff0c;刚好还有一台红米手机&#xff0c;能插音箱&#xff0c;为了让音箱更加灵活&#xff0c;买了个2元的蓝牙接收模块Type-c供电3.5接口。这就是本次尝试起…

2025-2-14算法打卡

一&#xff0c;右旋字符串 1.题目描述&#xff1a; 字符串的右旋转操作是把字符串尾部的若干个字符转移到字符串的前面。给定一个字符串 s 和一个正整数 k&#xff0c;请编写一个函数&#xff0c;将字符串中的后面 k 个字符移到字符串的前面&#xff0c;实现字符串的右旋转操…

repo学习使用

Repo 是以 Git 为基础构建的代码库管理工具。Repo 可以在必要时整合多个 Git 代码库&#xff0c;将相关内容上传到版本控制系统。借助单个 Repo 命令&#xff0c;可以将文件从多个代码库下载到本地工作目录。 Repo 命令是一段可执行的 Python 脚本&#xff0c;你可以将其放在路…