关于C++ QT中的Tcp
套接字定义:应用层通过传输层进行数据通信时,TCP和UDP会遇到同时为多个应用程序进程提供并发服务的问题。多个TCP连接或多个应用程序进程可能需要通过同一个TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了称为套接字 (Socket)的接口(函数库),区分并实现不同应用程序进程间的网络通信和连接。
其实Socket就是一个大类,实例化两个实例,调用里面的函数就能实现通信。
套接字其实就是函数的接口,需要三个参数:通讯类型(Tcp,Udp),IP,端口号。Socket原意是“插座”。通过将这3个参数结合起来,与一个“插座”Socket绑定,应用层就可以和传输层通过套接字接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。
要通过互联网进行通信,至少需要一对套接字,一个运行于客户机端,称之为ClientSocket,另一个运行于服务器端,称之为serverSocket。
步骤一:把这两个套接字实例化
这两类套接字都有自己对应的代码库,实例化就行。
步骤二:连接两个套接字
根据连接启动的方式以及本地套接字要连接的目标,套接字之间的连接过程可以分为两个小步骤:1服务器套接字设置为监听,2给客户端套接字指向服务器IP与端口,并连接。
1:服务器监听过程:服务器端套接字并不定位具体的客户端套接字,也就是说并不需要客户端的IP与端口号(只需要记录说话的内容而不需要记录说话的是谁),但是要在listen函数中绑定自己的网卡对应IP与端口用于监听。这时处于等待连接的状态,实时监控网络状态。
tcpServer->listen(QHostAddress::Any,8888);
这表示服务器套接字tcpServer监听自己网卡对应的所有的IP地址以及IP地址对应的8888端口。
2:连接:是指由客户端的套接字提出连接请求(方向固定!一般都是默认客户端请求服务器),要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字的地址和端口号,然后就向服务器端套接字提出连接请求。
tcpSocket->connectToHost(QHostAddress(ip),port);
代码含义:客户端套接字tcpSocket绑定服务器的IP以及端口号,也就是上面服务器监听的端口号。QT中的ConnectHost函数可以为客户端套接字连接服务器套接字
步骤三:确认连接
服务器端,如果接到客户端的套接字传来的连接信息,会发出newConnection()信号。
listen到了,connect(tcpServer,&QTcpServer::newConnection,={qDebug<<“收到客户端的连接信息”});
之后服务器的套接字会向客户端套接字发送已连接信息(也是系统自己操做的,实际是tcpServer生成的tcpSocket发送),客户端收到该信息后会产生一个connected()信号。
connect(tcpSocket, &QTcpSocket::connected,={ui->textread->setText(“成功建立连接”);});
连接成功后,tcpServer套接字会获得请求通信的套接字tcpSocket的IP与地址,以备回复信息使用。
查看ip与端口的代码:
QString ip= tcpSocket->peerAddress().toString();
qint64 port= tcpSocket->peerPort();
步骤四: 发送信息
套接字连接成功后,两个套接字之间就仿佛建立了通道,可以相互发送数据了!但需要注意的是:
1:信息从通道一端发出就必定会传播到另一端,就好比你打电话一方说一句话声音必然也只能传达到另一方,因此发信息的过程不需要再人为指定对方的IP与端口(通道建立的时候双方套接字内已经指定了对方套接字的IP,端口信息),你只需要调用tcpSocket->write(str.toUtf8().data());就可以发消息了。
2:tcpServer是监听套接字,也就是说服务器套接字只能监听端口,这就意味着服务器不能收发消息!怎么办呢?再在服务器端创造一个tcpSocket套接字呗。但再创造一个套接字的话就意味着上面的所有和客户端套接字的连接步骤还要重新来一遍,还要创立一条新通道,麻烦!如何用之前我们已经创造的通道给客户端通信呢?QT C++的库中已经给我们写好了按原通道回话的简便方法,依然使用连接好的通路,在服务器端的代码中:
tcpSocket = tcpServer->nextPendingConnection();就是在tcpServer的基础上生成一个tcpSocket。
注意,这个tcpSocket是服务器端的第二个套接字,但与重新实例化一个套接字不同,由于它是在tcpServer的基础上生成一个tcpSocket,它知道通道另一端的客户端套接字的IP与端口,因此可以直接回复信息(客户端的tcpSocket连接服务器的tcpServer,服务器端的TcpServer套接字生成了一个子套接字tcpSocket,由这个子套接字tcpSocket和客户端进行收发消息通信)。
至此,大功告成,实现了客户端和服务器的双向通信。通完信不要忘了断开客户端连接,关闭套接字
我们需要明确的是:1、接受信息的一方不需要发送者的IP与port,发送信息的一方必须要知道对方的IP与port。
2、tcpServer只有监听功能,服务器的收发信息功能是由它生成的tcpSocket负责的。
客户端cpp代码:
1 | #include "clientwidget.h" |
服务器.cpp代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
tcpServer = NULL;
tcpSocket = NULL;
tcpServer = new QTcpServer(this);
tcpServer->listen(QHostAddress::Any,8888);
setWindowTitle("服务器: 8888");
connect(tcpServer,&QTcpServer::newConnection,
[=]()
{
tcpSocket = tcpServer->nextPendingConnection();
QString ip= tcpSocket->peerAddress().toString();
qint64 port= tcpSocket->peerPort();
QString temp= QString("[%1:%2]:成功连接").arg(ip).arg(port);
ui->textread->setText(temp);
connect(tcpSocket, &QTcpSocket::readyRead,
[=]()
{
QByteArray array = tcpSocket->readAll();
ui->textread->append(array);
}
);
}
);
}
Widget::~Widget()
{
delete ui;
}
void Widget::on_Buttonsend_clicked()
{
if(NULL == tcpSocket)
{
return;
}
QString str =ui->textwrite->toPlainText();
tcpSocket->write(str.toUtf8().data());
}
void Widget::on_Buttonclose_clicked()
{
if(NULL == tcpSocket)
{
return;
}
tcpSocket->disconnectFromHost();
tcpSocket->close();
tcpSocket = NULL;
}
至于Tcp粘包处理,可以在接收函数中修改,详见下面视频。
https://www.bilibili.com/video/BV1yf4y1Y7CU/?spm_id_from=333.337.search-card.all.click&vd_source=b6da26061de3320a8170666f06381a91
TCP三次握手:
TCP协议是传输层里面的一个协议,TCP在建立连接之前进行的三次握手对于很多人来说可能一直是迷。干嘛要握三次手?握其他次数不行咩?
那么我们就来讲讲TCP的三次握手都干了些什么吧(才疏学浅,说错的地方希望大家指正)
一、首先要明确三次握手的目的是什么?
三次握手的目的是为了确认客户端和服务端的收发功能是正常的。
那么就是需确认的东西一共有4个:
1.客户端的发送功能;
2.客户端的接收功能;
3.服务端的发送功能;
4.服务端的接收功能;
二、那三次握手分别都确认了哪些功能呢?
1、第一次握手
第一次握手是客户端主动发起的,简单粗暴的理解就是客户端给服务端写了一封信;
这就跟以前的人寄信一样,我自己知道自己是把信放进了邮箱里面,但是我并不知道邮差是不是把它拿出来寄出去了,
也就是说,客户端只知道自己发送了报文,但是报文有没有被发送出去就不知道,所以这个时候客户端对于自己的发送功能是否完好是不确定的;
但是当第一次握手的报文被送达服务端的时候,服务端就知道自己的接收功能是完好的了;因为它已经收到了嘛;
所以第一次握手就确定服务端的接收功能(括号内为握手编号):
注意:你可能会说,服务端收到客户端的信了,看到信了不就可以知道客户端的发送功能是完好的了吗?很遗憾,我们要让客户端知道 自己 的发送功能和接收功能是正常的;要让服务端知道 自己 的发送和接收功能是正常的,这也是为什么客户端发送一个seq=x,服务端要回信一个ack=x+1,这就是第二次握手时告诉客户端你发送的具体内容我可以收到,因为我回的是你发内容+1!这样客户端在第二次握手就知道自己的发送功能正常了!
2.第二次握手
第一次握手是客户端发起的,第二次握手就是服务器端发起的;
别人给你写了信,你收到了肯定就要回信嘛;
那么这个时候我们就可以来看一下参数(为了方便理解,我们暂时将报文信息理解成参数):
第一次握手客户端会给服务端传过去两个参数,其中有一个是:seq=x;
SYN我们先不管,这个seq=x我们就可以看成是写在信里的内容;
第二次握手的时候服务器端给客户端返回的是4个参数,其中有2个是:seq=y;ack=x+1
那么这个ack=x+1我们就可以理解成是回答了客户端写在信里的内容,因为来信是x,客户端接收到来自服务端的x+1的时候就知道自己写出去的东西被收到了;就可以知道自己的发送功能的正常的;同时客户端也能知道自己的接收功能是正常的,因为能够接收来自服务器端的消息;
那为什么服务端也要写出信息seq=x给客户端呢?
因为服务端也不知道自己的发送功能是否正确嘛;
所以在第二次握手里面可以确定的功能是:
3.第三次握手
第三次握手是客户端发起的,就是这么一来一回的;
在第二次握手里面我们就知道握手和确认功能完善之间的逻辑,那么接下来也只剩下一个功能,就是服务端的接收功能;
服务端怎么知道自己的发送功能是正确的呢?
类比第二次握手我们就知道了,只要收到客户端关于seq的回答就好了;
第二次握手的时候服务向客户端发送了seq=y
有seq发出去,那么客户端就要将对信的回复写在ack里面;
所以第三次握手所传的参数里面有ack=y+1,也就是说客户端回复了服务端的seq;
当服务端接收到这个反馈的时候就知道自己的发送功能的正确的了。
总之呢就是一句话:能接收就说明接收功能正常;但是能发送不一定代表发送功能正常,发送功能需要通过 seq(写信)和ack(回信) 才能判断。