网络编程套接字(一) 【简单的Udp网络程序】

网络编程套接字<一>

  • 理解源端口号和目的端口号
  • PORT VS PID
  • 认识TCP协议和UDP协议
  • 网络字节序
  • socket编程接口
  • sockaddr结构
  • 简单的UDP网络程序
    • 服务端创建套接字
    • 服务端绑定
    • 运行服务器
    • 客户端创建套接字
    • 关于客户端的绑定问题
    • 启动客户端
    • 启动客户端
    • 本地测试
    • INADDR_ANY

理解源端口号和目的端口号

首先我们需要明确的是,两台主机之间通信的目的不仅仅是为了将数据发送给对端主机,而是为了访问对端主机上的某个服务。比如我们在用百度搜索引擎进行搜索时,不仅仅是想将我们的请求发送给对端服务器,而是想访问对端服务器上部署的百度相关的搜索服务。

socket通信的本质

现在通过IP地址和MAC地址已经能够将数据发送到对端主机了,但实际我们是想将数据发送给对端主机上的某个服务进程,此外,数据的发送者也不是主机,而是主机上的某个进程,比如当我们用浏览器访问数据时,实际就是浏览器进程向对端服务进程发起的请求。
在这里插入图片描述
也就是说,socket通信本质上就是两个进程之间在进行通信,只不过这里是跨网络的进程间通信。
比如逛淘宝和刷抖音的动作,实际就是手机上的淘宝进程和抖音进程在和对端服务器主机上的淘宝服务进程和抖音服务进程之间在进行通信。

因此进程间通信的方式除了管道、消息队列、信号量、共享内存等方式外,还有套接字,只不过前者是不跨网络的,而后者是跨网络的。

端口号

端口号(port)的作用实际就是标识一台主机上的一个进程。

  • 端口号是一个2字节16位的整数;
  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
  • 一个端口号只能被一个进程占用.

IP地址能够唯一标识公网内的一台主机,而端口号能够唯一标识一台主机上的一个进程,因此用IP地址+端口号就能够唯一标识网络上的某一台主机的某一个进程。

当数据在传输层进行封装时,就会添加上对应源端口号和目的端口号的信息。这时通过源IP地址+源端口号就能够在网络上唯一标识发送数据的进程,通过目的IP地址+目的端口号就能够在网络上唯一标识接收数据的进程,此时就实现了跨网络的进程间通信

注意: 因为端口号是隶属于某台主机的,所以端口号可以在两台不同的主机当中重复,但是在同一台主机上进行网络通信的进程的端口号不能重复。此外,一个进程可以绑定多个端口号,但是一个端口号不能被多个进程同时绑定。

PORT VS PID

端口号(port)的作用唯一标识一台主机上的某个进程,进程ID(PID)的作用也是唯一标识一台主机上的某个进程,那在进行网络通信时为什么不直接用PID来代替port呢?

进程ID(PID)是用来标识系统内所有进程的唯一性的,它是属于系统级的概念;而端口号(port)是用来标识需要对外进行网络数据请求的进程的唯一性的,它是属于网络的概念。

在不同的场景下可能需要不同的编号来标识某种事物的唯一性,因为这些编号更适合用于该场景。

认识TCP协议和UDP协议

网络协议栈是贯穿整个体系结构的,在应用层、操作系统层和驱动层各有一部分。当我们使用系统调用接口实现网络数据通信时,不得不面对的协议层就是传输层,而传输层最典型的两种协议就是TCP协议和UDP协议。

TCP协议

TCP协议叫做传输控制协议(Transmission Control Protocol),TCP协议是一种面向连接的、可靠的、基于字节流的传输层通信协议。

TCP协议是面向连接的,如果两台主机之间想要进行数据传输,那么必须要先建立连接,当连接建立成功后才能进行数据传输。其次,TCP协议是保证可靠的协议,数据在传输过程中如果出现了丢包、乱序等情况,TCP协议都有对应的解决方法。

UDP协议

UDP协议叫做用户数据报协议(User Datagram Protocol),UDP协议是一种无需建立连接的、不可靠的、面向数据报的传输层通信协议。

使用UDP协议进行通信时无需建立连接,如果两台主机之间想要进行数据传输,那么直接将数据发送给对端主机就行了,但这也就意味着UDP协议是不可靠的,数据在传输过程中如果出现了丢包、乱序等情况,UDP协议本身是不知道的。

既然UDP协议是不可靠的,那为什么还要有UDP协议的存在?

TCP协议是一种可靠的传输协议,使用TCP协议能够在一定程度上保证数据传输时的可靠性,而UDP协议是一种不可靠的传输协议,UDP协议的存在有什么意义?

首先,可靠是需要我们做更多的工作的,TCP协议虽然是一种可靠的传输协议,但这一定意味着TCP协议在底层需要做更多的工作,因此TCP协议底层的实现是比较复杂的,我们不能只看到TCP协议面向连接可靠这一个特点,我们也要能看到TCP协议对应的缺点。

同样的,UDP协议虽然是一种不可靠的传输协议,但这一定意味着UDP协议在底层不需要做过多的工作,因此UDP协议底层的实现一定比TCP协议要简单,UDP协议虽然不可靠,但是它能够快速的将数据发送给对方,虽然在数据在传输的过程中可能会出错。

编写网络通信代码时具体采用TCP协议还是UDP协议,完全取决于上层的应用场景。如果应用场景严格要求数据在传输过程中的可靠性,此时我们就必须采用TCP协议,如果应用场景允许数据在传输出现少量丢包,那么我们肯定优先选择UDP协议,因为UDP协议足够简单。

注意: 一些优秀的网站在设计网络通信算法时,会同时采用TCP协议和UDP协议,当网络流畅时就使用UDP协议进行数据传输,而当网速不好时就使用TCP协议进行数据传输,此时就可以动态的调整后台数据通信的算法。

网络字节序

计算机在存储数据时是有大小端的概念的:

  • 大端模式: 数据的高字节内容保存在内存的低地址处,数据的低字节内容保存在内存的高地址处。
  • 小端模式: 数据的高字节内容保存在内存的高地址处,数据的低字节内容保存在内存的低地址处。

如果编写的程序只在本地机器上运行,那么是不需要考虑大小端问题的,但如果涉及网络通信,那就必须考虑大小端的问题,否则对端主机识别出来的数据可能与发送端想要发送的数据是不一致的。

由于我们不能保证通信双方存储数据的方式是一样的,因此网络当中传输的数据必须考虑大小端问题。因此TCP/IP协议规定,网络数据流采用大端字节序,即低地址高字节。无论是大端机还是小端机,都必须按照TCP/IP协议规定的网络字节序来发送和接收数据。

  • 如果发送端是小端,需要先将数据转成大端,然后再发送到网络当中。
  • 如果发送端是大端,则可以直接进行发送。
  • 如果接收端是小端,需要先将接收到数据转成小端后再进行数据识别。
  • 如果接收端是大端,则可以直接进行数据识别。

网络字节序与主机字节序之间的转换

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络
字节序和主机字节序的转换。

#include <arpa/inet.h>uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
  • 函数名当中的h表示host,n表示network,l表示32位长整数,s表示16位短整数。
  • 例如htonl表示将32位长整数从主机字节序转换为网络字节序。
  • 如果主机是小端字节序,则这些函数将参数做相应的大小端转换然后返回。
  • 如果主机是大端字节序,则这些函数不做任何转换,将参数原封不动地返回。

socket编程接口

在这里插入图片描述

sockaddr结构

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同.

套接字不仅支持跨网络的进程间通信,还支持本地的进程间通信(域间套接字)。
在进行跨网络通信时我们需要传递的端口号和IP地址,而本地通信则不需要,因此套接字提供了sockaddr_in结构体sockaddr_un结构体,其中sockaddr_in结构体是用于跨网络通信的,而sockaddr_un结构体是用于本地通信的。

为了让套接字的网络通信和本地通信能够使用同一套函数接口,于是就出现了sockeaddr结构体,该结构体与sockaddr_in和sockaddr_un的结构都不相同,但这三个结构体头部的16个比特位都是一样的,这个字段叫做协议家族。

在这里插入图片描述

  • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址.
  • IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址, 不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
  • socket API可以都用struct sockaddr * 类型表示 , 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性 , 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;

简单的UDP网络程序

服务端创建套接字

socket函数

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

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

参数说明:

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

返回值说明:

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

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

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

socket函数是被谁调用的?

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

socket函数底层做了什么?

socket函数是被进程所调用的,而每一个进程在系统层面上都有一个进程地址空间PCB(task_struct)文件描述符表(files_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函数创建套接字,创建套接字时我们需要填入的协议家族就是AF_INET,因为我们要进行的是网络通信,而我们需要的服务类型就是SOCK_DGRAM,因为我们现在编写的UDP服务器是面向数据报的,而第三个参数之间设置为0即可。

//UdpServer.hpp
static const uint16_t defaultport = 8888;class UdpServer
{
public:void Init(){//1.创建套接字_sockfd = socket(AF_INET,SOCK_DGRAM,0);if (_sockfd < 0){ //创建套接字失败std::cerr << "socket error" << std::endl;return;}std::cout << "socket create success, sockfd: " << _sockfd << std::endl;}~UdpServer(){}private:std::string _ip; //IP地址uint16_t _port; //端口号int _sockfd; //文件描述符
};

注意: 当析构服务器时,我们可以将sockfd对应的文件进行关闭,但实际上不进行该操作也行,因为一般服务器运行后是就不会停下来的。

服务端绑定

套接字创建成功后,作为服务器来讲,就是在系统层面打开了一个文件,操作系统并不知道要将数据写入磁盘还是网卡,此文件还没有与网络关联起来。
在这里插入图片描述
所以我们来进行绑定:

bind函数

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

参数说明:

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

返回值说明:

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

struct sockaddr_in结构体

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

struct sockaddr_in属于系统级的概念,不同的平台接口设计可能会有点差别。

在这里插入图片描述
可以看到,struct sockaddr_in当中的成员如下:

  • sin_family:表示协议家族。
  • sin_port:表示端口号,是一个16位的整数。
  • sin_addr:表示IP地址,是一个32位的整数。

剩下的字段一般不做处理,当然你也可以进行初始化。

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

在这里插入图片描述

如何理解绑定?

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

增加IP地址和端口号

static const uint16_t defaultport = 8888;class UdpServer
{
public:UdpServer(const std::string& ip,uint16_t port = defaultport):_ip(ip), _port(port),_sockfd(-1){}void Init(){//1.创建套接字_sockfd = socket(AF_INET,SOCK_DGRAM,0);if (_sockfd < 0){ //创建套接字失败std::cerr << "socket error" << std::endl;return;}std::cout << "socket create success, sockfd: " << _sockfd << std::endl;}~UdpServer(){}private:std::string _ip; //IP地址uint16_t _port; //端口号int _sockfd; //文件描述符
};

服务端绑定

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

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

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

//UdpServer.hpp
static const uint16_t defaultport = 8888;class UdpServer
{
public:UdpServer(const std::string& ip,uint16_t port = defaultport):_ip(ip), _port(port),_sockfd(-1){}void Init(){//1.创建套接字_sockfd = socket(AF_INET,SOCK_DGRAM,0);if (_sockfd < 0){ //创建套接字失败std::cerr << "socket error" << std::endl;return;}std::cout << "socket create success, sockfd: " << _sockfd << std::endl;// 2. 绑定,指定网络信息struct sockaddr_in local;bzero(&local,sizeof(local)); //等同于memset;local.sin_family = AF_INET;local.sin_port = htons(_port);//端口号转网络序列local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1. 4字节IP   2.变成网络序列// 结构体填完,设置到内核中了吗??没有int n = bind(_sockfd,(struct sockaddr *)&local, sizeof(local));if(n != 0){std::cerr << "bind error" << std::endl;return;}std::cout << "bind success" << std::endl;}~UdpServer(){}private:std::string _ip; //IP地址uint16_t _port; //端口号int _sockfd; //文件描述符
};

inet_addr函数

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

in_addr_t inet_addr(const char *cp);

该函数使用起来非常简单,我们只需传入待转换的字符串IP,该函数返回的就是转换后的整数IP。

inet_ntoa函数

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

char *inet_ntoa(struct in_addr in);

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

运行服务器

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

recvfrom函数

UDP服务器读取数据的函数叫做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,同时错误码会被设置。

注意:

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

启动服务器函数

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

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

//UdpServer.hpp
static const uint16_t defaultport = 8888;class UdpServer
{
public:UdpServer(const std::string& ip,uint16_t port = defaultport):_ip(ip), _port(port),_sockfd(-1){}void Init(){//1.创建套接字_sockfd = socket(AF_INET,SOCK_DGRAM,0);if (_sockfd < 0){ //创建套接字失败std::cerr << "socket error" << std::endl;return;}std::cout << "socket create success, sockfd: " << _sockfd << std::endl;// 2. 绑定,指定网络信息struct sockaddr_in local;bzero(&local,sizeof(local)); //memset;local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1. 4字节IP   2.变成网络序列// 结构体填完,设置到内核中了吗??没有int n = bind(_sockfd,(struct sockaddr *)&local, sizeof(local));if(n != 0){std::cerr << "bind error" << std::endl;return;}std::cout << "bind success" << std::endl;}void Start(){//服务器永不退出
#define defaultsize 1024char buffer[defaultsize];for(;;){struct sockaddr_in peer;socklen_t len = sizeof(peer);ssize_t n = recvfrom(_sockfd,buffer,sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len); //recvfrom函数读取客户端数据if(n > 0){buffer[n] = '\0';int port = ntohs(peer.sin_port);std::string ip = inet_ntoa(peer.sin_addr);std::cout << ip << ":" << port << "# " << buffer << std::endl;}else{std::cerr << "recvfrom error" << std::endl;}}}~UdpServer(){}private:std::string _ip; //IP地址uint16_t _port; //端口号int _sockfd; //文件描述符
};

引入命令行参数

IP地址为127.0.0.1实际上等价于localhost表示本地主机,我们将它称之为本地环回,相当于我们一会先在本地测试一下能否正常通信,然后再进行网络通信的测试。

//Main.cc
// ./udp_server 127.0.0.1 8888
int main(int argc ,char *argv[])
{if(argc != 3){Usage(argv[0]);return 1;}std::string ip = argv[1];uint16_t port = std::stoi(argv[2]);std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(ip,port);usvr->Init();usvr->Start();return 0;
}

此时带上端口号运行程序就可以看到套接字创建成功、绑定成功,现在服务器就在等待客户端向它发送数据。
在这里插入图片描述
虽然现在客户端代码还没有编写,但是我们可以通过netstat命令来查看当前网络的状态,这里我们可以选择携带nlup选项。

netstat常用选项说明:

  • -n:直接使用IP地址,而不通过域名服务器。
  • -l:显示监控中的服务器的Socket。
  • -t:显示TCP传输协议的连线状况。
  • -u:显示UDP传输协议的连线状况。
  • -p:显示正在使用Socket的程序识别码和程序名称。

此时你就能查看到对应网络相关的信息,在这些信息中程序名称为./udp_server的那一行显示的就是我们运行的UDP服务器的网络信息。

在这里插入图片描述

客户端创建套接字

客户端可能有很多,我们就不封装成类了,客户端也同样需要socket

//UdpClient.cc
// ./udp_client server_ip server_port
int main(int argc, char *argv[])
{if(argc != 3){Usage(argv[0]);return 1;}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);//1.创建socketint sock = socket(AF_INET,SOCK_DGRAM,0);if(sock < 0){std::cerr << "socket error: " << strerror(errno) << std::endl;return 2;}std::cout << "create socket success: " << sock << std::endl;// 2. client要不要进行bind? 一定要bind的!!但是,不需要显示bind,client会在首次发送数据的时候会自动进行bind// 为什么?server端的端口号,一定是众所周知,不可改变的,client 需要 port,bind随机端口.// 为什么?client会非常多.// client 需要bind,但是不需要显示bind,让本地OS自动随机bind,选择随机端口号close(sock);return 0;
}

关于客户端的绑定问题

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

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

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

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

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

启动客户端

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,同时错误码会被设置。

注意:

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

启动客户端

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

// ./udp_client server_ip server_port
int main(int argc, char *argv[])
{if(argc != 3){Usage(argv[0]);return 1;}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);//1.创建socketint sock = socket(AF_INET,SOCK_DGRAM,0);if(sock < 0){std::cerr << "socket error: " << strerror(errno) << std::endl;return 2;}std::cout << "create socket success: " << sock << std::endl;// 2. client要不要进行bind? 一定要bind的!!但是,不需要显示bind,client会在首次发送数据的时候会自动进行bind// 为什么?server端的端口号,一定是众所周知,不可改变的,client 需要 port,bind随机端口.// 为什么?client会非常多.// client 需要bind,但是不需要显示bind,让本地OS自动随机bind,选择随机端口号// 2.1 填充一下server信息struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);server.sin_addr.s_addr = inet_addr(serverip.c_str());while (true){// 我们要发的数据std::string inbuffer;std::cout << "Please Enter# ";getline(std::cin,inbuffer);// 我们要发给谁呀?serverssize_t n = sendto(sock, inbuffer.c_str(), inbuffer.size(), 0, (struct sockaddr*)&server, sizeof(server));if(n < 0){break;}}close(sock);return 0;
}

本地测试

现在我们运行服务器时指明端口号为8088,再运行客户端,此时客户端要访问的服务器的IP地址就是本地环回127.0.0.1,服务端的端口号就是8088。

客户端运行之后提示我们进行输入,当我们在客户端输入数据后,客户端将数据发送给服务端,此时服务端再将收到的数据打印输出,这时我们在服务端的窗口也看到我们输入的内容。
在这里插入图片描述
此时我们再用netstat命令查看网络信息,可以看到服务端的端口是8088,客户端的端口是38567。这里客户端能被netstat命令查看到,说明客户端也已经动态绑定成功了,这就是我们所谓的网络通信。

在这里插入图片描述

INADDR_ANY

对于网络测试,如果需要让外网访问,此时我们需要bind 0。系统当当中提供的一个INADDR_ANY,这是一个宏值,它对应的值就是0。

绑定INADDR_ANY的好处

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

在这里插入图片描述

更改代码

如果想要让外网访问我们的服务,我们这里就需要将服务器类当中IP地址相关的代码去掉,而在填充网络相关信息设置struct sockaddr_in结构体时,将设置的IP地址改为INADDR_ANY就行了。由于INADDR_ANY的值本质就是0,不存在大小端的问题,因此在设置时可以不进行网络字节序的转换。

//UdpServer.hpp
class UdpServer
{
public:UdpServer(uint16_t port = defaultport): _port(port),_sockfd(-1){}void Init(){// 2. 绑定,指定网络信息struct sockaddr_in local;bzero(&local,sizeof(local)); //memset;local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr =INADDR_ANY;  // 0// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1. 4字节IP 2. 变成网络序列}
private://std::string _ip; //IP地址uint16_t _port; //端口号int _sockfd; //文件描述符
};
//Main.cc
// ./udp_server  8888
int main(int argc ,char *argv[])
{if(argc != 2){Usage(argv[0]);return 1;}uint16_t port = std::stoi(argv[1]);std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port);usvr->Init();usvr->Start();return 0;
}

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

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

相关文章

Nginx解决跨域问题

系列文章目录 文章目录 系列文章目录前言 前言 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站&#xff0c;这篇文章男女通用&#xff0c;看懂了就去分享给你的码吧。 W3C标准&#xff1…

Flutter实战记录-协作开发遇到的问题

一.前言 Android项目使用了混合架构&#xff0c;部分模块使用Flutter进行开发。在电脑A上开发的项目提交到git仓库&#xff0c;电脑B拉取后进行操作&#xff0c;遇到两个问题&#xff0c;特此做一下记录&#xff1b; 二.问题A Settings file ‘D:\xxx\settings.gradle’ line…

游戏专用设备指纹方案解析

如同人类拥有独一无二的指纹&#xff0c;设备也有设备的指纹&#xff0c;我们可以把设备指纹理解为设备的唯一识别码。 构建设备指纹需要采集设备硬件信息、软件信息、环境信息、网络信息等维度信息&#xff0c;进行加密/压缩&#xff0c;再通过算法处理&#xff0c;赋予设备唯…

【数据结构】栈和队列专题

前言 上篇博客我们讨论了栈和队列的有关结构&#xff0c;本篇博客我们继续来讨论有关栈和队列习题 这些题算是经典了 &#x1f493; 个人主页&#xff1a;小张同学zkf ⏩ 文章专栏&#xff1a;数据结构 若有问题 评论区见&#x1f4dd; &#x1f389;欢迎大家点赞&#x1f44d…

【c++】全面理解C++多态:虚函数表深度剖析与实践应用

&#x1f525;个人主页&#xff1a;Quitecoder &#x1f525;专栏&#xff1a;c笔记仓 朋友们大家好&#xff0c;通过本篇文章&#xff0c;来详细理解多态的内容 目录 1.多态的定义及实现1.1多态的构成条件1.2虚函数的重写1.3 C11 override 和 final1.4重载、覆盖(重写)、隐藏…

什么品牌洗地机最好?怎么选?2024家用洗地机推荐攻略

随着科技的不断发展&#xff0c;家用洗地机已经成为人们家庭清洁任务重非常重要的辅助工具。家用洗地机集吸尘、扫地、拖地等功能于一体&#xff0c;通过高速旋转的滚刷和强力的吸力&#xff0c;将地面上的污渍、细菌和毛发等吸入污水箱&#xff0c;从而达到清洁地面的目的。但…

网络库-POCO介绍

1.简介 POCO C Libraries 提供一套 C 的类库用以开发基于网络的可移植的应用程序&#xff0c;它提供了许多模块&#xff0c;包括网络编程、文件系统访问、线程和并发、数据库访问、XML处理、配置管理、日志记录等功能。Poco库的设计目标是易于使用、高度可定制和可扩展。 包含…

java项目之英语知识应用网站源码(springboot+vue+mysql)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的英语知识应用网站。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 项目简介&#xff1a; 英语知识应用网站的主要…

React 第三十章 React 和 Vue 描述页面的区别

面试题&#xff1a;React 和 Vue 是如何描述 UI 界面的&#xff1f;有一些什么样的区别&#xff1f; 标准且浅显的回答&#xff1a; React 中使用的是 JSX&#xff0c;Vue 中使用的是模板来描述界面 前端领域经过长期的发展&#xff0c;目前有两种主流的描述 UI 的方案&#xf…

node和npm版本太高导致项目无法正常安装依赖以及正常运行的解决办法:如何使用nvm对node和npm版本进行切换和管理

1&#xff0c;点击下载 nvm 并且安装 进入nvm的github&#xff1a; GitHub - coreybutler/nvm-windows: A node.js version management utility for Windows. Ironically written in Go. 这里下载发行版&#xff0c;Releases coreybutler/nvm-windows GitHub 找到 这个 nv…

huggingface:利用git克隆目标资源

前言 因为有很多模型资源都被放在了huggingface上&#xff0c;为了下载它们&#xff0c;着实让一个不懂git的人犯了难&#xff0c;绕了很多远路&#xff0c;甚至将不需要解决的问题也都拿上了台面&#xff0c;因此我将在本篇博客中记载一些关于【huggingface】中利用git克隆目标…

【MIT6.S081】Lab7: Multithreading(详细解答版)

实验内容网址:https://xv6.dgs.zone/labs/requirements/lab7.html 本实验的代码分支:https://gitee.com/dragonlalala/xv6-labs-2020/tree/thread2/ Uthread: switching between threads 关键点:线程切换、swtch 思路: 本实验完成的任务为用户级线程系统设计上下文切换机制…

C++笔试强训day22

目录 1.添加字符 2.数组变换 3.装箱问题 常规一维优化&#xff1a; 1.添加字符 链接 因为lenA < lenB < 50&#xff0c;因此可以无脑暴力解题&#xff1a; 遍历所有符合条件的匹配方法&#xff0c;找出最小的不同的数量&#xff0c;即最大的相同的数量 #include &…

棒材直线度测量仪 专为圆形产品研发设计 在线无损检测

棒材直线度测量仪采用了先进的技术&#xff0c;能够实现在线无损检测&#xff0c;为生产过程提供了极大的便利。专为圆形产品设计&#xff0c;它能够精确测量棒材的米直线度及外径、椭圆度尺寸&#xff0c;为质量控制提供可靠的数据支持。 在线直线度测量仪不仅具有出色的性能…

MySQL的msi格式安装

一、下载链接 MySQL :: Download MySQL Installer (Archived Versions) 二、安装步骤 ①选择自定义安装 ②选择要安装的产品 ③安装依赖环境 ④安装 ⑤点击下一步 ⑥配置 ⑦设置密码 ⑧命名 ⑨数据存放路径 ⑩安装配置 ①①配置环境变量 ①②验证 方法一&#xff1a; 方法二…

Docker需要代理下载镜像

systemctl status docker查看docker的状态和配置文件是/usr/lib/systemd/system/docker.service vi /usr/lib/systemd/system/docker.service&#xff0c; 增加如下配置项 [Service] Environment"HTTP_PROXYhttp://proxy.example.com:8080" "HTTPS_PROXYhttp:…

金融业开源软件应用 评估规范

金融业开源软件应用 评估规范 1 范围 本文件规定了金融机构在应用开源软件时的评估要求&#xff0c;对开源软件的引入、维护和退出提出了实现 要求、评估方法和判定准则。 本文件适用于金融机构对应用的开源软件进行评估。 2 规范性引用文件 下列文件中的内容通过文中的规范…

AI 图像生成-环境配置

一、python环境安装 Windows安装Python&#xff08;图解&#xff09; 二、CUDA安装 CUDA安装教程&#xff08;超详细&#xff09;-CSDN博客 三、Git安装 git安装教程&#xff08;详细版本&#xff09;-CSDN博客 四、启动器安装 这里安装的是秋叶aaaki的安装包 【AI绘画…

想要安装Word、Excel、PowerPoint,但却找不到对应软件?

前言 前几天有小伙伴在找Word和Excel软件&#xff0c;但找了半天都没发现怎么安装。 这件事情其实很简单&#xff0c;那就是Word、Excel并不是单独的一个个软件&#xff0c;而是集成在MS Office套件里的。 咱们大部分人常用的办公软件大概是Word、Excel和PowerPoint这三个。还…

Vue3组件库开发项目实战——03封装Button组件/输出vitePress文档

Vue3组件库开发项目实战——01组件开发必备知识导学-CSDN博客 Vue3组件库开发项目实战——02项目搭建&#xff08;配置Eslint/Prettier/Sass/Tailwind CSS/VitePress/Vitest&#xff09;-CSDN博客 在前面两篇博客中&#xff0c;我分别介绍了组件库开发必学知识&#xff0c;以及…