概述:
功能:
TCP(Transmission Control Protocol传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC793定义。在简化的计算机网络OSI模型中,它完成第四层传输层所指定的功能,用户数据报协议(UDP)是同一层内另一个重要的传输协议。在因特网协议族(Internet protocol suite)中,TCP层是位于IP层之上,应用层之下的中间层。不同主机的应用层之间经常需要可靠的、像管道一样的连接,但是IP层不提供这样的流机制,而是提供不可靠的包交换。
连接建立:
TCP的三次握手
TCP的三次握手
TCP是因特网中的传输层协议,使用三次握手协议建立连接。当主动方发出SYN连接请求后,等待对方回答SYN+ACK,并最终对对方的 SYN 执行 ACK 确认。这种建立连接的方法可以防止产生错误的连接,TCP使用的流量控制协议是可变大小的滑动窗口协议。
TCP三次握手的过程如下:
客户端发送SYN(SEQ=x)报文给服务器端,进入SYN_SEND状态。
服务器端收到SYN报文,回应一个SYN (SEQ=y)ACK(ACK=x+1)报文,进入SYN_RECV状态。
客户端收到服务器端的SYN报文,回应一个ACK(ACK=y+1)报文,进入Established状态。
三次握手完成,TCP客户端和服务器端成功地建立连接,可以开始传输数据了。
可靠性:
TCP提供一种面向连接的、可靠的字节流服务。面向连接意味着两个使用TCP的应用(通常是一个客户和一个服务器)在彼此交换数据包之前必须先建立一个TCP连接。这一过程与打电话很相似,先拨号振铃,等待对方摘机说“喂”,然后才说明是谁。在一个TCP连接中,仅有两方进行彼此通信。广播和多播不能用于TCP。
代码示例:
.pro
QT += core gui network
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
TARGET = TCPFile
TEMPLATE = app
SOURCES += main.cpp\
serverwidget.cpp \
clientwidget.cpp
HEADERS += serverwidget.h \
clientwidget.h
FORMS += serverwidget.ui \
clientwidget.ui
CONFIG += C++11
client.h
#ifndef CLIENTWIDGET_H
#define CLIENTWIDGET_H
#include <QWidget>
#include <QtNetwork>
#include <QFile>
namespace Ui {
class ClientWidget;
}
class ClientWidget : public QWidget
{
Q_OBJECT
public:
explicit ClientWidget(QWidget *parent = 0);
~ClientWidget();
private slots:
/**
* @brief ifConnected 连接成功槽函数
*/
void slot_Connected();
/**
* @brief slotReadyRead 数据接收槽函数
*/
void slot_ReadyRead();
private slots:
void on_buttonConnect_clicked();//客户端链接按钮
private:
Ui::ClientWidget *ui;
QTcpSocket *tcpSocket; //通信套接字
QFile file; //文件对象
QString fileName; //文件名字
qint64 fileSize; //文件大小
qint64 recvSize; //已接收文件的大小
bool isFile; //接收文件数据标志
};
#endif // CLIENTWIDGET_H
client.cpp
#include "clientwidget.h"
#include "ui_clientwidget.h"
#include <QMessageBox>
#include <QDebug>
ClientWidget::ClientWidget(QWidget *parent) :
QWidget(parent),
ui(new Ui::ClientWidget)
{
ui->setupUi(this);
//创建TCP套接字
tcpSocket = new QTcpSocket(this);
//进度条初始化
ui->progressBar->setValue(0);
//设置标题
setWindowTitle("客户端");
//连接成功
connect(tcpSocket,&QTcpSocket::connected,this,&ClientWidget::slot_Connected);
//数据接收
connect(tcpSocket,&QTcpSocket::readyRead,this,&ClientWidget::slot_ReadyRead);
}
ClientWidget::~ClientWidget()
{
delete ui;
}
void ClientWidget::slot_Connected()
{
//初始化数据
fileName = "";
fileSize = 0;
recvSize = 0;
isFile = false;
ui->buttonConnect->setEnabled(false);
ui->textEdit->clear();
ui->textEdit->append("和服务器连接成功");
}
void ClientWidget::slot_ReadyRead()
{
QByteArray buf = tcpSocket->readAll();
//buf.resize(tcpSocket->bytesAvailable());
if(false == isFile)//先接收头部信息
{
isFile = true;
//文件名字
fileName = QString(buf).section("#", 1, 1);
//文件大小
fileSize = QString(buf).section("#", 2, 2).toInt();
qDebug() << fileName << fileSize;
recvSize = 0;
file.setFileName(fileName);
if(false == file.open(QIODevice::WriteOnly)){
//初始化数据
fileName = "";
fileSize = 0;
recvSize = 0;
isFile = false;
QMessageBox::warning(this, "警告", "创建文件失败");
return;
}
ui->progressBar->setMinimum(0);
ui->progressBar->setMaximum(fileSize/1024);
ui->textEdit->append(QString("正在接收文件:\n%1").arg(fileName));
}
else//文件数据
{
//写入数据
qint64 len = file.write(buf);
recvSize += len;
qDebug() << len;
ui->progressBar->setValue(recvSize/1024);
}
if(recvSize == fileSize)//如果接收数据长度和发送数据长度相等做接收后处理
{
file.close();
ui->buttonConnect->setEnabled(true);
tcpSocket->disconnectFromHost();
QMessageBox::information(this, "ok", "文件接收完毕");
ui->textEdit->append("文件接收完毕");
}
}
//连接按钮
void ClientWidget::on_buttonConnect_clicked()
{
ui->progressBar->setValue(0);
QString ip = ui->lineEditIP->text();
qint16 port = ui->lineEditPort->text().toInt();
if(ip.isEmpty() == true || port == 0)
{
QMessageBox::warning(this, "警告", "ip或端口不能为空");
return;
}
tcpSocket->abort(); //取消已有的连接
//连接服务器
tcpSocket->connectToHost(QHostAddress(ip), port);
}
server.h
#ifndef SERVERWIDGET_H
#define SERVERWIDGET_H
#include <QWidget>
#include <QtNetwork> //网络相关头文件
#include <QFile>
#include <QTimer>
namespace Ui {
class ServerWidget;
}
class ServerWidget : public QWidget
{
Q_OBJECT
public:
explicit ServerWidget(QWidget *parent = 0);
~ServerWidget();
/**
* @brief sendFileData 发送文件数据函数
*/
void sendFileData();
private slots:
/**
* @brief slot_Timerstart 定时器启动*毫秒调用此槽函数
*/
void slot_TimerStart();
/**
* @brief slot_NewConnection 当有新数据到来时调用此槽函数
*/
void slot_NewConnection();
private slots:
/**
* @brief on_buttonChoose_clicked 选择文件按钮
*/
void on_buttonChoose_clicked();
/**
* @brief on_buttonSend_clicked 发送文件按钮
*/
void on_buttonSend_clicked();
private:
Ui::ServerWidget *ui;
QTcpServer *tcpServer; //监听套接字
QTcpSocket *tcpSocket; //通信套接字
QFile file; //文件对象
QString fileName; //文件名字
qint64 fileSize; //文件大小
qint64 sendSize; //已发送文件的大小
QTimer timer; //定时器
};
#endif // SERVERWIDGET_H
server.cpp
#include "serverwidget.h"
#include "ui_serverwidget.h"
#include <QMessageBox>
#include <QFileDialog>
#include <QFileInfo>
#include <QTimer>
#include <QThread>
ServerWidget::ServerWidget(QWidget *parent) :
QWidget(parent),
ui(new Ui::ServerWidget)
{
ui->setupUi(this);
//创建套接字
tcpServer = new QTcpServer(this);
//监听,端口:8888
bool isOk = tcpServer->listen(QHostAddress::Any, 8888);
if(false == isOk)//监听失败
{
QMessageBox::warning(this, "监听", "监听失败");
return;
}
//设置标题
setWindowTitle("服务器:8888");
//设置按钮(变灰)
ui->buttonChoose->setEnabled(false);
ui->buttonSend->setEnabled(false);
//当有客户端链接时,触发信号:newConnection
connect(tcpServer,&QTcpServer::newConnection,this,&ServerWidget::slot_NewConnection);
//定时器处理函数
connect(&timer,&QTimer::timeout,this,&ServerWidget::slot_TimerStart);
}
ServerWidget::~ServerWidget()
{
delete ui;
}
//选择文件按钮
void ServerWidget::on_buttonChoose_clicked()
{
QString path = QFileDialog::getOpenFileName(this, "请选择所要发送的文件", "../");
if(path.isEmpty() == false)//路径有效
{
//file为成员变量
file.setFileName(path); //设置文件路径
//只读方式打开文件
bool isOk = file.open(QIODevice::ReadOnly);
if(false == isOk)//打开文件失败
{
QMessageBox::warning(this, "警告", "打开文件失败");
return;
}
else
{
ui->textEdit->append("发送的文件:");
ui->textEdit->append(path);
//初始化数据
fileName = "";
fileSize = 0;
sendSize = 0;
//获取发送文件的信息
QFileInfo info(path);
fileName = info.fileName(); //文件名
fileSize = info.size(); //文件大小
ui->buttonSend->setEnabled(true); //恢复发送文件按钮
ui->buttonChoose->setEnabled(false); //选择文件按钮变灰
}
}
}
//发送文件按钮
void ServerWidget::on_buttonSend_clicked()
{
// 发送文件按钮变灰
ui->buttonSend->setEnabled(false);
//先发送文件头,自定义的数据,不是文件数据
//先发送头,自定义组包, 文件名#文件大小
QString buf = QString("head#%1#%2").arg(fileName).arg(fileSize);
//先发头
qint64 len = tcpSocket->write( buf.toUtf8().data());
tcpSocket->waitForBytesWritten(); //等待数据发送完毕
ui->textEdit->append("已经在发送文件!!!");
if(len > 0) //如果头部信息发送成功,开始发送文件数据
{
//10毫秒后再发送文件数据
//启动定时器,定时器内容发送文件数据
//防止TCP黏包问题
this->timer.start(1);
}
else
{
file.close(); //关闭文件
}
}
void ServerWidget::sendFileData()
{
//循环读取数据发送
qint64 len = 0;
do{
// 每次发送 2kb 大小的数据,如果剩余的数据不足 2kb,就发送剩余数据的大小
char buf[2*1024] = {0};
len = 0;
len = file.read( buf, sizeof(buf) ); //读数据
len = tcpSocket->write(buf, len); //发数据
sendSize +=len; //已发送的文件数据大小
}while(len > 0);
//文件数据发送完毕
if(sendSize == fileSize)
{
//QMessageBox::information(this, "ok", "文件发送完毕");
ui->textEdit->append("文件发送完毕");
//关闭文件
file.close();
//关闭客户端
tcpSocket->disconnectFromHost();
tcpSocket->close();
}
}
void ServerWidget::slot_TimerStart()
{
this->timer.stop(); //关闭定时器
sendFileData(); //发送文件数据
}
void ServerWidget::slot_NewConnection()
{
//取出链接套接字
tcpSocket = tcpServer->nextPendingConnection();
//客户端IP和端口
QString ip = tcpSocket->peerAddress().toString();
qint16 port = tcpSocket->peerPort();
QString str = QString("[%1:%2]和服务器连接成功").arg(ip).arg(port);
ui->textEdit->setText(str); //设置内容
//恢复选择按钮状态
ui->buttonChoose->setEnabled(true);
QMessageBox::information(this, "允许", "连接成功,可以选择文件发送");
}
测试结果
over:
欢迎大家关注作者在文末评论、点赞、转发以及批评指正!
如果大家有更好的方法或有问题可以在文末评论一起讨论!
共同学习!
共同进步!
文末一句话:
如今最好,别说来日方长,时光难留,只有一去不返。。