14.10 Socket 套接字选择通信

对于网络通信中的服务端来说,显然不可能是一对一的,我们所希望的是服务端启用一份则可以选择性的与特定一个客户端通信,而当不需要与客户端通信时,则只需要将该套接字挂到链表中存储并等待后续操作,套接字服务端通过多线程实现存储套接字和选择通信,可以提高服务端的并发性能,使其能够同时处理多个客户端的请求。在实际应用场景中,这种技术被广泛应用于网络编程、互联网应用等领域。

该功能的具体实现思路可以总结为如下流程;

在服务端启动时,创建套接字并进行绑定,然后开启一个线程(称为主线程)用于监听客户端的连接请求。主线程在接收到新的连接请求后,会将对应的套接字加入一个数据结构(例如链表、队列、哈希表等)中进行存储。同时,主线程会将存储套接字的数据结构传递给每个子线程,并开启多个子线程进行服务,每个子线程从存储套接字的数据结构中取出套接字,然后通过套接字与客户端进行通信。

在选择通信方面,用户可以指定要与哪个客户端进行通信。服务端会在存储套接字的数据结构中寻找符合条件的套接字,然后将通信数据发送给对应的客户端。

首先为了能实现套接字的存储功能,此处我们需要定义一个ClientInfo该结构被定义的作用只有一个那就是存储套接字的FD句柄,以及该套接字的IP地址与端口信息,这个结构体应该定义为如下样子;

typedef struct
{
  SOCKET client;
  sockaddr_in saddr;
  char address[128];
  unsigned short port;
}ClientInfo;

接着我们来看主函数中的实现,首先主函数中listen正常侦听套接字连接情况,当有新的套接字接入后则直接通过CreateThread函数开辟一个子线程,该子线程通过EstablishConnect函数挂在后台,在挂入后台之前通过std::vector<ClientInfo *> info全局变量用来保存套接字。

当读者需要发送数据时,只需要调用SendMessageConnect函数,函数接收一个套接字链表,并接收需要操作的IP地址信息,以及需要发送的数据包,当有了这些信息后,函数内部会首先依次根据IP地址判断是否是我们所需要通信的IP,如果是则从全局链表内取出套接字并发送数据包给特定的客户端。

弹出一个套接字调用PopConnect该函数接收一个全局链表,以及一个字符串IP地址,其内部通过枚举链表的方式寻找IP地址,如果找到了则直接使用ptr.erase(it)方法将找到的套接字弹出链表,并以此实现关闭通信的目的。

输出套接字元素时,通过调用ShowList函数实现,该函数内部首先通过循环枚举所有的套接字并依次Ping测试,如果发现存在掉线的套接字则直接剔除链表,如果没有掉线则客户端会反馈一个pong以表示自己还在,此时即可直接输出该套接字信息。

14.10.1 服务端实现

服务端的实现方式在上述概述中已经简单介绍过了,服务端实现的原理概括起来就是,通过多线程技术等待客户端上线,当有客户端上线后就直接将其加入到全局链表内等待操作,主函数执行死循环,等待用户输入数据,用于选择与某个套接字通信。

#include <iostream>
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <string>
#include <vector>

#pragma comment(lib,"ws2_32.lib")

using namespace std;

typedef struct
{
  SOCKET client;
  sockaddr_in saddr;
  char address[128];
  unsigned short port;
}ClientInfo;

std::vector<ClientInfo *> info;       // 全局主机列表
SOCKET server;                        // 本地套接字
sockaddr_in sai_server;               // 存放服务器IP、端口

// 弹出下线的主机
void PopConnect(std::vector<ClientInfo *> &ptr, char *address)
{
  // 循环迭代器,查找需要弹出的元素
  for (std::vector<ClientInfo *>::iterator it = ptr.begin(); it != ptr.end(); it++)
  {
    ClientInfo *client = *it;

    // 如果找到了,则将其从链表中移除
    if (strcmp(client->address, address) == 0)
    {
      ptr.erase(it);
      // std::cout << "地址: " << client->address << " 已下线" << std::endl;
      return;
    }
  }
}

// 输出当前主机列表
void ShowList(std::vector<ClientInfo *> &ptr)
{
  for (int x = 0; x < ptr.size(); x++)
  {
    // 发送Ping信号,探测
    bool ref = send(ptr[x]->client, "Ping", 4, 0);
    if (ref != true)
    {
      PopConnect(info, ptr[x]->address);
      continue;
    }

    // 接收探测信号,看是否存活
    char ref_buf[32] = { 0 };
    recv(ptr[x]->client, ref_buf, 32, 0);
    if (strcmp(ref_buf, "Pong") != 0)
    {
      PopConnect(info, ptr[x]->address);
      continue;
    }
    std::cout << "主机: " << ptr[x]->address << " 端口: " << ptr[x]->port << std::endl;
  }
}

// 发送消息
void SendMessageConnect(std::vector<ClientInfo *> &ptr, char *address, char *send_data)
{
  for (int x = 0; x < ptr.size(); x++)
  {
    // std::cout << ptr[x]->address << std::endl;

    // 判断是否为需要发送的IP
    if (strcmp(ptr[x]->address, address) == 0)
    {
      // 对选中主机发送数据
      send(ptr[x]->client, send_data, strlen(send_data), 0);
      int error_send = GetLastError();
      if (error_send != 0)
      {
        // std::cout << ptr[x]->address << " 已离线" << endl;

        // 弹出元素
        PopConnect(info, address);
        return;
      }

      // 获取执行结果
      char recv_message[4096] = { 0 };
      recv(ptr[x]->client, recv_message, 4096, 0);
      std::cout << recv_message << std::endl;
    }
  }
}

// 建立套接字
void EstablishConnect()
{
  while (1)
  {
    ClientInfo* cInfo = new ClientInfo();
    int len_client = sizeof(sockaddr);

    cInfo->client = accept(server, (sockaddr*)&cInfo->saddr, &len_client);

    // 填充主机地址和端口
    char array_ip[20] = { 0 };

    inet_ntop(AF_INET, &cInfo->saddr.sin_addr, array_ip, 16);
    strcpy(cInfo->address, array_ip);
    cInfo->port = ntohs(cInfo->saddr.sin_port);

    info.push_back(cInfo);
  }
}

int main(int argc, char* argv[])
{
  // 初始化 WSA ,激活 socket
  WSADATA wsaData;
  WSAStartup(MAKEWORD(2, 2), &wsaData);

  // 初始化 socket、服务器信息
  server = socket(AF_INET, SOCK_STREAM, 0);
  sai_server.sin_addr.S_un.S_addr = 0;    // IP地址
  sai_server.sin_family = AF_INET;        // IPV4
  sai_server.sin_port = htons(8090);        // 传输协议端口

  // 本地地址关联套接字
  if (bind(server, (sockaddr*)&sai_server, sizeof(sai_server)))
  {
    WSACleanup();
  }

  // 套接字进入监听状态
  listen(server, SOMAXCONN);

  // 建立子线程实现侦听连接
  CreateThread(0, 0, (LPTHREAD_START_ROUTINE)EstablishConnect, 0, 0, 0);

  while (1)
  {
    char command[4096] = { 0 };

  input:
    memset(command, 0, 4096);
    std::cout << "[ LyShell ] # ";

    // 发送命令
    int inputLine = 0;
    while ((command[inputLine++] = getchar()) != '\n');
    if (strlen(command) == 1)
      goto input;

    // 输出主机列表
    if (strcmp(command, "list\n") == 0)
    {
      ShowList(info);
    }
    // 发送消息
    else if (strcmp(command, "send\n") == 0)
    {
      SendMessageConnect(info, "127.0.0.1", "Send");
    }
    // 发送CPU数据
    else if (strcmp(command, "GetCPU\n") == 0)
    {
      SendMessageConnect(info, "127.0.0.1", "GetCPU");
    }
    // 发送退出消息
    else if (strcmp(command, "Exit\n") == 0)
    {
      SendMessageConnect(info, "127.0.0.1", "Exit");
    }
  }
  return 0;
}

14.10.2 客户端实现

客户端的实现与之前文章中的实现方式是一样的,由于客户端无需使用多线程技术所以在如下代码中我们只需要通过一个死循环每隔5000毫秒调用connect对服务端进行连接,如果没有连接成功则继续等待,如果连接成功了则直接进入内部死循环,在循环体内根据不同的命令执行不同的返回信息,如下是客户端实现完整代码片段。

#include <iostream>
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <string>

#pragma comment(lib,"ws2_32.lib")

using namespace std;

int main(int argc, char* argv[])
{
  while (1)
  {
    WSADATA WSAData;
    SOCKET sock;
    struct sockaddr_in ClientAddr;

    if (WSAStartup(MAKEWORD(2, 0), &WSAData) != SOCKET_ERROR)
    {
      ClientAddr.sin_family = AF_INET;
      ClientAddr.sin_port = htons(8090);
      ClientAddr.sin_addr.s_addr = inet_addr("127.0.0.1");

      sock = socket(AF_INET, SOCK_STREAM, 0);
      int Ret = connect(sock, (LPSOCKADDR)&ClientAddr, sizeof(ClientAddr));

      if (Ret == 0)
      {
        while (1)
        {
          char buf[4096] = { 0 };

          memset(buf, 0, sizeof(buf));
          recv(sock, buf, 4096, 0);

          // 获取CPU数据
          if (strcmp(buf, "GetCPU") == 0)
          {
            char* cpu_idea = "10%";
            int ServerRet = send(sock, cpu_idea, sizeof("10%"), 0);
            if (ServerRet != 0)
            {
              std::cout << "发送CPU数据包" << std::endl;
            }
          }

          // 发送消息
          else if (strcmp(buf, "Send") == 0)
          {
            char* message = "hello lyshark";
            int ServerRet = send(sock, message, sizeof("hello lyshark"), 0);
            if (ServerRet != 0)
            {
              std::cout << "发送消息数据包" << std::endl;
            }
          }

          // 终止客户端
          else if (strcmp(buf, "Exit") == 0)
          {
            closesocket(sock);
            WSACleanup();
            exit(0);
          }

          // 存活探测信号
          else if (strcmp(buf, "Ping") == 0)
          {
            int ServerRet = send(sock, "Pong", 4, 0);
            if (ServerRet != 0)
            {
              std::cout << "Ping 存活探测..." << std::endl;
            }
          }
        }
      }
    }
    closesocket(sock);
    WSACleanup();
    Sleep(5000);
  }
  return 0;
}

读者可自行编译并运行上述代码,当服务端启动后客户端上线,此时读者可根据输入不同的命令来操作不同的套接字,如下图所示;

posted @ 2023-10-18 08:46  lyshark  阅读(715)  评论(1编辑  收藏  举报

loading... | loading...
博客园 - 开发者的网上家园