14.10 Socket 通訊端選擇通訊

2023-10-18 09:00:23

對於網路通訊中的伺服器端來說,顯然不可能是一對一的,我們所希望的是伺服器端啟用一份則可以選擇性的與特定一個使用者端通訊,而當不需要與使用者端通訊時,則只需要將該通訊端掛到連結串列中儲存並等待後續操作,通訊端伺服器端通過多執行緒實現儲存通訊端和選擇通訊,可以提高伺服器端的並行效能,使其能夠同時處理多個使用者端的請求。在實際應用場景中,這種技術被廣泛應用於網路程式設計、網際網路應用等領域。

該功能的具體實現思路可以總結為如下流程;

在伺服器端啟動時,建立通訊端並進行繫結,然後開啟一個執行緒(稱為主執行緒)用於監聽使用者端的連線請求。主執行緒在接收到新的連線請求後,會將對應的通訊端加入一個資料結構(例如連結串列、佇列、雜湊表等)中進行儲存。同時,主執行緒會將儲存通訊端的資料結構傳遞給每個子執行緒,並開啟多個子執行緒進行服務,每個子執行緒從儲存通訊端的資料結構中取出通訊端,然後通過通訊端與使用者端進行通訊。

在選擇通訊方面,使用者可以指定要與哪個使用者端進行通訊。伺服器端會在儲存通訊端的資料結構中尋找符合條件的通訊端,然後將通訊資料傳送給對應的使用者端。

首先為了能實現通訊端的儲存功能,此處我們需要定義一個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;
}

讀者可自行編譯並執行上述程式碼,當伺服器端啟動後用戶端上線,此時讀者可根據輸入不同的命令來操作不同的通訊端,如下圖所示;

本文作者: 王瑞
本文連結: https://www.lyshark.com/post/4acbc25e.html
版權宣告: 本部落格所有文章除特別宣告外,均採用 BY-NC-SA 許可協定。轉載請註明出處!