当前位置: 首页 > news >正文

『Linux_网络』 第二章 UDP_Socket编程

     学习了网络的概念了,接下来我们开始实践,本次我们会通过UDP来模拟实现UDP客户端和UDP服务器之间的通信,以及在此基础上扩展几个应用。

      下面,我们将使用socket,bind,htons等接口实现UDP网络通信。

v1 版本 - echo server(回显服务器)

服务端

 Init 服务器的初始化

       这里我们先定义几个全局默认值,并且了解下有没有static修饰的区别

      socket函数

  • 第一个参数用于确认使用说明协议家族,AF_INET 表示IPv4协议,AF_INET6 表示IPv6协议;
  • 第二个参数 指定socket类型, SOCK_STREAM 表示TCP Socket,SOCK_DGRAM表示UDP Socket;
  • 第三个参数 指定具体的协议,通常为0,表示让系统根据domain和type自动选择合适的协议。

返回值是一个fd文件描述符,所以,socket函数本质上就是创建了一个文件描述符

UdpServer.hpp

这里引用我们之前写的日志功能文档,

这里解释下为什么不直接在构造函数中创建套接字,而是另外写了一个接口来创建套接字,有下面几点考虑:

  •  构造函数异常处理局限性: 如果构造函数在执行过程中抛出异常,对象就无法完整构造,对象未完全构造成功,析构函数就不会被调用,可能没法正确释放已分配的部分资源,也就可能会造成资源泄漏。
  • 独立出init接口则就规避了这个问题,如果Socket申请失败,可以返回错误码,我们可以通过错误码进行相应的处理,同时也能保证对象已经正确构造,析构函数可以正常释放资源。
  • 构造函数的初衷只是创建对象并进行必要的成员变量初始化,将 Socket 申请逻辑放入构造函数会让构造函数变得复杂,违背了单一职责原则。独立的 init 接口能让构造函数保持简洁,仅完成对象的基本创建。
  • 复用初始化逻辑:将 Socket 申请逻辑放入构造函数会让构造函数变得复杂,违背了单一职责原则。独立的 init 接口能让构造函数保持简洁,仅完成对象的基本创建。

基于以上几点,我们选择将创建套接字独立成一个init接口。

 bind函数

        服务器程序所监听的网络地址和端口号是固定不变的,客户端程序得知服务器程序的ip地址和端口号就可以向服务器发起连接,所以服务器需要调用bind绑定一个固定的网络地址和端口号来表示自己。

  • 第一个参数是刚刚socket函数所创建的套接字描述符也就是它的返回值;
  • 第二个参数是一个指向 struct sockaddr类型的指针,它包含了要绑定的网络地址和端口信息,不过在实际编程汇总,我们通常会使用 struct sockadd_in(用于IPv4)或者struct sockaddr_in6(用于IPv6)来填充地址,之后再强转为 struct sockaddr类型;
  • 第三个参数 表示的是 addr指向地址的结构体的长度。

主机中有那么多进程需要通信,每一个进程都会有一个sockfd,端口什么的一信息,那么OS是如何进行管理的呢?

        答案就是先描述在组织,只不过因为是我们进行网络编程,信息是什么只有我们知道,所以这次描述的这个结构体信息,是由我们来进行填写的,那么怎么将我们填写的信息和真正的socketfd绑定到一起呢?

        用的就是bind函数,来完成绑定的。绑定之后,OS就知道了这个socketfd和哪个结构体是关联的了。

        所以bind函数的作用就是将参数soketfd和myaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听 myaddr 所描述的地址和端口号。

需要注意的是,第二个参数:我们是要进行IPv4网络通信的,需要使用的是 struct sockaddr_in 这个结构体,而不是使用struct sockaddr。 struct sockaddr 更像是C++ 中的基类,是专门为设计接口用的。

        struct sockaddr* 是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。

bzero函数

 bzero 函数的主要作用是将一块内存区域的所有字节都设置为零。在网络通信中,常常用于需要对存储网络地址、套接字地址等信息的结构体进行初始化,确保这些结构体的数据是已知状态,避免了未初始化导致的错误。

参数: s一个void类型指针,指向要清零的内存区域的起始地址。 n表示要清零的字节数。

功能上也可以使用memset来代替。

在正式填写结构体前,我们需要使用bzero来将结构体内容初始化为0。

sockaddr_in 结构体,我们只需要填写前三个字段,sin_zero 称之为这个结构体的填充字段。什么是填充字段? 就是为了使sockaddr_in结构体的长度和通用的strcut sockaddr结构体长度相同,在使用的时候,一般将其全部置为0,起到占位符的效果。

需要注意的是: sockaddr_in 结构体位于 netinet/in.h 头文件中,sockaddr 结构体位于 sys/socket.h 头文件中。

我们可以通过跳转的方式,查看这个结构体和要填写的三个字段分别是什么

sin_port 本质上是一个16位短整型整数,代表进程端口号。

 通过上面的sockaddr_in 结构体,我们发现里面好像找不到sin_family字段啊。

其实这个字段就是sin_family 字段

我们跳转到它的定义看看

 这里实际上是用来一个宏定义函数,将传入的参数作为了变量的名称,##的用法还记得吗?

##的用法

在C++中,## 是预处理运算符,被称为 连接符 或者 记号连接运算符 作用就是在预处理阶段把左右两个记号连接成一个新的记号。

用法 :

        可以用于 动态生成标识符,也可以用于实现函数重载辅助宏

例如:

#include <iostream>// 定义函数重载辅助宏
#define FUNCTION_OVERLOAD(type) void func_##type(type value) { std::cout << "Function for " #type ": " << value << std::endl; }// 生成不同类型的函数
FUNCTION_OVERLOAD(int)
FUNCTION_OVERLOAD(double)int main() {func_int(10);func_double(3.14);return 0;
}

很显然,在这里## 是第一种用法,将左右两种记号生成了一个新的记号。这个字段的类型是sa_family_t ,本质上是一种无符号短整型类型。

 sin_addr本质上也是一种结构体, 结构体 in_addr 里面是一个无符号整型。这个字段可以理解为socket inet,表示IP地址。

 我们存储IP的时候,使用的是字符串类型。为什么使用字符串呢? 因为用户使用的是点分十进制字符串格式的IP地址。

     结构体存储IP地址使用整型格式,我们存储使用字符串,这就涉及到整型和字符串之间的转换。那怎么转换? 思路如下图伪代码:

点分十进制转换成整数,是非常常用的功能。C程序设计者早就考虑到了,已经在库中帮我们实现了,不需要我们自己实现。

 inet_addr函数

我们将点分十进制格式的IP转成整数后,赋值给sin_addr,发现报错了。这是为啥? 这是因为sin_addr 是一个in_addr_t的结构体,我们可以通过inet_addr函数将我们的ip点分十进制字符串格式转化为in_addr_t 的结构体,同时补充我们的成员变量,以及改写下构造函数。

设置好 sockaddr_in 结构体之后,就把socket信息,设置进入内核中了??

并没有,只是填充了结构体!

接下来,我们建立bind绑定,才是设置进内核中。

绑定之后,我们服务器就初始化结束了,接下来就该处理服务器运行时该处理的行为了—— 收发信息

Run 服务器的运行 

虽然UDP使用的是socketfd,本质上是一个文件,但UDP是无法直接使用read和write函数进行通信的,因为read和write是直接面向字节流套接字的,而UDP是面向数据包的。

recvfrom 函数

        这里我们使用的函数是recvfrom                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        

 recvfrom函数从制定套接字读到一个报文,读报文需要传一个缓冲区和缓冲区的长度,flag设置为0即可。要收到一条信息,你要不要知道是谁发的,这就是最后两个参数的作用。

如何知道对方是谁呢?  不要忘了 sockaddr_in 里面可是有端口号和IP的,通过端口号+IP就可以表示一个进程。

当我们把数据处理完后,怎么发回去给对方呢? 

sendto 函数

这里我们使用的是sendto函数

参数部分,都很熟悉,就不做过多介绍了。

Stop 服务器的退出

接下来,虽然服务器一般情况下,是不会停止的,但是在部分情况下,服务器也是需要停下来,维护的,所以这里我们可以写一个stop接口,来表示服务器的结束。

当服务器结束退出的时候,不要忘记关闭socketfd,socketfd本质上就是一个文件描述符,所以我们可以使用关闭文件描述符的方式关闭它。

 测试服务器

写一个简单的Main.cc 测试下服务器,这里端口号随便设置一个在1024 - 49151范围内的就行,IP就用我们的云服务器的IP。

Main.cc

 编译运行,我们发现报错了。

错误码99, 错误码 99 对应的是 EADDRNOTAVAIL,表示指定的地址不可用。

我们填写的事我们的云服务器的IP啊,为什么指定的地址不可用呢?

这是因为云服务器是禁止直接bind公网IP的。一台主机可能有两张网卡,如果我们bind一张网卡,就无法接受另一张网卡发来的数据。我们想要bind任意网址,怎么操作?

第一种就是使用 0.0.0.0 IP , 在IPv4 地址体系里,0.0.0.0 表示 接受任意IP发来的数据。

 第二种方法,将s_addr的值设置为INADDR_ANY。

INADDR_ANY  是一个在 <netinet/in.h> 头文件中定义的常量,通常被定义为 0.0.0.0,表示本地的任意IP地址,因为服务器有可能有多个网卡,每个网卡都可能绑定多个iP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接才确定下来到底用哪个IP地址。

我们这里先尝试下第二种方式,将local中sin_addr中的s_addr设置为 INADDR_ANY

编译运行,发现程序成功运行出来了。

接下来,我们来尝试第一种方式上。

由于我们把IP的缺省值设置为0.0.0.0,我们只用传端口号就可以了。

编译运行,服务器就也运行起来了。

 当我们把服务器运行起来后,可以使用 netstat -naup指令  

   -n 选项 表示能显示成数字的字段,就显示成数字; - u选项,表示协议为UDP的;-a选项,表示所有的;-p选项,表示需要显示PID信息。

  Recv-Q和Send-Q表示收发报文的个数。Loacl Address 是本地地址的意思。 Foreign Address 是接受外来地址的意思,0.0.0.0:* 表示接受任何客户端发来的信息。

封装InetAddr&&Common

对于服务器和客户机,填写sockaddr_in结构体都是必须的,那与其写那么多重复的代码,不如我们直接封装一个sockaddr_in ,用于填写sockaddr_in 结构体以及网络地址和主机地址之间的转换。

在此之间,我们先简单封装一个Common.hpp ,这个头文件放置一些,在服务器和客户端编程中都会用到的通用的部分,一般用于放置,错误码,报错信息,宏定义什么的。

Common.hpp

封装过程中,我们会用到下面这个函数 inet_ntop 用法就如NAME处所写的那样,将IPv4和IPv6地址的从二进制转化为文本格式,返回值是存储文本格式dst缓冲区的地址。

其实还有一个函数 inet_ntoa 这个函数也用的比较多。

在上图中我们还看到了我们之前使用的inet_addr函数,其实这两个函数才是一对,它们可以相互转换,只不过和inet_ntop函数不同的是,它们两个只能用于Ipv4,而inet_ntop 则也适用于Ipv6,。

地址转换函数

需要注意的是:

        inet_ntoa 返回的值是一个地址,也不是一个常量地址,那我们没有创建任何字符数组,那这个地址是在哪开辟的呢?

      其实inet_ntoa 是使用内部静态缓冲区来存储转换后的字符串的, 内部静态缓冲区,在多线程的环境下,这不就是一个临界资源吗!

       所以inet_ntoa是一个线程不安全函数,当多个线程访问时,可能会造成数据混乱,并且这个缓冲区是开在内部的,缓冲区大小,我们也无法知道,而  inet_ntop 则不会有这个问题,缓冲区是我们自己开辟的,大小我们自己决定,生命周期也由我们自己决定,所以这也是我们之后选择使用inet_ntop的原因。

InetAddr.hpp

 接下来,我们来将我们封装的InetAddr和 Common 加入到我们的Udpsever中

 我们也重新写一下Main.cc,改为交互式的,增加灵活性,不要硬编程端口号

客户端

        客户端的写法和服务器的写法基本一致,进行网络通信,创建属于自己的socketfd,再创建sockaddr_in ,填写信息,但不同的是这里填写的是服务器的信息,因为我们是要和服务器进行通信,sendto 需要的是目标服务器信息,填写之后就可以发送了。      

     这里还有一个问题,就是Client需要bind吗?  要! 只不过不需要用户来自己写结构体自己来bind,一般是由OS自由随机选择。一个端口只能被一个进程bind,对server也是如此,对于client,也是如此,其实Client的port是多少,并不重要,只要能保证主机上的唯一性就可以了,那么系统是什么时候给我bind的呢? 首次发送数据的时候!

        编译一下,我们就可以正常运行了

 这里需要注意的是,我这里是同一个主机进行的通信,所以这里Client的目的ip是127.0.0.1,任何一个主机都可以绑定127.0.0.1这个地址, 127.0.0.1 是本地回环地址,只能用于本地通信,通信的数据不会被发送到网络上,只会送到底层,再发回来。

      服务器Run的扩展

    为了方便我们后续的应用扩展,我们将让udp服务器进行优化,将run函数封装为一个回调函数,将处理数据的部分单独提取成一个函数,之后我们只需要将服务器执行的部分传入服务器,服务器就可以执行了。

编译之后,就可以正常运行了

Echo Server 服务器应用扩展

        如果服务器接受的数据不再是普通的数据,而是指令呢? 之前,我们模拟过一个微型xshell,这次我们就不再用这种方式了。

        有一个函数叫popen。

   函数popen

        函数popen,它会创建一个管道,和一个子进程,并且通过管道连接到子进程,并返回一个文件指针,用于读取或写入子进程的输入输出。

        具体执行就是你把你要执行的指令以字符串的形式交给popen,popen的底层会调用fork创建子进程,再创建一个管道帮我们建立父子进程之间的通信,在子进程调用我们输入的指令,并通过管道将运行结果,返回给调用方。

         当然,在指令通信中,我们为了防止Client传过来的指令对我们的服务器造成影响,比如删除所有数据等等,这里我们可以做一个检查,设置好不安全指令,分析Client传来的指令是否安全,如果不安全则不执行。

 

编译运行,客户端属于指令,我们就可以看到在服务器上执行的结果了 

 这里,我们之所以可以完成这个类似于shell的操作,是因为后台运行了一个服务,端口号为22的TCP服务。

我们可以通过 netstat -ntlp 来查看 tcp的服务

 Windows访问Linux服务器

        还记得我们之前说的网络模型吗,在不同操作系统中,网络部分的实现功能都是一致的,这也就使得网络的编程是可以跨平台的,只有一小部分系统调用的代码不同,下面,我们就把我们的客户端移植到Windows上。

        先加上一些前置准备。

 WinSock2.h 是Windows系统下用于支持 Windows Sockets 2.0 规范的头文件,它提供了在 Windows 平台上进行网络编程的接口。

Windows.h 是Windows 编程中最基础且核心的头文件之一,它几乎包含了 Windows API(应用程序编程接口)的所有基础定义和声明,为开发者提供了操作 Windows 操作系统底层功能的接口。

一般网络编程,通常需要同时包含这两个头文件,不过需要注意包含顺序,一般建议先包含WinSock2.h,再包含Windows.h

#pragma comment(lib, "ws2_32.lib") 指定链接ws2_32.lib库,该库实现了WinSock2.h中声明的函数。

WSADATA 类型是用于存储Windows Sockets API的数据,

WSAStartup (MAKEWORD(2, 2), &wsd);调用WSAStartup函数初始化 Windows Sockets API,参数MAKEWORD(2, 2)指定使用的 Windows Sockets 版本为 2.2。

WSACleanup();清理 Windows Sockets API 环境,释放相关资源。

配置好这些之后,我们就可以将我们的客户端代码拷贝过来了。

 编译运行,就实现了Windows和Linux平台的通信了。

v2 版本 - Dict Server 字典服务器

接下来,我们基于上一个Echo Server 来简单实现一个Dict Server,功能就是实现单词的英译汉。

具体思路就是,我们再额外封装一个dict,它的功能是将我们外置放着的字典加载进内存,并且给我们服务器提供翻译功能

剩下的就和上面的Echo实现的一样了。

具体如下:

Dictionary.hpp

 UdpServer.cc

 编译运行

 下面我们给一个封装版的,这样也方便我们以后使用

封装版本

socket.hpp

 万能udp_server.hpp 模版

 万能udp_client.hpp 模版 

dict_server.cc

dict_client.cc

V3 版本 - 多人chat聊天室

接下来,我们来写我们第三个版本 多线程群聊

多个人进行群聊的时候,我们可以看到别人的信息,并且知道是谁发的信息。

我们这里就使用IP+端口号来表示一个人,使用unordered_map记录有哪些群里有哪些人。每有一个人发消息,就把这条消息发给map中的所有人。

但是,上面的写法是单线程进行运行的,当用户多起来了,那么单进程是完全不够多,所以我们可以将信息封装成可以转发的任务,让其他线程去转发,主线程只负责数据的接收。 

同时,我们引入线程池,将我们接受到的信息转给线程池,让线程池去进行转发,这样即便在高负荷的情况下,我们的系统也能轻松拿捏

那么对于每一个用户,我们都要进行增删查改的管理,那么我们要怎么进行管理呢?

很简单,先描述,在组织!

我们设计出来一个User类,然后再次基础上在设计一个User类的管理类UserManager,使用UserManager在进行管理,之后将转发给所有用户的接口交给线程池让其进行执行即可。

接下来我们就来完成这些操作

我们首先先设计一个接口类,给我们的用户定下接口,他的作用等我们全部写完了,就能够感受到了。

 接着实现一个User类继承User Interface类,并实现所对应的接口

内部区分唯一性的标识符,我们这里采用我们之间写的InetAddr来标识,原因就是方便,这也是封装好解偶性好的好处。

标识符是通过InetAddr来标识的,所以我们在写一下InetAddr的比较重载运算符

至此对于User的描述就完成了,接下来我们来进行管理User,我们在实现一个Usermanager类来实现管理User

        由于对User的管理,实现增删查改,经常需要我们进行查找某个用户,所以UserManager我们底层选择采用unordered_map来实现,当然也可以采用其他容器例如list之类的,  

需要注意的是,Adduser和DelUser对共同临界区的访问,需要加锁保护。 

上面我们对于User和UserManger的实现,其实是基于一种叫观察者的行为设计模式

观察者模式(Observe Pattern)

观察者模式(Observer Pattern)是一种行为设计模式,它定义了对象之间一对多的依赖关系,当一个对象(主题)的状态发生变化时,所有依赖它的对象(观察者)都会被调用接口从而得到通知并自动更新。

模式结构

  • 主题(Subject):也被叫做被观察对象,它维护了一个观察者列表,提供了添加、删除观察者的方法,并且在状态变化时通过接口通知所有观察者(具备一个通知所有观察者的接口)。
  • 观察者(Observer):定义了一个更新接口,当主题状态改变时,主题会调用该接口通知观察者。
  • 具体主题(Concrete Subject):继承自主题,实现了主题的抽象方法,当自身状态发生变化时,调用通知方法通知所有观察者。
  • 具体观察者(Concrete Observer):实现了观察者接口,在接收到主题的通知后,执行相应的更新操作。

优缺点

优点
  • 松耦合:主题和观察者之间的耦合度较低,主题只需要知道观察者实现了更新接口,而不需要了解具体的观察者类。
  • 可扩展性:可以方便地添加或删除观察者,而不影响主题和其他观察者。
  • 支持广播通信:主题可以一次性通知所有观察者,实现广播式的消息传递。
缺点
  • 性能问题:如果观察者数量较多,通知所有观察者可能会带来性能开销。
  • 循环依赖问题:如果主题和观察者之间存在循环依赖,可能会导致系统不稳定。

应用场景

  • 消息通知系统:如邮件订阅、即时通讯等,当有新消息时,通知所有订阅者。
  • GUI 组件:当一个组件的状态发生变化时,通知其他相关组件进行更新。
  • 股票行情系统:股票价格的变化会通知所有关注该股票的投资者。

在上面我们实现的User.hpp中,Userinterfa就是观察者(抽象的观察者接口),User就是具体的观察者,Usermanager就是具体的主题,这里我们并没有写主题(抽象的主题接口)。

这么写的好处就是,当我们想要扩展的时候,比如我们不仅是想要转发用户消息,还想转发的时候给一个备份文件里面发,这样就可以将整个聊天记录存储到一个文件里面,这时,我们就可以基于抽象的观察者接口,在设计出来一个具体的观察者,然后将其放入具体的主题里的容器里,通知的时候就会自动调用我们在文件中设计的接口,将消息存储到一个文件里。

整体来说就是,易于代码扩展

我们完成了User用户的描述管理代码的编写,那么我们应该怎么做才能将User和我们的Udp服务器关联起来呢?

使两个类之间相互关联的方法有很多,像类继承,类的嵌套都可以实现。

但像这种模块之间的关联,我们为了让两个模块的耦合性最低,我们采用的是类间调用。

各个服务与业务之间最好尽量降低耦合想,方便后期代码维护,以及代码扩展。

模块之间,可以采用std::bind,lamba表达式以及回调函数这样的策略来实现模块间的耦合。

不使用类对象嵌套是因为类间带哦用更加的灵活,只依赖接口,可以随时切换接口而执行的底层不用变,类嵌套则改变的时候,往往还需要修改类的定义。

我们在Udpsever中定义几个任务类型,运行的时候,直接注册进去相应的任务就可以了。

上面三个我们都能理解,那么这个task_t 是干什么用的?

这是线程池的入队列接口,当我们直接使用_route写入两个参数传入Equeue接口的话,实际传入的事route_t 中的返回值 void,无法转换所以就会报错

所以这里需要提前将route绑定好,然后直接将变量传进去。

task_t是任务类型,在外面我们通过bind将route_t的两个参数绑定好,然后赋值给task_t类型传给线程池。

bind需要注意的是,_route 是一个函数对象,并不是成员函数,所以不需要传this指针

整个Run部分代码

最后,我们只需要给我们的服务器写一个注册接口,将我们所需要的服务,由外部注册进去就可以了

不要忘了,我们引入的线程池是单例的,所以这里我们的服务器也必须要将拷贝以及赋值运算符删除掉。

这里我们采用继承的方式删除,即继承一个删除了拷贝构造和赋值运算符重载的类

 还有一点非常关键需要注意的是

我们这UserManager这里使用的容器是unordered_map 并且里面的key值,是我们自己写的自定义类型,而std::unordered_map 需要为键类型提供哈希函数,用于计算键的哈希值,保证key其唯一性。若没有为 InetAddr 类型定义哈希函数,编译器就会报错。

所以这里我们就需要为InetAddr写一个hash类

直接写到最后即可,因为hash类本身是一个模版,所以我们这里只需要写一个InterAddr的特例即可,写法就是这样,简单的保证下唯一性。

最后直接运行就可以了

本机之间通信

 线程运行成功了,启动客户端

启动成功,我们发送的消息服务器可以收到,并且可以转给目前的所有在线用户。

不同主机之间通信

 我们既然是网络那么肯定就能够进行不同网络,不同子网之间的通信,那么接下来,我们来用另一台主机通过IP+端口号的形式访问我们的Udp服务器吧

安全组策略开放端口

我们直接在另一台主机上发送消息给Udp服务器,发现我们的Udp服务器好像没有收到任何信息,这是为什么呢?我们代码哪里写错了吗?

其实并不是这只是因为我们的云服务器安全组策略没有开放对应的端口而已。

这也就导致 Udp服务器没有收到消息,那么我们应该怎么开放端口呢?

我们以华为云服务器举例(其实其他的都大差不差的)

打开华为云官方网站,再进入到控制台管理界面,找到我们的服务器,也就是下面这样

 之后点击续费旁边的三个点,找到配置安全组规则

点开后,就可以配置安全策略规则了

可以选择所有协议端口,或者TCP全部端口,UDP所有端口,也可以自定义端口,根据需求配置即可。

这里配置好了之后,

我们再来访问下Udp服务器看下,是否可以访问上了

 我们可以看到我们另一台主机已经可以访问上主机了,可以我们还发现一个问题,在客户端我们想要接收消息,似乎必须得先发送一条消息,才能够接收到消息,那么这个问题应该怎么解决呢?

答案就是多线程!

    我们将读和写分成两个线程,一个线程只负责读信息,主线程只负责发信息,这样就解决掉上面的情况了。

     那这就有问题了,两个线程共享同一个sockfd,那这不就线程不安全了吗?

     其实并没有,udp的scoket是全双工的,允许被同时读写,所以我们的操作是可以合理的,不用加锁。

 我们引入我们之前封装的线程,将读写操作分开执行。

 编译好之后,再次运行

这样就可以了

客户端输入输出分离

在上面我们已经成功将多人chat聊天室实现了,但是客户端部分,输入输出在同一个显示屏,多少还是有点乱,那有没有什么方式,能够让他们分开呢? 当然有,这里我们提出两种方式

1. 基于管道实现输入输出分离

还记得C++中的输入流吗? 0为标准输入,代表键盘,1为标准输出,代表显示器,2为标准错误,代表显示器。

那么为什么C/C++要提供标准输出、标准错误呢??  可以通过重定向、让标准输出、标准错误分批打印到不同的文件中,方便我们进行debug!

我们可以测试下

测试代码:

我们发现只有标准输出输出到类文件中,标准错误依旧输出到了屏幕上。这是因为 > 重定向,起始默认省略了1,完整写就是 1>, ./a.out 1> log.txt 所以就只有标准输出到了文件中 

我们换了一种写法,这次标准输出和标准错误确实没有输出到屏幕上,但是文件中却只有标准错误的信息,却没有标准输出,这是为什么呢?

其实标准输出确实也输出到文件中了,但是却被标准错误给覆盖了,所以才看不到了,使用>输出重定向,当打开一个文件的时候就会自动清空,如果不想被覆盖就使用追加重定向>>, 

这样就可以了,其实还有一种写法,也可以追加输入到文件中,我们上面提到过当重新打开一个文件的时候会自动清空,那如果不会重新打开呢?

这样&也是可以的。

 接下来开始实现,首先创建一个管道

 需要注意的是,管道有一个特性,就是管道的读写需要同时打开,才会继续向后执行

所以我们在发送信息时最好先重定向FIFO的输出(先重定向FIFO的输入也是可以的)

这样就实现了客户端的输入输出分离

 2.基于终端显示文件

相较于这个方式,更推荐第一种,因为这种方式具有一定的局限性,代码迁移性受损,有些系统(例如Mac),他是不支持的,

在服务器终端,/dev/pts目录下对应的是各个终端。

有两个终端就有两个文件

 我们可以测试下

     由此,我们便可以利用这个特性,将输出内容分离开,我们还是将输出内容分为标准输出和标准错误,然后通过重定向的方式将标准错误重定向到其他终端即可

需要注意,新的终端分配的文件时由小到大分配的,并且不会发生改变除非退出终端。

       现在,我们先来编写一个简单的测试代码验证一下吧

编译运行 

      那我们怎么把这个技术用到我们的客户端呢? 很简单,只需要把这个文件的输出去掉,再改成头文件,加到我们的Udp客户端就可以了。

       我们这里是封装成成了RAII风格的类了(纯属最近封装封上瘾了,完全可以用更简单的方法的,只要注意不要忘了最后释放掉申请的fd就行)。

加入头文件后,在开头,创造一个变量就行了

 编译运行

至此Udp章节结束,接下来下一个章节 TCP

相关文章:

『Linux_网络』 第二章 UDP_Socket编程

学习了网络的概念了&#xff0c;接下来我们开始实践&#xff0c;本次我们会通过UDP来模拟实现UDP客户端和UDP服务器之间的通信&#xff0c;以及在此基础上扩展几个应用。 下面&#xff0c;我们将使用socket&#xff0c;bind&#xff0c;htons等接口实现UDP网络通信。 v1 版本 …...

【leetcode刷题日记】lc.322-零钱兑换

目录 1.题目 2.代码 1.题目 给你一个整数数组 coins &#xff0c;表示不同面额的硬币&#xff1b;以及一个整数 amount &#xff0c;表示总金额。 计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额&#xff0c;返回 -1 。 你可以认…...

从GET到POST:HTTP请求的攻防实战与CTF挑战解析

初探HTTP请求:当浏览器遇见服务器 基础协议差异可视化 # 典型GET请求 GET /login.php?username=admin&password=p@ssw0rd HTTP/1.1 Host: example.com User-Agent: Mozilla/5.0# 典型POST请求 POST /login.php HTTP/1.1 Host: example.com Content-Type: application/x…...

实现Azure Data Factory安全地请求企业内部API返回数据

需要配置一个Web Activity组件在Azure云上的Azure Data Factory运行&#xff0c;它需要访问企业内部的API获取JSON格式的数据&#xff0c;企业有网关和防火墙&#xff0c;API有公司的okta身份认证&#xff0c;通过公司的域账号来授权访问&#xff0c;现在需要创建一个专用的域账…...

JDOM处理XML:Java程序员的“乐高积木2.0版“

各位代码建筑师们&#xff01;今天我们要玩一款比原生DOM更"Java友好"的XML积木套装——JDOM&#xff01;它像乐高得宝系列&#xff08;Duplo&#xff09;一样简单易用&#xff0c;却能让你的XML工程稳如霍格沃茨城堡&#xff01;&#xff08;温馨提示&#xff1a;别…...

Grouped Query Attention (GQA) PyTorch实现

个人在网上看到的实现好像都长得奇奇怪怪的&#xff0c;没有简洁的感觉&#xff0c;因此在这里给出一种易读的GQA实现方法&#xff1a; import torch import torch.nn as nn import torch.nn.functional as Fclass GroupedQueryAttention(nn.Module):def __init__(self, embed…...

《AI大模型应知应会100篇》第27篇:模型温度参数调节:控制创造性与确定性

第27篇&#xff1a;模型温度参数调节&#xff1a;控制创造性与确定性 摘要 在大语言模型的使用中&#xff0c;“温度”&#xff08;Temperature&#xff09;是一个关键参数&#xff0c;它决定了模型输出的创造性和确定性之间的平衡。通过调整温度参数&#xff0c;您可以根据任…...

演讲比赛流程管理项目c++

对于一个基本项目&#xff0c;先分析基本的东西有哪些 1.类 演讲管理类:用于编写比赛流程用的功能 演讲者类&#xff1a;包含姓名&#xff0c;分数 创建比赛流程&#xff1a;创建选手12个人&#xff0c;分为两组&#xff0c;6人一组&#xff0c;每组进行两轮比赛&#xff0…...

在小米AX6000中通过米家控制tailscale

由于tailscale占用内存较大&#xff0c;AX6000中的可用内存非常有限&#xff0c;所以需要对AX6000的内存使用进行优化&#xff1a; 1.减小tmpfs内存占用的大小&#xff1a; #从150M -> 90M&#xff0c;由于tailscale下载安装包是27M作用&#xff0c; 解压后50M左右&#xf…...

REC: 引爆全球万亿级市场!Web3+消费革命重塑全球-东南亚-跨境商业未来

在全球数字经济浪潮下&#xff0c;东南亚已成为增长最快的互联网市场之一&#xff0c;其与全球之间蓬勃发展的跨境贸易更是蕴藏着巨大潜力。然而&#xff0c;传统模式下的效率瓶颈、信任壁垒和用户激励难题日益凸显。在此背景下&#xff0c;基于去中心化与消费相结合的 REC 颠覆…...

微服务与事件驱动架构(EDA)

微服务架构 微服务架构核心特征 服务自治&#xff1a;每个服务拥有独立的代码库、数据库和运维流程。轻量级通信&#xff1a;服务间通过API&#xff08;REST/gRPC&#xff09;或消息队列&#xff08;如Kafka&#xff09;交互。去中心化治理&#xff1a;允许技术栈多样化&…...

单片机如何通过串口与上位机进行数据交换

单片机通过串口与上位机进行数据交换是一种常见的方式&#xff0c;广泛应用于嵌入式系统中。以下是实现这一功能的具体步骤和注意事项&#xff1a; 1. 硬件连接 在硬件层面&#xff0c;需要确保单片机和上位机之间的串口连接正确&#xff1a; 信号线连接&#xff1a;通常使用…...

AI速读 Seed-Thinking-v1.5:大模型推理的新飞跃

在大语言模型&#xff08;LLM&#xff09;蓬勃发展的今天&#xff0c;推理模型的性能提升成为了AI领域的关键议题。今天为大家解读的论文&#xff0c;带来了名为Seed-Thinking-v1.5的推理模型&#xff0c;它在多个任务上表现惊艳&#xff0c;还创新性地解决了不少难题&#xff…...

Mysql从入门到上手(二)-全面了解增删改查(CRUD).

一、检索数据 MySQL 中的检索数据操作是数据库操作中最常见的任务之一。使用 SQL 查询语言中的 SELECT 语句&#xff0c;可以从数据库中的一个或多个表中检索数据。以下是 MySQL 中与数据检索相关的各种技术和用法的详细讲解。 1.1、基本查询 最基本的查询是使用 SELECT 语句来…...

220V转5V转12V电机驱动供电WT5105

220V转5V转12V电机驱动供电WT5105 WT5105 芯片概述 WT5105 是一款集成非隔离式电源控制器&#xff0c;内部集成了 650V 高雪崩能力功率 MOSFET 以及高压启动与自供电电路。该芯片具有多模式输出的特点&#xff0c;输出电压可通过 FB 电阻灵活调整&#xff0c;能够实现 3.3V 以…...

基于Python Django 的全国房价大数据可视化系统(附源码,部署)

博主介绍&#xff1a;✌程序员徐师兄&#xff0c;7年大厂开发经验。全网粉丝12w&#xff0c;CSDN博客专家&#xff0c;同时活跃在掘金、华为云、阿里云、InfoQ等平台&#xff0c;专注Java技术和毕业项目实战分享✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&a…...

leetcode0113. 路径总和 II - medium

1 题目&#xff1a;路径总和 II 官方标定难度&#xff1a;中 给你二叉树的根节点 root 和一个整数目标和 targetSum &#xff0c;找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。 叶子节点 是指没有子节点的节点。 示例 1&#xff1a; 输入&#xff1a;root …...

day46——两数之和-输入有序数组(LeetCode-167)

题目描述 给你一个下标从 1 开始的整数数组 numbers &#xff0c;该数组已按 非递减顺序排列 &#xff0c;请你从数组中找出满足相加之和等于目标数 target 的两个数。如果设这两个数分别是 numbers[index1] 和 numbers[index2] &#xff0c;则 1 < index1 < index2 &l…...

数据结构:以一个例题演示弗洛伊德算法

例 8.5.2 利用弗洛伊德算法&#xff0c;对图 8.5.5 中左侧的带权有向图求最短路径&#xff0c;给出每一对顶点之间的最短路径及其路径长度在求解过程中的变化。 图 8.5.5 带权图和邻接矩阵 【解】 根据图 8.5.5 中的带权有向图&#xff0c;可得所对应的邻接矩阵 g g g &#…...

Nginx 报错403 排查与解决

目录 前言-环境基础问题出现&#xff1a;403 Forbidden问题排查问题解决 前言 今天领导让我部署一个前端项目,一顿操作报错访问报错403,让我们一起搞定他。 环境 CentOS 7 x86 的服务器上部署了 Nginx 服务器。 配置文件 我把前端项目打包后的 dist 文件夹放在了 /root/…...

React-useImperativeHandle (forwardRef)

我们会遇到这样的场景&#xff1a;某个组件想要暴露一些方法&#xff0c;来供外部组件来调用。例如我们在开发form表单的时候&#xff0c;就需要把设置表单值、重置值、提交等方法暴露给外部使用。会有如下代码&#xff1a; import { forwardRef } from react;const Form for…...

研一自救指南 - 07. CSS面向面试学习

最近的前端面试多多少少都会遇到css的提问&#xff0c;感觉还是要把重点内容记记背背。这里基于b站和我自己面试的情况整理。 20250418更新&#xff1a; 1. BFC Block Formatting Context&#xff0c;一个块级的盒子&#xff0c;可以创建多个。里面有很多个块&#xff0c;他们…...

生成式人工智能驱动下的个性化学习资源开发研究——以K12学科知识图谱构建为例

一、引言 1.1 研究背景与意义 在当今数字化时代&#xff0c;教育领域正经历着深刻的变革&#xff0c;生成式 AI 技术的迅猛发展为 K12 教育带来了新的契机与挑战。长期以来&#xff0c;K12 教育主要采用标准化教学模式&#xff0c;这种 “一刀切” 的方式难以满足学生多样化的…...

A股周度复盘与下周策略 的deepseek提示词模板

以下是反向整理的股票大盘分析提示词模板&#xff0c;采用结构化框架数据占位符设计&#xff0c;可直接套用每周市场数据&#xff1a; 请根据一下markdown格式的模板&#xff0c;帮我检索整理并输出本周股市复盘和下周投资策略 【A股周度复盘与下周策略提示词模板】 一、市场…...

性能比拼: Deno vs. Node.js vs. Bun (2025版)

本内容是对知名性能评测博主 Anton Putra Deno vs. Node.js vs. Bun: Performance Comparison 2025 内容的翻译与整理, 有适当删减, 相关指标和结论以原作为准 在本视频中&#xff0c;我们将使用当前可用的最新版本对 Node.js、Bun 和 Deno 进行比较。我决定更新本视频&#x…...

猫咪如厕检测与分类识别系统系列【十二】猫咪进出事件逻辑及日志优化

前情提要 家里养了三只猫咪&#xff0c;其中一只布偶猫经常出入厕所。但因为平时忙于学业&#xff0c;没法时刻关注牠的行为。我知道猫咪的如厕频率和时长与健康状况密切相关&#xff0c;频繁如厕可能是泌尿问题&#xff0c;停留过久也可能是便秘或不适。为了更科学地了解牠的如…...

stm32| 中断标志位和中断挂起位 | TIM_ClearFlag 函数和TIM_ClearITPendingBit 函数

1. 中断标志位&#xff08;Interrupt Flag&#xff09; 作用&#xff1a; 中断标志位位于外设寄存器中&#xff08;如定时器的TIMx_SR、GPIO的EXTI_PR等&#xff09;&#xff0c;用于指示某个特定事件是否发生&#xff08;例如定时器溢出、GPIO引脚电平变化&#xff09;。该标志…...

QML Rectangle 组件

基本属性 属性类型默认值描述colorcolor"white"矩形填充颜色border.colorcolor"transparent"边框颜色border.widthint0边框宽度radiusreal0圆角半径gradientGradientnull渐变填充antialiasingbooltrue是否抗锯齿 几何属性 (继承自Item) 属性类型默认值描…...

Spring 学习笔记之 @Transactional详解

一、数据库事务基础 数据库事务&#xff08;Transaction&#xff09;是数据库管理系统中用于确保数据一致性和完整性的一种机制。它是一组操作的集合&#xff0c;这些操作要么全部成功&#xff0c;要么全部失败&#xff0c;从而保证数据库状态的正确性。 1.1 事务的基本概念 定…...

绕过UI的cooke和token的验证

很多的网站都在登录页面加入了识别文字&#xff0c;识别图片&#xff0c;拖动拼图的验证码方式来防止爬虫、恶意注册等&#xff0c;如果是做自动化&#xff0c;需要绕过 验证码才能进入下一步操作。 方案一、测试环境去除验证码 &#xff1a;最轻松的方法&#xff0c;找开发帮忙…...

2024-04-19| Java: Documented注解学习 JavaDoc

在 Java 中&#xff0c;Documented 是一个元注解&#xff08;meta-annotation&#xff09;&#xff0c;用于标记其他注解&#xff0c;表明这些注解应该被包含在 JavaDoc 文档中。以下是关于 Documented 注解的作用的简要说明&#xff1a; 作用 记录注解信息到 JavaDoc&#x…...

09-DevOps-Jenkins实现CI持续集成

前面已经把harbor搭建好了&#xff0c;也可以向harbor中推送自定义镜像。 原计划是在Jenkins这台服务器上&#xff0c;完成镜像构建&#xff0c;然后把镜像推送的harbor仓库中。现在改变计划了&#xff0c;Jenkins所在的服务器&#xff08;192.168.1.10&#xff09;不负责镜像…...

Java中包装类和泛型

包装类和泛型 包装类装箱和拆箱 泛型泛型的概念泛型的使用泛型的上界 通配符通配符概念通配符上界通配符下界 前言 在Java中&#xff0c;由于基本类型不是继承⾃Object&#xff0c;为了在泛型中可以⽀持基本类型&#xff0c;Java给每个基本类型都对应了⼀个包装类型&#xff0c…...

小程序 GET 接口两种传值方式

前言 一般 GET 接口只有两种URL 参数和路径参数 一&#xff1a;URL 参数&#xff08;推荐方式&#xff09; 你希望请求&#xff1a; https://serve.zimeinew.com/wx/products/info?id5124接口应该写成这样&#xff0c;用 req.query.id 取 ?id5124&#xff1a; app.get(&…...

8、表单控制:预言水晶球——React 19 复杂表单处理

一、水晶球的预言本质 "每个表单都是时空裂缝中的预言容器&#xff0c;"占卜课教授特里劳妮凝视着水晶球&#xff0c;"React-Hook-Form与Formik的融合&#xff0c;让数据捕获如同捕捉未来碎片&#xff01;" ——以魔法部神秘事务司的预言厅为隐喻&#xf…...

Android studio开发——room功能实现用户之间消息的发送

文章目录 1. Flask-SocketIO 后端代码后端代码 2. Android Studio Java 客户端代码客户端代码 3. 代码说明 SocketIO基础 1. Flask-SocketIO 后端代码 后端代码 from flask import Flask, request from flask_socketio import SocketIO, emit import uuidapp Flask(__name_…...

【测试文档】项目测试文档,测试管理规程,测试计划,测试文档模版,软件测试报告书(Word)

原件获取列表&#xff1a; 系统测试方案-2.docx B-Web安全服务渗透测试模板.docx 压力测试报告.docx安全测试用例及解析.docx 测试计划.doc 测试需求规范.doc 测试需求指南.docx 测试用例设计白皮.doc 单元测试报告模板.doc 单元测试计划模板.doc 回归测试指南.doc 集成测试报…...

将 DeepSeek 集成到 Spring Boot 项目实现通过 AI 对话方式操作后台数据

文章目录 项目简介GiteeMCP 简介环境要求项目代码核心实现代码MCP 服务端&#xff08;批量注册 Tool&#xff09;MCP 客户端&#xff08;调用 DeepSeek&#xff09; DeepSeek APIDockersse 连接http 连接 Cherry Studio配置模型配置 MCP调用 MCP 项目简介 在本项目中&#xff…...

接口自动化 ——fixture allure

一.参数化实现数据驱动 上一篇介绍了参数化&#xff0c;这篇 说说用参数化实现数据驱动。在有很多测试用例的时候&#xff0c;可以将测试用例都存储在文件里&#xff0c;进行读写调用。本篇主要介绍 csv 文件和 json 文件。 1.读取 csv 文件数据 首先创建 csv 文件&#xff…...

Datawhale AI春训营学习笔记

数据竞赛Baseline代码全解析&#xff1a;从数据加载到结果输出 一、环境配置与数据加载 1.1 依赖库导入 from netCDF4 import Dataset # 处理气象.nc格式数据 import numpy as np import pandas as pd from sklearn.model_selection import KFold from sklearn.metrics imp…...

关于学习STM32的C语言的知识

数据类型 关键字位数表示范围stdint关键字char8 -128 ~ 127 int8_tunsigned char8 0 ~ 255 uint8_tshort16 -32768 ~ 32767 int16_tunsigned short16 0 ~ 65535 uint16_tint32 -2147483648 ~ 2147483647 int32_t unsigned int32 0 ~ 429496729 uint32_t long32 -2147483648 ~…...

day28 学习笔记

文章目录 前言一、图像添加水印1.ROI操作2.添加水印 二、图像去除噪声1.均值滤波2.方框滤波3.高斯滤波4.中值滤波5.双边滤波6.总结 前言 通过今天的学习&#xff0c;我掌握了OpenCV中有关图像水印以及图像去除噪声的原理以及相关操作 一、图像添加水印 1.ROI操作 ROI操作即之…...

第34讲|遥感大模型对比实战:SAM vs. CLIP vs. iSAM

目录 🔍 一、遥感大模型简要介绍 1️⃣ SAM(Segment Anything Model) 2️⃣ CLIP(Contrastive Language–Image Pretraining) 3️⃣ iSAM(Improved SAM for Remote Sensing) 🧪 二、实战数据集与任务设计 🌟 任务设置: 🧠 三、代码实现片段(以 Python 为…...

EAGLE代码研读+模型复现

要对代码下手了&#xff0c;加油(ง •_•)ง 作者在他们自己的设备上展现了推理的评估结果&#xff0c;受第三方评估认证&#xff0c;EAGLE为目前最快的投机方法&#xff08;虽然加速度是评估投机解码方法的主要指标&#xff0c;但其他点也值得关注。比如PLD和Lookahead无需额…...

多线程使用——线程安全、线程同步

一、线程安全 &#xff08;一&#xff09;什么是线程安全问题 多个线程&#xff0c;同时操作同一个共享资源的时候&#xff0c;可能会出现业务安全的问题。 &#xff08;二&#xff09;用程序摹拟线程安全问题 二、线程同步 &#xff08;一&#xff09;同步思想概述 解决线…...

基于 Linux 环境的办公系统开发方案

基于 Linux 环境的办公系统开发方案 一、项目概述 1.1 项目背景 在当今数字化办公的时代&#xff0c;高效、稳定且功能丰富的办公系统对于企业和组织的日常运营至关重要。Linux 作为一种开源、稳定且高度可定制的操作系统&#xff0c;拥有庞大的开发者社区和丰富的软件资源&…...

mysql8.0.17以下驱动导致mybatis blob映射String乱码问题分析与解决

mysql8.0.17以下驱动导致blob映射String乱码问题分析与解决 一、问题复现二、问题深究三、解决方法方法1方法2 一、问题复现 1、docker安装mysql8.0&#xff0c;并创建测试数据库及测试数据表 CREATE DATABASE test DEFAULT CHARACTER SET utf8mb4; use test; CREATE TABLE t…...

Unity Nav Mesh导航系统的简单使用

标题 1.下载。2.面板位置3.object面板4.Area面板5.Bake面板6.Agent面板7.Nav Mesh Agent组件8.Nav Mesh Obstacle组件9.简单使用 1.下载。 unity2022以上版本要去packageManager中下载。 2.面板位置 3.object面板 Navigation Static&#xff1a;设置该物体是否被列入静态寻路…...

从零开始学A2A五:A2A 协议的安全性与多模态支持

A2A 协议的安全性与多模态支持 一、A2A 协议安全机制 1. 认证机制 A2A 协议采用多层次认证机制&#xff0c;确保智能体身份的真实性和通信的安全性。 基于 Agent Card 的身份认证&#xff1a; {"agent_id": "secure_agent_001","authentication&…...

PyTorch源码编译报错“fatal error: numpy/arrayobject.h: No such file or directory”

记录一下这个bug的fix过程 一开始以为是版本问题&#xff0c;尝试了几个不同版本都不可以&#xff0c;遂排除版本问题的可能 定位 首先 pip list 看到确实安装了这个库 接着 pip show 查看 numpy 库的安装路径 numpy/arrayobject.h 是 NumPy 的 C-API 头文件&#xff0c;其…...