TCP 简介
TCP 协议(Transmission Control Protocol)全称是传输控制协议是一种面向连接的、可靠的、 基于字节流的传输层通信协议。
TCP 通信必须先建立 TCP 连接,通信端分为客户端和服务端。服务端通过监听某个端口来监听是否有客户端连接到来,如果有连接到来,则建立新的 socket 连接;客户端通过 ip 和 port 连接服务端,当成功建立连接之后,就可进行数据的收发了。需要注意的是,在 Qt 中, Qt把 socket 当成输入输出流来对待的,数据的收发是通过 read()和 write()来进行的,需要与我们常见的 send()与 recv()进行区分。
TCP 客户端与服务端通信示意图如下。
更多关于网络的知识可以阅读网络专栏。
TCP 服务端应用实例
本例目的:了解 TCP 服务端的使用。
项目名称:tcpserver
流程:首先获取本地 IP 地址。创建一个 tcpSocket 套接字,一个 tcpServer 服务端。点击监听即监听本地的主机 IP 地址和端口,同时等待服务端的连接。此程序需要结合客户端一起使用。
项目文件tcpserver.pro 文件第一行添加的代码部分如下。
QT += core gui network
在头文件“mainwindow.h”具体代码如下。
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QTcpServer>
#include <QTcpSocket>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
#include <QTextBrowser>
#include <QLabel>
#include <QComboBox>
#include <QSpinBox>
#include <QHostInfo>
#include <QLineEdit>
#include <QNetworkInterface>
#include <QDebug>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
/* tcp服务器 */
QTcpServer *tcpServer;
/* 通信套接字 */
QTcpSocket *tcpSocket;
/* 按钮 */
QPushButton *pushButton[4];
/* 标签文本 */
QLabel *label[2];
/* 水平容器 */
QWidget *hWidget[3];
/* 水平布局 */
QHBoxLayout *hBoxLayout[3];
/* 垂直容器 */
QWidget *vWidget;
/* 垂直布局 */
QVBoxLayout *vBoxLayout;
/* 文本浏览框 */
QTextBrowser *textBrowser;
/* 用于显示本地ip */
QComboBox *comboBox;
/* 用于选择端口 */
QSpinBox *spinBox;
/* 文本输入框 */
QLineEdit *lineEdit;
/* 存储本地的ip列表地址 */
QList<QHostAddress> IPlist;
/* 获取本地的所有ip */
void getLocalHostIP();
private slots:
/* 客户端连接处理槽函数 */
void clientConnected();
/* 开始监听槽函数 */
void startListen();
/* 停止监听槽函数 */
void stopListen();
/* 清除文本框时的内容 */
void clearTextBrowser();
/* 接收到消息 */
void receiveMessages();
/* 发送消息 */
void sendMessages();
/* 连接状态改变槽函数 */
void socketStateChange(QAbstractSocket::SocketState);
};
#endif // MAINWINDOW_H
头文件里主要是声明界面用的元素,及一些槽函数。重点是声明 tcpServer 和 tcpSocket。
在源文件“mainwindow.cpp”具体代码如下。
#include "mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
/* 设置主窗体的位置与大小 */
this->setGeometry(0, 0, 800, 480);
/* 实例化tcp服务器与tcp套接字 */
tcpServer = new QTcpServer(this);
tcpSocket = new QTcpSocket(this);
/* 开始监听按钮 */
pushButton[0] = new QPushButton();
/* 停止监听按钮 */
pushButton[1] = new QPushButton();
/* 清空聊天文本按钮 */
pushButton[2] = new QPushButton();
/* 发送消息按钮 */
pushButton[3] = new QPushButton();
/* 水平布局一 */
hBoxLayout[0] = new QHBoxLayout();
/* 水平布局二 */
hBoxLayout[1] = new QHBoxLayout();
/* 水平布局三 */
hBoxLayout[2] = new QHBoxLayout();
/* 水平布局四 */
hBoxLayout[3] = new QHBoxLayout();
/* 水平容器一 */
hWidget[0] = new QWidget();
/* 水平容器二 */
hWidget[1] = new QWidget();
/* 水平容器三 */
hWidget[2] = new QWidget();
vWidget = new QWidget();
vBoxLayout = new QVBoxLayout();
/* 标签实例化 */
label[0] = new QLabel();
label[1] = new QLabel();
lineEdit = new QLineEdit();
comboBox = new QComboBox();
spinBox = new QSpinBox();
textBrowser = new QTextBrowser();
label[0]->setText("监听IP地址:");
label[1]->setText("监听端口:");
/* 设置标签根据文本文字大小自适应大小 */
label[0]->setSizePolicy(QSizePolicy::Fixed,
QSizePolicy::Fixed);
label[1]->setSizePolicy(QSizePolicy::Fixed,
QSizePolicy::Fixed);
/* 设置端口号的范围,注意不要与主机的已使用的端口号冲突 */
spinBox->setRange(10000, 99999);
pushButton[0]->setText("开始监听");
pushButton[1]->setText("停止监听");
pushButton[2]->setText("清空文本");
pushButton[3]->setText("发送消息");
/* 设置停止监听状态不可用 */
pushButton[1]->setEnabled(false);
/* 设置输入框默认的文本 */
lineEdit->setText("www.openedv.com正点原子论坛");
/* 水平布局一添加内容 */
hBoxLayout[0]->addWidget(pushButton[0]);
hBoxLayout[0]->addWidget(pushButton[1]);
hBoxLayout[0]->addWidget(pushButton[2]);
/* 设置水平容器一的布局为水平布局一 */
hWidget[0]->setLayout(hBoxLayout[0]);
/* 水平布局二添加内容 */
hBoxLayout[1]->addWidget(label[0]);
hBoxLayout[1]->addWidget(comboBox);
hBoxLayout[1]->addWidget(label[1]);
hBoxLayout[1]->addWidget(spinBox);
/* 设置水平容器二的布局为水平布局二 */
hWidget[1]->setLayout(hBoxLayout[1]);
/* 水平布局三添加内容 */
hBoxLayout[2]->addWidget(lineEdit);
hBoxLayout[2]->addWidget(pushButton[3]);
/* 设置水平容器三的布局为水平布局一 */
hWidget[2]->setLayout(hBoxLayout[2]);
/* 垂直布局添加内容 */
vBoxLayout->addWidget(textBrowser);
vBoxLayout->addWidget(hWidget[1]);
vBoxLayout->addWidget(hWidget[0]);
vBoxLayout->addWidget(hWidget[2]);
/* 设置垂直容器的布局为垂直布局 */
vWidget->setLayout(vBoxLayout);
/* 居中显示 */
setCentralWidget(vWidget);
/* 获取本地ip */
getLocalHostIP();
/* 信号槽连接 */
connect(pushButton[0], SIGNAL(clicked()),
this, SLOT(startListen()));
connect(pushButton[1], SIGNAL(clicked()),
this, SLOT(stopListen()));
connect(pushButton[2], SIGNAL(clicked()),
this, SLOT(clearTextBrowser()));
connect(pushButton[3], SIGNAL(clicked()),
this, SLOT(sendMessages()));
connect(tcpServer, SIGNAL(newConnection()),
this, SLOT(clientConnected()));
}
MainWindow::~MainWindow()
{
}
/* 新的客户端连接 */
void MainWindow::clientConnected()
{
/* 获取客户端的套接字 */
tcpSocket = tcpServer->nextPendingConnection();
/* 客户端的ip信息 */
QString ip = tcpSocket->peerAddress().toString();
/* 客户端的端口信息 */
quint16 port = tcpSocket->peerPort();
/* 在文本浏览框里显示出客户端的连接信息 */
textBrowser->append("客户端已连接");
textBrowser->append("客户端ip地址:"
+ ip);
textBrowser->append("客户端端口:"
+ QString::number(port));
connect(tcpSocket, SIGNAL(readyRead()),
this, SLOT(receiveMessages()));
connect(tcpSocket,
SIGNAL(stateChanged(QAbstractSocket::SocketState)),
this,
SLOT(socketStateChange(QAbstractSocket::SocketState)));
}
/* 获取本地IP */
void MainWindow::getLocalHostIP()
{
// /* 获取主机的名称 */
// QString hostName = QHostInfo::localHostName();
// /* 主机的信息 */
// QHostInfo hostInfo = QHostInfo::fromName(hostName);
// /* ip列表,addresses返回ip地址列表,注意主机应能从路由器获取到
// * IP,否则可能返回空的列表(ubuntu用此方法只能获取到环回IP) */
// IPlist = hostInfo.addresses();
// qDebug()<<IPlist<<endl;
// /* 遍历IPlist */
// foreach (QHostAddress ip, IPlist) {
// if (ip.protocol() == QAbstractSocket::IPv4Protocol)
// comboBox->addItem(ip.toString());
// }
/* 获取所有的网络接口,
* QNetworkInterface类提供主机的IP地址和网络接口的列表 */
QList<QNetworkInterface> list
= QNetworkInterface::allInterfaces();
/* 遍历list */
foreach (QNetworkInterface interface, list) {
/* QNetworkAddressEntry类存储IP地址子网掩码和广播地址 */
QList<QNetworkAddressEntry> entryList
= interface.addressEntries();
/* 遍历entryList */
foreach (QNetworkAddressEntry entry, entryList) {
/* 过滤IPv6地址,只留下IPv4 */
if (entry.ip().protocol() ==
QAbstractSocket::IPv4Protocol) {
comboBox->addItem(entry.ip().toString());
/* 添加到IP列表中 */
IPlist<<entry.ip();
}
}
}
}
/* 开始监听 */
void MainWindow::startListen()
{
/* 需要判断当前主机是否有IP项 */
if (comboBox->currentIndex() != -1) {
qDebug()<<"start listen"<<endl;
tcpServer->listen(IPlist[comboBox->currentIndex()],
spinBox->value());
/* 设置按钮与下拉列表框的状态 */
pushButton[0]->setEnabled(false);
pushButton[1]->setEnabled(true);
comboBox->setEnabled(false);
spinBox->setEnabled(false);
/* 在文本浏览框里显示出服务端 */
textBrowser->append("服务器IP地址:"
+ comboBox->currentText());
textBrowser->append("正在监听端口:"
+ spinBox->text());
}
}
/* 停止监听 */
void MainWindow::stopListen()
{
qDebug()<<"stop listen"<<endl;
/* 停止监听 */
tcpServer->close();
/* 如果是连接上了也应该断开,如果不断开客户端还能继续发送信息,
* 因为socket未断开,还在监听上一次端口 */
if (tcpSocket->state() == tcpSocket->ConnectedState)
tcpSocket->disconnectFromHost();
/* 设置按钮与下拉列表框的状态 */
pushButton[1]->setEnabled(false);
pushButton[0]->setEnabled(true);
comboBox->setEnabled(true);
spinBox->setEnabled(true);
/* 将停止监听的信息添加到文本浏览框中 */
textBrowser->append("已停止监听端口:"
+ spinBox->text());
}
/* 清除文本浏览框里的内容 */
void MainWindow::clearTextBrowser()
{
/* 清除文本浏览器的内容 */
textBrowser->clear();
}
/* 服务端接收消息 */
void MainWindow::receiveMessages()
{
/* 读取接收到的消息 */
QString messages = "客户端:" + tcpSocket->readAll();
textBrowser->append(messages);
}
/* 服务端发送消息 */
void MainWindow::sendMessages()
{
if(NULL == tcpSocket)
return;
/* 如果已经连接 */
if(tcpSocket->state() == tcpSocket->ConnectedState) {
/* 发送消息 */
tcpSocket->write(lineEdit->text().toUtf8().data());
/* 在服务端插入发送的消息 */
textBrowser->append("服务端:" + lineEdit->text());
}
}
/* 服务端状态改变 */
void MainWindow::socketStateChange(QAbstractSocket::SocketState state)
{
switch (state) {
case QAbstractSocket::UnconnectedState:
textBrowser->append("scoket状态:UnconnectedState");
break;
case QAbstractSocket::ConnectedState:
textBrowser->append("scoket状态:ConnectedState");
break;
case QAbstractSocket::ConnectingState:
textBrowser->append("scoket状态:ConnectingState");
break;
case QAbstractSocket::HostLookupState:
textBrowser->append("scoket状态:HostLookupState");
break;
case QAbstractSocket::ClosingState:
textBrowser->append("scoket状态:ClosingState");
break;
case QAbstractSocket::ListeningState:
textBrowser->append("scoket状态:ListeningState");
break;
case QAbstractSocket::BoundState:
textBrowser->append("scoket状态:BoundState");
break;
default:
break;
}
}
上面的代码主要是服务端开启监听,如果有客户端连到服务端,就会发射 newConnection() 信号,同时也连接到接收消息的信号与槽函数。点击发送消息按钮就可以使用 tcpSocket 发送消 息。注意发送消息和接收消息都是通过 tcpSocket 的 read()和 write()进行。
限于篇幅,tcp客户端的编写放在下篇