【网络】网络编程套接字(一)

鼠扑  论坛元老 | 2024-7-26 13:19:54 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 1072|帖子 1072|积分 3216

网络编程套接字(一)  
  
一、预备知识

1.1端口号

上网的行为一般可以归结为两种:

  • 把远端的数据拉取到本地;
  • 把本地的数据推送到远端。
   数据拉取到本地的过程我们可以理解为输入,数据推送到远端的过程我们可以理解为输出。
  以是,上网的本质就是IO,再具体点,网络通信的本质就是进程间通信。
   进程间通信的前提是让不同的进程看到同一份公共资源,很明显这个公共资源就是网络。
  那么如何在茫茫网络中找到两个进程呢?
IP(IP地点)+port(端口号)=互联网中唯一的一个进程。
   

  • IP地点可以让我们在互联网中找到唯一的一台主机。
  • port端口号可以让我们找到这台主机上唯一的一个进程。
  端口号(port)的作用实际就是标识一台主机上的一个进程。


  • 端口号是传输层协议的内容。
  • 端口号是一个2字节16位的整数。
  • 端口号用来标识一个进程,告诉操纵体系,当前的这个数据要交给哪一个进程来处理。
  • 一个进程可以绑定多个端口号,但是一个端口号不能被多个进程同时绑定。
我们可以将port端口号与进程绑定,这样进程就可以通过端口号来唯一标识了。
   为什么不使用进程ID实现这部分功能?
  专事专办,虽然进程ID也能够唯一区分进程,但是这毕竟分属了两个领域:操纵体系和网络,你可以理解为有一部分解耦的因素,同时你也应该意识到专事专办的思想是一种正确的体系设计思维。
    底层如何通过port找到对应进程的?
  实际底层接纳哈希的方式创建了端口号和进程PID或PCB之间的映射关系,当底层拿到端口号时就可以直接执行对应的哈希算法,然后就能够找到该端口号对应的进程。
  1.2传输层的TCP协议与UDP协议

TCP协议

TCP协议叫做传输控制协议(Transmission Control Protocol),TCP协议是一种面向毗连的、可靠的、基于字节流的传输层通信协议。
TCP协议是面向毗连的,如果两台主机之间想要进行数据传输,那么必须要先创建毗连,当毗连创建成功后才华进行数据传输。其次,TCP协议是保证可靠的协议,数据在传输过程中如果出现了丢包、乱序等环境,TCP协议都有对应的办理方法。
UDP协议

UDP协议叫做用户数据报协议(User Datagram Protocol),UDP协议是一种无需创建毗连的、不可靠的、面向数据报的传输层通信协议。
使用UDP协议进行通信时无需创建毗连,如果两台主机之间想要进行数据传输,那么直接将数据发送给对端主机就行了,但这也就意味着UDP协议是不可靠的,数据在传输过程中如果出现了丢包、乱序等环境,UDP协议无法处理。
   有关TCP协媾和UDP协议的可靠性标题:
  UDP协议不可靠性并不是一种缺点,因为TCP协议对于数据传输错误等环境可以做出处理就意味着TCP协议更复杂,实现了更多的接口,而UDP协议也必定更为简单。
  以是这两种协议并欠优劣之分,只是区别于使用场景,比如TCP协议实用长途登录:SSH(安全外壳协议)和Telnet(长途登录协议)使用TCP协议来确保长途登录会话的可靠性和安全性。
  而UDP协议实用于流媒体传输:如在线视频和音频播放等应用,需要快速的数据传输和低延长,但对数据的完整性和正确性要求不高。在这些场景下,即使部分数据丢失或堕落,也不会对用户体验产生太大影响。
  1.3网络字节序

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

  • 大端模式: 数据的高字节内容生存在内存的低地点处,数据的低字节内容生存在内存的高地点处。
  • 小端模式: 数据的高字节内容生存在内存的高地点处,数据的低字节内容生存在内存的低地点处。
  如果编写的步伐只在本地机器上运行,那么是不需要考虑巨细端标题标,因为同一台机器上的数据接纳的存储方式都是一样的,要么接纳的都是大端存储模式,要么接纳的都是小端存储模式。但如果涉及网络通信,那就必须考虑巨细端的标题,否则对端主机识别出来的数据可能与发送端想要发送的数据是不同等的。
而TCP/IP协议办理这一标题标方式非常简单:规定网络数据流应接纳大端字节序,即低地点高字节。


  • 如果发送端是小端,需要先将数据转成大端,然后再发送到网络当中。
  • 如果发送端是大端,则可以直接进行发送。
  • 如果接收端是小端,需要先将接收到数据转成小端后再进行数据识别。
  • 如果接收端是大端,则可以直接进行数据识别。
   为什么网络字节序接纳的是大端?而不是小端?
  网络字节序接纳的是大端,而主机字节序一般接纳的是小端,那为什么网络字节序不接纳小端呢?如果网络字节序接纳小端的话,发送端和接收端在发生和接收数据时就不消进行巨细端的转换了。
  该标题有很多不同说法,下面枚举了两种说法:
  

  • 说法一: TCP在Unix时代就有了,从前Unix机器都是大端机,因此网络字节序也就接纳的是大端,但之后人们发现用小端能简化硬件设计,以是现在主流的都是小端机,但协议已经欠好改了。
  • 说法二: 大端序更符合现代人的读写风俗。
  为使网络步伐具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,体系提供了四个函数,可以通过调用以下库函数实现网络字节序和主机字节序之间的转换。
  1. #include <arpa/inet.h>
  2. uint32_t htonl(uint32_t hostlong);
  3. uint16_t htons(uint16_t hostshort);
  4. uint32_t ntohl(uint32_t netlong);
  5. uint16_t ntohs(uint16_t netshort);
复制代码
函数名当中的h表现host,n表现network,l表现32位长整数,s表现16位短整数。
   比方htonl表现将32位长整数从主机字节序转换为网络字节序。
  

  • 如果主机是小端字节序,则这些函数将参数做相应的巨细端转换然后返回。
  • 如果主机是大端字节序,则这些函数不做任何转换,将参数原封不动地返回。
  二、socket编程接口

2.1 socket常见API

  1. // 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
  2. int socket(int domain, int type, int protocol);
  3. // 绑定端口号 (TCP/UDP, 服务器)
  4. int bind(int socket, const struct sockaddr *address, socklen_t address_len);
  5. // 开始监听socket (TCP, 服务器)
  6. int listen(int socket, int backlog);
  7. // 接收请求 (TCP, 服务器)
  8. int accept(int socket, struct sockaddr* address, socklen_t* address_len);
  9. // 建立连接 (TCP, 客户端)
  10. int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);  
复制代码
以上接口是一层抽象的网络编程接口,实用于各种底层网络协议,如ipv4、ipv6以及后面的UNIX Domain Socket,然而,各种网络协议的地点格式并不相同。那么我们如何把不同地点格式的地点变为统一的地点格式交给以上API呢?引入了sockaddr结构,
2.2 sockaddr结构

套接字不仅支持跨网络的进程间通信,还支持本地的进程间通信(域间套接字),而很明显本地的进程间通信是不需要IP和PORT的,因此提供了sockaddr_in结构体(ipv6—sockaddr_in6)和sockaddr_un结构体,sockaddr_in用于网络通信,sockaddr_un用于本地通信。
为了统一地质结构的表现方法,于是就出现了sockeaddr结构体,它用于统一地点结构的表现方法,使得不同的地点结构可以被bind()、connect()、recvfrom()、sendto()等函数调用。
该结构体与sockaddr_in和sockaddr_un的结构都不相同,但这三个结构体头部的16个比特位都是一样的,这个字段叫做协议眷属。

此时当我们在传参时,就不消传入sockeaddr_in或sockeaddr_un这样的结构体,而统一传入sockeaddr这样的结构体从而实现了统一的API接口。
在这些API内部就可以提取sockeaddr结构头部的16位进行识别,然后执行对应的操纵。此时我们就通过通用sockaddr结构,将参数类型进行了统一。
实在这种设计模式就是早期的多态。
三、简单的UDP网络步伐

起首说明下我们的编程思绪,起首肯定需要创建一个服务端对象,并初始化这个服务端开启服务。
3.1创建UDP套接字

那么对于初始化服务端,起主要做的肯定是创建socket套接字。
  1. int socket(int domain, int type, int protocol);
复制代码
参数说明:


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


  • 套接字创建成功返回一个文件形貌符,创建失败返回-1,同时错误码会被设置。
   socket函数底层做了什么?
  当我们调用socket函数创建套接字时,实际相称于我们打开了一个“网络文件”,打开后在内核层面上就形成了一个对应的struct file结构体,同时该结构体被连入到了该进程对应的文件双链表,并将该结构体的首地点填入到了fd_array数组当中下标为3的位置,此时fd_array数组中下标为3的指针就指向了这个打开的“网络文件”,末了3号文件形貌符作为socket函数的返回值返回给了用户。
  

  此中每一个struct file结构体中包罗的就是对应打开文件各种信息,比如文件的属性信息、操纵方法以及文件缓冲区等。此中文件对应的属性在内核当中是由struct inode结构体来维护的,而文件对应的操纵方法实际就是一堆的函数指针(比如read*和write*)在内核当中就是由struct file_operations结构体(方法集)来维护的。
  而文件缓冲区对于打开的普通文件来说对应的一般是磁盘,但对于现在打开的“网络文件”来说,这里的文件缓冲区对应的就是网卡。
  

  对于一般的普通文件来说,当用户通过文件形貌符将数据写到文件缓冲区,然后再把数据刷到磁盘上就完成了数据的写入操纵.
  而对于现在socket函数打开的“网络文件”来说,当用户将数据写到文件缓冲区后,操纵体系会定期将数据刷到网卡里面,而网卡则是负责数据发送的,因此数据最终就发送到了网络当中。
  3.2服务端绑定

现在套接字已经创建好了,我们还没有将这个套接字与网络进行绑定,即通信方式、IP和PORT等等都是未知的,而这些内容都存放在sockaddr结构体中,以是我们需要利用bind函数将socket与sockaddr进行绑定,即改变网络文件当中文件操纵函数的指向,将对应的操纵函数改为对应网卡的操纵方法,此时读数据和写数据对应的操纵对象就是网卡了,以是绑定实际上就是将文件和网络关联起来。
  1. int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
复制代码
参数说明:


  • sockfd:绑定的文件的文件形貌符。也就是我们创建套接字时获取到的文件形貌符。
  • addr:网络相关的属性信息,包括协议眷属、IP地点、端口号等。
  • addrlen:传入的addr结构体的长度。
返回值说明:


  • 绑定成功返回0,绑定失败返回-1,同时错误码会被设置。
这里我们接纳的是网络通信,以是我们需要传入sockaddr_in结构体的地点,留意强转为sockaddr*类型。
   struct sockaddr_in结构体
  成员:
  

  • sin_family:表现协议眷属。
  • sin_port:表现端口号,是一个16位的整数。
  • sin_addr:表现IP地点,是一个32位的整数。
  此中sin_addr的类型是struct in_addr,实际该结构体当中就只有一个成员s_addr,该成员就是一个32位的整数,IP地点实际就是存储在这个整数当中的。
  套接字创建完毕后我们就需要进行绑定了,但在绑定之前我们需要先定义一个struct sockaddr_in结构,将对应的网络属性信息添补到该结构当中。由于该结构体当中另有部分选填字段,因此我们最好在添补之前对该结构体变量里面的内容进行清空,然后再将协议眷属、端口号、IP地点等信息添补到该结构体变量当中。
需要留意的是,在发送到网络之前需要将端口号设置为网络序列,由于端口号是16位的,因此我们需要使用前面说到的htons函数将端口号转为网络序列。别的,由于网络当中传输的是整数IP,我们需要调用inet_addr函数将字符串IP转换成整数IP,然后再将转换后的整数IP进行设置。
当网络属性信息添补完毕后,由于bind函数提供的是通用参数类型,因此在传入结构体地点时还需要将struct sockaddr_in*强转为struct sockaddr*类型后再进行传入。
  1. UdpServer(uint16_t port) : _sockfd(sockfddefault), _port(port), _ip(ip), _isrunning(false) {}
  2. void InitServer()
  3. {
  4.     // 1.创建UDP套接字
  5.     _sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 返回文件描述符
  6.     if (_sockfd < 0)
  7.     {
  8.         LOG(FATAL, "socket error,%s,%d", strerror(errno), errno);
  9.         exit(SOCKET_ERROR);
  10.     }
  11.     LOG(INFO, "socket create success,sockfd:%d", _sockfd);
  12.     // 2.0填充sockaddr_in结构
  13.     struct sockaddr_in local;      // struct sockaddr_in 系统提供的数据类型。local是变量,用户栈上开辟空间。
  14.     bzero(&local, sizeof(local));  // 将从&local开始的sizeof(local)大小的内存区域置零
  15.     local.sin_family = AF_INET;    // 设置网络通信方式
  16.     local.sin_port = htons(_port); // port要经过网络传输给对面,所有需要从主机序列转换为网络序列
  17.     // a. 字符串风格的点分十进制的IP地址转成 4 字节IP
  18.     // b. 主机序列,转成网络序列
  19.     // in_addr_t inet_addr(const char *cp) -> 该函数可以同时完成 a & b
  20.     local.sin_addr.s_addr = inet_addr(_ip.c_str()); // "192.168.3.1" -> 字符串风格的点分十进制的IP地址 -> 4字节IP
  21.    
  22.     // 2.1bind绑定sockfd和网络信息(IP+PORT)
  23.     int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
  24.     if (n < 0)
  25.     {
  26.         LOG(FATAL, "bind error,%s,%d", strerror(errno), errno);
  27.         exit(BIND_ERROR);
  28.     }
  29.     LOG(INFO, "socket bind success");
  30. }
复制代码
  服务端绑定一般不指定IP,为什么?
  当一个服务器的带宽足够大时,一台机器接收数据的能力就束缚了这台机器的IO效率,因此一台服务器底层可能装有多张网卡,此时这台服务器就可能会有多个IP地点,但一台服务器上端口号为8081的服务只有一个。这台服务器在接收数据时,这里的多张网卡在底层实际都收到了数据,如果这些数据也都想访问端口号为8081的服务。
**此时如果服务端在绑定的时候是指明绑定的某一个IP地点,那么此时服务端在接收数据的时候就只能从绑定IP对应的网卡接收数据。**而如果服务端绑定的是INADDR_ANY(宏,值为0,表现恣意IP),那么只要是发送给端口号为8081的服务的数据,体系都会可以将数据自底向上交给该服务端。
以是这里我们对代码做修改:
  1. UdpServer(uint16_t port) : _sockfd(sockfddefault), _port(port), _isrunning(false) {}
  2. void InitServer()
  3. {
  4.     // 1.创建UDP套接字
  5.     _sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 返回文件描述符
  6.     if (_sockfd < 0)
  7.     {
  8.         LOG(FATAL, "socket error,%s,%d", strerror(errno), errno);
  9.         exit(SOCKET_ERROR);
  10.     }
  11.     LOG(INFO, "socket create success,sockfd:%d", _sockfd);
  12.     // 2.0填充sockaddr_in结构
  13.     struct sockaddr_in local;      // struct sockaddr_in 系统提供的数据类型。local是变量,用户栈上开辟空间。
  14.     bzero(&local, sizeof(local));  // 将从&local开始的sizeof(local)大小的内存区域置零
  15.     local.sin_family = AF_INET;    // 设置网络通信方式
  16.     local.sin_port = htons(_port); // port要经过网络传输给对面,所有需要从主机序列转换为网络序列
  17.     // a. 字符串风格的点分十进制的IP地址转成 4 字节IP
  18.     // b. 主机序列,转成网络序列
  19.     // in_addr_t inet_addr(const char *cp) -> 该函数可以同时完成 a & b
  20.     // local.sin_addr.s_addr = inet_addr(_ip.c_str()); // "192.168.3.1" -> 字符串风格的点分十进制的IP地址 -> 4字节IP
  21.     local.sin_addr.s_addr = INADDR_ANY; // INADDR_ANY宏的值为0,给local.sin_addr.s_addr设置为0代表任意IP,因为一个服务器有多个IP,为了确保所有请求_port端口的请求都能得到相应,所以设置为0
  22.     // 2.1bind绑定sockfd和网络信息(IP+PORT)
  23.     int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
  24.     if (n < 0)
  25.     {
  26.         LOG(FATAL, "bind error,%s,%d", strerror(errno), errno);
  27.         exit(BIND_ERROR);
  28.     }
  29.     LOG(INFO, "socket bind success");
  30. }
复制代码
字符串IP & 整数IP

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


  • 字符串IP:类似于192.168.233.123这种字符串形式的IP地点,叫做基于字符串的点分十进制IP地点,这种ip是给人看的
  • 整数IP:IP地点在进行网络传输时所用的形式,用一个32位的整数来表现IP地点,这种ip是网络传输用的
为什么要分两种IP表现形式?
如果我们在网络传输时直接以基于字符串的点分十进制IP的形式进行IP地点的传送,那么此时一个IP地点至少就需要15个字节,但实际并不需要耗费这么多字节。
点分十进制IP地点实际可以分别为四个地区,此中每一个地区的取值都是0~255,而这个范围的数字只需要用8个比特位就能表现,因此我们实际只需要32个比特位就能够表现一个IP地点。此中这个32位的整数的每一个字节对应的就是IP地点中的某个地区,我们将IP地点的这种表现方法称之为整数IP,此时表现一个IP地点只需要4个字节。
以是在网络编程中会涉及到字符串IP与整数IP之间的转换,而体系也提供给了我们转换的函数。
字符串IP转换为整数IP
  1. in_addr_t inet_addr(const char *cp);
复制代码
该函数使用起来非常简单,我们只需传入待转换的字符串IP,该函数返回的就是转换后的整数IP。除此之外,inet_aton函数也可以将字符串IP转换成整数IP,不外该函数使用起来没有inet_addr简单。
整数IP转化为字符串IP:
  1. char *inet_ntoa(struct in_addr in);
复制代码
需要留意的是,传入inet_ntoa函数的参数类型是in_addr,因此我们在传参时不需要选中in_addr结构当中的32位的成员(即s_addr)传入,直接传入in_addr结构体即可。
3.3运行

以上创建套接字和绑定的操纵都是属于初始化服务端的内容,那么接下来我们就需要编写服务端运行过程的代码,让服务端启动服务了。
服务器实际上就是在周而复始的为我们提供某种服务,服务器之以是称为服务器,是因为服务器运行起来后就永远不会退出,因此服务器实际执行的是一个死循环代码。
   由于UDP服务器是不面向毗连的,因此只要UDP服务器启动后,就可以直接读取客户端发来的数据。
  接收数据的函数:
  1. ssize_t recvfrom(int sockfd
  2.                  , void *buf
  3.                  , size_t len
  4.                  , int flags
  5.                  , struct sockaddr *src_addr
  6.                  , 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*类型进行强转。
发送数据的函数:
  1. ssize_t sendto(int sockfd
  2.                , const void *buf
  3.                , size_t len
  4.                , int flags
  5.                , const struct sockaddr *dest_addr
  6.                , socklen_t addrlen);
复制代码
参数说明:


  • sockfd:对应操纵的文件形貌符。表现将数据写入该文件形貌符索引的文件当中。
  • buf:待写入数据的存放位置。
  • len:期望写入数据的字节数。
  • flags:写入的方式。一般设置为0,表现阻塞写入。
  • dest_addr:对端网络相关的属性信息,包括协议眷属、IP地点、端口号等。 addrlen:传入dest_addr结构体的长度。
返回值说明:


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


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

以上主要是为了让各人熟悉以下网络编程的接口,那么接下来用一个例子带各人开端相识网络编程的思绪。
下面我们实现一个简易echo服务器,他的功能就是客户端向服务端发送什么数据,服务端再将数据发送回来。
也就是说服务端需要接收客户端发送的数据recvfrom,然后还需要将数据发送出去sendto。
启动服务端服务
  1. void Start()
  2. {
  3.     // 一直运行,直到管理者不想运行了, 服务器都是死循环
  4.     // UDP是面向数据报的协议
  5.     _isrunning = true;
  6.     while (true)
  7.     {
  8.         char buffer[1024];
  9.         struct sockaddr_in peer;
  10.         socklen_t len = sizeof(peer); // 必须初始化为sizeof(peer),不能是0
  11.         // 1.要先让server接收数据
  12.         ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
  13.         if (n > 0)
  14.         {
  15.             buffer[n] = 0;
  16.             InetAddr addr(peer);
  17.             LOG(DEBUG, "get message from [%s:%d]: %s\n", addr.Ip().c_str(), addr.Port(), buffer);
  18.             // 2. 我们要将server收到的数据,发回给对方
  19.             sendto(_sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&peer, len);
  20.         }
  21.     }
  22.     _isrunning = false;
  23. }
复制代码
  InetAddr类的实现
  我们想要将IP和PORT输出到屏幕上,这就必须进行一些转换工作,比如整数IP到点分十进制的IP转换,网络字节序到主机字节序的转换等,以是我们可以实现一个类,让类内部帮我们进行转换。
  1. #pragma once
  2. #include <iostream>
  3. #include <sys/types.h>
  4. #include <sys/socket.h>
  5. #include <arpa/inet.h>
  6. #include <netinet/in.h>
  7. // 这是一个可以获取点分十进制格式IP地址和Port端口号的类
  8. class InetAddr
  9. {
  10.     private:
  11.     struct sockaddr_in _addr;
  12.     std::string _ip;
  13.     uint16_t _port;
  14.     private:
  15.     void GetAddress(std::string *ip, uint16_t *port)
  16.     {
  17.         *port = ntohs(_addr.sin_port);
  18.         *ip = inet_ntoa(_addr.sin_addr); // inet_ntoa是一个用于将网络字节序的 IP 地址转换为点分十进制的字符串格式(如 "192.168.1.1")的函数
  19.     }
  20.     public:
  21.     InetAddr(const struct sockaddr_in &addr) : _addr(addr)
  22.     {
  23.         GetAddress(&_ip, &_port);
  24.     }
  25.     std::string Ip()
  26.     {
  27.         return _ip;
  28.     }
  29.     uint16_t Port()
  30.     {
  31.         return _port;
  32.     }
  33.     ~InetAddr() {}
  34. };
复制代码
客户端步伐的编写
客户端也需要进行类似服务端的初始化工作,即套接字的创建,绑定等。
  1. #include <iostream>
  2. #include <string>
  3. #include <cstring>
  4. #include <cstdlib>
  5. #include <sys/types.h>
  6. #include <sys/socket.h>
  7. #include <netinet/in.h>
  8. #include <arpa/inet.h>
  9. void Usage(std::string proc)
  10. {
  11.     std::cout << "Usage:\n\t" << proc << " serverip serverport\n"
  12.         << std::endl;
  13. }
  14. // ./udpclient serverip serverport
  15. int main(int argc, char *argv[])
  16. {
  17.     if (argc != 3)
  18.     {
  19.         Usage(argv[0]);
  20.         exit(1);
  21.     }
  22.     std::string serverip = argv[1];
  23.     uint16_t serverport = std::stoi(argv[2]);
  24.     // 1.创建socket
  25.     int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
  26.     if (sockfd < 0)
  27.     {
  28.         std::cerr << "socket error" << std::endl;
  29.     }
  30.     // 构建目标主机的socket信息
  31.     struct sockaddr_in server;
  32.     memset(&server, 0, sizeof(server)); // bzero
  33.     server.sin_family = AF_INET;
  34.     server.sin_port = htons(serverport);
  35.     server.sin_addr.s_addr = inet_addr(serverip.c_str()); // inet_addr用于将点分十进制的 IPv4 地址字符串转换成一个长整型数(通常是 u_long 或 in_addr_t 类型)。
  36.    
  37.     // 客户端要不要bind?
  38.     std::string message;
  39.     // 2.直接通信即可(Start)
  40.     while (true)
  41.     {
  42.         std::cout << "Please Enter# ";
  43.         std::getline(std::cin, message);
  44.         sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));
  45.         struct sockaddr_in peer;
  46.         socklen_t len = sizeof(peer);
  47.         char buffer[1024];
  48.         ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
  49.         if (n > 0)
  50.         {
  51.             buffer[n] = 0;
  52.             std::cout << "server echo# " << buffer << std::endl;
  53.         }
  54.     }
  55.     return 0;
  56. }
复制代码
  客户端要不要绑定?
  答案是肯定的,因为网络通信的前提就是需要客户端的IP和PORT,服务端的IP和PORT,通过他们两个网络中的进程才可以进行通信。但是客户端不能像服务端一样显式的bind,设想一个场景,淘宝写了一个客户端,表现绑定了端口号8080,而微信写的客户端也表现绑定的8080端口号,那此时就会因为端口冲突导致你只能使用一项服务,这很明显是不现实的,以是客户端绑定端口的操纵由操纵体系自动完成,就是为了防止客户端端口号冲突,一般在初次发送数据的时候绑定。
  我们已经实现好了服务端的类,和客户端步伐,接下来我们只需要再实现一个步伐,调用服务端对象的初始化和启动方法:
  1. #include <iostream>
  2. #include <memory>
  3. #include "UdpServer.hpp"
  4. void Usage(std::string proc)
  5. {
  6.     std::cout << "Usage:\n\t" << proc << " local_port\n"
  7.         << std::endl;
  8. }
  9. // ./udpserver port
  10. // 云服务器的port默认都是禁止访问的。云服务器放开端口8080 ~ 8085
  11. int main(int argc, char *argv[])
  12. {
  13.     if (argc != 2)
  14.     {
  15.         Usage(argv[0]);
  16.         exit(USAGE_ERROR);
  17.     }
  18.     EnableScreen();
  19.     uint16_t port = std::stoi(argv[1]);
  20.     std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port); // C++14
  21.     usvr->InitServer();
  22.     usvr->Start();
  23.     return 0;
  24. }
复制代码
本地测试
起首利用127.0.0.1(回环地点),进行本地测试。

   127.0.0.1 是一个特殊的IPv4地点,被称为“回环地点”或“localhost”。它通常用于指代本地计算机上的网络服务,而不是网络上的另一台计算机。在开辟或测试阶段,开辟人员经常需要在本地计算机上运行多个服务实例,并使用127.0.0.1来访问它们。
  网络测试
在网络测试前,你需要确保你的云服务器安全组配置已经打开了你所希望绑定的端口号,就像这样:

大概通过命令行的方式添加开放端口规则和重新加载:
  1. sudo ufw allow xx/udp
  2. sudo ufw reload
复制代码

我们可以利用netstat查看网络信息:

我们发现绑定的IP为0.0.0.0即恣意IP,端口号8888,链接方式UDP。
netstat的命令行参数:


  • -n:number的意思,即IP和端口号都用数字的形式展示。
  • -p:表现哪个进程或步伐正在使用套接字(socket)。
  • -u:仅表现 UDP 毗连。
  • -a:表现所有运动的网络毗连和监听的服务器套接字。
请留意,由于它表现了进程信息,因此你可能需要具有适当的权限才华运行它。在某些体系上,你可能需要使用 sudo 来运行此命令,如 sudo netstat -npua。

青年人珍重的描写罢,时间正翻着书页,请你着笔! —青年人
实例,并使用127.0.0.1来访问它们。
网络测试
在网络测试前,你需要确保你的云服务器安全组配置已经打开了你所希望绑定的端口号,就像这样:
[外链图片转存中…(img-P0BkEsE1-1720966085373)]
大概通过命令行的方式添加开放端口规则和重新加载:
  1. sudo ufw allow xx/udp
  2. sudo ufw reload
复制代码
[外链图片转存中…(img-s8ieJtHZ-1720966085374)]
我们可以利用netstat查看网络信息:
[外链图片转存中…(img-xXMCimr7-1720966085374)]
我们发现绑定的IP为0.0.0.0即恣意IP,端口号8888,链接方式UDP。
netstat的命令行参数:


  • -n:number的意思,即IP和端口号都用数字的形式展示。
  • -p:表现哪个进程或步伐正在使用套接字(socket)。
  • -u:仅表现 UDP 毗连。
  • -a:表现所有运动的网络毗连和监听的服务器套接字。
请留意,由于它表现了进程信息,因此你可能需要具有适当的权限才华运行它。在某些体系上,你可能需要使用 sudo 来运行此命令,如 sudo netstat -npua。

青年人珍重的描写罢,时间正翻着书页,请你着笔! —青年人

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

鼠扑

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表