【问题标题】:Server socket doesn't work properly - "accept is already open"服务器套接字无法正常工作 - “接受已打开”
【发布时间】:2022-01-13 10:33:44
【问题描述】:

我试图将我的服务器套接字分离为一个单例。代码如下:

ServerSocket.h

#pragma once
#include <asio.hpp>
#include <iostream>

using asio::ip::tcp;

class ServerSocket
{
public:
    ServerSocket(ServerSocket& otherSingleton) = delete;
    void operator=(const ServerSocket& copySingleton) = delete;

    tcp::acceptor* InitAcceptor();
    tcp::socket* InitSocket();
    void StartServerSocket();
    void SendData(std::string);
    std::array<char, 5000> RecieveData();

    static ServerSocket* GetInstance();
private:
    static ServerSocket* instance;

    tcp::acceptor* acceptor;
    tcp::socket* socket;
    asio::io_context io_context;

    ServerSocket() {
        acceptor = InitAcceptor();
        socket = InitSocket();
    }

    ~ServerSocket()
    {
        std::cout << "Server closed";
    }
};

ServerSocket.cpp

#include "ServerSocket.h"

tcp::acceptor* ServerSocket::InitAcceptor()
{
    try
    {
        tcp::acceptor* acceptor = new tcp::acceptor(io_context, tcp::endpoint(tcp::v4(), 27015));

        return acceptor;
    }
    catch (std::exception& e)
    {
        std::cerr << e.what() << std::endl;
    }
}

tcp::socket* ServerSocket::InitSocket()
{
    try
    {
        tcp::socket* socket = new tcp::socket(io_context);

        return socket;
    }
    catch (std::exception& e)
    {
        std::cerr << e.what() << std::endl;
    }
}

void ServerSocket::StartServerSocket()
{
    try
    {
        std::cout << "Server started";
        for (;;)
        {
            acceptor->accept(*socket);
        };
    }
    catch (std::exception& e)
    {
        std::cerr << e.what() << std::endl;
    }
}

std::array<char, 5000> ServerSocket::RecieveData()
{
    try {
        std::array<char, 5000> buf;
        asio::error_code error;

        size_t len = socket->read_some(asio::buffer(buf), error);
        buf[len] = '\0';
        return buf;
    }
    catch (std::exception& e)
    {
        std::cerr << e.what() << std::endl;
    }
}

ServerSocket* ServerSocket::instance(nullptr);

ServerSocket* ServerSocket::GetInstance()
{
    if (instance == nullptr)
    {
        instance = new ServerSocket();
    }
    return instance;
}

服务器套接字启动,我得到:

Server started

当客户端连接时,我得到:

accept: Already open 

然后服务器停止。

我认为错误来自接受器在 for 函数中。但根据文档,它应该以这种方式工作。 (或者至少我是这么理解的 - https://think-async.com/Asio/asio-1.20.0/doc/asio/tutorial/tutdaytime2.html

我尝试删除 for 循环,如下所示:

try
    {
        std::cout << "Server started";
        acceptor->accept(*socket);
    }

现在没有问题了。但是服务器没有保持连接打开。客户端连接一次,发送数据,服务器停止运行。

据我从文档中了解到,如果我在 for(;;) 中设置接受器,它应该正在运行 - 但在我的情况下它不起作用。

那么,我怎样才能在我的实现中保持我的套接字打开?我希望它运行多个 SendData - 只要客户端连接,我希望它能够与客户端通信。

谢谢。

//编辑:

这是客户端代码:

#include <iostream>
#include <asio.hpp>
#include "../../cereal/archives/json.hpp"

using asio::ip::tcp;

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

    try
    {
        if (argc != 2)
        {
            std::cerr << "Usage: client <host>" << std::endl;
            return 1;
        }

        // Socket Parameters
        const unsigned port = 27015;
        auto ip_address = asio::ip::make_address_v4(argv[1]);
        auto endpoint = tcp::endpoint{ ip_address, port };

        // Creating and Connecting the Socket
        asio::io_context io_context;
        auto resolver = tcp::resolver{ io_context };
        auto endpoints = resolver.resolve(endpoint);

        auto socket = tcp::socket{ io_context };
        asio::connect(socket, endpoints);

        std::array<char, 5000> buf;
        std::cout << "Message to server: ";

        asio::error_code ignored_error;

        std::string username = "test", password = "mihai";
        std::stringstream os;
        {
            cereal::JSONOutputArchive archive_out(os);
            archive_out(
                CEREAL_NVP(username),
                CEREAL_NVP(password)
            );
        }

        asio::write(socket, asio::buffer(os.str()), ignored_error);
        return 0;
    }
    catch (std::exception& e)
    {
        std::cerr << e.what() << std::endl;
        return 1;
    }

而 Communication.h 负责从客户端捕获操作并将其发送到服务器

#pragma once
#include <iostream>
#include "DBUser.h"
#include "DBPost.h"

class Communication
{
public:
    enum class Operations {
        eLogin,
        eRegister
    };
    void ExecuteOperation(Operations operation,const std::array<char, 5000>& buffer);
};

.cpp

#include "Communication.h"

void Communication::ExecuteOperation(Operations operation,const std::array<char, 5000>& buffer)
{
    DBUser* user= DBUser::getInstance();
    switch (operation)
    {
    case Communication::Operations::eLogin:
    {
        std::string username, password;
        std::stringstream is(buffer.data());
        {
            cereal::JSONInputArchive archive_in(is);
            archive_in(username,password);
        }
        try
        {
            user->LoginUser(username, password);
        }
        catch (const std::exception& e)
        {
            std::cout << e.what();
        }
        break;
    }
    case Communication::Operations::eRegister:
    {
        std::string username, password;
        std::stringstream is(buffer.data());
        {
            cereal::JSONInputArchive archive_in(is);
            archive_in(username, password);
        }
        try
        {
            user->CreateUser(username, password);
        }
        catch (const std::exception& e)
        {
            std::cout << e.what();
        }
        break;
    }
    }
}

主要

#include <iostream>
#include <pqxx/pqxx>
#include "DBLink.h"
#include "DBUser.h"
#include "DBPost.h"
#include "../Logging/Logging.h"
#include <iostream>
#include <string>
#include <asio.hpp>
#include "ServerSocket.h"
#include "Communication.h"

int main()
{
    ServerSocket* test = ServerSocket::GetInstance();
    test->StartServerSocket();

    std::array<char, 5000> buf = test->RecieveData();
    Communication communicationInterface;
    communicationInterface.ExecuteOperation(Communication::Operations::eRegister, buf);
system("pause");
}

【问题讨论】:

  • 我认为如果你使用异步而不是同步,你想做的事情会更容易。很难想象一个单线程同步服务器正在侦听新连接并同时读取客户端消息。我也看不到你在哪里读到的东西?
  • @KoronisNeilos 我没有包括该部分,因为问题来自服务器 - 所以客户端部分不是必需的。 (在我看来)
  • 如果您希望服务器从客户端读取消息,您需要在服务器上调用 read
  • 您可以发布一个工作示例并解释您想要做什么吗?你想写一个同步服务器,它接受一个客户端并打印客户端发送的所有内容吗?
  • 为了清楚起见,您的for loop*socket 上调用accept,然后——没有关闭*socket——再次调用acceptor-&gt;accept(*socket)。因此,您在已处于连接状态的套接字描述符上调用 accept - 因此出现错误。请注意,您的代码与link you posted 中的代码完全不同。

标签: c++ boost boost-asio


【解决方案1】:

有很多反模式正在发生。

  1. 过度使用指针。

  2. 过度使用new(没有任何delete,保证泄漏)

  3. 析构函数声称“服务器已关闭”,但它实际上并没有做任何事情来实现这一点。

  4. 两步初始化(InitXXXX 函数)。首先,你显然应该支持初始化列表

    ServerSocket()
     : acceptor_(InitAcceptor()), socket_(InitSocket())
    { }
    

    并且您需要将InitAcceptor/InitSocket 设为私有实现。

  5. 我会忘记 99% 的情况下是反模式的单例,但我想这几乎值得商榷。

在您的StartServerSocket 中,您有一个循环一直重复使用相同的socket。当然,它已经连接了。您需要单独的套接字实例:

    for (;;) {
        acceptor_->accept(*socket_);
    };

简化/修复

#include <boost/asio.hpp>
#include <iostream>

namespace asio = boost::asio;
using asio::ip::tcp;

struct Listener {
    void Start()
    {
        std::cout << "Server started";
        for (;;) {
            auto socket = acceptor_.accept();
            std::cout << "Accepted connection from " << socket.remote_endpoint()
                      << std::endl;
        };
    }

    static Listener& GetInstance() {
        static Listener s_instance{27015}; // or use weak_ptr for finite lifetime
        return s_instance;
    }

  private:
    asio::io_context ioc_; // order of declaration is order of init!
    tcp::acceptor    acceptor_;

    Listener(uint16_t port) : acceptor_{ioc_, tcp::endpoint{tcp::v4(), port}} {}
};

int main() {
    try {
        Listener::GetInstance().Start();
    } catch (std::exception const& e) {
        std::cerr << e.what() << std::endl;
    }
}

现在您可以将socket 实例交给一个线程。我同意其他评论者的观点,即每个请求线程可能也是一种反模式,您应该考虑使用带有 Asio 的异步 IO(因此得名)。

现场演示

【讨论】:

  • 请注意,我的答案是写到问题的初始版本stackoverflow.com/revisions/70275611/1
  • 我现在查看了您的解决方案,但我不知道如何做两件事:首先,没有套接字,我不知道如何从客户端读取数据。其次,客户端连接关闭 - 我希望它一直打开,直到终端窗口关闭/用户停止执行。
  • 你有套接字。就像我说的你必须对它做点什么(我什至建议使用 s 异步 IO)。如果您需要更多外部指导,最好从示例开始。或者,您可以提出一个新的、有针对性的问题。我知道我已经扩展了这个问题,但这确实不是这个网站的工作方式,它只是导致答案不匹配的问题。
【解决方案2】:

根据问题中的服务器代码编辑完整且有效的示例:

// main.cxx
#include "ServerSocket.hxx"
#include <boost/asio.hpp>
#include <iostream>
#include <string>
using boost::asio::ip::tcp;

int
main ()
{
  ServerSocket *test = ServerSocket::GetInstance ();
  test->StartServerSocket ();
  std::cout << std::endl;
  while (auto msg = test->RecieveData ())
    {
      std::cout << msg.value ();
    }
}
// ServerSocket.hxx
#pragma once
#include <boost/asio.hpp>
#include <iostream>
#include <optional>

using boost::asio::ip::tcp;

class ServerSocket
{
public:
  ServerSocket (ServerSocket &otherSingleton) = delete;
  void operator= (const ServerSocket &copySingleton) = delete;

  tcp::acceptor *InitAcceptor ();
  tcp::socket *InitSocket ();
  void StartServerSocket ();
  void SendData (std::string);
  std::optional<std::string> RecieveData ();

  static ServerSocket *GetInstance ();

private:
  static ServerSocket *instance;

  tcp::acceptor *acceptor;
  tcp::socket *socket;
  boost::asio::io_context io_context;

  ServerSocket ()
  {
    acceptor = InitAcceptor ();
    socket = InitSocket ();
  }

  ~ServerSocket () { 
    delete socket;
    delete acceptor;
    std::cout << "Server closed"; }
};
// ServerSocket.cxx
#include "ServerSocket.hxx"
#include <optional>

tcp::acceptor *
ServerSocket::InitAcceptor ()
{
  try
    {
      return new tcp::acceptor (io_context, tcp::endpoint (tcp::v4 (), 27015));
    }
  catch (std::exception &e)
    {
      std::cerr << e.what () << std::endl;
    }
  return nullptr;
}

tcp::socket *
ServerSocket::InitSocket ()
{
  try
    {
      return new tcp::socket (io_context);
    }
  catch (std::exception &e)
    {
      std::cerr << e.what () << std::endl;
    }
  return nullptr;
}

void
ServerSocket::StartServerSocket ()
{
  try
    {
      std::cout << "Server started";
      acceptor->accept (*socket);
    }
  catch (std::exception &e)
    {
      std::cerr << e.what () << std::endl;
    }
}

std::optional<std::string>
ServerSocket::RecieveData ()
{
  try
    {
      char data[5000];
      for (;;)
        {
          boost::system::error_code error;
          size_t length = socket->read_some (boost::asio::buffer (data), error);
          if (error == boost::asio::error::eof) return std::nullopt; // Connection closed cleanly by peer.
          else if (error)
            throw boost::system::system_error (error); // Some other error.
          return std::string{ data, length };
        }
    }
  catch (std::exception &e)
    {
      std::cerr << e.what () << std::endl;
    }
  return {};
}

ServerSocket *ServerSocket::instance (nullptr);

ServerSocket *
ServerSocket::GetInstance ()
{
  if (instance == nullptr)
    {
      instance = new ServerSocket ();
    }
  return instance;
}

注意服务器还是有一些问题:

  • 错误处理
  • 多个连接
  • 如果操作成功,服务器不会发送消息
  • 如果您断开客户端连接,服务器将关闭
  • 我们可以将一些指针替换为可选的,无需写“new”
  • 只做一个普通类不要写成单例。

如果你想测试你可以运行的服务器

telnet localhost 27015

然后写一些文字并按回车

【讨论】:

  • 这肯定不是解决方案。这将永远注册我的用户。我不希望这样 - 我想让服务器在客户端发出的请求中保持活跃。现在,如果我从客户端向服务器发送一次数据,服务器将停止。我不希望这样 - 我希望服务器能够满足客户端想要的尽可能多的请求。仅当客户端断开连接时,服务器才应停止。
  • 我做了更新。我希望它现在对你有用。
猜你喜欢
  • 2023-03-09
  • 2012-01-26
  • 2018-10-28
  • 2015-10-12
  • 1970-01-01
  • 1970-01-01
  • 2018-07-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多