- 日志1.0 :在QT中如何使用TCP协议进行套接字通信(即指网络通信)
- TCP 和 UDP是传输层协议, 二者的区别:
- TCP是面向连接的流式传输协议;TCP传输, 数据安全;
- UDP是面向无连接的报式传输协议;UDP的传输, 数据不安全;【比如, 抖音刷视频, 还有直播等等】
- 面向连接:TCP在连接的时候, 需要进行3次握手;断开的时候, 需要进行4次挥手;即TCP是一种双向连接, 双向断开的机制;
- UDP:在通信之前是不需要准备的, 直接通信不需要连接, 所以在UDP中是没有3次握手与4次挥手的;
- TCP在进行数据传输的时候, 有数据校验机制, 当数据包丢失之后, 会自动进行重传;而UDP中是没有数据校验机制的, 数据丢失就无法找回;
- 因为TCP是流式传输协议, 所以, 发送端和接收端处理的数据量可以不均等, 例如, 在发送端发送10M的数据, 而在接收端可以分10次, 每次抽1M;
- 而UDP是报文形式的, 在UDP传输数据的时候, 是以报文的形式发送的, 那么一个报文有多长, 在接收端就收多长, 例如:发送50M的数据包过去, 但是在接收端收不到50M, 就会把整个包都丢了, 不存在先收一部分数据的情况;
1.1. TCP的通信流程
1.2. UDP的通信流程
无论是TCP还是UDP通信, 其通信流程都是一样的, 只是使用的语言可能不一样, 而后面要用的QT, 也只是在QT中对通信流程进行封装, 让操作更加简单, 其实底层的通信原理和通信的过程是完全相同的;
如果要进行网络通信, 它和语言是没有任何关系的, 要进行网络通信, 只需要关注到底是基于TCP还是UDP(两个传输层的协议)通信; 在传输层协议定下来后, 往上还有应用层(FTP协议,HTTP协议)
1.3. QT 网络通信用到的两个类
- 使用QT提供的类进行基于TCP的套接字通信需要用到两个类:
- QTcpServer:服务器端使用, 服务器类, 用于监听客户端连接以及和客户端建立连接;
- QTcpSocket:服务器端和客户端都要用到的, 进行客户端和服务器端的通信, 接收和发送数据;通信的套接字类。
- 在QT中进行网络通信, 其实也要进行I/O操作, 这里的I/O是指网络I/O, 而不是磁盘I/O, 操作的是网络数据;
- 在QT中, 用于套接字通信的QTcpSocket类(网络数据的接收发送), 和QT中进行文件操作的QFile类(文件的读写), 其祖先类是一样的,即QIODevice;二者本质一样, 都是进行I/O操作, 只不过操作的对象不一样。
负责监听, 并且接受客户端的连接;
- 查阅QT帮助文档, 可以看到QTcpServer类属于network模块, 所以, 在.pro工程文件中, 需要把这个模块加上编译
1.1. 构造函数:
- , 这个构造函数接收一个参数, , 即指定一个父对象;
- 父对象这个概念很有用!(可以了解一下QT的对象树概念)在QT中存在一个内存回收机制, 通过指定父对象, 就可以将当前的节点挂在父对象的下面, 当父对象析构的时候, 会自动析构其子节点, 这就可以保证将挂在这个父对象下的所有子节点都析构掉(树状结构, 操作时递归完成);
- 父对象和父类不是一个概念;父类是有继承关系的, 而父对象没有;
1.2. 给监听的套接字设置监听:listen
- 设置监听 -》 对应前面TCP通信流程中的服务器的bind和listen两步(先绑定再设置监听)
- 参数1:const QHostAddress &address = QHostAddress::Any, 要绑定的本地地址, Any指绑定本地任意的IP地址, 可以使用这个默认参数, 这个Any支持IPV4也支持IPV6;
- 参数2:对应主机上的对应端口(会为某个进程绑定固定的端口)
- 确定了IP和端口, 数据就可以发送到某台主机上的某个进程上(确定主机的应用程序, 端口就是用来确定应用程序的 | 而IP用来定位主机);
- port = 0, 代表服务器随机分配一个端口(开发者就不知道了), 所以, 最好自己传入固定的端口号;端口号:0 - 65535;最好指定5000(8000, 10000以上)以上的端口号, 5000以下有些可能会被操作系统占用;
- 类封装IPV4, IPV6
1.3. 判断当前的服务器对象是否已经开始监听了:isListen
1.4. 服务器地址和端口信息
- : 如果当前服务器对象正在监听, 则返回监听的服务器地址信息, 否则返回;
- :如果当前服务器对象正在监听连接, 则返回服务器的端口, 否则返回0;
1.5. : 返回下一个挂起的连接作为已连接的QTcpSocket对象。
- :当服务器启动监听之后, 若有客户端连接上来, 那么服务器就会与客户端建立连接, 建立连接之后就会有一个用于通信的套接字对象;
- 这里一下, 虽然返回的是一个指针, 但是地址在函数体内部分配出来, 所以这里指针指向一块有效的堆内存;所以, 用完后要释放, 你可以自己释放掉,也可以让服务器对象自己释放:前面讲过了对象树的概念, 这里的QTcpSocket对象是由QTcpServer(父对象)对象返回的, 二者是父子对象关系, 这就组成了一个对象树, 当QTcpServer对象析构的时候会先析构QTcpSocket对象(结构上的父子关系 ),保证了内存不泄漏,但是这两者是没有继承关系的;
1.6. 等待新连接:
- :这个函数是一个阻塞函数, 当服务器端启动监听之后, 调用这个函数会阻塞当前的服务器线程, 阻塞线程等待客户端的连接, 若没有客户端来连接, 就会一直阻塞住, 所以可以指定阻塞时长msec(毫秒), 当时间到了还没有客户端连接, 函数就会解除阻塞;
- 而第二个参数timedOut:用来标识是超时解除的阻塞, 还是非超时解除的阻塞;
- :指定阻塞的最大时长, 单位为毫秒(ms);
- :传出参数, 如果操作超时timedOut为true, 没有超时为false;(这里是要传入地址的, 因为是bool *)
- 因为这是一个阻塞函数, 所以在开发的时候, 不推荐使用它来检测有没有新的客户端来连接, 而是使用信号和槽的方式, 这样就不会阻塞了;
2.1.
- 当有新的客户端连接上来, 这个类就会发出信号, 然后connect绑定槽函数, 在槽函数中(处理对应的新连接)调用, 得到一个用于通信的套接字对象, 基于这个对象去接收和发送数据;
2.2.
- 当服务器和客户端建立连接的时候失败了, 就会发出这样的信号, 通过这个信号传递出的参数, 就可以知道和客户端建立连接失败的原因。
- 用于通信的套接字类:, 关于这个类的使用有两种情况:
- case 1: 在服务器端调用, 得到直接用于通信的QTcpSocket;
- case 2: 在客户端创建对象, 创建出来的这个对象不能直接用来通信, 需要先连接服务器, 连接成功之后才能和服务器进行通信;
如何连接服务器;
:连接服务器
- 这个函数是重载函数;
- :
- 参数1 : 指定要连接的服务器的地址, IP地址;
- 参数2 : 端口, 是服务器端程序绑定的端口, 服务器绑定了什么端口, 在连接的时候就基于什么端口连接;
- 参数3 : 打开方式, 默认情况下对套接字对象操作的那个内存块是进行写还是读操作?
- 参数4 : 用默认值, IP协议;
- :
- 参数1 :指定要连接的服务器地址, 通过QHostAddress进行封装;
- 参数2 : 端口, 是服务器端程序绑定的端口;
- 参数3 : 打开方式, 默认情况下对套接字对象操作的那个内存块是进行写还是读操作?
- :
如何基于进行通信, 通信即是指数据的读或写;
- 是一个套接字通信类, 无论是在客户端还是服务器端都需要使用它。在Qt中发送和接收数据也属于I/O操作(网络I/O), 如下是这个类的继承关系:
1.1. 构造函数
- : 创建一个状态为UnconnectedState的QTcpSocket对象。
1.2. 连接服务器:
需要指定服务器绑定的IP和端口信息 继承自QAbstractSocket
- 这个函数是重载函数;
- :
- 参数1 : 指定要连接的服务器的地址, IP地址;
- 参数2 : 端口, 是服务器端程序绑定的端口, 服务器绑定了什么端口, 在连接的时候就基于什么端口连接;
- 参数3 : 打开方式, 默认情况下对套接字对象操作的那个内存块是进行写还是读操作?
- 参数4 : 用默认值, IP协议;
- :
- 参数1 :指定要连接的服务器地址, 通过QHostAddress进行封装;
- 参数2 : 端口, 是服务器端程序绑定的端口;
- 参数3 : 打开方式, 默认情况下对套接字对象操作的那个内存块是进行写还是读操作?
- :
1.3. 接收数据:相关的read
在 Qt 中不管调用读操作函数接收数据,还是调用写函数发送数据,操作的对象都是本地的由 Qt 框架维护的一块内存。所以,调用了发送函数,数据不一定会马上被发送到网络中,调用了接收函数也不是直接从网络中接收数据。
- : 指定可接收的最大字节数 maxSize 的数据到指针 data 指向的内存中;
- :指定可接收的最大字节数 maxSize,返回接收的字符串;
- :将当前可用操作数据全部读出,通过返回值返回读出的字符串;
1.4. 发送数据:相关的write
-
:发送指针 data 指向的内存中的 maxSize 个字节的数据
-
: 发送指针 data 指向的内存中的数据,字符串以 ‘0’ 作为结束标记
-
:发送参数指定的字符串
-
调用read和write操作的都是QT帮我们维护的这块内存, QT框架检测到有数据后, 会把数据发送到网络中,所以read和write不是直接操作的网络中的数据;
查阅帮助文档, 可以发现QTcpSocket所用到的信号都是从父类QAbstractSocket继承来的;
2.1. QAbstractSocket中的信号:
2.1.1.
- This signal is emitted after connectToHost() has been called and a connection has been successfully established:当我们调用connectToHost()时, 如果成功连接了服务器, 那么这个信号就会被发射出来;
2.1.2.
- This signal is emitted when the socket has been disconnected:在套接字断开连接时发出 disconnected() 信号;
- 若B端断开了连接, 在A端通信的就会发射出disconnected信号;(检测对端有没有断开连接, 不是当前这端, 而是通信的对端)
2.2. QIODevice中的信号:
2.2.1.
- 在使用QTcpSocket进行套接字通信的过程中, 如果该类对象发射出信号, 说明对端发送的数据达到了, 之后就可以调用接收数据了;
1.1 通信流程
- 创建套接字服务器对象(用于监听的套接字对象)
- 绑定 + 设置监听:通过对象设置监听, 即: : IP + 端口
- 基于信号检测是否有新的客户端连接;
- 如果发出了的信号, 说明有新的客户端连接, 就在对应的槽函数中调用 得到通信的套接字对象;
- 使用通信的套接字对象和客户端进行通信:中提供了接收和发送数据的函数read和write;并且在中提供了3个信号:
- QIODevice::readyRead():可以知道对端有没有发送数据过来, 若有数据过来, 对象会发出信号,在这个信号对应的槽函数中做对应的接收数据的操作即可;
- 还可以通过对象去检测当前的连接是否成功了:,这个连接的检测是在客户端进行检测的, 服务器端是不可以使用这个信号的;
- :无论在客户端或者服务器端都是可以使用的, 只要通信的两端(A, B), 其中的某一端(B)断开了连接, 那么在这一端(A)就可以通过对象发射出这个信号, 通知A端, 对端B已经断开了连接;
1.2 服务器窗口界面设计:
- 创建一个服务器的工程, base 基类选择QMainWindow, 这样会带菜单栏, 工具栏和状态栏;
- 菜单界面设计:详细图解
1.3 服务器端套接字通信处理
开始开发之前, 先在.pro工程文件中将network模块加入进去;记得保存刷新, 才能将模块重新导入; 另外, 在Qt Creator中, 提供了可以在UI设计界面, 直接右键控件转到槽(如下图所示:), 由系统自动帮我们去进行信号和槽的连接, 这里为了加深练习, 全部都统一使用手动连接信号和槽:
- 按照前面的流程, 先在服务器端创建一个用于监听的套接字对象, ;
- 将状态栏的两种连接状态图片(连接成功和连接失败)添加到项目的资源文件中;
1.4 完整服务器端代码
2.1 通信流程
- 创建用于通信的套接字类对象:实例化后要去连接服务器, 否则无法进行套接字通信;
- 使用服务器端绑定的IP和端口连接服务器:;
- 连接成功之后, 就可以使用对象和服务器进行通信;
2.2 客户端窗口界面设计
- 创建一个客户端的工程, base 基类选择QMainWindow, 这样会带菜单栏, 工具栏和状态栏;
- 菜单界面设计:详细图解
2.3 客户端套接字通信处理代码
按照通信流程走;
- 根据IP和端口连接服务器, 当点击发送文件按钮后, 会将选择的磁盘文件传输给服务器端;
- 当服务器端接收完毕后, 服务器会断开连接, 客户端这边会发射出一个disconnected信号, 通知服务器已经接收完毕, 客户端断开连接;
- 以上通信流程都是在子线程中完成;
Qt中提供了两种线程的创建方式;步骤如下,我们使用其中的一种, 更为灵活的创建方式
2.1 过程步骤
- 创建一个新的类SendFile, 让这个类从QObject派生
- 在这个类中添加自定义的任务函数(公共的成员函数 | 子线程执行任务), 函数体就是我们要子线程中执行的业务逻辑
- 在主线程中创建一个QThread对象, 这就是子线程的对象;
- 在主线程中创建工作的类对象, 并且不要给创建的对象指定父对象;
- 将创建出来的工作的类对象移动到创建的子线程对象中, 需要调用QObject类提供的方法:
- 启动子线程, 调用, 这时候线程启动了, 但是移动到线程中的对象是没有工作的;
- 借助信号和槽机制, 调用工作的类对象的工作函数, 让这个函数开始执行, 这时候是在移动到的那个子线程中运行的;
2.2 实例代码
- :准备要在子线程中完成的工作, 连接服务器和发送文件给服务器
上面采用了Qt中提供的多线程使用方式的其中一种, 接下来在服务器端使用另外一种较为简单的使用方法, 因为服务器端程序没有太多的功能, 仅仅是在子线程中接收文件;
- 需要创建一个线程类的子类, 让其继承QT中的线程类QThread, 比如:
- 重写父类的虚函数方法, 在该函数内部编写子线程要处理的具体的业务流程
- 在主线程中创建子线程对象, new一个
- 启动子线程, 调用方法
- 只要调用方法, 子线程中方法就可以被执行;
- 新建一个recvFile类, 继承自QThread
- recvfile.h
- 前面, 当用于监听的套接字对象发射出一个newConnection信号, 就说明有客户端发起了连接, 就调用得到一个用于通信的套接字对象;
- 然后把这个用于通信的套接字对象, 直接传递给了子线程; 但是有可能会遇到通信的套接字对象无法在子线程中工作的问题,下面介绍的就是如何解决? (无论哪种使用子线程的方式, 这里解决的方法一样)
- 在QTcpServer中有一个虚函数, :可以得到一个用于通信的文件描述符, 并且这个函数自动被QT框架所调用, 不需要调用;
- 当客户端向服务器发起了连接, 函数就会被调用, 得到一个用于通信的文件描述符;
- 调用, 其实是对通信的文件描述符进行了封装, 得到一个QTcpSocket对象, 而 没有对文件描述符进行封装, 所以我们需要自己对通信的文件描述符进行封装;
- 先在当前服务器端的项目中, 新添加一个类MyTcpServer, 继承自QTcpServer
- 在当前的这个子类中, 重写受保护的虚函数
- 在子线程中将通信的文件描述符封装成QTcpSocket
- mainwindow.h