概述特点
TCP(Transmission Control Protocol)通信的特点主要体现在以下几个方面:
- 面向连接的传输协议:TCP在应用程序使用它之前,必须先建立TCP连接。在传送数据完毕后,必须释放已经建立的TCP连接。这种连接是一对一的,即每一条TCP连接只能有两个端点。
- 可靠传输:TCP提供可靠交付的服务。通过TCP连接传送的数据,无差错、不丢失、不重复,并且按序到达。它采用序号、确认和重传机制来确保数据的可靠性。如果发现数据包丢失或损坏,TCP会重新传输数据。
- 全双工通信:TCP允许通信双方的应用进程在任何时候都能发送数据。TCP连接的两端都设有发送缓存和接收缓存,用来临时存放双向通信的数据。
- 面向字节流:TCP中的“流”指的是流入到进程或从进程流出的字节序列。TCP不关心应用层的消息边界,而是将数据视为一连续的字节流进行传输。发送方将数据划分为小的数据块,而接收方会根据需要重组这些数据块。
- 流量控制和拥塞控制:TCP使用流量控制机制来防止快速发送方导致慢速接收方无法处理的情况。通过接收方发送的窗口大小,TCP调整发送方的发送速率,以适应网络状况和接收方的处理能力。同时,TCP还通过拥塞控制机制来防止网络拥塞。当网络拥塞时,TCP会降低发送速率以减轻网络负担,从而保持整体网络的稳定性。
总的来说,TCP通信的特点主要体现在其面向连接、可靠传输、全双工通信、面向字节流以及流量控制和拥塞控制等方面。这些特点使得TCP在需要可靠数据传输的场合得到了广泛应用,如互联网和企业网上客户端应用等。
并发
概念:
并发(Concurrency) 是指在一个时间段内宏观上有多个程序在同时运行。这些程序在单处理机系统中实际上是交替地执行,但给人的感觉是同时执行,因而称为并发执行。并发是计算机在一段时间内同时运行或处理多个任务的能力。
但是TCP通信不支持并发通信,但可以通过一些技术或方法来实现TCP并发服务器。这是因为TCP服务器端有两个读阻塞函数,accept和recv,这两个函数需要先后运行,所以运行一个函数时另一个函数无法执行,导致无法同时连接客户端和与其他客户端通信。
有几种方法可以实现TCP并发服务器:
- 使用多线程:每个线程可以独立处理一个客户端连接,从而实现并发处理多个连接。
- 使用多进程:每个进程可以独立运行,处理不同的客户端连接,实现并发。
- 使用多路IO复用:如select、poll、epoll等机制,允许一个进程或线程同时监听多个socket,实现并发处理多个连接。
案例:
此次我们使用多线程实现服务器的并发:
用QT写一个小demo:
服务器端:
首先加入网络模块:network再.pro中
加入头文件
我们采用多线程实现服务器的并发,每次客户端请求连接后,我们都要分配一个子线程去处理客户端的请求,使用重写QTHread类的run函数,实现子线程的处理。
首先使用重写incomingConnection函数
当一个新的客户端连接到服务器时,QTcpServer
会自动调用 incomingConnection
函数。这个函数提供了一个标识符(通常是一个文件描述符或套接字描述符),该标识符唯一地标识了新建立的连接。在 incomingConnection
函数中,服务器通常会创建一个新的 QTcpSocket
对象,并使用传入的 socketDescriptor
来初始化它。这个新创建的 QTcpSocket
对象代表了一个与客户端的连接,并允许服务器与客户端进行通信。一旦有了 QTcpSocket
对象,服务器就可以开始处理这个连接了。这通常涉及到读取从客户端发送过来的数据、处理这些数据,以及向客户端发送响应。这里我们将封装一个类,将连接成功获得的可通信的标识符传入子线程,去进行处理具体的逻辑业务
QTcpSocket *clientSocket = pendingConnections.takeFirst();
ClientHandler *thread = new ClientHandler(clientSocket,dir);
封装好的ClientHandler如下
clienthandler.h
#ifndef CLIENTHANDLER_H
#define CLIENTHANDLER_H
#include <QThread>
#include <QObject>
#include <QTcpSocket>
#include <QHostAddress>
#include <QFile>
#include <QDataStream>
#include <QApplication>
#include <QFileInfo>
#include <QDir>
#include <QVector>
#include <QString>
#include "jsondata.h"
#include <QMutex>
struct FileDate
{
int readCnt;
int lastSize;
int type;
int data;
QString fileName;
};
class ClientHandler : public QThread
{
Q_OBJECT
public:
explicit ClientHandler(QTcpSocket *socket,QString dir,QObject *parent = nullptr);
~ClientHandler();
void ClientDismsg();
qint64 Mins(qint64 a,qint64 b);
void selectFile(QString filePath) ;
void SendFile();
void GetDirFileName();
public:
void run() override;
signals:
void threadStarted(QThread *thread);
void threadended(QThread *thread);
void threadsIdadd(QString);
void threadsIddel(QString);
private:
QTcpSocket *m_socket;
QFile file;
QString fileName;
qint64 fileSize;
qint64 sendSize;
JsonData jsondaga;
mutable QMutex mutex;
QString dirs;
};
#endif // CLIENTHANDLER_H
clienthandler.cpp
#include "clienthandler.h"
ClientHandler::ClientHandler(QTcpSocket *socket,QString dir, QObject *parent)
: QThread(parent), m_socket(socket),dirs(dir)
{
}
ClientHandler::~ClientHandler()
{
m_socket->deleteLater();
}
void ClientHandler::run()
{
// 获取当前线程的ID
Qt::HANDLE threadId = this->currentThreadId();
// 将线程ID转换为quintptr,然后转换为QString
quintptr threadIdValue = reinterpret_cast<quintptr>(threadId);
QString threadIdStr = QString::number(threadIdValue);
//qDebug()<<threadId<<"start";
emit threadsIdadd(threadIdStr);
emit threadStarted(this); //发出线程启动信号
// 在这里处理客户端数据
// 例如,读取数据并发送响应
connect(m_socket, &QTcpSocket::readyRead, this, [this]() {
QByteArray data = m_socket->readAll();
// 处理数据...
qDebug()<<data;
QString str = QString::fromUtf8(data);
if(str=="4")
{
GetDirFileName();
}
else if(str.at(0)=="3")
{
mutex.lock();
QString filePath = dirs+"/"+str.mid(1,str.size());
qDebug()<<filePath;
if(!filePath.isEmpty())
{
fileName.clear();
fileSize = 0;
sendSize = 0;
QFileInfo info(filePath);
fileName = info.fileName();
fileSize = info.size();
file.setFileName(filePath);
if(!file.open(QIODevice::ReadOnly))
{
qDebug()<<"文件打开失败";
return ;
}
}
else
{
qDebug()<<"文件路径出错";
return ;
}
QString head = tr("%1##%2").arg(fileName).arg(fileSize);
quint64 len = m_socket->write(head.toUtf8().data());
if(len<=0)
{
qDebug()<<"头部信息发送失败 ";
file.close();
}
}
else if(str == fileName)
{
SendFile();
}else
{
m_socket->write(data);
}
});
// 保持线程运行,直到连接关闭
connect(m_socket, &QTcpSocket::disconnected, this, &ClientHandler::ClientDismsg);
exec();
}
void ClientHandler::ClientDismsg()
{
QHostAddress clientaddress = m_socket->peerAddress();
quint16 clientPort = m_socket->peerPort();
qDebug() << "client:" <<clientaddress<<clientPort;
this->quit();
// 获取当前线程的ID
Qt::HANDLE threadId = this->currentThreadId();
// 将线程ID转换为quintptr,然后转换为QString
quintptr threadIdValue = reinterpret_cast<quintptr>(threadId);
QString threadIdStr = QString::number(threadIdValue);
// qDebug()<<threadId<<"end";
emit threadsIddel(threadIdStr);
threadended(this);
}
qint64 ClientHandler::Mins(qint64 a,qint64 b)
{
return (a < b) ? a : b;
}
void ClientHandler::SendFile()
{
qint64 len = 0;
do{
static quint32 cnt = 0;
char buf[64*1024] = {0};
len = 0;
len = file.read(buf,sizeof(buf));
len = m_socket->write(buf,len);
sendSize += len;
qDebug()<<cnt++;
}while(len > 0);
if(sendSize == fileSize)
{
file.close();
mutex.unlock();
}
}
void ClientHandler::GetDirFileName()
{
// 假设你有一个文件夹路径
//QString folderPath = "D:/ZKJ_SHAREDIR";
// 创建一个 QDir 对象
QDir dir(dirs);
// 获取文件夹中所有文件的名称列表
QStringList fileNames = dir.entryList(QDir::Files);
// 创建一个 QVector 来保存文件名
QVector<QString> fileVector;
// 将 QStringList 中的文件名转换到 QVector 中
fileVector.reserve(fileNames.size());
for (const QString &fileName : fileNames) {
fileVector.push_back(fileName);
}
QString str= jsondaga.ToJsonMSG(fileVector);
m_socket->write(str.toUtf8().data());
}
封装TcpServer的接口如下代码
tcpserver.h
#ifndef TCPSERVER_H
#define TCPSERVER_H
#include <QTcpServer>
#include <QTcpSocket>
#include "clienthandler.h"
class TcpServer : public QTcpServer
{
Q_OBJECT
public:
explicit TcpServer(QString dir,QObject *parent = nullptr);
protected:
void incomingConnection(qintptr socketDescriptor) override;
private:
QList<QTcpSocket*> pendingConnections; // 管理待处理的连接
QList<QThread*> threads;
QList<QString> threadsid;
QString dir;
};
#endif // TCPSERVER_H
tcpserver.cpp
#include "tcpserver.h"
TcpServer::TcpServer(QString dir,QObject *parent) : QTcpServer(parent),dir(dir)
{
}
void TcpServer::incomingConnection(qintptr socketDescriptor) {
QTcpSocket *socket = new QTcpSocket();
socket->setSocketDescriptor(socketDescriptor);
pendingConnections.append(socket);
QHostAddress clientAddress = socket->peerAddress();
quint16 clientPort = socket->peerPort();
qDebug() << "client:" <<clientAddress<<clientPort;
if (pendingConnections.isEmpty()) {
return;
}
QTcpSocket *clientSocket = pendingConnections.takeFirst();
ClientHandler *thread = new ClientHandler(clientSocket,dir);
connect(thread, &ClientHandler::threadStarted, this, [=](QThread *t) {
threads.append(t); // 将新线程添加到列表中
qDebug() << "输出线程数量:" << threads.count(); // 输出线程数量
});
connect(thread, &ClientHandler::threadended, this, [=](QThread *t) {
threads.removeAll(t);
// 在适当的时候等待线程退出并删除它
t->wait(); // 等待线程退出
delete t; // 删除线程对象
});
connect(thread, &ClientHandler::threadsIdadd, this, [=](QString id) {
threadsid.append(id);
for(int i=0;i<threadsid.count();i++)
{
qDebug()<<threadsid.at(i);
}
});
connect(thread, &ClientHandler::threadsIddel, this, [=](QString id) {
threadsid.removeAll(id);
for(int i=0;i<threadsid.count();i++)
{
qDebug()<<threadsid.at(i);
}
});
thread->start();//开启线程
};
mainwindow.cpp中
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
this->setFixedSize(830,125);
ui->pushButton->setHidden(true);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_pushButton_clicked()
{
if(!ui->lineEditPort->text().isEmpty())
{
if(Listens==false)
{
if (!server->listen(QHostAddress::Any, ui->lineEditPort->text().toInt()))
{
qDebug() << "Server could not start";
return ;
}else
{
qDebug() << "Server started";
ui->pushButton->setText("监听中……");
Listens=true;
}
}else
{
server->close();
ui->pushButton->setText("监听");
qDebug() << "Server ended";
Listens=false;
}
}
}
void MainWindow::on_pushButton_2_clicked()
{
QString dir = QFileDialog::getExistingDirectory(this,"请选择本地数据 ","./");
server = new TcpServer(dir);
ui->pushButton->setHidden(false);
ui->pushButton_2->setHidden(true);
}
【end】
备注:下一篇我将把client的demo以及整个完成的案例项目上传到资源中。下一篇(client)