14-2_Qt 5.9 C++开发指南_TCP通信(TCP & Socket 相关知识点;Socket连接过程分为三个步骤: 服务器监听,客户端请求,连接确认)

1. TCP & Socket 相关知识点

本部分是将参考书籍和TCP & Socket 相关知识点中的进行整合,另外关于OSI模型和TCP模型理解建议参考:OSI七层模型&TCP/IP五层模型,博文写的都是比较详细的,十分值得参考。

首先明确:

  • 从字面意义上来讲,TCP/IP是指[传输层]的TCP协议和[网络层]的IP协议
  • 实际上,TCP/IP只是利用 IP 进行通信时所必须用到的协议群的统称。具体来说,在网络层是IP/ICMP协议、在传输层是TCP/UDP协议、在应用层是SMTP、FTP、以及 HTTP 等。他们都属于 TCP/IP 协议。

TCP是一个传输层协议,提供可靠传输,支持全双工,是一个连接导向的协议。Socket是TCP /IP协议族的编程接口 (API)

1.1 TCP相关知识点

TCP(Transmission Control Protocol)是一种被大多数 internet 网络协议(如HTTP 和FTP)用于数据传输的低级网络协议,它是可靠的、面向流、面向连接的传输协议,特别适合用于连续数据传输。

1.1.1 双工/单工

(1)单工:在任何一个时刻,如果数据只能单向发送,就是单工。

(2)半双工:如果在某个时刻数据可以向一个方向传输,也可以向另一个方向反方向传输,而且交替进行,叫作半双;半双工需要至少 1 条线路。

(3)全双工:如果任何时刻数据都可以双向收发,这就是全双工,全双工需要大于 1 条线路.
TCP 是一个双工协议,数据任何时候都可以双向传输。这就意味着客户端和服务端可以平等地发送、接收信息。

1.1.2 TCP协议的主要特点

  • TCP是面向连接运输层协议,所谓面向连接就是双方传输数据之前,必须先建立一条通道,例如三次握手就是建议通道的一个过程,而四次挥手则是结束销毁通道的一个其中过程。
  • 每一条TCP连接只能有两个端点 (即两个套接字),只能是点对点的(服务器监控相应端口,客户端连接相应的端口+端口);
  • TCP提供可靠的传输服务。传送的数据无差错、不丢失、不重复、按序到达
  • TCP提供全双工通信。允许通信双方的应用进程在任何时候都可以发送数据,因为两端都设有发送缓存和
    接受缓存:
  • 面向字节流。虽然应用程序与TCP交互是一次一个大小不等的数据块,但TCP把这些数据看成一连串无结构的字节流,它不保证接收方收到的数据块和发送方发送的数据块具有对应大小关系,例如,发送方应用程序交给发送方的TCP10个数据块,接收方的TCP可能只用收到的4个数据块字节流交付给上层的应用程序。

1.1.3 TCP的可靠性原理可靠传输有如下两个特点:

1.传输信道无差错,保证传输数据正确:
2.不管发送方以多快的速度发送数据,接收方总是来得及处理收到的数据;
首先,采用三次握手来建立TCP连接,四次握手来释放TCP连接,从而保证建立的传输信道是可靠的。其次,TCP采用了连续ARQ协议 (回退N(Go-back-N); 超时自动重传)来保证数据传输的正确性,使用滑动窗口协议来保证接方能够及时处理所接收到的数据,进行流量控制。
最后,TCP使用慢开始、拥塞避免、快重传和快恢复来进行拥塞控制,避免网络拥塞

1.2 Socket相关知识点

1.2.1 Socket

Socket即套接字,是应用层 与 TCP/IP 协议族通信的中间软件抽象层,表现为一个封装了 TCP /IP协议族的编程接口(API)。Socket不是一种协议,而是一个编程调用接口 (API),属于传输层(主要解决数据如何在网络中传输)。对用户来说,只需调用Socket去组织数据,以符合指定的协议,即可通信。

那Socket抽象层大致在协议层级中的哪里呢?
在这里插入图片描述
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。 换句话说,socket本质是编程接口(AP),它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用以实现进程在网络中通信。TCP/IP只是一个协议栈,必须要具体实现,同时还要提供对外的操作接口 (API),这就是Socket接口。通过Socket,我们才能使用TCP/IP协议。
比如Java语言中的JDK的iava.net包下有两个类: Socket和ServerSocket,在Client和Server建立连接成功后,两端都会产生一个Socket实例,操作这个实例,完成所需的会话,而我们就通过这些API进行网络编程,不需要去关心底层的实现了。 Socket连接过程分为三个步骤: 服务器监听,客户端请求,连接确认。

1.2.2 Socket的工作原理

我们只是会用Socket进行通信的编程了,但Socket通信流程究竟是什么样的呢? 如下图

在这里插入图片描述
先从服务器端说起。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。

2. TCP通信概述及Qt中对应的类

TCP 通信必须先建立 TCP 连接,通信端分为客户端和服务器端(如图 14-2 所示)。Qt 提供QTcpSocket 类和 QTcpServer 类用于建立 TCP 通信应用程序。服务器端程序必须使用 QTcpServer 用于端口监听,建立服务器;QTcpSocket 用于建立连接后使用套接字(Socket)进行通信。
在这里插入图片描述
QTcpServer 是从QObject 继承的类,它主要用于服务器端建立网络监听,创建网络 Socket 连接。QTcpServer 类的主要接口函数见表 14-4 (省略了函数中的 const 关键字,省略了缺省参数)。

在这里插入图片描述
服务器端程序首先需要用 QTcpServer::listen()开始服务器端监听,可以指定监听的 IP 地址和端口,一般一个服务程序只监听某个端口的网络连接。
当有新的客户端接入时,QTcpServer 内部的 incomingConnection()函数会创建一个与客户端连接的 QTcpSocket 对象,然后发射信号 newConnection()。在 newConnection()信号的槽函数中,可以用netPendingConnection()接收客户端的连接,然后使用QTcpSocket与客户端通信。

所以在客户端与服务器建立 TCP 连接后,具体的数据通信是通过 QTcpSocket 完成的。QTcpSocket 类提供了 TCP 协议的接口,可以用 QTcpSocket 类实现标准的网络通信协议如 POP3、SMTP 和NNTP,也可以设计自定义协议。

QTcpSocket 是从 QIODevice 间接继承的类,所以具有流读写的功能。QTcpSocket 和下一节要讲到的 QUdpSocket 的类继承关系如图14-3 所示。
在这里插入图片描述
QTcpSocket 类除了构造函数和析构函数,其他函数都是从QAbstractSocket 继承或重定义的。QAbstractSocket 用于 TCP 通信的主要接口函数见表 14-5(省略了函数中的 const 关键字,省略了缺省参数)。

TCP客户端使用QTcpSocket 与 TCP 服务器建立连接并通信。

客户端的 QTcpSocket 实例首先通过 connectToHost()尝试连接到服务器,需要指定服务器的IP 地址和端口。connectToHost()是异步方式连接服务器,不会阻塞程序运行,连接后发射connected()信号。

如果需要使用阻塞方式连接服务器,则使用 waitForConnected()函数阻塞程序运行,直到连接成功或失败。例如:

socket->connectToHost("192.168.1.100"1340)
    if (socket->waitForConnected(1000))
        qDebug("Connected!");

与服务器端建立 socket 连接后,就可以向缓冲区写数据或从接收缓冲区读取数据,实现数据的通信。当缓冲区有新数据进入时,会发射 readyRead()信号,一般在此信号的槽函数里读取缓冲区数据。

QTcpSocket 是从 QIDevice 间接继承的,所以可以使用流数据读写功能。一个 QTcpSocket实例既可以接收数据也可以发送数据,且接收与发射是异步工作的,有各自的缓冲区。

作为演示TCP 通信的实例,创建了一个TCPClient 程序和一个TCPServer 程序,两个程序运行时界面如下图所示。
在这里插入图片描述
注:扫描出的服务器地址有一个127.0.0.1localhost,其实是一个意思,具体见下

127.0.0.1是一个非常有名的IP地址,它是主机环回地址。主机环回指的是,地址为127.0.0.1的数据包不应离开计算机(主机)发送,而不是发送到本地网络或互联网,它只是在自身上“环回”,发送数据包的计算机成为收件人。你可以使用127.0.0.1来测试你的网络设备是否正常工作,或者在本地计算机上运行一些只有你可以访问的服务。127.0.0.1也可以用localhost来代替,它们通常是默认情况下相互对应的。

因此,如下选择也是可以进行通信的
在这里插入图片描述

TCPServer 程序具有如下的功能:

  • 根据指定IP 地址(本机地址)和端口打开网络监听,有客户端连接时创建 socket 连接;
  • 采用基于行的数据通信协议,可以接收客户端发来的消息,也可以向客户端发送消息;
  • 在状态栏显示服务器监听状态和 socket 的状态。

TCPClient 程序具有如下的功能:

  • 通过 IP 地址和端口号连接到服务器;
  • 采用基于行的数据通信协议,与服务器端收发消息;
  • 处理 QTcpSocket 的 StateChange()信号,在状态栏显示 socket 的状态。

注意:TCP通信遵循的连接过程: Socket连接过程分为三个步骤: 服务器监听,客户端请求,连接确认。

3. TCP 服务器端程序设计

3.1 主窗口定义与构造函数

TCPServer 是一个窗口基于 QMainWindow 的应用程序,界面由 UI设计器设计,MainWindow类的定义如下(忽略了 UI 设计器自动生成的actions 和按钮的槽函数):

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

#include    <QTcpServer>
#include    <QLabel>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

private:
    QLabel  *LabListen;//状态栏标签
    QLabel  *LabSocketState;//状态栏标签

    QTcpServer *tcpServer; //TCP服务器

    QTcpSocket *tcpSocket;//TCP通讯的Socket

    QString getLocalIP();//获取本机IP地址

protected:
    void    closeEvent(QCloseEvent *event);

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

private slots:
//自定义槽函数
    void    onNewConnection();//QTcpServer的newConnection()信号

    void    onSocketStateChange(QAbstractSocket::SocketState socketState);
    void    onClientConnected(); //Client Socket connected
    void    onClientDisconnected();//Client Socket disconnected
    void    onSocketReadyRead();//读取socket传入的数据
...
private:
    Ui::MainWindow *ui;
};

#endif // MAINWINDOW_H

MainWindow 中定义了私有变量 tcpServer 用于建立 TCP 服务器,定义了 tcpSocket 用于与客户端进行 socket连接和通信。
定义了几个槽函数,用于与 QTcpServer 和 QTcpSocket 的相关信号连接,实现相应的处理。

MainWindow 构造函数代码如下:

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    LabListen=new QLabel(QString::fromLocal8Bit("监听状态:"));
    LabListen->setMinimumWidth(150);
    ui->statusBar->addWidget(LabListen);

    LabSocketState=new QLabel(QString::fromLocal8Bit("Socket状态:"));//
    LabSocketState->setMinimumWidth(200);
    ui->statusBar->addWidget(LabSocketState);

    QString localIP=getLocalIP();//本机IP
    this->setWindowTitle(this->windowTitle()+QString::fromLocal8Bit("----本机IP:")+localIP);
    ui->comboIP->addItem(localIP);

    tcpServer=new QTcpServer(this);
    connect(tcpServer,SIGNAL(newConnection()),this,SLOT(onNewConnection()));
}

QString MainWindow::getLocalIP()
{//获取本机IPv4地址
    QString hostName=QHostInfo::localHostName();//本地主机名
    QHostInfo   hostInfo=QHostInfo::fromName(hostName);
    QString   localIP="";

    QList<QHostAddress> addList=hostInfo.addresses();//

    if (!addList.isEmpty())
    for (int i=0;i<addList.count();i++)
    {
        QHostAddress aHost=addList.at(i);
        if (QAbstractSocket::IPv4Protocol==aHost.protocol())
        {
            localIP=aHost.toString();
            break;
        }
    }
    return localIP;
}

MainWindow 的构造函数创建状态栏上的标签用于信息显示,调用自定义函数 getLocalIP())获取本机IP 地址,并显示到标题栏上。创建 QTcpServer 实例 tcpServer,并将其 newConnection()信号与onNewConnection()槽函数关联。

3.2 网络监听与 socket 连接的建立

作为TCP 服务器,QTcpServer 类需要调用 listen()在本机某个IP 地址和端口上开始 TCP 监听,以等待 TCP 客户端的接入。

单击主窗口上“开始监听”按钮可以开始网络监听,其代码如下:

void MainWindow::on_actStart_triggered()
{//开始监听
    QString     IP=ui->comboIP->currentText();//IP地址
    quint16     port=ui->spinPort->value();//端口
    QHostAddress    addr(IP);
    tcpServer->listen(addr,port);//
//    tcpServer->listen(QHostAddress::LocalHost,port);// Equivalent to QHostAddress("127.0.0.1").
    ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("**开始监听..."));
    ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("**服务器地址:")
                       +tcpServer->serverAddress().toString());
    ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("**服务器端口:")
                       +QString::number(tcpServer->serverPort()));

    ui->actStart->setEnabled(false);
    ui->actStop->setEnabled(true);

    LabListen->setText(QString::fromLocal8Bit("监听状态:正在监听"));
}

程序读取窗口上设置的监听地址和监听端口,然后调用 QTcpServer 的 listen()函数开始监听。TCP 服务器在本机上监听,所以 IP 地址可以是表示本机的“127.0.0.1”,或是本机的实际 IP,亦或是常量 QHostAddress::LocalHost,即在本机上监听某个端口也可以写成:

tcpServer->listen(QHostAddress::LocalHost);

tcpServer 开始监听后,TCPClient 就可以通过IP 地址和端口连接到此服务器。当有客户端接入时,tcpServer 会发射 newConnection()信号,此信号关联的槽函数 onNewConnection()的代码如下:

void MainWindow::onNewConnection()
{
//    ui->plainTextEdit->appendPlainText("有新连接");
    tcpSocket = tcpServer->nextPendingConnection(); //创建socket

    connect(tcpSocket, SIGNAL(connected()),
            this, SLOT(onClientConnected()));
    onClientConnected();//

    connect(tcpSocket, SIGNAL(disconnected()),
            this, SLOT(onClientDisconnected()));

    connect(tcpSocket,SIGNAL(stateChanged(QAbstractSocket::SocketState)),
            this,SLOT(onSocketStateChange(QAbstractSocket::SocketState)));
    onSocketStateChange(tcpSocket->state());

    connect(tcpSocket,SIGNAL(readyRead()),
            this,SLOT(onSocketReadyRead()));
}

程序首先通过 nextPendingConnection()函数获取与接入连接进行通信的 QTcpSocket 对象实例tcpSocket,然后将 tcpSocket 的几个信号与相应的槽函数连接起来。QTcpSocket 的这几个信号的作用是:

  • connected()信号,客户端 socket 连接建立时发射此信号;
  • disconnected()信号,客户端 socket 连接断开时发射此信号;
  • stateChanged(),本程序的 socket 状态变化时发射此信号:
  • readyRead(),本程序的 socket 的读取缓冲区有新数据时发射此信号。

涉及状态变化的几个信号的槽函数代码如下:

void MainWindow::onClientConnected()
{//客户端接入时
    ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("**client socket connected"));
    ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("**peer address:")+
                                   tcpSocket->peerAddress().toString());
    ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("**peer port:")+
                                   QString::number(tcpSocket->peerPort()));
}

void MainWindow::onClientDisconnected()
{//客户端断开连接时
    ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("**client socket disconnected"));
    tcpSocket->deleteLater();
    //    deleteLater();//QObject::deleteLater();
}

void MainWindow::onSocketStateChange(QAbstractSocket::SocketState socketState)
{//socket状态变化时
    switch(socketState)
    {
    case QAbstractSocket::UnconnectedState:
        LabSocketState->setText(QString::fromLocal8Bit("scoket状态:UnconnectedState"));
        break;
    case QAbstractSocket::HostLookupState:
        LabSocketState->setText(QString::fromLocal8Bit("scoket状态:HostLookupState"));
        break;
    case QAbstractSocket::ConnectingState:
        LabSocketState->setText(QString::fromLocal8Bit("scoket状态:ConnectingState"));
        break;

    case QAbstractSocket::ConnectedState:
        LabSocketState->setText(QString::fromLocal8Bit("scoket状态:ConnectedState"));
        break;

    case QAbstractSocket::BoundState:
        LabSocketState->setText(QString::fromLocal8Bit("scoket状态:BoundState"));
        break;

    case QAbstractSocket::ClosingState:
        LabSocketState->setText(QString::fromLocal8Bit("scoket状态:ClosingState"));
        break;

    case QAbstractSocket::ListeningState:
        LabSocketState->setText(QString::fromLocal8Bit("scoket状态:ListeningState"));
    }
}

TCP 服务器停止监听,只需调用 QTcpServer 的 close()函数即可。窗口上的“停止监听”响应代码如下:

void MainWindow::on_actStop_triggered()
{//停止监听
    if (tcpServer->isListening()) //tcpServer正在监听
    {
        tcpServer->close();//停止监听
        ui->actStart->setEnabled(true);
        ui->actStop->setEnabled(false);
        LabListen->setText(QString::fromLocal8Bit("监听状态:已停止监听"));
    }
}

3.3 与TCPClient 的数据通信

TCP 服务器端和客户端之间通过 QTcpSocket 通信时,需要规定两者之间的通信协议,即传输的数据内容如何解析。QTcpSocket 间接继承于 QIODevice,所以支持流读写功能。

Socket 之间的数据通信协议一般有两种方式,基于行的或基于数据块的。

基于行的数据通信协议一般用于纯文本数据的通信,每一行数据以一个换行符结束。canReadLine()函数判断是否有新的一行数据需要读取,再用 readLine()函数读取一行数据,例如:

    while(tcpSocket->canReadLine())
        ui->plainTextEdit->appendPlainText("[in] "+tcpSocket->readLine());

基于块的数据通信协议用于一般的二进制数据的传输,需要自定义具体的格式。

实例程序 TCPServer 和 TCPClient 只是进行字符串的信息传输,类似于一个简单的聊天程序,程序采用基于行的数据通信协议。
单击窗口上的“发送消息”,将文本框里的字符串发送给客户端,其实现代码如下:

void MainWindow::on_btnSend_clicked()
{//发送一行字符串,以换行符结束
    QString  msg=ui->editMsg->text();
    ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("[out] ")+msg);
    ui->editMsg->clear();
    ui->editMsg->setFocus();

    QByteArray  str=msg.toUtf8();
    str.append('\n');//添加一个换行符
    tcpSocket->write(str);
}

从上面的代码中可以看到,读取文本框中的字符串到 msg 后,先将其转换为 QByteArray 类型字节数组 str,然后在 str 最后面添加一个换行符,用 QIODevice 的 write()函数写入缓冲区,这样就向客户端发送一行文字。
QTcpSocket 接收到数据后,会发射 readyRead()信号,在onNewConnection()槽函数中已经建立了这个信号与槽函数 onSocketReadyRead()的连接。

槽函数 onSocketReadyRead()实现缓冲区数据的读取,其代码如下:

void MainWindow::onSocketReadyRead()
{//读取缓冲区行文本
//    QStringList   lines;
    while(tcpSocket->canReadLine())
        ui->plainTextEdit->appendPlainText("[in] "+tcpSocket->readLine());
//        lines.append(clientConnection->readLine());
}

这样,TCPServer 就可以与 TCPClient 之间进行双向通信了,且这个连接将一直存在,直到某方的 QTcpSocket 对象调用 disconnectFromHost()函数断开 socket 连接。

3.4 源码

3.4.1 可视化UI设计框架

在这里插入图片描述

3.4.2 mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

#include    <QTcpServer>
#include    <QLabel>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

private:
    QLabel  *LabListen;//状态栏标签
    QLabel  *LabSocketState;//状态栏标签

    QTcpServer *tcpServer; //TCP服务器

    QTcpSocket *tcpSocket;//TCP通讯的Socket

    QString getLocalIP();//获取本机IP地址

protected:
    void    closeEvent(QCloseEvent *event);

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

private slots:
//自定义槽函数
    void    onNewConnection();//QTcpServer的newConnection()信号

    void    onSocketStateChange(QAbstractSocket::SocketState socketState);
    void    onClientConnected(); //Client Socket connected
    void    onClientDisconnected();//Client Socket disconnected
    void    onSocketReadyRead();//读取socket传入的数据
//UI生成的
    void on_actStart_triggered();

    void on_actStop_triggered();

    void on_actClear_triggered();

    void on_btnSend_clicked();

    void on_actHostInfo_triggered();

private:
    Ui::MainWindow *ui;
};

#endif // MAINWINDOW_H

3.4.3 mainwindow.cpp

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include    <QtNetwork>


QString MainWindow::getLocalIP()
{//获取本机IPv4地址
    QString hostName=QHostInfo::localHostName();//本地主机名
    QHostInfo   hostInfo=QHostInfo::fromName(hostName);
    QString   localIP="";

    QList<QHostAddress> addList=hostInfo.addresses();//

    if (!addList.isEmpty())
    for (int i=0;i<addList.count();i++)
    {
        QHostAddress aHost=addList.at(i);
        if (QAbstractSocket::IPv4Protocol==aHost.protocol())
        {
            localIP=aHost.toString();
            break;
        }
    }
    return localIP;
}

void MainWindow::closeEvent(QCloseEvent *event)
{//关闭窗口时停止监听
    if (tcpServer->isListening())
        tcpServer->close();;//停止网络监听
    event->accept();
}

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    LabListen=new QLabel(QString::fromLocal8Bit("监听状态:"));
    LabListen->setMinimumWidth(150);
    ui->statusBar->addWidget(LabListen);

    LabSocketState=new QLabel(QString::fromLocal8Bit("Socket状态:"));//
    LabSocketState->setMinimumWidth(200);
    ui->statusBar->addWidget(LabSocketState);

    QString localIP=getLocalIP();//本机IP
    this->setWindowTitle(this->windowTitle()+QString::fromLocal8Bit("----本机IP:")+localIP);
    ui->comboIP->addItem(localIP);

    tcpServer=new QTcpServer(this);
    connect(tcpServer,SIGNAL(newConnection()),this,SLOT(onNewConnection()));
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::onNewConnection()
{
//    ui->plainTextEdit->appendPlainText("有新连接");
    tcpSocket = tcpServer->nextPendingConnection(); //创建socket

    connect(tcpSocket, SIGNAL(connected()),
            this, SLOT(onClientConnected()));
    onClientConnected();//

    connect(tcpSocket, SIGNAL(disconnected()),
            this, SLOT(onClientDisconnected()));

    connect(tcpSocket,SIGNAL(stateChanged(QAbstractSocket::SocketState)),
            this,SLOT(onSocketStateChange(QAbstractSocket::SocketState)));
    onSocketStateChange(tcpSocket->state());

    connect(tcpSocket,SIGNAL(readyRead()),
            this,SLOT(onSocketReadyRead()));
}

void MainWindow::onSocketStateChange(QAbstractSocket::SocketState socketState)
{//socket状态变化时
    switch(socketState)
    {
    case QAbstractSocket::UnconnectedState:
        LabSocketState->setText(QString::fromLocal8Bit("scoket状态:UnconnectedState"));
        break;
    case QAbstractSocket::HostLookupState:
        LabSocketState->setText(QString::fromLocal8Bit("scoket状态:HostLookupState"));
        break;
    case QAbstractSocket::ConnectingState:
        LabSocketState->setText(QString::fromLocal8Bit("scoket状态:ConnectingState"));
        break;

    case QAbstractSocket::ConnectedState:
        LabSocketState->setText(QString::fromLocal8Bit("scoket状态:ConnectedState"));
        break;

    case QAbstractSocket::BoundState:
        LabSocketState->setText(QString::fromLocal8Bit("scoket状态:BoundState"));
        break;

    case QAbstractSocket::ClosingState:
        LabSocketState->setText(QString::fromLocal8Bit("scoket状态:ClosingState"));
        break;

    case QAbstractSocket::ListeningState:
        LabSocketState->setText(QString::fromLocal8Bit("scoket状态:ListeningState"));
    }
}

void MainWindow::onClientConnected()
{//客户端接入时
    ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("**client socket connected"));
    ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("**peer address:")+
                                   tcpSocket->peerAddress().toString());
    ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("**peer port:")+
                                   QString::number(tcpSocket->peerPort()));
}

void MainWindow::onClientDisconnected()
{//客户端断开连接时
    ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("**client socket disconnected"));
    tcpSocket->deleteLater();
    //    deleteLater();//QObject::deleteLater();
}

void MainWindow::onSocketReadyRead()
{//读取缓冲区行文本
//    QStringList   lines;
    while(tcpSocket->canReadLine())
        ui->plainTextEdit->appendPlainText("[in] "+tcpSocket->readLine());
//        lines.append(clientConnection->readLine());
}

void MainWindow::on_actStart_triggered()
{//开始监听
    QString     IP=ui->comboIP->currentText();//IP地址
    quint16     port=ui->spinPort->value();//端口
    QHostAddress    addr(IP);
    tcpServer->listen(addr,port);//
//    tcpServer->listen(QHostAddress::LocalHost,port);// Equivalent to QHostAddress("127.0.0.1").
    ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("**开始监听..."));
    ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("**服务器地址:")
                       +tcpServer->serverAddress().toString());
    ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("**服务器端口:")
                       +QString::number(tcpServer->serverPort()));

    ui->actStart->setEnabled(false);
    ui->actStop->setEnabled(true);

    LabListen->setText(QString::fromLocal8Bit("监听状态:正在监听"));
}

void MainWindow::on_actStop_triggered()
{//停止监听
    if (tcpServer->isListening()) //tcpServer正在监听
    {
        tcpServer->close();//停止监听
        ui->actStart->setEnabled(true);
        ui->actStop->setEnabled(false);
        LabListen->setText(QString::fromLocal8Bit("监听状态:已停止监听"));
    }
}

void MainWindow::on_actClear_triggered()
{
    ui->plainTextEdit->clear();
}

void MainWindow::on_btnSend_clicked()
{//发送一行字符串,以换行符结束
    QString  msg=ui->editMsg->text();
    ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("[out] ")+msg);
    ui->editMsg->clear();
    ui->editMsg->setFocus();

    QByteArray  str=msg.toUtf8();
    str.append('\n');//添加一个换行符
    tcpSocket->write(str);
}

void MainWindow::on_actHostInfo_triggered()
{//获取本机地址
    QString hostName=QHostInfo::localHostName();//本地主机名
    ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("本机主机名:")+hostName+"\n");
    QHostInfo   hostInfo=QHostInfo::fromName(hostName);

    QList<QHostAddress> addList=hostInfo.addresses();//
    if (!addList.isEmpty())
    for (int i=0;i<addList.count();i++)
    {
        QHostAddress aHost=addList.at(i);
        if (QAbstractSocket::IPv4Protocol==aHost.protocol())
        {
            QString IP=aHost.toString();
            ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("本机IP地址:")+aHost.toString());
            if (ui->comboIP->findText(IP)<0)
                ui->comboIP->addItem(IP);
        }
    }

}

4. TCP 客户端程序设计

4.1 主窗口定义与构造函数

客户端程序 TCPClient 只需要使用一个 QTcpSocket 对象,就可以和服务器端程序 TCPServer进行通信。TCPClient 也是一个窗口基于 QMainWindow 的应用程序,其主窗口的定义如下:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include    <QTcpSocket>
#include    <QLabel>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT
private:
    QTcpSocket  *tcpClient;  //socket
    QLabel  *LabSocketState;  //状态栏显示标签

    QString getLocalIP();//获取本机IP地址
protected:
    void    closeEvent(QCloseEvent *event);
public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

private slots:
//自定义槽函数
    void    onConnected();
    void    onDisconnected();
    void    onSocketStateChange(QAbstractSocket::SocketState socketState);
    void    onSocketReadyRead();//读取socket传入的数据
...

private:
    Ui::MainWindow *ui;
};

#endif // MAINWINDOW_H

这里只定义了一个用于 socket 连接和通信的 QTcpSocket 变量 tcpClient,自定义了几个槽函数,用于与 tcpClient 的相关信号关联。
下面是 MainWindow 的构造函数,主要功能是创建 tcpClient,并建立信号与槽函数的关联。

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    tcpClient=new QTcpSocket(this); //创建socket变量

    LabSocketState=new QLabel(QString::fromLocal8Bit("Socket状态:"));//状态栏标签
    LabSocketState->setMinimumWidth(250);
    ui->statusBar->addWidget(LabSocketState);

    QString localIP=getLocalIP();//本机IP
    this->setWindowTitle(this->windowTitle()+QString::fromLocal8Bit("----本机IP:")+localIP);
    ui->comboServer->addItem(localIP);


    connect(tcpClient,SIGNAL(connected()),this,SLOT(onConnected()));
    connect(tcpClient,SIGNAL(disconnected()),this,SLOT(onDisconnected()));

    connect(tcpClient,SIGNAL(stateChanged(QAbstractSocket::SocketState)),
            this,SLOT(onSocketStateChange(QAbstractSocket::SocketState)));
    connect(tcpClient,SIGNAL(readyRead()),
            this,SLOT(onSocketReadyRead()));
}

4.2 与服务器端建立 socket 连接

在窗口上设置服务器 IP 地址和端口后,调用 QTcpSocket 的函数 connectToHost()连接到服务器,也可以使用disconnectFromHost()函数断开与服务器的连接。

下面是两个按钮的响应代码,以及两个相关槽函数的代码:

void MainWindow::on_actConnect_triggered()
{//连接到服务器
    QString     addr=ui->comboServer->currentText();
    quint16     port=ui->spinPort->value();
    tcpClient->connectToHost(addr,port);
//    tcpClient->connectToHost(QHostAddress::LocalHost,port);
}

void MainWindow::on_actDisconnect_triggered()
{//断开与服务器的连接
    if (tcpClient->state()==QAbstractSocket::ConnectedState)
        tcpClient->disconnectFromHost();
}

void MainWindow::onConnected()
{ //connected()信号槽函数
    ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("**已连接到服务器"));
    ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("**peer address:")+
                                   tcpClient->peerAddress().toString());
    ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("**peer port:")+
                                   QString::number(tcpClient->peerPort()));
    ui->actConnect->setEnabled(false);
    ui->actDisconnect->setEnabled(true);
}

void MainWindow::onDisconnected()
{//disConnected()信号槽函数
    ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("**已断开与服务器的连接"));
    ui->actConnect->setEnabled(true);
    ui->actDisconnect->setEnabled(false);
}

槽函数onSocketStateChange()的功能和代码与 TCPServer 中的完全一样,这里不再赘述。

4.3 与TCPServer 的数据收发

TCPClient与TCPServer 之间采用基于行的数据通信协议。单击“发送消息”按钮将发送一行字符串。在 readyRead()信号的槽函数里读取行字符串,其相关代码如下:

void MainWindow::on_btnSend_clicked()
{//发送数据
    QString  msg=ui->editMsg->text();
    ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("[out] ")+msg);
    ui->editMsg->clear();
    ui->editMsg->setFocus();

    QByteArray  str=msg.toUtf8();
    str.append('\n');
    tcpClient->write(str);
}

void MainWindow::onSocketReadyRead()
{//readyRead()信号槽函数
    while(tcpClient->canReadLine())
        ui->plainTextEdit->appendPlainText("[in] "+tcpClient->readLine());
}

4.4 源码

4.4.1 可视化UI设计框架

在这里插入图片描述

4.4.2 mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include    <QTcpSocket>
#include    <QLabel>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT
private:
    QTcpSocket  *tcpClient;  //socket
    QLabel  *LabSocketState;  //状态栏显示标签

    QString getLocalIP();//获取本机IP地址
protected:
    void    closeEvent(QCloseEvent *event);
public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

private slots:
//自定义槽函数
    void    onConnected();
    void    onDisconnected();
    void    onSocketStateChange(QAbstractSocket::SocketState socketState);
    void    onSocketReadyRead();//读取socket传入的数据
//
    void on_actConnect_triggered();

    void on_actDisconnect_triggered();

    void on_actClear_triggered();

    void on_btnSend_clicked();

private:
    Ui::MainWindow *ui;
};

#endif // MAINWINDOW_H

4.4.3 mainwindow.cpp

#include "mainwindow.h"
#include "ui_mainwindow.h"

#include    <QHostAddress>
#include    <QHostInfo>

QString MainWindow::getLocalIP()
{
    QString hostName=QHostInfo::localHostName();//本地主机名
    QHostInfo   hostInfo=QHostInfo::fromName(hostName);
    QString   localIP="";

    QList<QHostAddress> addList=hostInfo.addresses();//

    if (!addList.isEmpty())
    for (int i=0;i<addList.count();i++)
    {
        QHostAddress aHost=addList.at(i);
        if (QAbstractSocket::IPv4Protocol==aHost.protocol())
        {
            localIP=aHost.toString();
            break;
        }
    }
    return localIP;
}
void MainWindow::closeEvent(QCloseEvent *event)
{
    if (tcpClient->state()==QAbstractSocket::ConnectedState)
        tcpClient->disconnectFromHost();
    event->accept();
}

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    tcpClient=new QTcpSocket(this); //创建socket变量

    LabSocketState=new QLabel(QString::fromLocal8Bit("Socket状态:"));//状态栏标签
    LabSocketState->setMinimumWidth(250);
    ui->statusBar->addWidget(LabSocketState);

    QString localIP=getLocalIP();//本机IP
    this->setWindowTitle(this->windowTitle()+QString::fromLocal8Bit("----本机IP:")+localIP);
    ui->comboServer->addItem(localIP);


    connect(tcpClient,SIGNAL(connected()),this,SLOT(onConnected()));
    connect(tcpClient,SIGNAL(disconnected()),this,SLOT(onDisconnected()));

    connect(tcpClient,SIGNAL(stateChanged(QAbstractSocket::SocketState)),
            this,SLOT(onSocketStateChange(QAbstractSocket::SocketState)));
    connect(tcpClient,SIGNAL(readyRead()),
            this,SLOT(onSocketReadyRead()));
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::onConnected()
{ //connected()信号槽函数
    ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("**已连接到服务器"));
    ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("**peer address:")+
                                   tcpClient->peerAddress().toString());
    ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("**peer port:")+
                                   QString::number(tcpClient->peerPort()));
    ui->actConnect->setEnabled(false);
    ui->actDisconnect->setEnabled(true);
}

void MainWindow::onDisconnected()
{//disConnected()信号槽函数
    ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("**已断开与服务器的连接"));
    ui->actConnect->setEnabled(true);
    ui->actDisconnect->setEnabled(false);
}

void MainWindow::onSocketReadyRead()
{//readyRead()信号槽函数
    while(tcpClient->canReadLine())
        ui->plainTextEdit->appendPlainText("[in] "+tcpClient->readLine());
}

void MainWindow::onSocketStateChange(QAbstractSocket::SocketState socketState)
{//stateChange()信号槽函数
    switch(socketState)
    {
    case QAbstractSocket::UnconnectedState:
        LabSocketState->setText(QString::fromLocal8Bit("scoket状态:UnconnectedState"));
        break;
    case QAbstractSocket::HostLookupState:
        LabSocketState->setText(QString::fromLocal8Bit("scoket状态:HostLookupState"));
        break;
    case QAbstractSocket::ConnectingState:
        LabSocketState->setText(QString::fromLocal8Bit("scoket状态:ConnectingState"));
        break;

    case QAbstractSocket::ConnectedState:
        LabSocketState->setText(QString::fromLocal8Bit("scoket状态:ConnectedState"));
        break;

    case QAbstractSocket::BoundState:
        LabSocketState->setText(QString::fromLocal8Bit("scoket状态:BoundState"));
        break;

    case QAbstractSocket::ClosingState:
        LabSocketState->setText(QString::fromLocal8Bit("scoket状态:ClosingState"));
        break;

    case QAbstractSocket::ListeningState:
        LabSocketState->setText(QString::fromLocal8Bit("scoket状态:ListeningState"));
    }
}

void MainWindow::on_actConnect_triggered()
{//连接到服务器
    QString     addr=ui->comboServer->currentText();
    quint16     port=ui->spinPort->value();
    tcpClient->connectToHost(addr,port);
//    tcpClient->connectToHost(QHostAddress::LocalHost,port);
}

void MainWindow::on_actDisconnect_triggered()
{//断开与服务器的连接
    if (tcpClient->state()==QAbstractSocket::ConnectedState)
        tcpClient->disconnectFromHost();
}

void MainWindow::on_actClear_triggered()
{
    ui->plainTextEdit->clear();
}

void MainWindow::on_btnSend_clicked()
{//发送数据
    QString  msg=ui->editMsg->text();
    ui->plainTextEdit->appendPlainText(QString::fromLocal8Bit("[out] ")+msg);
    ui->editMsg->clear();
    ui->editMsg->setFocus();

    QByteArray  str=msg.toUtf8();
    str.append('\n');
    tcpClient->write(str);
}

实例 TCPServer 和 TCPClient 只是简单演示了 TCP 通信的基本原理,TCPServer 只允许一个TCPClient 客户端接入。而一般的 TCP 服务器程序允许多个客户端接入,为了使每个 socket 连接独立通信互不影响,一般采用多线程,即为一个 socket 连接创建一个线程。
实例 TCPServer和 TCPClient 之间的数据通信采用基于行的通信协议,只能传输字符串数据。QTcpSocket 间接继承于 QIODevice,可以使用数据流的方式传输二进制数据流,例如传输图片任意格式文件等,但是这涉及到服务器端和客户端之间通信协议的定义,此处不具体介绍了。

5. 利用TCP 客户端程序进行网站服务器连接

这里演示使用上面的TCP 客户端程序与百度的网页服务器进行连接

5.1 获取网站IP地址

在这里插入图片描述

5.2 利用TCP 客户端程序进行网站服务器连接

网页服务器的标准端口是 80 端,因此客户端中填入服务器地址和端口后,点击“连接到服务器”即可

在这里插入图片描述

5.3 发送请求命令及回复释义(模拟浏览器操作)

#连接到网页服务器
**已连接到服务器
**peer address:14.119.104.189
**peer port:80
#发送GET命令,获取百度首页
[out] GET / HTTP/1.1
#可能是命令有问题,服务器断开
**已断开与服务器的连接
#再次连接服务器
**已连接到服务器
**peer address:14.119.104.189
**peer port:80
#发送1命令,看起来命令存在问题,并且断开连接
[out] 1
[in] HTTP/1.1 400 Bad Request

[in] 

**已断开与服务器的连接
#连接服务器,如果不进行动作,服务器过一段时间会自动断开
**已连接到服务器
**peer address:14.119.104.189
**peer port:80
**已断开与服务器的连接

6.网络中进程之间如何通信

以上述建立连接过程引出更深层的一个问题? 网络中进程之间如何通信?下面的介绍也能解决客户端与客户端之间识别的问题。

本地的进程间通信 (IPC) 有很多种方式,但可以总结为下面4类。

  • 消息传递 (管道、FIFO、消息队列)
  • 同步 (互斥量、条件变量、读写锁、文件和写记录锁、信号量)
  • 共享内存 (匿名的和具名的)
  • 远程过程调用(Solaris门和Sun RPC)

我们要讨论的是网络中进程之间如何通信? 首要解决的问题是如何唯一标识一个进程,否则通信无从谈起!

在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的。其实TCP/IP协议族已经帮我们解决了这个问题,网络层的 "ip地址” 可以唯一标识网络中的主机,而传输层的 “协议+端口” 可以唯一标识主机中的应用程序(进程)。这样利用 三元组 (ip地址,协议,端口) 就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。

使用TCP/IP协议的应用程序通常采用应用编程接口: UNIX BSD的套接字 (socket) 和UNIX System V的TLI (已经被淘汰),来实现网络进程之间的通信。

还有一个问题就是socket指的是(lP, Port),现在假设我已经有了一个istenfd 的socket, 端口是80 然后每次客户端发起连接还要创建新的connfd,因为80端口已经被占用,难道服务器端会为每个连接都创建新的端口吗?

其实新创建的connfd 并没有使用新的端口号,也是用的80,如在实现聊天室功能的时候,我们只是为每个客户端的连接单独创建一个线程去处理,但并没有为每个连接都创建新的端口。这是什么原因呢?

因为可以这么理解,这个socket描述符指向一个数据结构,例如 listenfd 指向的结构是这样的:
在这里插入图片描述

而一旦accept 新的连接,新的connfd 就会生成,像下面的表格,就生成了两个connfd,它们俩服务器端的ip和port都是相同的,但是客户端的IP和Port是不同的,自然就可以区分开来了。
在这里插入图片描述
结论: socket 得通过五元组(协议,客户端IP,客户端Port,服务器端IP,服务器端Port)来确定

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

十月旧城

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值