网络编程套接字 (二)---udosocket

本专栏内容为:Linux学习专栏,分为系统和网络两部分。 通过本专栏的深入学习,你可以了解并掌握Linux。

💓博主csdn个人主页:小小unicorn
⏩专栏分类:网络
🚚代码仓库:小小unicorn的代码仓库🚚
🌹🌹🌹关注我带你学习编程知识

udpsocket

  • 整体框架1(代码结构)
  • 服务端创建套接字
    • socket函数
    • socket函数属于什么类型的接口?
    • socket函数是被谁调用的?
    • socket函数底层做了什么?
    • 服务端创建套接字
  • 服务端绑定
    • bind函数
    • struct sockaddr_in结构体
    • 如何理解绑定?
    • 增加ip地址和端口号
  • 字符串IP VS 整数IP
    • 整数IP存在的意义
    • 字符串IP和整数IP相互转换的方式
    • inet_addr函数
    • inet_ntoa函数
    • 服务端绑定
  • 运行服务器
    • recvfrom函数
    • 启动服务器函数
  • INADDR_ANY
    • 绑定INADDR_ANY的好处
    • 更改代码
    • 引入命令行参数
  • 整体框架2(代码结构)
  • 客户端创建套接字
  • 关于客户端的绑定问题
  • 启动客户端
    • 增加服务端IP地址和端口号
    • sendto函数
    • 启动客户端函数
  • 引入命令行参数
  • 测试:
    • 网络测试:
    • 分发客户端
    • 进行网络测试

整体框架1(代码结构)

在正式编写我们的udpsocket之前我们先把准备工作做好将代码整体框架搭建好。
在这里插入图片描述

makefile

udpserve:Main.ccg++ -o $@ $^ -std=c++11
.PHONY:clean
clean:rm udpserve

对于我们的服务器来说,干两件事,初始化,初始化完成后就让它跑起来:

main.cc

#include"UdpServe.hpp"
#include<memory>int main()
{std::unique_ptr<UdpServe> svr(new UdpServe);//初始化svr->Init();//跑起来svr->Run();return 0;
}

udpserver.hpp

#pragma once#include"log.hpp"
class UdpServe
{
public:UdpServe(){}void Init(){}void Run(){}~UdpServe(){}
private:};

跑起来试一下:
在这里插入图片描述
能跑成功,接下来就实现我们相关的接口函数即可。

注意:
因为可能需要用到打印信息,也就是需要一个日志功能,我们将引用之前的log日志,具体代码如下:
log.hpp

#pragma once
#include<iostream>
#include<time.h>
#include<stdarg.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdlib.h
#define SIZE 1024
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define Screen 1
#define Onefile 2
#define Classfile 3
#define logFile "log.txt"
class Log
{
public:Log(){printMethod=Screen;path="./log/";}~Log(){}void Enable(int method){printMethod=method;}std::string levelToString(int level){switch(level){case Info:return "Info";case Debug:return "Debug";case Warning:return "Warning";case Error:return "Error";case Fatal:return "Fatal";default:return "None";}}void printLog(int level,const std::string &logtxt){switch(printMethod){case Screen:std::cout<<logtxt<<std::endl;break;case Onefile:printOneFile(logFile,logtxt);break;case Classfile:printClassfie(level,logtxt);break;default:break;}}void printOneFile(const std::string &logname,const std::string &logtxt){std::string _logname=path+logname;int fd=open(_logname.c_str(),O_WRONLY | O_CREAT | O_APPEND,0666);//"log.txt"if(fd<0){return;}write(fd,logtxt.c_str(),logtxt.size());close(fd);}void printClassfie(int level,const std::string &logtxt){std::string filename =logFile;filename += ".";filename += levelToString(level);//"log.txt.Debug/Warning/Fatal"printOneFile(filename,logtxt);}void operator()(int level,const char *format, ...){time_t t=time(nullptr);struct tm *ctime=localtime(&t);char leftbuffer[SIZE];snprintf(leftbuffer,sizeof(leftbuffer),"[%s][%d-%d-%d %d:%d:%d]",levelToString(level).c_str(),ctime->tm_year+1900,ctime->tm_mon+1,ctime->tm_mday,ctime->tm_hour,ctime->tm_min,ctime->tm_sec);va_list s;va_start(s,format);char rightbuffer[SIZE];vsnprintf(rightbuffer,sizeof(rightbuffer),format,s);va_end(s);//格式:默认部分+自定义部分char logtxt[SIZE*2];snprintf(logtxt,sizeof(logtxt),"%s %s",leftbuffer,rightbuffer);//print("%s",logtxt);//暂时打印printLog(level,logtxt);}private:int printMethod;std::string path;
};

服务端创建套接字

我们把服务器封装成一个类,当我们定义出一个服务器对象后需要马上初始化服务器,而初始化服务器需要做的第一件事就是创建套接字。

socket函数

在这里插入图片描述

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

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

参数解释:

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

在这里插入图片描述

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

同时需要包含两个头文件:

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

socket函数属于什么类型的接口?

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

socket函数是被谁调用的?

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

socket函数底层做了什么?

socket函数是被进程所调用,而每一个进程在系统层面上都有一个进程地址空间PCB(task_struct),文件描述符表(file_struct)以及对应打卡的各种文件,而文件描述符表里面包含了一个数组(fd_array),其中数组中的0,1,2下标依次对应的就是标准输入,标准输出和标准错误。
在这里插入图片描述
当我们调用socket函数创建套接字时,实际相当于我们打开的一个“网络文件”,打开后在内核层面上就形成了一个对应的struct file结构体,同时该结构体被连入到了该进程对应的文件双链表,并将该结构体的首地址填入到了fd_array数组中下标为3的位置,此时fd_array数组中下标为3的指针就指向了这个打开的“网络文件”,最后3号文件描述符作为socket函数的返回值返回给了客户。
在这里插入图片描述
其中每一个struct file结构体中包含的就是对应打开文件各种信息,比如文件的属性信息,操作方法以及文件缓冲区等。其中文件对应的属性在内核中是由struct inode结构体维护的,而文件对应的操作方法实际就是一堆的函数指针(比如read*write*)在内核当中就是由struct file_operations结构体来维护的。而文件缓冲区对于打开的普通文件来说对应的一般是磁盘,但对于现在打开的“网络文件”来说,这里的文件缓冲区对用的就是网卡。
在这里插入图片描述
对于一般的普通文件来说,当用户通过文件描述符将数据写到文件缓冲区,然后再把数据刷到磁盘上就完成了数据的写入操作,而对于现在socket函数打开的“网络文件”来说,当用户将数据写到文件缓冲区后,操作系统会定期将数据刷到网卡上,而网卡则是负责数据发送的,因此数据最终就发送到了网络当中。

服务端创建套接字

当我们在进行初始化服务器创建套接字时,就是调用socket函数创建套接字,创建套接字时我们需要填入的协议家族就是AF_INET,因为我们要进行的是网络通信,而我们需要的服务类型就是SOCK_DGRAM,因为我们现在编写的UDP服务器是面向数据报的,而第三个参数设置为0即可。
UdpServer.hpp

#pragma once
#include<sys/types.h>
#include<sys/socket.h>
#include<iostream>
#include<string>
#include<strings.h>
#include<cstring>
#include"log.hpp"
//创建日志功能
Log lg;
//枚举常量
enum
{SOCKET_ERR=1,BIND_ERR
};
class UdpServer
{
public:void Init(){//构造函数UdpServe(){}//1.创建udp socketsockfd_=socket(AF_INET,SOCK_DGRAM,0);//PF_INET//网络文件描述符小于0,打印错误信息if(sockfd_<0){//打印错误信息lg(Fatal,"socket create error,sockfd:%d",sockfd_);//以错误信息退出exit(SOCKET_ERR);}lg(Info,"socket creat  success, sockfd: %d",sockfd_);}~UdpServe(){if(sockfd_>0)//关闭close(sockfd_);}
private:int sockfd_; //网络文件描述符
};

注意: 当析构服务器时,我们可以将sockfd对应的文件进行关闭,但实际上不进行该操作也行,因为一般服务器运行后是就不会停下来的。(也可以这样理解,因为服务器一天24小时都在跑,因此他在代码里面是一个死循环)

这里我们可以做一个简单的测试,看看套接字是否创建成功。

#include"UdpServer.hpp"
#include<memory>
int main()
{std::unique_ptr<UdpServer> svr(new UdpServer);//初始化svr->Init();//跑起来svr->Run();return 0;
}

在这里插入图片描述

运行程序后可以看到套接字是创建成功的,对应获取到的文件描述符就是3,这也很好理解,因为0、1、2默认被标准输入流、标准输出流和标准错误流占用了,此时最小的、未被利用的文件描述符就是3。

服务端绑定

现在套接字已经创建成功了,但作为一款服务器来讲,如果只是把套接字创建好了,那我们也只是在系统层面上打开了一个文件,操作系统将来并不知道是要将数据写入到磁盘还是刷到网卡,此时该文件还没有与网络关联起来
在这里插入图片描述
由于现在编写的是不面向连接的UDP服务器,所以初始化服务器要做的第二件事就是绑定。

bind函数

在这里插入图片描述

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

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

参数解释:

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

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

struct sockaddr_in结构体

在绑定时需要将网络相关的属性信息填充到一个结构体当中,然后将该结构体作为bind函数的第二个参数进行传入,这实际就是struct sockaddr_in结构体。

我们转到socket_in的定义:
在这里插入图片描述
在这里插入图片描述

可以看到,struct sockaddr_in当中的成员如下:

  1. sin_family:表示协议家族。
  2. sin_port:表示端口号,是一个16位的整数。
  3. sin_addr:表示IP地址,是一个32位的整数。
    剩下的字段一般不做处理,当然你也可以进行初始化。

其中sin_addr的类型是struct in_addr,实际该结构体当中就只有一个成员,该成员就是一个32位的整数,IP地址实际就是存储在这个整数当中的。
在这里插入图片描述

如何理解绑定?

在进行绑定的时候需要将IP地址和端口号告诉对应的网络文件,此时就可以改变网络文件当中文件操作函数的指向,将对应的操作函数改为对应网卡的操作方法,此时读数据和写数据对应的操作对象就是网卡了,所以绑定实际上就是将文件和网络关联起来。

增加ip地址和端口号

由于绑定时需要用到IP地址和端口号,因此我们需要在服务器类当中引入IP地址和端口号,在创建服务器对象时需要传入对应的IP地址和端口号,此时我们就可以根据传入的IP地址和端口号对对应的成员进行初始化。

同时我们之前也说过,服务器要一天二十四小时工作,我们定义一个变量来记录服务器的工作情况,会在Run()函数接口用到,这里我们直接先定义好,在Run()函数那就直接拿来用啦。

同时我们给port和ip给一个缺省值:

class UdpServer
{
// 初始化  缺省值
//端口号默认设为8080
uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";
const int size = 1024;
public://构造函数UdpServe(const uint16_t &port = defaultport, const std::string &ip = defaultip):sockfd_(0),ip_(ip),port_(port),isrunning_(false)~UdpServer(){if (_sockfd >= 0){close(_sockfd);}};
private:int sockfd_; //网络文件描述符std::string ip_;//任意地址绑定uint16_t port_;//表明服务器进程的端口号bool isrunning_;//服务器的状态
};

注意: 虽然这里端口号定义为整型,但由于端口号是16位的,因此我们实际只会用到它的低16位。

字符串IP VS 整数IP

IP地址的表现形式有两种:

  1. 字符串IP:类似于192.168.233.123这种字符串形式的IP地址,叫做基于字符串的点分十进制IP地址。
  2. 整数IP:IP地址在进行网络传输时所用的形式,用一个32位的整数来表示IP地址。

整数IP存在的意义

网络传输数据时是寸土寸金的,如果我们在网络传输时直接以基于字符串的点分十进制IP的形式进行IP地址的传送,那么此时一个IP地址至少就需要15个字节,但实际并不需要耗费这么多字节。

IP地址实际可以划分为四个区域,其中每一个区域的取值都是0~255,而这个范围的数字只需要用8个比特位就能表示,因此我们实际只需要32个比特位就能够表示一个IP地址。其中这个32位的整数的每一个字节对应的就是IP地址中的某个区域,我们将IP地址的这种表示方法称之为整数IP,此时表示一个IP地址只需要4个字节。

在这里插入图片描述
因为采用整数IP的方案表示一个IP地址只需要4个字节,并且在网络通信也能表示同样的含义,因此在网络通信时就没有用字符串IP而用的是整数IP,因为这样能够减少网络通信时数据的传送。

字符串IP和整数IP相互转换的方式

转换的方式有很多,比如我们可以定义一个位段A,位段A当中有四个成员,每个成员的大小都是8个比特位,这四个成员就依次表示IP地址的四个区域,一共32个比特位。

然后我们再定义一个联合体IP,该联合体当中有两个成员,其中一个是32位的整数,其代表的就是整数IP,还有一个就是位段A类型的成员,其代表的就是字符串IP。
在这里插入图片描述
由于联合体的空间是成员共享的,因此我们设置IP和读取IP的方式如下:

  1. 当我们想以整数IP的形式设置IP时,直接将其赋值给联合体的第一个成员就行了。
  2. 当我们想以字符串IP的形式设置IP时,先将字符串分成对应的四部分,然后将每部分转换成对应的二进制序列依次设置到联合体中第二个成员当中的p1、p2、p3和p4就行了。
  3. 当我们想取出整数IP时,直接读取联合体的第一个成员就行了。
  4. 当我们想取出字符串IP时,依次获取联合体中第二个成员当中的p1、p2、p3和p4,然后将每一部分转换成字符串后拼接到一起就行了。

注意: 在操作系统内部实际用的就是位段和枚举,来完成字符串IP和整数IP之间的相互转换的

转换大小端函数:
在这里插入图片描述

inet_addr函数

在这里插入图片描述

实际在进行字符串IP和整数IP的转换时,我们不需要自己编写转换逻辑,系统已经为我们提供了相应的转换函数,我们直接调用即可。

将字符串IP转换成整数IP的函数叫做inet_addr,该函数的函数原型如下:

in_addr_t inet_addr(const char *cp);

该函数使用起来非常简单,我们只需传入待转换的字符串IP,该函数返回的就是转换后的整数IP。除此之外,inet_aton函数也可以将字符串IP转换成整数IP,不过该函数使用起来没有inet_addr简单。

inet_ntoa函数

在这里插入图片描述

将整数IP转换成字符串IP的函数叫做inet_ntoa,该函数的函数原型如下:

char *inet_ntoa(struct in_addr in);

需要注意的是,传入inet_ntoa函数的参数类型是in_addr,因此我们在传参时不需要选中in_addr结构当中的32位的成员传入,直接传入in_addr结构体即可。

服务端绑定

套接字创建完毕后我们就需要进行绑定了,但在绑定之前我们需要先定义一个struct sockaddr_in结构,将对应的网络属性信息填充到该结构当中。由于该结构体当中还有部分选填字段,因此我们最好在填充之前对该结构体变量里面的内容进行清空,然后再将协议家族、端口号、IP地址等信息填充到该结构体变量当中。

需要注意的是,在发送到网络之前需要将端口号设置为网络序列,由于端口号是16位的,因此我们需要使用前面说到的htons函数将端口号转为网络序列。此外,由于网络当中传输的是整数IP,我们需要调用inet_addr函数将字符串IP转换成整数IP,然后再将转换后的整数IP进行设置。

当网络属性信息填充完毕后,由于bind函数提供的是通用参数类型,因此在传入结构体地址时还需要将struct sockaddr_in*强转为struct sockaddr*类型后再进行传入。

#pragma once
#include<sys/types.h>
#include<sys/socket.h>
#include<iostream>
#include<string>
#include<strings.h>
#include<cstring>
//创建日志功能
Log lg;
//枚举常量
enum
{SOCKET_ERR=1,BIND_ERR
};
/// 初始化  缺省值
uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";
const int size = 1024;class UdpServer
{
public:void Init(){//构造函数UdpServe(const uint16_t &port = defaultport, const std::string &ip = defaultip):sockfd_(0),ip_(ip),port_(port),isrunning_(false){}//1.创建udp socketsockfd_=socket(AF_INET,SOCK_DGRAM,0);//PF_INET//网络文件描述符小于0,打印错误信息if(sockfd_<0){//打印错误信息lg(Fatal,"socket create error,sockfd:%d",sockfd_);//以错误信息退出exit(SOCKET_ERR);}lg(Info,"socket creat  success, sockfd: %d",sockfd_);//2. bind socket 绑定套接字struct sockaddr_in local;//全部置为0;bzero(&local,sizeof(local));//协议家族local.sin_family=AF_INET;//端口号//需要保证我的端口号是网络字节序列,因为该端口号是要给对方发送的//将主机序列转化成网络字节序列,//如果是大端,不用管,如果是小端将小端序列转化成大端序列local.sin_port=htons(port_);//1. string -> uint32_t 2. uint32_t必须是网络序列的 // ??//inet_addr直接将字符串转化成网络序列local.sin_addr.s_addr=inet_addr(ip_.c_str());// local.sin_addr.s_addr = htonl(INADDR_ANY);////绑定://对local进行强转if(bind(sockfd_,(const struct sockaddr*)&local,sizeof(local))<0){//绑定失败//打印错误信息lg(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));exit(BIND_ERR);}//方便进行查看:lg(Info,"bind success, errno: %d, err string: %s", errno, strerror(errno));}~UdpServe(){if(sockfd_>0)//关闭close(sockfd_);}
private:int sockfd_; //网络文件描述符std::string ip_;//任意地址绑定uint16_t port_;//表明服务器进程的端口号
};

看一下有没有绑定成功:
在这里插入图片描述
这里我们能看到绑定是成功了的。

运行服务器

UDP服务器的初始化就只需要创建套接字和绑定就行了,当服务器初始化完毕后我们就可以启动服务器了。

服务器实际上就是在周而复始的为我们提供某种服务,服务器之所以称为服务器,是因为服务器运行起来后就永远不会退出,因此服务器实际执行的是一个死循环代码。由于UDP服务器是不面向连接的,因此只要UDP服务器启动后,就可以直接读取客户端发来的数据。

recvfrom函数

在这里插入图片描述

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

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

参数解释:

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

返回值解释:

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

注意:

1.由于UDP是不面向连接的,因此我们除了获取到数据以外还需要获取到对端网络相关的属性信息,包括IP地址和端口号等。
2. 在调用recvfrom读取数据时,必须将addrlen设置为你要读取的结构体对应的大小。
3. 由于recvfrom函数提供的参数也是struct sockaddr*类型的,因此我们在传入结构体地址时需要将struct sockaddr_in*类型进行强转。

启动服务器函数

现在服务端通过recvfrom函数读取客户端数据,我们可以先将读取到的数据当作字符串看待,将读取到的数据的最后一个位置设置为'\0',此时我们就可以将读取到的数据进行输出,同时我们也可以将获取到的客户端的IP地址和端口号也一并进行输出。

需要注意的是,我们获取到的客户端的端口号此时是网络序列,我们需要调用ntohs函数将其转为主机序列再进行打印输出。同时,我们获取到的客户端的IP地址是整数IP,我们需要通过调用inet_ntoa函数将其转为字符串IP再进行打印输出。

class UdpServer
{
const int size = 1024;
public://将socket跑起来 运行void Run()//void Run(func_t func){isrunning_ = true;//定义一个缓冲区char inbuffer[size];while(isrunning_){//客户端结构体struct sockaddr_in client;socklen_t len = sizeof(client);ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len);if(n < 0){//创建失败lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));continue;}//将信息读到后,当成字符串来看inbuffer[n] = 0;printf("client say: %s\n", inbuffer);//充当一次数据的处理std::string info = inbuffer;std::string echo_string="server echo# "+info;//std::string echo_string = func(info);//发送给对方sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (const sockaddr*)&client, len);}}
private:int sockfd_; //网络文件描述符std::string ip_;//任意地址绑定uint16_t port_;//表明服务器进程的端口号bool isrunning_;//服务器的状态
};

注意: 如果调用recvfrom函数读取数据失败,我们可以打印一条提示信息,但是不要让服务器退出,服务器不能因为读取某一个客户端的数据失败就退出。

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

-n:直接使用IP地址,而不通过域名服务器。
-l:显示监控中的服务器的Socket。
-t:显示TCP传输协议的连线状况。
-u:显示UDP传输协议的连线状况。
-p:显示正在使用Socket的程序识别码和程序名称。
此时你就能查看到对应网络相关的信息,在这些信息中程序名称为./udp_server的那一行显示的就是我们运行的UDP服务器的网络信息。
在这里插入图片描述
你可以尝试去掉-n选项再查看,此时原本显示IP地址的地方就变成了对应的域名服务器。
在这里插入图片描述

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

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

但是这个能跑起来仅仅是一个巧合,这里面有坑,为什么呢?因为我们ip运行 用的是我们默认的值,最后字符串转成4字节会变成0值。
在这里插入图片描述但是我们想让服务器启动的时候绑定我们的ip地址,看一下效果:
在这里插入图片描述
显示绑定失败:
在这里插入图片描述
这是为什么呢?
在这里插入图片描述
其实这个代码在虚拟机上其实是可以运行的,但是云服务器就不行,这是因为服务器直接禁止bind公网ip,因此我们通常让我们的ip为0值,0值意思就是凡是给我这台主机的数据,我们都要根据端口号向上交付。

INADDR_ANY

由于云服务器的IP地址是由对应的云厂商提供的,这个IP地址并不一定是真正的公网IP,这个IP地址是不能直接被绑定的,如果需要让外网访问,此时我们需要bind 0。系统当当中提供的一个INADDR_ANY,这是一个宏值,它对应的值就是0。

因此如果我们需要让外网访问,那么在云服务器上进行绑定时就应该绑定INADDR_ANY,此时我们的服务器才能够被外网访问。

绑定INADDR_ANY的好处

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

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

更改代码

因此,如果想要让外网访问我们的服务,我们这里就需要将服务器类当中IP地址相关的代码去掉,而在填充网络相关信息设置struct sockaddr_in结构体时,将设置的IP地址改为INADDR_ANY就行了。

class UdpServer
{
public:void Init(){//1.创建udp socketsockfd_=socket(AF_INET,SOCK_DGRAM,0);//PF_INET//网络文件描述符小于0,打印错误信息if(sockfd_<0){//打印错误信息lg(Fatal,"socket create error,sockfd:%d",sockfd_);//以错误信息退出exit(SOCKET_ERR);}lg(Info,"socket creat  success, sockfd: %d",sockfd_);//2. bind socket 绑定套接字struct sockaddr_in local;//全部置为0;bzero(&local,sizeof(local));//协议家族local.sin_family=AF_INET;//端口号//需要保证我的端口号是网络字节序列,因为该端口号是要给对方发送的//将主机序列转化成网络字节序列,//如果是大端,不用管,如果是小端将小端序列转化成大端序列local.sin_port=htons(port_);//1. string -> uint32_t 2. uint32_t必须是网络序列的 // ??//inet_addr直接将字符串转化成网络序列//local.sin_addr.s_addr=inet_addr(ip_.c_str());local.sin_addr.s_addr = htonl(INADDR_ANY);////绑定://对local进行强转if(bind(sockfd_,(const struct sockaddr*)&local,sizeof(local))<0){//绑定失败//打印错误信息lg(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));exit(BIND_ERR);}//方便进行查看:lg(Info,"bind success, errno: %d, err string: %s", errno, strerror(errno));}
private:int sockfd_;  //网络文件描述符std::string ip_;//任意地址绑定uint16_t port_;//表明服务器进程的端口号bool isrunning_;//服务器的状态
};

此时当我们再重新编译运行服务器时就不会绑定失败了,并且此时当我们再用netstat命令查看时会发现,该服务器的本地IP地址变成了0.0.0.0,这就意味着该UDP服务器可以在本地读取任何一张网卡里面的数据。

引入命令行参数

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

由于云服务器的原因,后面实际不需要传入IP地址,因此在运行服务器的时候我们只需要传入端口号即可

int main(int argc,char*argv[])
{if(argc!=2){Usage(argv[0]);exit(0);}uint16_t port=std::stoi(argv[1]);std::unique_ptr<UdpServe> svr(new UdpServe(port));//初始化svr->Init();//跑起来svr->Run();return 0;
}

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

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

整体框架2(代码结构)

服务端写好后,我们为方便测试,我们将在写一个客户端。因此代码整体结构就要进行微调:
在这里插入图片描述

Makefile

.PHONY:all
all:udpserver udpclient
udpserver:Main.ccg++ -o $@ $^ -std=c++11
udpclient:ClientMain.ccg++ -o $@ $^ -std=c++11
.PHONY:clean
clean:rm -f udpserver udpclient

ClientMain.cc

#include"UdpClient.hpp"
#include<memory>
int main()
{UdpClient* clt = new UdpClient(server_ip, server_port);clt->InitClient();clt->Run();return 0;
}

客户端创建套接字

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

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

#pragma once
//两个socket头文件
#include<sys/types.h>
#include<sys/socket.h>
#include<iostream>
#include<string>
#include<strings.h>
#include<cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include"log.hpp"
//创建日志功能
Log lg;
//枚举常量:
enum
{SOCKET_ERR=1,BIND_ERR
};
class UdpClient
{
public:void InitClient(){//创建套接字sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd_ < 0){//打印错误信息lg(Fatal,"socket create error,sockfd:%d",sockfd_);//以错误信息退出exit(SOCKET_ERR);}lg(Info,"socket creat  success, sockfd: %d",sockfd_);}~UdpClient(){if (_sockfd >= 0){close(_sockfd);}}
private:int sockfd_; //文件描述符
};

关于客户端的绑定问题

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

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

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

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

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

启动客户端

增加服务端IP地址和端口号

作为一个客户端,它必须知道它要访问的服务端的IP地址和端口号,因此在客户端类当中需要引入服务端的IP地址和端口号,此时我们就可以根据传入的服务端的IP地址和端口号对对应的成员进行初始化。

class UdpClient
{
public://构造函数UdpClient(std::string server_ip, int server_port):sockfd_(-1),server_port_(server_port),server_ip_(server_ip){}void InitClient(){//创建套接字sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd_ < 0){//打印错误信息lg(Fatal,"socket create error,sockfd:%d",sockfd_);//以错误信息退出exit(SOCKET_ERR);}lg(Info,"socket creat  success, sockfd: %d",sockfd_);}~UdpClient(){if (_sockfd >= 0){close(_sockfd);}}
private:int sockfd_; //文件描述符int server_port_; //服务端端口号std::string server_ip_; //服务端IP地址
};

当客户端初始化完毕后我们就可以将客户端运行起来,由于客户端和服务端在功能上是相互补充的,既然服务器是在读取客户端发来的数据,那么客户端就应该想服务端发送数据。

sendto函数

在这里插入图片描述

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

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

参数解释:

  1. sockfd:对应操作的文件描述符。表示将数据写入该文件描述符索引的文件当中。
  2. buf:待写入数据的存放位置。
  3. len:期望写入数据的字节数。
  4. flags:写入的方式。一般设置为0,表示阻塞写入。
  5. dest_addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。 addrlen:传入dest_addr结构体的长度。

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

注意:
由于UDP不是面向连接的,因此除了传入待发送的数据以外还需要指明对端网络相关的信息,包括IP地址和端口号等。
由于sendto函数提供的参数也是struct sockaddr*类型的,因此我们在传入结构体地址时需要将struct sockaddr_in*类型进行强转。

启动客户端函数

现在客户端要发送数据给服务端,我们可以让客户端获取用户输入,不断将用户输入的数据发送给服务端。

需要注意的是,客户端中存储的服务端的端口号此时是主机序列,我们需要调用htons函数将其转为网络序列后再设置进struct sockaddr_in结构体。同时,客户端中存储的服务端的IP地址是字符串IP,我们需要通过调用inet_addr函数将其转为整数IP后再设置进struct sockaddr_in结构体。

class UdpClient
{
public:void Run(){std::string message;//std::string msg;struct sockaddr_in server;bzero(&server, sizeof(server));server.sin_family = AF_INET;//主机转网络server.sin_port = htons(server_port_); server.sin_addr.s_addr = inet_addr(server_ip_.c_str());socklen_t len = sizeof(server);char buffer[1024];while (true){std::cout << "Please Enter@ ";getline(std::cin, message);// std::cout << message << std::endl;// 1. 数据 2. 给谁发sendto(sockfd_, message.c_str(), message.size(), 0, (struct sockaddr *)&server, len);struct sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t s = recvfrom(sockfd_, buffer, 1023, 0, (struct sockaddr*)&temp, &len);if(s > 0){buffer[s] = 0;std::cout << buffer << std::endl;}}}}
private:int sockfd_; //文件描述符int server_port_; //服务端端口号std::string server_ip_; //服务端IP地址
};

引入命令行参数

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

#include"UdpClient.hpp"
#include<memory>
int main(int argc, char* argv[])
{if (argc != 3){std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;return 1;}std::string server_ip = argv[1];int server_port = atoi(argv[2]);UdpClient* clt = new UdpClient(server_ip, server_port);clt->InitClient();clt->Run();return 0;
}

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

测试:

整体写完后,我们可以进行测试:
我们复制会话,让左面跑我们的客户端,右面跑我们的服务器:

在这里插入图片描述
先进行编译,然后启动我们的服务器,我们默认端口号为8080:
在这里插入图片描述
发现服务器此时已经启动起来了,接下来启动我们的客户端:
小编的Ip地址为101.42.156.77.

在这里插入图片描述
这里我们从客户端输入,服务端能正常显示。

网络测试:

其实刚才已经我们进行了网络测试,接下来我们可以将生成的客户端的可执行程序发送给你的其他朋友,进行网络级别的测试。为了保证程序在你们的机器是严格一致的,可以选择在编译客户端时携带-static选项进行静态编译。
在这里插入图片描述
此时由于客户端是静态编译的,可以看到生成的客户端的可执行程序要比服务端大得多。
在这里插入图片描述

分发客户端

此时我们可以先使用sz命令将该客户端可执行程序下载到本地机器,然后将该程序发送给你的朋友。而我们分发客户端的过程实际上就是我们在网上下载各种PC端软件的过程,我们下软件下的实际上就是客户端的可执行程序,而与之对应的服务端就在Linux服务器上部署着。
在这里插入图片描述
当你的朋友收到这个客户端的可执行程序后,可以通过rz命令或拖拽的方式将这个可执行程序上传到他的云服务器上,然后通过chmod命令给该文件加上可执行权限。
在这里插入图片描述
点击rz回车将我们的客户端添加进去,此时我们的udpclient是默认没有可执行权限的,因此我们可以按照一下命令:

chomd +x udpclient

在这里插入图片描述

我们加上可执行权限:
在这里插入图片描述

进行网络测试

此时你先把你的服务器启动起来,然后你的朋友将你的IP地址和端口号作为命令行参数运行客户端,就可以访问你的服务器了。
在这里插入图片描述

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

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

相关文章

力扣每日一题-统计已测试设备-2024.5.10

力扣题目&#xff1a;统计已测试设备 题目链接: 2960.统计已测试设备 题目描述 代码思路 根据题目内容&#xff0c;第一感是根据题目模拟整个过程&#xff0c;在每一步中修改所有设备的电量百分比。但稍加思索&#xff0c;发现可以利用已测试设备的数量作为需要减少的设备电…

前端组件库图片上传时候做自定义裁剪操作

不论是vue还是react项目&#xff0c;我们在使用antd组件库做上传图片的时候&#xff0c;有一个上传图片裁剪的功能&#xff0c;但是这个功能默认是只支持1:1的裁剪操作&#xff0c;如何做到自定义的裁剪操作&#xff1f;比如显示宽高比&#xff1f;是否可以缩放和旋转操作&…

Leetcode—239. 滑动窗口最大值【困难】

2024每日刷题&#xff08;132&#xff09; Leetcode—239. 滑动窗口最大值 算法思想 用vector会超时的&#xff0c;用deque最好&#xff01; 实现代码 class Solution { public:vector<int> maxSlidingWindow(vector<int>& nums, int k) {deque<int> …

学习笔记:【QC】Android Q qmi扩展nvReadItem/nvWriteItem

一、qmi初始化 流程图 初始化流程: 1、主入口&#xff1a; vendor/qcom/proprietary/qcril-hal/qcrild/qcrild/rild.c int main(int argc, char **argv) { const RIL_RadioFunctions *(*rilInit)(const struct RIL_Env *, int, char **); rilInit RIL_Init; funcs rilInit…

react18【系列实用教程】JSX (2024最新版)

为什么要用 JSX&#xff1f; JSX 给 HTML 赋予了 JS 的编程能力 JSX 的本质 JSX 是 JavaScript 的语法扩展&#xff0c;浏览器本身不能识别&#xff0c;需要通过解析工具&#xff08;如babel&#xff09;解析之后才能在浏览器中运行。 bable 官网可以查看解析过程 JSX 的语法 …

【Java orm 框架比较】十 新增hammer_sql_db 框架对比

迁移到&#xff08;https://gitee.com/wujiawei1207537021/spring-orm-integration-compare&#xff09; orm框架使用性能比较 比较mybatis-plus、lazy、sqltoy、mybatis-flex、easy-query、mybatis-mp、jpa、dbvisitor、beetlsql、dream_orm、wood、hammer_sql_db 操作数据 …

【MySQL】SQL基本知识点DDL(1)

目录 1.SQL分类&#xff1a; 2.DDL-数据库操作 3.DDL-表操作-创建 4.DDL-表操作-查询 5.DDL-表操作-数据类型 6.DDL-表操作-修改 1.SQL分类&#xff1a; 2.DDL-数据库操作 3.DDL-表操作-创建 注意&#xff1a;里面的符号全部要切换为英文状态 4.DDL-表操作-查询 5.DDL…

Django开发实战之定制管理后台界面及知识梳理(中)

上一篇文章末尾讲到如何能够展示更多的字段在界面上&#xff0c;那么针对整个界面数据&#xff0c;如果我想按照某一个条件进行筛选&#xff0c;我该怎么做呢&#xff0c;只需要加上下面一行代码 注意&#xff1a;中途只有代码片段&#xff0c;文末有今天涉及的所有代码 1、增…

【Python可视化】pyecharts

Echarts 是一个由百度开源的数据可视化&#xff0c;凭借着良好的交互性&#xff0c;精巧的图表设计&#xff0c;得到了众多开发者的认可。而 Python 是一门富有表达力的语言&#xff0c;很适合用于数据处理。当数据分析遇上数据可视化时&#xff0c;pyecharts 诞生了。 需要安…

Lazada、Shopee测评自养号,快速出单技巧全解析!

每个人都憧憬着自己的店铺能够拥有一款或多款引人注目的热销商品&#xff0c;这些商品不仅能为店铺带来可观的收益&#xff0c;更重要的是它们能够成为吸引顾客的强大磁石&#xff0c;显著提升店铺的整体流量。一旦这样的爆款商品成功吸引顾客&#xff0c;其他产品也将随之受到…

PHP 框架安全:ThinkPHP 序列 漏洞测试.

什么是 ThinkPHP 框架. ThinkPHP 是一个流行的国内 PHP 框架&#xff0c;它提供了一套完整的安全措施来帮助开发者构建安全可靠的 web 应用程序。ThinkPHP 本身不断更新和改进&#xff0c;以应对新的安全威胁和漏洞。 目录&#xff1a; 什么是 ThinkPHP 框架. ThinkPHP 框架…

ASP.NET学生成绩管理系统

摘要 本系统依据开发要求主要应用于教育系统&#xff0c;完成对日常的教育工作中学生成绩档案的数字化管理。开发本系统可使学院教职员工减轻工作压力&#xff0c;比较系统地对教务、教学上的各项服务和信息进行管理&#xff0c;同时&#xff0c;可以减少劳动力的使用&#xf…

uniapp、web网页跨站数据交互及通讯

来来来&#xff0c;说说你的创作灵感&#xff01;这就跟吃饭睡觉一样&#xff0c;饿了就找吃的&#xff0c;渴了就倒水张口灌。 最近一个多月实在是忙的没再更新日志&#xff0c;好多粉丝私信说之前的创作于他们而言非常有用&#xff01;受益菲浅&#xff0c;这里非常感谢粉丝…

阿里云ECS服务器实例挂载数据盘步骤(磁盘自动挂载.、访问挂载点)

阿里云ECS服务器实例挂载数据盘步骤 1.磁盘自动挂载 首先登录阿里云ECS服务器&#xff0c;通过 df -h 命令查看当前磁盘挂载情况 通过 fdisk -l 命令查看磁盘情况&#xff0c;可以发现有两个盘&#xff1a; 系统盘 /dev/vda: 60GB&#xff0c; 数据盘 /dev/vdb: 500GB 使用…

反了!美国假冒邮政服务钓鱼网站访问量竟然超过正规官网

美国邮政是美国主要的包裹信件投递机构之一&#xff0c;长期以来该单位都是网络钓鱼和诈骗的针对目标。对美国公民来说&#xff0c;在假期通常都会收到声称来自美国邮政的诈骗。美国邮政甚至单独建设的网页提醒消费者警惕诈骗信息&#xff1a; 专用提醒网页 Akamai 的研究人员…

linux上用Jmter进行压测

在上一篇中安装好了Jmeter环境&#xff0c;在这一篇中将主要分享如何使用jmeter在linux中进行单机压测。 1.项目部署 在这里我们先简单部署一下测试环境&#xff0c;所用到的项目环境是个jar包&#xff0c;先在linux上home目录下新建app目录&#xff0c;然后通过rz命令将项目ja…

【c++】string深度刨析以及实现

#pragma once #include<iostream> using namespace std; #include<assert.h> namespace bite {class string{public://迭代器 //像指针 底层不一定是指针 typedef char* iterator;iterator begin(){return _str;}iterator end(){return _str _size;}//const 版本…

面试集中营—JVM篇

一、JVM内存模型 线程独占&#xff1a;栈&#xff0c;本地方法栈&#xff0c;程序计数器; 线程共享&#xff1a;堆&#xff0c;方法区 虚拟机栈&#xff1a;线程私有的&#xff0c;线程执行方法是会创建一个栈阵&#xff0c;用来存储局部变量表&#xff0c;操作栈&#xff0c;…

【Vue3进阶】- Pinia

什么是Pinia Pinia 是 Vue 的专属状态管理库&#xff0c;它允许你跨组件或页面共享状态。它提供了类似于 Vuex 的功能&#xff0c;但比 Vuex 更加简单和直观。 需要在多个组件之间共享状态或数据时使用 Pinia 的 store&#xff0c;这样可以避免 props 和 eventBus 等传统方法…

LeetCode:116.填充每个节点的下一个右侧节点指针

文章目录 1.层次遍历2.使用next层序遍历3.递归方法 LeetCode&#xff1a;116.填充每个节点的下一个右侧节点指针 题目&#xff1a; 示例&#xff1a; 分析题意容易关注到只需要将每层结点连接起来&#xff0c;因此我们只需要把每层结点求出来即可&#xff0c;即使用层次遍历。 …