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;
}

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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
目 录 一、目录…………………………………………………………………1 二、题目……………………………………………………………2 三、设计任务…………………………………………………2 四、WinSocket简介及特点原理…………………………………2 五、TCP简介及特点原理………………………………………3 六、Visual C++简介………………………………………………7 七、设计方案…………………………………………………8 八、系统的原理框图和程序流程图………………………10 九、实验中的问题…………………………………………………14 十、实验结果及分析………………………………………………14 十一、课程设计的总结体会………………………………………16 十二、参考文献……………………………………………………16 利用Socket实现双机通信 一、设计任务 1.利用WinSock来实现双机通信,理解TCP状态机图。 2.要求使用WinSock编程,采用其中的TCP面向连接方式,实现文本数据的交换。 二、WinSocket简介及特点原理 2.1、什么是socket 所谓socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄。应 用程序通常通过"套接字"向网络发出请求或者应答网络请求。 Socket接口是TCP/IP网络的API,Socket接口定义了许多函数或例程,程序员可以用 它们来开发TCP/IP网络上的应用程序。要学Internet上的TCP/IP网络编程,必须理解So cket接口。 Socket接口设计者最先是将接口放在Unix操作系统里面的。如果了解Unix系统的输入和 输出的话,就很容易了解Socket了。网络的Socket数据传输是一种特殊的I/O,Socket也 是一种文件描述符。Socket也具有一个类似于打开文件的函数调用Socket(),该函数返 回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现 的。 常用的Socket类型有两种:流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_D GRAM)。流式是一种面向连接的Socket,针对于面向连接的TCP服务应用;数据报式Soc ket是一种无连接的Socket,对应于无连接的UDP服务应用。   最重要的是,socket 是面向客户/服务器模型而设计的,针对客户和服务器程序提供不同的socket 系统调用。客户随机申请一个socket (相当于一个想打电话的人可以在任何一台入网电话上拨号呼叫),系统为之分配一个so cket号;服务器拥有全局公认的 socket ,任何客户都可以向它发出连接请求和信息请求(相当于一个被呼叫的电话拥有一个呼叫 方知道的电话号码)。 socket利用客户/服务器模式巧妙地解决了进程之间建立通信连接的问题。服务器so cket 半相关为全局所公认非常重要。不妨考虑一下,两个完全随机的用户进程之间如何建立 通信?假如通信双方没有任何一方的socket 固定,就好比打电话的双方彼此不知道对方的电话号码,要通话是不可能的。 2.2、WinSocket通信原理 WinSock是一个基于Socket模型的 API。WinSock在 Windows98,Window NT中使用。WinSock一般由两部分组成:开发组件和运行组件。开发组件是供程序员在w indows环境下开发网络应用程序使用的,它包括应用程序接口库函数、头文件和实现的 文档,其中最主要的是WINSOCK.H运行组件是以动态链接库(DlL)来实现socket接口的。 文件名为WINSOCK.DLL应用程序在执行时装入它就能实现网络通信功能 三、TCP简介及特点原理 1.什么是TCP TCP是一种面向连接(连接导向)的、可靠的、基于字节流的运输层(Transport layer)通信协议。在简化的计算机网络OSI模型中,它完成第四层传输层所指定的功能 。   在因特网协议族(Internet protocol suite)中,TCP层是位于IP层之上,应用层之下的中间层。不同主机的应用层之间经常 需要可靠的、像管道一样的连接,但是IP层不提供这样的流机制,而是提供不可靠的包 交换。   应用层向TCP层发送用于网间传输的、用8位字节表示的数据流,然后TCP把数据流分 割成适当长度的报文段(通常受该计算机连接的网络的数据链路层的最大传送单元(MTU )的限制)。之后TCP把结果包传给IP层,由它来通过网络将包传送给接收端实体的TCP层 。TCP为了保证不发生丢包,就给每个字节一个序号,同时序号也保证了传送到接收端实 体的包的按序接收。然后接收端实体对已成功收到的字节发回一个相应的确认(ACK)

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值