用Qt搭建一个简易的TCP客户端和服务器


你好! 这是本人的第一篇博客。如有错误,请批评指正。

简易理解TCP传输流程

什么是TCP协议,为什么要用TCP协议

TCP协议即transmission control protocal,即传输控制协议。是一种用于在应用程序之间进行信息传递的数据格式的协议。TCP协议并不是单独存在的,它还要结合网络层的IP协议使用。整个TCP/IP协议包括了IP协议,IMCP协议,TCP协议,以及我们更加熟悉的http、ftp、pop3协议等等。电脑有了这些协议就可以自由的和其他终端进行数据交流。而ip协议、TCP协议很像套在数据上的一层衣服,是为了让其他计算机能够认识里边的东西。整个数据传输过程就很像俄罗斯套娃,先给数据一层一层套娃,把套好的数据发送给另一个终端之后,在另一个终端那儿再进行一层一层解套,最终得到我们想要的数据。

TCP协议报文格式

要理解TCP协议的传输过程,我们得先了解TCP是如何给数据进行套娃的,也即数据是如何被包装的。所以接下来要了解TCP的报文格式(先上个图):
TCP报文格式
 
1)源端口号

2)目标端口号

3)序列号:因为在TCP是面向字节流的,他会将报文都分成一个个字节,给每个字节进行序号编写,比如一个报文有900个字节组成,那么就会编成1-900个序号,然后分几部分来进行传输。比如第一次传,序列号就是1,传了50个字节, 那么第二次传,序列号就为51,所以序列号就是传输的数据的第一个字节相对所有的字节的位置。

4)确认应答:如刚说的例子,第一次传了50个字节给对方,对方也会回应你,其中带有确认应答,就是告诉你下一次要传第51个字节来了,所以这个确认应答就是告诉对方要传第多少个字节了

5)首部长度:就是首部的长度,

6)保留:给以后有需要在用,这个保留的位置放的东西是跟控制位类似的

7)控制位:目前有的控制位为6个

URG:紧急控制位,当URG为1时,表名紧急指针字段有效,标识该报文是一个紧急报文,传送到目标主机后,不用排队,应该让该报文尽量往下排,让其早点让应用程序给接受。

ACK:确认序号控制位,当ACK为1时,确认序号ack才有效。当ACK为0时,确认序号没用。

PSH:推送控制位,当为1时,当遇到此报文时,会减少数据向上交付,本来想应用进程交付数据是要等到一定的缓存大小才发送的,但是遇到它,就不用在等足够多的数据才向上交付,而是让应用进程早点拿到此报文,这个要和紧急分清楚,紧急是插队,但是提交缓存大小的数据不变,这个推送就要排队,但是遇到他的时候,会减少交付的缓存数据,提前交付。

RST:复位控制位,报文遇到很严重的差错时,比如TCP连接出错等,会将RST置为1,然后释放连接,全部重新来过。

SYN:同步控制位,在进行连接的时候,也就是三次握手时用得到,下面会具体讲到,配合ACK一起使用

FIN:终止控制位,在释放连接时,也就是四次挥手时用的。

8)窗口:指发送报文段一方的接受窗口大小,用来控制对方发送的数据量(从确认号开始,允许对方发送的数据量)。也就是后面需要讲的滑动窗口的窗口大小

9)检验和:检验首部和数据这两部分,和UDP一样,需要拿到伪首部中的数据来帮助检测

10)选项:长度可变,介绍一种选项,最大报文段长度MSS。 能够告诉对方TCP,我的缓存能接受报文段的数据字段的最大长度是MSS个字节。如果没有使用选项,那么首部固定是20个字节。

11)填充:就是为了让其成为整数个字节

TCP建立连接时的三次握手过程

TCP客户端和服务器建立连接的三次握手,其实就是客户端和服务器之间总共发送了三个包。
TCP建立连接的三次握手过程

第一次握手:
由客户端先发送一个SYN包给服务器,将标志位SYN置为1,随机生成一个seq=j。发送后客户端进入SYN_SENT状态,等待服务器端确认。
第二次握手:
服务器端接收到客户端发来的SYN包后,检验SYN是否为1,SYN为1则知道了客户端想建立连接,随即将数据包标志位SYN和ACK都置为1,ACK为1表示服务器端确认收到了客户端的包数据。服务器端会随机产生一个seq=k,并且会将从客户端收到的序列号加1,作为确认序号ack再发回给客户端以确认连接请求。此时,服务器端将进入SYN_RCVD状态。
第三次握手:
客户端在收到数据包后,检验确认序号是否为j+1,ACK是否为1。若检验正确则将ACK置为1,ack设置为k+1。并将该数据包发送给服务器端,服务器端检查ack是否为k+1,ACK是否为1,如果正确则连接建立成功,服务器端和客户端进入ESTABLISHED状态,完成三次握手,随后客户端与服务器端之间就可以开始传输数据了。

TCP关闭连接的四次挥手

断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。
在这里插入图片描述

第一次挥手:
客户端发送一个FIN包,用来关闭客户端到服务器的数据传送,也就是客户端告诉服务器:我已经不 会再给你发数据了(当然,在fin包之前发送出去的数据,如果没有收到对应的ack确认报文,客户端依然会重发这些数据),但是,此时客户端还可以接受数据。FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
第二次挥手:
服务器收到FIN包后,发送一个ack给对方并且带上自己的序列号seq=k,确认序号ack为收到序号+1(u+1)。此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
这段状态结束后,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
第三次挥手:
服务器发送一个FIN,用来关闭服务器到客户端的数据传送,也就是告诉客户端,我的数据也发送完了,不会再给你发数据了。由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
第四次挥手:
主动关闭方收到FIN后,将ACK置1,发送一个ack给被动关闭方,确认序号为收到序号+1(即w+1),此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。

至此,完成四次挥手。

服务器端搭建

本次实验的开发环境是Qt5.1.12。本实验搭建TCP服务器/客户端主要利用sockect编程。
在服务器端的程序中,我们本地主机的一个端口,这里使用6666,然后关联newConnection()信号与自己写的sendMessage()槽。就是说一旦有客户端的连接请求,就会执行sendMessage()函数,在这个函数里我们发送一个简单的字符串。

1.新建QtGui应用

项目名为tcpServer,基类选择QWidget,类名为Widget。完成后打开项目文件tcpServer.pro并添加一行代码:QT += network ,然后保存该文件。

2.在widget.ui的设计区添加一个Label,更改其显示文本为“等待连接”,然后更改其objectNamestatusLabel,用于显示一些状态信息。设计效果如下:
TCP服务器端

3.在widget.h文件中做以下更改。

添加头文件:#include <QtNetWork>

添加private对象:

QTcpServer *tcpServer;

添加私有槽:

private slots:
void sendMessage();

4.在widget.cpp文件中进行更改。
在其构造函数中添加代码:

tcpServer = new QTcpServer(this);
if(!tcpServer->listen(QHostAddress::LocalHost,6666))
{  //本地主机的6666端口,如果出错就输出错误信息,并关闭
    qDebug() << tcpServer->errorString();
    close();
}
//连接信号和相应槽函数
connect(tcpServer,SIGNAL(newConnection()),this,SLOT(sendMessage()));

我们在构造函数中使用tcpServer的listen()函数进行,然后关联了newConnection()和我们自己的sendMessage()函数。

下面我们实现sendMessage()函数。

void Widget::sendMessage(){
   
    //用于暂存我们要发送的数据
    QByteArray block;   
    //使用数据流写入数据
    QDataStream out(&block,QIODevice::WriteOnly);

    //设置数据流的版本,客户端和服务器端使用的版本要相同
    out.setVersion(QDataStream::Qt_4_6);

    out<<(quint16) 0;
    out<<tr("测试消息!!!");
    out.device()->seek(0);
    out<<(quint16) (block.size() - sizeof(quint16));

    //我们获取已经建立的连接的子套接字
    QTcpSocket *clientConnection = tcpServer->nextPendingConnection();

    connect(clientConnection,SIGNAL(disconnected()),clientConnection,
           SLOT(deleteLater()));
    clientConnection->write(block);
    clientConnection->disconnectFromHost();

    //发送数据成功后,显示提示
    ui->statusLabel->setText("send message successful!!!");
}

对于sendMessage()函数:

(1)为了保证在客户端能接收到完整的文件,我们都在数据流的最开始写入完整文件的大小信息,这样客户端就可以根据大小信息来判断是否接受到了完整的文件。而在服务器端,在发送数据时就要首先发送实际文件的大小信息,但是,文件的大小一开始是无法预知的,所以这里先使用了out<<(quint16) 0;在block的开始添加了一个quint16大小的空间,也就是两字节的空间,它用于后面放置文件的大小信息。然后out<<tr("hello Tcp!!!");输入实际的文件,这里是字符串。当文件输入完成后我们再使用out.device()->seek(0);返回到block的开始,加入实际的文件大小信息,也就是后面的代码,它是实际文件的大小:out<<(quint16) (block.size() - sizeof(quint16));

(2)在服务器端我们可以使用tcpServernextPendingConnection()函数来获取已经建立的连接的Tcp套接字,使用它来完成数据的发送和其它操作。比如这里,我们关联了disconnected()信号和deleteLater()槽,然后我们发送数据

clientConnection->write(block);

然后是

clientConnection->disconnectFromHost();

它表示当发送完成时就会断开连接,这时就会发出disconnected()信号,而最后调用deleteLater()函数保证在关闭连接后删除该套接字clientConnection

客户端搭建

我们在客户端程序中向服务器发送连接请求,当连接成功时接收服务器发送的数据。

1.新建Qt Gui应用,

项目名tcpClient,基类选择QWidget,类名为Widget。完成后打开项目文件tcpClient.pro并添加一行代码:QT += network ,然后保存该文件。

2.我们在widget.ui中添加几个标签Label和两个Line Edit以及一个按钮Push Button。设计效果如下图所示。
TCP客户端UI
其中“主机”后的LineEditobjectName为hostLineEdit,“端口号”后的为portLineEdit

“收到的信息”标签的objectNamemessageLabel

3.在widget.h文件中做更改。

添加头文件:#include <QtNetwork>

添加private变量:

QTcpSocket *tcpSocket;
QString message;  //存放从服务器接收到的字符串
quint16blockSize; //存放文件的大小信息

添加私有槽:

private slots:
    void newConnect(); //连接服务器
    void readMessage();  //接收数据
void displayError(QAbstractSocket::SocketError);  //显示错误

4.在widget.cpp文件中做更改。

(1)在构造函数中添加代码:

tcpSocket = new QTcpSocket(this);
connect(tcpSocket,SIGNAL(readyRead()),this,SLOT(readMessage()));
connect(tcpSocket,SIGNAL(error(QAbstractSocket::SocketError)),
         this,SLOT(displayError(QAbstractSocket::SocketError)));

这里关联了tcpSocket的两个信号,当有数据到来时发出readyRead()信号,我们执行读取数据的readMessage()函数。当出现错误时发出error()信号,我们执行displayError()槽函数。

(2)实现newConnect()函数。

void Widget::newConnect()
{
    blockSize = 0; //初始化其为0
tcpSocket->abort(); //取消已有的连接

//连接到主机,这里从界面获取主机地址和端口号
    tcpSocket->connectToHost(ui->hostLineEdit->text(),
                             ui->portLineEdit->text().toInt());
}

这个函数实现了连接到服务器,下面会在“连接”按钮的单击事件槽函数中调用这个函数。

(3)实现readMessage()函数。

void Widget::readMessage()
{
    QDataStream in(tcpSocket);
    in.setVersion(QDataStream::Qt_4_6);
    //设置数据流版本,这里要和服务器端相同
    if(blockSize==0) //如果是刚开始接收数据
    {
       //判断接收的数据是否有两字节,也就是文件的大小信息
       //如果有则保存到blockSize变量中,没有则返回,继续接收数据
       if(tcpSocket->bytesAvailable() < (int)sizeof(quint16)) return;
       in >> blockSize;
    }
    if(tcpSocket->bytesAvailable() < blockSize) return;
    //如果没有得到全部的数据,则返回,继续接收数据
    in >> message;
    //将接收到的数据存放到变量中
    ui->messageLabel->setText(message);
    //显示接收到的数据
}

这个函数实现了数据的接收,它与服务器端的发送函数相对应。首先我们要获取文件的大小信息,然后根据文件的大小来判断是否接收到了完整的文件。

(4)实现displayError()函数。

void Widget::displayError(QAbstractSocket::SocketError)
{
    qDebug() << tcpSocket->errorString(); //输出错误信息
}

这里简单的实现了错误信息的输出。

(5)我们在widget.ui中进入“连接”按钮的单击事件槽函数,然后更改如下。

void Widget::on_pushButton_clicked() //连接按钮
{
    newConnect(); //请求连接
}

这里直接调用了newConnect()函数。

总结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值