Linux 网络编程之UDP套接字

前言

前面我们对网络的发展,网络的协议、网路传输的流程做了介绍,最后,我们还介绍了 IP端口号ip + port 叫做 套接字 socket, 本期我们就来介绍UDP套接字编程!

目录

1、预备知识

1.1 传输层协议: TCP/UDP

1.2 网络字节序

1.3 socket 接口

1.4 sockaddr

2、echo_server

2.1 核心功能分析

2.2 服务端设计

2.2.1 创建套接字

2.2.2 绑定ip和端口号

2.2.3 启动服务

2.2.4 服务端的全部源码

2.3 客户端设计

2.3.1 初始化客户端

2.3.2 启动客户端

2.3.3 客户端全部源码

2.3.4 解决遗留问题

2.4 优化和验证

 3、简单的英译汉

3.1 实现Dict 类

3.2 完成服务端的修改

3.3 主函数完善

4、多人聊天室

4.1 服务端改造

4.1.1 Route类实现

4.1.2 服务端主函数修改

4.2 客户端改造

4.2.1 客户端的主函数改造

5、地址函数补充

5.1 字符串转整数

5.2 整数转字符串


1、预备知识

1.1 传输层协议: TCP/UDP

前面我们结合系统了解了网络协议栈是基于OS的,而我们知道传输层是属于内核的,那么要通过网络协议栈进行通信,必定要调用系统调用!

简单认识 TCP

TCP (Transmission Control Protocol) 传输控制协议,它是一个传输层协议特点有:

• 面向连接

• 可靠传输

• 面向字节流

这里简单的了解一下就可以,后面会详细介绍它的工作原理和机制的!例如:常考的三次握手四次挥手

简单认识 UDP

UDP User Datagram Protocol用户数据报协议,它也是一个传输层协议特点有:

• 无连接

• 不可靠传输

• 面向数据报

关于 可靠性
TCP 的可靠传输并不意味着它可以将数据百分百递达,而是说它在数据传输过程中,如果发生了传输失败的情况,它会通过自己独特的机制,重新发送数据,确保对端百分百能收到数据;至于 UDP 就不一样,数据发出后,如果失败了,也不会进行重传,好在 UDP 面向数据报,并且没有很多复杂的机制,所以传输速度很快

总结起来就是:TCP 用于对数据传输要求较高的领域,比如金融交易、网页请求、文件传输等,至于 UDP 可以用于短视频、直播、即时通讯等对传输速度要求较高的领域

如果不知道该使用哪种协议,优先考虑 TCP,如果对传输速度又要求,可以选择 UDP

1.2 网络字节序

我们知道,在计算机系统中,多字节数据的存储方式有大端(Big-Endian)和小端(Little-Endian)之分。这种区别不仅存在于内存中,还影响到磁盘文件和网络数据流的字节序。

内存中的字节序

大端字节序:高字节(MSB)存储在低的内存地址上。

小端字节序:低字节(LSB)存储在低的内存地址上。

网络数据流的字节序:

发送过程:发送主机按照内存地址从低到高的顺序将数据发出。即,先发送的数据存储在发送缓冲区的低地址,后发送的数据存储在高地址。

接收过程:接收主机从网络上接收到的字节依次保存在接收缓冲区中,也是按照内存地址从低到高的顺序。

• 因此,网络数据流的地址是:主机先发出的数据是低地址,主机后发出的数据室高地址

TCP/IP 协议为了确保不同架构的主机之间能够正确解析数据,规定网络数据流采用大端字节序,即 低地址,高字节

• 所以,不管当前主机是大端还是小端,网络收发数据都必须要使用大端字节序

关于大小端我们在《C语言数据在内存中的存储》中详细的介绍过!

那现在的问题是:如果我当前的机子是大端机器那还好。但是如果是小端呢?我是不是还得自己手动的转换?

OK,你想到的人家设计网络的人也是想到了,为了让网络程序具有可移植性,使用同样的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 表示 hostn 表示 network l 表示 32 位长整数,s 表示 16 位 短整数。

如果主机是大端机器那就不做任何转换直接返回即可,如果是小端机器,转为大端然后返回!

1.3 socket 接口

socket 套接字提供了下面这一批常用接口,用于实现网络通信

#include <sys/types.h>
#include <sys/socket.h>// 创建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 中,频繁出现了一个结构体类型 sockaddr该结构体支持网络通信,也支持本地通信

socket 套接字就是用于描述 sockaddr 结构体的字段,复用了文件描述符的解决方案

1.4 sockaddr

socket  这套网络通信标准隶属于 POSIX  通信标准,该标准设计的初衷是为了实现 可移植性,程序可以直接在使用该标准的不同机器中运行,但是有的是网络通信,有的是本地通信, socket 套接字为了能同时兼顾这两种通信方式, 提供了 sockaddr 结构体

由  sockaddr 结构体衍生出了两个不同的结构体:sockaddr_in 网络套接字sockaddr_un 域间套接字 ,前者是网络通信, 后者是本地通信

• 后面可以提取 sockaddr 的头部的 16 位地址类型判断是网络通信,还是本地通信

• 在进行网络通信时,需要提供 ip 地址, 端口号 等,而本地通信时,只需要提供一个路径名即可,通过读写同一个文件的形式进行通信(类似于命名管道

•  socket 提供的接口参数为 sockaddr*类型,我们既可以传入 &sockaddr_in 进行网络通信,也可以传入 &sockaddr_un 进行本地通信,传参时将参数进行强制类型转换即可,这是使用 C语言 实现 多态 的典型做法,确保该标准的通用性

为什么不将参数设置为 void* 
因为在该标准设计时,C语言还不支持 void* 这种类型,为了确保向前兼容性,即便后续支持后也不能进行修改了

关于 socketaddr_in 结构和上述 socket API 的更多详细信息放到后面写代码时再细谈


2、echo_server

接下来我们将实现三个基于UDP的网络程序,分别是:字符串回响简易的汉译英多人聊天室

2.1 核心功能分析

分别实现客户端和服务端,客户端向服务端发送请求,服务端接收到请求之后,直接回响给客户端,和我们之前介绍的 echo 指令 类似

所以,我们还是基于上面的先来搭建一个框架出来:

UdpServer.hpp

#pragma once#include <iostream>class UdpServer
{
public:// 构造UdpServer(){}// 初始化服务器void InitServer(){}// 启动服务器void Start(){}// 析构~UdpServer(){}
private://属性字段
};

UdpServerMain.cc

无论在服务端,还是在客户端Main函数都将采用智能指针管理资源

#include "UdpServer.hpp"
#include <memory>int main()
{std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>();// C++14usvr->InitServer();usvr->Start();return 0;
}

UdpClient.hpp

#pragma once#include <iostream>class UdpClient
{
public:// 构造UdpClient(){}// 初始化客户端void UdpClient(){}// 启动客户端void Start(){}// 析构~UdpClient(){}
private:// 属性字段
};

UdpClientMain.cc

#include "UdpClient.hpp"
#include <memory>int main()
{std::unique_ptr<UdpClient> uclt = std::make_unique<UdpClient>();// C++14uclt->InitClient();uclt->Start();return 0;
}

Makefile

.PHONY: all
all : udpserver udpclientudpserver : UdpServerMain.ccg++ -o $@ $^ -std=c++14
udpclient : UdpClientMain.ccg++ -o $@ $^ -std=c++14.PHONY:clean
clean:rm -rf udpserver udpclient

2.2 服务端设计

服务端做的事情无非三个:

1、接受客户端的请求

2、处理客户端请求

3、响应给客户端

上面的三个操作,对应着的就是接收消息、处理消息、发送消息。如实现?当然是使用 socket 套接字接口喽!

2.2.1 创建套接字

创建套接字使用的是 socket 函数

#include <sys/types.h>
#include <sys/socket.h>// 创建套接字(TCP/UDP	服务器/客户端)
int socket(int domain, int type, int protocol);

参数

domain 创建套接字用于哪一种通信(网络/本地)

type 选择数据传输类型(流式/数据报)

protocol 选择协议类型(支持根据type自动推到,所以一般直接写0)

返回值

成功,返回一个文件描述符(套接字);失败,返回-1

我们这里因为是UDP协议实现的网络通信,所以,参数 domain 选择 AF_INET(基于IPv4标准),参数2 type 选择 SOCK_DGRAM(数据报传输),参数3 protocol 协议直接设置为 0,他会根据参数2自动推导

我们可以在服务端的初始化函数中,创建套接字!为了代码的可读性,我们在引入我们的日志,其次为了不让我们的服务端进行拷贝,我们可以把拷贝构造和赋值拷贝给禁用掉,也可以单独写一个类继承下来(建议),最后因为后面存在大量的判断退出的情况,我们把退出的码单独使用枚举放在一个Common.hpp中,后面谁用直接引用即可

nocopy.hpp

class nocopy
{
public:nocopy(){}~nocopy(){}nocopy(const nocopy&) = delete;const nocopy& operator=(const nocopy&) = delete;
};

这样写的好处就是,后面可以直接复用!

Common.hpp

#pragma onceenum
{SOCKET_ERROR = 1
};

目前公共的这里没有啥,只有一个 socket 的创建错误,后面了会加!

#pragma once#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>#include "nocopy.hpp"
#include "Common.hpp"
#include "Log.hpp"using namespace LogModule;class UdpServer : public nocopy
{
public:// 构造UdpServer(){}// 初始化服务器void InitServer(){// 创建 socket 套接字_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);// IPv4 面向数据报if(_sockfd < 0){LOG(FATAL, "socket create failed\n");exit(SOCKET_ERROR);}LOG(INFO, "socket create success, sockfd is %d\n", _sockfd);// 预期 3}// 启动服务器void Start(){}// 析构~UdpServer(){}
private:int _sockfd;
};

我们来编译一下,此时 sockfd 应该是3,因为 【文件描述符】那里介绍过,0、1、2被占用了

没有问题,这也证明了 套接字的本质就是 文件描述符,不过它用于描述网络资源

2.2.2 绑定ip和端口号

两台主机通信的本质是两台主机上的两个进程在通信,即进程间通信

• 如何在网络中标识不同主机?IP地址

• 网络中如何在一台主机上标识唯一的一个进程?端口号

所以,我们只要知道对方的 ip+port 就可以唯一在网络中确定一个进程,然后就可以通信了,所以我们在客户端和服务端都需要进行绑定 ip+port

注意:目前这里我的是云服务器,云服务器不建议绑定一个特定的ip,而客户端也是不用,用户显示的绑定的(OS自动绑定)!

这一点后面解释!

使用 bind 函数进行绑定

#include <sys/types.h>
#include <sys/socket.h>// 绑定IP地址和端口号(TCP/UDP	服务器)
int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen);

参数

sockfd    创建成功的套接字

addr       通信信息的结构体

addrlen  通信信息结构体的大小

这里第一个参数,就是上面刚刚创建 socket 成功的返回值,主要介绍的是第二个 addr 

上面说了, socket 套接字通信标准为了兼顾 网络通信 和 本地通信 所以提供了 struct sockaddr ,而实际网络通信用的是 struct sockaddr_in(本地通信用的是 struct sockaddr_un)这里是基于 UDP的网络通信所以,我们重点关注一下 struct sockaddr_in

网络通信本质是进程间通信,而ip标识不同网络中的主机,port标识主机中的不同进程,所以,双方通信前得知道对方的ipport,而它两就会存储在struct sockaddr_in 中,下面这就是 sockaddr_in 结构体中的字段

struct sockaddr_in
{__SOCKADDR_COMMON(sin_);in_port_t sin_port;      /* Port number.  */struct in_addr sin_addr; /* Internet address.  *//* Pad to size of `struct sockaddr'.  */unsigned char sin_zero[sizeof(struct sockaddr) - __SOCKADDR_COMMON_SIZE - sizeof(in_port_t) - sizeof(struct in_addr)];
};

  __SOCKADDR_COMMON(sin_);

这就是一个 短整数(16位),标识是 网络通信 还是 本地通信,这里其实就是一个,他的原型如下:

typedef unsigned short int sa_family_t;/* This macro is used to declare the initial common membersof the data types used for socket addresses, `struct sockaddr',`struct sockaddr_in', `struct sockaddr_un', etc.  */#define	__SOCKADDR_COMMON(sa_prefix) \sa_family_t sa_prefix##family#define __SOCKADDR_COMMON_SIZE	(sizeof (unsigned short int))

其中 sa_family_t 本质就是一个 短整数,而 sa_prefix 就是我们传递的通信类型即 sin_网络/本地),这里发现 sa_prefixfamily 给用 ## 连接了,这在C语言介绍过,这表示将传入的 sin_ family 拼接为一个新的类型 sa_family_t 即标识通信类型/方式的16

sin_port 就是端口号,其中 in_port_t 就是 uint16_t 的短整数

typedef uint16_t in_port_t;

sin_addr 表示的是 IP 地址,他这里又是一个结构体 in_addr 类型的变量 其实,这里面就一个整型变量:

/* Internet address.  */
typedef uint32_t in_addr_t;
struct in_addr
{in_addr_t s_addr;
};

这里明显的看到(IPv4)的时候是一个32位的整数,也就是说网络序列中存储ip使用的是一个整数!此时,你肯定好奇,不对呀!我平时用的是这种:127.0.0.1的字符串啊,其实这种叫做 点分十进制,方便用户看,真实的网络序列采用的是上面的整数存储的!

那是不是我也需要把这种点分十进制的字符串手动的转成整数呢?

是的!但是不需要你自己写函数转,人家已经写好了!例如:inet_addr 还有其他的,我们最后会补充的!目前暂时用这个就OK


介绍到这里我们就可以将 sockfd  和 存储 ip port 信息的结构体进行绑定了!

我们打算未来在启动的时候,可以让外部动态的指定端口,所以我们可以把端口利用构造暴露出去,而ip上面说了,不需要绑定固定的ip所以不用管

#pragma once#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <string>#include "nocopy.hpp"
#include "Common.hpp"
#include "Log.hpp"using namespace LogModule;static const int g_socket = -1;class UdpServer : public nocopy
{
public:// 构造UdpServer(uint16_t port): _sockfd(g_socket), _port(port){}// 初始化服务器void InitServer(){// 创建 socket 套接字_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0); // IPv4 面向数据报if (_sockfd < 0){LOG(FATAL, "socket create failed\n");exit(SOCKET_ERROR);}LOG(INFO, "socket create success, sockfd is %d\n", _sockfd); // 预期 3// 创建存储 服务端主机的 ip 和 端口号信息的 struct sockaddr_in 的结构体struct sockaddr_in local;bzero(&local, sizeof(local));       // 清空local.sin_family = AF_INET;         // 网络通信local.sin_port = htons(_port);      // 将主机转为网络序列local.sin_addr.s_addr = INADDR_ANY; // 与服务器主机不绑定固定的ip而是任意的ip// 将 socket 套接字和 struct sockaddr_in 绑定int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){LOG(FATAL, "bind failed\n");exit(BIND_ERROR);}LOG(INFO, "bind success\n");}// 启动服务器void Start(){}// 析构~UdpServer(){if (_sockfd > 0)::close(_sockfd);}private:int _sockfd; // socket 套接字uint16_t _port;// 端口号// std::string _ip;// ip
};

使用 struct sockaddr_in 需要包含头文件

#include <netinet/in.h>
#include <arpa/inet.h>

AF_INET 表示网络通信,当然也可以写成 PF_INET

INADDR_ANY 表示绑定任意 IP 地址

bzero <cstring> 中的一个设置初始值函数,和 memset 类似

2.2.3 启动服务

上面我们已经把当前主机某个服务(进程)的信息(ip和port)进行了和socket的绑定,此时我们就可以启动服务端,进行 接收处理 用户的请求了!

首先,服务端是得先收到客户端的请求,然后在处理,最后返回给客户端!

服务端收到请求的时候,也得知道是谁发给你的,所以也得用 sockaddr_in 结构体的存储客户端的 ip port 等信息;

• 使用 recvfrom 函数进行 接收客户端的请求

处理请求,这里的处理就是将收到的信息响应给用户即可

• 使用 sendto 响应给刚刚请求的客户端即可!

这里只需要介绍完这里两个函数,就可以启动服务了

recvfrom

作用:从 sockfd 中接收数据

#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);

参数

sockfd     创建成功的 socket 套接字

buf           一个接受数据用的缓冲区,

len           缓冲区的大小

flag          读取方式(阻塞/非阻塞)

src_addr  表示客户端请求的 ipport 信息

addrlen    客户端信息结构体的大小

所以,我们在接受客户端的请求前,先得有一个 sockaddr_in 的结构体来记录,请求客户端的信息

返回值

成功,返回收到的字节数,失败,返回-1( ssize_t 就是 long int

sendto

作用:通过 sockfd 给指定的 dest_addr 发送数据

#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);

参数

sockfd     创建成功的 socket 套接字

buf           一个接受数据用的缓冲区,

len           缓冲区的大小

flag          读取方式(阻塞/非阻塞)

dest_addr  表示发送给客户端的 ipport信息

addrlen    客户端信息结构体的大小

返回值

成功,返回发送的字节数;失败,返回-1

下面就是 启动服务的代码

// 启动服务器
void Start()
{_isrunning = true;while (_isrunning){// 创建客户端的 ip 端口信息的结构体struct sockaddr_in peer;socklen_t len = sizeof(peer);// 接受客户端的请求char buffer[1024];ssize_t n = ::recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (n > 0){buffer[n] = 0;// 处理请求std::string message = "[echo_server say]# ";message += buffer;// 响应给客户端ssize_t m = ::sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&peer, len);if (m < 0){LOG(WARNING, "sendto failed\n");}}else{LOG(FATAL, "recvfrom failed\n");}}_isrunning = false;
}

注意在析构的时候,需要将 sockfd 给关掉

// 析构
~UdpServer()
{if (_sockfd > 0)::close(_sockfd);
}

2.2.4 服务端的全部源码

#pragma once#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <string>#include "nocopy.hpp"
#include "Common.hpp"
#include "Log.hpp"using namespace LogModule;static const int g_socket = -1;class UdpServer : public nocopy
{
public:// 构造UdpServer(uint16_t port): _sockfd(g_socket), _port(port), _isrunning(false){}// 初始化服务器void InitServer(){// 创建 socket 套接字_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0); // IPv4 面向数据报if (_sockfd < 0){LOG(FATAL, "socket create failed\n");exit(SOCKET_ERROR);}LOG(INFO, "socket create success, sockfd is %d\n", _sockfd); // 预期 3// 创建存储 服务端主机的 ip 和 端口号信息的 struct sockaddr_in 的结构体struct sockaddr_in local;bzero(&local, sizeof(local));       // 清空local.sin_family = AF_INET;         // 网络通信local.sin_port = htons(_port);      // 将主机转为网络序列local.sin_addr.s_addr = INADDR_ANY; // 与服务器主机不绑定固定的ip而是任意的ip// 将 socket 套接字和 struct sockaddr_in 绑定int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){LOG(FATAL, "bind failed\n");exit(BIND_ERROR);}LOG(INFO, "bind success\n");}// 启动服务器void Start(){_isrunning = true;while (_isrunning){// 创建客户端的 ip 端口信息的结构体struct sockaddr_in peer;socklen_t len = sizeof(peer);// 接受客户端的请求char buffer[1024];ssize_t n = ::recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (n > 0){buffer[n] = 0;// 处理请求std::string message = "[echo_server say]# ";message += buffer;// 响应给客户端ssize_t m = ::sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&peer, len);if (m < 0){LOG(WARNING, "sendto failed\n");}}else{LOG(FATAL, "recvfrom failed\n");}}_isrunning = false;}// 析构~UdpServer(){if (_sockfd > 0)::close(_sockfd);}private:int _sockfd;     // socket 套接字uint16_t _port;  // 端口号// std::string _ip; // ip 云服务器不需要绑定指定 ipbool _isrunning; // 服务端的状态
};

2.3 客户端设计

客户端的任务分为:1、向服务端发送请求 2、接收来自服务端的响应

收发数据都是基于 sockfd,以及 ip 、port 的所以我们先把这些字段加上,并在构造函数初始化

static const int g_sockfd = -1;class UdpClient
{
public:// 构造UdpClient(std::string &ip, uint16_t port): _sockfd(g_sockfd), _ip(ip), _port(port){}// 初始化客户端void InitClient(){}// 启动客户端void Start(){}// 析构~UdpClient(){}private:int _sockfd;                // socket 套接字uint16_t _port;             // 端口号std::string _ip;            // ip 地址struct sockaddr_in _server; // 存储服务端的IP、port的结构体
};

2.3.1 初始化客户端

这里我们创建 socket 套接字,然后将存储 服务端 的结构体的内容填充即可

// 初始化客户端
void InitClient()
{// 创建套接字_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){std::cerr << "socket error" << std::endl;exit(1);}// 填充服务器端的数据memset(&_server, 0, sizeof(_server));             // 初始值设置为 0_server.sin_family = AF_INET;                     // 网络通信_server.sin_port = htons(_port);                  // 主机转网络_server.sin_addr.s_addr = inet_addr(_ip.c_str()); // 将点分十进制的ip转为整数// 注意这里客户端不需要 显示 的绑定自己的 ip 和 端口,OS 会自动绑定
}

注意:客户端这里,我们是需用户显示要绑定 ipport 的(最后解释原因)

2.3.2 启动客户端

启动客户端这里的任务主要是,向服务器发送数据,然后结束反馈,显示给用户即可

// 启动客户端
void Start()
{// 长服务while (true){// 客户端需要发送的消息std::string message;std::cout << "Please Enter@ ";std::getline(std::cin, message);// 向服务端发送请求int n = ::sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&_server, sizeof(_server));if (n > 0) // 发送成功,接受响应{// 记录服务端的ip和port等信息struct sockaddr_in peer;socklen_t len = sizeof(peer);// 创建接收数据的 缓冲区char buffer[1024];// 接收响应ssize_t m = ::recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (m > 0){buffer[m] = 0;std::cout << buffer << std::endl;// 接收到的数据,显示给用户}else{std::cerr << "recvfrom error" << std::endl;break;}}else{std::cerr << "sendto error" << std::endl;break;}}
}

注意,析构的时候需要将sockfd 给关掉

// 析构
~UdpClient()
{if(_sockfd > 0)::close(_sockfd);
}

2.3.3 客户端全部源码

#pragma once#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>static const int g_sockfd = -1;class UdpClient
{
public:// 构造UdpClient(std::string &ip, uint16_t port): _sockfd(g_sockfd), _ip(ip), _port(port){}// 初始化客户端void InitClient(){// 创建套接字_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){std::cerr << "socket error" << std::endl;exit(1);}// 填充服务器端的数据memset(&_server, 0, sizeof(_server));             // 初始值设置为 0_server.sin_family = AF_INET;                     // 网络通信_server.sin_port = htons(_port);                  // 主机转网络_server.sin_addr.s_addr = inet_addr(_ip.c_str()); // 将点分十进制的ip转为整数// 注意这里客户端不需要 显示 的绑定自己的 ip 和 端口,OS 会自动绑定}// 启动客户端void Start(){// 长服务while (true){// 客户端需要发送的消息std::string message;std::cout << "Please Enter@ ";std::getline(std::cin, message);// 向服务端发送请求int n = ::sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&_server, sizeof(_server));if (n > 0) // 发送成功,接受响应{// 记录服务端的ip和port等信息struct sockaddr_in peer;socklen_t len = sizeof(peer);// 创建接收数据的 缓冲区char buffer[1024];// 接收响应ssize_t m = ::recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (m > 0){buffer[m] = 0;std::cout << buffer << std::endl;// 接收到的数据,显示给用户}else{std::cerr << "recvfrom error" << std::endl;break;}}else{std::cerr << "sendto error" << std::endl;break;}}}// 析构~UdpClient(){if(_sockfd > 0)::close(_sockfd);}private:int _sockfd;                // socket 套接字uint16_t _port;             // 端口号std::string _ip;            // ip 地址struct sockaddr_in _server; // 存储服务端的IP、port的结构体
};

主函数修改

前面设计的时候,说了我们需要动态的指定服务端的 port 以及客户端的 ip+port,我们这里采用以前系统部分介绍的命令行参数来实现

#include "UdpServer.hpp"
#include <memory>// ./udpserver 8888
int main(int argc, char* argv[])
{if(argc != 2){std::cerr << "Usage " << argv[0] << " port" << std::endl;exit(1);}uint16_t port = std::stoi(argv[1]);std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port);// C++14usvr->InitServer();usvr->Start();return 0;
}
#include "UdpClient.hpp"
#include <memory>
// ./udpclient 127.0.0.1 8888
int main(int argc, char* argv[])
{if(argc != 3){std::cerr << "Usage " << argv[0] << " ip port" << std::endl;exit(1);}std::string ip = argv[1];uint16_t port = std::stoi(argv[2]);std::unique_ptr<UdpClient> uclt = std::make_unique<UdpClient>(ip,port);// C++14uclt->InitClient();uclt->Start();return 0;
}

我们先来测试一下效果,再来解决上面的遗留问题

是没有问题的!下面这是我云服务器的公网ip,这也证明了服务端可以接收不同ip的请求

2.3.4 解决遗留问题

为什么云服务器上服务器端不能绑定指定的ip?

云服务器如果绑定了一个特定的ip 就只能接收来自该ip+端口的客户端的请求, 该ip的请求都请求不了,一般我们采用的是将服务端的 ip 设置为 INADDR_ANY (0.0.0.0)他表示任意地址!这就意味着服务器接受 任意 ip+服务器端口的客户端 访问该服务!

举个例子:一般服务器会开放一个端口供客户端访问,我们把这个端口比作一个小区的警务室!警务室有很多的通信设备,有线电话、手机、对讲机等;这些设备就是ip,如果这个警务室指定了有线电话接受外界的通信请求的话,外界就只能用电话!手机、对讲机等都打不通!而如果不指定通信设备的话,外界就可以使用任意的同学设备给警务室通信!而不指定具体的通信设备就是 IP 中的INADDR_ANY

为什么客户端不需要 显示 的绑定ip和端口?

首先注意的是,客户端不是不需要绑定ip和端口,只是不用用户显示的绑定!而是由OS随机选择一个端口进行绑定的!原因是,端口号资源是有限的,而客户端主机的进程不止一个,众多进程/服务无法做到客户端的端口号不冲突,所以如果客户端显示的绑定,就会造成同一主机不同进程的端口冲突,这就很不好!

例如:快手 觉得 8888 这个端口号,给他的客户端显示的绑定了,隔壁 抖音 觉的8888也很好,也给他的客户端绑定了,此时它两冲突了,造成的后果就是,你一旦打开抖音就开不了快手!显然我们平时不是这样的!

现在的问题是,端口号既然是OS随机选择的,什么时候选择的?如何选择?

当客户端第一次向服务端发送请求的时候,OS会自动的给客户端选择一个没被用的端口,进行和本机ip绑定!而这个被随机选择的端口被称为临时端口/客户端端口!至于客户端ip也是由OS和网络配置决定的,用户不用操作!

2.4 优化和验证

我们发现,上面的客户端可以请求服务端了,服务端也可以将收到的请求处理并返回给客户端了!但是不够优雅!为我们期望看到的是,服务端回显客户端的的ip和port的信息!这样我么可以验证上面说的,客户端的端口是OS随机选择的!

由于服务端拿到的是网络序列,所以我们进行小优化一下,在服务端拿到主机序列,所以我们写一个类专门完成这个事情:

#pragma once
#include <string>
#include <netinet/in.h>
#include <arpa/inet.h>class Int_Addr
{
private:void ToHost(){// 将网络序列转为本机序列_ip = inet_ntoa(_addr.sin_addr);_port = ntohs(_addr.sin_port);}public:Int_Addr(const struct sockaddr_in &peer): _addr(peer){ToHost();}std::string Ip(){return _ip;}uint16_t Port(){return _port;}~Int_Addr(){}private:std::string _ip;uint16_t _port;struct sockaddr_in _addr;
};

此时我们把他给加到服务端:

现在来看一下效果:

此时服务端就可以看到来自客户端请求的ip和端口了,而且我们客户端是没有指定端口的,这里他们的端口是不一样的!

OK,因为这里第一个UDP网络程序很多东西的细节以前没有介绍过,所以这里就很详细!以后我们不在介绍这么详细了!就直接用了!

请点击我的gitee链接查看全部源码:echo_server全部源码


 3、简单的英译汉

这个网络程序,打算实现的功能是:客户端输入一个单词,服务端进行翻译成汉语

仔细一分析,要实现这个样的程序好像不难,直接在上面的 echo_server  的基础上加业务即可!这个业务就是实现将一个单词翻译成汉语

为了降低耦合度我们可以将执行翻译的这部分单独封装成一个类,利用包装器包装成一个可调用对象,给服务端这个可调用对象,服务端只需要调用即可!这样服务端并不关心处理业务在干啥,只需要调指定的可调用函数即可!做到了完美的解耦,优雅~!

3.1 实现Dict 类

我们将采用文件级别的英汉映射,所以提前得准备一个文件,外面在初始化对象的时候,只需要把这个文件的路径传过来就可以,然后构造的时候去加载该文件中的,单词和汉语将他们放到哈希表中,最后向外部提供翻译的接口 Transform 即可!

#pragma once#include <iostream>
#include <string>
#include <unordered_map>#include "Log.hpp"
#include "Common.hpp"using namespace LogModule;const std::string sep = ":";class Dict
{
private:void Load(){// 打开文件std::ifstream in(_path);if (!in.is_open()){LOG(FATAL, "file: %s open failed\n", _path);exit(FILE_OPEN);}// 读取每一行std::string line;while (std::getline(in, line)){LOG(DEBUG, "load %s success\n", line.c_str());if (line.empty()) // 空行continue;auto pos = line.find(sep);if (pos == std::string::npos) // 最后只有分隔符continue;std::string key = line.substr(0, pos);if (key.empty()) //       :xxxxcontinue;std::string value = line.substr(pos + sep.size());if (value.empty()) // xxxxx:continue;_dict.insert(std::make_pair(key, value));}LOG(DEBUG, "load done\n");in.close();}public:Dict(const std::string &path): _path(path){Load();}std::string Transform(std::string word){if (word.empty())return "None";auto iter = _dict.find(word);return iter == _dict.end() ? "None" : iter->second;}~Dict(){}private:std::string _path;                                  // 文件的路径std::unordered_map<std::string, std::string> _dict; // 映射
};

因为这个逻辑不复杂所以直接就给源码了!

3.2 完成服务端的修改

其实服务端和上面的 echo_server 几乎是一样的,不同的是加了一个可调用对象的属性:

包装一个参数string 返回值 string func_t函数对象

这个对象是外部启动服务器的时候传过来的,服务器只是调用它!

仅仅是和上面的 echo_server 就多了一个 调用 _func  此时,服务器也不知道 _func 在干嘛,服务器只负责收发数据!

3.3 主函数完善

#include "UdpServer.hpp"
#include "Dict.hpp"
#include <memory>int main(int argc, char* argv[])
{if(argc != 2){std::cerr << "Usage " << argv[0] << " port" << std::endl;exit(1);}uint16_t port = std::stoi(argv[1]);Dict dict("./dict.txt");func_t transfrom = std::bind(&Dict::Transform, &dict, std::placeholders::_1);std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, transfrom);// C++14usvr->InitServer();usvr->Start();return 0;
}

客户端只是创建了一个 Dict 对象,然后将它里面的 Transform 函数进行了绑定,然后给了服务器对象!关于std::bind 这里就是将 Dict 类中的Transform函数和&dict对象强关联了,此时就是返回值 string 参数string  的 可调用对象了!std::bind是解耦的神器,后面经常看到~

注意:这里客户端是不用动的!

请点击我的gitee链接查看全部源码:dict 全部源码


4、多人聊天室

最后一个UDP套接字的网络程序,我们打算实现一个简单的在线多人聊天室

主要功能是:不同主机的客户端都可以向服务端发送消息,服务端在将这些消息转给所有在线的客户端,即实现了多人聊天

其实这里和上面的翻译程序很相似,但是我们这里要采用多线程的转发!什么意思呢?

我们服务端的主线程负责收消息,向每个在线的用户转发的事情将采用线程转发!

由于转发是给每一个在线的用户转发的,所以我们需要在维护一个类Route专门负责维护在线用户和转发的,它里面提供一个转发的函数,外部主函数进行包装,主线程直接执行这个可调用的函数对象即可,即降低了耦合度

Route的转发函数内部将采用线程池当主线程调用时,只需要将转发的任务包装成线程池的任务类型,放到线程池即可,之后线程池中的线程就可以调用可!这里的线程池我们也采用我们自己以前手撕的那个~!

4.1 服务端改造

我们这个程序还是基于 echo_server 的,所以直接在它上面改造即可!

UdpServer.hpp 服务端 只需要把之前的转发消息换成_func即可

_func 是主函数启动服务器时给的,上面包装的类型是 server_t 是因为func_t 和线程池中的冲突了!这里因为要转发消息,所以得把 sockfd、转发的消息都给过去,最后给 Int_Addr  的addr是因为转发时得判断当前的用户是否在用户列表中!

而服务端主函数给的可调用对象是,包装的Route中的Forward的,所我们先介绍Route类

4.1.1 Route类实现

Route类的主要作用是:向所有的在线用户转发消,介绍到的消息

所以,得维护一个在线的用户列表,可以使用 vector,类型就是 Int_Addr主线程进来先检查该用户是否存在,然后判断是不是要退出(消息是:Q或者QUIT代表退出),如果是直接将从vector中删除即可!如果不删除,就包装线程池中线程的回调的函数,然后获取线程池的句柄(我们以前实现的是单例的),将任务放到线程池即可

#pragma once#include <iostream>
#include <vector>
#include <functional>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <pthread.h>#include "Inet_Addr.hpp"
#include "Log.hpp"class Route
{
private:void Check(Int_Addr &who){}void Offline(Int_Addr &who){}// 线程池中的线程回调的任务函数void ForwardHelper(int sockfd, std::string message, Int_Addr &who){}public:Route(){}void Forward(int sockfd, std::string message, Int_Addr &who){// 1、检查用户是否在,在线列表 ---》a.不在:添加  b.在:啥都不做Check(who);// 2、用户需要离线离线if (message == "QUIT" || message == "Q"){Offline(who);}// 3、向你在线的用户转发消息//    让线程去转发// 包装函数// 获取句柄,推送任务}~Route(){}private:std::vector<Int_Addr> _online_users; // 在线用户列表
};

这就是大概的框架,先来实现一下,上面的检查用户在线、是否下线、线程池中的回调(转发消息)的函数:

Check

void Check(Int_Addr &who)
{for (auto &user : _online_users){if (user == who)return; // 用户已经存在}// 不存在,添加_online_users.emplace_back(who);LOG(DEBUG, "%s add success\n", who.AddrStr().c_str());
}

这里使用到了 == Int_Addr中没有实现,所以得加一下:

Offline

void Offline(Int_Addr &who)
{auto iter = _online_users.begin();for (; iter != _online_users.end(); iter++){if (*iter == who){LOG(DEBUG, "%s remove success\n", who.AddrStr().c_str());_online_users.erase(iter);break; // 避免迭代器失效}}
}

 ForwardHelper

void ForwardHelper(int sockfd, std::string message, Int_Addr &who)
{std::string send_message = "[" + who.AddrStr() + "]# " + message;// 转发for (auto &user : _online_users){struct sockaddr_in peer = user.Addr(); // 发给谁::sendto(sockfd, send_message.c_str(), send_message.size(), 0, (struct sockaddr *)&peer, sizeof(peer));LOG(INFO, "%s forward a message is %s\n", who.AddrStr(), send_message.c_str());}
}

OK,接下来就是包装 线程的可调用对象了,转发需要的就是 sockfd 和 message 、who也就是和上面的 ForWardHelper 绑死,后面线程直接调用即可,无需参数!

void Forward(int sockfd, std::string message, Int_Addr &who)
{// 1、检查用户是否在,在线列表 ---》a.不在:添加  b.在:啥都不做Check(who);// 2、用户需要离线离线if (message == "QUIT" || message == "Q"){Offline(who);}// 3、向你在线的用户转发消息//    让线程去转发task_t t = std::bind(&Route::ForwardHelper, this, sockfd, message, who);ThreadPool<task_t>::getInstance()->PushTask(t);
}

OK,到这里我们的Route的类基本上就设计好了!但是还差最后一口气:这里的检查和下线是主线程做的,向所有的在线用户转发时新线程做的,而他们的操作都会操作同一个vector,可能造成线程安全的问题,所以得保护,如何保护?加锁!

#pragma once#include <iostream>
#include <vector>
#include <functional>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <pthread.h>#include "Inet_Addr.hpp"
#include "Log.hpp"
#include "ThreadPool.hpp"
#include "LockGuard.hpp"using namespace LogModule;using task_t = std::function<void(void)>; // 包装一个线程的任务函数class Route
{
private:void Check(Int_Addr &who){LockGuard lockguard(&_mutex);for (auto &user : _online_users){if (user == who)return; // 用户已经存在}// 不存在,添加_online_users.emplace_back(who);LOG(DEBUG, "%s add success\n", who.AddrStr().c_str());}void Offline(Int_Addr &who){LockGuard lockguard(&_mutex);auto iter = _online_users.begin();for (; iter != _online_users.end(); iter++){if (*iter == who){LOG(DEBUG, "%s remove success\n", who.AddrStr().c_str());_online_users.erase(iter);break; // 避免迭代器失效}}}void ForwardHelper(int sockfd, std::string message, Int_Addr &who){LockGuard lockguard(&_mutex);std::string send_message = "[" + who.AddrStr() + "]# " + message;// 转发for (auto &user : _online_users){struct sockaddr_in peer = user.Addr(); // 发给谁::sendto(sockfd, send_message.c_str(), send_message.size(), 0, (struct sockaddr *)&peer, sizeof(peer));LOG(INFO, "%s forward a message is %s\n", who.AddrStr(), send_message.c_str());}}public:Route(){pthread_mutex_init(&_mutex, nullptr);}void Forward(int sockfd, std::string message, Int_Addr &who){// 1、检查用户是否在,在线列表 ---》a.不在:添加  b.在:啥都不做Check(who);// 2、用户需要离线离线if (message == "QUIT" || message == "Q"){Offline(who);}// 3、向你在线的用户转发消息//    让线程去转发task_t t = std::bind(&Route::ForwardHelper, this, sockfd, message, who);ThreadPool<task_t>::getInstance()->PushTask(t);}~Route(){pthread_mutex_destroy(&_mutex);}private:std::vector<Int_Addr> _online_users; // 在线用户列表pthread_mutex_t _mutex;              // 互斥锁
};

这样就好了!最后还差服务端的主函数的传递给服务端可调用对象了

4.1.2 服务端主函数修改

只需要创建 Route 对象,然后绑定一个可接受三个参数的函数对象即可

#include "UdpServer.hpp"
#include "Route.hpp"
#include <memory>int main(int argc, char* argv[])
{if(argc != 2){std::cerr << "Usage " << argv[0] << " port" << std::endl;exit(1);}uint16_t port = std::stoi(argv[1]);Route route;server_t forward = std::bind(&Route::Forward, &route, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, forward);// C++14usvr->InitServer();usvr->Start();return 0;
}

4.2 客户端改造

我们期望客户端使用两个不用的线程分别进行收发消息,所以我们需要将原来的客户端的 Start函数进行改造成两个函数,供两个线程分别调用:

// 收消息
void RecvMsg(const std::string &name)
{while (true){// 记录服务端的ip和port等信息struct sockaddr_in peer;socklen_t len = sizeof(peer);// 创建接收数据的 缓冲区char buffer[1024];// 接收响应ssize_t m = ::recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (m > 0){buffer[m] = 0;std::cerr << buffer << std::endl; // 接收到的数据,显示给用户}else{std::cerr << "recvfrom error" << std::endl;break;}}
}void SendMsg(const std::string &name)
{// 长服务std::string cli_profix = name + "# ";while (true){// 客户端需要发送的消息std::string message;std::cout << cli_profix;std::getline(std::cin, message);// 向服务端发送请求int n = ::sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&_server, sizeof(_server));if (n < 0){std::cerr << "sendto error" << std::endl;break;}}
}

4.2.1 客户端的主函数改造

客户端的主函数就需要创建两个线程来分别执行,收消息和发消息的任务

#include "UdpClient.hpp"
#include "Thread.hpp"
#include <memory>using namespace ThreadModule;int main(int argc, char *argv[])
{if (argc != 3){std::cerr << "Usage " << argv[0] << " ip port" << std::endl;exit(1);}std::string ip = argv[1];uint16_t port = std::stoi(argv[2]);std::unique_ptr<UdpClient> uclt = std::make_unique<UdpClient>(ip, port); // C++14// 创建两个线程分别负责 收发消息Thread sender("sender-thread", std::bind(&UdpClient::SendMsg, uclt.get(), std::placeholders::_1));Thread recver("recver-thread", std::bind(&UdpClient::RecvMsg, uclt.get(), std::placeholders::_1));sender.start();recver.start();sender.join();recver.join();return 0;
}

注意这里bind时因为这里uclt是智能指针对象,所以需要获取原生对象的指针就需要使用get方法,这点我们在【智能指针】介绍过!

OK,测试一下:

这里我是在一台主机上使用 本地环回 公网ip 进行模拟的,为了让页面看起来不那么混乱,我用了 管道重定向 分离,因为客户端收到的消息是使用 std::cerr 打印的,也就是文件描述符是2,可以利用重定向将他们分开,将输出的cerr的内容都放到管道,一个专门的终端读取即可每个客户端都有一个管道和读取管道的终端,正如你的微信有一个输入区域(就是这里的下面的那个终端),有显示群消息的(上面的那个终端),且每个微信都是有各自的这两个的!

请点击我的gitee链接查看全部源码:chat全部源码


5、地址函数补充

sockaddr_in 中的成员 sin_addr.s_addr 表示一个32位的整数的 ip 地址,但是我们通常使用点分十进制的字符串表示 ip 地址,以下函数可以在 字符串整数的ip 之间进行转换

5.1 字符串转整数

// 将 cp 字符串转为 整数
int inet_aton(const char *cp, struct in_addr *inp);// typedef uint32_t in_addr_t;
in_addr_t inet_addr(const char *cp);// cp 字符串转为 整数 返回// af : IpV4和IPv6的哪一个  src : 表示字符串ip    dst :被转换之后的整数struct sockaddr_in 的 sin_addr.s_addr
int inet_pton(int af, const char *src, void *dst);

5.2 整数转字符串

// 将整数转为 整数 ip 返回
char *inet_ntoa(struct in_addr in);// af : IpV4和IPv6的哪一个 src:代表整数ip    dst:用户指定的字符串缓冲区, size 缓冲区大小
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

其中 inet_pton inet_ntop 不仅可以转换 IPv4 in_addr,还可以转换 IPv6 的 in6_addr,因此函数接口是 void *

#include <iostream>
#include <netinet/in.h>
#include <arpa/inet.h>int main()
{       struct sockaddr_in addr1;struct sockaddr_in addr2;addr1.sin_addr.s_addr = 0;addr2.sin_addr.s_addr = 0xffffffff;char* ptr1 = inet_ntoa(addr1.sin_addr);char* ptr2 = inet_ntoa(addr2.sin_addr);printf("ptr1: %s, ptr2: %s\n", ptr1, ptr2);return 0;
} 

我们发现,第二次把第一次的给覆盖了!

原因是: inet_ntoa 把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆

盖掉上一次的结果

这里就会有问题:如果有多个线程调用 inet_ntoa 可能会出现异常的情况!

APUE , 明确提出 inet_ntoa 不是线程安全的函数

但是在 centos7 上测试了没有出问题,内部可能加了互斥锁(猜测)!

在多线程环境下, 推荐使用 inet_ntop, 这个函数由调用者提供一个缓冲区保存 结果, 可以规避线程安全

由于 centos7 已停止维护所以,我们写一段代码在 ubuntu 上验证一下:
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
void *Func1(void *p)
{struct sockaddr_in *addr = (struct sockaddr_in *)p;while (1){char *ptr = inet_ntoa(addr->sin_addr);printf("addr1: %s\n", ptr);}return NULL;
}
void *Func2(void *p)
{struct sockaddr_in *addr = (struct sockaddr_in *)p;while (1){char *ptr = inet_ntoa(addr->sin_addr);printf("addr2: %s\n", ptr);}return NULL;
}
int main()
{pthread_t tid1 = 0;struct sockaddr_in addr1;struct sockaddr_in addr2;addr1.sin_addr.s_addr = 0;addr2.sin_addr.s_addr = 0xffffffff;pthread_create(&tid1, NULL, Func1, &addr1);pthread_t tid2 = 0;pthread_create(&tid2, NULL, Func2, &addr2);pthread_join(tid1, NULL);pthread_join(tid2, NULL);return 0;
}

我大概测试了5次,都没有出错!

但是,我们还是建议在多线程下使用 inet_ntopinet_pton  


OK,好兄弟本期分享就到这里,我是 cp 我们下期再见~!

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

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

相关文章

---Arrays类

一 java 1.Arrays类 1.1 toString&#xff08;&#xff09; 1.2 arrays.sort( )-----sort排序 1&#xff09;直接调用sort&#xff08;&#xff09; Arrays.sort() 方法的默认排序顺序是 从小到大&#xff08;升序&#xff09;。 2&#xff09;定制排序【具体使用时 调整正负…

Java 对象头、Mark Word、monitor与synchronized关联关系以及synchronized锁优化

1. 对象在内存中的布局分为三块区域&#xff1a; &#xff08;1&#xff09;对象头&#xff08;Mark Word、元数据指针和数组长度&#xff09; 对象头&#xff1a;在32位虚拟机中&#xff0c;1个机器码等于4字节&#xff0c;也就是32bit&#xff0c;在64位虚拟机中&#xff0…

6.7机器学习期末复习题

空间 样本空间 就是属性的所有可能情况&#xff0c;包括了一切可能出现或不可能出现的所有样本情况 版本空间&假设空间 假设空间就是在样本空间的基础上&#xff0c;给所有属性都加了一个通配符&#xff0c;表示任意即可&#xff1b;以及加上了一个空集&#xff0c;表示…

Qt界面设计时使各控件依据窗口缩放进行栅格布局的方法

图1 最终效果 想要达成上述图片的布局效果&#xff0c;具体操作如下&#xff1a; 新建一窗体&#xff1a; 所需控件如下&#xff1a; Table View控件一个&#xff1b; Group Box控件一个&#xff1b; Push Button控件2个&#xff1b; Horiziontal Spacer控件2个&#xf…

mac安装Pytest、Allure、brew

安装环境 安装pytest 命令 pip3 install pytest 安装allure 命令&#xff1a;brew install allure 好吧 那我们在安装allure之前 我们先安装brew 安装brew 去了官网复制了命令 还是无法下载 如果你们也和我一样可以用这个方法哦 使用国内的代码仓库来执行brew的安装脚本…

数据结构C语言描述5(图文结合)--队列,数组、链式、优先队列的实现

前言 这个专栏将会用纯C实现常用的数据结构和简单的算法&#xff1b;有C基础即可跟着学习&#xff0c;代码均可运行&#xff1b;准备考研的也可跟着写&#xff0c;个人感觉&#xff0c;如果时间充裕&#xff0c;手写一遍比看书、刷题管用很多&#xff0c;这也是本人采用纯C语言…

一篇文章了解机器学习

一篇文章了解机器学习&#xff08;上&#xff09; 一、软件版本安装二、数据集的加载三、数据集的切分四、数据特征提取及标准化1、字典数据的特征提取2、文本特征向量的提取3、数据标准化处理 四、特征降维注&#xff1a;训练器的区别&#xff1a;&#xff1a;五、模型的训练与…

day03(单片机高级)RTOS

目录 RTOS(实时操作系统) 裸机开发模式 轮询方式 前后台&#xff08;中断方式&#xff09; 改进&#xff08;前后台&#xff08;中断&#xff09;&#xff09;定时器 裸机进一步优化 裸机的其他问题 RTOS的概念 什么是RTOS 为什么要使用 RTOS RTOS的应用场景 RTOS的…

Hello-Go

Hello-Go 环境变量 GOPATH 和 GOROOT &#xff1a;不同于其他语言&#xff0c;go中没有项目的说法&#xff0c;只有包&#xff0c;其中有两个重要的路径&#xff0c;GOROOT 和 GOPATH Go开发相关的环境变量如下&#xff1a; GOROOT&#xff1a;GOROOT就是Go的安装目录&…

pytorch官方FasterRCNN代码详解

本博文转自捋一捋pytorch官方FasterRCNN代码 - 知乎 (zhihu.com)&#xff0c;增加了其中代码的更详细的解读&#xff0c;以帮助自己理解该代码。 代码理解的参考Faster-RCNN全面解读(手把手带你分析代码实现)---前向传播部分_手把手faster rcnn-CSDN博客 1. 代码结构 作为 to…

全志T113双核异构处理器的使用基于Tina Linux5.0——RTOS系统定制开发

8、RTOS系统定制开发 此处以在rtos/components/aw目录下创建一个简单的软件包为例&#xff0c;帮助客户了解RTOS环境&#xff0c;为RTOS系统定制开发提供基础。 RTOS环境下的软件包主要由三部分组成&#xff0c;源文件&#xff0c;Makefile&#xff0c;Kconfig&#xff0c;如下…

springboot实战(13)(@PatchMapping、@RequestParam、@URL、ThreadLocal线程局部变量)

目录 一、PATCH请求方式。 二、实现用户更新头像功能。 三、注解RequestParam。 四、注解URL。&#xff08;对传来的参数是否是合法地址进行校验&#xff09; 一、PATCH请求方式。 patch中文翻译&#xff1a;局部、小块。PATCH 请求主要用于对已存在的资源进行局部修改&#xf…

nvm安装node遇到的若干问题(vscode找不到npm文件、环境变量配置混乱、npm安装包到D盘)

问题一&#xff1a;安装完nvm后需要做哪些环境变量的配置&#xff1f; 1.打开nvm文件夹下的setting文件&#xff0c;设置nvm路径和安装node路径&#xff0c;并添加镜像。 root: D:\software\nvm-node\nvm path: D:\software\nvm-node\nodejs node_mirror: https://npmmirror.c…

面向FWA市场!移远通信高性能5G-A模组RG650V-NA通过北美两大重要运营商认证

近日&#xff0c;全球领先的物联网整体解决方案供应商移远通信宣布&#xff0c;其旗下符合3GPP R17标准的新一代5G-A模组RG650V-NA成功通过了北美两家重要运营商认证。凭借高速度、大容量、低延迟、高可靠等优势&#xff0c;该模组可满足CPE、家庭/企业网关、移动热点、高清视频…

2024年11月21日Github流行趋势

项目名称&#xff1a;twenty 项目维护者&#xff1a;charlesBochet, lucasbordeau, Weiko, FelixMalfait, bosiraphael项目介绍&#xff1a;正在构建一个由社区支持的现代化Salesforce替代品。项目star数&#xff1a;21,798项目fork数&#xff1a;2,347 项目名称&#xff1a;p…

AWTK 最新动态:支持鸿蒙系统(HarmonyOS Next)

HarmonyOS是全球第三大移动操作系统&#xff0c;有巨大的市场潜力&#xff0c;在国产替代的背景下&#xff0c;机会多多&#xff0c;AWTK支持HarmonyOS&#xff0c;让AWTK开发者也能享受HarmonyOS生态的红利。 AWTK全称为Toolkit AnyWhere&#xff0c;是ZLG倾心打造的一套基于C…

docker 配置同宿主机共同网段的IP 同时通过通网段的另一个电脑实现远程连接docker

docker配置网络 #宿主机执行命令 ifconfig 查询对应的主机ip 子网掩码 网关地址 #[网卡名称]&#xff1a;inet[主机IP] netmask[子网掩码] broadcast[网关地址]这里需要重点关注&#xff1a;eno1[网卡名称]以及【192.168.31.225】网关地址 在宿主机执行docker命令创建一个虚拟…

使用 Elastic AI Assistant for Search 和 Azure OpenAI 实现从 0 到 60 的转变

作者&#xff1a;来自 Elastic Greg Crist Elasticsearch 推出了一项新功能&#xff1a;Elastic AI Assistant for Search。你可以将其视为 Elasticsearch 和 Kibana 开发人员的内置指南&#xff0c;旨在回答问题、引导你了解功能并让你的生活更轻松。在 Microsoft AI Services…

React (三)

文章目录 项目地址十二、性能优化12.1 使用useMemo避免不必要的计算12.2 使用memo缓存组件,防止过度渲染12.3 useCallBack缓存函数12.4 useCallBack里访问之前的状态(没懂)十三、Styled-Components13.1 安装13.2给普通html元素添加样式13.3 继承和覆盖样式13.4 给react组件添…

Etcd 框架

基本了解 客户端、长连接与租约的关系 客户端对象 etcd的客户端对象是用户与etcd服务进行交互的主要接口&#xff0c;主要功能就是存储、通知和事务等功能访问 键值存储&#xff1a;客户端通过put 和 get操作存储数据&#xff1b;数据存储在etcd的层级化键值数据库中监听器&a…