【ONE·Linux || 网络基础(一)】

总言

  主要内容:①简述网络传输流程(TCP/IP五层模式概念认知,对Mac地址、端口号、网络字节序等有一个总体上的了解,后续博文将对其展开说明)。②演示socke网络套接字编程(熟悉相关接口,并以UDP网络模式演示)。


  
  
  

1、基础简述

1.1、计算机网络背景

  1)、网络发展说明
  独立模式: 计算机之间相互独立;
  网络互联: 多台计算机连接在一起, 完成数据共享;
  局域网LAN: 计算机数量更多了, 通过交换机和路由器连接在一起。
  广域网WAN: 将远隔千里的计算机都连在一起;
  PS:所谓 “局域网” 和 “广域网” 只是一个相对的概念.。
  
  
  问题:为什么要有协议?
  
  
  
  
  
  

1.2、认识网络协议(TCP/IP五层结构模型)

  1)、协议分层
  计算机之间的传输媒介是光信号和电信号,通过 “频率” 和 “强弱” 来表示 0 和 1 这样的信息。要想传递各种不同的信息,就需要约定好双方的数据格式。
  
  实际的网络通信会需要分更多的层次,分层最大的好处在于 “封装”。
  
  
  
  2)、OSI七层模型

  OSI(Open System Interconnection,开放系统互连) 七层网络模型称为开放式系统互联参考模型,是一个逻辑上的定义和规范。把网络从逻辑上分为了7层,每一层都有相关、相对应的物理设备,比如路由器,交换机。
  OSI 七层模型是一种框架性的设计方法,其最主要的功能使就是帮助不同类型的主机实现数据传输。它的最大优点是将服务、接口和协议这三个概念明确地区分开来,概念清楚,理论也比较完整,通过七个层次化的结构模型,使不同的系统不同的网络之间实现可靠的通讯。

  虽然OSI 所定义的协议虽然并没有得到普及,但是在OSI协议设计之初作为其指导方针的OSI参考模型却常被用于网络协议的制定当中。
  
  实际中,主流的通信协议常按照TCP/IP五层模型来处理。

  
  OSI协议分层模型:各层大致功能如下。

名称功能
7应用层针对特定应用的协议。
6表示层设备固有数据格式和网络标准数据格式的转换。
5会话层通信管理。负责建立和断开通信连接(数据流动的逻辑通路)。管理传输层以下的分层。
4传输层管理两个节点之间的数据传输。负责可靠传输(确保数据被可靠地传送到目标地址)。
3网络层地址管理与路由选择。
2数据链路层互连设备之间传送和识别数据帧。
1物理层以“0”、 “1”代表电压的高低、灯光的闪灭。界定连接器和网线的规格。

  
  
  

  3)、TCP/IP五层(或四层)模型
  TCP/IP是一组协议的代名词,是利用IP进行通信时所必须用到的协议群的统称。 它还包括许多协议,组成了TCP/IP协议簇(网络协议簇)。具体来说,IP 或ICMP、TCP或UDP、TELNET或FTP、以及HTTP等都属于TCP/IP的协议。
在这里插入图片描述
  
  TCP/IP通讯协议采用了5层的层级结构,每一层都呼叫它的下一层所提供的网络来完成自己的需求。
  

名称功能具体说明
应用层负责应用程序间沟通如简单电子邮件传输(SMTP)、文件传输协议(FTP)、网络远程访问协议(Telnet)等。
传输层负责两台主机之间的数据传输。如传输控制协议 (TCP), 能够确保数据可靠的从源主机发送到目标主机。
网络层负责地址管理和路由选择。例如在IP协议中, 通过IP地址来标识一台主机, 并通过路由表的方式规划出两台主机之间的数据传输的线路(路由)。路由器(Router)工作在网路层。
数据链路层负责设备之间的数据帧的传送和识别。例如网卡设备的驱动、帧同步(就是说从网线上检测到什么信号算作新帧的开始)、冲突检测(如果检测到冲突就自动重发)、数据差错校验等工作。有以太网、令牌环网, 无线LAN等标准。交换机(Switch)工作在数据链路层。
物理层负责光/电信号的传递方式。比如现在以太网通用的网线(双绞线)、早期以太网采用的的同轴电缆(现在主要用于有线电视)、光纤, 现在的wifi无线网使用电磁波等都属于物理层的概念。物理层的能力决定了最大传输速率、传输距离、抗干扰性等。集线器(Hub)工作在物理层。

  
  网络编程主要是针对应用层:
在这里插入图片描述

  
  
  
  
  
  
  
  

1.3、网络传输的基本流程(预备知识)

1.3.1、TCP/IP通讯过程(封装和分用)

  1)、总体呈现
在这里插入图片描述

  
  
  2)、TCP/IP通讯过程
  同一个网段内的两台主机可以进行文件传输(局域网中两台主机是可以直接通信的)。 虽然在通讯双方看来是彼此之间的通信(应用层->应用层),实际其经过了一个自顶向下,又自底向上的过程(应用层到底层,底层到应用层)。

在这里插入图片描述
  
  这里,重要的是要理解数据传输时的物理视角,以及引入报头概念(下述介绍到)。
  
  TCP/IP网络传输的每个分层中,都会对所发送的数据附加一个首部,在这个首部中包含了该层必要的信息,如发送的目标地址以及协议相关信息。通常,为协议提供的信息为包首部,所要发送的内容为数据。如图2.17,

  
  
  3)、数据包封装和分用

  应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部(header),称该过程为封装(Encapsulation)
  首部信息中包含了该层必要的信息,如首部有多长,载荷(payload)有多长,上层协议是什么等信息。 在下一层的角度看,从上一分层收到的包全部都被认为是本层的数据

在这里插入图片描述

  数据封装成帧后发到传输介质上,到达目的主机后每层协议再剥掉相应的首部,根据首部中的 “上层协议字段” 将数据交给对应的上层协议处理。
在这里插入图片描述

  
  不同的协议层对数据包有不同的称谓,在传输层叫做段(segment),在网络层叫做数据报 (datagram),在链路层叫做帧(frame)。

包、帧、数据报、段、消息。 以上五个述语都用来表述数据的单位,大致区分如下:
可以说是全能性述语。
用于表示数据链路层中包的单位。
数据包是IP和UDP等网络层以上的分层中包的单位。
则表示TCP数据流中的信息。
最后,消息是指应用协议中数据的单位。

  

  数据分选细节如下:
在这里插入图片描述
  
  
  
  
  
  

1.3.2、认识Mac地址、IP地址(ifconfig指令)

  1)、认识Mac地址、IP地址
  MAC地址: 用来识别数据链路层中相连的节点。 长度为48位,即6个字节。一般用16进制数字加上冒号的形式来表示,例如: 08:00:27:03:fb:19
  PS:
  1、MAC地址通常是唯一的,在网卡出厂时就已确定,不能修改。
  2、①虚拟机中的mac地址不是真实的mac地址,可能会冲突。②也有些网卡支持用户配置mac地址。

  
  ifconfig指令:Linux 和 UNIX 系统上用于配置和显示网络接口信息的命令。可以查看相关配置属性(IP地址、Mac地址)
在这里插入图片描述  
  
  
  IP地址: IP地址是在IP协议中,用来标识网络中不同主机的地址。 有两个版本,IPv4和IPv6。对于IPv4来说,IP地址是一个4字节,32位的整数。我们通常也使用 “点分十进制” 的字符串表示IP地址,例如:192.168.0.1 ,用点分割的每一个数字表示一个字节,范围是 0 - 255
  
   如何区别IP地址和Mac地址: ①在IP数据包头部中,有两个IP地址,分别叫做源IP地址,和目的IP地址。 ②数据要从源IP目的IP,则需要从源Mac地址,途经各个下一站Mac地址。换而言之,在IP数据包头部中,源IP地址和目的IP地址用于标识发送者和接收者。而数据在传输过程中,会根据目的IP地址来确定下一跳的Mac地址,以确保数据包能够正确地从一个设备传输到另一个设备。
  
  
  
  
  

  2)、为什么局域网中两台主机能相互通信?如何通信?
  主机收到以太网包以后,首先从以太网的包首部找到MAC地址判断是否为发给自己的包。如果不是发给自己的包则丢弃数据。而如果接收到了恰好是发给自己的包,就查找以太网包首部中的类型域从而确定以太网协议所传送过来的数据类型。继续向上交付。
在这里插入图片描述
  
  
  
  
  3)、假如两台主机不在同一局域网中,如何通信?
  跨网段的主机的文件传输方式:数据从一台计算机到另一台计算机传输过程中要经过一个或多个路由器

在这里插入图片描述

  
  
  
  
  

1.3.3、认识端口号

  1)、问题引入
  问题:对当前主机而言,把自身的数据送到对方的主机,是最终目的吗?
  回答:并不是。真正的网络通信过程,本质其实是进程间通信。(例如:客户端进程、服务器进程。)。将数据在主机间转发仅仅是用于完成进程间通信的手段,对方主机接收到数据之后,需要将数据交付给指定的进程

  知道IP地址我们可以确定一台主机,然而一台主机中,OS可有多个进程同时运行,如何确定将数据交给哪一个进程?
  因此,引入端口号。

在这里插入图片描述

  PS:在TCP/IP模型中,上述通信是通过套接字(socket)来完成的。 套接字是进程间通信的端点,它允许进程发送和接收数据,而无需关心底层网络协议的实现细节。(网络套接字在后文中将介绍到,这里先来了解一些关于它的预备知识)
  
  
  
  
  2)、概念与相关说明

  端口号(port):传输层协议的内容,是一个2字节(16比特位)的整数,用来标识OS中的一个进程。(根据端口号,可知晓当前数据需要交给主机中的哪一个进程来处理。)
  注意事项: 端口号具有唯一性,一个端口号只能被一个进程占用。IP地址 + 端口号能够标识网络上的某一台主机的某一个进程。
  
  
  细节理解一:“端口号” 和 “进程ID”。(问题:进程ID也是唯一的,为什么不使用进程ID来标识特定的进程?)
  ①二者都具唯一性,但一个是网络模块,一个是进程管理模块。不是不能互相只用一个,但这样一来会将两个不同模块之间关联起来,不如各自搞各自的一套执行方案,实现数据解耦
  ②并非所有进程都需要网络通信(端口号),一个进程可以绑定多个端口号, 但是一个端口号不能被多个进程绑定
  
  细节理解二:源端口号和目的端口号
  传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫做源端口号和目的端口号。就是在描述 “数据是谁发的、要发给谁”,(谁发数据,谁就是源端口号;谁接收数据,谁就是目的端口号)。
  
  
  
  
  
  

1.3.4、初步认识TCP协议和UDP协议

  1)、各自特点
  TCP协议Transmission Control Protocol 传输控制协议。

传输层协议
有连接
可靠传输
面向字节流

  (关于可靠、不可靠:不能肤浅的字面理解。如数据丢包之类,有些场景丢包影响并不大,而可靠的背后代表着为了数据安全该协议会做大量处理工作,增加工作量和维护成本。)
  
  
  UDP协议User Datagram Protocol 用户数据报协议。

传输层协议
无连接
不可靠传输
面向数据报

  
  
  具体内容后续博文将详细讲述。
  
  
  
  
  
  

1.3.5、网络字节序(字节序转换的函数)

在这里插入图片描述

  
  1)、基本说明:网络中也存在大小端

  内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分
  一个认识:网络通信是在不同主机间进行的,进行通信的两台主机间,其算机系统的字节序(Byte Order)可能不同。这也就意味着网络数据也存在大小端问题。
  
  
  发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是需要按内存地址从低到高的顺序保存。因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
  TCP/IP协议规定:网络数据流应采用大端字节序,即低地址高字节。不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据。如果当前发送主机是小端,就需要先将数据转成大端,否则就忽略直接发送即可。(即网络字节序和主机字节序之间的转换)
  
  
  
  
  2)、用于做网络字节序和主机字节序转换的函数
  为使网络程序具有可移植性、使同样的C代码在大端和小端计算机上编译后都能正常运行。可以调用以下库函数做网络字节序和主机字节序的转换。

NAME
       htonl, htons, ntohl, ntohs - convert values between host and network byte order

SYNOPSIS
       #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。hton:主机传网络;ntoh:网络传主机。
  l表示32位长整数(unsigned integer),s表示16位短整数(unsigned short integer)。
  例如:htonl表示将32位的长整数从主机字节序转换为网络字节序。实际场景举例:将IP地址转换后准备发送。

DESCRIPTION
       The htonl() function converts the unsigned integer hostlong from host byte order to network byte order.

       The htons() function converts the unsigned short integer hostshort from host byte order to network byte order.

       The ntohl() function converts the unsigned integer netlong from network byte order to host byte order.

       The ntohs() function converts the unsigned short integer netshort from network byte order to host byte order.

       On the i386 the host byte order is Least Significant Byte first, whereas the network byte order, as used on the Internet, is Most Sig‐
       nificant Byte first.

  
  
  
  
  
  
  
  
  

2、socket网络套接字编程

  以下函数后续第三部分编写UDP使用时讲解,这里只是将其总结性拎出,方便查阅。

在这里插入图片描述
  主要介绍基于IPv4的socket网络编程
  
  

2.1、sockaddr结构

  1)、整体说明
  说明: socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6等。理论上有三套套接字,分别对应不同场景, 各种网络协议的地址格式并不相同。但OS在设计时统一使用sockaddr接口,再根据传参,sockaddr结构体的首地址(地址类型字段)可以确定究竟是哪一类型套接字,从而指向对应套接字结构体中的内容。
在这里插入图片描述

  1、IPv4IPv6的地址格式定义在netinet/in.h中。IPv4地址用sockaddr_in结构体表示,包括16位地址类型、16位端口号和32位IP地址。
  2、IPv4IPv6地址类型分别定义为常数AF_INETAF_INET6(宏)。 这样设置的好处在于:只要取得某种sockaddr结构体的首地址,就可以根据地址类型字段确定结构体中的内容(即:不需要知道具体的sockaddr结构体内部格式)。
  3、socket API可以都用struct sockaddr *类型表示,在使用的时候需要强制转化成sockaddr_in。这样的好处在于程序的通用性,可以接收IPv4IPv6以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数。
  
  
  
  
  2)、一些常见的地址格式(内部实现简览)
  sockaddr 结构:

/* Structure describing a generic socket address.  */
struct sockaddr
  {
    __SOCKADDR_COMMON (sa_);	/* Common data: address family and length.  */
    char sa_data[14];		/* Address data.  */
  };

  
  
  
  
  sockaddr_in 结构: 该结构里主要有三部分信息,地址类型、端口号、IP地址。

struct sockaddr_in
  {
    __SOCKADDR_COMMON (sin_);
    in_port_t sin_port;			/* Port number.  */
    struct in_addr sin_addr;		/* Internet address.  */

    /* Pad to size of `struct sockaddr'.  */
    unsigned char sin_zero[sizeof (struct sockaddr) -
			   __SOCKADDR_COMMON_SIZE -
			   sizeof (in_port_t) -
			   sizeof (struct in_addr)];
  };

  结构体struct in_addr sin_addr;,对于struct in_addr:表示一个IPv4的IP地址, 其实就是一个32位的整数uint32_t

/* Internet address.  */
typedef uint32_t in_addr_t;
struct in_addr
  {
    in_addr_t s_addr;
  };

  
  
  
  
  sockaddr_in6 结构:

#if !__USE_KERNEL_IPV6_DEFS
/* Ditto, for IPv6.  */
struct sockaddr_in6
  {
    __SOCKADDR_COMMON (sin6_);
    in_port_t sin6_port;	/* Transport layer port # */
    uint32_t sin6_flowinfo;	/* IPv6 flow information */
    struct in6_addr sin6_addr;	/* IPv6 address */
    uint32_t sin6_scope_id;	/* IPv6 scope-id */
  };
#endif /* !__USE_KERNEL_IPV6_DEFS */

  
  
  
  
  

2.2、地址转换函数

在这里插入图片描述
在这里插入图片描述

  
  

2.2.1、字符串转in_addr的函数

  in_addr:将一个点分制的IP地址(如192.168.0.1),转换为网络中(sockaddr结构中)需要的32位IP地址(0xC0A80001)。即一次能完成两步骤:①、将点分十进制字符串风格的IP地址→ 4字节;②、4字节主机序列 → 网络序列。

  inet_aton:将cp所指的字符串转换成一个32位的网络字节序的二进制值,并通过指针inp来存储(该函数对输入的字符串执行有效性检查)。

SYNOPSIS
       #include <sys/socket.h>
       #include <netinet/in.h>
       #include <arpa/inet.h>

       in_addr_t inet_addr(const char *cp);
       int inet_aton(const char *cp, struct in_addr *inp);

DESCRIPTION
       inet_aton() converts the Internet host address cp from the  IPv4  numbers-and-dots
       notation  into  binary form (in network byte order) and stores it in the structure
       that inp points to.  inet_aton() returns nonzero if the address is valid, zero  if
       not.      

  
  
  
  
  inet_pton:对于IPv4地址和IPv6地址都适用,函数中p和n分别代表表达(presentation)和数值(numeric)。其功能同样是将点分十进制格式的地址字符串,转换为网络字节序整型数。

NAME
       inet_pton - convert IPv4 and IPv6 addresses from text to binary form

SYNOPSIS
       #include <arpa/inet.h>

       int inet_pton(int af, const char *src, void *dst);
       
DESCRIPTION
       This  function  converts the character string src into a network address structure
       in the af address family, then copies the network address structure to  dst.   The
       af argument must be either AF_INET or AF_INET6.      

  参数:

af  :转换格式。AF_INET(IPV4),或者AF_INET6(IPV6)
src :点分格式的地址
dst :转换后的整型变量的地址

  使用演示:

    // 绑定:int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
    void Bind(int sock, uint16_t port, const std::string& ip = "0.0.0.0")
    {
        // 准备工作:sockaddr结构体
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);                    // 对端口号:需要转换为网络字节序
        inet_pton(AF_INET, ip.c_str(), &local.sin_addr); // 对ip:点分十进制风格-->网络字节序+四字节

        // 绑定:
        if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "bind:绑定失败。%d:%s", errno, strerror(errno));
            exit(3); // 退出
        }
        logMessage(NORMAL, "bind: 绑定成功。");
    }

  
  
  

2.2.2、in_addr转字符串的函数

  相关函数如下:

SYNOPSIS
       #include <sys/socket.h>
       #include <netinet/in.h>
       #include <arpa/inet.h>
       
       char *inet_ntoa(struct in_addr in);

DESCRIPTION
       The  inet_ntoa()  function converts the Internet host address in, given in network
       byte order, to a string in IPv4 dotted-decimal notation.  The string  is  returned
       in a statically allocated buffer, which subsequent calls will overwrite.

  说明1: inet_ntoa函数返回了一个char* ,实则就是转换后的结果。根据阅读手册,该函数自己在内部为我们申请了一块内存来保存返回的ip结果,该返回结果放到了静态存储区,不需要我们手动进行释放。但这样一来存在一个问题,在同一程序中调用多次这个函数,会出样第二次调用时的结果会覆盖掉上一次的结果的情况。此外,APUE明确提出inet_ntoa不是线程安全的函数,那么多线程中调用存在线程安全问题。所以一般推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题。

  
  说明2: inet_ptoninet_ntop不仅可以转换IPv4in_addr,还可以转换IPv6in6_addr,因此函数接口是void*addrptr

NAME
       inet_ntop - convert IPv4 and IPv6 addresses from binary to text form

SYNOPSIS
       #include <arpa/inet.h>

       const char *inet_ntop(int af, const void *src,char *dst, socklen_t size);

DESCRIPTION
       This  function converts the network address structure src in the af address family
       into a character string.  The resulting string is copied to the buffer pointed  to
       by  dst,  which  must  be  a non-NULL pointer.  The caller specifies the number of
       bytes available in this buffer in the argument size.

       inet_ntop() extends the inet_ntoa(3) function to support  multiple  address  fami‐
       lies,  inet_ntoa(3)  is  now  considered to be deprecated in favor of inet_ntop().

  参数:

af  :转换格式。AF_INET(IPV4),或者AF_INET6(IPV6)
src :整型变量的地址
dst :用来存储转换后的数据的地址
cnt :存储空间的大小

  
  
  
  
  
  
  

3、基于套接字的UDP网络程序

3.0、log.hpp日志

#pragma once

#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>

// 日志级别
#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4

const char *gLevelMap[] = {
    "DEBUG",
    "NORMAL",
    "WARNING",
    "ERROR",
    "FATAL"
};



// 完整的日志功能,至少有 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...)//const char *format, ... 可变参数
{
#ifndef DEBUG_SHOW
    if(level== DEBUG) return;
#endif

    //标准部分:固定输出的内容
    char stdBuffer[1024]; 
    time_t timestamp = time(nullptr);
    snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", gLevelMap[level], timestamp);


    //自定义部分:允许用户根据自己的需求设置
    char logBuffer[1024]; 
    va_list args; //定义一个va_list对象
    va_start(args, format); 
    vsnprintf(logBuffer, sizeof logBuffer, format, args);
    va_end(args); //相当于 args == nullptr

    printf("%s%s\n", stdBuffer, logBuffer);
}

  
  
  
  
  

3.1、udp_server.hpp(服务器)

3.1.1、成员变量与构造、析构函数

  以下为服务器基本框架搭建:使用时,只需要在udp_server.cc端调用InitServerStart函数,那么服务端的程序运行时,就会得到以供客户端传送消息的服务器。

class UdpServer
{
public:
    // 构造:将对应的端口号、IP传入
    UdpServer(uint16_t port, std::string ip = "")//对ip默认值说明:方便后续bind操作,1、可从任意IP获取数据(默认情况),2、也可指定需要的IP(自己传入参数的情况)
        : port_(port), ip_(ip), sock_(-1)
    {
    }

    // 析构:关闭套接字
    ~UdpServer()
    {
        if (sock_ >= 0)
            close(sock_);
    }

    //初始化服务器
    bool InitServer()
    {

    }

    //启动服务器
    void Start()
    {

    }

private:
    uint16_t port_;  // 端口号:16位的整数
    std::string ip_; // IP地址:点分十进制字符串风格
    int sock_;       // 通讯时的套接字:需要供多处使用
};

  
  
  
  
  

3.1.2、初始化服务器:bool initServer()

  初始化服务器常做的两件事:
  1、创建套接字;
  2、绑定服务器的端口号、IP地址(将用户设置的/分配的ip和port在内核中与当前进程创建的套接字强关联)。
  

3.1.2.1、socket

  1)、相关函数介绍

  man socket用于创建一个新的套接字(socket),注意其包含的头文件。

NAME
       socket - create an endpoint for communication

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

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

DESCRIPTION
       socket() creates an endpoint for communication and returns a descriptor.

  protocol:协议。一般只要前两个参数确定好,那么对应的协议也就确定了。(忽略可填0)

  domain:套接字的域。以下列举出的最常使用的三种类型,IPv4、IPv6、本地通讯。(其它可查阅文档)

       The  domain  argument specifies a communication domain; this selects the protocol family which
       will be used for communication.  These families are defined in <sys/socket.h>.  The  currently
       understood formats include:

       Name                Purpose                          Man page
       AF_UNIX, AF_LOCAL   Local communication              unix(7)
       AF_INET             IPv4 Internet protocols          ip(7)
       AF_INET6            IPv6 Internet protocols          ipv6(7)

  type:通讯种类。这里也只列举了常用的两个类型,面向数据报(UDP)模式,以及面向流式(TCP)。其它可查阅文档。

       The  socket  has  the  indicated type, which specifies the communication semantics.  Currently
       defined types are:
		//面向流式
       SOCK_STREAM     Provides sequenced, reliable, two-way, connection-based byte streams.  An out-
                       of-band data transmission mechanism may be supported.
		//面向数据报
       SOCK_DGRAM      Supports  datagrams  (connectionless,  unreliable  messages of a fixed maximum
                       length).

  返回值:若成功,则返回一个file descriptor,即文件描述符(指向对应的套接字)。若失败则返回-1,并设置错误码。

RETURN VALUE
       On success, a file descriptor for the new socket is returned.  On error, -1 is  returned,  and
       errno is set appropriately.

  
  
  
  2)、创建套接字
  写法如下:这里演示的是IPv4,因此使用AF_INET,通讯种类为UDP面向数据报协议,故而是SOCK_DGRAM。

        //1、创建套接字
        sock_ = socket(AF_INET, SOCK_DGRAM, 0);
        if(sock_ < 0)
        {
            logMessage(ERROR,"%d:%s ", errno, strerror(errno));
            exit(2);
        }
        logMessage(DEBUG, "创建套接字成功,sock: %d ",sock_);

  
  
  
  

3.1.2.2、bind、bzero、htons、inet_addr

  1)、相关函数介绍

在这里插入图片描述bind

  man bind:当使用socket()创建套接字时,它存在于命名空间(地址族)中,但没有地址分配给它。bind()将addr指定的地址分配给文件描述符引用的套接字。

NAME
       bind - bind a name to a socket

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

       int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
       
DESCRIPTION
       When  a  socket  is  created  with  socket(2),  it  exists in a name space (address family) but has no address
       assigned to it.  bind() assigns the address specified by addr to the socket referred to by the file descriptor
       sockfd.   addrlen  specifies  the size, in bytes, of the address structure pointed to by addr.  Traditionally,
       this operation is called “assigning a name to a socket”.

       It is normally necessary to assign a local address using bind() before a SOCK_STREAM socket may  receive  con‐
       nections (see accept(2)).

  返回值: 成功返回0,失败返回-1。

RETURN VALUE
       On success, zero is returned.  On error, -1 is returned, and errno is set appropriately.

  参数说明:
  sockfd:来源于socket()函数成功创建时的返回值。
  addrlen:即设置的sockaddr结构体的大小。

  const struct sockaddr *addr:sockaddr结构体,根据之前2.1小节内容,这里使用的是统一的接口,但实际需要根据我们的需求来设置。(如:IPv4为sockaddr_in,所以存在类型转换问题。)
在这里插入图片描述

  这里学习了解sockaddr_in结构体,是因为bind()函数的第二参数中需要使用它。
  在server.hpp文件中,该结构体填充的是服务器的端口号和IP地址。(原因解释:实际网络通信过程中,有时服务器需要对客户端发送的请求做出响应,故也需要将其IP地址和端口号告知客户端。)
在这里插入图片描述
  (PS:之前我们介绍过网络也存在字节序问题,因此,这里sockadr_in填入的数据,涉及字节序转换
  
  
  
  

在这里插入图片描述bzore

  用于清零: 这里主要用于sockaddi_in结构体中,末尾8字节填充位,也可以调用其成员对象sin_zero完成此操作。(PS:除了bzero,也可以使用void *memset(void *s, int c, size_t n);函数。)

NAME
       bzero - write zero-valued bytes

SYNOPSIS
       #include <strings.h>

       void bzero(void *s, size_t n);

DESCRIPTION
       The bzero() function sets the first n bytes of the area starting at s to zero (bytes containing '\0').

RETURN VALUE
       None.

  
  

在这里插入图片描述htons、inet_addr

  htons:网络字节序转换的函数。
  说明:这里主要用于port端口号(大小通常为16位)。服务器的IP和端口未来也是要发送给客户端主机,因此要先将数据发送到网络中,而发送到网络时涉及网络字节序。

SYNOPSIS
       #include <arpa/inet.h>

       uint32_t htonl(uint32_t hostlong);

       uint16_t htons(uint16_t hostshort);

DESCRIPTION
       The  htonl()  function  converts  the  unsigned  integer hostlong from host byte order to network byte
       order.

       The htons() function converts the unsigned short integer hostshort from host  byte  order  to  network
       byte order.

  
  inet_addr:一套接口,可以一次做完两件事情。①、将点分十进制字符串风格的IP地址 -> 4字节;②、4字节主机序列 -> 网络序列。 int_addr_tuint32_t

  说明: 通常,为了阅读性,IP地址以点分十进制字符串风格表示,例如"192.168.110.132",每一个区域取值范围是[0,255],刚好对应1字节。而三个.将IP地址划分为4个区域,理论上,4字节就可以存储IP地址。(若直接传递字符串,一共有15个字符,即15字节,增大数据大小。)

SYNOPSIS
       #include <sys/socket.h>
       #include <netinet/in.h>
       #include <arpa/inet.h>
       
      in_addr_t inet_addr(const char *cp);



DESCRIPTION
       The inet_addr() function converts the Internet host address cp from IPv4 numbers-and-dots  notation
       into  binary  data  in  network  byte  order.  If the input is invalid, INADDR_NONE (usually -1) is
       returned.  Use of this function is problematic because -1 is  a  valid  address  (255.255.255.255).
       Avoid  its use in favor of inet_aton(), inet_pton(3), or getaddrinfo(3) which provide a cleaner way
       to indicate error return.

  
  
  
  2)、 bind: 将用户设置的ip和port在内核中和当前的进程强关联

  写法如下:

        // 2、bind绑定:
        // 2.1、绑定前的准备工作:
        struct sockaddr_in localaddr;
        bzero(&localaddr, sizeof localaddr);                                           // 将结构体清零
        localaddr.sin_family = AF_INET;                                                // 告知通讯方式,通常与domain同
        localaddr.sin_port = htons(port_);                                             // 端口号:注意转为网络字节序
        localaddr.sin_addr.s_addr = ip_.empty() ? INADDR_ANY : inet_addr(ip_.c_str()); // IP:点分十进制->四字节+网络字节序
        // 2.2、绑定:将用户设置的ip和port在内核中和当前的进程强关联
        if (bind(sock_, (struct sockaddr *)&localaddr, sizeof localaddr) < 0)
        {
            logMessage(ERROR,"%d:%s",errno,strerror(errno));
            exit(2);
        }
        logMessage(DEBUG,"绑定成功,初始化服务器完成!");

  关于INADDR_ANY:让服务器在工作过程中,可以从任意IP中获取数据。(例如:同一台机器有多个网卡,那么,当发送数据给当前主机时,只要端口号确定,任意IP传来的数据都接收。)

/* Address to accept any incoming messages.  */
#define	INADDR_ANY		((in_addr_t) 0x00000000)

  
  
  
  
  

3.1.3、启动服务器:void Start()

3.1.3.1、recvfrom、ntohs、inet_ntoa

  1)、相关函数介绍
  这三个函数是用于从套接字(sockets)接收数据的不同的系统调用,在Unix和类Unix系统(如Linux)的网络编程中非常常见。

NAME
       recv, recvfrom, recvmsg - receive a message from a socket

SYNOPSIS
       #include <sys/types.h>
       #include <sys/socket.h>

       ssize_t recv(int sockfd, void *buf, size_t len, int flags);

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

       ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

  recv函数用于从已连接的套接字接收数据。对于基于连接的套接字(如TCP),这是常用的函数。
  recvfrom函数用于从无连接的套接字(如UDP)接收数据。由于UDP是无连接的,因此我们需要知道数据来自哪里,这就是为什么此函数需要src_addr和addrlen参数的原因。
  recvmsg函数提供了更高级别的消息接收功能,允许我们接收带有控制消息(如文件描述符)的数据,并可以处理更复杂的源和目标地址。这对于处理套接字选项和扩展功能(如UNIX域套接字上的文件描述符传递)特别有用。
  
  说明
在这里插入图片描述

  
  
  返回值:

RETURN VALUE
       These calls return the number of bytes received, or -1 if an error occurred.  In the  event  of  an
       error,  errno is set to indicate the error.  The return value will be 0 when the peer has performed
       an orderly shutdown.

  
  
  由于演示UDP套接字编程,这里我们介绍recvfrom:

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

  sockfd,即套接字(socket)
  buf、len:读取数据,就要有对应缓冲区来接收(缓冲区及缓冲区大小)。
  flags:读取数据的方式,默认0为阻塞读取

  src_addr纯输出型参数。 除了接收数据,服务端也想知道给它发消息的客户端谁(即通讯是双向的,有来有回)。故这里的参数是为了获取客户端IP和端口号。src_ ip源IP、src_ port源端口号。
  addrlen输入输出型参数。 输入时,一般填充src_addr的大小。输出时,会被设置为实际读到的src_addr大小。
  
  
  
  2)、recvfrom:服务器不断接收来自客户端的数据
  首次演示,这里接收、发送的都是字符串数据,处理时也只是在服务端打印于显示器上。PS:这里服务端具体做什么业务处理,要根据需求而定。

        //1、作为一款网络服务器,永远不退出地在接收客服端通过网络传来的请求
        while(true)
        {
            //1.1、准备工作:
            //a、用于后续从网络中读取客户端的IP、端口号
            struct sockaddr_in clientaddr;
            bzero(&clientaddr,sizeof clientaddr);
            socklen_t len = sizeof clientaddr;//输入输出型参数:输入时传递的是clientaddr当前定义的大小,使用是会输出实际大小
            //b、用于存储数据
            char server_buffer[SIZE];

            //1.2、读取数据:
            ssize_t s = recvfrom(sock_,server_buffer,strlen(server_buffer)-1,0,(struct sockaddr*)&clientaddr,&len);//注意对clientaddr类型转换
            if(s > 0 )//读取成功
            {
                server_buffer[s]='\0';//或 = 0

                //a、获取客户端端口号、IP
                uint16_t client_port = ntohs(clientaddr.sin_port);
                std::string client_ip = inet_ntoa(clientaddr.sin_addr);
                printf("[%s:%d]# %s\n",client_ip.c_str(),client_port,server_buffer);

                //b、处理客服端发来的数据请求(自定义TODO)

            }
        }

  
  补充说明: 上述打印时,我们为了观察,一并获取了客户端的端口号和IP地址,这些数据来源于网络,要在当前进程(服务端)中使用,同样存在网络字节序和主机字节序转换的问题。
  
  
  ntohs:网络字节序转换的函数。端口号从网络中读取,需要将网络字节序转换为主机字节序(这里是服务端)

SYNOPSIS
       #include <arpa/inet.h>
       
       uint32_t ntohl(uint32_t netlong);

       uint16_t ntohs(uint16_t netshort);
       
DESCRIPTION
       The  ntohl()  function  converts  the unsigned integer netlong from network byte order to host byte
       order.

       The ntohs() function converts the unsigned short integer netshort from network byte order  to  host
       byte order.

  
  inet_ntoa:inet_addr的反向转换,对IP地址使用(注意其参数类型)。①网络字节序到服务器字节序;②4字节到点分十进制风格;

SYNOPSIS
       #include <sys/socket.h>
       #include <netinet/in.h>
       #include <arpa/inet.h>

       char *inet_ntoa(struct in_addr in);

DESCRIPTION
       The inet_ntoa() function converts the Internet host address in, given in network byte order,  to  a
       string  in  IPv4 dotted-decimal notation.  The string is returned in a statically allocated buffer,
       which subsequent calls will overwrite.

  
  
  
  

3.1.3.2、sendto

  1)、相关函数介绍

NAME
       send, sendto, sendmsg - send a message on a socket

SYNOPSIS
       #include <sys/types.h>
       #include <sys/socket.h>

       ssize_t send(int sockfd, const void *buf, size_t len, int flags);

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

       ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

DESCRIPTION
       The system calls send(), sendto(), and sendmsg() are used to transmit a message to another socket.

       If  sendto()  is  used  on  a  connection-mode  (SOCK_STREAM, SOCK_SEQPACKET) socket, the arguments
       dest_addr and addrlen are ignored (and the error EISCONN may be returned when they are not NULL and
       0),  and the error ENOTCONN is returned when the socket was not actually connected.  Otherwise, the
       address of the target is given by dest_addr with addrlen specifying its size.  For  sendmsg(),  the
       address of the target is given by msg.msg_name, with msg.msg_namelen specifying its size.
       For  send()  and sendto(), the message is found in buf and has length len. 

  send函数用于向已连接的套接字发送数据。对于基于连接的套接字(如TCP),这是常用的函数。
  sendto函数用于向无连接的套接字(如UDP)发送数据。由于UDP是无连接的,因此您需要指定数据的目标地址,这就是为什么此函数需要dest_addr和addrlen参数的原因。
  sendmsg函数提供了更高级别的消息发送功能,允许您发送带有控制消息(如文件描述符)的数据,并可以处理更复杂的目标地址。这对于处理套接字选项和扩展功能(如UNIX域套接字上的文件描述符传递)特别有用。
  
  返回值:

RETURN VALUE
       On success, these calls return the number of characters sent.  On error, -1 is returned, and errno  is
       set appropriately.

  参数说明:
  sockfd:用于通信的套接字
  buf、len:需要发送的数据及其大小
  flags:发送的方式,默认可设置为0
  dest_addr:用于存储发送对象的IP和端口号
  addrlen:对应dest_addr的大小。
  
  
  
  
  2)、响应:将处理好的结果返回
  下述演示中,服务端只是接收数据将其显示于服务端,并将其接收到的信息原封不动传回给客户端,也就是echo版服务器。

            // 2、响应:将处理好的结果返回。
            sendto(sock_, server_buffer, sizeof server_buffer, 0, (struct sockaddr *)&clientaddr, len);

  关于sendto的参数dest_addr,因先前获取到的clientaddr中本身就按照网络的需求设置好了,故不用再根据client_portclient_ip 进行序列转换。
  
  
  
  
  

3.1.4、该部分整体框架(测试一:echo版服务器)

3.1.4.1、相关代码

  这里还会随着后续通讯的业务处理需求不断调整改变。

#ifndef _UDP_SERVER_HPP
#define _UDP_SERVER_HPP

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
#include <memory>
#include <cstdlib>
#include <cerrno>
#include <cstring>
#include "log.hpp"

#define SIZE 1024 // 服务端缓冲区大小

class UdpServer
{
public:
    // 构造:将对应的端口号、IP传入
    UdpServer(uint16_t port, std::string ip = "") // 对ip默认值说明:方便后续bind操作,1、可从任意IP获取数据(默认情况),2、也可指定需要的IP(自己传入参数的情况)
        : port_(port), ip_(ip), sock_(-1)
    {
    }

    // 析构:关闭套接字
    ~UdpServer()
    {
        if (sock_ >= 0)
            close(sock_);
    }

    // 初始化服务器
    bool InitServer()
    {
        // 1、创建套接字:此处AF_INET也可以是FP_INET
        sock_ = socket(AF_INET, SOCK_DGRAM, 0);
        if (sock_ < 0)
        {
            logMessage(ERROR, "%d:%s ", errno, strerror(errno));
            exit(2);
        }
        logMessage(DEBUG, "创建套接字成功, sock: %d ", sock_);

        // 2、bind绑定:
        // 2.1、绑定前的准备工作:
        struct sockaddr_in localaddr;
        bzero(&localaddr, sizeof localaddr); // 将结构体清零
        localaddr.sin_family = AF_INET; // 告知通讯方式,通常与domain同
        localaddr.sin_port = htons(port_); // 端口号:注意转为网络字节序
        localaddr.sin_addr.s_addr = ip_.empty() ? INADDR_ANY : inet_addr(ip_.c_str()); // IP:点分十进制->四字节+网络字节序
        // 2.2、绑定:
        if (bind(sock_, (struct sockaddr *)&localaddr, sizeof localaddr) < 0) // 注意对localaddr类型转换
        {
            logMessage(ERROR, "%d:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(DEBUG, "绑定成功,初始化服务器完成!");

        return true;
    }

    // 启动服务器
    void Start()
    {

        // 1、作为一款网络服务器,永远不退出地在接收客服端通过网络传来的请求
        while (true)
        {
            // 1.1、准备工作:
            // a、用于后续从网络中读取客户端的IP、端口号
            struct sockaddr_in clientaddr;
            bzero(&clientaddr, sizeof clientaddr);
            socklen_t len = sizeof clientaddr; // 输入输出型参数:输入时传递的是clientaddr当前定义的大小,使用是会输出实际大小
            // b、用于存储数据
            char server_buffer[SIZE];

            // 1.2、读取数据:
            ssize_t s = recvfrom(sock_, server_buffer, sizeof(server_buffer) - 1, 0, (struct sockaddr *)&clientaddr, &len); // 注意对clientaddr类型转换
            if (s > 0) // 读取成功
            {
                server_buffer[s] = 0;

                // a、获取客户端端口号、IP:因为是从网络中获取,这里本地显示时需要转换字节序和风格
                uint16_t client_port = ntohs(clientaddr.sin_port);
                std::string client_ip = inet_ntoa(clientaddr.sin_addr);
                printf("[%s:%d]# %s\n", client_ip.c_str(), client_port, server_buffer);

                // b、处理客服端发来的数据请求(自定义TODO)
            }
            // 2、响应:将处理好的结果返回。
            sendto(sock_, server_buffer, strlen(server_buffer), 0, (struct sockaddr *)&clientaddr, len);
            
        }
    }




private:
    uint16_t port_;  // 端口号:16位的整数
    std::string ip_; // IP地址:点分十进制字符串风格
    int sock_;       // 通讯时的套接字:需要供多处使用
};

#endif

  
  
  

3.1.4.2、验证:(netstat指令引入)

  netstat -anup:可查看当前存在的所有UDP。(PS:该指令的相关学习见网络基础四

在这里插入图片描述

  127.0.0.1:本地环回,client和server发送数据只在本地协议栈中进行数据流动,不会把数据发送到网络中。(通常用于本地网络测试,若使用127.0.0.1可正常通讯,而连接其它网络无法通信,大概率是网络问题)。

在这里插入图片描述

  
  
  
  
  
  
  

3.2、服务端和客户端

3.2.1、udp_server.cc

  关于“启动服务端是否需要在命令行参数传入IP地址”的问题说明:
  ①云服务器无法bind公网IP,也不建议。
  ②对server服务器来讲,也不推荐bind确定的IP(若有需要可使用固定IP,若无需求默认分配即可,上述INADDR_ANY有相关介绍)。

#include "udp_server.hpp"
#include <memory>

//使用手册:当命令行输入错误时,可提示正确启动信息
void Usage(std::string proc)
{
    std::cout << "\nUsage:" << proc << " port\n" << std::endl;
}

// 服务端:启动服务端所需指令 upd_server.cc port
int main(int argc, char **argv)
{
    // 1、检测输入指令是否正确:关系到服务器启动
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }
    
    // 2、获取端口号
    uint16_t server_port=atoi(argv[1]);

    // 3、使用智能指针来管理服务器
    std::unique_ptr<UdpServer> server(new UdpServer(server_port));
    server->InitServer();//初始化服务器
    server->Start();//启动服务器

    return 0;
}

  
  
  
  
  

3.2.2、udp_client.cc

  问题:客户端需不需要bind?
  回答:需要,但一般不会显示bind一个指定的port,而是由OS自动随机选择。(原因:client若绑定了固定的IP和port,其它应用程序的客户端站用该Port时,岂非启动不了。)

#include <iostream>
#include <unistd.h>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#include "log.hpp"
#define SIZE 1024

// 使用手册:当命令行输入不正确时,可提示正确使用信息
void Usage(std::string proc)
{
    std::cout << "\nUsage:" << proc << " server_ip server_port\n"
              << std::endl;
}

// 启动方式:udp_client server_ip server_port要知道服务端的端口号和IP地址
int main(int argc, char **argv)
{
    // 1、检测命令行参数是否输入正确
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }

    // 2、创建套接字
    int sock = socket(AF_INET, SOCK_DGRAM, 0);//这里是UDP通信
    if (sock < 0) // 创建失败
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }
    logMessage(DEBUG, "socket succes. sock: %d", sock);

    // 3、进行网络通信
    // 3.1、通信前的准备:获取服务端的IP、端口号
    struct sockaddr_in serveraddr;
    bzero(&serveraddr, sizeof serveraddr);
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(atoi(argv[2])); // 要向服务器发送数据,要先经过网络,故这里存在字节序的转换(使用与server同)
    serveraddr.sin_addr.s_addr = inet_addr(argv[1]);

    // 3.2、循环式向服务端发出请求并接收响应结果
    // PS:首次测试,演示字符型数据传递,echo服务器
    while (true)
    {
        // a、向服务器发送请求
        std::string message; // 将交付的信息数据
        std::getline(std::cin, message);
        if (message == "quit") // string类中有operator==,建议访问官网查阅
            break;

        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&serveraddr, sizeof serveraddr);

        // b、接收来自服务器的响应
        char client_buffer[SIZE]; // 用于存储数据
        struct sockaddr_in temp;  // 临时变量:用于recvfrom中参数(为保证该函数成功使用)
        memset(&temp, 0, sizeof temp);
        socklen_t len = sizeof temp;
        ssize_t s = recvfrom(sock, client_buffer, sizeof client_buffer, 0, (struct sockaddr *)&temp, &len);
        if (s > 0) // 接收到实际数据
        {
            client_buffer[s] = 0;
            // echo版服务器,相关后续处理:打印到显示器表示服务端有确切地将数据通过网络sendto到客户端
            std::cout << "server echo : " << client_buffer << std::endl;
        }
    }

    return 0;
}

  
  
  
  
  
  
  

3.3、测试二:多进程版指令执行

3.3.1、相关函数介绍:popen、strcasestr

  需求说明: 客户端发送指令数据,服务端接收,执行指令,并将其结果返回给客户端。
在这里插入图片描述

  
  
  1)、popen
  如何实现: 从命令行参数接收指令并执行,该操作我们在进程控制中学习过。服务端可创建管道,创建子进程,让子进程从管道中读取指令,进行替换执行相关指令。这里我们介绍一个函数,将上述操作一步到位:popen

NAME
       popen, pclose - pipe stream to or from a process

SYNOPSIS
       #include <stdio.h>

       FILE *popen(const char *command, const char *type);

       int pclose(FILE *stream);

  函数说明: 1、创建管道pipe(),创建子进程fork()exec*进程替换执行相关指令command。2、FILE*返回值:将执行结果通过FILE*指针读取。

DESCRIPTION
       The popen() function opens a process by creating a pipe, forking, and invoking the shell.  Since a pipe is by
       definition unidirectional, the type argument may specify only reading or writing,  not  both;  the  resulting
       stream is correspondingly read-only or write-only.

       The  command argument is a pointer to a null-terminated string containing a shell command line.  This command
       is passed to /bin/sh using the -c flag; interpretation, if any, is performed by the shell.  The type argument
       is  a  pointer to a null-terminated string which must contain either the letter 'r' for reading or the letter
       'w' for writing.  Since glibc 2.9, this argument can additionally include the letter 'e',  which  causes  the
       close-on-exec  flag  (FD_CLOEXEC)  to  be  set  on the underlying file descriptor; see the description of the
       O_CLOEXEC flag in open(2) for reasons why this may be useful.

       The return value from popen() is a normal standard I/O stream in all respects save that  it  must  be  closed
       with  pclose()  rather than fclose(3).  Writing to such a stream writes to the standard input of the command;
       the command's standard output is the same as that of the process that called popen(), unless this is  altered
       by  the command itself.  Conversely, reading from a "popened" stream reads the command's standard output, and
       the command's standard input is the same as that of the process that called popen().

       Note that output popen() streams are fully buffered by default.

       The pclose() function waits for the associated process to terminate and returns the exit status of  the  com‐
       mand as returned by wait4(2).

RETURN VALUE
       The popen() function returns NULL if the fork(2) or pipe(2) calls fail, or if it cannot allocate memory.

       The pclose() function returns -1 if wait4(2) returns an error, or some other error is detected.  In the event
       of an error, these functions set errnro to indicate the cause of the error.

  
  
  
  2)、strcasestr
  说明: 为了防止客户端发送来rmrmdir等系列指令,需要进行一层过滤处理。可以使用此函数,其和strstr类似,只是在找字串时,忽略大小写。

NAME
       strstr, strcasestr - locate a substring

SYNOPSIS
       #include <string.h>

       char *strstr(const char *haystack, const char *needle);

       #define _GNU_SOURCE         /* See feature_test_macros(7) */

       #include <string.h>

       char *strcasestr(const char *haystack, const char *needle);

DESCRIPTION
       The  strstr() function finds the first occurrence of the substring needle in the string haystack.  The termi‐
       nating null bytes ('\0') are not compared.

       The strcasestr() function is like strstr(), but ignores the case of both arguments.

RETURN VALUE
       These functions return a pointer to the beginning of the substring, or NULL if the substring is not found.

  
  
  
  

3.3.2、代码实现与结果演示

  实则此部分只需要变动udp_server.hpp中,业务处理部分的逻辑。


    // 2、测试二:演示指令执行
    // 启动服务器
    void Start()
    {

        // 1、作为一款网络服务器,永远不退出地在接收客服端通过网络传来的请求
        while (true)
        {
            // 1.1、准备工作:
            // a、用于后续从网络中读取客户端的IP、端口号
            struct sockaddr_in clientaddr;
            bzero(&clientaddr, sizeof clientaddr);
            socklen_t len = sizeof clientaddr; // 输入输出型参数:输入时传递的是clientaddr当前定义的大小,使用是会输出实际大小
            // b、用于存储数据
            char server_buffer[SIZE];

            // 1.2、读取数据:
            ssize_t s = recvfrom(sock_, server_buffer, sizeof(server_buffer) - 1, 0, (struct sockaddr *)&clientaddr, &len); // 注意对clientaddr类型转换
            std::string respond;//用于业务处理后,将结果sendto发送给客户端
           
            if (s > 0) // 读取成功
            {
                server_buffer[s] = 0;

                // a、获取客户端端口号、IP:因为是从网络中获取,这里本地显示时需要转换字节序和风格
                uint16_t client_port = ntohs(clientaddr.sin_port);
                std::string client_ip = inet_ntoa(clientaddr.sin_addr);
                

                // b、处理客服端发来的数据请求(指令执行):读取指令、子进程执行、结果返回
                //

                // b-1:检测读取到的指令是否为rm系列
                if (strcasestr(server_buffer, "rm") != nullptr || strcasestr(server_buffer, "rmdir") != nullptr)
                {
                    std:: string respond = "Wrong:该系列指令无效处理!\n";
                    std::cout<< respond << "---> 客户端输入指令为: " << server_buffer << std::endl;

                    sendto(sock_,respond.c_str(), respond.size(), 0, (struct sockaddr *)&clientaddr, len);
                    continue;
                }
                // b-2:执行其它指令
                char result[SIZE];//从fd(popen返回值)中读取执行后的结果

                FILE* fd = popen(server_buffer,"r");//type选项只可以是读或写,不可以同时进行(详细情查看函数)
                if (fd == nullptr)// 读取失败
                {
                    logMessage(ERROR, "popen: %d-%s", errno, strerror(errno));
                    continue;
                }
                while (fgets(result, sizeof result, fd) != nullptr)
                {
                    respond += result;
                }
                pclose(fd);
                //

            }
            // 2、响应:将处理好的结果返回。
            sendto(sock_, respond.c_str(), respond.size(), 0, (struct sockaddr *)&clientaddr, len);
            
        }
    }

  演示结果:
在这里插入图片描述

  
  
  
  

3.4、测试三:多线程版简易聊天系统

  需求说明: 当某一用户发送消息时,将该消息推送给所有用户。
在这里插入图片描述

3.4.1、server.hpp

  改动说明:同理,只需要改变void Start()里业务处理部分的内容。注意需要将结果一一传回。

#ifndef _UDP_SERVER_HPP
#define _UDP_SERVER_HPP

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unordered_map>
#include <string>
#include <memory>
#include <cstdlib>
#include <cerrno>
#include <cstring>
#include "log.hpp"

#define SIZE 1024 // 服务端缓冲区大小




//演示三:简易版聊天系统(多线程模式)
class UdpServer
{
public:
    // 构造:将对应的端口号、IP传入
    UdpServer(uint16_t port, std::string ip = "") // 对ip默认值说明:方便后续bind操作,1、可从任意IP获取数据(默认情况),2、也可指定需要的IP(自己传入参数的情况)
        : port_(port), ip_(ip), sock_(-1)
    {
    }

    // 析构:关闭套接字
    ~UdpServer()
    {
        if (sock_ >= 0)
            close(sock_);
    }

    // 初始化服务器
    bool InitServer()
    {
        // 1、创建套接字:此处AF_INET也可以是FP_INET
        sock_ = socket(AF_INET, SOCK_DGRAM, 0);
        if (sock_ < 0)
        {
            logMessage(ERROR, "%d:%s ", errno, strerror(errno));
            exit(2);
        }
        logMessage(DEBUG, "创建套接字成功, sock: %d ", sock_);

        // 2、bind绑定:
        // 2.1、绑定前的准备工作:
        struct sockaddr_in localaddr;
        bzero(&localaddr, sizeof localaddr); // 将结构体清零
        localaddr.sin_family = AF_INET; // 告知通讯方式,通常与domain同
        localaddr.sin_port = htons(port_); // 端口号:注意转为网络字节序
        localaddr.sin_addr.s_addr = ip_.empty() ? INADDR_ANY : inet_addr(ip_.c_str()); // IP:点分十进制->四字节+网络字节序
        // 2.2、绑定:
        if (bind(sock_, (struct sockaddr *)&localaddr, sizeof localaddr) < 0) // 注意对localaddr类型转换
        {
            logMessage(ERROR, "%d:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(DEBUG, "绑定成功,初始化服务器完成!");

        return true;
    }

 
    // 启动服务器
    void Start()
    {

        // 1、作为一款网络服务器,永远不退出地在接收客服端通过网络传来的请求
        while (true)
        {
            // 1.1、准备工作:
            // a、用于后续从网络中读取客户端的IP、端口号
            struct sockaddr_in clientaddr;
            bzero(&clientaddr, sizeof clientaddr);
            socklen_t len = sizeof clientaddr; // 输入输出型参数:输入时传递的是clientaddr当前定义的大小,使用是会输出实际大小
            // b、用于recvfrom时存储数据
            char server_buffer[SIZE];
            // c、用于业务处理时,记录当前发送消息的客户端:ip-prot(unordered_map的key值)
            char key[64];

            // 1.2、读取数据:
            ssize_t s = recvfrom(sock_, server_buffer, sizeof(server_buffer) - 1, 0, (struct sockaddr *)&clientaddr, &len); // 注意对clientaddr类型转换
       
           
            if (s > 0) // 1.3、读取成功,进行业务处理
            {
                server_buffer[s] = 0;

                // a-1、获取客户端端口号、IP:因为是从网络中获取,这里本地显示时需要转换字节序和风格
                uint16_t client_port = ntohs(clientaddr.sin_port);
                std::string client_ip = inet_ntoa(clientaddr.sin_addr);

                // a-2:检测本次发送消息的客户端是否存在_users中,若无,则需要添加进来
                snprintf(key, sizeof key, "[%s-%d]", client_ip.c_str(), client_port);
                logMessage(DEBUG, "key:%s send a massage to server.", key);
                auto ifexist = _users.find(key); // iterator find ( const key_type& k );
                if(ifexist ==_users.end())// unordered_map::end 
                {
                    // 说明_users中没有记录该客户端,需要插入
                    _users.insert({key, clientaddr});
                    logMessage(DEBUG, "add a new user: %s .", key);
                }
                //

            }
            // 2、响应:将处理好的结果返回。
            // c-1:遍历_users,挨个推送消息
            for (auto &iter : _users)
            {
                // 传回格式: ip-prot# XXXXXX ,例如:[127.0.0.1-8080]# 你好!
                std::string respond = key;
                respond += "# ";
                respond += server_buffer;
                logMessage(DEBUG,"push message to: %s .",key);
                sendto(sock_, respond.c_str(), respond.size(), 0, (struct sockaddr *)&(iter.second), sizeof(iter.second));
            }
        }
    }



private:
    uint16_t port_;  // 端口号:16位的整数
    std::string ip_; // IP地址:点分十进制字符串风格
    int sock_;       // 通讯时的套接字:需要供多处使用
    std::unordered_map<std::string, struct sockaddr_in> _users;
    //用于记录连接上服务器的所有客户端:[ip-prot,sockaddr结构体],即[字符串信息显示的IP和端口号,实际用于获取IP和端口号的结构]
};

#endif

  
  
  

3.4.2、udp_client.cc

  说明:引入了多线程,将读取数据和接收数据分开处理。(线程部分这里直接使用了之前在生产者消费者模式中写过的封装类)。

#include <iostream>
#include <unistd.h>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <memory>
#include "thread.hpp"
#include "log.hpp"
#define SIZE 1024

//演示三:简易版聊天系统(多线程模式)

//服务器IP、Port:
//由于客户端我们没有封装,而这两个参数在线程中需要,一种方法是写类进行处理,另一种方法是定义成全局函数
//不存在线程安全问题,因为我们只是使用它,不是对它进行修改。(将sock当作一个文件,UDP是全双工的,可以同时进行收发而不受干扰)
in_addr_t server_ip;
uint16_t server_port;

// 使用手册:当命令行输入不正确时,可提示正确使用信息
void Usage(std::string proc)
{
    std::cout << "\nUsage:" << proc << " server_ip server_port\n"
              << std::endl;
}

// 用于向服务端发送数据消息
void *udpSend(void *pdata)
{
    // 1、通信前的准备:
    // a、获取args参数:
    int sock = *(int*)(((ThreadData *)pdata)->_args);


    // b、获取服务端的IP、端口号
    struct sockaddr_in serveraddr;
    bzero(&serveraddr, sizeof serveraddr);
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = server_port; // 这里已经在main函数中做了转换
    serveraddr.sin_addr.s_addr = server_ip;

    // 2、客户端从显示器中输入数据,将其发送给服务端:sendto(循环式发送)
    while (true)
    {
        string message;
        //std::cout << "client-请输入# ";
        std::cerr << "client-请输入# ";

        std::getline(std::cin, message);
        if (message == "quit") // string类中有operator==,建议访问官网查阅
            break;
        // 当client首次发送消息给服务器的时候,OS会自动给client bindIP和PORT.
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&serveraddr, sizeof serveraddr);
    }
}

// 用于从服务端接收数据信息
void* udpRecv(void* pdata)
{

    // 1、通信前的准备:
    // a、获取args参数
    int sock = *(int*)(((ThreadData*)pdata)->_args);

    // 2、recvfrom循环式接收服务器发送过来的数据
    while(true)
    {
                   
        char client_buffer[SIZE]; // 用于存储数据
        struct sockaddr_in temp;  // 临时变量:用于recvfrom中参数(为保证该函数成功使用)
        memset(&temp, 0, sizeof temp);
        socklen_t len = sizeof temp;
        ssize_t s = recvfrom(sock, client_buffer, sizeof client_buffer, 0, (struct sockaddr *)&temp, &len);
        if (s > 0) // 接收到实际数据
        {
            client_buffer[s] = 0;
            std::cout << client_buffer << std::endl;
        }
    }
}

// 启动方式:udp_client server_ip server_port
int main(int argc, char **argv)
{
    // 1、检测命令行参数是否输入正确
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    //获取服务器的IP、套接字,将其转换为网络传送需要的套接字
    server_ip = inet_addr(argv[1]);//点分十进制字符串风格--->网络字节序+4字节
    server_port = htons(atoi(argv[2]));//字符型--->网络字节序+整型

    // 2、创建套接字
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0) // 创建失败
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }
    logMessage(DEBUG, "socket succes. sock: %d", sock);


    // 3、创建两个线程,分别用于管理客户端的[发送消息]和[接收消息]
    std::unique_ptr<Thread> sender(new Thread(1,udpSend,(void*)&sock));
    std::unique_ptr<Thread> recever(new Thread(2,udpRecv,(void*)&sock));
    sender->start();
    recever->start();

    sender->join();
    recever->join();

    // 4、结束:关闭套接字
    close(sock);

    return 0;
}

  
  
  

3.4.3、演示结果、thread.hpp

3.4.3.1、演示结果

  以下为当个客户端时的结果演示:
在这里插入图片描述

  多个客户端时的结果演示:

在这里插入图片描述

  PS:这些演示都是在同一主机下进行,若要网络通信,收发消息的主机不同,可将客户端程序传送给其它主机,让其拿着服务器的端口号和IP地址运行程序即可。(需要开放服务器所在主机的端口号)
  
  
  

3.4.3.2、thread.hpp

  实则上述客户端部分还可以进行封装处理。

#pragma once
#include<iostream>
#include<string>
#include<pthread.h>

using namespace std;
typedef void* (*func_t)(void*);//函数指针:此处用于线程表示线程的执行函数


//args传参设置:设置成类,增加args传参选择
class ThreadData
{
public:
    string _name;//对应线程名称
    void* _args;//对应线程回调函数中args参数
};




class Thread
{
public:
    Thread(int inode, func_t rountine, void* args)
    :_routine_func(rountine)//注意:这里线程的执行函数、参数args都是需要通过外部传入的
    {
        char buffer[64]="";
        snprintf(buffer, sizeof(buffer), "thread-%d",inode);
        _name = buffer;

        _tdata._args = args;
        _tdata._name = _name;
    }

    ~Thread()
    {}

    void start()//启动线程:用于创建线程,构造函数只是做了线程名称、ID等各参数设置,实则并未真正创建出线程
    {
        pthread_create(&_tid, nullptr, _routine_func, (void*)&_tdata);
    }

    void join()//终止线程
    {
        pthread_join(_tid, nullptr);
    }

private:
    string _name;//线程名
    pthread_t _tid;//线程ID
    func_t _routine_func;//线程的执行函数
    ThreadData _tdata;//传入回调函数的参数(这里做了封装)
};

  
  
  
  
  
  
  
  
  
  
  
  
  
  
  

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值