所有文章 → 文章详情

C++ 使用 Socket 实现 TCP 服务器/客户端

Socket 不能面向对象?自己封装一个!

最近在做一个物联网的病人监护系统,需要网络通信并支持同时连接多台设备。故学习了一下 Socket,顺便记录一下自己的实现方法。

服务器

Socket 本身的使用方法很简单,在此不多赘述。我们需要关心的是,如何将其封装到一个类中。

在进行通信时,服务器需要:

  1. 初始化套接字,指定协议族,类型。
  2. 将一个地址和端口绑定到套接字上。
  3. 监听连接请求。
  4. 接受连接请求并创建一个新的客户端描述符。

然后,服务器和客户端之间就可以:

  • 接受数据
  • 发送数据
  • 关闭连接

使用步骤有了,于是,我们就可以将上面的每一个步骤定义为一个方法,以下是服务器类的定义:

#ifndef TCP_SERVER_H
#define TCP_SERVER_H

#include <string.h>
#include <unistd.h>
#include <QObject>
#include <QDebug>
#include <sys/socket.h>
#include <netinet/in.h>

class CTcpServer : public QObject
{
    Q_OBJECT

public:
    CTcpServer();
    int Bind(int target_port);
    int Listen(int max_connect);
    void Accept();
    void Receive();
    int Send(char* message);
    void Close();

signals:
    void ReceivedData(QString data);

private:
    int sockfd;
    struct sockaddr_in serveraddr;
    int clientfd;
    char recv_content[512];
};

#endif // TCP_SERVER_H

服务器类的实现:

#include "tcp_server.h"

CTcpServer::CTcpServer()
{
    sockfd=socket(AF_INET,SOCK_STREAM,0);
}

int CTcpServer::Bind(int target_port)
{
    memset(&serveraddr,0,sizeof(struct sockaddr_in));
    serveraddr.sin_family=AF_INET;
    serveraddr.sin_addr.s_addr=htonl(INADDR_ANY);
    serveraddr.sin_port=htons(target_port);
    return bind(sockfd,(struct sockaddr*)&serveraddr,sizeof(struct sockaddr));
}

int CTcpServer::Listen(int max_connect)
{
    return listen(sockfd,max_connect);
}

void CTcpServer::Accept()
{
    struct sockaddr_in clientaddr;
    socklen_t len=sizeof(clientaddr);
    clientfd=accept(sockfd,(struct sockaddr*)&clientaddr,&len);
}

void CTcpServer::Receive()
{
    QString acc_data;

    while(1)
    {
        memset(recv_content,0,sizeof(recv_content));
        int recv_count=recv(clientfd,recv_content,512,0);
        if(recv_count==0)
        {
            Close();
            break;
        }
        //qDebug()<<"Receved: "<<recv_content;
        acc_data+=QString::fromUtf8(recv_content);
    }
    emit ReceivedData(acc_data);
}

int CTcpServer::Send(char* message)
{
    return send(clientfd,message,strlen(message),0);
}

void CTcpServer::Close()
{
    close(clientfd);
}

客户端

客户端的实现要更为简单一些,方法什么的和服务器大差不差,直接放代码了。

定义:

#ifndef TCP_CILENT_H
#define TCP_CILENT_H

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <QDebug>

class CTcpClient
{
public:
    CTcpClient();
    int Connect(const char* server_addr,int port);
    int Send(const char* message);
    void Receive();
    void Close();

private:
    int sockfd;
    struct sockaddr_in srvaddr;
    char recv_content[128];
};

#endif // TCP_CILENT_H

实现:

#include "tcp_cilent.h"

CTcpClient::CTcpClient()
{
}

int CTcpClient::Connect(const char* server_addr,int port)
{
    qDebug()<<"attempting to connect to server"<<server_addr<<"on port"<<port;
    sockfd=socket(AF_INET,SOCK_STREAM,0);
    memset(&srvaddr,0,sizeof(srvaddr));
    srvaddr.sin_family=AF_INET;
    srvaddr.sin_port=htons(port);
    srvaddr.sin_addr.s_addr=inet_addr(server_addr);
    return connect(sockfd,(struct sockaddr*)&srvaddr,sizeof(srvaddr));
}

int CTcpClient::Send(const char *message)
{
    return send(sockfd,message,strlen(message),0);
}

void CTcpClient::Receive()
{
    while(1)
    {
        memset(recv_content,0,sizeof(recv_content));
        int recv_count=recv(sockfd,recv_content,128,0);
        qDebug()<<"Received: "<<recv_content;
        if(recv_count==0)
            break;
    }
}

void CTcpClient::Close()
{
    close(sockfd);
}

多客户端支持

本节内容提供一个较为简单的多客户端支持方式,仅供参考。

我们知道,在服务器套接字接受连接是,会返回一个客户端描述符(一般代码里的clientfd就负责存储这个),通过这个描述符,我们可以和指定客户端进行通信。

但是一旦客户端断线,重新建立连接后,在服务器端就会使用一个新的描述符来与其通信。此时对于服务器而言,这个新的描述符相当于一个全新身份。这对辨别设备身份来说是一个难点。

感谢卡卡吞星老师绘制的超强配图!

另一种更简单的做法则是:给所有客户端一个唯一ID,每次发送数据时都在头上包含这个唯一ID,每次发完数据均马上关闭连接。服务器套接字则进行 accept 和 recv 死循环,每当对方关闭连接时就将接收到的数据发送一个信号出去,然后再次开始 accept。

这样就解决了更换连接后无法识别身份的问题,而且只需要单线程就可以完成多客户端通信(当然如果客户端数量过多的话也可以考虑同时多开几个线程进行 accept 的)。

附一段代码使用例以供参考:

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    connect(&tcp_s,SIGNAL(ReceivedData(QString)),this,SLOT(CheckData(QString)));
    InitSocket();
    QtConcurrent::run([this]{StartSocketUpdate();});
}

void MainWindow::InitSocket()
{
    if(tcp_s.Bind(7777)<0)
    {
        qDebug()<<"bind error";
        QMessageBox::StandardButton msgbox;
        msgbox=QMessageBox::critical(this,tr("Error"),"Failed to start socket server",QMessageBox::Retry|QMessageBox::Cancel);
        if(msgbox==QMessageBox::Retry)
        {
            std::exit(-1);
        }
        else if(msgbox==QMessageBox::Cancel)
            std::exit(-1);
    }
    else
    {
        qDebug()<<"bind success on port 7777";
    }
    tcp_s.Listen(10);
}

void MainWindow::StartSocketUpdate()
{
    while(1)
    {
        tcp_s.Accept();
        tcp_s.Receive();
    }
}


void MainWindow::CheckData(QString data)
{
    qDebug()<<data;
}