我们知道TCP建立连接需要经过三次握手,而断开连接需要经过四次分手,那三次握手和四次分手分别做了什么和如何进行的。
第一次握手: 建立连接。客户端发送连接请求报文段,将SYN位置为1,Sequence Number为x;然后,客户端进入SYN_SEND状态,等待服务器的确认;
第二次握手: 服务器收到客户端的SYN报文段,需要对这个SYN报文段进行确认,设置Acknowledgment Number为x+1(Sequence Number+1);同时,自己自己还要发送SYN请求信息,将SYN位置为1,Sequence Number为y;服务器端将上述所有信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端,此时服务器进入SYN_RECV状态;
第三次握手: 客户端收到服务器的SYN+ACK报文段。然后将Acknowledgment Number设置为y+1,向服务器发送ACK报文段,这个报文段发送完毕以后,客户端和服务器端都进入ESTABLISHED状态,完成TCP三次握手。
完成了三次握手,客户端和服务器端就可以开始传送数据。以上就是TCP三次握手的总体介绍。通信结束客户端和服务端就断开连接,需要经过四次分手确认。
第一次分手: 主机1(可以使客户端,也可以是服务器端),设置Sequence Number和Acknowledgment Number,向主机2发送一个FIN报文段;此时,主机1进入FIN_WAIT_1状态;这表示主机1没有数据要发送给主机2了;
第二次分手: 主机2收到了主机1发送的FIN报文段,向主机1回一个ACK报文段,Acknowledgment Number为Sequence Number加1;主机1进入FIN_WAIT_2状态;主机2告诉主机1,我“同意”你的关闭请求;
第三次分手: 主机2向主机1发送FIN报文段,请求关闭连接,同时主机2进入LAST_ACK状态;
第四次分手: 主机1收到主机2发送的FIN报文段,向主机2发送ACK报文段,然后主机1进入TIME_WAIT状态;主机2收到主机1的ACK报文段以后,就关闭连接;此时,主机1等待2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,主机1也可以关闭连接了。
可以看到一次tcp请求的建立及关闭至少进行7次通信,这还不包过数据的通信,而UDP不需3次握手和4次分手。
TCP是面向链接的,虽然说网络的不安全不稳定特性决定了多少次握手都不能保证连接的可靠性,但TCP的三次握手在最低限度上(实际上也很大程度上保证了)保证了连接的可靠性;而UDP不是面向连接的,UDP传送数据前并不与对方建立连接,对接收到的数据也不发送确认信号,发送端不知道数据是否会正确接收,当然也不用重发,所以说UDP是无连接的、不可靠的一种数据传输协议。
也正由于1所说的特点,使得UDP的开销更小数据传输速率更高,因为不必进行收发数据的确认,所以UDP的实时性更好。知道了TCP和UDP的区别,就不难理解为何采用TCP传输协议的MSN比采用UDP的QQ传输文件慢了,但并不能说QQ的通信是不安全的,因为程序员可以手动对UDP的数据收发进行验证,比如发送方对每个数据包进行编号然后由接收方进行验证啊什么的,即使是这样,UDP因为在底层协议的封装上没有采用类似TCP的“三次握手”而实现了TCP所无法达到的传输效率。
现在我们了解到TCP/IP只是一个协议栈,就像操作系统的运行机制一样,必须要具体实现,同时还要提供对外的操作接口。就像操作系统会提供标准的编程接口,比如Win32编程接口一样,TCP/IP也必须对外提供编程接口。这就是Socket。
现在我们知道,Socket跟TCP/IP并没有必然的联系。Socket编程接口在设计的时候,就希望也能适应其他的网络协议。所以,Socket的出现只是可以更方便的使用TCP/IP协议栈而已,其对TCP/IP进行了抽象,形成了几个最基本的函数接口。比如create,listen,accept,connect,read和write等等。
不同语言都有对应的建立Socket服务端和客户端的库
对于Qt,自带的 “帮助” 可谓是神器,这里讲首先介绍如何通过帮助开启一个不熟悉的设计
输入关键词tcp,可以看到有tcp seriver和tcp socket两个索引,对于tcp主要就是需要这两个部分的函数。
在上图的红色圆圈中,可以得到两个重要的信息,一个是
qmake:QT += network
这个需要在工程的 .pro 文件中添加qmake,也就是如图所示的位置
帮助中显示还有一个重要的头文件需要添加,就是这个:
Header:#include <QTcpServer>
例如在使用 TCP 通讯建立连接时采用客户端服务器模式,这种模式又常 常被称为主从式架构,简称为 C/S 结构,属于一种网络通讯架构,将通讯的双 方以客户端(Client )与服务器 (Server) 的身份区分开来。使用 C/S 结构的通 信常见的还有 S7 通信, ISO-on-TCP 通信。
服务器的特征:被动角色,等待来自客户端的连接请求,处理请求并回传结果。
客户端的特征:主动角色,发送连接请求,等待服务器的响应。
根据主从关系,还是比较容易区分服务器和客户端的区别,其实就是多了一个请求的功能。
先来展示一下最终的效果图片,因为是TCP服务器的设计,所以只需要接受就可以,也就是只需要输入正确的端口号就可以接收数据。
对于界面中的按钮和模块,在菜单中的位置如下
需要注意的是下面几点
布局,优先小布局,尽量的能行排列还有列排列的先进行排列,最后在整体布局
QT编写的TCP服务器
有了ui界面,就可以直接对界面进行槽函数的设计,需要的模块右键,转到槽
然后就会跳转到我们的代码界面,也就是自动的生成了一个槽函数来供我们使用。
在头文件中,也可以看到生成的对应的函数声明
因为我们现在是对打开服务器这个按钮进行功能的设计,所以要实现的功能就是绑定端口号到socket,然后进行全局的监听。
void Widget::on_openBt_clicked()
{
/*
* QHostAddress::Any 监听来自所有人的连接
* listen返回值是无符号类型,所以要对text进行转换
* 监听所有的设备,读取端口号转化成符合规范的形式
*/
tcpServer->listen(QHostAddress::Any, ui->portEdit->text().toUInt());
}
对于listen这个函数,端口号格式需要是quint16,可以通过鼠标的悬停获得这样的 信息。
但是这样只是打开了设备的监听功能,需要通过自定义信号槽,来实现创建连接的功能,tcp是面向连接的服务,所以,要建立新连接才能进行正常的通信。
首先是我们在开启了监听端口的服务后,监听到了是需要建立连接才能进行下一步的通信,所以,要创建连接,带着这个目的,来到tcpserver的帮助,可以看到有个SINGAL
点进去SIGNAL就可以看到有两个函数,newConnection显然就是我们需要建立新连接的函数,然后查看详情,证实了我们的猜测。
所以可以设置我们的槽函数代码如下
connect(tcpServer, SIGNAL(newConnection()), this, SLOT(newConnection_Slot()));
建立的新连接之后,我们需要一个读取和显示的操作,所以建立连接之后,我们要再执行自定义函数newConnection_Slot()
怎么写,还是先看qt的帮助文档
Detailed Description
The QTcpServer class provides a TCP-based server.
This class makes it possible to accept incoming TCP connections. You can specify the port or have QTcpServer pick one automatically. You can listen on a specific address or on all the machine's addresses.
Call listen() to have the server listen for incoming connections. The newConnection() signal is then emitted each time a client connects to the server.
Call nextPendingConnection() to accept the pending connection as a connected QTcpSocket. The function returns a pointer to a QTcpSocket in QAbstractSocket::ConnectedState that you can use for communicating with the client.
If an error occurs, serverError() returns the type of error, and errorString() can be called to get a human readable description of what happened.
When listening for connections, the address and port on which the server is listening are available as serverAddress() and serverPort().
Calling close() makes QTcpServer stop listening for incoming connections.
Although QTcpServer is mostly designed for use with an event loop, it's possible to use it without one. In that case, you must use waitForNewConnection(), which blocks until either a connection is available or a timeout expires.
See also QTcpSocket, Fortune Server Example, Threaded Fortune Server Example, Loopback Example, and Torrent Example.
// Google翻译
详细说明
QTcpServer类提供了一个基于TCP的服务器。
此类可以接受传入的TCP连接。您可以指定端口或让QTcpServer自动选择一个。您可以侦听特定地址或所有机器的地址。
调用listen()以使服务器侦听传入的连接。每次客户端连接到服务器时,都会发出newConnection()信号。
调用nextPendingConnection()以接受挂起的连接作为已连接的QTcpSocket。该函数返回指向QAbstractSocket :: ConnectedState中的QTcpSocket的指针,可用于与客户端进行通信。
如果发生错误,则serverError()返回错误的类型,并且可以调用errorString()以获取对所发生情况的易于理解的描述。
侦听连接时,服务器正在侦听的地址和端口可用作serverAddress()和serverPort()。
调用close()使QTcpServer停止侦听传入的连接。
尽管QTcpServer主要设计用于事件循环,但也可以不使用事件循环。在这种情况下,您必须使用waitForNewConnection(),该阻塞将一直阻塞直到可用连接或超时到期为止。
另请参见QTcpSocket,Fortune Server示例,Threaded Fortune Server示例,Loopback示例和Torrent示例。
所以这里我们需要nextPendingConnection()函数,以接受挂起的连接作为已连接的QTcpSocket。如果没有这一步,那么我们每次通信都要建立一次连接,这样显然是不符合设计。建立了稳定的连接后,就是来进行正常的读写的操作了。
/* 获得已经连接的客户端的socket */
tcpSocket = tcpServer->nextPendingConnection();
牵扯到信号的读写,就像单片机的中断接收函数一样,我们要先检测到有数据进来,才能进行处理,所以,找到监听数据读取的函数,我们开始查看Tcpsever下的函数,有没有类似功能的,
一眼望去应该是没有的,所以要去找函数的父类,看看有没有读写相关的函数
很不幸,QObject里也没有read相关的函数,只能继续看QObject的父类
这里有一个父类,QIODevice,跟硬件驱动有关,打开看一下
我们在信号中可以找到我们需要的函数了
每当有新数据可用于从设备读取时,都会发出此信号。 仅当有新数据可用时(例如,当网络套接字上有新的网络数据有效负载时,或将新的数据块附加到设备上时),才会再次发出该数据。
readyRead()不会递归地发出; 如果您重新进入事件循环或在连接到readyRead()信号的插槽内调用waitForReadyRead(),则不会重新发出该信号(尽管waitForReadyRead()可能仍返回true)。
对于开发人员实现从QIODevice派生的类的开发人员,请注意:当新数据到达时,您应该始终发出readyRead()(不要仅因为缓冲区中仍有待读取的数据而发出它)。 在其他情况下不要发出readyRead()。
函数的说明很清晰的告诉我们了这是个监听数据的函数,所以我们离成功很近了,检测到数据之后,自然就是保存下来然后进行显示或者其他的处理,所以自定义槽函数来进行数据的处理。
/* 获得了socket之后就可以进行读写的操作 */
connect(tcpSocket, SIGNAL(readyRead()), this, SLOT(readyRead_Slot()));
自定义的槽函数readyRead_Slot()就是在监听到有数据进来之后,开始准备一个缓冲区,接收存储数据,然后进行处理,这里我就只进行了一个显示的处理。
void Widget::readyRead_Slot()
{
QString buf;
buf = tcpSocket->readAll();
ui->recvEdit->appendPlainText(buf); // ui界面的接收框显示
}
到这里,小总结一下
打开网络监听->绑定端口号到socket->挂起连接->监听数据读取的信号->处理读取到的数据
同理,然后就是关闭服务器和发送数据的函数了,但是这个比较简单,所以就不详细说明了
同样的,发送函数write,也需要注意发送数据的格式。
现在有了服务器,那么可以使用网络上下载的网络调试助手开启一个tcp客户端功能来验证了
这里的验证过程可以加深对于TCP服务器的理解,服务器本身具有IP和端口号,IP一般是固定的,这里的默认就是我电脑设备的IP(我电脑的IP地址是192.168.0.105,也就是服务器IP是192.168.0.105),所以不需要手动来输入.
只需要自定义服务器自己的端口号就可以了。
对于TCP的客户端通过我们已经有的网络调试助手,可以看到,需要输入指定服务器的IP和端口号,才能进行连接。
客户端的设计流程和服务端大致相同,唯一不同的因为是**“主动设备”**,所以相比于服务端,多了一个主动发出建立连接请求的功能,所以重点也是这个功能的设计。
因为多了选择连接的服务器地址和端口号的主动连接功能,所以在UI界面的设计上需要增加这部分的设计
QT编写的TCP客户端上位机
tcpSocket->connectToHost(ui->ipLineEdit->text(), ui->portLineEdit->text().toUInt());
配置好要链接的从机ip和端口号了,也连接了,那么还需要一个检测到连接的过程,这样才能触发后面的操作,也就是又要开始设计自定义的槽函数了,在tcpsocket的父类QAbstractSocket中有这样一个函数。
connect(tcpSocket, SIGNAL(connected()), this, SLOT(connected_Slot()));
所以,检测到连接成功,那么就执行connected_Slot()函数,实现读写的功能,这里就和之前的服务器大同小异了。
这时候可以用我们自己设计的服务器和客户端直接进行互相的验证了。
TCP上位机的设计到这里算是告一段落,后面还有UDP的上位机设计,还有UDP上位机与板间通信的设计与调试,尽请期待。