简述:
TCP (Transmission Control Protocol ),传输控制协议,是一种可靠、面向连接、面向数据流的传输协议,许多高层应用协议(包括HTTP、FTP等)都是以它为基础,TCP协议非常适合数据的连续传输。
TCP 协议与 UDP 协议的差别见下表:
注意:在.pro文件中要添加 QT += network,否则无法使用Qt的网络功能。
Qt 中通过 QTcpSocket 类 和 QTcpServer 类实现TCP 协议的编程。
QTcpServer的基本操作:
A. 调用listen监听端口
B. 连接信号newConnection,在槽函数里调用nextPendingConnection获取连接进来的socket。
QTcpSocket的基本操作:
A. 调用connectToHost连接服务器
B. 调用waitForConnected判断是否连接成功
C. 连接信号readyRead槽函数,异步读取数据
D. 调用waitForReadyRead,阻塞读取数据
本文介绍一个基于TCP 协议的网络聊天室应用,由客户端和服务器两部分组成。
操作系统:windows 7
Qt构建套件:qt-opensource-windows-x86-mingw530-5.7.0.exe
Qt Creator版本:4.0.2
1 > TCP 工作原理
TCP协议能够为应用程序提供可靠的通信连接,使一台计算机发出的字节流无差错地送达网络上的其他计算机。
因此,对可靠性要求高的数据通信系统往往使用TCP协议传输数据,但在正式收发数据前,通信双方必须先建立连接。
2 > TCP 编程模型
基于TCP协议的经典编程模型,程序编写的通用流程如下:
1 > tcpserver.h ,声明了需要的各个控件,TcpServer 继承自 QDialog ,实现了服务器端的对话框的显示与控制 。
2 > tcpserver.cpp , TcpServer 类的构造函数主要实现窗体各控件的创建、布局等。#ifndef TCPSERVER_H #define TCPSERVER_H #include
#include #include #include #include #include #include "server.h" class TcpServer : public QDialog { Q_OBJECT public: TcpServer(QWidget *parent = 0,Qt::WindowFlags f=0); ~TcpServer(); private: QListWidget *ContentListWidget; QLabel *PortLabel; QLineEdit *PortLineEdit; QPushButton *CreateBtn; QGridLayout *mainLayout; int port; Server *server; public slots: void slotCreateServer(); void updateServer(QString,int); }; #endif // TCPSERVER_H
#include "tcpserver.h" TcpServer::TcpServer(QWidget *parent,Qt::WindowFlags f) : QDialog(parent,f) { setWindowTitle(tr("TCP Server")); //设置窗体的标题 //初始化各个控件 ContentListWidget = new QListWidget; PortLabel = new QLabel(tr("端口:")); PortLineEdit = new QLineEdit; CreateBtn = new QPushButton(tr("创建聊天室")); //设置布局 mainLayout = new QGridLayout(this); mainLayout->addWidget(ContentListWidget,0,0,1,2); mainLayout->addWidget(PortLabel,1,0); mainLayout->addWidget(PortLineEdit,1,1); mainLayout->addWidget(CreateBtn,2,0,1,2); port=8010; //初始化TCP端口号 PortLineEdit->setText(QString::number(port)); connect(CreateBtn,SIGNAL(clicked()),this,SLOT(slotCreateServer())); } TcpServer::~TcpServer() { } //slotCreateServer():创建一个TCP 服务器 void TcpServer::slotCreateServer() { //创建一个Server对象 server = new Server(this,port); //将Server 对象的updateServer()信号与相应的槽函数进行连接 connect(server,SIGNAL(updateServer(QString,int)),this,SLOT(updateServer(QString,int))); CreateBtn->setEnabled(false); } //updateServer()用于更新服务器上的信息显示 void TcpServer::updateServer(QString msg,int length) { ContentListWidget->addItem(msg.left(length)); }
3 > tcpclientsocket.h 及 tcpclientsocket.cpp , TcpClientSocket 继承自 QTcpSocket ,创建一个 TCP 套接字,以便在服务器端实现与客户端程序的通信 。
tcpclientsocket.h :
#ifndef TCPCLIENTSOCKET_H #define TCPCLIENTSOCKET_H #include
#include class TcpClientSocket : public QTcpSocket { Q_OBJECT //添加宏(Q_OBJECT)是为了实现信号与槽的通信 public: TcpClientSocket(QObject *parent=0); signals: void updateClients(QString,int); void disconnected(int); protected slots: void dataReceived(); void slotDisconnected(); }; #endif // TCPCLIENTSOCKET_H
tcpclientsocket.cpp :#include "tcpclientsocket.h" TcpClientSocket::TcpClientSocket(QObject *parent) { //指定了信号与槽的连接关系 //readyRead()是QIODevice的signal,由QTcpSocket继承而来 //在QT中,QTcpSocket被看成一个QIODevice,readyRead()信号在有数据到来时发出。 connect(this,SIGNAL(readyRead()),this,SLOT(dataReceived())); //disconnected()信号在断开连接时发出 connect(this,SIGNAL(disconnected()),this,SLOT(slotDisconnected())); } //当有数据到来时,触发dataReceived() void TcpClientSocket::dataReceived() { /*bytesAvailable()表示有效数据*/ while(bytesAvailable()>0) { int length = bytesAvailable(); char buf[1024]; read(buf,length); QString msg=buf; //updateClients()信号是通知服务器向聊天室内所有成员广播信息 emit updateClients(msg,length); } } void TcpClientSocket::slotDisconnected() { emit disconnected(this->socketDescriptor()); }
4 > server.h 及 server.cpp,Server 类继承自QTcpServer ,实现一个TCP 协议的服务器。利用QTcpServer ,可以监听到指定的端口的TCP连接 。
server.h :
#ifndef SERVER_H #define SERVER_H #include
#include #include "tcpclientsocket.h" //包含TCP的套接字 class Server : public QTcpServer { Q_OBJECT //添加宏(Q_OBJECT)是为了实现信号与槽的通信 public: Server(QObject *parent=0,int port=0); //保存与每一个客户端连接的TcpClientSocket QList tcpClientSocketList; signals: void updateServer(QString,int); public slots: void updateClients(QString,int); void slotDisconnected(int); protected: void incomingConnection(int socketDescriptor); }; #endif // SERVER_H
server.cpp :#include "server.h" Server::Server(QObject *parent,int port) :QTcpServer(parent) { //在指定的端口对任意地址进行监听 listen(QHostAddress::Any,port); /******************************* * QHostAddress::Null 表示一个空地址, * QHostAddress::LocalHost 表示IPv4的本地地址127.0.0.1, * QHostAddress::LocalHostIPv6 表示IPv6的本地地址, * QHostAddress::Broadcast 表示广播地址255.255.255.255, * QHostAddress::Any 表示IPv4的任意地址0.0.0.0, * QHostAddress::AnyIPv6 表示IPv6的任意地址 *******************************/ } /******************************* * 当出现一个新的连接时,QTcpServer 触发incomingConnection()函数 * 参数socketDescriptor 指定了连接Socket描述符 *******************************/ void Server::incomingConnection(int socketDescriptor) { //创建一个新的TcpClientSocket与客户端通信 TcpClientSocket *tcpClientSocket=new TcpClientSocket(this); //连接TcpClientSocket的updateClients()信号 connect(tcpClientSocket,SIGNAL(updateClients(QString,int)), this,SLOT(updateClients(QString,int))); //连接TcpClientSocket的disconnected()信号 connect(tcpClientSocket,SIGNAL(disconnected(int)), this,SLOT(slotDisconnected(int))); //将新创建的TcpClientSocket的套接字描述符指定为参数socketDescriptor tcpClientSocket->setSocketDescriptor(socketDescriptor); //将tcpClientSocket加入客户端套接字列表以便管理 tcpClientSocketList.append(tcpClientSocket); } /******************************* * updateClients()函数将任意客户端发来的信息进行广播, * 保证聊天室的所有用户均能看到其他人看发言 *******************************/ void Server::updateClients(QString msg,int length) { //发出updateServer()信号,用来通知服务器对话框更新相应的显示状态 emit updateServer(msg,length); /******************************* * 实现信息的广播, * tcpClientSocketList中保存了所有与服务器相连的TcpClientSocket对象 *******************************/ for(int i=0;i
write(msg.toLatin1(),length)!=length) { continue; } } } /******************************* * slotDisconnected()函数 * 实现从tcpClientSocketList列表中将断开连接的TcpClientSocket对象删除的功能 *******************************/ void Server::slotDisconnected(int descriptor) { for(int i=0;i socketDescriptor()==descriptor) { tcpClientSocketList.removeAt(i); return; } } return; }
1 > tcpclient.h ,TcpClient 类继承自QDialog 类,声明了需要的各种的控件。
#ifndef TCPCLIENT_H #define TCPCLIENT_H #include
#include #include #include #include #include #include #include class TcpClient : public QDialog { Q_OBJECT public: TcpClient(QWidget *parent = 0,Qt::WindowFlags f=0); ~TcpClient(); private: QListWidget *contentListWidget; QLineEdit *sendLineEdit; QPushButton *sendBtn; QLabel *userNameLabel; QLineEdit *userNameLineEdit; QLabel *serverIPLabel; QLineEdit *serverIPLineEdit; QLabel *portLabel; QLineEdit *portLineEdit; QPushButton *enterBtn; QGridLayout *mainLayout; bool status; int port; QHostAddress *serverIP; QString userName; QTcpSocket *tcpSocket; public slots: void slotEnter(); void slotConnected(); void slotDisconnected(); void dataReceived(); void slotSend(); }; #endif // TCPCLIENT_H
2 > tcpclient.cpp
#include "tcpclient.h" #include
#include TcpClient::TcpClient(QWidget *parent,Qt::WindowFlags f) : QDialog(parent,f) { setWindowTitle(tr("TCP Client"));//设置窗体的标题 //初始化各个控件 contentListWidget = new QListWidget; sendLineEdit = new QLineEdit; sendBtn = new QPushButton(tr("发送")); userNameLabel = new QLabel(tr("用户名:")); userNameLineEdit = new QLineEdit; serverIPLabel = new QLabel(tr("服务器地址:")); serverIPLineEdit = new QLineEdit; portLabel = new QLabel(tr("端口:")); portLineEdit = new QLineEdit; enterBtn= new QPushButton(tr("进入聊天室")); //设置布局 mainLayout = new QGridLayout(this); mainLayout->addWidget(contentListWidget,0,0,1,2); mainLayout->addWidget(sendLineEdit,1,0); mainLayout->addWidget(sendBtn,1,1); mainLayout->addWidget(userNameLabel,2,0); mainLayout->addWidget(userNameLineEdit,2,1); mainLayout->addWidget(serverIPLabel,3,0); mainLayout->addWidget(serverIPLineEdit,3,1); mainLayout->addWidget(portLabel,4,0); mainLayout->addWidget(portLineEdit,4,1); mainLayout->addWidget(enterBtn,5,0,1,2); status = false; //初始化TCP端口号 port = 8010; portLineEdit->setText(QString::number(port)); serverIP =new QHostAddress(); connect(enterBtn,SIGNAL(clicked()),this,SLOT(slotEnter())); connect(sendBtn,SIGNAL(clicked()),this,SLOT(slotSend())); sendBtn->setEnabled(false); } TcpClient::~TcpClient() { } void TcpClient::slotEnter() { //status表示当前的状态 if(!status) { //完成输入合法性检验 QString ip = serverIPLineEdit->text(); if(!serverIP->setAddress(ip)) //判断给定的IP是否能够被正确解析 { QMessageBox::information(this,tr("error"),tr("server ip address error!")); return; } if(userNameLineEdit->text()=="") { QMessageBox::information(this,tr("error"),tr("User name error!")); return; } userName=userNameLineEdit->text(); //初始化TCP客户端:创建一个QTcpSocket类对象,并将信号/槽连接起来 tcpSocket = new QTcpSocket(this); //实例化tcpSocket tcpSocket->abort(); //取消原有连接 connect(tcpSocket,SIGNAL(connected()),this,SLOT(slotConnected())); connect(tcpSocket,SIGNAL(disconnected()),this,SLOT(slotDisconnected())); connect(tcpSocket,SIGNAL(readyRead()),this,SLOT(dataReceived())); //与TCP服务器连接,连接成功后发出conneted()信号 tcpSocket->connectToHost(*serverIP,port); status=true; } else { int length=0; QString msg=userName+tr(":Leave Chat Room"); //构建一条离开聊天室的消息 //通知服务器端以上构建的信息 if((length=tcpSocket->write(msg.toLatin1(),msg.length()))!=msg. length()) { return; } //与服务器断开连接,断开连接后发出disconnected()信号 tcpSocket->disconnectFromHost(); status=false; } } /******************************* * slotConnected()为connected()信号的响应槽, * 当与服务器连接成功后,客户端构造一条进入聊天室的信息,并通知服务器 *******************************/ void TcpClient::slotConnected() { sendBtn->setEnabled(true); enterBtn->setText(tr("离开")); int length=0; QString msg=userName+tr(":Enter Chat Room"); if((length=tcpSocket->write(msg.toLatin1(),msg.length()))!=msg.length()) { return; } } void TcpClient::slotSend() { if(sendLineEdit->text()=="") { return ; } QString msg=userName+":"+sendLineEdit->text(); //write()发送数据 tcpSocket->write(msg.toLatin1(),msg.length()); sendLineEdit->clear(); } void TcpClient::slotDisconnected() { sendBtn->setEnabled(false); enterBtn->setText(tr("进入聊天室")); } /******************************* * dataReceived()函数,当有数据到来时,触发此函数, * 从套接字中将有效数据取出并显示 *******************************/ void TcpClient::dataReceived() { /*bytesAvailable()表示有效数据*/ while(tcpSocket->bytesAvailable()>0) { QByteArray datagram; datagram.resize(tcpSocket->bytesAvailable()); //read()读数据 tcpSocket->read(datagram.data(),datagram.size()); QString msg=datagram.data(); contentListWidget->addItem(msg.left(datagram.size())); } }
QTcpSocket 继承于QAbstractSocket继承于QIODevice 。
// ### Qt6: make the first one virtual bool bind(const QHostAddress &address, quint16 port = 0, BindMode mode = DefaultForPlatform); bool bind(quint16 port = 0, BindMode mode = DefaultForPlatform); //与服务器连接或断开连接 // ### Qt6: de-virtualize connectToHost(QHostAddress) overload virtual void connectToHost(const QString &hostName, quint16 port, OpenMode mode = ReadWrite, NetworkLayerProtocol protocol = AnyIPProtocol); virtual void connectToHost(const QHostAddress &address, quint16 port, OpenMode mode = ReadWrite); virtual void disconnectFromHost(); bool isValid() const; qint64 bytesAvailable() const Q_DECL_OVERRIDE;//表示有效数据 qint64 bytesToWrite() const Q_DECL_OVERRIDE; bool canReadLine() const Q_DECL_OVERRIDE; // ### Qt6: remove me quint16 localPort() const; QHostAddress localAddress() const; quint16 peerPort() const; QHostAddress peerAddress() const; QString peerName() const; qint64 readBufferSize() const; virtual void setReadBufferSize(qint64 size); void abort();//取消原有连接 virtual qintptr socketDescriptor() const; virtual bool setSocketDescriptor(qintptr socketDescriptor, SocketState state = ConnectedState, OpenMode openMode = ReadWrite); virtual void setSocketOption(QAbstractSocket::SocketOption option, const QVariant &value); virtual QVariant socketOption(QAbstractSocket::SocketOption option); SocketType socketType() const; SocketState state() const; SocketError error() const; // from QIODevice void close() Q_DECL_OVERRIDE; bool isSequential() const Q_DECL_OVERRIDE; bool atEnd() const Q_DECL_OVERRIDE; // ### Qt6: remove me bool flush(); // for synchronous access virtual bool waitForConnected(int msecs = 30000); bool waitForReadyRead(int msecs = 30000) Q_DECL_OVERRIDE; bool waitForBytesWritten(int msecs = 30000) Q_DECL_OVERRIDE; virtual bool waitForDisconnected(int msecs = 30000); #ifndef QT_NO_NETWORKPROXY void setProxy(const QNetworkProxy &networkProxy); QNetworkProxy proxy() const; #endif Q_SIGNALS: void hostFound(); void connected(); void disconnected(); void stateChanged(QAbstractSocket::SocketState); void error(QAbstractSocket::SocketError); #ifndef QT_NO_NETWORKPROXY void proxyAuthenticationRequired(const QNetworkProxy &proxy, QAuthenticator *authenticator); #endif protected: qint64 readData(char *data, qint64 maxlen) Q_DECL_OVERRIDE; qint64 readLineData(char *data, qint64 maxlen) Q_DECL_OVERRIDE; qint64 writeData(const char *data, qint64 len) Q_DECL_OVERRIDE; void setSocketState(SocketState state); void setSocketError(SocketError socketError); void setLocalPort(quint16 port); void setLocalAddress(const QHostAddress &address); void setPeerPort(quint16 port); void setPeerAddress(const QHostAddress &address); void setPeerName(const QString &name);
故 QTcpSocket 提供的几种接收和发送数据方法如下:
write ( const char *, qint64 ) : qint64 write ( const char * ) : qint64 write ( const QByteArray & ) : qint64 writeData ( const char*, qint64 ) : qint64 read ( char * data, qint64 maxSize): qint64 read ( qint64 maxSize):QByteArray readAll ():QByteArray readLine ( char *data, qint64 maxSize ):qint64 readLine ( qint64 maxSize =0 ):QByteArray