【网络编程】二、UDP网络套接字编程详解

文章目录

  • 前言
  • Ⅰ. UDP服务端
    • 一、服务器创建流程
    • 二、创建套接字 -- socket
      • socket 属于什么类型的接口❓❓❓
      • socket 是被谁调用的❓❓❓
      • socket 底层做了什么❓❓❓和其函数返回值有没有什么关系❓❓❓
    • 三、绑定对应端口号、IP地址到套接字 -- bind
    • 四、数据的发送和接收 -- `sendto` && `recvfrom`
    • 五、搭建服务器框架
      • ① 服务器类实现:udpserver.hpp
      • ② 服务器主函数:udpserver.cpp
      • 绑定INADDR_ANY的好处
  • Ⅱ. UDP客户端
    • 一、客户端创建流程
    • 二、创建套接字
    • 三、关于客户端的绑定问题
    • 四、搭建客户端框架
      • ① 客户端类单进程版实现(读写会互相阻塞):udpclient.hpp
      • 💥② 客户端类多线程版实现(读写分离):udpclient.hpp
      • ③ 客户端主函数:udpclient.cpp
  • Ⅲ. 服务端加入业务处理
    • 一、简单的中英文翻译
    • 二、简单的远程命令行指令解析
    • 三、简易的小聊天室
  • Ⅳ. windows系统的客户端

在这里插入图片描述

前言

UDP 相当于 TCP 来说细节少了很多,所以我们先从简单的 UDP 套接字编程下手,因为其涉及到的细节比较少,也很直接,就是面向报文,服务端拿到信息就直接丢给客户端,丢了也不管,无需我们关系太多细节,等我们把 UDP 套接字编程稍微掌握了,其实 TCP 的那套接口也是类似的,但是我们学 TCP 的时候,需要去了解它的传输细节。

​ 下面我们就从 UDP 套接字编程开始,分为服务端和客户端,我们先讲服务端,客户端也就顺其自然了解了!

Ⅰ. UDP服务端

一、服务器创建流程

  1. 创建套接字(socket
  2. 绑定端口号和 IP 地址(一般来说端口号是固定的,而 IP 地址就作为服务器来说,我们设置为 0.0.0.0 或者 INADDR_ANY,表示任意 IP 都能访问)
  3. 接收(recvfrom)、发送(sendto)数据,包括对数据的业务处理

二、创建套接字 – socket

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>int socket(int domain, int type, int protocol);

参数说明:

  • domain:创建套接字的域或者叫做协议家族,也就是创建套接字的类型。该参数就相当于 struct sockaddr 结构的前 16 位。

    • 如果是本地通信就设置为**AF_UNIX**,如果是网络通信就设置为 AF_INETIPv4)或 AF_INET6IPv6)。
  • type:创建套接字时所需的服务类型。

    • 其中最常见的服务类型是 SOCK_STREAMSOCK_DGRAM,如果是基于 UDP 的网络通信,我们采用的就是**SOCK_DGRAM**,叫做用户数据报服务,如果是基于 TCP 的网络通信,我们采用的就是 SOCK_STREAM,叫做流式套接字,提供的是流式服务。
  • protocol:创建套接字的协议类别。

    • 你可以指明为 TCPUDP,但该字段 一般直接设置为 0 就可以了,设置为 0 表示的就是默认,此时会根据传入的前两个参数自动推导出你最终需要使用的是哪种协议。

返回值说明:

  • 套接字创建 成功返回一个文件描述符,创建失败返回 -1,同时错误码会被设置。

socket 属于什么类型的接口❓❓❓

​ 网络协议栈是分层的,按照 TCP/IP 四层模型来说,自顶向下依次是应用层、传输层、网络层和数据链路层。而我们现在所写的代码都叫做 用户级代码,也就是说我们是在应用层编写代码,因此我们调用的实际是下三层的接口,而传输层和网络层都是在操作系统内完成的,也就意味着我们在应用层调用的接口都叫做 系统调用接口

socket 是被谁调用的❓❓❓

socket 这个函数是被程序调用的,但并不是被程序在编码上直接调用的,而是程序编码形成的可执行程序运行起来变成进程,当这个进程被 CPU 调度执行到 socket 函数时,然后才会执行创建套接字的代码,也就是说 socket 函数是被进程所调用的

socket 底层做了什么❓❓❓和其函数返回值有没有什么关系❓❓❓

socket 函数是被进程所调用的,而每一个进程在系统层面上都有一个进程地址空间 PCBtask_struct文件描述符表(files_struct 以及对应打开的各种文件。而文件描述符表里面包含了一个数组 fd_array,其中数组中的 0、1、2 下标依次对应的就是 标准输入标准输出 以及 标准错误

在这里插入图片描述

当我们调用 socket 函数创建套接字时,实际相当于我们打开了一个“网络文件”,打开后在内核层面上就形成了一个对应的 struct file 结构体,同时该结构体被连入到了该进程对应的文件双链表,并将该结构体的首地址填入到了 fd_array 数组当中下标为 3 的位置(除非此时先打开了其它文件描述符),此时 fd_array 数组中下标为 3 的指针就指向了这个打开的“网络文件”,最后该文件描述符作为 socket 函数的返回值返回给了用户
在这里插入图片描述

​ 其中每一个 struct file 结构体中包含的就是对应打开文件各种信息,比如文件的属性信息、操作方法以及文件缓冲区等。其中文件对应的属性在内核当中是由 struct inode 结构体来维护的,而文件对应的操作方法实际就是一堆的函数指针(比如 read*write*),它们在内核当中就是由 struct file_operations 结构体来维护的。而文件缓冲区对于打开的普通文件来说对应的一般是磁盘,但对于现在打开的 “网络文件” 来说,这里的文件缓冲区对应的就是网卡

在这里插入图片描述

​ 对于一般的普通文件来说,当用户通过文件描述符将数据写到文件缓冲区,然后再把数据刷到磁盘上就完成了数据的写入操作。而对于现在 socket 函数打开的 “网络文件” 来说,当用户将数据写到文件缓冲区后,操作系统会定期将数据刷到网卡里面,而网卡则是负责数据发送的,因此数据最终就发送到了网络当中。所以可以总结一点,网络中数据的通信,其实就是文件之间的拷贝

三、绑定对应端口号、IP地址到套接字 – bind

​ 现在套接字已经创建成功了,但作为一款服务器来讲,如果只是把套接字创建好了,本质上只是在系统层面上打开了一个文件,操作系统将来并不知道是要将数据写入到磁盘还是刷到网卡,此时该文件还没有与网络关联起来。

​ 所以我们要绑定对应的端口号、IP 地址到套接字文件中,此时就可以改变网络文件当中文件操作函数的指向,将对应的操作函数改为对应网卡的操作方法,此时读数据和写数据对应的操作对象就是网卡了,所以绑定实际上就是将文件和网络关联起来。而绑定使用的函数就是 bind() 函数!

​ 该函数的函数原型如下:

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数说明:

  • sockfd:套接字描述符,即要绑定的套接字。
  • addr:一个指向 sockaddr 结构体的指针,该结构体包括协议家族、IP地址、端口号等。
    • sin_family:表示协议家族。
    • sin_port:表示端口号,是一个 16 位的无符号整数。
    • sin_addr:表示 IP 地址,是一个 32 位的无符号整数。
      • 其中 sin_addr 的类型是 struct in_addr,实际该结构体当中就只有一个成员,该成员就是一个 32 位的无符号整数,IP 地址实际就是存储在这个整数当中的。
  • addrlen:传入的 sockaddr 结构体的大小。

返回值说明:

  • 绑定成功返回 0,绑定失败返回 -1,同时错误码会被设置。

对于上面的参数 addr 来说,一般需要借助到我们之前讲的几个函数来转换格式:

  • 一开始最好将该结构体初始化一下,也就是清空数据。
  • sin_port 需要转化为网络字节格式也就是大端格式,又因为它是 16 位的,那么就得使用 htons() 函数;
  • sin_addr 中的 s_addr 其实一般我们是给一个点分十进制字符串,然后通过 inet_addr() 函数将其转化为网络字节序的 32 位无符号整型,但是对于服务器来说没必要这么麻烦,因为 IP 地址直接设为 0.0.0.0 即可,表示任意 IP 都能访问,所以直接用头文件中给的宏 INADDR_ANY

​ 总结起来就是这样子:

struct sockaddr_in local;
bzero(&local, sizeof local);    // 先将一段local的内存清零,即将其中的每个字节都设置为0local.sin_family = AF_INET;
local.sin_port = htons(_port);  // 因为端口号是多个字节组成,所以要保证先转化为大段序列
local.sin_addr.s_addr = INADDR_ANY;

在这里插入图片描述

​ 关于 struct sockaddr_in 结构体,我们之前有谈过,我们可以用 grep 命令在 /usr/include 目录下查找该结构,此时就可以找到定义该结构的文件。在该文件中就可以找到 struct sockaddr_in 结构的定义,需要注意的是,struct sockaddr_in 属于系统级的概念,不同的平台接口设计可能会有点差别

/* Structure describing an Internet socket address.  */
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)];
};/* Internet address.  */
typedef uint32_t in_addr_t;
struct in_addr
{in_addr_t s_addr;
};

四、数据的发送和接收 – sendto && recvfrom

UDP 套接字是无连接协议,必须使用 recvfrom 函数接收数据,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);
  • 作用:该函数用于将数据报发送到指定的目的地。
  • 参数:
    • sockfd:表示要发送数据的套接字文件描述符。
    • buf:指向要发送的数据缓冲区。
    • len:表示要发送的数据长度。
    • flags:表示发送数据的选项,一般我们 设为 0 就行了!常用的有 MSG_DONTWAIT 表示非阻塞发送。
    • dest_addr:(输入参数)指向目标地址的 sockaddr 结构体指针,包括目标 IP 地址和端口号等信息。
    • addrlen:(输入参数)目标地址结构体的大小。
  • 返回值:
    • 如果发送成功,返回发送的字节数
    • 如果发送失败,返回 -1,并设置 errno 错误码。

​ 需要注意的是,sendto() 函数是阻塞型函数,如果要进行非阻塞型发送,可以设置 MSG_DONTWAIT 标志或使用 select() 函数等。同时,sendto() 函数在发送数据时不保证数据一定会被对方接收,如果需要保证数据可靠性,应该使用 TCP 协议。


#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);
  • 作用:从已连接的 socket 中接收数据,并将数据存储到指定的缓冲区中。

  • 参数:

    • sockfd:已经建立好连接的 socket

    • buf:指向接收数据存放的缓冲区。

    • len:缓冲区长度。

    • flags:读取方式,一般设为 0,阻塞式读取。

    • src_addr:(输出型参数)返回发送方的地址信息。

    • addrlen:(输入输出型参数)地址信息的长度。

  • 返回值:

    • 成功接收到数据时,它会 返回接收到的字节数
    • 失败返回值:发生错误,则返回 -1,并设置 errno 变量以指示错误类型。

五、搭建服务器框架

​ 搭建服务器的细节都在注释中!

① 服务器类实现:udpserver.hpp

#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include "onlineUser.hpp"
using namespace std;namespace Server
{static const string defaultIP = "0.0.0.0"; // 默认设为0,表示接收任意IPstatic const int MAXSIZE = 1024; 		   // 接收到的数据最大值enum { USAGE_ERR = 1, BIND_ERR, SOCKET_ERR, CLOSE_ERR, OPEN_ERR, NOTFOUND_ERR, SEND_ERR }; // 错误码using func_t = function<void(int, string, uint16_t, string)>; // 业务处理函数的类型class udpServer{private:uint16_t _port;   // 当前服务端进程的端口号string _ip;       // 当前服务端的ip,但是作为一个服务器,一般都是将ip设为全0,代表任意ip都能访问int _socketfd;    // 套接字文件的文件描述符func_t _callback; // 服务端要完成的业务public:udpServer(func_t callback, const uint16_t port, const string& ip = defaultIP):_port(port), _ip(ip), _socketfd(-1), _callback(callback){}// 初始化服务端void initServer(){// 1. 创建套接字(本质是创建文件)_socketfd = socket(AF_INET, SOCK_DGRAM, 0);if(_socketfd == -1){cerr << "socket error: " << errno << " : " << strerror(errno) << endl; exit(SOCKET_ERR);}cout << "socket success: " << _socketfd << endl;// 2. 绑定端口号和ip地址到当前的套接字文件struct sockaddr_in local;bzero(&local, sizeof local);    // 先将一段local的内存清零,即将其中的每个字节都设置为0,更推荐用memset函数!local.sin_family = AF_INET;local.sin_port = htons(_port);  // 因为端口号是多个字节组成,所以要保证先转化为大段序列// inet_addr函数帮我们将格式化字符串转化为in_addr_t类型,并且调整成大段序列// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 但是一般我们将作为服务器的ip设为全0,所以不需要做上述工作,直接利用一个值为全0的宏赋值就行local.sin_addr.s_addr = INADDR_ANY;int n = bind(_socketfd, (struct sockaddr*)&local, sizeof local);if(n == -1){cerr << "bind error: " << errno << " : " << strerror(errno) << endl; exit(BIND_ERR);}}// 启动服务端void start(){// 服务器的本质就是一个死循环,称为常驻内存的进程char buffer[MAXSIZE];while(true){struct sockaddr_in src;socklen_t srclen = sizeof(src); // 这是因为操作系统不知道我们传过去的是哪个sockaddr的哪个结构体,所以我们要传大小过去// 这里要传sizeof(buffer)-1是因为腾出一个位置给\0ssize_t n = recvfrom(_socketfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&src, &srclen);if(n > 0){// 将接收到的客户端的信息保存起来uint16_t src_port = ntohs(src.sin_port); // 要考虑大端接收问题,所以要转化一下string src_ip = inet_ntoa(src.sin_addr); // 考虑到原本是个uint32_t类型,且还要转化为点分法,所以我们借用inet_ntoa函数帮我们完成buffer[n] = '\0';	// 此时因为n就是数据的长度,相当于可以定位到有效数据末尾,记得要将其置为'\0'string recvmessage = buffer;// 打印收集到的信息,并且执行业务处理cout << "[" << src_ip << ", " << src_port << "]: " <<  recvmessage << endl;_callback(_socketfd, src_ip, src_port, recvmessage);}}}~udpServer(){int n = close(_socketfd);if(n != 0){cout << "close error: " << errno << " : " << strerror(errno) << endl;exit(CLOSE_ERR); }}};
}

② 服务器主函数:udpserver.cpp

​ 鉴于构造服务器时需要传入 IP 地址和端口号,我们这里可以引入命令行参数。此时当我们运行服务器时在后面跟上对应的 IP 地址和端口号即可!

​ 由于使用云服务器的原因,后面实际不需要传入 IP 地址,因此在运行服务器的时候我们只需要传入端口号即可,目前我们就手动将 IP 地址设置为127.0.0.1IP 地址为 127.0.0.1 实际上等价于 localhost 表示本地主机,我们将它称之为本地环回,相当于我们一会先在本地测试一下能否正常通信,然后再进行网络通信的测试。

#include "udpServer.hpp"
#include <memory>
#include <stdio.h>
using namespace std;
using namespace Server;// 提示用法函数 -- 设为static是因为防止与客户端的用法函数冲突了!
static void Usage(string proc)
{cout << "\nUsage:\n\t" << proc << " local_port\n\n";
}void handler(int sockfd, string clientip, uint16_t clientport, string message)
{// 业务处理,后面会填充
}int main(int argc, char* argv[])
{if(argc != 2) // 如果参数传递不为两个,则提醒使用者,并且退出程序{Usage(argv[0]);exit(USAGE_ERR);}uint16_t port = atoi(argv[1]); // 将端口号类型转换为uint16_tunique_ptr<udpServer> udper(new udpServer(handlerMessage, port)); // 使用智能指针创建服务端对象udper->initServer();	// 初始化服务器udper->start();			// 启动服务器return 0;
}

​ 需要注意的是,agrv 数组里面存储的是字符串,而端口号是一个整数,因此需要使用 atoi 函数将字符串转换成整数。然后我们就可以用这个 IP 地址和端口号来构造服务器了,服务器构造完成并初始化后就可以调用 start 函数启动服务器了。

​ 此时带上端口号运行程序就可以看到套接字创建成功、绑定成功,现在服务器就在等待客户端向它发送数据。

在这里插入图片描述

​ 虽然现在客户端代码还没有编写,但是我们可以通过 netstat 命令来查看当前网络的状态,这里我们可以选择携带 -nlup 选项。

在这里插入图片描述

​ 其中 Proto 表示协议的类型,Recv-Q 表示网络接收队列,Send-Q 表示网络发送队列,Local Address 表示本地地址,Foreign Address 表示外部地址,State 表示当前的状态,PID 表示该进程的进程ID,Program name 表示该进程的程序名称。

​ 而 Foreign Address 写成 0.0.0.0:* 表示任意 IP 地址、任意端口号的程序都可以访问当前进程

绑定INADDR_ANY的好处

​ 当一个服务器的带宽足够大时,一台机器接收数据的能力就约束了这台机器的 IO 效率,因此一台服务器底层可能装有多张网卡,此时这台服务器就可能会有多个 IP 地址,但一台服务器上端口号为 8081 的服务只有一个。这台服务器在接收数据时,这里的多张网卡在底层实际都收到了数据,如果这些数据也都想访问端口号为 8081 的服务。此时如果服务端在绑定的时候是指明绑定的某一个 IP 地址,那么此时服务端在接收数据的时候就只能从绑定 IP 对应的网卡接收数据。而如果服务端绑定的是 INADDR_ANY,那么只要是发送给端口号为 8081 的服务的数据,系统都会可以将数据自底向上交给该服务端。

在这里插入图片描述

​ 因此服务端绑定 INADDR_ANY 这种方案也是 强烈推荐的方案,所有的服务器具体在操作的时候用的也就是这种方案。

​ 当然,如果你既想让外网访问你的服务器,但你又指向绑定某一个 IP 地址,那么就不能用云服务器,此时可以选择使用虚拟机或者你自定义安装的 Linux 操作系统,那个 IP 地址就是支持你绑定的,而云服务器是不支持的。

Ⅱ. UDP客户端

一、客户端创建流程

  1. 创建套接字(socket
  2. 绑定 IP 地址(一般来说 端口号不需要我们自己去绑定,操作系统在底层会判断当前有无空闲的端口号进行绑定,防止端口号冲突)
  3. 发送(sendto)、接收(recvfrom)数据,包括对数据进行处理

二、创建套接字

​ 同样的,我们把客户端也封装成一个类,当我们定义出一个客户端对象后也是需要对其进行初始化,而客户端在初始化时也需要创建套接字,之后客户端发送数据或接收数据也就是对这个套接字进行操作。

​ 客户端创建套接字时选择的协议家族也是 AF_INET,需要的服务类型也是 SOCK_DGRAM,当客户端被析构时也可以选择关闭对应的套接字。与服务端不同的是,客户端在初始化时只需要创建套接字就行了,而 不需要进行绑定操作

三、关于客户端的绑定问题

​ 首先,由于是网络通信,通信双方都需要找到对方,因此服务端和客户端都需要有各自的 IP 地址和端口号,只不过 服务端需要进行端口号的绑定,而 客户端不需要

​ 因为服务器就是为了给别人提供服务的,因此服务器必须要让别人知道自己的 IP 地址和端口号,IP 地址一般对应的就是域名,而端口号一般没有显示指明过,因此服务端的端口号一定要是一个众所周知的端口号,并且选定后不能轻易改变,否则客户端是无法知道服务端的端口号的,这就是服务端要进行绑定的原因,只有绑定之后这个端口号才真正属于自己,因为一个端口只能被一个进程所绑定,服务器绑定一个端口就是为了独占这个端口。

​ 而客户端在通信时虽然也需要端口号,但客户端一般是不进行手动绑定的,客户端在访问服务端的时候,端口号只要是唯一的就行了,不需要和特定客户端进程强相关。

​ 如果客户端绑定了某个端口号,那么以后这个端口号就只能给这一个客户端使用,就是这个客户端没有启动,这个端口号也无法分配给别人,并且如果这个端口号被别人使用了,那么这个客户端就无法启动了。所以客户端的端口只要保证唯一性就行了,因此客户端端口可以动态的进行设置,并且客户端的端口号不需要我们来设置,当 我们调用类似于 sendto 这样的接口时,操作系统会自动给当前客户端获取一个唯一的端口号

​ 也就是说,客户端每次启动时使用的端口号可能是变化的,此时只要我们的端口号没有被耗尽,客户端就永远可以启动。

四、搭建客户端框架

① 客户端类单进程版实现(读写会互相阻塞):udpclient.hpp

#pragma once
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <pthread.h>
using namespace std;namespace Client
{enum { USAGE_ERR = 1, SEND_ERR, SOCKET_ERR, CLOSE_ERR };class udpClient{private:uint16_t _serverPort; // 服务器端口string _serverIP;	  // 服务器ip地址int _socketfd; 		  // 文件描述符bool _quit;			  // 判断是否推出的标志public:udpClient(const string& ip, const uint16_t port): _serverPort(port), _serverIP(ip), _socketfd(-1), _quit(false){}void initClient(){// 1. 创建套接字文件_socketfd = socket(AF_INET, SOCK_DGRAM, 0);if(_socketfd == -1){cerr << "socket error: " << errno << " : " << strerror(errno) << endl; exit(SOCKET_ERR);}cout << "socket success: " << _socketfd << endl;// 2. 绑定当前客户端的ip和端口号到套接字,而ip和端口号其实不需要明确绑定,// 因为套接字会自动绑定到系统分配的本地IP地址和端口号上,所以一般我们可以不显式绑定!       }void run(){   // 将要发送到目的端的信息填上struct sockaddr_in destination;memset(&destination, 0, sizeof destination);destination.sin_family = AF_INET;destination.sin_addr.s_addr = inet_addr(_serverIP.c_str());destination.sin_port = htons(_serverPort);string message; // 要发送的信息while(!_quit){// 发送信息,建议还是统一使用C语言的形式使用读写操作cout << "Please enter the message you want to send: ";char line[1024];fgets(line, sizeof(line), stdin);message = line;ssize_t n = sendto(_socketfd, message.c_str(), message.size(), 0, (struct sockaddr*)&destination, sizeof(destination));if(n == -1){cerr << "send error: " << errno << " : " << strerror(errno) << endl; exit(SEND_ERR);}// 接收信息,但是后面这里我们会改成多线程,因为上面的输入导致了阻塞char buffer[1024];struct sockaddr_in tmp;socklen_t tmplen = sizeof(tmp);ssize_t s = recvfrom(_socketfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&tmp, &tmplen);if(s > 0){buffer[s] = '\0';cout << buffer << endl;}}}};
}

💥② 客户端类多线程版实现(读写分离):udpclient.hpp

​ 因为上面的客户端是读写不分离的,所以会互相影响,那我们就搞个多线程的版本,其实也就是多开一个线程,给读写消息自己开一个空间去完成任务!主要和上面区分开的点和注意的点如下:

  1. 在类内 多线程函数需要加一个 static 保证其不会接收到一个 this 指针!
  2. 因为我们可以直接在 run() 启动函数中进行消息的发送,所以我们只需要多开一个线程去完成消息的接收即可,不需要开两个线程!并且==记得接收信息的线程要进行线程分离==,让它们撇清关系!
  3. 下面在发送信息的时候,因为我们下面会给出几个业务处理的小样例,其中包括简单的聊天室,那么我们就可以简单的通过管道来进行输入框和显示框的分离达到聊天软件那种效果!但是因为如果我们都是通过 stdout 来重定向到管道文件中的话,就做不到输入框也能显示内容了,因为全都被重定向到管道文件中去了!
    • 为了解决这个问题,我们就用 stderr 来充当输入框的内容显示!因为与 stdout 不同的是,stderr 通常用于输出不应被重定向的信息,以便及时地将错误和警告信息显示给用户,它也是属于输出流!并且它们还是独立的,互不影响!
    • stderr 输出流的内容没有被重定向到管道文件中的原因:重定向操作符 >>> 默认只会重定向标准输出 stdout,而不会重定向标准错误 stderr
  4. 要注意使用 fgets 函数来接收键盘输入的时候,回车键也是会被接收的,所以如果需要输入打印出来不显示多一个回车的话,那么就要将其置为 \0
#pragma once
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <pthread.h>
using namespace std;namespace Client
{enum { USAGE_ERR = 1, SEND_ERR, SOCKET_ERR, CLOSE_ERR };class udpClient{private:uint16_t _serverPort; // 服务器端口string _serverIP;	  // 服务器ip地址int _socketfd; 		  // 文件描述符bool _quit;			  // 判断是否推出的标志pthread_t _reader;	  // 线程标识符public:udpClient(const string& ip, const uint16_t port): _serverPort(port), _serverIP(ip), _socketfd(-1), _quit(false){}void initClient(){// 1. 创建套接字文件_socketfd = socket(AF_INET, SOCK_DGRAM, 0);if(_socketfd == -1){cerr << "socket error: " << errno << " : " << strerror(errno) << endl; exit(SOCKET_ERR);}cout << "socket success: " << _socketfd << endl;// 2. 绑定当前客户端的ip和端口号到套接字,而ip和端口号其实不需要明确绑定,// 因为套接字会自动绑定到系统分配的本地IP地址和端口号上,所以一般我们可以不显式绑定!       }// 读信息线程static void* recv_routine(void* args){// 读取服务端信息pthread_detach(pthread_self()); // 分离线程int socketfd = *(static_cast<int*>(args));char buffer[1024];while(true){struct sockaddr_in tmp;socklen_t tmplen = sizeof(tmp);ssize_t s = recvfrom(socketfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&tmp, &tmplen);if(s > 0){buffer[s] = '\0';cout << buffer << endl;}}return nullptr;}void run(){   // 创建一个读线程,负责读取服务端发来的信息,是为了防止下面的写造成了堵塞而读取的效果不佳pthread_create(&_reader, nullptr, recv_routine, (void*)&_socketfd);// 将要发送到目的端的信息填上struct sockaddr_in destination;memset(&destination, 0, sizeof destination);destination.sin_family = AF_INET;destination.sin_addr.s_addr = inet_addr(_serverIP.c_str());destination.sin_port = htons(_serverPort);string message; // 要发送的信息char line[1024];while(!_quit){// 负责发送信息// 这里用stderr和stdout来区分开打印,是为了配合后面聊天室功能的输入框和显示框的分离// 其中我们让stderr负责的是输入框显示输入内容,而stdout负责的是显示框部分的显示接收消息的内容// 而stderr的内容没有被重定向到管道文件中的原因是,重定向操作符>和>>默认只会重定向标准输出(stdout),而不会重定向标准错误(stderr)// 具体还是结合后面业务处理功能函数一起看效果会更清楚一些!fprintf(stderr, "Enter# ");       // 输入框提示内容写到stderr上fflush(stderr);                   // 直接刷新stderr的话,与stdout并不冲突,它们是独立的,所以会立刻显示到stderrfgets(line, sizeof(line), stdin); // 建议还是统一使用C语言的形式使用读写操作line[strlen(line) - 1] = '\0';    // 注意这里回车也会被放到字符串中,所以要将回车变成0message = line;ssize_t n = sendto(_socketfd, message.c_str(), message.size(), 0, (struct sockaddr*)&destination, sizeof(destination));if(n == -1){cerr << "send error: " << errno << " : " << strerror(errno) << endl; exit(SEND_ERR);}}}};
}

③ 客户端主函数:udpclient.cpp

​ 客户端的主函数的主要任务就是启动客户端去发送信息给服务端即可!

#include "udpClient.hpp"
#include <memory>
using namespace Client;static void Usage(string proc)
{// 这里用cerr是为了配合聊天室的输入框和显示框的分离,stderr负责的是输入框,所以我们要在提示框进行用法提醒cerr << "\nUsage:\n\t" << proc << " destination_ip destination_port\n\n"; 
}int main(int argc, char* argv[])
{if(argc != 3) // 如果参数传递不为两个,则提醒使用者,并且退出程序{Usage(argv[0]);exit(USAGE_ERR);}string destination_ip = argv[1];uint16_t destination_port = atoi(argv[2]);unique_ptr<udpClient> udper(new udpClient(destination_ip, destination_port)); // 使用智能指针管理客户端对象udper->initClient();udper->run();return 0;
}

Ⅲ. 服务端加入业务处理

​ 下面给出三个常见的例子,这部分是为了完善服务端内部的业务处理,因为服务端收到客户端的消息,肯定是为了做业务处理才存在的,而不是单单接收一个消息!

一、简单的中英文翻译

#include "udpServer.hpp"
#include <memory>
#include <unordered_map>
#include <fstream>
#include <signal.h>
#include <stdio.h>
using namespace std;
using namespace Server;static void Usage(string proc)
{cout << "\nUsage:\n\t" << proc << " local_port\n\n";
}static const string dictpath = "./dict.txt"; // 单词文件的路径
static unordered_map<string, string> dict;   // 存放单词的容器// 初始化字典的函数 -- 也就是将文件中的数据拿出来放到哈希表中
static void initdict(char Separator)
{ifstream in(dictpath, ios::binary);if(!in.is_open()) {cerr << "open file " << dictpath << " error" << endl; exit(OPEN_ERR);}// 按行获取文件中的单词,比如apple:苹果,注意下面使用的substr是左闭右开的string line;while(getline(in, line)){size_t pos = line.find(Separator);if(pos == string::npos){cerr << "未找到对应单词的翻译" << endl;exit(NOTFOUND_ERR);}dict[line.substr(0, pos)] = line.substr(pos + 1); // 找到了就写到字典中}in.close();cout << "load dict success" << endl;
}// 测试单词是否成功读取的测试函数
static void debugPrint()
{for(auto &e : dict){cout << e.first << " : " << e.second << endl;}
}// demo1 -- 简单的中英文翻译
void handlerMessage1(int sockfd, string clientip, uint16_t clientport, string message)
{// 这样子做就可以对message进行特定的业务处理,而不关心message怎么来的 ---- server通信和业务逻辑解耦!// 这是相对婴儿版的业务逻辑string response_message;auto iter = dict.find(message);if(iter == dict.end())response_message = "unknown!";elseresponse_message = iter->second;// 发回给客户端 -- 这部分与下面的其它demo都是一样的struct sockaddr_in client;bzero(&client, 0);client.sin_family = AF_INET;client.sin_port = htons(clientport);client.sin_addr.s_addr = inet_addr(clientip.c_str());ssize_t n = sendto(sockfd, response_message.c_str(), response_message.size(), 0, (struct sockaddr*)&client, sizeof client);if(n == -1){cerr << "send error: " << errno << " : " << strerror(errno) << endl; exit(SEND_ERR);}
}// 自定义信号,达到热加载目的
static void reload(int signo)
{(void)signo;initdict(':');
}int main(int argc, char* argv[])
{if(argc != 2) // 如果参数传递不为两个,则提醒使用者,并且退出程序{Usage(argv[0]);exit(USAGE_ERR);}uint16_t port = atoi(argv[1]); // 将端口号类型转换为uint16_t// 热加载功能,也就是我们不需要退出程序进行字典的更新// 只需要捕捉2号信号,让它帮助我们去重新调用一次initdict()函数即可!signal(2, reload); initdict(':');debugPrint();unique_ptr<udpServer> udper(new udpServer(handlerMessage1, port)); // 创建服务端对象udper->initServer();udper->start();return 0;
}

​ 下面我们创建一个字典文档,填几个单词翻译进去:

apple:苹果
liren:利刃
banana:香蕉
tcp:传输控制协议

​ 然后运行起来看看效果:

在这里插入图片描述

​ 因为我们有热加载功能,所以当我们需要往字典中添加新的单词翻译的时候,只需要往文档中直接加入,然后通过 ctrl+c 来捕捉 2 号信号,在该信号中又会去调用一次加载字典的函数,达到热加载的功能!

​ 比如我们现在往字典中添加 nihao:你好呀,然后在服务端通过键盘输入热键:
在这里插入图片描述

​ 这样子我们就不用退出服务器重新加载了,非常方便!

二、简单的远程命令行指令解析

​ 这里用到的函数就是我们在套接字接口那介绍的 popen 函数了,非常的好用,就是 pipe + fork + exec* 的组合函数!另外为了防止有人在远程命令行捣乱,用一些不合法的指令,所以我们可以适当的排除一些风险的指令的执行!

​ 并且其实我们 只需要改动业务处理部分,对于回传信息给客户端的内容,其实和上面是一模一样的,所以有兴趣的话是可以包装成一个接口去调用的!

#include "udpServer.hpp"
#include <memory>
#include <unordered_map>
#include <fstream>
#include <signal.h>
#include <stdio.h>
using namespace std;
using namespace Server;
static void Usage(string proc)
{cout << "\nUsage:\n\t" << proc << " local_port\n\n";
}// demo2 -- 远程命令行指令解析
void handlerMessage2(int sockfd, string clientip, uint16_t clientport, string cmd)
{// 先排除一些有风险的指令if(cmd.find("rm") != string::npos|| cmd.find("mv") != string::npos|| cmd.find("cp") != string::npos|| cmd.find("rmdir") != string::npos|| cmd.find("while") != string::npos){cerr << clientip << ": " << clientport << " 正在做一个非法的操作: " << cmd << endl;return;}// 使用popen函数,其相当于pipe + fork + exec*FILE* stream = popen(cmd.c_str(), "r");string response;if(stream == nullptr)response = cmd + " exec failed";char line[1024];while(fgets(line, sizeof(line), stream)){response += line; // 按行读取}///// 发回给客户端(这部分都是一致的,无需修改)struct sockaddr_in client;bzero(&client, 0);client.sin_family = AF_INET;client.sin_port = htons(clientport);client.sin_addr.s_addr = inet_addr(clientip.c_str());ssize_t n = sendto(sockfd, response.c_str(), response.size(), 0, (struct sockaddr*)&client, sizeof client);if(n == -1){cerr << "send error: " << errno << " : " << strerror(errno) << endl; exit(SEND_ERR);}pclose(stream); // 使用这个而不是fclose来关闭
}int main(int argc, char* argv[])
{if(argc != 2) // 如果参数传递不为两个,则提醒使用者,并且退出程序{Usage(argv[0]);exit(USAGE_ERR);}uint16_t port = atoi(argv[1]); // 将端口号类型转换为uint16_tunique_ptr<udpServer> udper(new udpServer(handlerMessage1, port)); // 创建服务端对象udper->initServer();udper->start();return 0;
}

在这里插入图片描述

三、简易的小聊天室

​ 首先我们得先有描述一个成员的类以及管理聊天室里面成员的类,所以我们先创建一个 onlineUser.hpp 来实现这两个类:

#pragma once
#include <iostream>
#include <string>
#include <unordered_map>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
using namespace std;class User
{
public:User(const string& ip, uint16_t port) : _ip(ip), _port(port) {}string& getip() { return _ip; }uint16_t getport() { return _port; }
private:string _ip;uint16_t _port;
};class onlineUser
{
private:unordered_map<string, User> _users; // 通过哈希表来管理用户public:void addOnlineUser(const string& ip, uint16_t port) // 添加用户函数{string id = ip + "-" + to_string(port);_users.insert(make_pair(id, User(ip, port)));}void delOnlineUser(const string& ip, uint16_t port) // 删除用户函数{string id = ip + "-" + to_string(port);_users.erase(id);}bool isOnlineUser(const string& ip, uint16_t port)  // 判断用户是否在线函数{string id = ip + "-" + to_string(port);return _users.find(id) == _users.end() ? false : true;}void broadcastMessage(int sockfd, const string& ip, uint16_t port, const string& message) // 向聊天室的成员进行广播函数{for(auto& user: _users){// 这里广播的消息就是发消息这个用户的【ip + 端口 + 消息】string id = ip + "-" + to_string(port) + "# " + message;struct sockaddr_in client;bzero(&client, sizeof client);client.sin_family = AF_INET;client.sin_port = htons(user.second.getport());client.sin_addr.s_addr = inet_addr(user.second.getip().c_str());sendto(sockfd, id.c_str(), id.size(), 0, (struct sockaddr*)&client, sizeof(client));}}
};

​ 接着就是继续在 udpServer.cc 中实现这部分的业务处理逻辑代码:

#include "udpServer.hpp"
#include <memory>
#include <unordered_map>
#include <fstream>
#include <signal.h>
#include <stdio.h>
using namespace std;
using namespace Server;
static void Usage(string proc)
{cout << "\nUsage:\n\t" << proc << " local_port\n\n";
}// demo3 -- 一个简易的小聊天室
onlineUser users;void handlerMessage3(int sockfd, string clientip, uint16_t clientport, string cmd)
{// 判断是否为上下线请求if(cmd == "online")users.addOnlineUser(clientip, clientport);else if(cmd == "offline")users.delOnlineUser(clientip, clientport);// 判断是否在线,是的话则进行信息的广播,不是的话则提示请登录if(users.isOnlineUser(clientip, clientport) == true){// 在线的话,进行消息的广播users.broadcastMessage(sockfd, clientip, clientport, cmd);}else{// 不在线的话,单独提示需要上线struct sockaddr_in client;bzero(&client, sizeof(client));client.sin_family = AF_INET;client.sin_port = htons(clientport);client.sin_addr.s_addr = inet_addr(clientip.c_str());string response = "你还没有上线,请先上线,运行: online";sendto(sockfd, response.c_str(), response.size(), 0, (struct sockaddr*)&client, sizeof client);}
}int main(int argc, char* argv[])
{if(argc != 2) // 如果参数传递不为两个,则提醒使用者,并且退出程序{Usage(argv[0]);exit(USAGE_ERR);}uint16_t port = atoi(argv[1]); // 将端口号类型转换为uint16_tunique_ptr<udpServer> udper(new udpServer(handlerMessage3, port)); // 创建服务端对象udper->initServer();udper->start();return 0;
}

​ 为了有输入框和显示框的分离效果,这里我们 创建一个命名管道,将我们输入的内容显示在输入框中,而输出的内容则通过管道显示在另一个正在查看命名管道的终端下,达到分离效果,如下所示:

在这里插入图片描述

Ⅳ. windows系统的客户端

​ 其实我们写的那一套代码,在其它的操作系统上同样是能够访问的,比如 windows,下面我们就拿这个操作系统来做演示!

​ 具体的一些接口这里就不介绍了,主要是为了展现一下在不同操作系统之间同样是能够互通的,说明的是网络编程其实底层用的都是同一套,但是可能不同平台的接口封装不太一样,就像 c/c++linux 上的接口,很原滋原味的!

​ 而 windows 系统的网络接口其实也差不多,只是有些函数要需要依赖其它的一些组件罢了!

#define  _WINSOCK_DEPRECATED_NO_WARNINGS
#include <iostream>
#include <string>
#include <cstring>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
using namespace std;uint16_t server_port = 8080;	   // 服务器端口号
string server_ip = "81.71.97.127"; // 服务器ipint main() 
{WSADATA wsaData;// 初始化套接字环境WSAStartup   if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0){cout << "WSAStartup failed with error: " << WSAGetLastError() << endl;return 1;}else{cout << "WSAStartup Success" << endl;}// 创建套接字,初始化地址和端口SOCKET csock = socket(AF_INET, SOCK_DGRAM, 0);if (csock == SOCKET_ERROR){cout << "socket failed with error: " << WSAGetLastError() << endl;closesocket(csock);WSACleanup();return 1;}else{cout << "socket success" << endl;}struct sockaddr_in server;memset(&server, 0, sizeof server);server.sin_family = AF_INET;server.sin_port = htons(server_port);server.sin_addr.s_addr = inet_addr(server_ip.c_str());char inbuffer[1024];string send_message;while (true){// 发送信息cout << "Please Enter# ";getline(cin, send_message);int s = sendto(csock, send_message.c_str(), (int)send_message.size(), 0, (struct sockaddr*)&server, (int)sizeof(server));if (s == -1){cout << "send failed" << endl;break;}// 接收信息struct sockaddr_in peer;int peerlen = sizeof peer;inbuffer[0] = 0; // C语言风格的清零int n = recvfrom(csock, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&peer, &peerlen);if (n > 0){inbuffer[n] = 0;cout << "Server返回的信息是# " << inbuffer << endl;}elsebreak;}closesocket(csock);WSACleanup();return 0;
}

​ 服务端的话我们就用简单的业务处理,进行返回即可:

#include "udpServer.hpp"
#include <memory>
#include <unordered_map>
#include <fstream>
#include <signal.h>
#include <stdio.h>
using namespace std;
using namespace Server;
static void Usage(string proc)
{cout << "\nUsage:\n\t" << proc << " local_port\n\n";
}
void handlerMessage(int sockfd, string clientip, uint16_t clientport, string message)
{string response_message;response_message += " [server echo]";// 发回给客户端struct sockaddr_in client;bzero(&client, 0);client.sin_family = AF_INET;client.sin_port = htons(clientport);client.sin_addr.s_addr = inet_addr(clientip.c_str());ssize_t n = sendto(sockfd, response_message.c_str(), response_message.size(), 0, (struct sockaddr*)&client, sizeof client);if(n == -1){cerr << "send error: " << errno << " : " << strerror(errno) << endl; exit(SEND_ERR);}
}
int main(int argc, char* argv[])
{if(argc != 2) // 如果参数传递不为两个,则提醒使用者,并且退出程序{Usage(argv[0]);exit(USAGE_ERR);}uint16_t port = atoi(argv[1]); // 将端口号类型转换为uint16_tunique_ptr<udpServer> udper(new udpServer(handlerMessage, port)); // 创建服务端对象udper->initServer();udper->start();return 0;
}

在这里插入图片描述

在这里插入图片描述

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

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

相关文章

准确--Notepad++ 实用的插件介绍

Notepad 提供了很多实用的插件&#xff0c;可以极大地提升编程和文本编辑的效率。以下是一些常用且有用的插件介绍&#xff1a; 1. NPP Export 功能&#xff1a;可以将打开的文件导出为 HTML 或 RTF 格式&#xff0c;方便生成漂亮的代码文档。用途&#xff1a;适合需要将代码…

[20250507] AI边缘计算开发板行业调研报告 ​​(2024年最新版)​

[20250507] AI边缘计算开发板行业调研报告 ​​(2024年最新版&#xff09;​ 一、行业背景​​ 随着物联网设备激增与AI模型轻量化&#xff0c;边缘计算成为AI落地核心场景。AI边缘计算开发板&#xff08;Edge AI Board&#xff09;作为硬件载体&#xff0c;需满足​​低延迟…

传输层协议 1.TCP 2.UDP

传输层协议 1.TCP 2.UDP TCP协议 回顾内容 传输层功能&#xff1a;定义应用层协议数据报文的端口号&#xff0c;流量控制对原始数据进行分段处理 传输层所提供服务 传输连接服务数据传输服务&#xff1a;流量控制、差错控制、序列控制 一、传输层的TCP协议 1.面向连接的…

LVGL -meter的应用

1 meter介绍 lv_meter 是 LVGL v8 引入的一种图形控件&#xff0c;用于创建仪表盘样式的用户界面元素&#xff0c;它可以模拟像速度表、电压表、温度表这类模拟表盘。它通过可视化刻度、指针、颜色弧线等来展示数值信息&#xff0c;是一种非常直观的数据展示控件。 1.1 核心特…

GoFly企业版框架升级2.6.6版本说明(框架在2025-05-06发布了)

前端框架升级说明&#xff1a; 1.vue版本升级到^3.5.4 把"vue": "^3.2.40",升级到"vue": "^3.5.4"&#xff0c;新版插件需要时useTemplateRef,所以框架就对齐进行升级。 2.ArcoDesign升级到2.57.0&#xff08;目前最新2025-02-10&a…

阿里联合北大开源数字人项目FantasyTalking,输出内容更加动态化~

简介 FantasyTalking 的核心目标是从单一静态图像、音频&#xff08;以及可选的文本提示&#xff09;生成高保真、连贯一致的说话肖像。研究表明&#xff0c;现有方法在生成可动画化头像时面临多重挑战&#xff0c;包括难以捕捉细微的面部表情、整体身体动作以及动态背景的协调…

基于nnom的多选择器

核心组件 元件类型目的接口STM32F103CB微控制器主处理单元-MPU60506 轴 IMU移动侦测I2C 接口W25Q64 系列闪存信号和配置存储SPI 系列按钮用户输入模式选择和激活GPIO &#xff08;通用输出&#xff09;搭载了LED用户反馈系统状态指示GPIO &#xff08;通用输出&#xff09;RT6…

Redis中6种缓存更新策略

Redis作为一款高性能的内存数据库&#xff0c;已经成为缓存层的首选解决方案。然而&#xff0c;使用缓存时最大的挑战在于保证缓存数据与底层数据源的一致性。缓存更新策略直接影响系统的性能、可靠性和数据一致性&#xff0c;选择合适的策略至关重要。 本文将介绍Redis中6种缓…

项目优先级频繁变动,如何应对?

项目优先级频繁变动是许多公司和团队在工作中常遇到的挑战。 这种情况通常由业务需求变化、市场压力或高层决策调整等因素引起&#xff0c;常常让团队成员感到困惑和不安。首先&#xff0c;制定明确的优先级管理框架是应对项目优先级变动的基础&#xff0c; 通过清晰的优先级排…

屏蔽力 | 在复杂世界中从内耗到成长的转变之道

注&#xff1a;本文为“屏蔽力”相关文章合辑。 略作重排&#xff0c;未全整理。 世上的事再复杂&#xff0c;不外乎这三种 原创 小鹿 读者 2022 年 12 月 02 日 18 : 27 甘肃 文 / 小鹿 在这世上&#xff0c;每天都有大事小事、琐事烦事。我们总为世事奔波忙碌&#xff0c;…

[数据处理] 3. 数据集读取

&#x1f44b; 你好&#xff01;这里有实用干货与深度分享✨✨ 若有帮助&#xff0c;欢迎&#xff1a;​ &#x1f44d; 点赞 | ⭐ 收藏 | &#x1f4ac; 评论 | ➕ 关注 &#xff0c;解锁更多精彩&#xff01;​ &#x1f4c1; 收藏专栏即可第一时间获取最新推送&#x1f514;…

IIS配置SSL

打开iis 如果搜不到iis&#xff0c;要先开 再搜就打得开了 cmd中找到本机ip 用http访问本机ip 把原本的http绑定删了 再用http访问本机ip就不行了 只能用https访问了

RabbitMQ的交换机

一、三种交换机模式 核心区别对比​​ ​​特性​​​​广播模式&#xff08;Fanout&#xff09;​​​​路由模式&#xff08;Direct&#xff09;​​​​主题模式&#xff08;Topic&#xff09;​​​​路由规则​​无条件复制到所有绑定队列精确匹配 Routing Key通配符匹配…

(2025,AR,NAR,GAN,Diffusion,模型对比,数据集,评估指标,性能对比)文本到图像的生成和编辑:综述

【本文为我在去年完成的综述&#xff0c;因某些原因未能及时投稿&#xff0c;但本文仍能为想要全面了解文本到图像的生成和编辑的学习者提供可靠的参考。目前本文已投稿 ACM Computing Surveys。 完整内容可在如下链接获取&#xff0c;或在 Q 群群文件获取。 中文版为论文初稿&…

MCU怎么运行深度学习模型

Gitee仓库 git clone https://gitee.com/banana-peel-x/freedom-learn.git项目场景&#xff1a; 解决面试时遗留的问题&#xff0c;面试官提了两个问题&#xff1a;1.单片机能跑深度学习的模型吗&#xff1f; 2.为什么FreeRTOS要采用SVC去触发第一个任务&#xff0c;只用Pend…

多模态学习(一)——从 Image-Text Pair 到 Instruction-Following 格式

前言 在多模态任务中&#xff08;例如图像问答、图像描述等&#xff09;&#xff0c;为了使用指令微调&#xff08;Instruction Tuning&#xff09;提升多模态大模型的能力&#xff0c;我们需要构建成千上万条**指令跟随&#xff08;instruction-following&#xff09;**格式的…

MySQL基础关键_011_视图

目 录 一、说明 二、操作 1.创建视图 2.创建可替换视图 3.修改视图 4.删除视图 5.对视图内容的增、删、改 &#xff08;1&#xff09;增 &#xff08;2&#xff09;改 &#xff08;3&#xff09;删 一、说明 只能将 DQL 语句创建为视图&#xff1b;作用&#xff1a; …

『深夜_MySQL』数据库操作 字符集与检验规则

2.库的操作 2.1 创建数据库 语法&#xff1a; CREATE DATABASE [IF NOT EXISTS] db_name [create_specification [,create_specification]….]create_spcification:[DEFAULT] CHARACTER SET charset_nam[DEFAULT] COLLATE collation_name说明&#xff1a; 大写的表示关键字 …

Spark jdbc写入崖山等国产数据库失败问题

随着互联网、信息产业的大发展、以及地缘政治的变化,网络安全风险日益增长,网络安全关乎国家安全。因此很多的企业,开始了国产替代的脚步,从服务器芯片,操作系统,到数据库,中间件,逐步实现信息技术自主可控,规避外部技术制裁和风险。 就数据库而言,目前很多的国产数据…

数字化转型-4A架构之应用架构

系列文章 数字化转型-4A架构&#xff08;业务架构、应用架构、数据架构、技术架构&#xff09;数字化转型-4A架构之业务架构 前言 应用架构AA&#xff08;Application Architecture&#xff09;是规划支撑业务的核心系统与功能模块&#xff0c;实现端到端协同。 一、什么是应…