Linux-UDP套接字编程
一.认识IP地址
IP 协议有两个版本, IPv4 和 IPv6. 我们之后凡是提到 IP 协议, 没有特殊说明的,默认都是指 IPv4。
- IP 地址是在 IP 协议中, 用来标识网络中不同主机的地址;
- 对于 IPv4 来说, IP 地址是一个 4 字节, 32 位的整数;
- 我们通常也使用 "点分十进制" 的字符串表示 IP 地址, 例如 192.168.0.1 ; 用点分割的每一个数字表示一个字节, 范围是 0 - 255;
跨网段的主机的数据传输. 数据从一台计算机到另一台计算机传输过程中要经过一个或多个路由器:
为什么去目标主机要经过路由器呢?同时目的IP又有什么用呢?
上图展示了源主机发送数据到目标主机的过程。我们发现,比如说有一个人今天要从广州到北京。从广州开始出发,他的下一站假设是湖南,此时湖南就是他当前要去往的地方,相当于mac地址。最终的目的地是北京,其实就相当于IP地址。
我们再结合这个流程重新认识下数据的封装和解包过程:
TIPS:为什么需要使用IP地址?
我们之前也已经提到过,mac地址本就可以标识主机的唯一性,为什么还需要使用IP地址呢。这里我们从五个方面说明:
1.MAC地址虽然全球唯一,但缺乏位置信息
MAC地址是设备出厂时烧录的物理标识,虽然能确保全球唯一性,但它只是单纯的编号,没有任何地理位置或网络拓扑信息。如果只用MAC地址通信,数据包无法高效路由,因为网络设备无法判断目标主机在哪个方向,只能盲目广播,导致网络拥塞。
2.IP地址提供分层结构,支持高效路由
IP地址的设计包含网络号和主机号,形成层次化结构。路由器可以根据IP地址的网段快速判断数据包的转发方向,就像快递系统根据省、市、区逐级递送。这种分层寻址让互联网的规模扩展成为可能,而MAC地址的扁平结构无法做到这一点。
3.MAC地址只在局域网内有效
MAC地址的作用范围局限于本地网络(如家庭Wi-Fi或公司内网),交换机依靠它进行精确的端口转发。但跨网络通信时,IP地址才是核心,路由器不关心目标的MAC地址,只根据IP地址选择最佳路径。
4.IP地址能屏蔽底层差异
不同的局域网可能使用不同的传输技术(如以太网、Wi-Fi、光纤等),IP地址作为统一逻辑地址,能隐藏底层细节,让上层应用无需关心具体网络环境。而MAC地址与硬件绑定,无法提供这种抽象能力。
5.目的IP决定路径,MAC地址决定最后一跳
数据包在传输过程中,每经过一个路由器,其MAC地址都会变化(更新为下一跳的地址),但IP地址始终不变。IP负责宏观路径选择,MAC地址只负责当前局域网的精准送达,二者各司其职才能实现高效通信。
就好比为什么人们本来就有身份证可以标识自身的唯一性。为什么点外卖时需要提供手机号和个人位置信息。为什么在公司和学校需要有个人对应的工号学号。所以我们总是在理解计算机中的概念时往往会借助现实生活中的例子,因为人类世界与计算机世界总是息息相关的,计算机本为人的产物。
二.网络套接字编程预备
2.1理解目的IP地址和源IP地址
IP在网络中标识主机的唯一性,但是IP也是有分类的,后面我们会介绍IP 的分类, 以及会详细阐述 IP 的特点。
眼下还有一个重要的问题,数据通过了IP地址传到了目标主机,这些数据传过来干什么?让用户使用!也就是说,数据传输到主机是目的吗? 不是的。 因为数据是给人用的。
那么怎么让用户使用这些数据呢?比如,怎么看到聊天信息的呢? 怎么执行下载任务呢? 怎么浏览网页信息呢? 通过启动的qq, 迅雷, 浏览器。我们发现,qq, 迅雷, 浏览器不就是用户所启动的进程吗,换句话说, 进程是人在系统中的代表, 只要把数据给进程, 人就相当于就拿到了数据。
所以: 数据传输到主机不是目的, 而是手段。 到达主机内部, 在交给主机内的进程,才是目的。那么怎么标识进程的唯一性呢?当然是pid了?错!端口号。
2.2认识端口号
不要着急,我们先来认识端口号,在解释为什么不用pid。端口号(port)是传输层协议的内容:
- 端口号是一个 2 字节 16 位的整数
- 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理
- IP 地址 + 端口号能够标识网络上的某一台主机的某一个进程
- 一个端口号只能被一个进程占用
2.2.1端口号的划分
-
0 - 1023: 知名端口号, HTTP, FTP, SSH 等这些广为使用的应用层协议, 他们的端口号都是固定的(除非我们调用管理员权限才可以绑定使用)。
-
1024 - 65535: 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的。
2.2.2端口号与进程ID
为什么不使用进程ID,本质其实是为了解耦。进程 ID 属于系统概念, 技术上也具有唯一性, 确实可以用来标识唯一的一个进程, 但是这样做, 会让系统进程管理和网络强耦合, 实际设计的时候, 并没有选择这样做。
另外, 一个进程可以绑定多个端口号,但是一个端口号不能被多个进程绑定。可以把端口号想象成公司里的分机号,而进程就像接听电话的员工。一个员工可以负责多个分机号(比如客服小王同时管理投诉热线8080和售后咨询9090),这就好比一个进程(如Web服务器)可以绑定监听多个端口(如80和443)。但一个分机号不能同时分配给多个员工(比如8080不能既归小王又归小李),否则来电时系统会不知道转给谁——同理,操作系统不允许多个进程同时绑定同一个端口,否则数据包来了无法确定交给哪个程序处理。
不过也有例外,像SO_REUSEPORT这种特殊技术,就像让一个团队共享同一个分机号,但需要系统自动分配来电来避免冲突。这种情况我们一般不考虑。
TIPS:源端口号和目的端口号
传输层协议(TCP 和 UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号。就是在描述 "数据是谁发的, 要发给谁"。
2.3认识套接字(Socket) 及传输层的典型代表(TCP/UDP)
2.3.1什么是套接字
综上, IP 地址用来标识互联网中唯一的一台主机, port 用来标识该主机上唯一的一个网络进程。IP+Port 就能表示互联网中唯一的一个进程。
所以, 通信的时候, 本质是两个互联网进程代表人来进行通信, {srcIp,srcPort, dstIp, dstPort}这样的 4 元组就能标识互联网中唯二的两个进程。我们发现,网络通信的本质,其实就是进程间通信!
我们把 ip+port 叫做套接字 socket。
2.3.2 认识TCP/UDP
如果我们了解了系统, 也了解了网络协议栈, 我们就会清楚, 传输层是属于内核的, 那么我们要通过网络协议栈进行通信, 必定调用的是传输层提供的系统调用, 来进行的网络通信。
2.3.3网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址
- TCP/IP 协议规定,网络数据流应采用大端字节序,即低地址高字节.
- 不管这台主机是大端机还是小端机, 都会按照这个 TCP/IP 规定的网络字节序来发送/接收数据
- 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可
为使网络程序具有可移植性,使同样的 C 代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
#include <arpa/inet.h>uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
- h 表示 host(主机),n 表示 network(网络),l 表示 32 位长整数,s 表示 16 位短整数
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回
- 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
例如 htonl 表示将 32 位的长整数从主机字节序转换为网络字节序,例如将 IP 地址转换后准备发送。
2.3.4Socket编程接口
1.Socket常用API
// 创建通信端点
int socket(int domain, int type, int protocol);
/* domain: 协议族(AF_INET/IPv4, AF_INET6/IPv6等)* type: 套接字类型(SOCK_STREAM/TCP, SOCK_DGRAM/UDP)* protocol: 通常填0(自动选择)*/// 绑定地址和端口
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/* sockfd: socket描述符* addr: 指向sockaddr结构体的指针(需转换为sockaddr_in等具体类型)* addrlen: 地址结构体长度*/// 设置监听队列
int listen(int sockfd, int backlog);
/* sockfd: 已绑定的socket描述符* backlog: 最大挂起连接数(典型值5-10)*/// 接受连接请求
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
/* sockfd: 处于监听状态的socket* addr: 用于存储客户端地址(可NULL)* addrlen: 输入输出参数(需先初始化)*/// 发起连接请求
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/* sockfd: 客户端socket描述符* addr: 服务器地址结构体* addrlen: 地址结构体长度*/
2.SocketAddr结构
socket API 是一层抽象的网络编程接口,适用于各种底层网络协议,如 IPv4、 IPv6,以及后面要讲的 UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同
- IPv4 和 IPv6 的地址格式定义在 netinet/in.h 中,IPv4 地址用 sockaddr_in 结构体表示,包括 16 位地址类型, 16 位端口号和 32 位 IP 地址。
- IPv4、 IPv6 地址类型分别定义为常数 AF_INET、 AF_INET6. 这样,只要取得某种 sockaddr 结构体的首地址,不需要知道具体是哪种类型的 sockaddr 结构体,就可以根据地址类型字段确定结构体中的内容(其实就是c版本的多态啦)。
- socket API 可以都用 struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr, 这样的好处是程序的通用性, 可以接收 IPv4, IPv6, 以及 UNIX DomainSocket 各种类型的 sockaddr 结构体指针做为参数。
我们再来结合源码简化的看下sockaddr的结构:
#include <sys/socket.h>struct sockaddr {sa_family_t sa_family; // 地址族(如 AF_INET、AF_INET6)char sa_data[14]; // 地址数据(实际存储 IP 和端口)
};
由于我们一般使用的是sockaddr_in结构,所以我们详细介绍它:
#include <netinet/in.h>//IPv4
struct sockaddr_in {sa_family_t sin_family; // 地址族(必须为 AF_INET/PF_INET)in_port_t sin_port; // 16 位端口号(需用 htons() 转换字节序)struct in_addr sin_addr; // 32 位 IPv4 地址char sin_zero[8];// 填充字段(未使用,通常置0)
};struct in_addr {uint32_t s_addr; // 网络字节序的 IPv4 地址(需用 inet_pton() 转换)
};//IPv6(了解即可,虽然比4好,但不是目前重点)
struct sockaddr_in6 {sa_family_t sin6_family; // AF_INET6in_port_t sin6_port; // 端口号uint32_t sin6_flowinfo; // 流信息struct in6_addr sin6_addr; // 128 位 IPv6 地址uint32_t sin6_scope_id; // 作用域 ID
};
对于结构体in_addr:
#include <netinet/in.h> // 定义位置struct in_addr {uint32_t s_addr; // 32位网络字节序的IPv4地址
};
专门用于存储 网络字节序(大端) 的IPv4地址(例如 0x7F000001
表示 127.0.0.1
)。
对于in_port_t类型其实就是uint16_t类型:
/* Type to represent a port. */
typedef uint16_t in_port_t;
sa_family_t:
/* POSIX.1g specifies this type name for the `sa_family' member. */
typedef unsigned short int sa_family_t;
其实本质上就是一个无符号32位整型。它的地址簇:
/* Protocol families. */
#define PF_UNSPEC 0 /* Unspecified. */
#define PF_LOCAL 1 /* Local to host (pipes and file-domain). */
#define PF_UNIX PF_LOCAL /* POSIX name for PF_LOCAL. */
#define PF_FILE PF_LOCAL /* Another non-standard name for PF_LOCAL. */
#define PF_INET 2 /* IP protocol family. */
#define PF_AX25 3 /* Amateur Radio AX.25. */
#define PF_IPX 4 /* Novell Internet Protocol. */
#define PF_APPLETALK 5 /* Appletalk DDP. */
#define PF_NETROM 6 /* Amateur radio NetROM. */
#define PF_BRIDGE 7 /* Multiprotocol bridge. */
#define PF_ATMPVC 8 /* ATM PVCs. */
#define PF_X25 9 /* Reserved for X.25 project. */
#define PF_INET6 10 /* IP version 6. */
#define PF_ROSE 11 /* Amateur Radio X.25 PLP. */
#define PF_DECnet 12 /* Reserved for DECnet project. */
#define PF_NETBEUI 13 /* Reserved for 802.2LLC project. */
#define PF_SECURITY 14 /* Security callback pseudo AF. */
#define PF_KEY 15 /* PF_KEY key management API. */
#define PF_NETLINK 16
#define PF_ROUTE PF_NETLINK /* Alias to emulate 4.4BSD. */
#define PF_PACKET 17 /* Packet family. */
#define PF_ASH 18 /* Ash. */
#define PF_ECONET 19 /* Acorn Econet. */
#define PF_ATMSVC 20 /* ATM SVCs. */
#define PF_RDS 21 /* RDS sockets. */
#define PF_SNA 22 /* Linux SNA Project */
#define PF_IRDA 23 /* IRDA sockets. */
#define PF_PPPOX 24 /* PPPoX sockets. */
#define PF_WANPIPE 25 /* Wanpipe API sockets. */
#define PF_LLC 26 /* Linux LLC. */
#define PF_IB 27 /* Native InfiniBand address. */
#define PF_MPLS 28 /* MPLS. */
#define PF_CAN 29 /* Controller Area Network. */
#define PF_TIPC 30 /* TIPC sockets. */
#define PF_BLUETOOTH 31 /* Bluetooth sockets. */
#define PF_IUCV 32 /* IUCV sockets. */
#define PF_RXRPC 33 /* RxRPC sockets. */
#define PF_ISDN 34 /* mISDN sockets. */
#define PF_PHONET 35 /* Phonet sockets. */
#define PF_IEEE802154 36 /* IEEE 802.15.4 sockets. */
#define PF_CAIF 37 /* CAIF sockets. */
#define PF_ALG 38 /* Algorithm sockets. */
#define PF_NFC 39 /* NFC sockets. */
#define PF_VSOCK 40 /* vSockets. */
#define PF_KCM 41 /* Kernel Connection Multiplexor. */
#define PF_QIPCRTR 42 /* Qualcomm IPC Router. */
#define PF_SMC 43 /* SMC sockets. */
#define PF_XDP 44 /* XDP sockets. */
#define PF_MCTP 45 /* Management component transport protocol. */
#define PF_MAX 46 /* For now.. *//* Address families. */
#define AF_UNSPEC PF_UNSPEC
#define AF_LOCAL PF_LOCAL
#define AF_UNIX PF_UNIX
#define AF_FILE PF_FILE
#define AF_INET PF_INET
#define AF_AX25 PF_AX25
#define AF_IPX PF_IPX
#define AF_APPLETALK PF_APPLETALK
#define AF_NETROM PF_NETROM
#define AF_BRIDGE PF_BRIDGE
#define AF_ATMPVC PF_ATMPVC
#define AF_X25 PF_X25
#define AF_INET6 PF_INET6
#define AF_ROSE PF_ROSE
#define AF_DECnet PF_DECnet
#define AF_NETBEUI PF_NETBEUI
#define AF_SECURITY PF_SECURITY
#define AF_KEY PF_KEY
#define AF_NETLINK PF_NETLINK
#define AF_ROUTE PF_ROUTE
#define AF_PACKET PF_PACKET
#define AF_ASH PF_ASH
#define AF_ECONET PF_ECONET
#define AF_ATMSVC PF_ATMSVC
#define AF_RDS PF_RDS
#define AF_SNA PF_SNA
#define AF_IRDA PF_IRDA
#define AF_PPPOX PF_PPPOX
#define AF_WANPIPE PF_WANPIPE
#define AF_LLC PF_LLC
#define AF_IB PF_IB
#define AF_MPLS PF_MPLS
#define AF_CAN PF_CAN
#define AF_TIPC PF_TIPC
#define AF_BLUETOOTH PF_BLUETOOTH
#define AF_IUCV PF_IUCV
#define AF_RXRPC PF_RXRPC
#define AF_ISDN PF_ISDN
#define AF_PHONET PF_PHONET
#define AF_IEEE802154 PF_IEEE802154
#define AF_CAIF PF_CAIF
#define AF_ALG PF_ALG
#define AF_NFC PF_NFC
#define AF_VSOCK PF_VSOCK
#define AF_KCM PF_KCM
#define AF_QIPCRTR PF_QIPCRTR
#define AF_SMC PF_SMC
#define AF_XDP PF_XDP
#define AF_MCTP PF_MCTP
#define AF_MAX PF_MAX
我们可以看到AF与PF是一样的,底层其实都是对整数的重定义。
三.使用UDP实现简单的网络服务
3.1(IP)地址转换函数
通常用点分十进制的字符串表示 IP 地址,以下函数可以在字符串表示 和in_addr 表示之间转换;
inet_aton / inet_ntoa (IPv4):
#include <arpa/inet.h>int inet_aton(const char *cp, struct in_addr *inp); // 字符串转二进制
char *inet_ntoa(struct in_addr in); // 二进制转字符串
in_addr_t inet_addr(const char *cp); //inet_addr()本身是线程安全的,因为它不依赖静态缓冲区,当 //输入无效时会返回INADDR_NONE,但要注意"255.255.255.255"也会返回-1(即INADDR_NONE)
对于inet_ntoa,由于该函数转换完后字符串存储于静态存储区。所以当多次调用时会覆盖前一次的IP字符串。所以我们一般不推荐使用上面的inet_aton与inet_ntoa组合地址转换函数,我们更推荐使用下面的组合:
inet_pton / inet_ntop (同时支持IPv4和IPv6):
#include <arpa/inet.h>int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
即确保线程安全的同时又兼顾IPV4与IPV6地址。(或者和inet_addr混着使用,但还是成对的使用看着优雅舒服)。
3.2UdpEchoserver
在写通信代码之前,我们需要明确几个问题,先来看udp套接字相关的核心函数:bind,recvfrom与sendto:(socket本质是文件,所以通信之前需要给文件绑定通信目标信息,通信时需要传递绑定好目标信息的socket以给函数告诉其目标信息)
bind:
#include <sys/types.h>
#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
recvfrom:
#include <sys/types.h>
#include <sys/socket.h>ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
sendto:
#include <sys/types.h>
#include <sys/socket.h>ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
而我们在写通信代码时,通常是有两个端的,一个服务端一个客户端。那么对于服务端,由于未来访问其的客户端不只一个,且各自的IP地址均不相同。所以我们通常让服务端的socket绑定的IP地址是0.0.0.0,即INADDR_ANY,同时绑定固定的端口号,表示但凡访问当前主机的客户端,只要它要访问的端口号是我所绑定的那个端口号,我一律接受并尝试处理请求。
对于客户端,我们的端口号也需要绑定,但不需要绑定固定的端口号,因为我自己知道就行不需要别人去使用。所以我们通常是直接不显示绑定,直接传一个未绑定的socket即可,此时sendto会自动帮助客户端socket绑定一个随机的端口号和当前客户端主机IP地址。
因此我们在执行客户端和服务端各自的可执行文件时,前者需要在后面加上服务端的IP地址和端口号,后者则需要加一个端口号(表示服务端提供服务的固定端口号)。
3.2.1Echoserver代码
这里会直接使用之前写的log.hpp与mutex.hpp,我们这里不再展示直接使用:
udpserver.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include "log.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>const int default_socket = -1;
using namespace LogModule;using func_t = std::function<void(std::string,sockaddr_in)>;class UdpServer
{
public:UdpServer(uint16_t port,func_t func):_socket(default_socket),_port(port),_isrunning(false),_func(func){}void Init(){//1.创建网络套接字_socket = socket(AF_INET,SOCK_DGRAM,0);if(_socket == -1){LOG(LogLevel::FATAL) << "create socket error";exit(1);}LOG(LogLevel::INFO) << "create socket success,socket:" << _socket;//2.绑定对应端口号(IP地址不显示绑定,表示所有传给端口port的消息全部接受)struct sockaddr_in server_addr;bzero(&server_addr,sizeof(server_addr));server_addr.sin_family = PF_INET;server_addr.sin_port = htons(_port);server_addr.sin_addr.s_addr = INADDR_ANY;int n = bind(_socket,(struct sockaddr*)&server_addr,sizeof(server_addr));if(n == -1){LOG(LogLevel::FATAL) << "bind error!" << _socket;exit(2);}LOG(LogLevel::INFO) << "bind success!" << _socket;}void Start(){_isrunning = true;while(_isrunning){//接受消息char buffer[1024];memset(buffer,0,sizeof(buffer));struct sockaddr_in src_addr;socklen_t src_len = sizeof(src_addr);ssize_t n = recvfrom(_socket,buffer,sizeof(buffer),0,(struct sockaddr*)&src_addr,&src_len);if(n == -1){std::cout << "recvfrom error" << std::endl;exit(3);}std::cout << "[" << inet_ntoa(src_addr.sin_addr) << "]" << "[" << ntohs(src_addr.sin_port) << "]" << ":" << buffer << std::endl;//发送消息std::string echo_string = "server echo@ ";echo_string += buffer;n = sendto(_socket,echo_string.c_str(),echo_string.size(),0,(struct sockaddr*)&src_addr,src_len);}}~UdpServer() {}
private:func_t _func;int _socket;//网络文件符uint16_t _port;//进程端口号(1024~65535)bool _isrunning;
};
udpserver.cc:
#include "UdpServer.hpp"
#include <memory>int main(int argc, char *argv[])
{//./udpserver portif(argc != 2){std::cerr << "Usage: " << argv[0] << " port" << std::endl;return 1;}uint16_t port = std::stoi(argv[1]);Output_To_Screen();std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port);usvr->Init();usvr->Start();return 0;
}
udpclient.cc:
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>int main(int argc, char *argv[])
{//./udpclient server_ip server_portif(argc != 3){std::cerr << "Usage: " << argv[0] << "server_ip" << "server_port" << std::endl;return 1;}std::string server_ip = argv[1];uint16_t server_port = std::atoi(argv[2]);//1.创建客户端套接字int c_socket = socket(AF_INET,SOCK_DGRAM,0);if(c_socket == -1){std::cout << "create socket error" << std::endl;exit(1);}std::cout << "create socket success,socket:" << c_socket << std::endl;//2.填写服务器信息struct sockaddr_in server_addr;bzero(&server_addr,sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(server_port);server_addr.sin_addr.s_addr = inet_addr(server_ip.c_str());//3.向服务器发送消息,同时接收服务器发回的消息while(true){std::string input;std::cout << "Please Enter# ";std::getline(std::cin, input);int n = sendto(c_socket, input.c_str(), input.size(), 0, (struct sockaddr*)&server_addr, sizeof(server_addr));(void)n;char buffer[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer);int n1 = recvfrom(c_socket, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);if(n1 > 0){buffer[n1] = 0;std::cout << buffer << std::endl;}}return 0;
}
3.2.2基于echoserver版本实现的简单聊天室
其实思路很简单,因为我们发消息的时候,比如qq或微信,通常发出的消息所有在线用户都可以看到,包括我们自己。所以我们只需要在服务端维护一张在线用户表,当有用户发消息时将该消息回显给所有的用户即可:
由于我们之后需要频繁使用网络地址和主机地址之间进行转换,所以我们这里实现一个类(InterAddr)方便我们之后直接调用:
#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
// 网络地址和主机地址之间进行转换的类class InetAddr
{
public:InetAddr(struct sockaddr_in &addr) : _addr(addr){_port = ntohs(_addr.sin_port); _ip = inet_ntoa(_addr.sin_addr);}uint16_t Port() {return _port;}std::string Ip() {return _ip;}const struct sockaddr_in &NetAddr() { return _addr; }bool operator==(const InetAddr &addr){return addr._ip == _ip && addr._port == _port;}std::string StringAddr(){return _ip + ":" + std::to_string(_port);}~InetAddr(){}private:struct sockaddr_in _addr;std::string _ip;uint16_t _port;
};
(大家这里可以先使用下inet_ntoa,之后再换成inet_ptoa即可,博主这里就投个懒了)。
维护在线用户表:Route.hpp
#include "InterAddr.hpp"
#include <vector>
#include "log.hpp"class Route
{
private:bool IsOnline(InetAddr& peer){for(auto& i : _online_users){if(i == peer){return true;}}return false;}
public:Route() {}void MessageHandle(int socket,std::string& message,InetAddr& peer){if(!IsOnline(peer)){_online_users.emplace_back(peer);LOG(LogLevel::INFO) << "新增一个在线用户:" << "[" << peer.StringAddr() << "]";}std::string send_message = "[" + peer.StringAddr() + "]:" + message; //将当前用户所发消息发给所有用户(包括发消息的用户自己)for(InetAddr& i : _online_users){sendto(socket,send_message.c_str(),send_message.size(),0,(struct sockaddr*)&i,sizeof(i));}//默认用户在线if(message == "QUIT"){auto it = _online_users.begin();while(it != _online_users.end()){if(*it == peer){_online_users.erase(it);break;}}LOG(LogLevel::INFO) << "用户离线" << "[" << peer.StringAddr() << "]";}}~Route() {}
private:std::vector<InetAddr> _online_users;
};
udpserver.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include "log.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "InterAddr.hpp"const int default_socket = -1;
using namespace LogModule;using func_t = std::function<void(int,std::string,InetAddr& peer)>;class UdpServer
{
public:UdpServer(uint16_t port,func_t func):_socket(default_socket),_port(port),_isrunning(false),_func(func){}void Init(){//1.创建网络套接字_socket = socket(AF_INET,SOCK_DGRAM,0);if(_socket == -1){LOG(LogLevel::FATAL) << "create socket error";exit(1);}LOG(LogLevel::INFO) << "create socket success,socket:" << _socket;//2.绑定对应端口号(IP地址不显示绑定,表示所有传给端口port的消息全部接受)struct sockaddr_in server_addr;bzero(&server_addr,sizeof(server_addr));server_addr.sin_family = PF_INET;server_addr.sin_port = htons(_port);server_addr.sin_addr.s_addr = INADDR_ANY;int n = bind(_socket,(struct sockaddr*)&server_addr,sizeof(server_addr));if(n == -1){LOG(LogLevel::FATAL) << "bind error!" << _socket;exit(2);}LOG(LogLevel::INFO) << "bind success!" << _socket;}void Start(){_isrunning = true;while(_isrunning){//接受消息char buffer[1024];memset(buffer,0,sizeof(buffer));struct sockaddr_in src_addr;socklen_t src_len = sizeof(src_addr);ssize_t n = recvfrom(_socket,buffer,sizeof(buffer) - 1,0,(struct sockaddr*)&src_addr,&src_len);if(n == -1){std::cout << "recvfrom error" << std::endl;exit(3);}//使服务器仅接收数据,不发送数据buffer[n] = 0;//n为实际接收到的消息大小InetAddr client(src_addr);_func(_socket,buffer,client);// std::cout << "[" << inet_ntoa(src_addr.sin_addr) << "]" << "[" << ntohs(src_addr.sin_port) << "]" << ":" << buffer << std::endl;// //发送消息// std::string echo_string = "server echo@ ";// echo_string += buffer;// n = sendto(_socket,echo_string.c_str(),echo_string.size(),0,(struct sockaddr*)&src_addr,src_len);}}~UdpServer() {}
private:func_t _func;int _socket;//网络文件符uint16_t _port;//进程端口号(1024~65535)bool _isrunning;
};
udpserver.cc
#include "UdpServer.hpp"
#include "ThreadPool.hpp"
#include <memory>
#include "Route.hpp"using task_t = std::function<void()>;int main(int argc, char *argv[])
{//./udpserver portif(argc != 2){std::cerr << "Usage: " << argv[0] << " port" << std::endl;return 1;}uint16_t port = std::stoi(argv[1]);Output_To_Screen();//1.路由功能,将用户所发消息发给用户自己和所有在线用户Route r;//2.创建线程池auto t = ThreadPool_Module::ThreadPool<task_t>::GetThreadPool();//3.网络服务std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port,[&r,&t](int socket,std::string Message,InetAddr& peer){task_t task = std::bind(&Route::MessageHandle,&r,socket,Message,peer);t->Enque(task);});usvr->Init();usvr->Start();return 0;
}
udpclient.cc
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Thread.hpp"using namespace My_Thread;std::string server_ip;
uint16_t server_port;
int c_socket;void Recv()
{while(true){char buffer[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer);int n1 = recvfrom(c_socket, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);if(n1 > 0){buffer[n1] = 0;std::cerr << buffer << std::endl;}}
}void Send()
{//2.填写服务器信息struct sockaddr_in server_addr;bzero(&server_addr,sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(server_port);server_addr.sin_addr.s_addr = inet_addr(server_ip.c_str());while(true){std::string input;std::cout << "Please Enter# ";std::getline(std::cin, input);int n = sendto(c_socket, input.c_str(), input.size(), 0, (struct sockaddr*)&server_addr, sizeof(server_addr));(void)n;if(input == "QUIT"){break;}}
}int main(int argc, char *argv[])
{//./udpclient server_ip server_portif(argc != 3){std::cerr << "Usage: " << argv[0] << "server_ip" << "server_port" << std::endl;return 1;}server_ip = argv[1];server_port = std::atoi(argv[2]);//1.创建客户端套接字c_socket = socket(AF_INET,SOCK_DGRAM,0);if(c_socket == -1){std::cout << "create socket error" << std::endl;exit(1);}std::cout << "create socket success,socket:" << c_socket << std::endl;//分别使用两个线程,一个接受消息,一个发送消息Thread rececer(Recv);Thread sender(Send);rececer.Start();sender.Start();rececer.join();sender.join();return 0;
}
上面的聊天室代码之前实现过的不再在此展现出来,需要的读者可移步线程池代码实现的文章。
相关文章:
Linux-UDP套接字编程
一.认识IP地址 IP 协议有两个版本, IPv4 和 IPv6. 我们之后凡是提到 IP 协议, 没有特殊说明的,默认都是指 IPv4。 IP 地址是在 IP 协议中, 用来标识网络中不同主机的地址;对于 IPv4 来说, IP 地址是一个 4 字节, 32 位的整数;我们通常也使用 "点分十进制" 的字符串表…...
tsconfig.json和tsconfig.node.json和tsconfig.app.json有什么区别
通过pnpm i vite 生成vue3项目时,会生成三个ts配置文件,分别是什么作用呢? 在Vue 3项目中,tsconfig.json、tsconfig.node.json和tsconfig.app.json是三个不同的TypeScript配置文件,它们分别用于不同的场景和目的。其中tsconfig.n…...
机器学习——Seaborn练习题
1、使用tips数据集,创建一个展示不同时间段(午餐/晚餐)账单总额分布的箱线图 import seaborn as sns import matplotlib.pyplot as plt import numpy as np import pandas as pdplt.rcParams["font.sans-serif"] ["SimHei"] plt.rcParams[&qu…...
Spring XML 外部实体(XXE)指南:示例和预防
什么是XXE? XML 文档遵循特定的标准。该标准强调了 XML 文档的构造方式,概述了有效 XML 文档与无效 XML 文档的区别等等。 该标准还指定了一个称为“实体”的术语。实体是某些内容的占位符。 实体可以是内部的,也可以是外部的(就像我们的情况一样)。 实体通过系统标识…...
C语言(3)—分支和循环
文章目录 一、程序的基本结构二、分支结构1. if语句2. if-else语句 三、关系与逻辑运算符1. 关系运算符2. 逻辑运算符 四、条件运算符(三目运算符)五、switch语句六、循环结构1. while循环2. for循环 七、循环控制语句1. break2. continue 八、循环嵌套九…...
【MCP】从一个天气查询服务带你了解MCP
1. 前言 这篇文章将通过一个集成高德天气查询的 MCP Server 用例,带你上手开发自己的 MCP Server ,文章将通过以下三种方式(自己编写 Client 端代码,使用 mcp-cli 自带页面,集成到 Claude 桌面版等)带你测试自己的 MC…...
【Leetcode 每日一题】3392. 统计符合条件长度为 3 的子数组数目
问题背景 给你一个整数数组 n u m s nums nums,请你返回长度为 3 3 3 的 子数组,满足第一个数和第三个数的和恰好为第二个数的一半。 子数组 指的是一个数组中连续 非空 的元素序列。 数据约束 3 ≤ n u m s . l e n g t h ≤ 100 3 \le nums.length…...
SALOME源码分析:Geomtry模块
本文分析SALOME Geometry模块。 一、核心组件 1.1 GeometryGUI 二、关键流程 三、插件 3.1 插件接口 GEOMPluginGUI定义了Geometry可以加载的插件接口。 3.2 插件列表 插件命令描述 BasicGUI BlocksGUI BooleanGUI BuildGUI DisplayGUI EntityGUI GenerationGUI GEOM…...
力扣HOT100之链表:23. 合并 K 个升序链表
这道题我是用最淳朴最简单的思路去做的,用一个while循环持续地将当前遍历到的最小值加入到合并链表中,while循环中使用一个for循环遍历整个指针数组,将其中的最小值和对应下标记录下来,并将其值加入到合并链表中,同时对…...
ArkTS 组件 通用事件 通用属性 速查表
ArkTS 组件 组件 通用事件 速查表 通用事件事件名称简要说明点击事件onClick(event: Callback<ClickEvent>, distanceThreshold: number): T相较于原有 onClick 接口,新增 distanceThreshold 参数作为点击事件移动阈值,当手指的移动距离超出所设…...
SOAP API 和 REST API
SOAP API 和 REST API 是两种主流的 Web 服务通信架构,它们在设计理念、数据格式、协议支持和应用场景上有显著差异。以下是两者的核心对比及典型应用场景: 1. 核心概念与设计哲学 特性SOAP APIREST API本质协议(基于 XML 的标准化协议&…...
简单了解Java的I/O流机制与文件读写操作
一、理解Java的I/O流机制 字节流 Java中的字节流主要由 InputStream 和 OutputStream 这两个抽象类及其子类构成。字节流以字节(byte)为基本处理单元,适用于处理所有类型的数据,包括文本、图片、音频、视频等。 1. InputStream…...
PCIe 转 U.2 接双硬盘指南 - 超微(Supermicro)主板
前言 公司服务器空间不够想扩容,尝试折腾了下超微(Supermicro)服务器的 PCIe 转 U.2,踩了一点小坑,特地写下来给大家参考一下。 现在市面上 U.2 接口的企业级固态硬盘相对其他类型接口的固态硬盘 便宜很多 ÿ…...
【上位机——MFC】文档
相关类 CDocument提供了一个用于管理数据的类,封装了关于数据的管理(数据提取、数据转换、数据存储等),并和视图类进行数据交互。 文档类使用 定义一个自己的文档类,派生自CDocument 程序的创建过程 1.利用框架类对象地址pFrame调用Load…...
JavaEE-多线程实战02
接上 多线程编程实战01 第三个多线程程序 package thread.test;//定义了一个叫MyThread3的类,实现了Runable接口,所以它必须重写run()方法 class MyThread3 implements Runnable {Overridepublic void run() {//线程执行的具体内容//进入一个无限循环,…...
计算机网络 | 应用层(6) -- 套接字编程
💓个人主页:mooridy 💓专栏地址:《计算机网络:自顶向下方法》 大纲式阅读笔记_mooridy的博客-CSDN博客 💓本博客内容为《计算机网络:自顶向下方法》第二章应用层第七节知识梳理 关注我…...
基于大模型的急性肠套叠全流程预测与诊疗方案研究报告
目录 一、引言 1.1 研究背景与目的 1.2 研究意义与创新点 二、急性肠套叠概述 2.1 定义与分类 2.2 病因与发病机制 2.3 流行病学特征 三、大模型技术原理与应用现状 3.1 大模型基本原理 3.2 在医疗领域的应用案例 3.3 用于急性肠套叠预测的可行性分析 四、术前风险…...
游戏遭遇DDoS攻击如何快速止损?实战防御策略与应急响应指南
是不是很抽象 我自己画的 一、游戏DDoS攻击特征深度解析 游戏行业DDoS攻击呈现复合型特征,2023年监测数据显示,针对游戏服务器的攻击中,63%采用UDP反射放大HTTP慢速攻击组合,攻击峰值达3.2Tbps。攻击者利用游戏协议特性ÿ…...
Linux电源管理(2)_常规的电源管理的基本概念和软件架构
原文: Linux电源管理(2)_Generic PM之基本概念和软件架构 1. 前言 Linux系统中那些常规的电源管理手段,包括关机(Power off)、待机(Standby or Hibernate)、重启(Reboot)等。这些…...
回文链表力扣234
思路: 对于这个题同样的找出题目的要求 1.判断回文 那么我们思考一下判断回文的方法,对于字符串我们只需要翻转一下就行,但是这不是通用的方法,在思考一下,我们是不是可以用双指针,一个在前一个在后,向中…...
互联网的下一代脉搏:深入理解 QUIC 协议
互联网的下一代脉搏:深入理解 QUIC 协议 互联网是现代社会的基石,而数据在其中高效、安全地传输是其运转的关键。长期以来,传输层的 TCP(传输控制协议)一直是互联网的主力军。然而,随着互联网应用场景的日…...
榕壹云国际版短剧系统:基于Spring Boot+MySQL+UniApp的全球短剧创作平台
一、项目背景与简介 在短视频行业高速发展的今天,短剧内容已成为全球用户娱乐消费的新宠。为满足市场对高质量、多样化短剧的需求,我们基于Spring Boot + MySQL + UniApp技术栈开发了榕壹云国际版短剧系统,这是一款面向全球市场的短剧创作与分发平台。系统不仅提供丰富的基…...
为什么选择 Spring Boot? 它是如何简化单个微服务的创建、配置和部署的?
为什么选择 Spring Boot? Spring Boot 的核心目标就是简化 Spring 应用的初始搭建以及开发过程。它并不是要取代 Spring Framework,而是构建在其之上,通过一系列“约定优于配置”的原则和自动化手段,让开发者能够更快的创建出独立…...
方向倒数、梯度和梯度下降的对比关系
一、方向导数与梯度的定义 方向导数 方向导数描述多元函数在某点沿特定方向的变化率。对于函数f(x,y),在点(x0,y0)沿单位向量u(u1,u2)的方向导数定义为: 其物理意义是函数值沿该方向的瞬时变化速率,正负表示增减趋势,…...
全星APQP软件系统:驱动芯片半导体行业研发管理迈向高效与合规新高度
全星APQP软件系统:驱动芯片半导体行业研发管理迈向高效与合规新高度 在芯片半导体行业,一款芯片的研发周期长达数年,涉及设计验证、工艺开发、良率爬坡、量产交付等数百个关键节点,任何一个环节的偏差都可能导致数千万美元的损失…...
文章记单词 | 第50篇(六级)
一,单词释义 fun:英 [fʌn] 美 [fʌn],名词,意为 “享乐;乐趣;快乐;嬉戏;有趣的事;玩笑;逗乐”;形容词,意为 “逗乐的;有…...
RISC-V MCU定时器架构与低功耗设计
RISC-V核低功耗MCU在定时器架构和功耗控制方面具有以下特点: 定时器配置 高级控制定时器:支持互补PWM输出和刹车功能,适合电机控制等场景 通用定时器:提供输入捕获、输出比较和单脉冲模式等基础功能 系统定时器:内置6…...
redis_Windows中安装redis
①Windows安装包下载地址:https://github.com/tporadowski/redis/releases 当前最新版本截图 ②根据自己系统平台的实际情况选择对应的安装包,如:64位win10系统可选择Redis-x64-5.0.14.msi ③下载完成后运行安装,没有特殊要求的话…...
Spring中生成Bean的方式总结-笔记
1. 概略版 Spring生成Bean方式有如下几种,可根据需求选择合适的方式,通常优先使用注解驱动的声明式配置(如Component、Bean),复杂场景结合条件或作用域控制。 方式特点适用场景Component 组件扫描简单直观ÿ…...
学习insightface 的人脸识别
1.Insightface_pytorch版本的自定义数据准备过程 https://github.com/artintel/LearningCode/blob/master/insightface_pytorch_datasets_make/README.md https://github.com/artintel/LearningCode/tree/master/insightface_pytorch_datasets_make 2.mxnet_insightface开源…...
【MQ篇】RabbitMQ之消费失败重试!
目录 引言:消息不丢是底线,失败了优雅重试是修养!消费失败了,为啥不能老是原地复活?🤔智能重试策略一:本地重试(Spring Retry 的魔法)🏠✨智能重试策略二&…...
权力结构下的人才价值重构:从 “工具论” 到 “存在论” 的转变
引言 在现在的公司管理里,常常能听到这样一种说法:“我用你,你才是人才;不用你,你啥都不是。” 这其实反映了一种很常见的评判人才价值的标准,就是只看公司的需求,把人才当作实现公司目标的工…...
【上位机——MFC】视图
相关类 CView及其子类,父类为CWnd类,封装了关于视图窗口的各种操作,以及和文档类的数据交互。 视图窗口的使用 1.定义一个自己的视图类(CMyView),派生自CView,并重写父类成员纯虚函数OnDraw。 2.其余框架类和应用程…...
Unity:Sprite Shapes(精灵形状)
游戏世界的基本构建单位——精灵(Sprite) Sprite(精灵)是什么? Sprite指的是一张小图片,在游戏里代表一个角色、道具、背景元素。 在2D游戏里,比如滑雪游戏,角色、小树、雪地……很…...
火语言RPA--钉钉群通知
【组件功能】:向钉钉群发送文本或markdown消息 在钉钉群创建自定义机器人(Webhook),在组件配置Webhook地址、密钥、文本内容即可向钉钉群发送文本或markdown消息,还可以at群成员或所有人。 配置预览 配置说明 Webho…...
详细图解 Path-SAM2: Transfer SAM2 for digital pathology semantic segmentation
✨ 背景动机 数字病理中的语义分割(semantic segmentation)是非常关键的,比如肿瘤检测、组织分类等。SAM(Segment Anything Model)推动了通用分割的发展,但在病理图像上表现一般。 病理图像(Pa…...
驯龙日记:用Pandas驾驭数据的野性
引言:为什么选择Pandas? "NumPy是手术刀,Pandas是急救箱" 手术刀(NumPy):精密的数值计算 急救箱(Pandas):处理现实数据的全套工具 维度NumPy数组Pandas Se…...
产品经理面经(1)
今天开一个新的栏目,是关于产品经理方面的。产品经理这个岗位每年的需求都是不少的,尤其是近年来AI的兴起造就了产品经理与AI方面深度融合从而催生了“AI产品经理”这种类型的岗位。具体数据如下 总体规模: 2020 年:受疫情影响&am…...
【黑马JavaWeb+AI知识梳理】前端Web基础02 - JS+Vue+Ajax
JS(行为/交互效果) JavaScript(JS)跨平台、面向对象,是用来控制网页行为,实现页面交互效果的脚本语言。 和Java完全不同,但基础语法类似。 组成: ECMAScript:规定了JS…...
Unity Post Processing 小记 【使用泛光实现灯光亮度效果】
一、前言 本篇适用于Unity 2018 - 2019及以上版本,以默认渲染管线为例。文章内容源于个人研究尝试与网络资料收集,可能存在不准确之处。初衷是因新版本制作时老的Bloom插件失效,经研究后分享开启Bloom效果的方法。若在项目中使用Post Proces…...
NFC 碰一碰发视频贴牌技术,音频功能的开发实践与技术解析
在数字化营销与信息交互场景中,NFC 碰一碰技术凭借其便捷性和高效性,成为快速传递多媒体内容的新选择。通过 NFC 实现视频音频的快速传输,不仅能提升用户体验,还能为各类场景带来创新应用。本文将深入探讨该功能开发过程中的关键技…...
新型“电力寄生虫“网络钓鱼攻击瞄准能源企业与知名品牌
本周发布的综合威胁报告显示,自2024年以来,一场名为"电力寄生虫"(Power Parasites)的复杂网络钓鱼活动持续针对全球能源巨头和知名品牌展开攻击。 该攻击活动主要通过精心设计的投资骗局和虚假招聘信息,冒用…...
如何将数据输入到神经网络中
引言 在前面的文章学习中,我们初步了解到神经网络在人工智能领域扮演着至关重要的角色,它具备实现真正人工智能的潜力。真正的人工智能意味着机器能够像人类一样进行感知、学习、推理和决策等复杂活动。而神经网络作为实现这一目标的关键技术,…...
【quantity】2 Unit 结构体(unit.rs)
一、源码 下面代码实现了一个基于类型级别的物理量单位系统,使用Rust的类型系统在编译期保证单位运算的正确性。 use typenum::{Integer, Sum, Diff, Z0, // 0P1, P2, P3, P4, // 1, 2, 3, 4N1, N2, N3 // -1, -2, -3 }; use std::marker::PhantomData; use st…...
OpenCV 图形API(66)图像结构分析和形状描述符------将一条直线拟合到三维点集上函数fitLine3D()
操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 算法描述 拟合一条直线到3D点集。 该函数通过最小化 ∑iρ(ri) 来将一条直线拟合到3D点集,其中 ri 是第 i 个点与直线之间的距离,…...
uniapp: 低功耗蓝牙(BLE)的使用
在微信小程序中实现蓝牙对接蓝牙秤的重量功能,主要依赖微信小程序提供的低功耗蓝牙(BLE)API。以下是一个清晰的步骤指南,帮助你完成从连接蓝牙秤到获取重量数据的开发流程。需要注意的是,具体实现可能因蓝牙秤的协议和…...
谢飞机的Java面试之旅:从Spring Boot到Kubernetes的挑战
场景:互联网大厂Java求职 在一家知名互联网大厂的面试现场,严肃的面试官坐在谢飞机的对面,开始了面试。 第一轮:基础技术与平台 面试官: 谢先生,您能简单介绍一下Java SE 8的主要新特性吗? 谢飞机: 当然,Java 8引入了Lambda表达式、Stream API和新的日期时间API。 …...
Shadertoy着色器移植到Three.js经验总结
Shadertoy是一个流行的在线平台,用于创建和分享WebGL片段着色器。里面有很多令人惊叹的画面,甚至3D场景。本人也移植了几个ShaderToy上的着色器。本文将详细介绍移植过程中需要注意的关键点。 1. 基本结构差异 想要移植ShaderToy的shader到three.js&am…...
基于BenchmarkSQL的OceanBase数据库tpcc性能测试
基于BenchmarkSQL的OceanBase数据库tpcc性能测试 安装BenchmarkSQL及其依赖安装软件依赖编译BenchmarkSQLBenchmarkSQL props文件配置数据库和测试表配置BenchmarkSQL压测装载测试数据TPC-C压测(固定事务数量)TPC-C压测(固定时长)生成测试报告重复测试流程梳理安装Benchmar…...
Flutter 泛型 泛型方法 泛型类 泛型接口
目录 泛型简单使用 泛型类的简单使用 泛型接口的使用 通俗理解:泛型就是解决 类 接口 方法的复用性、以及对不特定数据类型的支持(类型校验) 泛型简单使用 main(){print(getData2("XXX"));getData2<String>("XXX");getData2<int>(1);}Str…...