Tcp理论
Tcp协议
TCP三次握手
假设客户端主机A向服务器端主机B请求一个TCP连接。主机B运行了一个服务器进程,它要提供相应的服务,就首先发出一个被动打开命令,要求它的TCP准备接收客户进程的连接请求,此时服务器进程就处于“监听”状态,等待客户的连接请求。如有,就做出响应。这里以SYN、ACK表示TCP报文段中的控制位,以seq、ack分别表示TCP的发送序号和确认序号。连接过程分为以下3步。
这里以SYN、ACK表示TCP报文段中的控制位,以seq、ack分别表示TCP的发送序号和确认序号;
三次握手流程:
第一次握手:主机A请求连接:这里主机A发送一个SYN=1(请求连接标志)和seq=x(发送序号初始值为随机数x),若主机B收到A的连接请求,就完成了第一次握手。
第二次握手:如果主机B同意建立连接,则向主机A发送确认报文,,用SYN=1和ACK=1表示同意连接,用ack=x+1表明正确收到A的序号为x的连接请求,同时也为自己选择一个随即发送序号seq=y作为它的发送序号的初始值。主机A收到主机B的请求应答报文,完成第二次握手。
第三次握手:当主机A收到主机B的确认后,还要向主机B发出确认,ACK=1表示同意连接,用ack=y+1表明收到B对连接的应答,同时发送A的第一个数据seq=x+1,主机B收到主机A的确认报文,完成第三次握手。此时双方就可以使用协定好的参数以及各自分配的资源进行正常的数据通信了。
第一个人想对第二个人说话,于是他说,“我想和你说话”(SYN)。第二个人回答“好的,我愿意和你说话”(SYN,ACK)。第一个人说,“好,开始吧”(ACK)。
通过第三次握手,主要目的是为了防止已失效的连接请求报文段突然又传送到了主机B,因而产生错误。所谓“失效的连接请求报文段”是指一端(如A)发出的连接请求,由于没有在允许的时间内传送到目的方(如B),使得发送方不得不又发送一个新的连接请求报文段。而在新的连接请求建立并传送完数据将连接释放后,出现了一种情况,即主机A发出的第一个连接请求报文段迟迟到达了B。本来,这是一个已经失效的报文段,但主机B收到此失效的连接请求报文段后,就误认为是主机A又发出一次新的连接请求,于是向主机A发出确认报文段,同意建立连接。主机A由于并没有要求建立连接,因此不会理睬主机B的确认,也不会向主机B发送数据。但主机B却以为传输连接就这样建立了,并一直等待主机A发来数据。主机B的许多资源就这样白白浪费了。采用三次握手机制可以防止上述现象的发生。在上述情况下,主机A就不会理睬主机B发来的确认,也不会向主机B发出确认报文,连接也就建立不起来。
TCP连接的拆除—用四次握手释放TCP连接
由于一个TCP连接是全双工(即数据在两个方向上能同时传递),因此每个方向必须单独地进行关闭。关闭的原则就是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向连接。当一端收到一个FIN,它必须通知应用层另一端已经终止了那个方向的数据传送。四次握手释放过程如图所示:
第一次握手:主机A的应用进程先向其TCP发出连接释放请求,发送FIN=1的报文段来终止这个方向连接,其序号u等于已传送过的数据的最后一个字节的序号加1。这时A处于等待B确认的状态。
第二次握手:主机B的TCP收到释放连接的通知后,即发出确认,其确认序号ack=u+1,而这个报文段自己的序号是v,等于主机B已经传送过的数据的最后一个字节的序号加1,同时通知高层的应用进程。这样,从A到B的连接就释放了,连接处于半关闭状态,即主机A已经没有数据要发送了,但主机B若发送数据,A仍要接收。
也就是说,从B到A这个方向的连接并未关闭,可能还要等待一段时间。等待是因为若主机B还有一些数据要发往主机A,则可以继续发送,主机A只要收到数据,仍应向主机B发送确
第三次握手:在主机B向主机A的数据发送结束后,其应用进程就通知TCP释放连接。主机B发出的连接释放报文段必须将FIN置1,先假设B的序号为w(在半关闭状态下主机B可能又发送了一些数据),同时还必须重复上次已发送过的确认序号ack=u+1。这时主机B进入等待A的确认状态。
第四次握手:主机A收到B的连接释放报段后,必须对此发出确认,在确认报文段中将ACK置1,给出确认序号ack=w+1,而自己的序号是seq=u+1。从B到A的连接被释放掉。主机A的TCP再向其应用进程报告,整个连接已经全部释放。
TCP 通信代码
1.tcp通信的server代码
QT += network
server.h文件
class QTcpServer;
private:
QTcpServer* tcpServer;
private slots:
void sendMessage();
server.cpp 文件
#include <QtNetwork>
Server::Server(QWidget *parent) :QDialog(parent), ui(new Ui::tcpserver)
{
ui->setupUi(this);
tcpServer = new QTcpServer(this); // 1.创建一个QTcpServer类的对象
if(!tcpServer->listen(QHostAddress::LocalHost,6666)) // 2. 调用listen()函数来监听到来的连接,
{ // 这里监听的是本机的6666端口,
qDebug()<<tcpServer->errorString(); // 这样可以实现客户端和服务器端在同一台计算机上运行并通信
close();
}
// 3. 一旦有客户端连接到服务器,则发射newConnection()信号
connect(tcpServer,&QTcpServer::nextPendingConnection,this,&Server::sendMessage);
}
Server::~Server()
{
delete ui;
delete tcpServer;
}
void Server::sendMessage()
{
QByteArray block; // 1.使用QByteArray对象来暂存要发送的数据对象
QDataStream out(&block,QIODevice::WriteOnly); // 2.使用数据流将要发送的数据写到QByteArray对象中
out.setVersion(QDataStream::Qt_5_6); // 3.需要设置数据流的版本,而且客户端在接收数据时要使用相同的版本
out << (quint16)0; // 4.为了在接收数据时可以接收到完整的数据,在发送数据时,一定要在最开始
// 写入实际数据的大小,该大小信息占用两个字节,可以使用(quint16)0来占用两个字节的位置,
// 以便以后填写数据的大小信息。
out << tr("hello TCP!!!"); // 5.输入实际的数据,这里就是一个字符串,注意,字符串要使用tr()进行编码
out.device()->seek(0); // 6.使用seek(0)跳转到数据块的开头
out << (quint16)(block.size() - sizeof(quint16)); // 7.用数据块总的大小减去开头两个字节的大小,获得实际数据大小,然后写到开头
QTcpSocket *clientConnection = tcpServer->nextPendingConnection(); // 8. 使用nextPendingConnection()函数获取连接的套接字
connect(clientConnection,&QTcpSocket::disconnected,clientConnection,&QTcpSocket::deleteLater); // 9.表明当连接断开时,删除该套接字
clientConnection->write(block); // 10.使用write()函数将block数据发送出去,
clientConnection->disconnectFromHost(); // 11.调用disconnectFromHost()函数会一直等待套接字将所有数据发送完毕,然后关闭套接字,并发射disconnected()信号
ui->label->setText("发送数据成功!!!");
}
client.h
#include <QAbstractSocket>
class QTcpSocket;
private:
Ui::Client *ui;
QTcpSocket* tcpSocket;
QString message;
qint16 blockSize; // 用来存放数据的大小信息
private slots:
void sltNewConnect();
void sltReadMessage();
void sltDisplayError(QAbstractSocket::SocketError);
void on_pushButton_connect_clicked();
client.cpp
#include <QtNetwork>
Client::Client(QWidget *parent) :QDialog(parent),ui(new Ui::Client)
{
ui->setupUi(this);
tcpSocket = new QTcpSocket(this);
connect(tcpSocket,&QTcpSocket::readyRead,this,&Client::sltReadMessage);
connect(tcpSocket,SIGNAL(error(QAbstractSocket::SocketError)),this,SLOT(sltDisplayError(QAbstractSocket::SocketError)));
}
Client::~Client()
{
delete ui;
}
void Client::sltNewConnect()
{
blockSize = 0; // 1.将存储接收数据的变量置为零
tcpSocket->abort(); // 2.调用abort()函数,将嵌套字已有连接全部取消
tcpSocket->connectToHost(ui->lineEdit_local_host->text(),ui->lineEdit_port->text().toInt()); // 3.调用connectToHost()函数连接到指定主机的指定端口
}
void Client::sltReadMessage()
{
QDataStream in(tcpSocket); // 1.设置数据流对象
in.setVersion(QDataStream::Qt_5_6); // 2.设置数据流版本,这里要和服务器端相同
if(blockSize == 0) // 3.如果是刚刚开始接收数据
{
// 4.判断接收的数据是否大于两个字节,也就是文件的大小信息所占空间
// 如果大于大于两个字节,则保存到blockSize变量中,否则直接返回,继续接收数据
if(tcpSocket->bytesAvailable()<(int)sizeof(qint16)) return;
in >> blockSize;
}
if(tcpSocket->bytesAvailable()<blockSize) return; // 5.如果没有得到全部数据,则返回,继续接收数
in >> message; // 6.将接收到的数据存放在变量中
ui->label_message->setText(message); // 7.显示接收到的数据
}
void Client::sltDisplayError(QAbstractSocket::SocketError)
{
qDebug()<<tcpSocket->errorString();
}
void Client::on_pushButton_connect_clicked()
{
sltNewConnect();
}