【KCP】UDP可靠性传输

news2024/11/19 23:13:44

1 如何做到可靠性传输

◼ ACK机制
◼ 重传机制
◼ 序号机制 3 2 1 -》2 3 1
◼ 重排机制 2 3 1 ->3 2 1
◼ 窗口机制
Tcp不用我们管
可靠性udp 5种机制都需要用户层处理

2 UDP与TCP,我们如何选择

image.png

3 UDP如何可靠,KCP协议在哪些方面有优势

以10%-20%带宽浪费的代价换取了比 TCP快30%-40%的传输速度。

RTO翻倍vs不翻倍
TCP超时计算是RTOx2,这样连续丢三次包就变成RTOx8了,十分恐怖,而 KCP启动快速模式后不x2,只是x1.5(实验证明1.5这个值相对比较好), 提高了传输速度。 200 300 450 675 – 200 400 800 1600

选择性重传 vs 全部重传
TCP丢包时会全部重传从丢的那个包开始以后的数据,KCP是选择性重传, 只重传真正丢失的数据包。
快速重传(跳过多少个包马上重传)(如果使用了快速重传,可以不考虑 RTO)): 发送端发送了1,2,3,4,5几个包,然后收到远端的ACK: 1, 3, 4, 5,当收到ACK3时,KCP知道2被跳过1次,收到ACK4时,知道2被跳过了2次,此时可以认为2号丢失,不用等超时,直接重传2号包,大大改善了丢包时的传输速度。 fastresend =2

延迟ACK vs 非延迟ACK
TCP为了充分利用带宽,延迟发送ACK(NODELAY都没用),这样超时计算会算出较大 RTT时间,延长了丢包时的判断过程。KCP的ACK是否延迟发送可以调节。

UNA vs ACK+UNA
ARQ模型响应有两种,UNA(此编号前所有包已收到,如TCP)和ACK(该编号包已收到),光用UNA将导致全部重传,光用ACK则丢失成本太高,以往协议都是二选其一,而 KCP协议中,除去单独的 ACK包外,所有包都有UNA信息。

非退让流控
KCP正常模式同TCP一样使用公平退让法则,即发送窗口大小由:发送缓存大小、接收端剩余接收缓存大小、丢包退让及慢启动这四要素决定。但传送及时性要求很高的小数据时,可选择通过配置跳过后两步,仅用前两项来控制发送频率。
以牺牲部分公平性及带宽利用率之代价,换取了开着BT都能流畅传输的效果。

4 KCP精讲

4.1 kcp名词说明

kcp官方:https://github.com/skywind3000/kcp

名词说明

  • 用户数据:应用层发送的数据,如一张图片2Kb的数据
  • MTU:最大传输单元。即每次发送的最大数据
  • RTO:Retransmission TimeOut,重传超时时间。
  • cwnd:congestion window,拥塞窗口,表示发送方可发送多少个KCP数据包。与接收方窗口有关,与网络状况(拥塞控制)有关,与发送窗口大小有关。
  • rwnd:receiver window,接收方窗口大小,表示接收方还可接收多少个KCP数据包
  • snd_queue:待发送KCP数据包队列
  • snd_nxt:下一个即将发送的kcp数据包序列号
  • snd_una:下一个待确认的序列号

4.2 kcp使用方式

  1. 创建 KCP对象:ikcpcb *kcp = ikcp_create(conv, user);
  2. 设置传输回调函数(如UDP的send函数):kcp->output = udp_output;

真正发送数据需要调用sendto

  1. 循环调用 update:ikcp_update(kcp, millisec);
  2. 输入一个应用层数据包(如UDP收到的数据包):

ikcp_input(kcp,received_udp_packet,received_udp_size);
我们要使用recvfrom接收,然后扔到kcp里面做解析

  1. 发送数据:ikcp_send(kcp1, buffer, 8); 用户层接口
  2. 接收数据:hr = ikcp_recv(kcp2, buffer, 10);

4.3 kcp源码流程图

image.png

4.4 kcp配置模式

  1. 工作模式:int ikcp_nodelay(ikcpcb *kcp, int nodelay, int interval, int resend, int nc)
     nodelay :是否启用 nodelay模式,0不启用;1启用。
     interval :协议内部工作的 interval,单位毫秒,比如 10ms或者 20ms
     resend :快速重传模式,默认0关闭,可以设置2(2次ACK跨越将会直接重传)
     nc :是否关闭流控,默认是0代表不关闭,1代表关闭。
    普通模式: ikcp_nodelay(kcp, 0, 40, 0, 0);
    极速模式: ikcp_nodelay(kcp, 1, 10, 2, 1)
    2. 最大窗口:int ikcp_wndsize(ikcpcb *kcp, int sndwnd, int rcvwnd);
    该调用将会设置协议的最大发送窗口和最大接收窗口大小,默认为32,单位为包。
    3. 最大传输单元:int ikcp_setmtu(ikcpcb *kcp, int mtu);
    kcp协议并不负责探测 MTU,默认 mtu是1400字节
    4. 最小RTO:不管是 TCP还是 KCP计算 RTO 时都有最小 RTO 的限制,即便计算出来RTO为40ms,由于默认的 RTO是100ms,协议只有在100ms后才能检测到丢包,快速模式下为30ms,可以手动更改该值: kcp->rx_minrto = 10;

4.5 kcp协议头

image.png
 conv:连接号。UDP是无连接的,conv用于表示来自于哪个客户端。对连接的一种替代
 cmd:命令字。如,
IKCP_CMD_ACK确认命令,
IKCP_CMD_WASK接收窗口大小询问命令,
IKCP_CMD_WINS接收窗口大小告知命令,
 frg:分片,用户数据可能会被分成多个KCP包,发送出去
 wnd:接收窗口大小,发送方的发送窗口不能超过接收方给出的数值
 ts:时间序列
 sn:序列号
 una:下一个可接收的序列号。其实就是确认号,收到 sn=10的包,una为11
 len:数据长度
 data:用户数据
image.png

4.6 kcp发送数据过程

image.png

4.7 kcp接收数据过程

image.png

4.8 kcp确认包处理流程

image.png

4.9 kcp快速确认

image.png

4.10 流量控制和拥塞控制

RTO计算(与TCP完全一样)
RTT:一个报文段发送出去,到收到对应确认包的时间差。
SRTT(kcp->rx_srtt):RTT的一个加权RTT平均值,平滑值。
RTTVAR(kcp->rx_rttval):RTT的平均偏差,用来衡量
RTT的抖动。

4.11 如何在项目中集成kcp

#include <sys/types.h>
#include <sys/socket.h>
#include <pthread.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include "ikcp.h"
#define RECV_BUF 1500

static int number = 0;

typedef struct
{
	unsigned char *ipstr;
	int port;

	ikcpcb *pkcp;

	int sockfd;

	struct sockaddr_in addr;	  //存放服务器信息的结构体
	struct sockaddr_in CientAddr; //存放客户机信息的结构体

	char buff[RECV_BUF]; //存放收发的消息

} kcpObj;
// 编译:  gcc -o server server.c ikcp.c  
// 特别需要注意,这里的服务器端也只能一次使用,即是等客户端退出后,服务端也要停止掉再启动
// 之所以是这样,主要是因为sn的问题,比如客户端第一次启动 sn 0~5, 第二次启动发送的sn还是0 ~5 如果服务器端不停止则自己以为0~5已经收到过了就不会回复。

// 在真正使用的时候,还需要另外的通道让客户端和服务器端之前重新创建ikcpcb,以匹配ikcpcb的conv
/* get system time */
void itimeofday(long *sec, long *usec)
{
#if defined(__unix)
	struct timeval time;
	gettimeofday(&time, NULL);
	if (sec)
		*sec = time.tv_sec;
	if (usec)
		*usec = time.tv_usec;
#else
	static long mode = 0, addsec = 0;
	BOOL retval;
	static IINT64 freq = 1;
	IINT64 qpc;
	if (mode == 0)
	{
		retval = QueryPerformanceFrequency((LARGE_INTEGER *)&freq);
		freq = (freq == 0) ? 1 : freq;
		retval = QueryPerformanceCounter((LARGE_INTEGER *)&qpc);
		addsec = (long)time(NULL);
		addsec = addsec - (long)((qpc / freq) & 0x7fffffff);
		mode = 1;
	}
	retval = QueryPerformanceCounter((LARGE_INTEGER *)&qpc);
	retval = retval * 2;
	if (sec)
		*sec = (long)(qpc / freq) + addsec;
	if (usec)
		*usec = (long)((qpc % freq) * 1000000 / freq);
#endif
}

/* get clock in millisecond 64 */
IINT64 iclock64(void)
{
	long s, u;
	IINT64 value;
	itimeofday(&s, &u);
	value = ((IINT64)s) * 1000 + (u / 1000);
	return value;
}

IUINT32 iclock()
{
	return (IUINT32)(iclock64() & 0xfffffffful);
}

int64_t first_recv_time = 0;
/* sleep in millisecond */
void isleep(unsigned long millisecond)
{
#ifdef __unix /* usleep( time * 1000 ); */
	struct timespec ts;
	ts.tv_sec = (time_t)(millisecond / 1000);
	ts.tv_nsec = (long)((millisecond % 1000) * 1000000);
	/*nanosleep(&ts, NULL);*/
	usleep((millisecond << 10) - (millisecond << 4) - (millisecond << 3));
#elif defined(_WIN32)
	Sleep(millisecond);
#endif
}

int udp_output(const char *buf, int len, ikcpcb *kcp, void *user)
{

	kcpObj *send = (kcpObj *)user;

	//发送信息
	int n = sendto(send->sockfd, buf, len, 0, (struct sockaddr *)&send->CientAddr, sizeof(struct sockaddr_in));
	if (n >= 0)
	{
		//会重复发送,因此牺牲带宽
		printf("send: %d bytes, t:%lld\n", n, iclock64() - first_recv_time); //24字节的KCP头部
		return n;
	}
	else
	{
		printf("error: %d bytes send, error\n", n);
		return -1;
	}
}

int init(kcpObj *send)
{
	send->sockfd = socket(AF_INET, SOCK_DGRAM, 0);

	if (send->sockfd < 0)
	{
		perror("socket error!");
		exit(1);
	}

	bzero(&send->addr, sizeof(send->addr));

	send->addr.sin_family = AF_INET;
	send->addr.sin_addr.s_addr = htonl(INADDR_ANY); //INADDR_ANY
	send->addr.sin_port = htons(send->port);

	printf("服务器socket: %d  port:%d\n", send->sockfd, send->port);

	if (send->sockfd < 0)
	{
		perror("socket error!");
		exit(1);
	}

	if (bind(send->sockfd, (struct sockaddr *)&(send->addr), sizeof(struct sockaddr_in)) < 0)
	{
		perror("bind");
		exit(1);
	}
}

void loop(kcpObj *send)
{
	unsigned int len = sizeof(struct sockaddr_in);
	int n, ret;
	//接收到第一个包就开始循环处理
	int recv_count = 0;

	isleep(1);
	ikcp_update(send->pkcp, iclock());

	char buf[RECV_BUF] = {0};

	while (1)
	{
		isleep(1);
		ikcp_update(send->pkcp, iclock());
		//处理收消息
		n = recvfrom(send->sockfd, buf, RECV_BUF, MSG_DONTWAIT, (struct sockaddr *)&send->CientAddr, &len);
		if (n > 0)
		{
			printf("UDP recv[%d]  size= %d   \n", recv_count++, n);
			if (first_recv_time == 0)
			{
				first_recv_time = iclock64();
			}
			//预接收数据:调用ikcp_input将裸数据交给KCP,这些数据有可能是KCP控制报文,并不是我们要的数据。
			//kcp接收到下层协议UDP传进来的数据底层数据buffer转换成kcp的数据包格式
			ret = ikcp_input(send->pkcp, buf, n);
			if (ret < 0)
			{
				continue;
			}
			//kcp将接收到的kcp数据包还原成之前kcp发送的buffer数据
			ret = ikcp_recv(send->pkcp, buf, n); //从 buf中 提取真正数据,返回提取到的数据大小
			if (ret < 0)
			{ // 没有检测ikcp_recv提取到的数据
				isleep(1);
				continue;
			}
			int send_size = ret;
			//ikcp_send只是把数据存入发送队列,没有对数据加封kcp头部数据
			//应该是在kcp_update里面加封kcp头部数据
			//ikcp_send把要发送的buffer分片成KCP的数据包格式,插入待发送队列中。
			ret = ikcp_send(send->pkcp, buf, send_size);
			printf("Server reply ->  bytes[%d], ret = %d\n", send_size, ret);
			ikcp_flush(send->pkcp);	// 快速flush一次 以更快让客户端收到数据
			number++;
		}
		else if (n == 0)
		{
			printf("finish loop\n");
			break;
		}
		else
		{
			// printf("n:%d\n", n);
		}
	}
}

int main(int argc, char *argv[])
{
	printf("this is kcpServer\n");
	if (argc < 2)
	{
		printf("请输入服务器端口号\n");
		return -1;
	}

	kcpObj send;
	send.port = atoi(argv[1]);
	send.pkcp = NULL;

	bzero(send.buff, sizeof(send.buff));
	char Msg[] = "Server:Hello!"; //与客户机后续交互
	memcpy(send.buff, Msg, sizeof(Msg));

	ikcpcb *kcp = ikcp_create(0x1, (void *)&send); //创建kcp对象把send传给kcp的user变量
	ikcp_setmtu(kcp, 1400);
	kcp->output = udp_output;		//设置kcp对象的回调函数
	ikcp_nodelay(kcp, 0, 10, 0, 0); //1, 10, 2, 1
	ikcp_wndsize(kcp, 128, 128);

	send.pkcp = kcp;

	init(&send); //服务器初始化套接字
	loop(&send); //循环处理

	return 0;
}
#include <sys/types.h>
#include <sys/socket.h>
#include <pthread.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include "ikcp.h"

#include <sys/time.h>
#include <sys/wait.h>
#include <arpa/inet.h>


#include "delay.h"
#define DELAY_TEST2_N 5
#define UDP_RECV_BUF_SIZE 1500

// 编译:  gcc -o client client.c ikcp.c delay.c  -lpthread


 typedef struct {
	unsigned char *ipstr;
	int port;
	
	ikcpcb *pkcp;
	
	int sockfd;
	struct sockaddr_in addr;//存放服务器的结构体
	
	char buff[UDP_RECV_BUF_SIZE];//存放收发的消息
}kcpObj;


/* sleep in millisecond */
void isleep(unsigned long millisecond)
{
	#ifdef __unix 	/* usleep( time * 1000 ); */
	struct timespec ts;
	ts.tv_sec = (time_t)(millisecond / 1000);
	ts.tv_nsec = (long)((millisecond % 1000) * 1000000);
	/*nanosleep(&ts, NULL);*/
	usleep((millisecond << 10) - (millisecond << 4) - (millisecond << 3));
	#elif defined(_WIN32)
	Sleep(millisecond);
	#endif
}



int udp_output(const char *buf, int len, ikcpcb *kcp, void *user){
   
 //  printf("使用udp_output发送数据\n");
   
    kcpObj *send = (kcpObj *)user;

	//发送信息
    int n = sendto(send->sockfd, buf, len, 0,(struct sockaddr *) &send->addr,sizeof(struct sockaddr_in));//【】
    if (n >= 0) 
	{       
		//会重复发送,因此牺牲带宽
	 	printf("send:%d bytes\n", n);//24字节的KCP头部
        return n;
    } 
	else 
	{
        printf("udp_output: %d bytes send, error\n", n);
        return -1;
    }
}


int init(kcpObj *send)
{	
	send->sockfd = socket(AF_INET,SOCK_DGRAM,0);
	
	if(send->sockfd < 0)
	{
		perror("socket error!");
		exit(1);
	}
	
	bzero(&send->addr, sizeof(send->addr));
	
	//设置服务器ip、port
	send->addr.sin_family=AF_INET;
    send->addr.sin_addr.s_addr = inet_addr((char*)send->ipstr);
    send->addr.sin_port = htons(send->port);
	
	printf("sockfd = %d ip = %s  port = %d\n",send->sockfd,send->ipstr,send->port);
	
}

// 特别说明,当我们使用kcp测试rtt的时候,如果发现rtt过大,很大一种可能是分片数据没有及时发送出去,需要调用ikcp_flush更快速将分片发送出去。
void delay_test2(kcpObj *send) {
    // 初始化 100个 delay obj
    char buf[UDP_RECV_BUF_SIZE];
	unsigned int len = sizeof(struct sockaddr_in);

    size_t obj_size = sizeof(t_delay_obj);
    t_delay_obj *objs = malloc(DELAY_TEST2_N * sizeof(t_delay_obj));
	int ret = 0;

	int recv_objs = 0;
	//ikcp_update包含ikcp_flush,ikcp_flush将发送队列中的数据通过下层协议UDP进行发送
 	ikcp_update(send->pkcp,iclock());//不是调用一次两次就起作用,要loop调用
    for(int i = 0; i < DELAY_TEST2_N; i++) {
		//  isleep(1);
		delay_set_seqno_send_time(&objs[i], i);  
		ret = ikcp_send(send->pkcp, (char *) &objs[i], obj_size); 
        if(ret < 0) {
            printf("send %d seqno:%u failed, ret:%d, obj_size:%ld\n", i, objs[i].seqno, ret, obj_size);
            return;
        } 
        // ikcp_flush(send->pkcp);		// 调用flush能更快速把分片发送出去
		//ikcp_update包含ikcp_flush,ikcp_flush将发送队列中的数据通过下层协议UDP进行发送
		ikcp_update(send->pkcp,iclock());//不是调用一次两次就起作用,要loop调用
		
		int n = recvfrom(send->sockfd, buf, UDP_RECV_BUF_SIZE, MSG_DONTWAIT,(struct sockaddr *) &send->addr,&len);
		// printf("print recv1:%d\n", n);
		if(n < 0) {//检测是否有UDP数据包 
			// isleep(1);
			continue;
		}
		ret = ikcp_input(send->pkcp, buf, n);	// 从 linux api recvfrom先扔到kcp引擎
		if(ret < 0)//检测ikcp_input是否提取到真正的数据
		{
			//printf("ikcp_input ret = %d\n",ret);
			continue;			// 没有读取到数据
		}	
		ret = ikcp_recv(send->pkcp, (char *)&objs[i], obj_size);		
		if(ret < 0)//检测ikcp_recv提取到的数据	
		{
			printf("ikcp_recv1 ret = %d\n",ret);
			continue;
		}
		delay_set_recv_time(&objs[recv_objs]);
		recv_objs++;
		printf("recv1 %d seqno:%d, ret:%d\n", recv_objs, objs[i].seqno, ret);
        if(ret != obj_size) {
            printf("recv1 %d seqno:%d failed, size:%d\n", i, objs[i].seqno, ret);
            delay_print_rtt_time(objs, i);
            return;
        }
    }

	// 还有没有发送完毕的数据
	for(int i = recv_objs; i < DELAY_TEST2_N; ) {
		//  isleep(1);
		//ikcp_update包含ikcp_flush,ikcp_flush将发送队列中的数据通过下层协议UDP进行发送
		ikcp_update(send->pkcp,iclock());//不是调用一次两次就起作用,要loop调用
		//ikcp_flush(send->pkcp);		// 调用flush能更快速把分片发送出去  
		int n = recvfrom(send->sockfd, buf, UDP_RECV_BUF_SIZE, MSG_DONTWAIT,(struct sockaddr *) &send->addr,&len);
		// printf("recv2:%d\n", n);
		if(n < 0) {//检测是否有UDP数据包
			printf("recv2:%d\n", n);
			isleep(1);
			continue;
		}
			
		ret = ikcp_input(send->pkcp, buf, n);	
		if(ret < 0)//检测ikcp_input是否提取到真正的数据
		{
			printf("ikcp_input2 ret = %d\n",ret);
			continue;			// 没有读取到数据
		}	
		ret = ikcp_recv(send->pkcp, (char *)&objs[i], obj_size);		
		if(ret < 0)//检测ikcp_recv提取到的数据	
		{
			printf("ikcp_recv2 ret = %d\n",ret);
			continue;
		}
		printf("recv2 %d seqno:%d, ret:%d\n", recv_objs,  objs[i].seqno, ret);
		delay_set_recv_time(&objs[recv_objs]);
		recv_objs++;
		i++;
        if(ret != obj_size) {
            printf("recv2 %d seqno:%d failed, size:%d\n", i, objs[i].seqno, ret);
            delay_print_rtt_time(objs, i);
            return;
        }
        
	}
	ikcp_flush(send->pkcp);

    delay_print_rtt_time(objs, DELAY_TEST2_N);
}

void loop(kcpObj *send)
{
	unsigned int len = sizeof(struct sockaddr_in);
	int n,ret;

	// while(1)
	{
		isleep(1);
		delay_test2(send);
	}
	printf("loop finish\n");
	close(send->sockfd);
	
}

int main(int argc,char *argv[])
{
	//printf("this is kcpClient,请输入服务器 ip地址和端口号:\n");
	if(argc != 3)
	{
		printf("请输入服务器ip地址和端口号\n");
		return -1;
	}
	printf("this is kcpClient\n");
	
	unsigned char *ipstr = (unsigned char *)argv[1];
	unsigned char *port  = (unsigned char *)argv[2];
	
	kcpObj send;
	send.ipstr = ipstr;
	send.port = atoi(argv[2]);
	
	init(&send);//初始化send,主要是设置与服务器通信的套接字对象
	
	bzero(send.buff,sizeof(send.buff));
	
	// 每个连接都是需要对应一个ikcpcb
	ikcpcb *kcp = ikcp_create(0x1, (void *)&send);//创建kcp对象把send传给kcp的user变量
	kcp->output = udp_output;//设置kcp对象的回调函数
	ikcp_nodelay(kcp,0, 10, 0, 0);//(kcp1, 0, 10, 0, 0); 1, 10, 2, 1
	ikcp_wndsize(kcp, 128, 128);
	ikcp_setmtu(kcp, 1400);
	send.pkcp = kcp;	
	loop(&send);//循环处理
	ikcp_release(send.pkcp);
	printf("main finish\n");
	return 0;	
}

5.0 QUIC衍生版本XQUIC(作学习参考)

https://www.yuque.com/docs/share/01d83f75-0fd5-4c9f-976a-0dfcf417e0cc?#
《阿里XQUIC:标准QUIC实现自研之路》

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

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

相关文章

Java人事管理系统oa人力人事办公(含源码+论文+答辩PPT等)

项目功能简介: 该项目采用技术JSP、Struts2、MyBatis、dwr、Tomcat服务器、MySQL数据库、项目含有源码、配套开发软件、软件安装教程、项目发布教程以及代码讲解教程 【项目使用技术&#xff1a;】Struts2MyBatisdwrjqueryjscss等技术 【技术特色】 文件上传&#xff1a; Strut…

Android依赖注入与Hilt的使用

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 基础知识 一、依赖注入是什么&#xff1f; 二.如果依赖注入这么简单&#xff0c;为什么需要专门开发一个框架&#xff1f; 三.安卓的依赖注入框架Dagger与Hilt 四.…

[附源码]计算机毕业设计基于Springboot甜品购物网站

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

如何恢复文件夹隐藏文件?超实用的2种技巧来了

案例&#xff1a;电脑中毒&#xff0c;文件夹数据自动隐藏&#xff0c;现在全都找不到了怎么办&#xff1f;——在日常办公使用电脑过程当中&#xff0c;总会遇到这样那样的问题&#xff0c;比如文件夹数据隐藏问题&#xff0c;当我们需要这些数据时如何恢复正常呢&#xff1f;…

java计算机毕业设计ssm图书馆管理系统z3z90(附源码、数据库)

java计算机毕业设计ssm图书馆管理系统z3z90&#xff08;附源码、数据库&#xff09; 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;…

[附源码]计算机毕业设计基于SpringBoot的疫苗接种管理系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

算法leetcode|19. 删除链表的倒数第 N 个结点(rust重拳出击)

文章目录19. 删除链表的倒数第 N 个结点&#xff1a;样例 1&#xff1a;样例 2&#xff1a;样例 3&#xff1a;提示&#xff1a;原题传送门&#xff1a;分析题解rustgocpythonjava19. 删除链表的倒数第 N 个结点&#xff1a; 给你一个链表&#xff0c;删除链表的倒数第 n 个结…

专享策略No.3 | 商品截面交易策略

量化策略开发&#xff0c;高质量社群&#xff0c;交易思路分享等相关内容 『正文』 ˇ 大家好&#xff0c;2022松鼠俱乐部临近收官。前面发布了专享策略01V3 | 小短波策略&#xff0c;专享策略No.2 | 套利策略-自动换仓-出场加速。今天我们交付第三个专享策略&#xff1a;商品…

C语言—详解库函数中常用的字符串函数

C语言中对字符和字符串的处理很是频繁&#xff0c;但是C语言本身是没有字符串类型的&#xff0c;字符串通常放在 常量字符串中或者字符数组中。字符串常量适用于那些对它不做修改的字符串函数。本篇文章为大家详解八个常用的字符串操作函数&#xff0c;同时我们尝试模拟实现该函…

OpenCV入门(C++/Python)-使用OpenCV裁剪图像(四)

裁剪是为了从图像中删除所有不需要的物体或区域。甚至突出显示图像的特定功能。 使用OpenCV裁剪没有特定的功能&#xff0c;NumPy数组切片是工作。读取的每个图像都存储在2D数组中&#xff08;对于每个颜色通道&#xff09;。只需指定要裁剪区域的高度和宽度&#xff08;以像素…

Linux安装 dotnet sdk 6.0

一、离线安装&#xff08;推荐&#xff09; 1、下载安装包 官网下载地址 2、安装 &#xff08;1&#xff09;将安装包上传到linux服务器上的指定目录&#xff0c;如&#xff1a;/root/dotnet/sdk&#xff0c;运行下面指令解压即可 # cd /root/dotnet/sdk # mkdir -p $HOM…

毕设项目 - SSM留学生交流互动论坛系统(含源码+论文)

文章目录1 项目简介2 实现效果2.1 界面展示3 设计方案3.1 概述3.2 系统流程3.2.1 系统开发流程3.2.2 学生登录流程3.2.3 系统操作流程3.3 系统结构设计4 项目获取1 项目简介 Hi&#xff0c;各位同学好呀&#xff0c;这里是M学姐&#xff01; 今天向大家分享一个今年(2022)最新…

微服务的好处(优点)有哪些?

显然&#xff0c;随着系统复杂度的提升&#xff0c;以及对系统扩展性的要求越来越高&#xff0c;微服务化是一个很好的方向&#xff0c;但除此之外&#xff0c;微服务还会给我们带来哪些好处&#xff1f; 独立&#xff0c;独立&#xff0c;还是独立 我们说微服务打响的是各自…

虹科方案|具有RDMA复制写入日志的高可用性HDD存储集群可提供出色的性能

一、引言 Open-E、Western Digital 和 ATTO 数据存储专家和工程师联手推出了基于 HDD 的数据存储设备&#xff0c;该设备结合Western Digital Ultrastar Data60 JBOD 的海量容量和Ultrastar 系列 HDD 以及加速的性能&#xff0c;这得益于ATTO HBA SAS 控制器和 100GbE NIC&…

制作一个简单HTML大学生抗疫感动专题网页(HTML+CSS)

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

一文刨析C/C++全局常量的定义

目录 文件特性常量 文件常量 堆常量 EOF, WEOF errno 常量 数学常量 数学错误常量 共享常量 转换模式常量 文件读/写访问常量 NULL (CRT) stdin, stdout, stderr 文件特性常量 语法 #include <io.h> 备注 这些常量指定由函数指定的文件或目录的当前特性。…

软件杂志软件杂志社软件编辑部2022年第9期目录

基金项目论文 基于Blockly和树莓派的多模态控制智能车 杨杰;宋俊炜;吴思玮;魏苏州;赖晓晨; 1-313 基于神经网络的网络异常研究与实现 郭稼逸; 4-6《软件》投稿&#xff1a;cnqikantg126.com 计算机视觉技术在电力系统自动化中的应用 陈海远;顾雅青; 7-9 基于优…

【吴恩达机器学习笔记】十二、降维

✍个人博客&#xff1a;https://blog.csdn.net/Newin2020?spm1011.2415.3001.5343 &#x1f4e3;专栏定位&#xff1a;为学习吴恩达机器学习视频的同学提供的随堂笔记。 &#x1f4da;专栏简介&#xff1a;在这个专栏&#xff0c;我将整理吴恩达机器学习视频的所有内容的笔记&…

PM说 | 一文全方位解析C端用户的评论功能!

随着各种自媒体、资讯平台的肆意发展&#xff0c;评论区逐渐成为各路英雄豪杰的必争之地。在评论区&#xff0c;我们可以与”“杠精”来一场遭遇战&#xff0c;邂逅文人墨客&#xff0c;看一场舌战群雄大戏。 评论区越热闹&#xff0c;说明产品越活跃&#xff0c;用户粘性高&a…

iwebsec靶场 文件上传漏洞通关笔记1-第01关 前端js过滤绕过

目录 第01关 前端js过滤绕过 1.禁用js法 &#xff08;1&#xff09;禁用js &#xff08;2&#xff09;刷新页面使生效 &#xff08;3&#xff09;上传脚本 &#xff08;4&#xff09;开启js并刷新页面 2.修改页面法1 &#xff08;1&#xff09;右键查询元素 &#xff…