Linux之socket编程(一)
前言
网络通信的目的
我们已经大致了解了网络通信的过程: 如果主机A想发送数据给主机B, 就需要不断地对本层的协议数据单元(PDU)封装, 然后经过交换设备的转发发送给目的主机, 最终解封装获取数据.
那么网络传输的意义只是将数据由一台主机发送到另一台主机吗?
不是, 将数据从A传送到B只是网络通信的手段, 并不是目的.
真正进行通信的是用户A和用户B主机上的两个应用程序, 实质上是进程之间在通信.
所以网络通信的目的是让两台计算机上的两个进程进行通信.
端口号
我们已经知道, IP地址可以标识计算机在互联网中的唯一性, 但是一台计算机上会存在大量的进程, 那如何保证 A主机某个进程发送的数据 能准确地让 B主机指定的进程 接收到呢?
所以我们需要标识一台计算机上一个进程的唯一性, 所以有了端口号(port).
端口号有以下特点:
1. port是一个16位的整型(uint_16).
2. port可标识当前主机上唯一的一个网络进程.3. 一个port不能和多个进程关联, 这是和port本身的定义相违背的, 也就是不能一个key(port)映射多个value(进程); 但是一个进程可以关联多个port, 也就是多个key可以映射同一个vlaue.
有一个疑问, 我们知道系统中有一个pid可以唯一标识一个进程, 为什么网络通信不直接用pid?
1. PID 是操作系统内核用来管理进程的一个标识符, 是系统层面的, 所以将进程管理和网络管理进行解耦合, 进程管理无论如何变化(PID的变化), 都不会影响端口号.
2. port是专门用来网络通信的, 系统中并不是所有进程都有网络通信的需求.
3. 网络连接的生命周期通常独立于进程的生命周期, 即使一个进程结束, 网络连接可能会保持一段时间, 或者由其他进程接管.
由于IP地址可以标识计算机在网络中唯一性, 端口号port又能用来标识进程在计算机中唯一性, 所以如果我们需要寻找全网某一个进程, 先通过IP地址查找全网唯一的主机, 然后通过端口号port找到该主机唯一的进程.
所以我们使用socket套接字实现网络通信就需要: 双方的IP地址 + 双方的端口号port.
认识TCP和UDP协议
TCP和UDP是传输层两个使用较多的协议:
TCP协议(Transmission Control Protocol)中文名为传输控制协议, Internet 面向连接的服务:
- 有连接(请求响应)
- 可靠地、按顺序地传送数据(确认和重传)
- 流量控制, 发送方不会淹没接收方
-
拥塞控制, 当网络拥塞时,发送方降低发送速率
应用在HTTP (Web), FTP (文件传送), Telnet (远程登录), SMTP (email)等
UDP协议(User Datagram Protocol)中文名为用户数据报协议, 无连接的服务:
-
无连接
-
不可靠数据传输
-
无流量控制
-
无拥塞控制
应用在流媒体、远程会议、 DNS、Internet电话等
网络字节序
大小端
我们知道计算机分为大端机和小端机, 如果两台计算机的字节序不同, 那么接收到的数据解释出来意义也完全不同. 所以网络为了适配所有类型的主机, 规定: 网络中的字节序一律采用大端. 具体来说:
1. 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出; 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
2. 因此,网络数据流的地址应这样规定: 先发出的数据是低地址, 后发出的数据是高地址.
3. TCP/IP协议规定,网络数据流应采用大端字节序, 即低地址高字节.
4. 所以不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
既然网络有这样的规定, 那我们在写代码时是否需要先判断一下字节序呢?
不需要, 正因为机器大小端的判断很繁琐, 所以操作系统早就提供了支持主机字节序和网络的字节序相互转换的接口.
- 如果主机是小端字节序, 这些函数将参数做相应的大小端转换然后返回
- 如果主机是大端字节序, 这些函数不做转换, 将参数原封不动返回
字节序转换接口
一共有四个接口, 在头文件arpa/inet.h中, 函数名称中的 h代表host, n代表net, ,l表示32位长整数,s表示16位短整数
uint32_t htonl(uint32_t hostlong);
头文件: arpa/inet.h
功能: 将主机上unsigned int类型的数据转换成对应网络字节序
参数: uint32_t hostlong是需要转换的unsigned int类型数据
返回值: 返回转换后的数据
uint16_t htons(uint16_t hostshort);
头文件: arpa/inet.h
功能: 将主机上unsigned short类型的数据转换成对应网络字节序
参数: uint16_t是需要转换的unsigned short类型数据
返回值: 返回转换后的数据
uint32_t ntohl(uint32_t netlong);
头文件: arpa/inet.h
功能: 将从网络上读取的unsigned int类型的数据转换成主机的字节序
参数: uint32_t是需要转换的unsigned int类型数据
返回值: 返回转换后的数据
uint16_t ntohs(uint16_t netshort);
头文件: arpa/inet.h
功能: 将从网络上读取的unsigned short类型的数据转换成主机的字节序。
参数: uint16_t是需要转换的unsigned short类型数据。
返回值: 返回转换后的数据
socket套接字
socket 的原意是“插座”, 在计算机通信领域, socket 被翻译为“套接字”, 它是计算机之间进行通信的一种约定或一种方式. 其命名寓意着Socket协议可以像插座一样即插即用, 快速联通网络上的两台电脑.
套接字作为TCP的上层协议, 可以使用户十分轻易地在计算机网络中互相传递消息, 而无需过多关注复杂的TCP以及IP协议. 其中, 使用文件描述 fd 来标记套接字对象.
我们上网的所有行为无非就两种: 发数据和读数据. 所以, 网络通信实质是数据的IO. Linux下一切皆文件, 所以网络在系统看来也是一个"文件", 也有维护它的结构体, 也有自己的文件描述符.
socket 常见API
//创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,
socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
API的设计
先对其中的两个API的参数进行介绍, 从这两个参数可以看出网络编程API的设计思路:
1. domain参数体现了网络编程的不同场景:
网络编程的时候, socket是有很多类别的. 我们进程间通信用到的SYSTEM V标准, 只限于本主机间进程的通信; 而我们用到的socket编程, 有很多种通信方式, 可以网络通信, 也可以本主机通信, 也可以绕过TCP/UDP底层开发等. 常见的有三种通信方式:
a. unix socket: 域间socket -> 不用ip, 用文件路径通信, 和命名管道很类似, 主要用于本主机内部通信.
b. 网络socket: ip+port, 进行网络通信, 也可以本地通信
c. 原始socket: 当我们进行不以数据传输为目的的通信, 主要用来编写一些网络工具, 它会直接
- 绕过传输层去访问网络层
- 或绕过传输层和网络层去访问数据链路层.
所有的domain_family类型, 最常用的有AF_INET(ipv4):
The domain argument specifies a communication domain; this selects the protocol family which will be used for communication. These families are defined in
<sys/socket.h>. The formats currently understood by the Linux kernel include:Name Purpose Man page
AF_UNIX Local communication unix(7)
AF_LOCAL Synonym for AF_UNIX
AF_INET IPv4 Internet protocols ip(7)
AF_AX25 Amateur radio AX.25 protocol ax25(4)
AF_IPX IPX - Novell protocols
AF_APPLETALK AppleTalk ddp(7)
AF_X25 ITU-T X.25 / ISO-8208 protocol x25(7)
AF_INET6 IPv6 Internet protocols ipv6(7)
AF_DECnet DECet protocol sockets
AF_KEY Key management protocol, originally de‐veloped for usage with IPsec
AF_NETLINK Kernel user interface device netlink(7)
AF_PACKET Low-level packet interface packet(7)
AF_RDS Reliable Datagram Sockets (RDS) protocol rds(7)rds-rdma(7)
AF_PPPOX Generic PPP transport layer, for settingup L2 tunnels (L2TP and PPPoE)
AF_LLC Logical link control (IEEE 802.2 LLC)protocol
AF_IB InfiniBand native addressing
AF_MPLS Multiprotocol Label Switching
AF_CAN Controller Area Network automotive busprotocol
AF_TIPC TIPC, "cluster domain sockets" protocol
AF_BLUETOOTH Bluetooth low-level socket protocol
AF_ALG Interface to kernel cr
2. struct sockaddr 参数
我们已经知道, 网络编程时有不同的应用场景, 理论上而言, 我们要给每一种场景设计一套独立的接口, 类似之前的进程间通信. 但是这里出现了一种通用地址类型struct sockaddr, 说明网络API设计者希望所有通信场景使用一套接口:
下图可以看出, 不同的通信场景都为其设计了不同的结构体, 当函数中struct sockaddr拿到一个地址, 取出其前16位标识, 然后根据标识类型的不同区把参数强转为不同的类型, 这是C语言多态思想的一种应用:
简单提一句, 为什么此处不使用 void* 呢?
原因很简单, 设计接口的时候C语言还不支持void*
sockaddr结构的一种实现:
sockaddr_in结构的实现:
我们真正在基于IPv4编程时, 使用的数据结构是 sockaddr_in; 这个结构里主要有三部分信息:
- 地址类型: sin_family
- 端口号: sin_port
- IP地址: sin_addr.s_addr
接口介绍:
1. int socket(int domain, int type, int protocol);
头文件:sys/types.h、sys/socket.h
功能:创建套接字, 而且在创建的时候需要指定使用的通信协议.
参数:
- int domain是地址族, 上面说过是套接字编程的通信类型. 最常用的是AF_INET, 表示使用IPv4的网络套接字进行网络通信.
- int type可指定通信语义, 比如是面向字节流还是面向用户数据报. SOCK_STREAM是面向字节, SOCK_DGRAM是面向用户数据报, 对应了TCP和UDP协议.
- int protocol是用来指定具体协议名的, 比如TCP或者UDP. 如果设置为0, 默认使用由前两个参数所确定的推荐使用的协议.
返回值: 成功创建返回一个文件描述符sockfd; 失败则返回-1, 并且设置错误码errno
2. int bind(int socket, const struct sockaddr *address, socklen_t address_len);
头文件: sys/types.h、sys/socket.h
功能: 将socket套接字 和 address结构体绑定, 以网络通信为例, 因为结构体中有IP地址和端口号port, 所以就是实现了IP地址和端口号的绑定.
参数:
- int sockfd表示之前使用socket()返回的文件描述符
- sockfd, const struct sockaddr * addr 介绍过了;
- socklen_t addrlen表示sockaddr结构体的大小, 单位是字节.
返回值: 成功返回0, 失败返回-1并设置错误码errno
接下来三个接口暂时用不到, 之后再介绍.
UDP单向网络编程
我们先设计一个简单的通信场景, 客户端向服务器发送消息, 服务器打印出客户端发送的数据内容, 并给客户端回复一个回复.
udp_echo_server
服务端
udp_echo_server.hpp
1.设计类
由于当前服务器只有一个, 所以设置成一个不能被拷贝的类, 并为当前服务器存储其socket_id, IP 和Port:
#pragma onceclass NotCopable
{
public:NotCopable(){}
private:NotCopable(const NotCopable&) = delete;NotCopable& operator=(const NotCopable&) = delete;
};
#include "NotCopable.hpp"#include <string>
#include <iostream>const std::string default_ip = "0.0.0.0";
const uint16_t default_port = 8888;
const int default_num = 1024;class UdpEchoServer : NotCopable
{
public:UdpEchoServer(std::string ip = default_ip, uint16_t port = default_port):_port(port)_ip(ip){}void Init(){}void Start(){}~UdpEchoServer(){}private:std::string _ip; uint16_t _port;int _socket_id;
};
2. 初始化(Init)
然后完成初始化的工作:
#include "NotCopable.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <string>
#include <iostream>const std::string default_ip = "0.0.0.0";
const uint16_t default_port = 8888;
const int default_num = 1024;class UdpEchoServer : NotCopable
{
public:UdpEchoServer(std::string ip = default_ip, uint16_t port = default_port):_port(port)_ip(ip){}void Init(){//1. 创建socket_socket_id = socket(AF_INET, SOCK_DGRAM, 0);if(_socket_id < 0){lg.LogMessage(Fatal, "socket errr, ", errno, ": ", strerror(errno));exit(Socket_Err);}lg.LogMessage(Info, "socket create success");//2. 绑定ip+端口号struct sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);//绑定端口inet_pton(AF_INET, _ip.c_str(), &local.sin_addr);//绑定ipint bind_ret = bind(_socket_id, (sockaddr*)&local, sizeof(sockaddr_in));if(bind_ret < 0){lg.LogMessage(Fatal, "bind errr, ", errno, ": ", strerror(errno));exit(Bind_Err);}lg.LogMessage(Info, "socket bind success");}void Start(){}~UdpEchoServer(){}private:std::string _ip; uint16_t _port;int _socket_id;
};
a. 先用socket接口创建一个socket, 并输出对应的日志信息
b. 完成绑定工作, 我们需要自己定义一个socketaddr_in结构体:
- 可以利用bzero对其内容初始化为0, 其实和memset差不多
- 注意绑定端口时我们要用htons转换为网络序列, 因为这也是网络的数据.
- 绑定ip需要介绍新的接口了, 我们之前提到过, 我们用户更喜欢看点分十进制的ip地址, 而数据传输中是32位的整型传输的, 所以这之间的转换是不可避免的:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>//将"x.x.x.x"的ip转换为sockaddr_in里的in_addr
int inet_aton(const char *cp, struct in_addr *inp);//将点分十进制的 IPv4 地址转为in_addr内部的整型
in_addr_t inet_addr(const char *cp);//提取点分十进制的 IPv4 地址字符串的网络号
in_addr_t inet_network(const char *cp);//将 struct in_addr 结构中的网络字节序地址转换为点分十进制字符串
char *inet_ntoa(struct in_addr in);//通过网络号和主机号生成一个 IPv4 地址
struct in_addr inet_makeaddr(in_addr_t net, in_addr_t host);//提取 struct in_addr 中的主机号部分
in_addr_t inet_lnaof(struct in_addr in);//提取 struct in_addr 中的网络号部分
in_addr_t inet_netof(struct in_addr in);
还有一种可以转换IPV6地址的接口:
#include <arpa/inet.h>// convert IPv4 and IPv6 addresses from text to binary form
int inet_pton(int af, const char *src, void *dst);
c. 最后调用bind接口绑定即可, 也可以打印日志信息
其中exit用到了一些自定义的枚举常量, 保存在commond.h 用于保存错误码:
#pragma once
enum UdpError
{Usage_Err = 0,Socket_Err,Bind_Err,Recv_Err,Sendto_Err
};
3. 运行(Start)
运行阶段我们需要创建一个缓冲区去接收客户端发来的信息, 又需要新的接口 recvfrom 去进行数据的接收, 对应的也要求服务器向客户端发送一个回复信息, 需要接口 sendto.
recvfrom:
头文件: sys/types.h 和 sys/socket.h
功能: 从一个外部套接字中接收数据存在buf中, 并同时获取发送方的地址信息存储在src_addr中.
参数:
sockfd:
指定接收数据的套接字描述符(文件描述符). 必须是一个已绑定的 UDP 套接字
buf:
一个指向接收缓冲区的指针, 用于存储接收到的数据
len:
指定缓冲区buf
的长度, 即可接收的最大字节数
flags
用于设置接收选项, 常见值包括:
0
:默认行为(阻塞模式接收)MSG_PEEK
:查看数据但不将其从队列中移除。MSG_WAITALL
:等待所有指定字节被接收。
src_addr:
一个指向sockaddr
结构的指针, 用于存储发送方的地址信息. 如果不需要发送方地址信息, 可以设置为NULL.
addrlen:
一个指向socklen_t
类型的变量的指针, 用于存储src_addr
的大小. 调用前需设置为结构体大小, 调用后返回实际地址长度.
返回值: 成功返回接收到的字节数, 失败返回 -1,
同时设置 errno
sendto:
原型: ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
头文件: sys/types.h 和 sys/socket.h
功能: 函数用于向一个指定地址的进程发送数据.
参数:
sockfd:
指定发送数据的套接字描述符, 通常是一个 UDP 套接字
buf:
一个指向要发送的数据的指针
len:
要发送的数据的长度
flags
用于设置接收选项, 常见值包括:
0
:默认发送行为。MSG_DONTWAIT
:非阻塞模式发送。dest_addr
:
一个指向sockaddr
结构的指针, 用于指定目标地址和端口
addrlen:
dest_addr
的大小,ipv4为sizeof(struct sockaddr_in)
返回值: 成功返回发送到的字节数, 失败返回 -1,
同时设置 errno
提前准备好缓冲区, 然后recvfrom接收消息即可, 注意要给\0留一个位置; 接收完之后这里我把接收到的消息再用sendto返回给客户端:
#include "NotCopable.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <string>
#include <iostream>const std::string default_ip = "0.0.0.0";
const uint16_t default_port = 8888;
const int default_num = 1024;class UdpEchoServer : NotCopable
{
public:UdpEchoServer(std::string ip = default_ip, uint16_t port = default_port):_port(port)_ip(ip){}void Init(){}void Start(){char buffer[default_num];while(true){struct sockaddr client;socklen_t len = sizeof(client);ssize_t recvfrom_ret = recvfrom(_socket_id, buffer, sizeof(buffer)-1, 0, &client, &len);if(recvfrom_ret < 0){lg.LogMessage(Debug, "recvfrom errr, ", errno, ": ", strerror(errno), "\n");exit(Recv_Err);}//lg.LogMessage(Info, "recvfrom success");buffer[recvfrom_ret] = '\0';InetAddr ia(&client);std::cout << ia.Debug() << ":" << buffer << std::endl;//打印发送方地址信息//回复发送方ssize_t sendto_ret = sendto(_socket_id, buffer, sizeof(buffer), 0, &client, len);if(sendto_ret < 0){lg.LogMessage(Debug, "sendto errr, ", errno, ": ", strerror(errno), "\n");exit(Sendto_Err);}}~UdpEchoServer(){}private:std::string _ip; uint16_t _port;int _socket_id;
};
上面的 InetAddr ia(&client); 还没解释, 它主要是封装了一个套接字的 ip 和 port 用于输入打印:
#pragma once#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>class InetAddr
{
public:InetAddr(sockaddr* sock):_sock(sock){if(sock->sa_family == AF_INET){sockaddr_in* temp = (sockaddr_in*) sock;_port = ntohs(temp->sin_port);_ip = inet_ntoa(temp->sin_addr);}else{_port = 0;_ip = "0.0.0.0";}}uint16_t Port() const{return _port;}std::string Ip() const{return _ip;}std::string Debug(){std::string temp = "[";temp += (_ip + ":" + std::to_string(_port) + "]");return temp;}
private:std::string _ip;uint16_t _port;sockaddr* _sock;
};
4. 测试
main.cc:
#include "udp_echo_server.hpp"
#include "commond.h"
#include <iostream>
#include <memory>void Usage(char* proc)
{std::cout << "Usage: \n\t" << proc << "proc_name port" << std::endl;
}int main(int argc, char* argv[])
{if(argc != 3){Usage(argv[0]);return Usage_Err;}std::string ip = argv[1];uint16_t port = std::stoi(argv[2]);std::unique_ptr<UdpEchoServer> server = std::make_unique<UdpEchoServer>(ip, port);server->Init();server->Start();return 0;
}
输入要绑定的ip和端口号即可, 可以看到服务端正在等待客户端给它发消息:
输入指令 : netstat -aunp
-a: 显示所有连接和监听的端口
-p: 显示网络连接的进程信息
-u: 仅显示 UDP 连接
-n: 以数字形式显示地址和端口, 而不是将地址解析为主机名或端口解析为服务名称
可以看到本地环回ip:8888 , 由进程./udpserver建立:
ss指令也可以达到类似的效果, 使用和netstat类似.
客户端
最后来实现客户端即可, 步骤和服务端类似, 依然是:
1. 需要创建一个套接字socket
2. 然后bind绑定, 但是我们这里不需要显式手写bind, 客户端会在首次发送数据的时候OS会自动的随机进行bind, 而不是手写与固定的ip和port绑定死.
所以我们可以明确:
a. server端的端口号一定是众所周知的, 且不可被改变的.
比如一些服务: HTTP、FTP、SMTP 等, 都对应一个固定端口号(例如 HTTP 为 80, HTTPS 为 443). 客户端通过固定的端口号, 客户端可以通过预定义的方式找到对应的服务, 避免混乱.
b. 而client也需要port, 但是是随机的端口号. 为什么?
因为客户端可能会非常多, 为了解决多客户端并发通信的问题, 随机分配保证了每个客户端连接的 <客户端IP, 客户端端口> 都是唯一的, 避免一个key映射多个value.
3. 然后向服务端发送数据即可, 随后接收服务端发来的回复信息:
#include <string>
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "commond.h"void Usage(const std::string& proc)
{std::cout << "Usage " << proc << ": server_ip server_port\n";
}int main(int argc, char* argv[])
{if(argc != 3){Usage(argv[0]);return Usage_Err;}std::string server_ip = argv[1];std::uint16_t server_port = std::stoi(argv[2]);//1. 建立服务int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);if(socket_fd < 0){std::cerr << "socket fail: %d" << errno << strerror(errno) << std::endl;}std::cout << "socket create success: " << socket_fd << std::endl;//2. 自动bind//do nothing//3. 封装server的socketsockaddr_in server_socket;server_socket.sin_family = AF_INET;server_socket.sin_port = htons(server_port);inet_pton(AF_INET, server_ip.c_str(), &server_socket.sin_addr);while(true){std::string content;std::cout << "Please enter content: ";std::getline(std::cin, content);ssize_t sendto_ret = sendto(socket_fd, content.c_str(), content.size(), 0, (sockaddr*)&server_socket, sizeof(server_socket));if(sendto_ret > 0){char buffer[1024];sockaddr_in socket;socklen_t len = sizeof(socket);ssize_t recvfrom_ret = recvfrom(socket_fd, buffer, sizeof(buffer)-1, 0, (sockaddr*)&socket, &len);if(recvfrom_ret > 0){buffer[recvfrom_ret] = '\0';std::cout << "server respond: " << buffer << std::endl; }else{std::cerr << "recvfrom fail: %d" << errno << strerror(errno) << std::endl;}}else{std::cerr << "sendto fail: %d" << errno << strerror(errno) << std::endl;}}close(socket_fd);return 0;
}
综合测试
先来进行本地的测试, 客户端向本地127.0.0.1发送数据:
如图, 左侧是服务端, 右侧是客户端. 客户端向服务端发送的数据:
- 服务端能接收 客户端的[ip:port]和消息的内容,
- 客户端也能收到服务端的回复内容
用netstat查看一下连接状态, 发现客户端确实自动被分配了一个端口号54066 :
补充问题:
我们把服务器的ip地址绑定为127.0.0.1, 只能进行本地的通信, 进行代码的测试. 那我们如果绑定到一个固定的公网ip上, 想让其他主机也访问服务器呢?
当我想绑定到我云服务器固定的IP时, 它会报错. 因为云服务器的公网 IP 通常是通过 NAT 或负载均衡器(LB)映射到云服务器的内网 IP, 而不是直接分配到服务器的网卡上. 也就是说, 云服务器的网卡通常只有内网 IP, 而公网 IP 并不是直接绑定到该网卡的物理接口上. 因此, Cannot assign requested address 是
因为该公网 IP 不直接存在于服务器的网络接口上. 但是如果我们使用的是一个真实的Linux环境, 就可以bind其ip.
所以结论是: 其实并不推荐给服务器bind固定的ip, 这样做的一个弊端是: 未来此服务器只能接受到向固定ip发送的消息, 假如当前机器有多个网卡 iA ipB ipC.., 但是只bind了ipA, 我们只能收到发送给ipA的报文, 对于发送到此机器的其它ip的报文都无法收到.
所以更推荐bind任意ip的方式, 把ip绑定到 INADDR_ANY (0.0.0.0), 因此我们服务器只需要指定特定port, 往后发送给服务端的数据, 只要发送的目的端口是8888, 且ip是此机器上的ip, 都能被接收到, 实现了ip的动态绑定. 这样, 服务器就会监听所有可用的网络接口上的指定端口, 无论是内网 IP、公网 IP 还是其他虚拟接口上的 IP.
所以代码中server里的_ip成员其实根本不用维护, 直接绑定任意ip即可.
测试一下, 此时启动了一个客户端1(本地), 一个客户端2(linux虚拟机). 服务器都可以接收到客户端的信息.
注意: 云服务器需要开放对应的UDP端口, 否则无法外部通信.
windows客户端测试
windows端的socket接口和linux端的差别很小, 基本相同, 明白了linux的socket API, 基本就可以在windows写一个类似的客户端, 但还是有一点差别:
1. windows下需要包含 winsock2.h
和 ws2tcpip.h
头文件, 且Windows 使用 ws2_32.lib
库来链接 Winsock 相关的函数, 需要添加 #pragma comment(lib, "ws2_32.lib")
语句帮助自动链接该库
2. 需要先使用 WSAStartup()
初始化 Winsock 库, 在程序结束时需要调用 WSACleanup()
来清理资源。
//初始化winsocket库
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
//清理
WSACleanup();
3. SOCKET
是 Windows 中的特定类型, 是一个无符号整数
SOCKET fd = socket(AF_INET, SOCK_DGRAM, 0);
4. Windows 上, 使用 closesocket()
来关闭套接字.
#define _CRT_SECURE_NO_WARNINGS
#include <winsock2.h>
#include <ws2tcpip.h>
#include <iostream>
#include <string>#pragma warning(disable: 4996)
#pragma comment(lib, "ws2_32.lib")void Usage(char* s)
{std::cout << s << ": server_ip server_port" << std::endl;
}int main(int argc, char* argv[])
{if (argc != 3){Usage(argv[0]);exit(-1);}//初始化winsocket库WSADATA wsaData;WSAStartup(MAKEWORD(2, 2), &wsaData);//1.创建socketSOCKET fd = socket(AF_INET, SOCK_DGRAM, 0);if (fd < 0){std::cerr << "sock fail: " << WSAGetLastError() << std::endl;}//2.bind//自动bindsockaddr_in serverAddr;memset(&serverAddr, 0, sizeof(serverAddr));serverAddr.sin_family = AF_INET;serverAddr.sin_addr.s_addr = inet_addr(argv[1]);serverAddr.sin_port = htons(std::stoi(argv[2]));std::string msg;char buffer[1024];while (true){std::cout << "Please enter: ";std::getline(std::cin, msg);int sendto_ret = sendto(fd, msg.c_str(), msg.size(), 0, (sockaddr*)&serverAddr, sizeof(serverAddr));if (sendto_ret < 0){std::cerr << "send fail: " << WSAGetLastError() << std::endl;}sockaddr_in originAddr;int len = sizeof(originAddr);int recvfrom_ret = recvfrom(fd, buffer, sizeof(buffer), 0, (sockaddr*)&originAddr, &len);if (recvfrom_ret < 0){std::cerr << "recv fail: " << WSAGetLastError() << std::endl;}buffer[recvfrom_ret] = '\0';std::cout << "[" << inet_ntoa(originAddr.sin_addr) << ":" << ntohs(originAddr.sin_port) <<"]: " << buffer << std::endl;}closesocket(fd);WSACleanup();
}
Windows客户端:
Linux服务端:
由于windows下是GBK编码不是UTF-8, 所以数据传递过去会显示乱码.
udp_command_server
我们已经实现了最基本的通信, 现在想要在此基础上添加一些功能, 比如: 客户端端向服务端发送一条指令, 服务端在本地执行这条指令并把结果返回给客户端.
1. 所以我们需要在客户端添加一个函数对象成员, 其类型自定义为string(*)(string)类型, 用于接受一个给定字符串, 然后结果以字符串形式返回:
using command_func_t = std::function<std::string(const std::string&)>;
#pragma once
#include <string>
#include <iostream>
#include <cerrno>
#include <cstring>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include "Log.hpp"
#include "NotCopable.hpp"
#include "commond.h"
#include "InetAddr.hpp"const uint16_t default_port = 8888;
const int default_num = 1024;class UdpEchoServer : NotCopable
{using command_func_t = std::function<std::string(const std::string&)>;public:UdpEchoServer(command_func_t fun, uint16_t port = default_port):_port(port),_func(fun){}void Init(){}void Start(){}~UdpEchoServer(){}
private:uint16_t _port;int _socket_id;command_func_t _func;
};
2. 主要需要修改一下Start的部分, 接收到客户端的消息之后, 不是无脑把消息打印出来, 而是执行一次函数功能后再发送结果:
void Start(){char buffer[default_num];while(true){//从客户端接受信息struct sockaddr_in client;socklen_t len = sizeof(client);ssize_t recvfrom_ret = recvfrom(_socket_id, buffer, sizeof(buffer)-1, 0, (sockaddr*)&client, &len);if(recvfrom_ret < 0){lg.LogMessage(Debug, "recvfrom errr, ", errno, ": ", strerror(errno), "\n");exit(Recv_Err);}buffer[recvfrom_ret] = '\0';//客户端提示信息InetAddr ia(client);std::cout << ia.Debug() << ":" << buffer << std::endl;//执行服务端功能std::string msg = _func(buffer);ssize_t sendto_ret = sendto(_socket_id, msg.c_str(), msg.size(), 0, (sockaddr*)&client, len);if(sendto_ret < 0){lg.LogMessage(Debug, "sendto errr, ", errno, ": ", strerror(errno), "\n");exit(Sendto_Err);}}
3. main.cc部分也要修改, 需要自定义"指令结果"->"字符串"的函数, 并作为参数传递给udpserver的构造函数.
其中用到了popen函数:
popen 函数首先创建一个管道, 然后父进程fork创建一个子进程, 子进程通过进程替换执行给定的外部命令(如 ls
、grep
、cat
等).
- 如果父进程指定
mode
为"r",
popen
将子进程的标准输出(stdout)重定向到管道的写端. 这样, 父进程可以从管道的读取端读取(fgets, fread等)子进程的输出. - 如果使用
"w"
模式, 父进程可以通过管道向子进程传送数据. 它可以使用fputs,
fprintf
等函数将数据写入管道.
参数:
command: 是一个字符串, 表示要执行的命令. 通常是一个 shell 命令, 或者任何可以在命令行中执行的程序. 如果你希望执行一个 shell 命令, 可以直接传递给它.
type: 是一个字符串, 指定文件流的打开方式, 取值可以是:
- "r": 表示以只读方式打开管道 (即从子进程读取输出)
- "w": 表示以写入方式打开管道 (即将数据传递给子进程的标准输入)
返回值:
- 成功时,
popen
返回一个FILE *
类型的文件指针, 指向打开的管道. 你可以通过该指针使用标准的文件操作函数 (如fgets
,fputc
,fputs
,fprintf
,fread
,fwrite
等) 来与子进程进行通信。- 如果失败, 返回
NULL.
可以通过errno
来查看失败的具体原因.
#include "udp_command_server.hpp"
#include "commond.h"
#include <iostream>
#include <memory>void Usage(char* proc)
{std::cout << "Usage: \n\t" << proc << "proc_name port" << std::endl;
}std::string CommandToResult(const std::string& command)
{FILE* fp = popen(command.c_str(), "r");if(fp == nullptr){return "Execute error, create file fail\n";}std::string response;char buffer[1024];while(true){char *s = fgets(buffer, sizeof(buffer), fp);if(!s) break;else response += buffer;}pclose(fp);return response.empty() ? "erro command" : response;
} int main(int argc, char* argv[])
{if(argc != 2){Usage(argv[0]);return Usage_Err;}uint16_t port = std::stoi(argv[1]);std::unique_ptr<UdpEchoServer> server = std::make_unique<UdpEchoServer>(CommandToResult, port);server->Init();server->Start();return 0;
}
linux服务端:
windows客户端:
UDP双向网络编程
现在来实现一个简单的聊天室, 建立一个类似群聊的功能, 多个客户端可以互相接受和发送消息.
简单的聊天室
思路: 肯定会有很多使用聊天室的客户端, 服务端一旦接受到消息, 就可以保存用户的地址信息, 然后服务器把数据通过保存的地址转发给每个用户.
之前我们的服务器都是单线程的, 而udp/tcp都是支持全双工通信的, 所以可以支持读写并发执行, 所以可以考虑使用多线程. 因此让主线程专门从客户端读取数据(recvfrom), 由其它的线程去负责发数据(sendto).
服务端
server的初始化工作没有太大改动, 只需要启动线程池即可:
服务器执行任务部分做了很大的改动, 之前我们是主线程接受到数据之后还负责转发数据给客户端, 现在不需要了, 主线程专门负责接受数据, 此外还有一些额外的工作:
1. 我们服务端需要维护在线用户的地址, 设计一个AddUser函数把用户添加到一个容器里, 这里先选择vector, 注意:
- a. 不要重复添加
- b. 访问全局变量记得加锁
AddUser:
2. 我们需要把发送数据的任务(称之为路由任务Route), 封装为一个函数对象传递给线程池由线程负责执行发数据的任务
Route 负责把数据转发给所有用户.
注意: 访问临界资源_online_users要加锁,
3. 其它工作: 声明任务的类型, 初始化和销毁锁.
using command_func_t = std::function<void()>;
public:
UdpChatServer(uint16_t port = default_port)
:_port(port)
{pthread_mutex_init(&_user_mutex, nullptr);
}
~UdpChatServer()
{pthread_mutex_destroy(&_user_mutex);
}
客户端
客户端的改动就比较大了, 因为我们不再是单纯的一个人和服务器进行对话, 我们在发送消息的同时也要能接收信息, 而之前单线程的客户端无法并发地执行收发两个任务, 是先完成send再recv的, 如果我不send, 我将无法收到服务器的消息.
比如这个场景: 当我不想发言, 一直阻塞在输入框时, 我也需要能看到其它客户端发送来的消息, 而不是串行地读写: 只有我发送了一条消息后, 才能接收到服务器发来的消息.
我们把收消息和发消息封装为两个函数, 启动两个线程分别去执行:
class ThreadData
{
public:ThreadData(int sockfd, sockaddr_in server): _serveraddr(server), _sockfd(sockfd){}public:int _sockfd;InetAddr _serveraddr;
};int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);return Usage_Err;}std::string server_ip = argv[1];std::uint16_t server_port = std::stoi(argv[2]);// 1. 建立服务int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);if (socket_fd < 0){std::cerr << "socket fail: %d" << errno << strerror(errno) << std::endl;}std::cout << "socket create success: " << socket_fd << std::endl;// 2. 自动bind// do nothing// 3. 封装server的socketsockaddr_in server_socket;server_socket.sin_family = AF_INET;server_socket.sin_port = htons(server_port);inet_pton(AF_INET, server_ip.c_str(), &server_socket.sin_addr);ThreadData td(socket_fd, server_socket);Thread<ThreadData> send_thread("thread_1", SendTo, td);Thread<ThreadData> recv_thread("thread_2", RecvFrom, td);send_thread.Start();recv_thread.Start();send_thread.Join();recv_thread.Join();return 0;
}
注意看这里特意向错误流中输出, 是为了等会能更好的显示内容:
测试一下:
主要观察右侧客户端的内容, 左侧是服务器会打印一些日志信息.
1. 本地客户端先发送了一条消息, 此时虚拟机还没输入消息, 所以服务器没有添加该用户地址, 所以右侧显示框没有内容:
2. 虚拟机发送消息后, 两端正式可以开始互相交流了 , 一方发送消息, 另一方可以同步的显示内容:
相关文章:
Linux之socket编程(一)
前言 网络通信的目的 我们已经大致了解了网络通信的过程: 如果主机A想发送数据给主机B, 就需要不断地对本层的协议数据单元(PDU)封装, 然后经过交换设备的转发发送给目的主机, 最终解封装获取数据. 那么网络传输的意义只是将数据由一台主机发送到另一台主机吗? …...
STM32之SDIO通讯接口和SD卡(九)
STM32F407 系列文章 - SDIO-To-SD Card(九) 目录 前言 一、SDIO接口 二、SD卡 三、实现程序 1.SD卡结构体参数说明 2.头文件定义 3.函数sd_init() 4.函数HAL_SD_MspInit() 5.函数get_sd_card_info() 6.函数get_sd_card_state() 7.函数sd_read…...
【Rust在WASM中实现pdf文件的生成】
Rust在WASM中实现pdf文件的生成 前言概念和依赖问题描述分步实现pdf转Blob生成URL两种方式利用localstorage传递参数处理图片Vec<u8>到pdf格式的Vec<u8>使用rust创建iframe显示pdf的Blob最后 前言 实现了一个通用的前端jpg转pdf的wasm,因为动态响应框架无法直接打…...
蓝桥杯真题——砍竹子(C语言)
问题描述 这天, 小明在砍竹子, 他面前有 n 棵竹子排成一排, 一开始第 ii 棵竹子的 高度为 .他觉得一棵一棵砍太慢了, 决定使用魔法来砍竹子。魔法可以对连续的一 段相同高度的竹子使用, 假设这一段竹子的高度为 H, 那么用一次魔法可以,把这一段竹子的高度都变为, 其中 [x]表…...
技术栈4:Docker入门 Linux入门指令
目录 1.Linux系统目录结构 2.处理目录的常用命令 3.Docker概述 4.Docker历史 5.Docker基本组成 6.Docker底层原理 7.Docker修改镜像源 8.Docker基本命令 在学习docker之前我们先要熟悉Linux系统,推荐阅读:Linux笔记(狂神说࿰…...
项目开发之Docker
文章目录 基础核心概念常用命令 实操1 windows11 docker mysql2 docker部署 xxljob3 container间调用异常问题4 部署mysql数据库5 docker desktop unexpected wsl error 基础 核心概念 其中的三个核心概念:dockerfile image/镜像 container/容器 image:…...
状态模式的理解和实践
在软件开发中,我们经常遇到需要根据对象的不同状态执行不同行为的情况。如果直接将这些状态判断和行为逻辑写在同一个类中,会导致该类变得臃肿且难以维护。为了解决这个问题,状态模式(State Pattern)应运而生。状态模式…...
正点原子imx6ull配置MQTT客户端上传数据到Ubuntu MQTT服务器
目录 使用QT自带的MQTT模块部署客户端创建一个class专门用于MQTT客户端通讯使用QT在ui界面上生成按钮在Windows上订阅相应主题测试在imx6ull上订阅Windows发布的消息 在上一篇中介绍了在Ubuntu22.04的Docker中部署MQTT服务器,然后在window上测试订阅和发布ÿ…...
【联表查询】.NET开源 ORM 框架 SqlSugar 系列
.NET开源 ORM 框架 SqlSugar 系列 【开篇】.NET开源 ORM 框架 SqlSugar 系列【入门必看】.NET开源 ORM 框架 SqlSugar 系列【实体配置】.NET开源 ORM 框架 SqlSugar 系列【Db First】.NET开源 ORM 框架 SqlSugar 系列【Code First】.NET开源 ORM 框架 SqlSugar 系列【数据事务…...
CAN接口设计
CAN总线的拓扑结构 CAN总线的拓扑结构有点像485总线,都是差分的传输方式,总线上都可以支持多个设备,端接匹配电阻都是120Ω。 485和CAN通信方面最大的区别:网络特性。485是一主多从的通讯方式,CAN是多主通讯,多个设备都可以做主机。那多个设备都相要控制总线呢?…...
基于遗传优化SVM的电机参数预测matlab仿真
目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 4.1 数据收集与预处理 4.2模型构建与训练 5.算法完整程序工程 1.算法运行效果图预览 (完整程序运行后无水印) 输入:电机结构参数x1 x2 x3 x4 x5(分别是铁心高度 铁心厚度 绕组…...
C++设计模式之代理模式
动机 在面向对象系统中,有些对象由于某种原因(比如对象创建的开销很大,或者某些操作需要安全控制,或者需要进程外的访问等),直接访问会给使用者、或者系统结构带来很多麻烦。 如何在不失去透明操作对象的…...
多线程JUC 第2季 控制线程的执行顺序,依次,交替,同时
一 场景1 交替执行 1.1 方案1使用completablefuture public class TestA {public static void main(String[] args) {for(int k0;k<10;k) {CompletableFuture<Void> t1 CompletableFuture.runAsync(() -> {System.out.println("A: >" Thread.curr…...
数据分析(一): 掌握STDF 掌握金钥匙-码农切入半导体的捷径
中国的半导体行业必然崛起!看清这个大势,就会有很多机会。 今天,我们一起来了解一下半导体行业的一朵金花:STDF。 实际上这只是一种文件格式,但是当你熟练掌握解析这种文件的时候,你就已经打开在这个基础…...
NPM镜像详解
NPM镜像详解 什么是NPM镜像 NPM镜像(NPM Mirror)是一个完整的NPM包的副本服务器。由于npm的官方registry服务器部署在国外,国内访问可能会比较慢,因此使用镜像可以加快包的下载速度。 常用的NPM镜像源 npm官方镜像 https://reg…...
【springboot】 多数据源实现
文章目录 1. 引言:多数据源的必要性和应用场景**为什么需要多数据源?****应用场景** 2. Spring Boot中的数据源配置2.1 默认数据源配置简介2.2 如何在Spring Boot中配置多个数据源 3. 整合MyBatis与多数据源**配置MyBatis使用多数据源****Mapper接口的数…...
Zephyr 入门-设备树与设备驱动模型
学习链接:https://www.bilibili.com/video/BV1L94y1F7qS/?spm_id_from333.337.search-card.all.click&vd_source031c58084cf824f3b16987292f60ed3c 讲解清晰,逻辑清楚。 1. 设备树概述(语法,如何配置硬件,c代码如…...
css实现圆周运动效果
在CSS中可以通过 keyframes 动画 和 transform 属性实现元素的圆周运动。以下是一个示例代码: 示例代码 <!DOCTYPE html> <html lang"en"> <head> <meta charset"UTF-8"> <meta name"viewport" content…...
乐鑫科技嵌入式面试题及参考答案(3万字长文)
嵌入式开发为什么用 C 语言,而不用 C++ 语言? 在嵌入式开发中,C 语言被广泛使用而 C++ 相对少用有以下一些原因。 首先,C 语言具有更高的效率。嵌入式系统通常资源受限,包括处理器速度、内存容量等。C 语言的代码生成效率高,能够生成紧凑的机器码,占用较少的内存空间和处…...
这就是IoC容器
IoC(Inversion of Control,控制反转),也叫依赖注入(Dependency Injection),是一种决定容器如何装配组件的模式。使用 Spring 来实现 IoC,意味着将设计好的对象交给 Spring 容器控制,而不是直接在对象内部控制。控制反转不能很好地描述这个模式,依赖注入却能更好地描述…...
KVM OVS双网卡配置trunk模式
一、宿主机配置 1. 确保必要的软件包已安装 确保宿主机上已安装 Open vSwitch 和 VLAN 工具。如果尚未安装,可以使用以下命令进行安装: yum install openvswitch-switch vlan 2. 配置现有 OVS 桥接 br0 假设已有一个 OVS 桥接接口 br0,并…...
氢能源车和电动车,谁将成为未来?
很多人都觉得氢能源是未来的终极绿色能源,因为氢气燃烧后只产生水,听起来是不是很环保?但这只是从化学能的角度来看。实际上,氢能源汽车还有很多问题需要解决。氢气的制作成本高得吓人 目前,制作氢气最理想的方法是电…...
CTF-PWN: WEB_and_PWN [第一届“吾杯”网络安全技能大赛 Calculator] 赛后学习(不会)
附件 calculate.html <!DOCTYPE html> <html lang"en"> <head><!-- 设置字符编码为 UTF-8,支持多语言字符集 --><meta charset"UTF-8"><!-- 设置响应式视图,确保页面在不同设备上自适应显示 --&…...
解决Jupyter Notebook无法转化为Pdf的问题(基于Typora非常实用)
笔者在完成各项作业和做笔记时,经常用到jupyter notebook;其因为可以同时运行python并提供格式化的数字公式的输入方式,得到了广大用户的喜爱。 当我们想要将.ipynb文件导出为pdf时,有两种常用方法。 1.Ctrlp 2.通过File ->…...
矩阵转置
矩阵转置 C语言代码C 语言代码Java语言代码Python语言代码 💐The Begin💐点点关注,收藏不迷路💐 输入一个n行m列的矩阵A,输出它的转置 A T A^T AT。 输入 第一行包含两个整数n和m,表示矩阵A的行数和列数。…...
【阅读笔记】Three ways ChatGPT helps me in my academic writing
Three ways ChatGPT helps me in my academic writing 论文地址 关于GPT进行润色的文章,摘取了里面的提示词做个记录。 1. Polishing academic writing(学术润色) 模板:I’m writing a paper on [topic] for a leading [discip…...
python学习笔记15 python中的类
上一篇我们介绍了python中的库 ,学习了一些常见的内置库。详细内容可点击–>python学习笔记14 python中的库,常见的内置库(random、hashlib、json、时间、os) 这一篇我们来看一下python中的类 创建一个类 class 类的名称():de…...
3D数字化革新,探索博物馆的正确打开新方式!
3D数字化的发展,让博物馆也焕发新机,比如江苏省的“云上博物”,汇聚江苏全省博物馆展陈资源,采取线上展示和线下体验两种方式进行呈现的数字展览项目。在线上,用户可以通过H5或小程序进入“云上博物”数字展览空间&…...
金融数学在股市交易中的具体应用
### 1. 风险管理 - **VaR(在险价值)**: VaR是衡量投资组合潜在损失的指标。例如,如果一个投资组合的VaR为100万元,置信水平为95%,这意味着在未来的一个交易日内,有95%的可能性该投资组合的损失不会超过100…...
Springboot注解
什么是Spring Boot 注解 Spring Boot 注解是 Spring Boot 框架中的核心部分,它们用于简化配置和执行特定的编程任务。这些注解通常分为几个类别,包括用于 MVC 层的类注解、依赖注入注解、Web 开发常用注解、Spring Boot 常用注解、面向切面编程…...
CSS3 布局样式及其应用
深入探讨 CSS3 布局样式及其应用 引言 在现代网页设计中,CSS(层叠样式表)不仅是设计视觉样式的工具,也是布局的核心技术。CSS3引入了新的布局模型,其中Flexbox与Grid布局在满足复杂布局需求方面表现尤为出色。本文将…...
工业机器视觉-基于深度学习的水表表盘读数识别
字轮数字识别、指针读数识别(角度换算)、根据指针角度进行读数修正、根据最高位指针(x0.1)读数对字轮数字进行修正、得到最终读数。 基于深度学习的目标检测技术和OpenCV图像处理技术,可识别所有类型的表盘机械读数。...
kafka admin client 如何计算kafka发送速度
文章目录 方法 1:使用 AdminClient 获取消息数量示例代码:计算 Kafka 生产速度代码解释:解释:结果示例:方法 2:使用 Kafka JMX 监控JMX 指标: 总结: 要使用 Kafka Admin Client 来计…...
Ubuntu20.04 配置虚拟显示器和切回物理显示器
1、安装软件,用中软安装虚拟显示器软件 sudo apt-get install xserver-xorg-core-hwe-18.04 sudo apt-get install xserver-xorg-video-dummy2、添加配置文件 进入 /usr/share/X11/xorg.conf.d/ 文件夹下创建xorg.conf文件 # 创建xorg.conf文件 touch xorg.conf …...
【解决pycharm下site-packages文件标记为红色的问题】
怎么解决pycharm下site-packages文件标记为红色的问题 这是一个pycharm无法识别本地site-packages问题答案解释器设置路径如下: 这是一个pycharm无法识别本地site-packages问题 最近正在搭建一个python3requestsunittestHTMLTestRunner接口测试框架,发现…...
力扣-图论-2【算法学习day.52】
前言 ###我做这类文章一个重要的目的还是给正在学习的大家提供方向和记录学习过程(例如想要掌握基础用法,该刷哪些题?)我的解析也不会做的非常详细,只会提供思路和一些关键点,力扣上的大佬们的题解质量是非…...
项目-02-数学学院后台项目开发过程中的问题总结
目录 一、后台(pc端,vue2)1. dialog对话框被黑色蒙层盖住2. 将前端表格导出为word文档3. 在线查看、下载 .docx、.doc、.pdf文档 一、后台(pc端,vue2) 1. dialog对话框被黑色蒙层盖住 问题: d…...
数据结构-排序
目录 一、冒泡排序 二、选择排序 三、插入排序 四、希尔排序 五、堆排 六、快速排序 1、hoare: 2、挖坑法: 3、前后指针法: 4、快排非递归 七、归并排序 1、递归写法: 2、非递归写法: 八、计数排序 九、排…...
神经网络中常见的激活函数Sigmoid、Tanh和ReLU
激活函数在神经网络中起着至关重要的作用,它们决定了神经元的输出是否应该被激活以及如何非线性地转换输入信号。不同的激活函数适用于不同的场景,选择合适的激活函数可以显著影响模型的性能和训练效率。以下是三种常见的激活函数:Sigmoid、T…...
[报错] Error: PostCSS plugin autoprefixer requires PostCSS 8 问题解决办法
报错:Error: PostCSS plugin autoprefixer requires PostCSS 8 原因:autoprefixer版本过高 解决方案: 降低autoprefixer版本 执行:npm i postcss-loader autoprefixer8.0.0 参考: Error: PostCSS plugin autoprefix…...
多线程---创建及方法
*线程创建的方式: 1.继承Thread类,重写run方法。 2.实现Runnable接口,重写run方法。 实际这两个run方法都重写的是Runnable中的run方法 简化方法: 1.匿名内部类创建Thread 子类对象 Thread thread new Thread(){Overridepub…...
C++设计模式之单例模式
动机 在软件系统中,经常有一些特殊的类,必须保证它们在系统中只存在一个实例,才能确保它们的逻辑正确性,以及良好的效率。 如何绕过常规的构造器,提供一种机制来保证一个类只有一个实例? 这应该是类设计…...
给你一个整数n, 判断它是否是素数。注意1不是素数。:JAVA
链接:登录—专业IT笔试面试备考平台_牛客网 来源:牛客网 题目描述 给你一个整数n, 判断它是否是素数。注意1不是素数。 输入描述: 第一行输入一个整数T,表示需要判断的数的个数接下来T行每行一个整数n,表示需要判断的数。1<n<1e5,1<T<…...
Java项目实战II基于微信小程序的电子竞技信息交流平台的设计与实现(开发文档+数据库+源码)
目录 一、前言 二、技术介绍 三、系统实现 四、核心代码 五、源码获取 全栈码农以及毕业设计实战开发,CSDN平台Java领域新星创作者,专注于大学生项目实战开发、讲解和毕业答疑辅导。获取源码联系方式请查看文末 一、前言 随着互联网技术的飞速发展…...
temu登录接口逆向分析(含Py纯算)
文章目录 1. 写在前面2. 接口分析3. 算法还原 【🏠作者主页】:吴秋霖 【💼作者介绍】:擅长爬虫与JS加密逆向分析!Python领域优质创作者、CSDN博客专家、阿里云博客专家、华为云享专家。一路走来长期坚守并致力于Python…...
Java ArrayList 详解
Java ArrayList 详解 ArrayList 是 Java 集合框架(Collection Framework)中最常用的类之一,是一种基于动态数组的数据结构,属于 List 接口的实现类。它允许存储重复的元素,有序,支持随机访问,且…...
CTF之密码学(仓颉编码)
一、仓颉码(用于建立中文索引) 定义与目标: 仓颉码是为了建立中文的索引观念而设计的一种编码方式。其主要目标是方便对中文资料或程式进行索引功能的处理。 工作原理: 仓颉码的索引以ASCII的字符码为基准,但在内部会转…...
OpenCV的圆形检测HoughCircles
HoughCircles 函数是 OpenCV 库中用于在灰度图像中检测圆的函数,它基于霍夫变换(Hough Transform)的一种变体——梯度霍夫变换(HOUGH_GRADIENT)函数原型如下: void HoughCircles( InputArray image, OutputArray circles,int method, double dp, double minDist,double …...
Latex 英文双栏排版时,摘要和引言之间的距离
目标:解决这个间隔 打开“cas-common.sty" 搜索“\NewDocumentCommand \dashrule" 本来是两个 m m 变成 m m m然后是增加下面这个代码,其他地方不动 \skip_vertical:n {#4} 搜索”MaketitleBox“和“LongMaketitleBox ” 本来的代码应该…...
AD学习笔记·空白工程的创建
编写不易,禁止搬运,仅供学习,感谢理解 序言 本文参考B站,凡亿教育,连接放在最后。 创建工程文件 在使用AD这个软件的电路板设计中,有很多的地方跟嘉立创eda还是有不一样的地方,其中一个地方就…...