网络入门---网络编程初步认识和实践(使用udp协议)

目录标题

  • 前言
  • 准备工作
  • udpserver.hpp
    • 成员变量
    • 构造函数
    • 初始化函数(socket,bind)
    • start函数(recvfrom)
  • udpServer.cc
  • udpClient.hpp
    • 构造函数
    • 初始化函数
    • run函数(sendto)
  • udpClient.cc
  • 测试

前言

在上一篇文章中我们初步的认识了端口号的作用,ip地址和MAC地址在网络通信时起到的作用,以及网络套接字的基本了解,那么这篇文章我们就对网络编程常见的接口进行讲解,并且使用这些接口实现一个简单udp通信功能,本篇文章采用边讲边实现的方式来带着大家理解这些函数,在目录里面我添加实现该功能时哪些函数会被讲解,大家可以看一下。

准备工作

我们下面要实现一个简单的通信功能,该功能分为客服端和服务端,客户端向服务端发送数据,服务端将发送过来的数据打印到屏幕上即可:
在这里插入图片描述
因为客户端和服务端要同时运行所以这里就创建两个含有main函数的文件udpclient.cc udpserver.cc
这里采用面向对象的方式进行实现,所以这里就创建两个类udpClient udpServer来分别描述这里的发送消息和接收消息的行为,并分别将两个类放到文件udpClient.hpp udpServer.hpp里面,最后再创建一个makefile文件:
在这里插入图片描述
我们要给每个类都添加对应的构造函数,析构函数,初始化函数,运行函数(开始执行功能的函数),所以要实现的框架就是下面这样:

//udpServer.hpp
class udpServer
{
public:
    udpServer()//构造函数
    {}
    void initServer()//初始化函数
    {}
    void start()//运行函数
    {}
    ~udpServer()//析构函数
    {}
private: 
};
//udpClient.hpp
class udpClient
{
  public:
  udpClient()
  {}
  void initClient()
  {}
  void run()
  {}
  ~udpClient()
  {}
  private:  
};

那么接下来的工作就是先实现udpserver类的具体内容。

udpserver.hpp

成员变量

udpserver用来描述服务端进程的类,所以他得有自己对应的ip地址和端口号,那么在类中就创建一个string类型的对象用来存储ip地址和一个16位的无符号整数来存储端口号:

class udpServer
{
public:
    udpServer(){}
    void initServer(){}
    void start(){}
    ~udpServer(){}
private:
    string  _ip;//存储ip地址
    uint16_t _port;//存储端口号
};

构造函数

构造函数就负责对这两个类内成员变量进行初始化,因为有两个变量所以构造函数就有两个参数,因为在函数里面不会对参数进行修改所以参数的类型就得是const &类型,那么这里的代码如下:

 udpServer(const uint16_t& port, const string & ip)//构造函数
    //构造函数就负责获取ip和端口号
    :_ip(ip)
    ,_port(port)
    {}

大家在后面学习的时候会发现ip地址是可以通过某种方式被设置的,所以这里我们就先创建一个全局变量defaultip将其内容初始化为全0,然后将缺省参数设置为defaultip,那么这里代码如下:

static const string defaultIp="0.0.0.0";
class udpServer
{
public:
    udpServer(const uint16_t& port, const string & ip=defaultIp)//构造函数
    //构造函数就负责获取ip和端口号
    :_ip(ip)
    ,_port(port)
    {}
    void initServer(){}
    void start(){}
    ~udpServer(){}
private:
    string  _ip;//存储ip地址
    uint16_t _port;//存储端口号
};

初始化函数(socket,bind)

当前是使用udp协议来进行通信,所以在通信之前就得使用套接字socket来创建一套网络通信的文件机制,也就相当于在底层创建了一个网卡文件,然后将该文件与网卡设备关联起来,我们来看看这个函数的参数:
在这里插入图片描述
第一个参数是标记位表示当前你想进行的是网络通信还是本地通信,该参数的标记位有很多:
在这里插入图片描述
但是实际上经常使用的就只有前两个:AF_UNIX(本地通信)和AF_INET(网络通信)也就是上一篇文章中的这张图片图片:
在这里插入图片描述
第二个参数也是标记位表示当前套接字提供服务的类型也就是socket提供的能力类型,该标记位有下面这些:
在这里插入图片描述
比如说SOCK_STREAM就表示的是流式套接字,说人话就是该标记位在底层开了一套TCP策略来进行通信,SOCK_DGRAM表示的就是用户数据报套接字说人话就是在底层开了一套UDP策略来进行通信,SOCK_RAW表示的就是原始套接字等等等,所以下面我们在使用socket的时候第一个传输就传AF_INET,第二个参数就传递SOCK_DGRAM,第三个参数表示你想使用TCP_PROTOCOL还是UDP_PROTOCOL,但是前两个参数的确定就已经确定socket套接字所提供的功能,所以第三个参数就显得有点画蛇添足,所以在传递第三个参数的时候直接传递0即可,那么这就是socket参数的意义,socket函数执行完之后就会返回一个int类型的数据,该数据就是一个文件描述符也就是之前说的在底层创建一个和网卡相关联的文件的文件描述符,通过这个描述符就可以接受和发送消息, 如果创建套接字失败该函数就会返回-1,所以对网络的操作就和相当于之前对文件的操作,对网络的读写相当于对文件的读写那么,所以在初始化函数里面我们就可以创建一个变量来接收socket函数的返回值,然后对该值进行判断如果等于-1,我们就使用errno打印错误码和错误码对应的原因,并使用exit函数结束该进程,这里为了方便查看退出的原因就可以使用枚举来进行替换,那么这里的代码如下:

static const string defaultIp="0.0.0.0";
enum {USAGE_ERR = 1, SOCKET_ERR, BIND_ERR};
class udpServer
{
public:
    udpServer(const uint16_t& port, const string & ip=defaultIp)//构造函数
    //构造函数就负责获取ip和端口号
    :_ip(ip)
    ,_port(port)
    {}
    void initServer()//初始化函数
	//初始化函数里面就创建对应的端口号,然后对端口号进行bind
	{
	   _sockfd=socket(AF_INET,SOCK_DGRAM,0);
	   if(_sockfd==-1)
	   {
	       //运行到这里说明创建端口失败
	       cout<<"socket error:"<< errno<<strerror(errno)<<endl;
	       exit(SOCKET_ERR);
	   }
	   cout<<"socket success"<<" : "<<_sockfd<<endl;
	}
    void start(){}
    ~udpServer(){}
private:
    string  _ip;//存储ip地址
    uint16_t _port;//存储端口号
};

套接字创建完成之后这里就存在一个问题,我们上篇文章说过套接字是要有自己的ip地址和端口的,那这个套接字知道自己所要服务的ip地址和端口吗?答案是不知道的,所以我们接下来就要将端口和ip地址绑定到这个套接字上面,那么这里就要用到函数bind
在这里插入图片描述
该函数的第一个参数表示要绑定哪个端口号也就是之前socket函数的返回值,第二个参数是一个sockaddr的结构体类型指针,在上一篇文章中我们说过因为套接字的类型有很多种并且当时创建接口的时候void*这种类型还没有创建出来,所以这里就得创建了一个新的结构体类型以掩盖底层套接字不同,这个结构体就是sockaddr,有了这个类型之后就可以用一个接口来服务多个套接字,该结构的构成如下:
在这里插入图片描述
虽然传递参数得是sockaddr的指针类型,但是实际在填写的内容的时候依然得按照sockaddr_in的类型来进行填写,该结构的构成如下:
在这里插入图片描述
最上面表示你想要通信的类型然后就是你要绑定的端口号和ip地址,所以我们得先创建一个结构体对象然后再以取地址加强制类型转换的形式进行参数传递,该函数的第三个参数是一个socklen_t的类型实际上就是一个整形用来表示你传递的结构体长度,因为每个套接字结构体的长度都不一样所以将长度传递给他之后他便知道了你之前填写的是哪种类型,他在函数内部再将第二个参数的类型转换回来,那么这就是bind函数的参数接收,该函数绑定成功之后就会返回0如果返回失败就返回-1,所以我们就可以根据该函数的返回值来进行判断是否成功,就接下来我们就先完成sockaddr_in结构体的填写,首先该结构体的内容如下:

struct sockaddr_in
{
    _SOCKADDR_COMMON(sin_);
    in_port_t sin_port;
    struct in_addr sin_addr;
    //....
};

_SOCKADDR_COMMON(sin_);是一个宏,该宏的定义如下:

#define  _SOCKADDR_COMMON(sa_prefix) \
sa_family_t sa_prefix##family

这里的##就是用来形成新符号的,所以这里传递过来一个sin_最终这个参数就会形成sin_family,所以这个宏就会替换成为下面这样:

_SOCKADDR_COMMON(sin_);
sa_family_t sin_family;

而这个sa_family_t类型就是之前说的协议家族也就是AF_INET,AF_UNIX等等等,结构体的第二个参数就是对应的端口号虽然是in_port_t类型但是本质上还是一个16位的无符号整数

typedef uint16_t in_Port_t;

第三个参数虽然是一个结构体但是in_addr结构体内部就只有一个in_addr_t类型的变量:

typedef uint32_t in_addr_t;
struct in_addr
{
	in_addr_t s_addr;
};

所以通过上面的代码我们可以看到在操作系统中是用一个32位的整数来存储ip地址的,但是我们在类中却是使用string类型来存储,那为什么要这样呢?原因很简单采用点分十进制的字符串可读性特别的好符合我们人类的直觉,但是这种形式即需要转换又需要特别大的空间并且网络中的空间寸土寸金,所以在操作系统中就采用32位的无符号整数来进行存储,在用户层面中就是使用string类型来进行存储,既然两种类型完全不一样所以在赋值之前必须得做一些转换,那么这里的转换就不需要我们自己来转,操作系统中有对应的函数来实现,那么有了这些基础我们就可以继续完成初始化函数的实现,首先创建一个sockadd_in函数的对象,然后将其内容全部都初始化为0,那么这里我们可以使用bzero函数:
在这里插入图片描述
该函数就是将指定位置的值往后n个大小的空间全部都初始化为0,初始化完之后就将结构体内部的sin_family成员初始化为AF_INET,然后再将端口号填入到结构体的sin_port对象里面,这里大家要注意的一点就是在发送消息的时候也会将自己的端口号和ip地址发送过去,因为端口号的大小是两个字节所以在绑定的时候得将其从主机序列转换成为网络序列,那么这里就可以使用htons函数来进行转换:
在这里插入图片描述
然后再填写ip地址,因为ip地址在用户层和操作系统层存储的形式不一样所以这里就得做一些转换,那么这里就可以使用函数inet_addr来将其转换成为网络序列并且该函数在转换的时候还会对其大小端也进行转换,将主机端转化为网络端:
在这里插入图片描述
将内容填完之后便可以使用bind函数将该结构体和套接字绑定起来,并创建一个变量用来记录返回值以判断绑定是否成功,那么该函数完整的代码如下:

void initServer()//初始化函数
//初始化函数里面就创建对应的端口号,然后对端口号进行bind
{
    _sockfd=socket(AF_INET,SOCK_DGRAM,0);
    if(_sockfd==-1)
    {
        //运行到这里说明创建端口失败
        cout<<"socket error:"<< errno<<strerror(errno)<<endl;
        exit(SOCKET_ERR);
    }
    cout<<"socket success"<<" : "<<_sockfd<<endl;
    struct sockaddr_in local;
    bzero(&local,sizeof(sockaddr_in));
    local.sin_family=AF_INET;
    local.sin_port=htons(_port);
    local.sin_addr.s_addr=inet_addr(_ip.c_str());
    int res=bind(_sockfd,(struct sockaddr*)&local,sizeof(local));
    if(res==-1)
    {
        cout<<"bind error: "<<errno<< strerror(errno)<<endl;
        exit(BIND_ERR);
    }   
}

start函数(recvfrom)

服务器的本质就是死循环吧,所以start函数是一个死循环,在循环里面不停的接收别人发送过来的消息:

void start()//运行函数
{
   for(;;)
   {}
}

那么用来接收消息的函数就是recvfrom,该函数的参数如下:
在这里插入图片描述
第一个参数表示从哪个套接字中读取消息,第二个参数表示将读取的数据放到哪个缓冲区中,第三个参数就是一次性读取多少数据 ,第四个参数就表示以什么样的方式来进行读取这里我们就默认为0表示阻塞式的读取,因为在接收消息的时候我们得知道是谁将消息发送了过来,所以第五个参数是一个输出性参数该函数会将发送方的ip地址和端口号全部都填入第五个参数指向的结构体对象里面,第六个参数就表示传过来指针指向的对象的大小,该函数调用结束之后就会返回读取字符的个数,那么这就是该函数的使用形式用了这个函数我们就可以继续完善start函数,首先在for循环的外面创建一个缓冲区用来接收数据,在for循环里面首先创建一个sockaddr_in对象,然后调用recvfrom函数进行接收消息,接收完了之后就可以根据返回值进行判断如果返回值大于0我们就可以就读取的数据进行打印:

void start()//运行函数
{
    char buffer[gnum]={0};
    for(;;)
    {
        struct sockaddr_in peer; 
        socklen_t len = sizeof(peer); //必填
        ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
        if(s>0)
        {}
    }
}

打印数据的时候我们还要标注是那个ip地址和端口号发送过来的,所以在打印数据之前我们还得获取ip地址和端口号,那么这里获取就是从输出型参数sockaddr_in中进行获取,因为是从网络发到主机来的所以在获取端口号的时候得先进行转换所以这里得用到ntohs函数将大端数据转换成为主机端,因为网络中的ip地址和存储的形式不太一样所以在获取的时候得用到inet_ntoa函数来进行转换将网络形式转换成为客户形式并将大端数据转换成为主机端数据,那么该函数完整的代码如下:

void start()//运行函数
{
    char buffer[gnum]={0};
    for(;;)
    {
        struct sockaddr_in peer; 
        socklen_t len = sizeof(peer); //必填
        ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
        if(s>0)
        {
            buffer[s]=0;
            string clientip=inet_ntoa(peer.sin_addr);
            uint16_t clientport = ntohs(peer.sin_port);
            cout << clientip <<"[" << clientport << "]# " << buffer << endl; 
        }
    }
}

udpServer.cc

该文件装的就是main函数,在main函数里面就创建一个udpServer对象然后调用初始化函数和start函数执行任务,但是这里存在一个问题udpServer对象的构造函数需要传递ip地址和端口号那从哪来获取这两个东西呢?答案是在运行可执行程序的时候传递这两个参数就好比这样:./udpServer.cc ip地址 端口号,所以就得添加main函数的两个参数:

int main(int argc,char* argv[])
{
    unique_ptr<udpServer> usr();
    usr->initServer();
    usr->start();
    return 0;
}

但是这里存在一种情况就是使用者传递多了或者少了参数,那么这个时候就会出现问题,所以在执行之前我们得判断一下参数的个数,如果参数传递不对我们就执行一个函数用来告诉其正确的形式然后直接退出,然后将argv的第二个元素转化成为端口号,第三个元素赋值给一个string对象,那么这里的代码如下:

static void Usage(string proc)
{
    cout << "\nUsage:\n\t" << proc << "local_ip local_port\n\n";
}

int main(int argc,char* argv[])
{
    if(argc!=3)
    {
        Usage(argv[0]);
        exit(1);
    }
    uint16_t port = atoi(argv[1]);
    string ip=argv[1];
    unique_ptr<udpServer> usr(new udpServer(port,ip));
    usr->initServer();
    usr->start();
    return 0;
}

那么这里我们可以单纯的运行一下不传递任何的ip地址和端口号:
在这里插入图片描述
可以看到这里直接退出并显示了正确的执行格式,然后我们可以使用ifconfig查看一下本地环回:
在这里插入图片描述
本地换回的地址就是127.0.0.1,我们网络通信的地址是分层,发送消息的时候从上到下分装的,发送到另外一台机器的时候又是从下到上进行解包分用的比如说下面的图片:
在这里插入图片描述
那么本地换回就是自己用来测试的,也就是在自己的主机上从上往下封装,然后再在自己的自己主机上从下往上进行解包分用:
在这里插入图片描述
所以本地换回就是用来进行测试的,先来自己的地址进行测试如果测试成功了再考虑其他机器的测试,那么这里我们就可以进行一下测试:

在这里插入图片描述
在这里插入图片描述
可以看到当前有个进程确实是在运行,然后使用指令netstat -naup便可以查看所有的网络进程:
在这里插入图片描述
可以到当前确实是有一个端口号为8081的进程并且该网络进程的名字就是udpServer,所以这就证明当前是可以正常的执行本地换回的,那么平时我们是使用的公网ip链接的服务器,那么这里也是否能够绑定公网ip呢?
在这里插入图片描述
答案是不可以的(有些还是可以具体情况以实践为主)因为云服务器是虚拟化的服务器·,不能直接bind你的公网ip,但是如果你有一个虚拟机或者是一个真实的linux环境的话则可以bing公网ip,虽然公网ip我们不能直接bind但是还是可以直接绑定内网ip的:
在这里插入图片描述
但是虽然局域网ip或者私有ip可以直接绑定但是依然可能会出现华为云的机器不能和阿里云腾讯云的用户进行通信,那我们该如何保证我们的机器能够被其他的机器找到呢?实际上,一款网络服务器是不建议指明一个IP的,一个服务器可能会有多个网卡所以也就可能存在多个IP的,如果只绑定了一个ip的话就可能会出现有很多的数据发到了不同的ip上这些数据都属于这台机器上的某个进程,但是却只有这一个指定的ip能够正常的收到,因为你只指明的绑定了那一个ip,所以为了让所有的机器都能够找到我们,我们在绑定ip地址的时候就是采用这样的方法:

 //local.sin_addr.s_addr=inet_addr(_ip.c_str());
 local.sin_addr.s_addr=INADDR_ANY;

而INADDR_ANY也就是一个宏他的真实定义就是一个宏:

#define INADDR_ANY ((in_addr_t)0x00000000)

也就是我们之前给的缺省参数,ip地址修改成这样之后表示的意思就是未来发给这个机器的所有数据只要是发给8080端口的就会将数据发给8080对应的进程,所以这里就不会出现发给了一个ip而漏掉其他ip的现象,这是任意地址bind也是服务器的真实写法。所以未来在执行服务端进程的时候就不需要传递ip地址直接传递端口就可以了:

#include"udpServer.hpp"
#include<memory>
static void Usage(string proc)
{
    cout << "\nUsage:\n\t" << proc << " local_port\n\n";
}

int main(int argc,char* argv[])
{
    if(argc!=2)
    {
        Usage(argv[0]);
        exit(1);
    }
    uint16_t port = atoi(argv[1]);
    unique_ptr<udpServer> usr(new udpServer(port));
    usr->initServer();
    usr->start();
    return 0;
}

udpClient.hpp

在这个文件夹里面装的是客户端所要执行的操作,这里要进行网络通信所以也得有套接字,因为通信的时候得知道服务端的端口号和ip地址所以还得有两个变量来存储这些值,那么这里的代码如下:

class udpClient
{
  public:
  udpClient(){}
  void initClient(){}
  void run(){}
  ~udpClient(){}
  private:  
  int _sockfd;
  string _serverip;
  uint16_t _serverport;
};

构造函数

构造函数需要两个参数用来初始化_serverip和_serverport的值,这里的逻辑和客户端差不多这里就不多说,直接上代码:

udpClient(const string serverip,const uint16_t port)
:_serverip(serverip)
,_serverport(port)
,_sockfd(-1)
{}

初始化函数

初始化函数也是同样的道理首先创建套接字然后对返回值进行判断看是否创建成功?

void initClient()
{
    //创建套接字
    _sockfd=socket(AF_INET,SOCK_DGRAM,0);
    if(_sockfd==-1)
    {
        cerr<<"socket error: "<<errno<<strerror(errno)<<endl;
        exit(SOCKET_ERR);
    }
    cout << "socket success: " << " : " << _sockfd << endl;
}

那接下来要对套接字进行绑定吗?答案是必须要绑定的但是这里不需要我们人为的绑定,我们把端口值的选择权交给了操作系统,那这是为什么呢?为什么服务端要人为的绑定而客户端确实操作系统来绑定呢?原因是未来客户端服务器是要明确的端口号不能随意的改变,他得让所有人都能够知道,而服务端的端口号却不需要明确他可以随意的改变他只需要保证唯一性就可以了,并且写服务器的一定是一家公司,但是客户端却可能是无数家公司,比如说抖音,今日头条,西瓜视频都属于字节跳动的所以在这个公司内部肯定有明确的规定,但是客户端在用户的手机上面,一个手机上可能会有各种各样的软件所以而且这些软件来自各种各样的公司所以一旦客户端也指定了端口号很可能就会出现两个软件争抢一个端口号的现象,所以为了避免这样的显现就把客户端的端口号选着的权利交给操作系统让操作系统来自行分配,那么这里就存在一个问题:操作系统又是什么时候来进行分配呢?这个问题我们后面再进行解答,那么这就是初始化函数的全部内容。

run函数(sendto)

同样的道理run函数也得是一个死循环,在run函数里面我们就需要向客服端发送消息,那么这里就得使用sendto函数,该函数的参数如下:
在这里插入图片描述
第一个参数就是将数据放到哪个套接字上,第二个参数就是消息现在存放在哪里,第三个参数表示消息有多少,第四个参数表示当前的属性直接默认为0也就是阻塞式发送有数据就发没数据就不发,因为发送数据的时候我们得顺便告诉服务器是谁向你发送的数据,所以第五第六个参数就是用来填写目标主机的端口号和ip地址,这个跟前面的类似就不多说了,所以run函数的实现就是先创建一个sockaddr_in对象然后填写相应的信息,再创建一个string的对象和一个循环,在循环里面就往string对象里面填入信息然后将string对象的内容通过sendto函数发送到对应的主机里面,那么run函数的完整代码如下:

void run()
{
  struct sockaddr_in server;//用来记录服务端口的信息
  server.sin_family = AF_INET;
  server.sin_addr.s_addr = inet_addr(_serverip.c_str());
  server.sin_port = htons(_serverport);
  string tmp;
  while(1)
  {
    cout << "Please Enter# ";
    cin>>tmp;
    sendto(_sockfd,tmp.c_str(),tmp.size(),0,(struct sockaddr*)&server,sizeof(server));
  }
}

udpClient.cc

该文件的实现和udpServer.cc文件的实现相差不大,唯一的区别就是这里必须得指明你往哪个机器发送,那么这里就不多说了:

#include"udpClient.hpp"
#include<memory>
static void Usage(string proc)
{
    cout << "\nUsage:\n\t" << proc << "local_ip local_port\n\n";
}
int main(int argc,char *argv[])
{
    if(argc!=3)
    {
        Usage(argv[0]);
        exit(1);
    }
    string serverip=argv[1];
    uint16_t serverport=atoi(argv[2]);
    unique_ptr<udpClient> ucil(new udpClient(serverip,serverport));
    ucil->initClient();
    ucil->run();
    return 0;
}

测试

下面就可以开始进行测试,首先运行客户端:
在这里插入图片描述
再运行服务端:

然后在服务端中输入信息就可以看到客户端中立马将信息显示了出来:
在这里插入图片描述
那么这就说明我们的代码实现的没有问题,那么完整的代码就如下,首先是文件udpServer.hpp

#include <iostream>
#include <string>
#include <strings.h>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using namespace std;
static const string defaultIp="0.0.0.0";
static const int gnum =1024;
enum {USAGE_ERR = 1, SOCKET_ERR, BIND_ERR};
class udpServer
{
public:
    udpServer(const uint16_t& port, const string & ip=defaultIp)//构造函数
    //构造函数就负责获取ip和端口号
    :_ip(ip)
    ,_port(port)
    {}
    void initServer()//初始化函数
    //初始化函数里面就创建对应的端口号,然后对端口号进行bind
    {
        _sockfd=socket(AF_INET,SOCK_DGRAM,0);
        if(_sockfd==-1)
        {
            //运行到这里说明创建端口失败
            cout<<"socket error: "<< errno<<strerror(errno)<<endl;
            exit(SOCKET_ERR);
        }
        cout<<"socket success"<<" : "<<_sockfd<<endl;
        struct sockaddr_in local;
        bzero(&local,sizeof(sockaddr_in));
        local.sin_family=AF_INET;
        local.sin_port=htons(_port);
        //local.sin_addr.s_addr=inet_addr(_ip.c_str());
        local.sin_addr.s_addr=INADDR_ANY;
        int res=bind(_sockfd,(struct sockaddr*)&local,sizeof(local));
        if(res==-1)
        {
            cout<<"bind error: "<<errno<< strerror(errno)<<endl;
            exit(BIND_ERR);
        }
        
    }
    void start()//运行函数
    {
        char buffer[gnum]={0};
        for(;;)
        {
            struct sockaddr_in peer; 
            socklen_t len = sizeof(peer); //必填
            ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
            if(s>0)
            {
                buffer[s]=0;
                string clientip=inet_ntoa(peer.sin_addr);
                uint16_t clientport = ntohs(peer.sin_port);
                cout << clientip <<"[" << clientport << "]# " << buffer << endl; 
            }
        }
    }
    ~udpServer()//析构函数
    {}

private:
    int _sockfd;
    string  _ip;
    uint16_t _port;   
};

udpServer.cc文件的内容如下:

#include"udpServer.hpp"
#include<memory>
static void Usage(string proc)
{
    cout << "\nUsage:\n\t" << proc << " local_port\n\n";
}

int main(int argc,char* argv[])
{
    if(argc!=2)
    {
        Usage(argv[0]);
        exit(1);
    }
    uint16_t port = atoi(argv[1]);
    unique_ptr<udpServer> usr(new udpServer(port));
    usr->initServer();
    usr->start();
    return 0;
}

udpClient.hpp的内容如下:

#include <iostream>
#include <string>
#include <strings.h>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using namespace std;
enum {USAGE_ERR = 1, SOCKET_ERR, BIND_ERR};
class udpClient
{
  public:
  udpClient(const string serverip,const uint16_t port)
  :_serverip(serverip)
  ,_serverport(port)
  ,_sockfd(-1)
  {}
  void initClient()
  {
      //创建套接字
      _sockfd=socket(AF_INET,SOCK_DGRAM,0);
      if(_sockfd==-1)
      {
          cerr<<"socket error: "<<errno<<strerror(errno)<<endl;
          exit(SOCKET_ERR);
      }
      //无需绑定
       cout << "socket success: " << " : " << _sockfd << endl;
  }
  void run()
  {
    struct sockaddr_in server;//用来记录服务端口的信息
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(_serverip.c_str());
    server.sin_port = htons(_serverport);
    string tmp;
    while(1)
    {
      cout << "Please Enter# ";
      cin>>tmp;
      sendto(_sockfd,tmp.c_str(),tmp.size(),0,(struct sockaddr*)&server,sizeof(server));
    }
  }
  ~udpClient()
  {}
  private:  
  int _sockfd;
  string _serverip;
  uint16_t _serverport;
};

udpClient.cc文件的内容如下:

#include"udpClient.hpp"
#include<memory>
static void Usage(string proc)
{
    cout << "\nUsage:\n\t" << proc << "local_ip local_port\n\n";
}
int main(int argc,char *argv[])
{
    if(argc!=3)
    {
        Usage(argv[0]);
        exit(1);
    }
    string serverip=argv[1];
    uint16_t serverport=atoi(argv[2]);
    unique_ptr<udpClient> ucil(new udpClient(serverip,serverport));
    ucil->initClient();
    ucil->run();
    return 0;
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【记忆】施密特正交化

图片源自https://www.zhihu.com/question/485986805

学员自创“编程羔手”,作为新手来看待攻城狮,程序猿,码农,码奴,码畜...

在编程的世界里&#xff0c;我们就是那些编程羔手&#xff0c;如同懵懂的羔羊一样&#xff0c;刚刚踏入代码的草原。我们是攻城狮的侄子&#xff0c;程序猿的徒弟&#xff0c;码农的学徒&#xff0c;尚未尝过码奴的辛酸&#xff0c;更没有领略过码畜的无奈。我们是那些初出茅庐…

idea import 不让打包时自动带*

场景 因为多人开发&#xff0c;合代码时经常发现因自动import带*了&#xff0c;导致各种问题。 解决

视觉检测系统在半导体行业的应用

一、半导体产业链概述 半导体产业链是现代电子工业的核心组成部分&#xff0c;涵盖了从原材料到最终产品的整个生产过程。这个产业链主要分为以下几个环节&#xff1a; 1.原材料供应&#xff1a;半导体行业的基石是半导体材料&#xff0c;如硅片、化合物半导体等。这些材料需要…

EP15:动态内存管理概述(c语言)malloc,calloc,realloc函数的介绍使用及柔性数组的介绍

如果学习方向是c方向那么c语言有三个板块的知识是非常重要的. 1:指针 2:结构体 3;动态内存管理. 序言:在c语言中,什么是动态内存 C语言中的动态内存是指在程序运行时&#xff0c;根据需要动态地分配内存空间的一种内存管理方式。与静态内存相比&#xff0c;动态内存的大小和生…

详细了解STM32----GPIO

提示&#xff1a;永远支持免费开源知识文档&#xff0c;喜欢的点个关注吧&#xff01;谢谢&#xff01; 文章目录 一、什么是GPIO&#xff1f;二、GPIO基本结构三、GPIO的输入输出模式1、推挽输出2、开漏输出3、复用推挽4、复用开漏1、浮空输入2、上拉输入&#xff13;、下拉输…

大模型对数据分布变化的鲁棒性研究综述

摘要&#xff1a; 标准机器学习的一个主要限制是它产生的模型对分布变化不鲁棒&#xff0c;其中训练分布与测试分布不匹配&#xff08;对于下游任务&#xff09;。现有的工作表明&#xff0c;调整在广泛的未标记数据上训练的基础模型可以提高适应模型在各种变化中的鲁棒性。这为…

代码序随想录二刷 |二叉树 | 二叉树的层序遍历II

代码序随想录二刷 &#xff5c;二叉树 &#xff5c; 二叉树的层序遍历II 题目描述解题思路代码实现 题目描述 107.二叉树的层序遍历II 给你二叉树的根节点 root &#xff0c;返回其节点值 自底向上的层序遍历 。 &#xff08;即按从叶子节点所在层到根节点所在的层&#xff0…

pta模拟题——7-34 刮刮彩票

“刮刮彩票”是一款网络游戏里面的一个小游戏。如图所示&#xff1a; 每次游戏玩家会拿到一张彩票&#xff0c;上面会有 9 个数字&#xff0c;分别为数字 1 到数字 9&#xff0c;数字各不重复&#xff0c;并以 33 的“九宫格”形式排布在彩票上。 在游戏开始时能看见一个位置上…

基于JAVA+SpringBoot+Vue的前后端分离的旅游网站

✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取项目下载方式&#x1f345; 一、项目背景介绍&#xff1a; 近年来&#xff0c;随…

【Vue】设置路由默认跳转指定页面

目录 设置路由默认跳转 上一篇&#xff1a; 登录注册界面制作 https://blog.csdn.net/m0_67930426/article/details/134895214?spm1001.2014.3001.5502 以这篇文章为例 首先我们要了解一下vue项目的router包的作用 上一篇文章里&#xff0c;创建了登录注册页面 如果我们…

14、SQL注入——HTTP文件头注入

文章目录 一、HTTP Header概述1.1 HTTP工作原理1.2 HTTP报文类型1.3 较重要的HTTP Header内容 二、HTTP Header注入2.1 HTTP Header注入的前提条件2.2 常见的HTTP Header注入类型 一、HTTP Header概述 1.1 HTTP工作原理 1.2 HTTP报文类型 &#xff08;1&#xff09;请求报文 …

InnoDB Architecture MySQL 5.7 vs 8.0

innodb-architecture-5-7 innodb-architecture-8-0 图片均来源于MySQL官网

wireshark

今天学习一下wireshark的基础命令。 如何筛选所需要的协议&#xff1a;http 剔除不需要的协议&#xff1a;&#xff01;http 过滤器&#xff0c;逻辑符号&#xff0c;多条件筛选&#xff1a;tcp && http 比较运算符号&#xff1a; 过滤IP地址&#xff1a; 我们可以…

大三上实训内容

项目一&#xff1a;爬取天气预报数据 【内容】 在中国天气网(http://www.weather.com.cn)中输入城市的名称&#xff0c;例如输入信阳&#xff0c;进入http://www.weather.com.cn/weather1d/101180601.shtml#input 的网页显示信阳的天气预报&#xff0c;其中101180601是信阳的…

连接Redis报错解决方案

连接Redis报错&解决方案 问题描述&#xff1a;Could not connect to Redis at 127.0.0.1:6379: 由于目标计算机积极拒绝&#xff0c;无法连接。 问题原因&#xff1a;redis启动方式不正确 解决方案&#xff1a; 在redis根目录下打开命令行窗口&#xff0c;输入命令redi…

合并一个文件夹下的多个txt文件,并对文本内容分列处理。

python 合并一个文件夹下的多个txt文件&#xff0c;并对文本内容分列。 原始文件&#xff1a; 最终结果&#xff1a; import pandas as pd import xlwt import pandas as pd from sqlalchemy import create_engine import pandas as pd import os import glob dirPath g…

《演说之禅》——笔记+原书电子版+禅宗的思维与原则

目录 演说之禅说什么思考以下&#xff1a;设计幻灯片步骤&#xff1a;禅宗的思维与原则 演说之禅说什么 “演说之禅”并非某种方法。方法是重要且必要&#xff0c;但世间并无万能药&#xff0c;方法的背后通常隐含着一个循序渐进的、系统化的过程&#xff0c;一个有准备的、线…

C语言趣练习:两个字符串不用strcmp函数怎么比较大小

目录 1习题一&#xff1a;两个字符串不用strcmp函数怎么比较大小&#xff0c;并输出其差值 2不用strcpy函数将s2字符串中内容复制到s1中 3译密码问题 4总结&#xff1a; 1习题一&#xff1a;两个字符串不用strcmp函数怎么比较大小&#xff0c;并输出其差值 解题思路&#x…
最新文章