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

Linux-> UDP 编程1

目录

本文说明

一:socket编程的本质

二:Socket接口介绍

1:struct sockaddr* address

2:socket

3:bind

4:recvfrom

5:sendto

三:UDP 下的CV程序的要点

1:填充结构体

2:IP和PORT转化为网络字节序

3:关于CS双方的IP和POAT

4:如何得到对方的IP+PORT

四:UDP echo程序代码

1:UdpServer.hpp(服务端)

2:main.cc

3:UdpClient.cc(客户端)

4:makefile

5:Log.hpp

6:运行效果


本文说明

本文重点在于介绍清楚socket编程的本质,然后用Socket编程接口来创建和使用UDP协议的Socket,去实现出一个echo程序~(echo就是客户端输入什么 服务端就返回什么!)

一:socket编程的本质

首先我们知道,一个数据要先通过网络通信到到另一台主机,其实过程是复杂的,需要来回贯穿两次协议栈,而这是数据在底层的实现,但是如果我们普通开发者想要实现网络通信,或者用网络通信去传输数据,我们不可能也极难做到自己手动的去让数据贯穿两次协议栈!

所以网络的设计者,肯定也不会让想使用网络的人的上手门槛这么高,但是又需要提供一些接口让网络的使用者来进行开发!

所以网络的设计者隐藏了底层网络协议的极端复杂性,为开发者提供一个简单、统一、抽象的接口,让开发者根据自己的需求去调用对应的接口即可!

那隐藏了什么呢?

设计者隐藏了传输层及下层的实现,让开发者通过一个名为"Socket"(套接字)的编程接口来实现网络通信。这个接口抽象了网络操作的细节,大大简化了网络应用的开发过程。

并且socket的编程接口允许开发者像操作文件一样进行网络的连接和数据收发,从而极大地简化了网络编程。

Socket编程接口是多个接口,是用户在应用层去使用的接口,需要使用其中一个接口先选择一种传输层协议,然后再用其余的接口进行数据在网络中的传输和接收等操作.......

Q1:为什么Socket编程接口是在应用层调用?

A1:首先,我们平时写的绝大部分代码,都只停留在应用层。而在应用层 "使用接口" 这个动作,本质是向系统层(内核)发起请求。也就是我们任何接口其实都不能直接的操作内核或底层,只是一种请求,这就是为什么接口存在调用失败的可能,本质就是被内核拒绝了请求罢了!

所以同理,Socket编程接口肯定也是在应用层被调用,用来向内核发起请求!这样才能避免开发者直接操作底层来实现网络行为!这样才是安全,抽象,标准的做法!

Q2:为什么只需要用Socket编程接口来选择传输层的协议,就能进行网络通信?而不是应该让我们每层选择一个协议才能进行网络通信吗?

A2:这几个问题可以一起回答!选择传输层的协议UDP或TCP,就好比你选择JD快递还是中通快递寄东西罢了,这两种快递的差别大,一种便宜,慢,有损坏的风险,一种昂贵,快,无损坏风险,而你选择好了一种快递之后,就直接把寄的东西给它就行,你不需要关心如何分拣包裹、用什么卡车运输、走哪条路线、如何派送。

同理UDP和TCP是数据在网络中通信的两种风格,区别在于:

特性TCP (传输控制协议)UDP (用户数据报协议)
可靠性可靠 (确认、重传、排序)不可靠 (尽力而为)
连接面向连接 (三次握手、四次挥手)无连接 (即发即走)
数据传输模式面向字节流 (无消息边界)面向数据报 (有消息边界)
速度较慢 (由于连接和可靠性开销)非常快 (开销极小)
应用场景Web浏览、邮件、文件传输、数据库视频直播、语音通话、在线游戏、DNS
复杂度高 (协议复杂,占用系统资源多)低 (协议简单,资源占用少)

所以当你选择了UDP或TCP协议之后,直接决定了应用程序的通信模型,之后的层状网络已经有事实上的全球标准,你无需在意!你的数据会贯穿整个协议栈到网络,再贯穿协议栈到对方主机中,但是你仅需通过Socket编程接口选择传输层协议之后,再进行数据的传输和接收即可!!

二:Socket接口介绍

Socket接口有很多,本博客实现的是UDP的程序,所以我们传输层肯定选择UDP协议,其次我们只讲解本博客在UDP程序中涉及到的Socket接口。

如下:

// 创建socket 
int socket(int domain, int type, int protocol);// 绑定端口号
int bind(int socket, const struct sockaddr *address,socklen_t address_len);// 接收数据
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);// 发送数据 
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

注:socket又叫做套接字,二者一个意思

1:struct sockaddr* address

在讲解接口之前,我们要先讲解一个多个接口中都会出现的参数,struct sockaddr* address!!我们给该参数传递的不是&(address),而是其他的,为什么呢?

首先Socket接口不仅支持跨网络通信,还支持本地的进程间通信(叫作域间套接字),因此Socket提供了sockaddr_in结构体和sockaddr_un结构体,其中sockaddr_in结构体是用于跨网络通信的,而sockaddr_un结构体是用于本地通信的。

如下图:

这就意味着,如果我们选择网络通信:

则我们需要传递(struct sockaddr*)&(struct sockaddr_in类型的变量)让接口中的struct sockaddr* address去接受

反之选择本地通信:

则(struct sockaddr*)&(strusct sockaddr_un类型的变量)即可

Q:为什么要这么做呢?为什么可以这么做的?

A:本质是因为三者的前16位是相同的,所以接口内部接收到我们传递的参数之后,会自动通过判断前16位,就可以知道我们是要进行网络通信中的哪种还是本地通信中的哪种!而我们一般选择的都是网络通信,所以我们一般定义的是struct sockaddr_in类型的变量,其实就是C语言的多态~~

Q:所以现在的问题在于我们选择网络通信,那我们的sockaddr_in类型的变量内部是什么?总不可能直接定义一个struct sockaddr_in类型的变量就传给接口对应的参数接收吧??

A:sockaddr_in类型的变量内部有四个字段:地址族(地址类型),端口号,IP地址,8字节填充

其中8字节填充则不用我们填写,其是用来主要进行大小对齐,实现兼容性和通用性的!

struct sockaddr_in {sa_family_t    sin_family;   // 地址族in_port_t      sin_port;     // 端口号struct in_addr sin_addr;     // IPv4地址...
};

解释:为什么需要填写这三个字段

很显然我们进行网络通信,分为客户端和服务端两个进程,而在上篇博客中,我们说过网络通信本质就是两个进程的通信,只不过两个进程在不同网络中的不同主机罢了,所以对于进程而言,必须要找得到对方的进程,才能进行网络通信!!

而得知对方的IP+PORT,就能找到对方的进程!

所以sockaddr_in类型的变量中的两个字段就是IP和PORT!

其次的地址类型,它还可以叫作地址家族或协议族或地址族,在上面讲过是用来让系统区分我们是要进行网络通信中的哪种还是本地通信中的哪种,系统提供了多个宏让我们选择!

Q:为什么要用struct sockaddr* address来接收,直接void*不就可以了?

A:因为当时网络设计者设计到这里的时候,C语言还没有void*,仅此而已,可见网络诞生之早!

2:socket

创建socket/套接字的函数叫做socket,该函数的函数原型如下:

int socket(int domain, int type, int protocol);

参数说明:

  • domain:创建套接字的域或者叫做协议家族,该参数就相当于struct sockaddr结构的前16个位。如果是本地通信就设置为AF_UNIX,如果是网络通信就设置为AF_INET(IPv4)或AF_INET6(IPv6)。
    • 本博客是UDP,所以设置为AF_INET!
  • type:创建套接字时所需的服务类型。其中最常见的服务类型是SOCK_STREAM和SOCK_DGRAM,如果是基于UDP的网络通信,我们采用的就是SOCK_DGRAM,叫做用户数据报服务,如果是基于TCP的网络通信,我们采用的就是SOCK_STREAM,叫做流式套接字,提供的是流式服务。本质就是UDP/TCP的数据传输模式的区别!
    • 本博客是UDP,所以设置为SOCK_DGRAM
  • protocol:创建套接字的协议类别。你可以指明为TCP或UDP,但该字段一般直接设置为0就可以了,设置为0表示的就是默认,此时会根据传入的前两个参数自动推导出你最终需要使用的是哪种协议。
    • 我们设置为0,干净简洁

返回值说明:

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

Q:为什么返回值是文件描述符?

A:我们上文说过,socket的编程接口允许开发者像操作文件一样进行网络的连接和数据收发,从而极大地简化了网络编程,所以返回了一个文件描述符!

下面从两个层面来理解socket函数作用:

①:抽象理解

使用socket接口,意味着你打开一个端口,该端口用于网络通信,调用Socket接口的进程就可以通过该端口进行网络通信了,同理对方进程肯定也要通过socket接口打开一个端口,双方就可以进行网络通信了。但是该端口暂时无法用于通信,因为我们还需要调用bind接口才可以用于通信,你可以理解为:目前只是安装一部电话。电话拿到了,没有电话号码。所以无法打电话

②:底层理解

本质是打开了一个“网络文件”,一般该文件的文件描述符为3,因为012已被占用,也就是socket的返回值一般为3。而文件缓冲区对于打开的普通文件来说对应的一般是磁盘,但对于现在打开的“网络文件”来说,这里的文件缓冲区对应的就是【内核的网络协议栈】。

所以对于一般的普通文件来说,当用户通过文件描述符将数据写到文件缓冲区,然后再把数据刷到磁盘上就完成了数据的写入操作。而对于现在socket函数打开的“网络文件”来说,当用户将数据写到文件缓冲区(【内核的网络协议栈】)后,操作系统会定期将缓冲区的数据刷到网卡里面,而网卡则是负责数据发送的,因此数据最终就发送到了网络当中。

所以不是这个“网络文件”携带着数据在网络中传输,而是网卡完成数据的传输,当用户将数据写到这个“网络文件”的文件缓冲区(【内核的网络协议栈】)后,操作系统会定期将数据刷到网卡里面,而网卡则是负责数据发送的,因此数据最终就发送到了网络当中。

为什么发到【内核的网络协议栈】,因为数据要在网络的层状结构中进行层层的封装,最后才会被刷新到网卡,然后由网卡发送到网络!!

本质:用户数据 -> Socket发送缓冲区 -> 网络协议栈(加工封装)-> 网卡驱动 -> 网卡硬件

这也再次的证明了上文中说的,传输层之后的层状网络已经有事实上的全球标准,你无需在意!

3:bind

绑定的函数叫做bind,该函数的函数原型如下:

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数说明:

  • sockfd:绑定的文件的文件描述符。也就是我们创建套接字时获取到的文件描述符。
  • addr:网络相关的属性信息,包括协议家族、以及被绑定进程的IP地址、端口号等。
  • addrlen:传入的addr结构体的长度。

返回值说明:

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

函数作用:

把我们打开的端口和进程绑定起来,这也是为什么bind的第二个参数是struct sockaddr *addr,因为这里面就有IP+PORT,这两个字段可以标识地球上唯一的一个进程!绑定之后,就代表这个用户网络通信的端口和该进程绑定了,以后该进程发送数据,接收数据,都是从这个端口中发送接收!换句话说:我们现在只有电话,没有电话号码,需要让电话绑定一个电话号码,才可以打电话

4:recvfrom

读取数据的函数叫做recvfrom,该函数的函数原型如下:

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

参数说明:

  • sockfd:对应操作的文件描述符。表示从该文件描述符索引的文件当中读取数据。
  • buf:读取数据的存放位置。
  • len:期望读取数据的字节数。
  • flags:读取的方式。一般设置为0,表示阻塞读取。
  • src_addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
  • addrlen:调用时传入期望读取的src_addr结构体的长度,返回时代表实际读取到的src_addr结构体的长度,这是一个输入输出型参数。

返回值说明:

  • 读取成功返回实际读取到的字节数,读取失败返回-1,同时错误码会被设置。

函数作用:

从我们创建的套接字,也就是我们打开的端口进行读取数据

解释:

①:因为我们从我们socket打开的端口读取数据,所以第一个参数sockfd就是我们的本身进程调用的socket的返回值

②:src_addr表示对端网络的属性,其实本质就是我们接收到信息,我们一定是知道是谁发给我们的,比如服务器端接收到客户端的信息,进行处理之后,可能是要发回给用户,毋庸置疑!所以如果我是服务端,则src_addr代表的就是客户端的信息

5:sendto

UDP客户端发送数据的函数叫做sendto,该函数的函数原型如下:

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,表示阻塞写入。
  • dest_addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。 addrlen:传入dest_addr结构体的长度。

返回值说明:

  • 写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。

函数作用:

从我们创建的套接字,也就是我们打开的端口进行发送数据

解释:

①:因为我们从我们socket打开的端口发送数据,所以第一个参数sockfd就是我们的本身进程调用的socket的返回值

②:dest_addr是对端网络的属性,因为我们发送数据,要知道对方是谁,这个函数一般就是recvfrom函数接受到的对端信息,直接填写进来即可!         

③:最后一个参数socklen_t定义的变量,不可以是直接定义为0, 必须用倒数第二个参数const struct sockaddr *dest_addr去初始化,这是规定!

三:UDP 下的CV程序的要点

我们一般在UDP/TCP下实现的程序,都是CV程序,也就是客户端服务端的程序,而这类程序都有几个固定的写法~

1:填充结构体

我们在bind的时候,bind的第二个参数是const struct sockaddr *addr,我们是网络通信,所以选择创建一个stuuct sockaddr_in的结构体local去填充,所以我们只需要填充local的三个字段,如下:

struct sockaddr_in {sa_family_t    sin_family;   // 地址族in_port_t      sin_port;     // 端口号struct in_addr sin_addr;     // IPv4地址...
};

但是在填充之前,我们一般使用bzero函数(memset也可以 ) 对创建的结构体清零,这是好习惯

其次其中的in_addr不能直接填充IP地址,而是local.sin_addr.s_addr再填充IP地址,很显然,这是一个结构体嵌套结构体的类型

2:IP和PORT转化为网络字节序

而我们的不管是服务端还是客户端的IP和PORT都会经过网络,所以在填充进const struct sockaddr *addr 类型的变量之前,我们需要先使用接口,确保先转化为网络字节序即可!

PORT填充进结构体之前,使用htons接口即可,但是IP则不一样了!

我们知道IP一般是点分十进制的一个字符串,点分十进制的好处是方便用户观看,("192.168.3.1”)但是真正的IP地址其实根本不需要点( . ),应该只保留192和168和3和1,说白了就是只需要四个字节,所以IP需要先转换为4个字节,再进行转化为网络字节序,这是网络设计者规定的,因为4字节相对于点分十进制,大大减少了占用的大小!

所以只需要接口inet_addr,就能完成这两件事(点分十进制转化为4字节,4字节转化为网络字节序)

3:关于CS双方的IP和POAT

Q1:服务端怎么知道自己的IP和PORT,然后去bind?

A1:任何服务器,都是设计者给定了IP和PORT,然后服务器代码内部获取到了给定的IP和PORT去进行bind的,所以我们写的程序,一般在服务端运行起来的时候,把IP和PORT作为main的参数列表 传进去即可!

Q2:服务器怎么知道客户端的IP和PORT?

A2:服务器使用recvfrom接受到数据的时候,从recvfrom的参数中就能获取到客户端的IP+PORT,然后使用sendto发信息的时候,把从recvfrom参数中获取到的客户端的信息填入到sendto的参数中即可!

Q3:客户端怎么知道自己的IP和PORT,然后去bind?

A3:客户端不需要显式的bind,OS会自己替你去隐式的bind,所以客户端代码不需要知道自己的IP+端口号,也不需要显式的bind,OS会替你做!

设计者这样设计是为了防止端口号的冲突!!如果显式也就是写死了端口号,这就意味着可能存在端口号的冲突的情况!

例子:

现在,抖音 和 拼多多 的工程师都决定:“好,那我就让我这个App的所有网络连接都使用客户端的 12345 端口吧。”
你先打开了抖音。
抖音启动,成功绑定了你手机的 12345 端口。
它开始用这个端口和抖音的服务器愉快通信。
然后,你又打开了拼多多:
拼多多启动,它也试图去绑定 12345 端口。
操作系统的反应:“不行!拒绝访问!这个端口已经被抖音占用了!”
结果就是:拼多多网络初始化失败,无法连接服务器,App卡死、闪退或者提示网络错误。
这下问题就严重了!两个App,你只能用一个,因为它们“撞门”了。这对用户体验来说是灾难性的。

Q4:客户端怎么知道服务端的IP和PORT?

A4:我们需要告诉客户端的服务端的IP和PORT,这是必然的,就好比我们访问百度官网,我们是通过网址去访问的,网址本身就是IP+PORT,再比如平时听到的广告,买东西就上什么,网址www.xxxx,这都说明了,任何客户端想要访问服务端,都是需要自己知道服务端的IP+PORt的

总结:

①:我们运行服务端需要传给服务端代码IP+PORT,运行客户端也需要告诉客户端代码服务端的IP+PORT,而客户端自己的IP+PORT是OS隐式选择然后进行绑定bind的,无需我们关心!

②:读取信息recvfrom:读取信息的同时,通过参数得知是谁发给自己的!
发送信息sendto:你需要知道你要发给谁,一般从recvfrom的参数中获取!

③:所以服务端一般收到请求知道了谁发给自己的,就会完成操作后发给这个人

④:所以我们的运行客户端或者服务端的时候,main的参数列表都要传递IP+PORT,这两套IP+PORT都是服务端的!

Q5:那服务端IP和PORT怎么选择?

A5:

IP:服务端IP我们可以设置为127.0.0.1,其叫做本地还回,专门用来本地的两个进程模拟实现网络通信的,实际上是数据在本主机中贯穿了两次协议栈。

一般云服务器是不允许绑定除开127.0.0.1之外的其他IP的,这是因为一旦绑定了一个确定的IP,则代表任何客户端只能通过该IP+端口号来进行网络通信,这是不允许的,因为一个主机不仅仅只有一个IP,明明可以通过多种IP来网络通信,但是你的行为让其效率大大降低

所以我们一般直接把服务端的IP直接设置为0 或 0.0.0.0,代表服务端的任何IP+正确的端口号即可向服务端发送数据!

端口号:一般选择8888即可

4:如何得到对方的IP+PORT

很显然我们的sendto是发送数据,recvfrom是接收数据!

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

我们recvfrom的时候,可以获取到对方的网络属性结构体,内含IP+PORT!

我们的sendto的时候,需要填写对方的网络属性结构体,去进行发送数据!

Q:我们sendto压根没有发送自己端的网络属性结构体,为什么对方能知道我们的网络属性?

A:因为我们说过,其实我们的socket接口,其实只是做了一小部分的事情,更多的事情是协议栈去完成的,所以当您调用 sendto() 时,发生了以下事情:


①:您告诉内核:“请把‘Hello’发给 192.168.1.100:8888”。
②:内核拿到数据后,自动为您封装一个标准的数据包。它会在“Hello”前面加上一个UDP头和IP头

③:IP头里包含了:

源IP地址 (Source IP Address):您的客户端机器的IP地址(比如 172.16.2.5)。这是内核知道的。
目的IP地址 (Destination IP Address):您指定的服务端IP 192.168.1.100。

④:UDP头里包含了:

源端口 (Source Port):内核为您的客户端套接字自动分配的临时端口(比如 54321)
目的端口 (Destination Port):您指定的服务端端口 8080。

所以不需要担心我们任何一端怎么在发送信息的同时,去发送本端的IP+PORT,协议栈会做!而我们使用recvfrom的时候,只用定义出对应的输出型参数,发送的时候直接使用即可!

     

四:UDP echo程序代码

注:代码中包含的日志函数,如想了解,在此篇博客中单独有讲:

https://blog.csdn.net/shylyly_/article/details/151263351

1:UdpServer.hpp(服务端)

#pragma once#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"// 一些发生错误时候 返回的枚举变量
enum
{SOCKET_ERROR = 1,BIND_ERROR,USAGE_ERROR
};// socket接口默认的返回值
const static int defaultfd = -1;class UdpServer
{
public:// 构造函数                 socket的返回值       端口号         是否运行UdpServer(uint16_t port) : _sockfd(defaultfd), _port(port), _isrunning(false){}// 析构函数~UdpServer(){}// 初始化服务端void InitServer(){// 1:创建udp socket 套接字_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // AF_INET代表网络通信  SOCK_DGRAM代表数据报 所以就是UDP 第三个参数填写0即可// 创建套接字失败 打印语句提醒if (_sockfd < 0){LOG(FATAL, "socket error, %s, %d\n", strerror(errno), errno);exit(SOCKET_ERROR);}// 创建套接字成功 打印语句提醒LOG(INFO, "socket create success, sockfd: %d\n", _sockfd);// 2.0 填充sockaddr_in结构struct sockaddr_in local; // 网络通信 所以定义struct sockaddr_in类型的变量bzero(&local, sizeof(local)); // 先把结构体情况 好习惯local.sin_family = AF_INET;         // 填写第一个字段 (地址类型)local.sin_port = htons(_port);      // 填写第二个字段PORT (需先转化为网络字节序)local.sin_addr.s_addr = INADDR_ANY; // 填写第三个字段IP (直接填写0即可,INADDR_ANY就是为0的宏)// 2:bind绑定// 我们填充好的local和我们创建的套接字进行绑定(绑定我们接收信息发送信息的端口)int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));// 绑定失败 打印语句提醒if (n < 0){LOG(FATAL, "bind error, %s, %d\n", strerror(errno), errno);exit(BIND_ERROR);}// 绑定成功 打印语句提醒LOG(INFO, "socket bind success\n");}// 启动服务端void Start(){// 先把bool值置为true 代表服务端在运行_isrunning = true;while (true) // 服务端都是死循环{char buffer[1024];            // 对方端发来的信息 存储在buffer中struct sockaddr_in peer;      // 对方端的网络属性socklen_t len = sizeof(peer); // 必须初始化成为sizeof(peer) 不能初始化为0// 我们要让server先收数据ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (n > 0){buffer[n] = 0;                                                             // 打印服务器回复sendto(_sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&peer, len); // 再要将server收到的数据,发回给对方}}_isrunning = false; // 再把bool值置为false}private:int _sockfd;     // socket的返回值 在多个接收中都需要使用uint16_t _port;  // 服务器所用的端口号bool _isrunning; // 反映是否在运行的bool值
};

解释:逻辑和上文的描述一致

①:socket创建套接字(创建一个通信的端口)

②:bind绑定套接字(把打开的端口和进程绑定起来)

③:recvfrom接收客户端发来的信息

④:sendto返回相同的信息给客户端

在bind之前,我们需要事先定义好一个struct sockaddr_in local,把服务端的网络属性(IP,PORT,地址类型)填充进去,再进行绑定

在recvfrom之前,我们需要事先定义好一个struct sockaddr_in peer,并且要用peer去初始化我们recvfrom的第四个参数(socklen_t len = sizeof(peer)),用于接收客户端的网络属性

在sendto的时候,我们就可以把peer填入到sendto的参数中了,明确向谁发送

2:main.cc

#include <iostream>
#include <memory>
#include "UdpServer.hpp"// 运行服务端的时候,因为IP在服务端文件中给定0了
// 所以只需要使用者只需要给服务端一个PORT即可
void Usage(std::string proc)
{std::cout << "Usage:\n\t" << proc << " local_port\n"<< std::endl;
}int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(USAGE_ERROR);}EnableScreen(); // 表示把日志打印到屏幕上uint16_t port = std::stoi(argv[1]); // 从main的参数列表中获取到PORTstd::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port); // 创建服务端对象usvr->InitServer();                                                  // 初始化服务端usvr->Start();                                                       // 启动服务端return 0;
}

解释:

①:main.cc是用来调用UdpServer.hpp(服务端)的,这种两个文件分离的写法是松耦合的,比较优秀,但在后面的客户端,我们就不分开写了,避免麻烦

②:而我们的服务端运行起来原本是需要输入服务端主机的IP+PORT的,但是我们上文说过,IP已经在服务端代码内部置为了INADDR_ANY,这个宏就是0,所以我们运行服务端的时候,我们只需要输入一个main的参数即可,端口号选作8888

③:创建服务端的对象,采取智能指针,比较安全

④:EnableScreen(); // 表示把日志打印到屏幕上,这是日志文件的功能

3:UdpClient.cc(客户端)

#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>// 启动客户端 需要用户输入服务端的IP+PORT
void Usage(std::string proc)
{std::cout << "Usage:\n\t" << proc << " serverip serverport\n"<< std::endl;
}int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(1);}std::string serverip = argv[1];           // 从main的参数列表中获取到服务端IPuint16_t serverport = std::stoi(argv[2]); // 从main的参数列表中获取到服务端PORT// 1. 创建socketint sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cerr << "socket error" << std::endl;}// 2. 无需显式的bind OS会自己做// 用户已经传递服务端的IP+PORT// 所以我们sendto就知道了 给哪个服务端传递信息// 所以需要事先构建好服务端的网络属性struct sockaddr_in server;                            // 定义出struct sockaddr_in的变量memset(&server, 0, sizeof(server));                   // 清零server.sin_family = AF_INET;                          // 地址类型字段的填写server.sin_port = htons(serverport);                  // PORT字段的填写server.sin_addr.s_addr = inet_addr(serverip.c_str()); // IP字段的填写std::string message; // 存放用户输入的信息// 3. 直接通信即可while (true){std::cout << "Please Enter# ";   // 提示用户可以输入你想向服务端发送的信息了std::getline(std::cin, message); // 获取到用户输入的信息 cin到message中// 发送信息sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));// 构建出接收服务端的网络属性结构体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::cout << "server echo# " << buffer << std::endl; // 打印服务器回复的信息}}return 0;
}

解释:

①:为什么客户端不像服务端一样分为两个文件,因为客户端的代码简单,分了反而显得繁琐

②:客户端不需要bind,OS会自己进行bind选择随机的端口号,避免端口号冲突

③:客户端第一件事就是sendto,而我们是知道了服务端的IP+PORT的,所以我们的客户端运行起来需要输入两个main的参数;并且在sendto之前,我们也要事先用我们已知的服务端的信息去创建出服务端网络属性 struct sockaddr_in server;   

④:而我们recvfrom的输出型参数struct sockaddr_in peer;没有复用之前的server是因为,以后我们可能同时被多个服务端发送信息,所以不能共用一个结构体

4:makefile

.PHONY:all
all:udpserver udpclientudpserver:main.ccg++ -o $@ $^ -std=c++14
udpclient:UdpClient.ccg++ -o $@ $^ -std=c++14
.PHONY:clean
clean:rm -f udpserver udpclient

5:Log.hpp

#pragma once#include <iostream>    //C++必备头文件
#include <cstdio>      //snprintf
#include <string>      //std::string
#include <ctime>       //time
#include <cstdarg>     //va_接口
#include <sys/types.h> //getpid
#include <unistd.h>    //getpid
#include <thread>      //锁
#include <mutex>       //锁
#include <fstream>     //C++的文件操作std::mutex g_mutex;                    // 定义全局互斥锁
bool gIsSave = false;                  // 定义一个bool类型 用来判断打印到屏幕还是保存到文件
const std::string logname = "log.txt"; // 保存日志信息的文件名字// 日志等级
enum Level
{DEBUG = 0,INFO,WARNING,ERROR,FATAL
};// 将日志写进文件的函数
void SaveFile(const std::string &filename, const std::string &message)
{std::ofstream out(filename, std::ios::app);if (!out.is_open()){return;}out << message << std::endl;out.close();
}// 日志等级转字符串--->字符串才能表示等级的意义 0123意义不清晰
std::string LevelToString(int level)
{switch (level){case DEBUG:return "Debug";case INFO:return "Info";case WARNING:return "Warning";case ERROR:return "Error";case FATAL:return "Fatal";default:return "Unknown";}
}// 获取当前时间的字符串
// 时间格式包含多个字符 所以干脆糅合成一个字符串
std::string GetTimeString()
{// 获取当前时间的时间戳(从1970-01-01 00:00:00开始的秒数)time_t curr_time = time(nullptr);// 将时间戳转换为本地时间的tm结构体// tm结构体包含年、月、日、时、分、秒等字段struct tm *format_time = localtime(&curr_time);// 检查时间转换是否成功if (format_time == nullptr)return "None";// 缓冲区用于存储格式化后的时间字符串char time_buffer[1024];// 格式化时间字符串:年-月-日 时:分:秒snprintf(time_buffer, sizeof(time_buffer), "%d-%02d-%02d %02d:%02d:%02d",format_time->tm_year + 1900, // tm_year: 从1900年开始的年数,需要加1900format_time->tm_mon + 1,     // tm_mon: 月份范围0-11,需要加1得到实际月份format_time->tm_mday,        // tm_mday: 月中的日期(1-31)format_time->tm_hour,        // tm_hour: 小时(0-23)format_time->tm_min,         // tm_min: 分钟(0-59)format_time->tm_sec);        // tm_sec: 秒(0-60,60表示闰秒)return time_buffer; // 返回格式化后的时间字符串
}// 日志函数-->打印出日志
// 格式:时间 + 等级 + PID + 文件名 + 代码行数 + 可变参数
void LogMessage(int level, std::string filename, int line, bool issave, const char *format, ...)
{std::string levelstr = LevelToString(level); // 得到等级字符串std::string timestr = GetTimeString();       // 得到时间字符串pid_t selfid = getpid();                     // 得到PID// 使用va_接口+vsnprintf得到用户想要的可变参数的字符串 存储与buffer中char buffer[1024];va_list arg;va_start(arg, format);vsnprintf(buffer, sizeof(buffer), format, arg);va_end(arg);std::lock_guard<std::mutex> lock(g_mutex); // 引入C++的RAII的锁 保护打印功能// 保存格式为时间 + 等级 + PID + 文件名 + 代码行数 + 可变参数 的日志信息 到message中std::string message = "[" + timestr + "]" + "[" + levelstr + "]" +"[" + std::to_string(selfid) + "]" +"[" + filename + "]" + "[" + std::to_string(line) + "] " + buffer;// 打印到屏幕if (!issave){std::cout << message << std::endl;}// 保存进文件else{SaveFile(logname, message);}
}// 宏定义 省略掉__FILE__  和 __LINE__
#define LOG(level, format, ...)                                                \do                                                                         \{                                                                          \LogMessage(level, __FILE__, __LINE__, gIsSave, format, ##__VA_ARGS__); \} while (0)// 用户调用则意味着保存到文件
#define EnableScreen() (gIsSave = false)// 用户调用则意味着打印到屏幕
#define EnableFile() (gIsSave = true)

解释:https://blog.csdn.net/shylyly_/article/details/151263351

6:运行效果

解释:

①:服务端只需要指定端口号即可,因为IP已经内置为了0

②:客户端则需指定服务端的IP+PORT,端口号必须是8888,而IP只要是服务端主机的就行


文章转载自:

http://Pan59WHT.hnzrL.cn
http://lj882cZz.hnzrL.cn
http://9gDEfgq0.hnzrL.cn
http://xVZpxYXw.hnzrL.cn
http://kHCSZ03E.hnzrL.cn
http://rw1sbWDk.hnzrL.cn
http://HSH5E4B7.hnzrL.cn
http://COXWjgcN.hnzrL.cn
http://ZFZ8j0Nw.hnzrL.cn
http://utovNVnk.hnzrL.cn
http://QfQ9bhuL.hnzrL.cn
http://60XDiSDE.hnzrL.cn
http://tA0EHf1N.hnzrL.cn
http://PLq5gHr7.hnzrL.cn
http://ku5Pfiia.hnzrL.cn
http://epax4FVm.hnzrL.cn
http://PUC9DE7q.hnzrL.cn
http://1EItnZfe.hnzrL.cn
http://VJ0NYUaZ.hnzrL.cn
http://dmE3GER0.hnzrL.cn
http://yLA3vLlg.hnzrL.cn
http://sNJTiioQ.hnzrL.cn
http://8Kz78r6S.hnzrL.cn
http://LLB5KbbQ.hnzrL.cn
http://do5DCYbB.hnzrL.cn
http://YbpinUMg.hnzrL.cn
http://vR4Ybh4x.hnzrL.cn
http://ffWVENUI.hnzrL.cn
http://WCOBX0D0.hnzrL.cn
http://mJsKyKLi.hnzrL.cn
http://www.dtcms.com/a/378564.html

相关文章:

  • Pytest+requests进行接口自动化测试2.0(yaml)
  • 【容器使用】如何使用 docker 和 tar 命令来操作容器镜像
  • 科普:在Windows个人电脑上使用Docker的极简指南
  • 【面试场景题】电商订单系统分库分表方案设计
  • 微服务保护全攻略:从雪崩到 Sentinel 实战
  • springcloud二-Sentinel
  • Redis 持久化与高可用实践(RDB / AOF / Sentinel / Cluster 全解析)
  • Semaphore 信号量深度解析
  • 门店网络重构:告别“打补丁”,用“云网融合”重塑数字竞争力!
  • Linux操作系统之Ubuntu
  • WSL自定义安装多个相同版本的Ubuntu子系统
  • 晶振在5G时代的角色:高精度时钟的核心支撑
  • 【JavaEE】(25) Spring 原理
  • 【科研绘图系列】R语言绘制模型预测与数据可视化
  • 音频中的PDM、PCM概念解读
  • 离线应用开发:Service Worker 与缓存
  • 1、RocketMQ概念详解
  • ZooKeeper Multi-op+乐观锁实战优化:提升分布式Worker节点状态一致性
  • 使用yolo算法对视频进行实时目标跟踪和分割
  • Tomcat日志乱码了怎么处理?
  • 新手该选哪款软件?3ds Max vs Blender深度对比
  • 剧本杀小程序系统开发:构建线上线下融合的剧本杀生态圈
  • 常用加密算法之 AES 简介及应用
  • 【SQL注入系列】JSON注入
  • 盲盒抽卡机小程序:从0到1的蜕变之路
  • 设计模式(C++)详解—工厂方法模式(1)
  • 【Proteus仿真】【51单片机】教室灯光控制器设计
  • java语言中,list<String>转成字符串,逗号分割;List<Integer>转字符串,逗号分割
  • Jenkins运维之路(Jenkins流水线改造Day01)
  • 9月11日星期四今日早报简报微语报早读