C++ 使用Socket实现主机间的UDP/TCP通信

news2024/12/26 2:56:08

前言

完整代码放到github上了:cppSocketDemo
服务器端的代码做了跨平台(POSIX和WINDOWS),基于POSIX平台(Linux、Mac OS X、PlayStation等)使用sys/socket.h库,windows平台使用winsock2.h库。
客户端代码因为基本都在windows运行,所以没做跨平台,需要的话你可以参考服务器端代码自己做一下。
文中写的函数原型均为windows平台,部分函数的返回类型或参数类型在POSIX会有不同。

头文件

根据_WIN32标志区分,导入头文件。

#include<iostream>
#include<cstring>
#ifdef _WIN32

#include<winsock2.h>

#else

#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <netdb.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
typedef int SOCKET;

#endif

因为POSIX平台的socket库没有SOCKET类型,所以我们手动定义一下。

UDP Socket

服务器端

对于windows,使用socket前需要手动开启:

#ifdef _WIN32
	WSADATA wsd;
	if(WSAStartup(MAKEWORD(2, 2), &wsd)){
		std::cout << "WSAStartup Error" << std::endl;
		exit(-1);
	}   
#endif

WSAStartup第一个参数表示使用版本号。该函数会向第二个参数填入被激活的socket库的信息。

SOCKET函数

SOCKET socket(int af, int type, int protocol);

参数:

  • af:socket使用协议族,如AF_INET表示IPv4, AF_INET6表示IPv6等。
  • type:指明通过socket发送和接收分组的形式。如SOCK_STREAM表示有序、可靠的数据流分段;SOCK_DGRAM表示离散的报文;SOCK_RAW表示数据头部可以由应用层自定义。
  • protocol:指明发送数据使用什么协议。IPPROTO_UDP;IPPROTO_TCP;IPPROTO_IP;0表示根据socket类型选择默认协议。

通过socket函数创建并返回一个udp类型socket对象:

SOCKET udpSocket = socket(AF_INET, SOCK_DGRAM, 0);

bind函数

将一个socket绑定到一个地址和端口号,使用bind函数:
int bind(SOCKET sock, const sockaddr *address, int address_len);
参数:

  • sock: 绑定socket
  • address:注意是指发送数据包的源地址,而不是发送目的地址
  • address_len:存储address的sockaddr结构体大小
    bind成功时返回0,出现错误时返回-1
    给端口号赋值0,将告诉socket库找一个未被使用的端口并绑定
    如果一个进程试图使用一个未绑定的socket发送数据,网络库将自动为这个socket绑定一个可用的端口号。
    所以对于服务器来说手动调用bind绑定是必须的,而对于客户端来说通常是没有必要的。

服务器端socket需要显式绑定地址和端口,以便客户端访问:

sockaddr_in sain;
sain.sin_family = AF_INET;    //使用IPv4
sain.sin_addr.s_addr = htonl(INADDR_ANY);
sain.sin_port = htons(atoi("50002"));

if(bind(udpSocket, (sockaddr *)&sain, sizeof(sockaddr)) == -1){
	std::cout << "绑定失败" << std::endl;
}

recvfrom函数

从UDP Socket接收数据
int recvfrom(SOCKET s,char *buf,int len,int flags,struct sockaddr *from,int *fromlen);
参数:

  • s: 查询数据的socket。默认情况下,
  • buf: 接收的数据包的缓冲区。
  • len: buf可以存储的最大字节数。到达的数据包的剩余字节将被丢弃。
  • flags: 同sendto flags。
  • from: 指向发送者的地址和端口号的指针,该值由recvfrom函数写入(每接收一个数据包写入一次)。不要手动填写。
  • fromlen: from所指向sockaddr的大小
    如果recvfrom成功执行会返回复制到buf的字节数,发生错误返回-1。

服务器通过recvfrom函数接收客户端信息:

const size_t BufMaxSize = 1000;
char buf[BufMaxSize] = {}; 
sockaddr fromAddr;
#ifndef _WIN32
unsigned
#endif
int fromAddrLen = sizeof(sockaddr);
std::cout << "等待接收..." << std::endl;
while(true){
	if(recvfrom(udpSocket, buf, BufMaxSize, 0, &fromAddr, &fromAddrLen) != -1){
		std::cout << "接收到数据:" << buf << std::endl;
		memset(buf, 0, sizeof(buf));
	}   
	else{
		std::cout << "接收失败或发生错误!" << std::endl;
		return -1; 
	}   
}

最后记得做关闭工作

#ifdef _WIN32
	WSACleanup();
	closesocket(udpSocket);
#else
	close(udpSocket);
#endif

客户端

和服务器一样的先激活:

WSADATA wsd;
if(WSAStartup(MAKEWORD(2, 2), &wsd)){
	std::cout << "WSAStartup Error" << std::endl;
	exit(-1);
}

创建socket:

SOCKET udpSocket = socket(AF_INET, SOCK_DGRAM, 0);

将目标远程主机的IP和端口信息填入sockaddr:

先写一个工具函数:

sockaddr GetSockAddr(uint8_t b1, uint8_t b2, uint8_t b3, uint8_t b4, uint16_t inPort){
	sockaddr addr;
	sockaddr_in *addrin = reinterpret_cast<sockaddr_in*>(&addr);
	addrin->sin_family = AF_INET;
	addrin->sin_addr.S_un.S_un_b.s_b1 = b1;
	addrin->sin_addr.S_un.S_un_b.s_b2 = b2;
	addrin->sin_addr.S_un.S_un_b.s_b3 = b3;
	addrin->sin_addr.S_un.S_un_b.s_b4 = b4;
	addrin->sin_port = htons(inPort);
	
	return addr;
}
sockaddr toAddr = GetSockAddr(127, 0, 0, 1, 50002);

sendTo函数

从UDP Socket发送数据
int sendto(SOCKET s,const char *buf,int len,int flags,const struct sockaddr *to,int tolen);
参数:

  • s: 数据包应该使用的socket,如果没有绑定,socket库将自动绑定一个可用的端口。
  • buf: 待发送数据的起始地址的指针。可以是任何能够被转为char*的数据类型。
  • len: 待发送数据的大小。尽量避免发送数据大于1300字节的数据包,详见p75。
  • flags: 对控制发送的标志进行按位或运算的结果,该值通常取0即可。
  • to: 目标接收者的sockaddr。注意to的地址族必须和用于创建socket的地址族一致。
  • tolen:to的sockaddr的大小。对于IPv4,传入sizeof(sockaddr_in)即可。
    sendto操作成功返回等待发送的数据长度(说明成功进入发送队列),否则返回-1。

通过senTo函数发送数据:

const size_t BufMaxSize = 1000;
char buf[BufMaxSize] = {};
sockaddr toAddr = GetSockAddr(127, 0, 0, 1, 50002); 
int toAddrLen = sizeof(sockaddr);
std::cout << ">>> ";
while(true){
	if(std::cin >> buf){
		sendto(udpSocket, buf, strlen(buf), 0, &toAddr, toAddrLen);
		std::cout << "已发送!" <<std::endl;
		std::cout << ">>> ";
		memset(buf, 0, sizeof(buf));
	}
}

注意,这样发送给linux服务器带中文的字符串的话,可能出现乱码,因为linux通常为UTF-8编码,而windows通常为gb2312编码,所以我们可以在客户端实现两个编码转换函数,并在恰当时机转换:

//UTF-8转GB2312
char* U2G(const char* utf8){
	int len = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, NULL, 0);
	wchar_t* wstr = new wchar_t[len+1];
	memset(wstr, 0, len+1);
	MultiByteToWideChar(CP_UTF8, 0, utf8, -1, wstr, len);
	len = WideCharToMultiByte(CP_ACP, 0, wstr, -1, NULL, 0, NULL, NULL);
	char* str = new char[len+1];
	memset(str, 0, len+1);
	WideCharToMultiByte(CP_ACP, 0, wstr, -1, str, len, NULL, NULL);
	if(wstr) delete[] wstr;
	return str;
}
//GB2312转UTF-8
char* G2U(const char* gb2312){
	int len = MultiByteToWideChar(CP_ACP, 0, gb2312, -1, NULL, 0);
	wchar_t* wstr = new wchar_t[len+1];
	memset(wstr, 0, len+1);
	MultiByteToWideChar(CP_ACP, 0, gb2312, -1, wstr, len);
	len = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, NULL, 0, NULL, NULL);
	char* str = new char[len+1];
	memset(str, 0, len+1);
	WideCharToMultiByte(CP_UTF8, 0, wstr, -1, str, len, NULL, NULL);
	if(wstr) delete[] wstr;
	return str;
}

发送改为:

char *buf_UTF8 = G2U(buf);
sendto(udpSocket, buf_UTF8, strlen(buf_UTF8), 0, &toAddr, toAddrLen);

最后同样的关闭操作:

closesocket(udpSocket);
WSACleanup();

测试

注意,如果把服务器代码放到windows下执行,记得把客户端的转编码代码改下。

注意,编译使用C++11以上编译,链接时加入库:

-lwsock32

image.png

将udpServer.cpp放到服务器上,服务器防火墙记得放行目标端口或暂时关闭防火墙。
udpClient.cpp在本地(windows)。
udpClient中的目标远程主机地址改为服务器ip地址,编译运行:

服务器:
image.png

客户端:
image.png

TCP Socket(单客户端连接)

服务器

同样先激活winsock:

#ifdef _WIN32
	WSADATA wsd;
	if(WSAStartup(MAKEWORD(2, 2), &wsd)){
		std::cout << "WSAStartup Error" << std::endl;
		return -1;
	}
#endif

创建tcp类型socket:

SOCKET tcpsocket = socket(AF_INET, SOCK_STREAM, 0);

绑定本机地址和指定端口号:

sockaddr_in sain;
sain.sin_family = AF_INET;
sain.sin_addr.s_addr = htonl(INADDR_ANY);
sain.sin_port = htons(atoi("50002"));
if(bind(tcpsocket, (sockaddr*)&sain, sizeof(sockaddr)) == -1){
	std::cout << "bind Error" <<  std::endl;
	return -1;
}

listen函数

用listen函数启动监听,等待客户端的连接:
int listen(SOCKET sock, int backlog);
backlog指队列允许传入的最大连接数,超过最大值的连接都将被丢弃。可以使用SOMAXCONN表示默认的backlog值。
函数执行成功返回0,失败返回-1。

使用listen函数开启监听:

listen(tcpsocket, 10);

主机针对每个保持的TCP连接,都需要一个独立的socket存储连接状态。
这里先做只能连接单个客户端,创建连接客户端的socket:

sockaddr clientAddr;		//
	
#ifndef _WIN32
	unsigned
#endif
	int clientAddrLen = sizeof(sockaddr);

accept函数

接受传入的连接并继续TCP握手过程,调用accept函数:
SOCKET accept(SOCKET sock, sockaddr* addr, int* addrlen);
参数:
sock: 接收传入连接的监听socket
addr: 将被写入请求连接的远程主机地址。同样不要手动填写
addrlen: 指向addr缓冲区大小的指针。当真正写入地址之后,accept会更新该值。
如果accept执行成功,将创建并返回一个可以与远程主机通信的新socket。

接受传入的连接并继续TCP握手过程:

SOCKET linkSocket = accept(tcpsocket, &clientAddr, &clientAddrLen);

recv函数

调用recv函数从一个连接的TCP socket接收数据:
int recv(SOCKET s,char *buf,int len,int flags);
参数:
s: 待接收数据的socket
buf: 数据接收缓冲区。
len: 拷贝到buf中的数据的最大数量。
flags: 标志位,大多数情况填0。
调用成功返回接收的数据大小。如果返回0,说明连接的另一端发送了一个FIN数据包,承诺没有更多需要发送的数据。
如果发生错误,返回-1
默认情况下,如果socket的接收缓冲区中没有数据,recv函数将阻塞调用线程,直到数据流中的下一组数据到达或超时。

send函数

通过连接的socket使用send函数发送数据:
因为连接的socket存储了远程主机地址信息,所以不需要传入地址参数:
int send(SOCKET s,const char *buf,int len,int flags);
参数:
s: 用于发送数据的socket
buf: 写入缓冲区。注意:和UDP不同,是将数据放到socket的输出缓冲区中,由socket库来决定在将来某一时刻发出。
len: 传输的字节数量。注意:与UDP不同,不需要保持这个值低于链路层的MTU。
flags:标志位,大多数情况下填0即可。
send调用成功返回发送数据的大小,如果发送错误返回-1.
默认情况下该函数会阻塞线程,直到调用超时或发送了足够的数据。
非0的返回值不代表成功发送出去了,只说明数据被存入队列中等待发送。

使用recv函数和send函数接收和响应客户端信息:

const size_t BufMaxLen = 1000;
char buf[BufMaxLen] = {};
char sendBuf[BufMaxLen] = "服务器成功接收!";
while(true){
	int ret = recv(linkSocket, buf, BufMaxLen, 0);
	if(ret > 0){
		std::cout << "从客户端接收到数据:" << buf << std::endl;
		memset(buf, 0, BufMaxLen);
		send(linkSocket, sendBuf, strlen(sendBuf), 0);
	}
	else if(ret == 0){
		std::cout << "客户端停止发送数据,准备关闭连接..." << std::endl;
		break;
	}
	else{
		std::cout << "recv发生错误!" << std::endl;
	}
}

最后关闭:

#ifdef _WIN32
	closesocket(tcpsocket);
	closesocket(linkSocket);
	WSACleanup();
#else
	close(tcpsocket);
	close(linkSocket);
#endif
	std::cout << "已关闭服务器Socket..." << std::endl;

客户端

客户端没有新函数,直接看代码吧!

TCPSocketClient.cpp:

#include<iostream>
#include<winsock2.h>
#include<windows.h>
#include<memory>
#include<cstring>

using namespace std;

//UTF-8转GB2312
char* U2G(const char* utf8){
	int len = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, NULL, 0);
	wchar_t* wstr = new wchar_t[len+1];
	memset(wstr, 0, len+1);
	MultiByteToWideChar(CP_UTF8, 0, utf8, -1, wstr, len);
	len = WideCharToMultiByte(CP_ACP, 0, wstr, -1, NULL, 0, NULL, NULL);
	char* str = new char[len+1];
	memset(str, 0, len+1);
	WideCharToMultiByte(CP_ACP, 0, wstr, -1, str, len, NULL, NULL);
	if(wstr) delete[] wstr;
	return str;
}
//GB2312转UTF-8
char* G2U(const char* gb2312){
	int len = MultiByteToWideChar(CP_ACP, 0, gb2312, -1, NULL, 0);
	wchar_t* wstr = new wchar_t[len+1];
	memset(wstr, 0, len+1);
	MultiByteToWideChar(CP_ACP, 0, gb2312, -1, wstr, len);
	len = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, NULL, 0, NULL, NULL);
	char* str = new char[len+1];
	memset(str, 0, len+1);
	WideCharToMultiByte(CP_UTF8, 0, wstr, -1, str, len, NULL, NULL);
	if(wstr) delete[] wstr;
	return str;
}

sockaddr GetSockAddr(uint8_t b1, uint8_t b2, uint8_t b3, uint8_t b4, uint16_t inPort){
	sockaddr addr;
	sockaddr_in *addrin = reinterpret_cast<sockaddr_in*>(&addr);
	addrin->sin_family = AF_INET;
	addrin->sin_addr.S_un.S_un_b.s_b1 = b1;
	addrin->sin_addr.S_un.S_un_b.s_b2 = b2;
	addrin->sin_addr.S_un.S_un_b.s_b3 = b3;
	addrin->sin_addr.S_un.S_un_b.s_b4 = b4;
	addrin->sin_port = htons(inPort);
	
	return addr;
}


int main(){
	WSADATA wsd;
	if(WSAStartup(MAKEWORD(2, 2), &wsd)){
		std::cout << "WSAStartup Error" << std::endl;
		return -1;
	}
	SOCKET tcpSocket = socket(AF_INET, SOCK_STREAM, 0);
	sockaddr serverAddr = GetSockAddr(127, 0, 0, 1, 50002);   
	int serverAddrLen = sizeof(sockaddr);
	if(connect(tcpSocket, &serverAddr, serverAddrLen) == -1){
		std::cout << "connect Error!" << std::endl;
		return -1;
	}
	std::cout << "已成功连接到服务器" << std::endl;
	//客户端的socket就是用于连接的socket
	const int BufMaxLen = 1000;
	char sendBuf[BufMaxLen] = {};
	char buf[BufMaxLen] = {};
	std::cout << ">>> ";
	while(true){
		if(std::cin >> sendBuf){
			if(strcmp(sendBuf, "exit") == 0){
				std::cout << "正在关闭连接..." << std::endl;
				break;
			}
			char *sendBuf_UTF8 = G2U(sendBuf);
			send(tcpSocket, sendBuf_UTF8, strlen(sendBuf_UTF8), 0);
			memset(sendBuf, 0, BufMaxLen);
			int ret = recv(tcpSocket, buf, BufMaxLen, 0);
			if(ret > 0){
				std::cout << "从服务器接收到回应:" << U2G(buf) << std::endl;
				memset(buf, 0, BufMaxLen);
			}
			std::cout << ">>> ";
		}
	}
	
	
	shutdown(tcpSocket, SB_BOTH);
	closesocket(tcpSocket);
	WSACleanup();
	std::cout << "已关闭客户端Socket..." << std::endl;
	
	return 0;
}

测试

注意,如果把服务器代码放到windows下执行,记得把客户端的转编码代码改下。

测试方式同上面UDP。

客户端:
image.png
服务器:
image.png

TCP Socket(多客户端连接)

服务端

使用多线程,每响应一个客户端连接为它创建一个线程。
多线程头文件:

#include<thread>

将之前的响应代码搬到函数中作为线程入口:

void linkClientThread(SOCKET linkSocket, unsigned int linkId){
	printf("客户端(id:%d) 已连接!\n", linkId);
	const size_t BufMaxLen = 1000;
	char buf[BufMaxLen] = {};
	char sendBuf[BufMaxLen] = "服务器成功接收!";
	while(true){
		int ret = recv(linkSocket, buf, BufMaxLen, 0);
		if(ret > 0){
			printf("从客户端(id:%d)接收到数据:%s\n", linkId, buf);
			memset(buf, 0, BufMaxLen);
			send(linkSocket, sendBuf, strlen(sendBuf), 0);
		}
		else if(ret == 0){
			printf("客户端(id:%d)停止发送数据,关闭连接...\n", linkId);
			break;
		}
		else{
			printf("recv发生错误!\n");
			break;
		}
	}
	
#ifdef _WIN32
	closesocket(linkSocket);
#else
	close(linkSocket);
#endif
	
}

当接收到连接请求,为它单独创建一个线程服务:

while(true){
	SOCKET linkSocket = accept(tcpsocket, &clientAddr, &clientAddrLen);
	std::thread linkThread(linkClientThread, linkSocket, ++linkId);
	linkThread.detach();
}

客户端

客户端直接继续使用之前的tcpClient.cpp即可,没有区别。

测试

同样的注意,如果把服务器代码放到windows下执行,记得把客户端的转编码代码改下。

服务器还是使用linux系统的,所有客户端在本地的windows执行:
注意:server代码在linux编译时要加入-lpthread.h选项:

g++ -g tcpServer_multiConnection.cpp -o tcpServer_multiConnection -std=c++11 -lpthread

客户端1:
image.png

客户端2:
image.png

服务器:
image.png


完整代码放到github上了:cppSocketDemo

参考文献:《网络多人游戏架构与编程》

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

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

相关文章

STM32配置LED模块化

文章目录前言一、LED的模块化二、GPIO初始化详细解析三、LED代码封装总结前言 本篇文章将带大家深入了解GPIO的配置&#xff0c;并带大家实现LED模块化编程。 一、LED的模块化 什么叫模块化编程&#xff1f;我的理解就是每一个模块都分别写成对应的.c和.h文件&#xff0c;有…

S32K144—从0到1一个MBD模型的诞生

一个MBD模型的诞生&#xff0c;分为以下几步&#xff1a; 1、连接好硬件S32K144 EVB 2、选择一个合适的工作空间&#xff0c;新建一个simulink模型&#xff0c;保存 3、在模型中拖入模块&#xff1a; MBD_S32K1xx_Config_Information Digital_Input_ISR Periodic_Interrupt…

C++ 设计模式

设计模式序创建型模式工厂方法模式抽象工厂模式单例模式建造者模式&#xff08;生成器模式&#xff09;原型模式结构型模式适配器模式装饰器代理模式外观模式桥接模式组合模式&#xff08;部分--整体模式&#xff09;享元模式行为型模式策略模式模板模式观察者模式迭代器模式责…

对抗js前端加密的万能方法

1、前言 现在越来越多的网站采用全报文加密&#xff0c;测试的时候需要逆向提取加密算法以及密钥&#xff0c;过程十分繁琐和复杂。本文提供一种更为简单快捷的方法来解决此问题。 原理大致如下&#xff1a;使用浏览器的Override Hook加密前的数据&#xff0c;配置代理地址发…

[Linux]Linux编译器-gcc/g++

&#x1f941;作者&#xff1a; 华丞臧. &#x1f4d5;​​​​专栏&#xff1a;【LINUX】 各位读者老爷如果觉得博主写的不错&#xff0c;请诸位多多支持(点赞收藏关注)。如果有错误的地方&#xff0c;欢迎在评论区指出。 推荐一款刷题网站 &#x1f449; LeetCode刷题网站 文…

SpringBoot+Redis(官方案例)

在线文档项目结构 1.源码克隆&#xff1a;git clone https://github.com/spring-guides/gs-messaging-redis.git 2.包含两个项目initial和complete&#xff0c;initial可以根据文档练习完善&#xff0c;complete是完整项目 3.功能描述&#xff1a;构建应用程序&#xff0c;使用…

【谷粒商城基础篇】商品服务:商品维护

谷粒商城笔记合集 分布式基础篇分布式高级篇高可用集群篇简介&环境搭建项目简介与分布式概念&#xff08;第一、二章&#xff09;基础环境搭建&#xff08;第三章&#xff09;整合SpringCloud整合SpringCloud、SpringCloud alibaba&#xff08;第四、五章&#xff09;前端知…

xxx.lua入门编程

lua入门级编程,openresty的前置技能lua入门级编程,openresty的前置技能 看上图 lua示例&#xff1a; 入门示例 print("hello world!") local arr {"java","mysql","oracle"}; local map {usernamezhangsan,password123}; local fu…

Debezium 同步 PostgreSQL 数据到 RocketMQ 中

1.RocketMQ Connect概览 RocketMQ Connect是RocketMQ数据集成重要组件&#xff0c;可将各种系统中的数据通过高效&#xff0c;可靠&#xff0c;流的方式&#xff0c;流入流出到RocketMQ&#xff0c;它是独立于RocketMQ的一个单独的分布式&#xff0c;可扩展&#xff0c;可容错系…

字节二面:Redis 的大 Key 对持久化有什么影响?

Redis 的持久化方式有两种&#xff1a;AOF 日志和 RDB 快照。 所以接下来&#xff0c;针对这两种持久化方式具体分析分析。 大 Key 对 AOF 日志的影响 先说说 AOF 日志三种写回磁盘的策略 Redis 提供了 3 种 AOF 日志写回硬盘的策略&#xff0c;分别是&#xff1a; Always&am…

Git(四) - Git 分支操作

​​​​​​​ 一、什么是分支 在版本控制过程中&#xff0c;同时推进多个任务&#xff0c;为每个任务&#xff0c;我们就可以创建每个任务的单独分支。使用分支意味着程序员可以把自己的工作从开发主线上分离开来&#xff0c;开发自己分支的时候&#xff0c;不会影响主线分支…

前端面试常考 | js原型与原型链

文章目录一. 什么是原型?二. 什么是原型链?一. 什么是原型? 在js中所有的引用类型都有一个__proto__(隐式原型)属性&#xff0c;属性值是一个普通的对象。 而在js中的引用类型包括&#xff1a;Object&#xff0c;Array&#xff0c;Date&#xff0c;Function 而所有函数都有…

基于K8s的DevOps平台实践(二)

文章目录1. 流水线入门&#x1f351; 流水线基础语法&#x1f351; 脚本示例&#x1f351; 脚本解释&#x1f351; Blue Ocean2. Jenkinsfile实践&#x1f351; 演示一&#x1f351; 演示二&#x1f351; 演示三&#x1f351; 演示四&#x1f351; 总结3. 多分支流水线实践&…

BEV视觉3D感知算法梳理

1. 基于BEV空间的自动驾驶感知任务 最近&#xff0c;基于BEV空间下的感知任务已经涌现出了众多优秀算法&#xff0c;并在多个自动驾驶公开数据集&#xff08;KITTI&#xff0c;Waymo&#xff0c;nuScenes&#xff09;上取得了非常不错的成绩。根据自动驾驶汽车上安装的传感器类…

【从零开始学习深度学习】37. 深度循环神经网络与双向循环神经网络简介

目录1. 深度循环神经网络2. 双向循环神经网络总结1. 深度循环神经网络 之前介绍的循环神经网络只有一个单向的隐藏层&#xff0c;在深度学习应用里&#xff0c;我们通常会用到含有多个隐藏层的循环神经网络&#xff0c;也称作深度循环神经网络。下图演示了一个有LLL个隐藏层的…

数字化时代,全方位解读商业智能BI

商业智能BI是一种通用的数据类技术解决方案&#xff0c;不会因为行业BI没有进行针对性开发而出现不适配、无法使用的情况。同时&#xff0c;也正因为商业智能BI核心是数据&#xff0c;只要企业有数据沉淀&#xff0c;不管是哪些行业BI商业智能都能发挥出作用。 不过考虑到不同…

文件IO操作开发笔记(一):使用Qt的QFile对磁盘文件存储进行性能测试以及测试工具

文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/128438303 红胖子(红模仿)的博文大全&#xff1a;开发技术集合&#xff08;包含Qt实用技术、树莓派、三维、OpenCV、OpenGL、ffmpeg、OSG、单片机、软硬结…

portraiture2023智能磨皮修饰滤镜插件中文版

在人像后期修图的时候免不了需要进行磨皮处理&#xff0c;很多人在挑选磨皮软件的时候都不知道该如何选择&#xff0c;今天的文章就来带大家看看磨皮软件哪个好&#xff0c;能磨皮的修图软件和插件!借助磨皮软件即使是新手也能做出高级的人像图片&#xff0c;下面挑选了几款好用…

Java 并发编程知识总结【五】

6. 线程中断与 LockSupport 6.1 线程中断机制 大厂&#xff08;蚂蚁金服&#xff09;面试题&#xff1a; 什么是中断&#xff1f; 首先&#xff0c;一个线程不应该由其他线程来强制中断或停止&#xff0c;而是应该由线程自己自行停止。所以&#xff0c;Thread.stop, Thread.…

Exynos_4412——中断控制器

目录 一、中断控制器 中断控制器的作用&#xff1a; 二、Exynos_4412下的中断控制器 它支持三种类型的中断 可以编程设置&#xff1a; 三、中断控制器详解 四、中断控制器编程 一、中断控制器 外设产生的中断信号&#xff0c;先要经过中断控制器&#xff0c;中断是异常…