套接字编程 --- 一

news2024/11/20 13:33:57

目录

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.coloradmin.cn/o/1500442.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

代码随想录算法训练营第day10|232.用栈实现队列、 225. 用队列实现栈

目录 a.232.用栈实现队列 b. 225. 用队列实现栈 a.232.用栈实现队列 题目链接 请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作&#xff08;push、pop、peek、empty&#xff09;&#xff1a; 实现 MyQueue 类&#xff1a; void push(int x) 将元素…

专题一 - 双指针 - leetcode 202. 快乐数 | 简单难度

leetcode 202. 快乐数 leetcode 202. 快乐数 | 简单难度1. 题目详情1. 原题链接2. 基础框架 2. 解题思路1. 题目分析2. 算法原理3. 时间复杂度 3. 代码实现4. 知识与收获 leetcode 202. 快乐数 | 简单难度 1. 题目详情 编写一个算法来判断一个数 n 是不是快乐数。 「快乐数」…

掼蛋的牌型与规律(上篇)

掼蛋是一项配合类的棋牌竞技游戏&#xff0c;掼蛋的最大魅力以及最集中的特点在于变化&#xff0c;在于组牌的变数。有的掼蛋新手往往先把牌配死&#xff0c;并且直接决定好出牌计划&#xff0c;然后守株待兔。掼蛋的取胜之道在于静态组合加上动态变化。本文主要介绍一下掼蛋的…

【广度优先搜索】【图论】【并集查找】2493. 将节点分成尽可能多的组

作者推荐 视频算法专题 本文涉及知识点 广度优先搜索 图论 并集查找 LeetCod2493. 将节点分成尽可能多的组 给你一个正整数 n &#xff0c;表示一个 无向 图中的节点数目&#xff0c;节点编号从 1 到 n 。 同时给你一个二维整数数组 edges &#xff0c;其中 edges[i] [ai…

神州大地人类来源猜想

在公元前2000年以前&#xff0c;伟大的中华民族还是石器时代&#xff0c;我们有很多美好的神话和传说&#xff0c;三皇五帝就是这个时代伟大部落或者部落首领的故事。 关于人类的历史&#xff0c;从基因学上最新的研究成果大概是这样的&#xff0c;虽然从300万年前就诞生了人类…

3D资产管理

3D 资产管理是指组织、跟踪、优化和分发 3D 模型和资产以用于游戏、电影、AR/VR 体验等各种应用的过程。 3D资产管理也称为3D内容管理。 随着游戏、电影、建筑、工程等行业中 3D 内容的增长&#xff0c;实施有效的资产管理工作流程对于提高生产力、减少错误、简化工作流程以及使…

Xinstall微信调起APP,提升用户体验与转化率

在移动互联网时代&#xff0c;APP已经成为人们日常生活中不可或缺的一部分。然而&#xff0c;随着市场竞争的加剧&#xff0c;如何让用户更便捷地使用APP&#xff0c;提高分享营销的下载转化率&#xff0c;成为了开发者们亟待解决的问题。今天&#xff0c;我们将向大家介绍一款…

C/C++的内存管理与初阶模板

引言 我们在学习C的时候&#xff0c;会经常在堆上申请空间&#xff0c;所以这个时候就体现了内存管理遍历。 图下是我们常见的计算机的内存划分&#xff1a; 我也在图下对部分变量存在的位置&#xff0c;及时标注。(如果有任何问题可以联系博主修改&#xff0c;感谢大家。) 那…

如何在Windows上使用Docker,搭建一款实用的个人IT工具箱It- Tools

文章目录 1. 使用Docker本地部署it-tools2. 本地访问it-tools3. 安装cpolar内网穿透4. 固定it-tools公网地址 本篇文章将介绍如何在Windows上使用Docker本地部署IT- Tools&#xff0c;并且同样可以结合cpolar实现公网访问。 在前一篇文章中我们讲解了如何在Linux中使用Docker搭…

Docker安装主从数据库

首先开启docker后直接执行命令 docker run -d \ -p 3307:3306 \ -v /xk857/mysql/master/conf:/etc/mysql/conf.d \ -v /xk857/mysql/master/data:/var/lib/mysql \ -e MYSQL_ROOT_PASSWORD123456 \ --name mysql-master \ mysql:8.0.29 默认情况下MySQL的binlog日志是自动开…

深入浅出Redis(八):Redis的集群模式

引言 Redis是一款优秀的键值对、内存非关系型数据库&#xff0c;单机节点下的Redis存在无法保证高可用、容量不足等问题 上篇文章介绍的哨兵主要能够保证主从架构下Redis的可用性&#xff0c;但是仍然存在容量不足、推举新的主节点时不能访问Redis的问题&#xff0c;集群可水…

#QT(串口助手-界面)

1.IDE&#xff1a;QTCreator 2.实验&#xff1a;编写串口助手 3.记录 接收框:Plain Text Edit 属性选择&#xff1a;Combo Box 发送框:Line Edit 广告&#xff1a;Group Box &#xff08;1&#xff09;仿照现有串口助手设计UI界面 &#xff08;2&#xff09;此时串口助手大…

ai脚本创作的软件有哪些?分享3款好用的工具!

随着人工智能技术的飞速发展&#xff0c;AI脚本创作软件已经成为内容创作者们的新宠。这些软件不仅能够帮助我们更高效地生成文章、视频脚本等&#xff0c;还能为我们提供独特的创意视角和无限的灵感。本文将带您深入了解几款备受瞩目的AI脚本创作软件&#xff0c;看看它们如何…

带胶囊按钮的标题是如何实现的

使用uni-app开发小程序经常会遇到胶囊按钮和标题之间融合的问题&#xff0c;因为这样可以大大提高页面的美观和整体性&#xff0c;那么接下来简单拆分步骤看下是如何实现的吧 &#x1f601; 可以看到我们设置的标题是在默认标题栏之下的&#xff08;这不是我们想要的效果 &…

许多人可能还不了解这个信息差:美赛的第一批 EI 已经录用,不用再犹豫啦

格局打开&#xff0c;美赛论文转学术论文发表 &#x1f680;&#x1f680; 各位同学&#xff0c;美赛已经结束了一段时间&#xff0c;你们是否还在焦急地等待最终成绩的公布&#xff1f;一些有远见的同学已经提前收到了一份喜讯&#xff1a;他们的美赛论文已被转化为学术论文并…

VMware虚拟机

1、虚拟机介绍 虚拟机&#xff08;Virtual Machine&#xff09;是一种软件&#xff0c;可以用来模拟具有完整硬件功能的完整的计算机系统的软件&#xff0c;并且可以和主机环境隔离开&#xff0c;互不影响。也就是&#xff0c;在实体计算机中能够完成的工作都可以通过虚拟机实…

【SQL】1068. 产品销售分析 I

题目描述 leetcode题目&#xff1a;1068. 产品销售分析 I 写法 select Product.product_name, Sales.year, Sales.price from Sales left join Product on Sales.product_id Product.product_id记录细节&#xff1a;加上表名检索效率更高。 -- ERROR: 时间超出限制 > 加…

UE4 Niagara 关卡4.1官方案例解析

we now directly supporting playing audio from arbitrary locations in particle systems.users have control over volume and pitch,and the system can directly play sound waves,or sound cues which have multiple waves in them.(我们现在直接支持在粒子系统中从任意…

一个用libcurl多线程下载断言错误问题的排查

某数据下载程序&#xff0c;相同版本的代码&#xff0c;在64位系统中运行正常&#xff0c;但在32位系统中概率性出现断言错误。一旦出现&#xff0c;程序无法正常继续&#xff0c;即使重启亦不行。从年前会上领导提出要追到根&#xff0c;跟到底&#xff0c;到年后的今天&#…

报考照片大小不能超过30k怎么设置?赶快学习起来

某些在线表格和申请表要求上传照片&#xff0c;但对文件大小有严格的限制&#xff0c;这种限制可能出现在学校申请、工作申请、签证申请、奖学金申请等各种在线表格中&#xff0c;比如有些平台要求上传报考照片大小不能超过30k&#xff0c;那么如何将图片压缩指定大小呢&#x…