今天学了什么
今天学习Qt的TCP部分。
TCP
定义:TCP(传输控制协议),是面向连接的字节流传输层协议。
TCP 五大特点:
- 面向连接:通信前建立连接,通信结束断开连接;
- 可靠性传输:数据不丢、不错、不乱序、不重复;
- 字节流传输:无消息边界,数据是连续字节;
- 全双工:双方可同时收发数据;
- 自带流量控制、拥塞控制;
TCP 为什么可靠(6点):
- 三次握手:建立连接,确认双方收发能力正常;
- ACK应答机制:收到数据必须回复确认;
- 超时重传:未收到ACK,自动重发数据;
- 序号+确认号:实现数据排序、去重;
- 流量控制(滑动窗口):防止接收缓冲区溢出;
- 拥塞控制:防止网络拥堵瘫痪;
三次握手(建立连接):
- 客户端发 SYN:请求连接;
- 服务端回 SYN+ACK:同意连接;
- 客户端回 ACK:连接正式建立;
四次挥手(断开连接):
TCP全双工,收发通道分开关闭,所以需要四次:
- 主动方发 FIN:我不发数据了;
- 被动方回 ACK:收到,我还能继续发数据;
- 被动方发 FIN:我也发完了,也不发了;
- 主动方回 ACK:确认关闭(等待2MSL);
2MSL 概念:
- MSL:报文在网络中的最大存活时间;
- 2MSL:主动关闭方最后等待的时间;
作用:保证最后一次 ACK 报文对方一定收到;等待网络中残留的旧数据包全部消失,不干扰新连接。
粘包、拆包:
-
产生原因:TCP是无边界字节流 + 内核缓冲区 + Nagle算法 + 网络MTU分片。
-
定义:
- 粘包:多条消息合并成一条接收;
- 拆包:一条大消息被分成多次接收;
- 三种解决办法(从优到劣):
- 头部长度法(项目最常用):前4字节存消息长度;
- 特殊分隔符法:文本结尾加标记;
- 定长数据包:固定每条消息长度;
Nagle 算法(粘包核心原因):
作用:攒小包、合并发送,减少网络数据包数量,优化网络效率。
副作用:攒包机制 导致 TCP 粘包;
Qt关闭方式:开启低延迟,小包立刻发送:
socket->setSocketOption(QAbstractSocket::LowDelayOption, 1);
Qt TCP 核心信号:
- QTcpSocket 客户端/通信端:
- connected:连接成功(只发数据,不读数据);
- readyRead:有数据到达(只能在这里读数据);
- disconnected:连接断开;
- errorOccurred:通信出错;
- QTcpServer 服务端:
- newConnection:监听到新客户端连接;
TCP客户端连接服务器步骤:
① 创建客户端对象;
② socket->connectToHost(Ip,port); //"127.0.0.1",8080
③ connect(socket,&QTcpSocket::connected,...
TCP服务器连接客户端步骤:
① 创建服务器对象;
② serve->listen(QHostAddress::Any,port);
③ connect(serve,&QTcpServer::newConnection,this,=(){
soc=serve->nextPendingConnection();
connect(soc,&QTcpSocket::readyRead,this,=(){
QByteArray text=soc->readAll();
...}); });
任务1:极简 TCP 服务端与客户端
服务端:
QTcpServer 监听端口,收到 newConnection() 后获取 QTcpSocket,收到数据打印出来。
客户端:
QTcpSocket 连接服务端,发送 "Hello Server"。
实现:
① 服务器端:
cpp
serve=new QTcpServer(this);
serve->listen(QHostAddress::Any,8080);
connect(serve,&QTcpServer::newConnection,this,[=](){
soc=serve->nextPendingConnection();
connect(soc,&QTcpSocket::readyRead,this,[=](){
QByteArray text=soc->readAll();
qDebug()<<"服务器接收到客户端发送的消息:"<<text;
soc->write("hello Scoket");
});
});
② 客户端:
cpp
socket=new QTcpSocket(this);
socket->connectToHost("127.0.0.1",8080);
connect(socket,&QTcpSocket::connected,this,[=](){
socket->write("Hello Server");
});
connect(socket,&QTcpSocket::readyRead,[=](){
QByteArray text=socket->readAll();
qDebug()<<"客户端接收到服务器发送的消息:"<<text;
});
带界面的简易聊天室
要求:点击按钮,客户端发送消息给服务端;服务端收到后回复客户端。
实现:
① 设计UI界面:

② 编写代码
cpp
#include "mainwindow.h"
#include "./ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
serve=new QTcpServer(this);
socket=new QTcpSocket(this);
//显示服务器Ip
QString ip;
for(auto &a:QNetworkInterface::allAddresses())
{
if(a.protocol()==QAbstractSocket::IPv4Protocol&&!a.isLoopback())
{
ip=a.toString();
break;
}
}
qDebug()<<"拿到的Ip:"<<ip;
ui->server_Ip_line->setText(ip);
connect(serve,&QTcpServer::newConnection,this,[=](){
QTcpSocket *s=serve->nextPendingConnection();
soc=s;
connect(s,&QTcpSocket::readyRead,this,[=]()
{
//读取发送来的内容
QByteArray text=s->readAll();
qDebug()<<"原始字节:"<<text;
ui->server_textEdit->append(QString("收到客户端发送信息:%1").arg(QString::fromUtf8(text)));
});
});
connect(socket,&QTcpSocket::connected,this,[=](){
ui->cus_textEdit->append("客户端连接服务器成功。");
});
connect(socket,&QTcpSocket::readyRead,this,[=](){
QByteArray text=socket->readAll();
ui->cus_textEdit->append("收到服务器发送信息:"+text);
});
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_server_con_btn_clicked()
{
int port=ui->server_port_line->text().toUInt();
bool flag=serve->listen(QHostAddress::Any,port);
if(flag)
{
ui->server_textEdit->append("服务端监听成功,端口:"+QString::number(port));
}
else
{
ui->server_textEdit->append("监听失败!端口被占用");
}
}
void MainWindow::on_server_sendmsg_btn_clicked()
{
if(serve->isListening())
{
soc->write(ui->server_sendmsg_line->text().toUtf8());
}
}
void MainWindow::on_cus_con_btn_clicked()
{
QString ip=ui->cus_Ip_line->text();
int port=ui->cus_port_line->text().toInt();
socket->connectToHost(ip,port);
}
void MainWindow::on_cus_sendmsg_btn_clicked()
{
if(socket->isOpen())
{
socket->write(ui->cus_sendmsg_line->text().toUtf8());
}
}
void MainWindow::on_dele_clicked()
{
ui->cus_textEdit->setText("客户端连接服务器成功。");
}
复盘:① 文本框勾选readOnly,防止运行后,输入干扰;
② 显示服务器的IP地址,以供客户端连接;
③ 文本格式用UTF-8,防止读出来乱码;
④ 代码只是练习用,正式项目里服务端、客户端一定是分开两个程序(两个工程);同一
个窗口里一边服务端一边客户端,只适合学习调试;
⑤ soc 指针没有进行空指针判断:
在 on_server_sendmsg_btn_clicked 里,如果还没有客户端连接,soc 可能是空指针或者野指针,
直接 soc->write() 会导致程序崩溃。应该先判断:
if(soc && soc->state() == QAbstractSocket::ConnectedState);
⑥ socket->write 的返回值:
TCP 是流式协议,write 不一定能一次性把所有数据发出去。虽然小数据通常没问题,但严谨的做
法是检查返回值或者写大块数据时处理发送缓冲区;
⑦ 客户端断线没有处理:没有监听 disconnected 信号。如果服务端关掉,客户端不知道,再发数
据就会报错。应该监听 disconnected 信号并在界面上提示"连接已断开"。