套接字编程 --- 一

目录

1. 预备知识

1.1. 端口号

1.2. 认识TCP协议

1.3. 认识UDP协议

1.4. 网络字节序

2. socket

2.1. socket 常见系统调用

2.1.1. socket 系统调用

2.1.2. bind 系统调用

 2.1.3. recvfrom 系统调用

2.1.4. sendto系统调用 

2.3. 其他相关接口 

2.3.1. bzero

2.3.2. 网络字节序和主机字节序的相关转换接口

2.3.2. IPV4地址信息的转换处理

2.4. sockaddr结构

通用文件 

1. Log.hpp 日志

2. Date.hpp 时间处理

3. Makefile

3. UDP demo1 

3.1. Udp_Server.cc

3.2. Udp_Server.hpp

3.3. Udp_Client.cc

3.4. 细节总结


1. 预备知识

在网络基础中,我们已经知道了IP地址。再IP数据包中,有两个IP地址,分别叫做源IP地址,目的IP地址, 再数据包转发过程中,源IP和目的IP地址通常不会发生改变。

IP地址 (公网IP) 用来标识主机的唯一性。

通常情况下,当有了IP地址,主机就可以将数据发送给另一台主机,可是,把数据发送给另一台主机是通信的目的吗? 

答案:并不是,一般的应用级软件,都会有用户客户端软件和服务器软件。而客户端软件和服务器软件本质上不就是进程吗? 因此,客户端软件我们称之为客户端进程,服务器软件就是服务器进程。

以抖音客户端和服务器为例,当用户在手机端的抖音客户端软件上访问时,实际上是在与抖音服务器上运行的特定进程通信,向服务器发送请求并接收响应,从而获取和共享视频内容。

互联网上的通信本质上是由运行在不同计算机上的进程(或者称为应用程序)之间的通信

因此,网络通信的目的是确保不同计算机上的进程能够相互通信和交换数据底层的网络传输过程(比如IP数据包的转发)只是为了实现这一目的而采取的手段,真正的通信实体是运行在计算机上的进程。网络通信确保数据能够从一个进程传输到另一个进程,从而实现用户所期望的功能和服务。

现在,我们知道,网络通信本质上还是进程间通信,但又由于,网络通信是跨主机的,因此在进程间通信之前,我们需要完成主机间的数据转发,而这是为了达到进程间通信这一目的手段。

当我们完了主机间的数据转发之后,就需要将该数据发送给指定的进程, 可是问题来了, 如何确定这个进程呢?

因此需要端口号,端口号就是解决如何定位目标进程的问题。

1.1. 端口号

端口号 (port) 是传输层协议的内容。

端口号是一个2字节(16位)的整数。

端口号用来标识特定主机上的唯一的一个进程。

而IP地址标识了主机的唯一性,因此IP地址 + 端口号就可以标识网络中某一台机器中的某一个进程,且是全网唯一的。

未来, 任何一个发出的报文,必须包含: IP地址,port端口号。

一个端口号只能绑定一个进程, 因为要标识进程的唯一性,但是一个进程可以绑定多个端口号。

在我们学习系统编程时,也说过进程的PID,其也是用来表示进程的唯一性啊,为什么这里不用PID来标识它的唯一性呢?

首先,PID是进程管理模块的内容,如果网络也采用PID来标识进程唯一性,那么可能导致网络模块和进程管理模块的紧耦合,提高了系统复杂度;

其次,PID是在操作系统层面被分配和管理的,而网络通信可能涉及到多个主机和操作系统的情况。在不同操作系统和主机上,相同的进程可能拥有不同的PID,因此在网络通信中使用PID来唯一标识进程可能会带来差异化,提高了管理和维护成本。

相比之下,使用端口号来标识进程的唯一性更加灵活和可靠。端口号是通过网络套接字(socket)来管理的,属于网络通信相关的范畴,可以在不同操作系统和主机上保持一致。这样设计可以降低系统复杂度,达到功能解耦的目的,确保网络通信模块和进程管理模块之间的独立性。

因此,采用端口号而非PID来标识进程的唯一性是出于系统设计和功能解耦的考虑,符合模块化设计的原则,使得网络通信模块能够独立管理进程的通信需求,降低了系统的复杂度和耦合度。

传输层协议 (TCP和UDP) 的数据段中有两个端口号, 分别是源端口号和目的端口号。

源端口号是指发送数据的进程或应用程序所使用的端口号,表示数据的发送者

目的端口号则是数据需要发送到的进程或应用程序所使用的端口号,表示数据的接收者

通过源IP + 源端口号,可以锁定特定主机的唯一进程;

通过目的IP + 目的端口号, 可以锁定特定主机的唯一进程。

因此网络通信,本质上就是进程间通信。  

而我们将 { SRC_IP (源IP) , SRC_PORT (源端口号) } 称之为套接字!

{ DST_IP(目的IP) ,DST_PORT (目的端口号) } 也称之为套接字!

因此,我们也称之为套接字编程。

1.2. 认识TCP协议

我们这里只是初识 TCP(Transmission Control Protocol 传输控制协议) ,后面详细介绍。

  1. 传输层协议
  2. 有连接
  3. 可靠传输
  4. 面向字节流

1.3. 认识UDP协议

我们这里只是初识 UDP(User Datagram Protocol 用户数据报协议),后面详细介绍。

  1. 传输层协议
  2. 无连接
  3. 不可靠传输
  4. 面向数据报

在这里就只解释一点:

我们可以清楚的看到, TCP是可靠传输的,传输数据不会发生丢包问题;而UDP是不可靠传输的,传输数据可能发生丢包问题。 有人一听,那我们还学UDP干什么呢?

TCP协议提供可靠的数据传输,使用了各种机制来确保数据的准确性、完整性和顺序性,例如序列号、确认应答、重传等。这些机制在保证数据可靠性的同时,也增加了协议的复杂性和维护成本。TCP适用于对数据准确性要求较高的场景,例如文件传输、Web页面的请求和响应等。

而UDP协议则是一种不可靠的数据传输协议。它只提供了一种简单的数据传输机制,不具备重传、确认和流量控制等功能。UDP在传输过程中可能发生丢包、乱序等问题,但相应地,它的开销较小,处理逻辑简单,适合一些对实时性要求较高的场景。像直播、音视频传输等实时应用,对于偶尔的丢包用户可能会有一定的容忍度。

因此,对于不同的应用场景,选择TCP还是UDP取决于可靠性、实时性和处理成本的权衡。如果数据的完整性和顺序性是关键,且可以承受一定的处理成本,那么TCP是一个更好的选择。而如果实时性和传输效率更重要,且可以容忍一些数据丢失,那么UDP可能更适合。

1.4. 网络字节序

大端字节序: 数据的高位字节存储在低位地址,低位字节存储在高位地址。

小端字节序: 数据的低位字节存储在低位地址,高位字节存储在高位地址。

为什么要谈论这个问题呢? 

首先,我们知道,不同的计算机可能是以不同的字节序存储的 (大端 / 小端),  那么在进行主机数据转发时, 如果一方主机是小端存储,另一方主机是大端存储, 那么此时转发数据就会有问题,导致接收方可能无法获得正确信息。

因此, 为了解决这个问题,网络规定:所有网络数据都必须是大端的

相关接口:

#include <arpa/inet.h>

uint32_t htonl(uint32_t hostlong);     // 主机 ---> 网络  (uint32_t)
uint16_t htons(uint16_t hostshort);    // 主机 ---> 网络  (uint16_t)
uint32_t ntohl(uint32_t netlong);      // 网络 ---> 主机  (uint32_t)
uint16_t ntohs(uint16_t netshort);     // 网络 ---> 主机  (uint16_t)

这些函数名很好记,h表示host (本地/主机),n表示network (网络),l表示32位长整数,s表示16位短整数。
例如 htonl 表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。 

2. socket

2.1. socket 常见系统调用

2.1.1. socket 系统调用

int socket(int domain, int type, int protocol);
RETURN VALUE
On  success,  a file descriptor for the new socket is returned. 
On error, -1 is returned, and errno is set appropriately.

socket 函数是用于创建套接字(socket)的系统调用,其作用是在操作系统中创建一个套接字对象,以便进程能够通过网络进行通信。

  1. domain:指定套接字的协议域(protocol family),常见的有 AF_INET(IPv4 地址)和 AF_INET6(IPv6 地址)等,表示套接字将使用的是哪种地址类型,即代表着你想创建哪一类别的套接字(域间、网络套接字?)。
  2. type:指定套接字的类型,常见的有 SOCK_STREAM流式套接字,提供面向连接的、可靠的数据传输,如 TCP)和 SOCK_DGRAM数据报套接字,提供无连接的、不可靠的数据传输,如 UDP)等。
  3. protocol:指定协议类型,通常为 0 表示根据 domain 和 type 参数选择默认协议。

返回值

如果成功创建套接字,返回新的文件描述符 (file descriptor)

如果失败,返回 -1,并设置 errno变量以指明错误原因。

2.1.2. bind 系统调用

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

bind 函数是用于将一个本地地址(local address)绑定到一个已创建的套接字(socket)。

bind 函数的作用是告诉操作系统,将指定的本地地址与指定的套接字关联起来,使得该套接字可以使用该地址进行通信

一般在服务器端创建套接字后,需要使用 bind 函数将套接字与服务器端的特定 IP 地址和端口绑定在一起,以便客户端可以连接到该地址,并且服务器端可以接受客户端的连接请求。

参数:

  1. sockfd:要进行地址绑定的套接字的文件描述符。
  2. addr:指向要绑定的本地地址(sockaddr 结构体)的指针。 注意, 如果是网络套接字,那么需要使用 sockaddr_in,然后通过类型转换再传参。
  3. addrlen:表示本地地址结构体的长度。

返回值:函数执行成功时返回 0,否则返回 -1 并设置 errno 变量以指明错误原因。

 2.1.3. recvfrom 系统调用

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

 recvfrom 函数用于接收数据报文,并把数据存放到指定的缓冲区中该函数允许从指定的套接字接收数据,并同时获取数据发送方的地址信息。以下是对该函数的参数介绍:

  • sockfd: 表示要接收数据的套接字的文件描述符。
  • buf: 指向存放接收数据的缓冲区的指针。
  • len: 表示接收数据缓冲区的大小。
  • flags: 用于指定接收操作的额外选项,通常可以设为 0。如果 flags为0,表示采用默认的阻塞方式接收数据。在阻塞模式下,如果没有接收到数据,进程会一直等待直到接收到数据,否则函数调用会一直阻塞,直到有数据可读或者发生错误。
  • src_addr: 指向 socfaddr 结构体的指针,用于存放发送方的地址信息,如果是网络通信,一般是传递 socfaddr_in类型,然后强转为 sockaddr 类型 (输出型参数)。
  • addrlen: 一个指向整数的指针,在调用函数时指定发送方地址结构体的长度,接收时将被改变为实际的发送方地址结构体的长度 (可以理解为输入输出型参数),。

recvfrom 函数的作用是接收数据报文,一般用于 UDP 套接字的数据接收它从指定的套接字接收数据,并将数据存储在指定的缓冲区中,在接收数据的同时可以获取发送方的地址信息。通常在接收到数据后,可以通过 src_addr 和 addrlen 获取发送方的地址信息,以便进程进一步处理数据。

补充:

除了阻塞模式 (flags == 0),flags 还支持一些其他选项,如 MSG_DONTWAITMSG_WAITALL

MSG_DONTWAIT 表示采用非阻塞方式接收数据,即使当前没有数据可读也会立即返回;

MSG_WAITALL 表示需要一次性接收完所有的数据。

2.1.4. sendto系统调用 

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

sendto 函数用于向指定的套接字发送数据。它通过指定目标地址和目标端口号来将数据发送给对应的主机和进程

参数介绍

  1. sockfd:表示要发送数据的套接字的文件描述符。
  2. buf:指向存放要发送数据的缓冲区的指针。
  3. len:表示要发送数据的长度。
  4. flags:用于指定发送数据的额外选项,通常可以设为 0,表示阻塞的发送。
  5. dest_addr:指向 sockaddr 结构体的指针,用于指定目标地址和端口号。
  6. addrlen:整数类型,指定目标地址结构体的长度。

一般在使用前,我们需要对 dest_addr 进行初始化, 然后填充相关信息 (例如 sa_family、sin_addr、sin_port)。

sendto 函数的作用是向指定的套接字发送数据报文。它将缓冲区中的数据发送到指定套接字,并传递目标地址、目标端口号等信息以便于数据到达正确的目的地进程。一般在使用 UDP 协议时,可以使用该函数向其他主机发送数据报文。

2.3. 其他相关接口 

2.3.1. bzero

void bzero(void *ptr, size_t n);

bzero 将指定的一段空间的内容设置为0, 即将该内存块的每个字节都设置为0。

  1. ptr: 这是一个指向要清零的内存块的指针。
  2. n:要清零的内存块的大小,以字节为单位

2.3.2. 网络字节序和主机字节序的相关转换接口

#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 代表 net, 即 网路字节序。 

l  代表 long, 即 uint32_t。

s  代表 short, 即 uint16_t。

比如,htons, 就是将16位的数据从主机字节序转化为网络字节序。

如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
如果主机是大端字节序,这些函数不做转换,并将参数原封不动地返回。

2.3.2. IPV4地址信息的转换处理

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

in_addr_t inet_addr(const char *cp);
char *inet_ntoa(struct in_addr in);

inet_addr:这个函数用于将点分十进制表示的IPv4地址转换为网络字节顺序的32位二进制形式的IPv4地址

简而言之这个函数就做两件事:

1、 将点分十进制的数据转换为32位的整形地址。

2、 并将数据转换为网络字节序。

如果输入的IPv4地址字符串无效,函数将返回INADDR_NONE(通常是-1)。

inet_ntoa:该函数用于将一个网络字节序的32位整数(通常是表示IPv4地址的in_addr结构体中的s_addr成员)转换为主机字节顺序的点分十进制形式的字符串表示的IP地址

简而言之这个函数就做两件事:

1、 将32位的整形IP地址转化为点分十进制的IP。

2、 并将网络字节序转为主机序列。

2.4. sockaddr结构

  1. 域间套接字(AF_UNIX/AF_LOCAL套接字)

域间套接字也被称为UNIX套接字,是一种用于实现本地进程间通信的套接字可在同一台计算机上的进程之间传递数据。该套接字通过一个文件系统路径来标识,打开时它会创建一个文件,在通信结束后会自动将该文件删除。因此域间套接字通常被用在本地进程间的通信,比如X Window、数据库和Web服务器等。

  1. 原始套接字(Raw Socket)

原始套接字也被称为原始套接字,其可以接受和发送数据链路层数据包,允许用户构造自己的协议报文,适用于网络安全、网络监视和网络协议开发等方面。可以使用原始套接字来进行网络数据包的抓取、欺骗和注入等操作,同时也可以用于开发新的通信协议,网络协议栈的实现等。

  1. 网络套接字(AF_INET/AF_INET6套接字)

网络套接字也被称为Internet套接字,用于在网络上实现进程间通信,是Linux中最常用的一种套接字。网络套接字可协同使用传输层协议TCP和UDP,以及网络层协议IP和IPv6,用于实现应用层协议,例如HTTP、FTP、SMTP、SSH等。网络套接字的地址由IP地址和端口号组成,可以通过网络传递消息,实现分布式系统中的通信。

由于有三种套接字,理论上,是三种应用场景, 对应的应该是三套套接字接口!但是Linux不想设计过多的套接字接口!因此将所有的套接字接口进行了统一。

我们可以将 struct sockaddr 理解为一个基类。

struct sockaddr 类型前两个字节标识我是什么套接字,例如,如果前两个字节是AF_INET (本质上是一个宏),那么代表着是网络套接字; 如果前两个字节是AF_UNIT(本质上是宏),那么代表是域间套接字。

换言之,就好比通过 struct sockaddr 这个基类模拟多态。根据前两个字节确定是网络通信还是本地通信。

因此,在未来,因为我们编写的是网络套接字,那么我们使用的就是 struct sockaddr_in,通过类型转换传参给socket系统调用。 

如果要使用通用型接口,为什么不使用 void* 呢?

因为在设计出网路这套接口时, C语言还不支持 void*, 现在已经无法更改了(向前兼容);

通用文件 

1. Log.hpp 日志

#pragma once

#include "Date.hpp"
#include <iostream>
#include <map>
#include <string>
#include <cstdarg>

#define LOG_SIZE 1024

// 日志等级
enum Level
{
  DEBUG, // DEBUG信息
  NORMAL,  // 正常
  WARNING, // 警告
  ERROR, // 错误
  FATAL // 致命
};

void LogMessage(int level, const char* format, ...)
{
// 如果想打印DUBUG信息, 那么需要定义DUBUG_SHOW (命令行定义, -D)
#ifndef DEBUG_SHOW
  if(level == DEBUG)
    return ;
#endif
  std::map<int, std::string> level_map;
  level_map[0] = "DEBUG";
  level_map[1] = "NORAML";
  level_map[2] = "WARNING";
  level_map[3] = "ERROR";
  level_map[4] = "FATAL";

  std::string info;
  va_list ap;
  va_start(ap, format);

  char stdbuffer[LOG_SIZE] = {0};  // 标准部分 (日志等级、日期、时间)
  snprintf(stdbuffer, LOG_SIZE, "[%s],[%s],[%s] ", level_map[level].c_str(), Xq::Date().get_date().c_str(),  Xq::Time().get_time().c_str());
  info += stdbuffer;

  char logbuffer[LOG_SIZE] = {0}; // 用户自定义部分
  vsnprintf(logbuffer, LOG_SIZE, format, ap);
  info += logbuffer;

  std::cout << info ;
  fflush(stdout);
  va_end(ap);
}

2. Date.hpp 时间处理

没啥特别需要说明的点。

#ifndef __DATE_HPP_
#define __DATE_HPP_

#include <iostream>
#include <ctime>

namespace Xq
{
  class Date
  {
  public:
    Date(size_t year = 1970, size_t month = 1, size_t day = 1)
      :_year(year)
       ,_month(month)
       ,_day(day)
      {}

    std::string& get_date()
    {
      size_t num = get_day();
      while(num--)
      {
        operator++();
      }
      char buffer[32] = {0};
      snprintf(buffer, 32, "%ld/%ld/%ld", _year,_month, _day);
      _data = buffer;
      return _data;
    }

  private:
    Date& operator++()
    {
      size_t cur_month_day = month_day[_month];
      if((_month == 2) && ((_year % 400 == 0 )|| (_year % 4 == 0 && _year % 100 != 0)))
        ++cur_month_day;
      ++_day;
      if(_day > cur_month_day)
      {
        _day = 1;
        _month++;
        if(_month > 12)
        {
          _month = 1;
          ++_year;
        }
      }
      return *this;
    }

   // 获得从1970.1.1 到 今天相差的天数
    size_t get_day()
    {
      return (time(nullptr) + 8 * 3600) / (24 * 60 * 60);
    }

  private:
    size_t _year;
    size_t _month;
    size_t _day;
    static int month_day[13];
    std::string _data;
  };

  int Date::month_day[13] = {
    0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
  };

  class Time
  {

  public:
    Time(size_t hour = 0, size_t min = 0, size_t second = 0)
      :_hour(hour)
       ,_min(min)
       ,_second(second)
    {}

    std::string& get_time()
    {
      size_t second = time(nullptr) + 8 * 3600;
      _hour = get_hour(second);
      _min = get_min(second);
      _second = get_second(second);

      char buffer[32] = {0};
      snprintf(buffer, 32, "%ld:%ld:%ld", _hour, _min, _second);
      _time = buffer;
      return _time;
    }


  private:

    size_t get_hour(time_t second)
    {
      //  不足一天的剩余的秒数
      size_t verplus_second = second % (24 * 60 * 60);
      return verplus_second / (60 * 60);
    }

    size_t get_min(time_t second)
    {
      // 不足一小时的秒数
      size_t verplus_second = second % (24 * 60 * 60) % (60 * 60);
      return verplus_second / 60;
    }

    size_t get_second(time_t second)
    {
      // 不足一分钟的秒数
      return second % (24 * 60 * 60) % (60 * 60) %  60;
    }
    
  private:
    size_t _hour;
    size_t _min;
    size_t _second;
    std::string _time;
  };
}

#endif

3. Makefile

.PHONY:all
all:Client Server

Client:Udp_Client.cc
	g++ -o $@ $^ -std=gnu++11
Server:Udp_Server.cc
	g++ -o $@ $^ -std=gnu++11

.PHONY:clean
clean:
	rm -f Client Server

3. UDP demo1 

第一个版本:echo 服务器, 客户端向服务器发送消息, 服务端原封不动的返回给客户端。

3.1. Udp_Server.cc

#include "Udp_Server.hpp"

void standard_usage(void)
{
  printf("please usage: ./Server port\n");
}

int main(int argc, char* argv[])
{  
  // 服务端我们不用显式传递IP了, 默认用INADDR_ANY
  // 因此, 我们只需要两个命令行参数
  if(argc != 2)
  {
    standard_usage();
    exit(1);
  }

  // 传递端口号即可
  Xq::udp_server* server = new Xq::udp_server(atoi(argv[1]));
  server->init_server();
  server->start();

  delete server;
  return 0;
}

3.2. Udp_Server.hpp

#ifndef __UDP_SERVER_HPP_
#define __UDP_SERVER_HPP_

#include "Date.hpp"
#include "Log.hpp"
#include <iostream>
#include <string>
#include <cstring>

// 下面的这四个头文件,我们称之为网络四件套
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <unistd.h>

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

namespace Xq
{
	class udp_server
	{
	public:
		// 需要显示传递服务器的 port
		udp_server(uint16_t port, const std::string ip = "")
			:_ip(ip)
			, _port(port)
			, _sock(-1)
		{
		}

		void init_server(void)
		{
			//1. 创建套接字 --- socket
			// AF_INET 是一个宏值, 在这里代表着网络套接字
			// SOCK_DGRAM, 标定这是数据报套接字
			// protocol 默认情况下都是0
			_sock = socket(AF_INET, SOCK_DGRAM, 0);

			if (_sock == -1)
			{
				// 套接字创建失败对于网络通信而言是致命的
				LogMessage(FATAL, "%s\n", "socket failed");
				exit(1);
			}

			//2. 绑定端口号 --- bind

			// bind 将相应的ip和port在内核中与指定的进程强关联
			// 服务器跑起来就是一个进程, 因此需要通过
			// 服务器的IP + port 绑定服务器这个进程
			// 因此我们需要通过 sockaddr_in 设置地址信息
			struct sockaddr_in server;
			// 我们可以初始化一下这个对象
			// 通过bzero(), 对指定的一段内存空间做清0操作
			bzero(static_cast<void*>(&server), sizeof(server));

			// 初始化完毕后, 我们就需要填充字段
			// sockaddr_in 内部成员
			// in_port_t sin_port;  ---  对port的封装
			// struct in_addr sin_addr; --- 对ip的封装, 这里面的Ip实际上就是一个32位 (uint32_t) 的整数。
			// sin_family  sa_family; --- 如果我们是网络套接字, 那么填充 AF_INET

			// 我们要知道, 0.0.0.0 这种IP地址我们称之为"点分十进制" 字符串风格的IP地址
			// 每个点分割的区域数值范围 [0, 255];
			// 四个区域代表着四个字节, 理论上标识一个IP地址, 其实四字节就足够了
			// 点分十进制的字符串风格的IP地址是给用户使用的
			// 在这里我们需要将其转成32位的整数 uint32_t

			server.sin_family = AF_INET;

			// 当我们在网络通信时, 一方不仅要将自己的数据内容告诉对方
			// 还需要将自己的IP地址以及端口号告诉对方。
			// 即服务器的IP和端口号未来也是要发送给对方主机的特定进程(客户端进程)
			// 那么是不是我需要先将数据从 本地 发送到 网络呢?
			// 答案: 是的, 因此我们还需要注意不同主机内的大小端问题
			// 因此, 我们在这里统一使用网络字节序
			server.sin_port = htons(_port);

			// 而对于IP地址而言, 也是同理的
			// 只不过此时的IP地址是点分十进制的字符串
			// 因此我们需要先将其转为32位的整数, 在转化为网络字节序
			// 而 inet_addr() 这个接口就可以帮助我们做好这两件事

			//server.sin_addr.s_addr = inet_addr(_ip.c_str());

			// 作为 server 服务端来讲,我们不推荐绑定确定的IP,
			// 我们推荐采用任意IP的方案,即INADDR_ANY(是一个宏值), 本质就是((in_addr_t) 0x00000000)
			// 作为服务器, 我们可以不用暴露IP, 只暴露端口号即可。
			// 通常使用 INADDR_ANY 来 bind 服务器的套接字,从而使服务器能够接收来自任意IP地址的客户端连接
			// INADDR_ANY可以让服务器,在工作过程中,可以从任意IP中获取数据

			// 如果我们在服务器端 bind 了一个固定IP, 那么此时这个服务器就只能
			// 收取某个具体IP的消息, 但如果我们采用INADDR_ANY
			// 那么就是告诉操作系统, 凡是给该主机的特定端口(_port)的数据都给我这个服务端
			// 有了这样的认识之后,服务端只需要端口,不需要传递IP了 (默认设置为 INADDR_ANY)、

			server.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());

			// 填充 struct sockaddr_in 结束

			// 这里的 socklen_t 本质上就是 unsigned int
			// 得到这个缓冲区 (地址信息) 的大小
			socklen_t server_addr_len = sizeof(server);

			if (bind(_sock, reinterpret_cast<const struct sockaddr*>(&server), server_addr_len) == -1)
			{
				// 如果 bind 失败, 对于服务器而言是致命的
				LogMessage(FATAL, "%s\n", "bind error");
				exit(2);
			}

			// 初始化done
			LogMessage(NORMAL, "%s\n", "init_server success");
		}

		// 启动服务器 --- start
		// 第一个简单版本: echo 服务器, 客户端向服务器发送消息, 服务端原封不动的返回给客户端

		// 站在网络视角, 作为一款网络服务器, 永远不退出
		// 站在操作系统视角, 服务器本质上就是一个进程,
		// 因此对于这种永远不退出的进程我们也称之为常驻进程,
		// 永远在内存中存在, 除非系统挂了或者服务器宕机了。
		// 因此针对服务器我们要特别注意内存问题。绝不能内存泄露。

		void start(void)
		{
			char buffer[SER_BUFFER_SIZE] = { 0 };
			for (;;)
			{
				// 这里的 client 作 输出型参数, 当客户端发送数据给服务端时, 得到客户端的地址信息
				// 这里的 client_addr_len 作 输出(输入)型参数
				struct sockaddr_in client; 
				bzero(static_cast<void*>(&client), sizeof(client));
				socklen_t client_addr_len = sizeof(client);

				buffer[0] = 0;

				// 1. 读取客户端数据 --- recvfrom
				// 当服务器收到客户端发送的数据
				// 那么是不是服务端还需要将后续的处理结果返回给客户端呢?
				// 答案: 是的. 因此除了拿到数据之外, 服务端是不是还需要客户端的地址信息(IP + port)
				// 因此, 我们就可以理解为什么 recvfrom 系统调用会要后两个参数了
				// struct sockaddr *src_addr 是一个输出型参数, 用来获取客户端的地址信息
				// socklen_t *addrlen 是一个输入型参数、 输出型参数 如何理解
				// 输入型: 这个缓冲区 src_addr 的初始值大小,做输入型参数
				// 输出型: 这个缓冲区 src_addr 的实际值, 填充sockaddr_in的实际大小,做输出型参数
				// flags == 0 代表阻塞式的读取数据
				ssize_t real_read_size = recvfrom(_sock, buffer, SER_BUFFER_SIZE - 1, 0, \
					reinterpret_cast<struct sockaddr*>(&client), &client_addr_len);
				if (real_read_size > 0 /* 代表读取成功 */)
				{
					// 我们将这个数据当作字符串处理
					buffer[real_read_size] = 0;

					// 1. 获取发送方的地址信息, 即客户端的IP 和 port

					// 当我们通过recvfrom 成功读取了数据之后,
					// 那么我们可以获取发送方的信息 (对于服务端而言,那么发送方就是客户端,即客户端向服务端发送信息),
					// 但是这个数据是客户端通过网络发送过来的,其遵守网络字节序 (大端数据),
					// 因此我们需要将其由网络序列 转化为 主机序列 

					// 而recvfrom 中的clien (struct sockaddr_in), 不就是客户端的地址信息吗?
					// 因此我们提取client中的信息即可, 不过此时这个信息是网络字节序的

					// 获取客户端的IP地址, 我们用点分十进制的字符串表示
					// inet_ntoa 就可以将一个网络字节序的32位整形
					// 转换为主机序列的点分十进制字符串式的IP地址
					std::string client_ip = inet_ntoa(client.sin_addr);

					// 获取客户端的端口号, 需要从网络 -> 本地
					uint16_t client_port = ntohs(client.sin_port);

					// 2. 可以显示打印一下, 发送方 (在这里就是客户端的地址信息, IP + port),以及数据信息
					printf("client[%s][%d]: %s\n", client_ip.c_str(), client_port, buffer);
				}

				// 2. 向客户端写回数据 --- sendto

				// 既然我们要向客户端写回数据
				// 那么是不是需要, 客户端的IP、port
				// 我们不用过多处理, 因为 recvfrom 已经有了客户端的地址信息
				// 而我们就将客户端传过来的数据, 重发给客户端即可
				ssize_t real_write_size = sendto(_sock, buffer, strlen(buffer), 0, \
					reinterpret_cast<const struct sockaddr*>(&client), client_addr_len);

				if (real_write_size < 0)
				{
					LogMessage(ERROR, "%s\n", "write size < 0");
					exit(3);
				}
			}
		}

		~udp_server(){
			if (_sock != -1)
			{
				close(_sock);
			}
		}

	private:
		// IP地址, 这里之所以用string, 是因为想表示为点分十进制的字符串风格的IP地址
		std::string _ip;
		// 端口号, 16位整数
		uint16_t _port;
		// 套接字, socket系统调用的返回值,代表返回一个新的文件描述符 
		int _sock;
	};
}

#endif

3.3. Udp_Client.cc

#include "Date.hpp"
#include "Log.hpp"
#include <iostream>
#include <string>
#include <cstring>

// 下面的这四个头文件,我们称之为网络四件套
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <unistd.h>

#define CLIENT_BUFFER 1024


void Usage(void)
{
	printf("please usage: ./Client ServerIp  ServerPort\n");
}

int main(int arg, char* argv[])
{

	if (arg != 3)
	{
		Usage();
		exit(-2);
	}

	// 客户端创建套接字
	// 这里的PF_INET 是 AF_INET的封装
	int client_sock = socket(PF_INET, SOCK_DGRAM, 0);
	if (client_sock == -1)
	{
		LogMessage(FATAL, "%s\n", "client create sock failed");
		exit(1);
	}

	// 这里有一个问题, 客户端需不需要bind呢?
	// 答案: 肯定是需要的, 但是一般 client 不会显示的bind。换言之,程序员一般不会在客户端 bind。
	// client 是一个客户端, 是普通用户下载安装启动使用的, 如果程序员自己bind了,
	// 那么是不是就要求客户端一定bind了一个固定的ip和port,
	// 那么万一其他的客户端提前占用了这个port呢?那不就会导致bind失败吗?
	// 因为一个端口号只能绑定一个进程。
	// 因此,客户端一般不需要显式的bind指定port,而是让OS自动bind;
	// 可是操作系统是什么时候做的呢?

	// 1. 客户端向服务端发送数据
	// 因为客户端是向服务器发送数据,因此需要服务器的地址信息 IP + port;
	// 即需要服务器的端口和IP,通过命令行参数 (注意是 服务器的IP和port)。
	// 注意, 我们这里都是主机数据, 因此要转化为网络字节序。
	sockaddr_in server;
	memset(&server, 0, sizeof(server));
	// 填充sin_family
	server.sin_family = AF_INET;
	// 填充sin_addr(服务器的IP)
	server.sin_addr.s_addr = inet_addr(argv[1]);
	// 填充sin_port(服务器的端口)
	server.sin_port = htons(atoi(argv[2]));
	socklen_t server_len = sizeof(server);

	char buffer[CLIENT_BUFFER] = { 0 };

	while (true)
	{
		std::string client_message;
		std::cout << "client: " << "请输入信息" << std::endl;
		std::getline(std::cin, client_message);
		// 如果客户端输入 "quit" , 退出客户端
		if (client_message == "quit")
			break;
		// 当client 首次发送消息给服务器的时候,
		// OS会自动给客户端 bind 它的套接字以及IP和port (即绑定客户端的 ip + port);
		// 即第一次sendto的时候,操作系统会自动 bind
		ssize_t real_client_write = sendto(client_sock, client_message.c_str(), client_message.size(), 0, \
			reinterpret_cast<const struct sockaddr*>(&server), server_len);
		if (real_client_write < 0)
		{
			LogMessage(ERROR, "client write size < 0\n");
			exit(2);
		}

		// 2. 读取返回数据 (服务端发送给客户端的数据)

		buffer[0] = 0;
		// 因为我们的目的是 echo, 服务器发送给客户端的数据,客户端还是原封不动的打印一下。
		// 因为 sockaddr_in 是一个输出型参数, 因此调用完后,其实它就是发送方的地址信息
		// 以及发送方的这个结构体(缓冲区)的长度 (输入输出型参数)
		sockaddr_in server;
		bzero(&server, sizeof server);
		socklen_t server_addr_len = 0;

		ssize_t real_client_read = recvfrom(client_sock, buffer, CLIENT_BUFFER - 1, 0, \
			reinterpret_cast<struct sockaddr*>(&server), &server_addr_len);

		if (real_client_read > 0)
		{
			// 当返回值 > 0, 代表着读取成功
			// 客户端原封不动的打印一下这个信息
			buffer[real_client_read] = 0;
			printf("server: %s\n", buffer);
		}
	}

	if (client_sock >= 0)
		close(client_sock);

	return 0;
}

3.4. 细节总结

1、 AF_INET 是一个宏值,代表着网络套接字。

2、 SOCK_DGRAM, 标定这里是数据报套接字 

3、 socket 创建一个套接字,成功返回一个文件描述符。

4、 bind 主要目的是地址信息 (struct sockaddr_in) 与特定的套接字绑定起来。

5、 主机序列和网络字节序,在网络通信时,要注意数据的字节序。例如,传输到网络的数据,需要使用网络字节序,特别是填充 struct sockaddr_in,和提取struct sockaddr_in 的特定属性时,要格外注意。

6、 服务端一般情况下不用确定的IP bind 服务器的套接字, 一般我们采用任意IP的方案 (INADDR_ANY), 使用任意IP,可以使服务器能够接受来自任意IP地址的客户端连接,只要端口号一定, 凡是给我这台主机的数据,我都可以收到,因此,服务端一般只需要指明端口,IP采用任意地址方案。

7、 网络服务器永不退出,除非服务器宕机了或者系统挂了。

8、 recvfrom 会从发送方 (客户端 / 服务端) 进程获取数据,并且获得发送方的地址信息, 因此这里的地址信息就是输出型参数,当成功调用后,此时的地址信息就是发送方的地址信息。 例如: 客户端向服务端发送 (sendto) 数据、服务端接收 (recvfrom) 数据,那么服务端调用完毕后,recvfrom 里面的地址信息 (在使用时要根据情况进行类型转换,如果是网络通信,那么我们需要将 struct sockaddr_in 转为 struct sockaddr) 就是客户端的地址信息。

9、 sendto 会将数据发送给指定地址信息的 (客户端 / 服务端) 进程,因此这里的地址信息我们需要提前确定好。例如: 客户端向服务端发送 (sendto) 数据、服务端接收 (recvfrom) 数据,那么 sendto 中填的就是服务端的地址信息 (在使用时要根据情况进行类型转换) ,这个地址信息必须是在调用之前就确定好的。

 未完, 续篇 套接字编程 --- 二 。

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

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

相关文章

智慧公厕的三大特点:信息化、数字化、智慧化

智慧公厕是以物联网、互联网、大数据、云计算等先进技术为支撑&#xff0c;对公共厕所的使用、运营、管理、养护进行全方位高效应用的创新型公厕。它具有三大显著特点&#xff1a;&#xff08;ZonTree中期&#xff09;信息化、数字化和智慧化。本文以智慧公厕源头实力厂家广州中…

【鸿蒙 HarmonyOS 4.0】常用组件:List/Grid/Tabs

一、背景 列表页面&#xff1a;List组件和Grid组件&#xff1b; 页签切换&#xff1a;Tabs组件&#xff1b; 二、列表页面 在我们常用的手机应用中&#xff0c;经常会见到一些数据列表&#xff0c;如设置页面、通讯录、商品列表等。下图中两个页面都包含列表&#xff0c;“…

【PCIe 链路训练】之均衡(equalization)

1、概述 这篇文章简单介绍一下PCIE phy的均衡原理和过程,USB phy,ethernet phy这些高速的串行serdes也有相同或者相似的结构。可以不用太关注其中的细节,等到debug的时候可以查询协议,但是需要了解这个故事讲的大概内容。整个equalization过程是controller和phy一起配合完成…

什么是智慧公厕?智慧公厕设备有哪些

在现代社会&#xff0c;公共厕所作为城市基础设施的重要一环&#xff0c;承载着城市卫生、居民生活品质的重要责任。然而&#xff0c;传统公厕存在的问题仍然不可忽视&#xff1a;脏乱差、资源浪费、安全隐患等等。 为了解决这些问题&#xff0c;针对公共厕所日常使用、运营管…

go语言-k8s宿主信息采集运维脚本

背景 由于工作需要&#xff0c;需要采集k8s集群中的宿主相关信息&#xff0c;包括cpu,memory,lvm,标签等信息。通常作为SRE会主选shell或python脚本来实现。但最近我们团队主流开发语言已经切换到golang.所以本次尝试用go语言来写写运维脚本。 实现流程图 代码实现 package m…

[笔记]Crash Course Computer Science

二进制 byte 在电脑中的单位换算&#xff1a; kilobyte 千字节 megabyte 兆字节 gigabyte 千兆字节 1kb210bit 1024byte 1000b 1Mb 220bit 1024kb 1Gb 230bit 1TB1000GB 1GB十亿字节1000MB10^6KB Gb 和 GB 一般而言GB用于文件&#xff0c;Gb用于通信。B代表Byte&…

类与对象(三)--static成员、友元

文章目录 1.static成员1.1概念&#x1f3a7;面试题✒️1.2static的特性&#x1f3a7;1.3思考&#x1f3a7; 2.友元2.1什么是友元&#xff1f;&#x1f3a7;2.2两种友元关系&#xff1a;&#x1f3a7; 1.static成员 1.1概念&#x1f3a7; &#x1f50e; static关键字用于声明类…

SpringCloudAlibaba微服务之Nacos架构及基础概念梳理

SpringCloudAlibaba微服务之Nacos架构及基础概念梳理 文章目录 SpringCloudAlibaba微服务之Nacos架构及基础概念梳理1. 官网介绍1. 简介2. Naocs是什么3. Nacos 地图4. Nacos 生态图 2. Nacos 架构1. 基本架构及概念1. 服务 (Service)2. 服务注册中心 (Service Registry)3. 服务…

第四届信息通信与软件工程国际会议(ICICSE 2024)即将召开!

2024年第四届信息通信与软件工程国际会议&#xff08;ICICSE 2024&#xff09;将于2024年5月10-12日在中国北京举办。本次会议由北京工业大学、IEEE以及Comsoc 联合主办。随着当今社会信息化的高速发展&#xff0c;电子信息技术的应用更是随处可见。其中&#xff0c;信息通信与…

K倍区间(蓝桥杯)

文章目录 K倍区间题目描述前缀和数学优化代码部分解释 K倍区间 题目描述 给定一个长度为 N的数列&#xff0c;A1,A2,…AN&#xff0c;如果其中一段连续的子序列 Ai,Ai1,…Aj 之和是 K的倍数&#xff0c;我们就称这个区间 [i,j]是 K倍区间。 你能求出数列中总共有多少个 K倍区…

Zabbix(四)

Zabbix Proxy zabbix作为一个分布式监控系统(分布式监控解决方案)&#xff0c;支持通过代理(proxy)收集zabbix agent的监控数据&#xff0c;然后由zabbix proxy再把数据发送给zabbix server&#xff0c;也就是zabbix proxy 可以代替zabbix server收集监控数据&#xff0c;然后…

Mybatis-Spring | Mybatis与Spring的“整合“

目录 : 一、配置环境1. 整合环境需导入的JAR :Spring框架所需JARMybatis框架所需JARMyBatis与Spring整合的中间JAR数据库驱动JAR包数据源所需JAR包 &#xff08;下面的例子中 : 用的不是这个数据源&#xff09; 2. 编写“配置文件” 和 “.properties文件” ( 只是概述&#xf…

电磁铁通电后测不到磁场是什么原因

电磁铁 电磁铁没有磁力的一般原因有多种&#xff0c;以下是一些常见原因&#xff1a; 1. 电源问题&#xff1a;电磁铁没有连接好电源或电源电压不足&#xff0c;无法产生足够强的磁场。电磁铁所需要的电流和电压应符合制造商的规定。另外的话&#xff0c;电源接头也需要注意接…

SAP 消息编号 M8147

月末执行物料分类账的时候&#xff0c;出现以下报错 解决方法&#xff1a;OBYC-PRM

Linux运维:实现光盘开机自动挂载、配置本地yum源教程

Linux运维&#xff1a;实现光盘开机自动挂载、配置本地yum源教程 一、光盘开机自动挂载1、检查光驱设备2、创建挂载点3、编辑/etc/fstab文件4、测试挂载 二、配置本地yum源(挂载光盘或ISO文件)1、挂载ISO文件2、创建YUM仓库配置文件3、清理YUM缓存并测试 &#x1f496;The Begi…

萌新小白对于ctf学习的笔记--CTF中的RCE

概念 RCE(Remote code execution&#xff09;远程代码执行漏洞&#xff0c;RCE又分命令执行和代码执行。 RCE-远程代码执行&#xff1a;远程执行PHP代码RCE-远程命令执行&#xff1a;远程执行Linux或者Windows等系统命令。 常见函数有&#xff1a; PHP&#xff1a;eval(),a…

解决syslog服务器启动问题

Syslog 监控和管理对于每个组织来说都很重要&#xff0c;可以减少系统停机时间、提高网络性能并加强企业的安全策略。而在网络系统管理中&#xff0c;syslog服务用于收集、存储和管理系统和设备的日志信息。 然而&#xff0c;有时候我们可能会遇到syslog服务器无法启动的问题&…

mysql中 COALESCE和CASE WHEN的使用以及创建或替换视图

create or replace view 自理能力评估视图 as SELECT ehr_zlnlpg.ID AS ID, ehr_zlnlpg.GRID AS GRID, ehr_zlnlpg.TJID AS TJID, ehr_grjbxx.Name AS 姓名, ehr_grjbxx.Sex AS 性别, ehr_grjbxx.Cardnum AS 身份证号, ehr_zlnlpg.SCORESUM AS 总…

[每周一更]-第90期:认识Intel的CPU

市面上的CPU分类主要分有两大阵营&#xff0c;一个是Intel、AMD为首的复杂指令集CPU&#xff0c;另一个是以IBM、ARM为首的精简指令集CPU。 两个不同品牌的CPU&#xff0c;其产品的架构也不相同&#xff0c;例如&#xff0c;Intel、AMD的CPU是X86架构的&#xff0c;而IBM公司的…

基于java+springboot+vue实现的校园悬赏任务平台(文末源码+Lw)23-277

摘 要 使用旧方法对校园悬赏任务平台的信息进行系统化管理已经不再让人们信赖了&#xff0c;把现在的网络信息技术运用在校园悬赏任务平台的管理上面可以解决许多信息管理上面的难题&#xff0c;比如处理数据时间很长&#xff0c;数据存在错误不能及时纠正等问题。这次开发的…
最新文章