套接字编程 --- 二

目录

1. 相关接口说明

1.1. popen 接口

1.2. strcasestr 接口

2. UDP --- demo2

2.1. Udp_Client.cc

2.2. Udp_Server.cc

2.3. Udp_Server.hpp

2.4. demo2 总结 

3. UDP --- demo3

3.1. Thread.hpp

3.2. Udp_Client.cc

3.2. Udp_Server.cc

3.3. Udp_Server.hpp

3.4. demo3总结 


接上篇套接字编程 --- 一,继续。

1. 相关接口说明

1.1. popen 接口

#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE* stream)

popen 函数: 在函数内部调用 fork() 和 pipe() ,并创建标准的输出或输入管道。

popen 内部会执行command命令,并将结果写入管道。

我们可以通过返回值 FILE*, 读取管道内容 (执行命令的结果)。

  1. command:需要执行的 shell 命令或进程名。
  2. mode:打开管道的模式,可以是 " r "(读模式)或 " w "(写模式)。
  3. 若模式为 " r ",则返回可用于读取管道输出流的 FILE 指针。
  4. 若模式为 " w ",则返回可用于向管道输入流写入数据的 FILE 指针。

在使用完毕后,必须使用 pclose() 函数来关闭由 popen() 函数打开的管道,并回收相关资源 (等等子进程退出,回收子进程资源),以避免出现资源泄露的情况。

pclose() 函数将阻塞调用进程,直到被调用进程终止并关闭它所打开的管道。

popen() 和 pclose() 函数的结合机制类似于 Linux 系统上的 shell 命令中管道(|)的功能,即将一个进程的输出连接到另一个进程的输入,可以方便地实现进程间的通信。

1.2. strcasestr 接口

#include <string.h>
char *strcasestr(const char *haystack, const char *needle);

strcasestr 函数用于在一个字符串中查找另一个字符串,并返回第一次出现的匹配子串的指针,不区分大小写。

其参数如下:

  1. haystack:要搜索的字符串,即被查找的字符串。
  2. needle:要查找的子字符串,即需要匹配的字符串。

函数将会在 haystack 字符串中查找第一个不区分大小写的 needle 子字符串,并返回该子字符串在 haystack 中的位置。如果未找到匹配的子串,则返回 nullptr。

2. UDP --- demo2

如果服务端收到客户端发送的信息是一个字符串,如果这个字符串是一串命令呢 ?

因此此时服务端的目的:
将客户端传递过来的命令,进行分析处理执行,服务端执行完毕,并将执行结果返回给客户端。

像Date.hpp、Log.hpp、Makefile和套接字编程 --- 一 里面的demo1一致,在这里就不重复了。

2.1. 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的时候,操作系统会自动绑定
    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;

    // 此时客户端发送的就是命令, 服务端处理后, 将处理数据返回给客户端

    // 因为 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;
}

2.2. Udp_Server.cc

#include "Udp_Server.hpp"

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

int main(int argc, char* argv[])
{
  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;
}

2.3. 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", "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的封装
      // 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让服务器,在工作过程中,可以从任意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 done

      // 这里的 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);
      }

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

    void start(void)
    {
      char buffer[SER_BUFFER_SIZE] = {0};
      for(;;)
      {
        // 客户端的地址信息
        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;
          std::cout <<  buffer  << std::endl;

          // 做一层保险工作, 防止客户端调用 rm、rmdir 等命令
          // 检测一下 buffer 这个字符串
          // 我们可以通过 strcasestr 这个接口
          // char *strcasestr(const char *haystack, const char *needle);
          // 用于在一个字符串中查找另一个字符串,并返回第一次出现的匹配子串的指针
          // haystack: 要搜索的字符串
          // needle: 要查找的子字符串
          if(strcasestr(buffer, "rm") != nullptr || strcasestr(buffer, "rmdir"))
          {
            // 如果出现了, 就提示一下
            std::string malice_argv = "坏人do: ";
            malice_argv += buffer;
            std::cout << malice_argv << std::endl;
            // 避免客户端被阻塞
            sendto(_sock, malice_argv.c_str(), malice_argv.size(), 0,\
            reinterpret_cast<const struct sockaddr*>(&client), client_addr_len);
            continue;
          }

          // 因为我们此时是将这个buffer当成命令字符串的
          // 调用popen
          // FILE *popen(const char *command, const char *type);
          // popen 会在内部调用 fork(), pipe()
          // popen内部会执行command命令, 并将结果写入管道
          // 我们可以通过返回值 FILE*, 读取管道内容 (执行命令的结果)
          // type 代表打开管道的模式, "r" ---> 读取管道  "w" ---> 写管道
          // 因为此时的处理结果在管道内, 因此我们已读方式打开管道, 读取数据
          FILE* client_result_info = popen(buffer, "r");
          if(client_result_info == nullptr)
          {
            LogMessage(ERROR, "%s\n", "command not found");
            continue;
          }
          // popen调用成功
          // 通过FILE* 读取管道内容
          std::string client_message;
          char client_message_buffer[256] = {0};
          while(nullptr != fgets(client_message_buffer, 256, client_result_info))
          {
            client_message += client_message_buffer;
          }

          pclose(client_result_info);
				
          // 2. 向客户端写回数据 --- sendto
          
          // 既然我们要向客户端写回数据
          // 那么是不是需要, 客户端的IP、port
          // 我们不用过多处理, 因为 recvfrom 已经有了客户端的地址信息
          // 而我们就将客户端传过来的数据, 重发给客户端即可
          // 将服务端的处理结果返回给客户端, 即就是client_message
          ssize_t real_write_size = sendto(_sock, client_message.c_str(), client_message.size(), 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;
    // 端口号
    uint16_t _port;
    // 套接字, socket系统调用的返回值,代表返回一个新的文件描述符 
    int _sock;
  };
}

#endif

2.4. demo2 总结 

事实上,demo2 和  套接字编程 --- 一 中的 demo1只有在服务端处理数据不同罢了。 demo2中是将客户端的数据当成了命令行字符串处理, 借用 popen 达到命令行解析、执行命令 or 进程,并将处理结果写入管道。 服务端通过返回的文件指针读取管道内容,并将数据写回客户端。

popen 本质上是调用了 fork 和 pipe ,因此,popen处理完毕后,使用 pclose 函数来关闭文件指针并等待子进程结束。这是因为 popen 在内部创建了一个子进程,而 pclose 会等待子进程结束并返回其退出状态。

3. UDP --- demo3

如果我想完成一个群聊功能呢?

服务端收到一条消息,发送给客户端(不同的进程)。

服务端将收到的信息广播给所有客户端进程

要求客户端一直接收数据、一直发送数据。

因此我们需要将客户端改为多线程。

3.1. Thread.hpp

线程的封装,在线程池中就已经详细解释了,在这就不赘述了。

#ifndef __THREAD_HPP_
#define __THREAD_HPP_

#include <iostream>
#include <pthread.h>
#include <string>
#include "Log.hpp"

const int BUFFER_SIZE = 64;

typedef void*(*Tfunc_t)(void*);

namespace Xq
{

  class thread_info
  {
  public:
    thread_info(const std::string& name = std::string (), void* arg = nullptr)
      :_name(name)
       ,_arg(arg)
    {}

    void set_info(const std::string& name, void* arg)
    {
      _name = name;
      _arg = arg;
    }

    std::string& get_name()
    {
      return _name;
    }

    void*& get_arg()
    {
      return _arg;
    }

  private:
    std::string _name;
    void* _arg;
  };

  class thread
  {
  public:
    thread(size_t num, Tfunc_t func, void* arg)
      :_func(func)
       ,_arg(arg)
    {
      // 构造线程名
      char buffer[BUFFER_SIZE] = {0};
      snprintf(buffer, BUFFER_SIZE, "%s %ld", "thread", num);
      _name = buffer;

      // 设置线程所需要的信息, 线程名 + _arg
      _all_info.set_info(_name, _arg);
    }

    // 创建线程
    void create(void)
    {
      pthread_create(&_tid, nullptr, _func, static_cast<void*>(&_all_info));
      //std::cout << "创建线程: " << _name << " success" << std::endl;
      //LogMessage(NORMAL, "%s: %s %s", "创建线程", _name.c_str(), "success");
    }

    pthread_t get_tid()
    {
      return _tid;
    }

  private:
    std::string _name;  // 线程名
    Tfunc_t _func;     // 线程的回调
    pthread_t _tid;   //线程ID

    thread_info _all_info;  // 装载的是 线程名 + _arg;

    // 线程参数, 未来我们会将其和线程名封装到一起(thread_info),整体传递给线程
    void* _arg; 
  };
}

#endif

3.2. Udp_Client.cc

因为我们要求,客户端收发是同时的, 因此需要多线程,即一个线程进行发数据,一个线程进行写数据。

#include "Date.hpp"
#include "Log.hpp"
#include "Thread.hpp"
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <pthread.h>

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

#include <unistd.h>

#define CLIENT_BUFFER 1024

// 将套接字设置为全局, 方便新线程访问
int client_sock = -1; 

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

void* send_data(void* arg)
{
  Xq::thread_info *T_info = static_cast<Xq::thread_info*>(arg);
  struct sockaddr_in* addr = static_cast<struct sockaddr_in*>(T_info->get_arg());
  int addrlen = sizeof (*addr);
  while(true)
  {

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

void* recv_data(void* arg)
{
  arg = nullptr;
  char buffer[CLIENT_BUFFER] = {0};
  while(true)
  {
    buffer[0] = 0;
    // 因为 sockaddr_in 是一个输出型参数, 因此调用完后,其实他就是发送方的地址信息
    // 以及发送方的这个结构体(缓冲区)的长度 (输入输出型参数)
    sockaddr_in server;
    bzero(&server, sizeof server);
    socklen_t server_addr_len = sizeof server;

    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;
      std::cout << buffer;
      fflush(stdout);
    }
  }
  return nullptr;
}


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

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

  // 客户端创建套接字
  // 我们预期是客户端进程有两个新线程
  // 一个新线程用来向服务端发送数据 --- send_thread
  // 一个新线程用来向服务端读取数据 --- recv_thread
  // 而这里的套接字, 两个线程都要用
  // 但是这两个线程不会修改这个套接字
  // 即不涉及线程安全问题, 因此我们将其改为全局的
  // 这里的PF_INET 是 AF_INET的封装
  client_sock = socket(PF_INET, SOCK_DGRAM, 0);
  if(client_sock == -1)
  {
    LogMessage(FATAL, "%s\n", "client create sock failed");
    exit(1);
  }

  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]));

  Xq::thread* send_thread = new Xq::thread(1, send_data, static_cast<void*>(&server));
  send_thread->create();
  Xq::thread* recv_thread = new Xq::thread(2, recv_data, nullptr);
  recv_thread->create();

  pthread_join(send_thread->get_tid(), nullptr);
  pthread_join(recv_thread->get_tid(), nullptr);

  delete send_thread;
  delete recv_thread;

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

3.2. 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.3. Udp_Server.hpp

#ifndef __UDP_SERVER_HPP_
#define __UDP_SERVER_HPP_

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

// 下面的这四个头文件,我们称之为网络四件套
#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的封装
      // 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让服务器,在工作过程中,可以从任意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 done

      // 这里的 socklen_t 本质上就是 unsigned int
      socklen_t server_addr_len = sizeof(server);

      if(bind(_sock, reinterpret_cast<const struct sockaddr*>(&server), server_addr_len) == -1)
      {
        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(;;)
      {
        struct sockaddr_in client; // 这里的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;

          // 我们的目的是完成群发功能
          // 为了标识不同客户端进程发送的信息
          // 我们提取一下IP和地址
          // 因此未来,我们的数据信息 [客户端IP][客户端port]: info
          
          // 提取端口, 并将网络字节序 -> 主机序列
          uint16_t client_port = ntohs(client.sin_port);
          // 提取IP, IP -> 主机序列 -> 点分十进制的字符串风格
          // inet_ntoa 这个接口就可以帮助我们完成上面两件事
          std::string client_ip = inet_ntoa(client.sin_addr);

          // 客户端信息标志
          char info_sign[256] = {0};
          snprintf(info_sign, 256, "[%s][%d]", client_ip.c_str(), client_port);

          auto it = _map.find(buffer);
          if(it == _map.end())
          {
            LogMessage(NORMAL, "add client: %s\n", info_sign);
            _map[info_sign] = client;
            //_map.insert(std::make_pair(info_sign, client));
          }

          std::string all_data;
          all_data += info_sign;
          all_data += ": ";
          all_data += buffer;
          all_data += "\n";

          // 2. 向所有的客户端写回数据 --- sendto
          
          for(const auto& it : _map)
          {
            // 向每一个客户端发送消息
            ssize_t real_write_size = sendto(_sock, all_data.c_str(), all_data.size(), 0,\
                reinterpret_cast<const struct sockaddr*>(&it.second), sizeof ((it.second)));
            LogMessage(NORMAL, "push data [%s] to client %s\n", buffer, it.first.c_str());

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

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

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

    // 这个map就将客户端标志信息和相应的sockaddr_in结构关联起来
    std::map<std::string, struct sockaddr_in> _map;
  };
}

#endif

3.4. demo3总结 

我们发现,当客户端进行读数据还是写数据,用的都是同一个套接字  (sock), sock代表的就是文件, 因此UDP是全双工的, 可以同时进行收发数据而不受到干扰。

而我们以前学习的管道就是半双工的。

总而言之,UDP提供了更灵活的通信方式,适用于需要快速传输、不需要建立连接的应用场景,而管道通常用于进程间通信,其中一个进程负责写入,另一个进程负责读取。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值