Five years past when I last did socket programming in UNIX ENV. Socket programming, or network programming, is quite difficult because of the nature of network environment. Check out 《UNIX network programming》 volume 1, you will find how difficult it is. Especially, the debugging and testing of network application are harder -- however, this difficulty of debugging and testing can be eliminated by using mock frameworks such as google mock.
Fortunately, we have many frameworks / libraries that encapsulate the complexity of network programming and provide a set of simple but powerful API's. Qt Framework is excellent one.
Upon installation of Qt SDK, you will get a serials of examples, covering all kinds of Qt modules. In networking module, there's a chat tool example using TCP protocol. Now I would like to provide a chat tool using UDP protocol.
Before start, let's check the some characteristics of UDP protocol:
- Unreliable but meeting is easy to implement
- Connectionless so protocol needed.
To make meeting function possible, C/S model is used. A tiny protocol is provided, as well. The protocol looks like below:
A protocol parser is provided so that the implementation of protocol can be replaced when needed -- with fixed protocol fields.
Server’s job:
- Maintains a client pool (so called registry pool)
- Receives and handles the register/deregister request from client
- Notifies all clients about changes to registry
- Forwards chatting request to corresponding client
Client’s job:
- Registers to / deregisters from server for identification
- Initiates chat session with other client
- Responses chatting request from other client
- Creates dialog for chatting
Messages exchanged between server and client for client registration:
Messages exchanged between server and client for client deregistration:
Messages exchanged between clients for session initiation:
Main classes used to achieve the functionalities:
- ProtocolParser
- Encapsulates parsing of protocol
- Generates protocol message
- ClientWindow
- Subscribes to server
- Registers to server
- Initiates chatting requests and responses to requests
- ChatDialog: Transfer chatting msg between clients
- ServerWindow
- Maintains registry pool
- Handles register/deregister requests
Server handles registration request:
void ServerWindow::processTheDatagram(QByteArray* datagram, QHostAddress& sender, quint16 senderPort) { ProtocolParser pp(*datagram); // 'switch' statement is not good choice. Command pattern is better. switch (pp.getCmd()) { case CMD_REGISTER: // Registration of client. addRegistryItem(ProtocolParser::generateCliString(pp.getName(), sender, senderPort), pp.getCliString()); addTableItem(pp.getCliString()); mTxtOutput->append(tr("%1: %2 @ %3:%4") .arg(REG_STR) .arg(pp.getName()) .arg(sender.toString()) .arg(senderPort)); break;
case CMD_DEREGISTER: // Deregistration of client. removeRegistryItem(ProtocolParser::generateCliString(pp.getName(), sender, senderPort), pp.getCliString()); removeTableItem(pp.getCliString()); mTxtOutput->append(tr("%1: %2 @ %3:%4") .arg(DEREG_STR) .arg(pp.getName()) .arg(sender.toString()) .arg(senderPort)); break;
case CMD_ERROR:
default: mOutputMsg.append(tr("Protocol error: %1 @ %2:%3\n") .arg(pp.getName()) .arg(sender.toString()) .arg(senderPort)); mTxtOutput->setText(mOutputMsg); break; } }
|
Server publishes the registry pool:
void ServerWindow::publishListenClients() { // Through subscribe channel of each client, foreach (QString subscriber, mSubscribeRegistry) { ClientStringParser csp(subscriber); QString addr = csp.getAddr(); quint16 port = csp.getPort();
if (port != 0xFFFF) { // tell them the change to registry pool. foreach (QString listener, mCliRegistry) mSrvChannel->writeDatagram(listener.toAscii(), QHostAddress(addr), port); } } }
|
Client initialization:
void ClientWindow::init() { // "Listen" on subscribe channel which subscribing server's registry pool // infomation. mChannelSubscribe = new QUdpSocket(this); mChannelSubscribe->open(QIODevice::ReadWrite); if (!mChannelSubscribe->bind(QHostAddress(mAddr), 0, QUdpSocket::ShareAddress)) { QMessageBox::critical(this, tr("Error"), mChannelSubscribe->errorString()); QApplication::exit(-1); }
// "Listen" on listen channel which waiting for incoming protocol msg. mChannelListen = new QUdpSocket(this); mChannelListen->open(QIODevice::ReadWrite); if (!mChannelListen->bind(QHostAddress(mAddr), 0, QUdpSocket::ShareAddress)) { QMessageBox::critical(this, tr("Error"), mChannelListen->errorString()); QApplication::exit(-1); } }
|
Registration of client:
void ClientWindow::registerClient() { // Datagram for registration. QByteArray datagram = ProtocolParser::generateDatagram(CMD_REGISTER, mName, mChannelListen->localAddress(), mChannelListen->localPort());
// Register to server. if (mChannelSubscribe->writeDatagram(datagram, datagram.length(), QHostAddress(mSrvAddr), mSrvPort) == -1) { QMessageBox::critical(this, tr("Error"), tr("Register on server %1:%2 failed") .arg(mSrvAddr) .arg(mSrvPort)); QApplication::exit(-1); } mBtnDereg->setEnabled(true); mBtnReg->setEnabled(false); }
|
Deregistration of client:
void ClientWindow::deregisterClient() { // Datagram for deregistration. QByteArray datagram = ProtocolParser::generateDatagram(CMD_DEREGISTER, mName, mChannelListen->localAddress(), mChannelListen->localPort());
// Deregister from server. if (mChannelSubscribe->writeDatagram(datagram, datagram.length(), QHostAddress(mSrvAddr), mSrvPort) == -1) { QMessageBox::critical(this, tr("Error"), tr("Deregister on server %1:%2 failed") .arg(mSrvAddr) .arg(mSrvPort)); QApplication::exit(-1); } clearBuddyTable(); mBtnDereg->setEnabled(false); mBtnReg->setEnabled(true); }
|
Client handles server's publish:
void ClientWindow::processSubscribeData(const QByteArray& datagram) { QString str = QString::fromAscii(datagram.constData()); if (tr("%1:%2:%3").arg(mName) .arg(mChannelListen->localAddress().toString()) .arg(mChannelListen->localPort()) .compare(str) != 0) { QTableWidgetItem* item = new QTableWidgetItem(str); int curRow = mTblBuddies->rowCount(); mTblBuddies->insertRow(curRow); mTblBuddies->setItem(curRow, 0, item); } }
|
Client handles chatting request and response:
void ClientWindow::processData(const QByteArray& datagram, QString listenAddr, quint16 listenPort) { ProtocolParser pp(datagram);
// To identify a chatting dialog in ClientWindow's object tree. QString objName= FMT_OBJ_NAME // PeerName:PeerAddr:PeerListenPort .arg(pp.getName()).arg(listenAddr).arg(listenPort) // SelfName:SelfAddr:SelfListenPort .arg(mName).arg(mAddr).arg(mChannelListen->localPort());
ChatDialog* chatDlg = this->findChild<ChatDialog*>(objName); QByteArray peerMsg;
// Command pattern should be better. switch (pp.getCmd()) { case CMD_PEER_RESP: if (chatDlg == 0) { // No chatting dialog is opened when response received. // Indicates an stray CMD_PEER_RESP received. QMessageBox::critical(this, tr("Error"), tr("Stray datagram:\n%1").arg(pp.toString())); if (mBtnDereg->isEnabled()) deregisterClient(); QApplication::exit(-1); } // Session established. chatDlg->setPeerAddrAndPort(QHostAddress(pp.getAddr()), pp.getPort()); chatDlg->show(); break;
case CMD_PEER_REQ: if (chatDlg == 0) chatDlg = createChatDialog(objName); peerMsg = ProtocolParser::generateDatagram(CMD_PEER_RESP, mName, QHostAddress(mAddr), chatDlg->getChannel()->localPort());
// Response the chatting req. if (mChannelListen->writeDatagram(peerMsg, QHostAddress(listenAddr), listenPort) == -1) { QMessageBox::critical(this, tr("Error"), tr("Send response to %1@%2:%3 failed!") .arg(pp.getName()) .arg(listenAddr) .arg(listenPort)); if (mBtnDereg->isEnabled()) deregisterClient(); QApplication::exit(-1); } chatDlg->setPeerAddrAndPort(QHostAddress(pp.getAddr()), pp.getPort()); chatDlg->show(); break;
default: QMessageBox::critical(this, tr("Error"), tr("Protocol error:\n %1").arg(pp.toString())); if (mBtnDereg->isEnabled()) deregisterClient(); QApplication::exit(-1); break; } }
|
Chatting - client sends message:
void ChatDialog::sendMsg() { // Generates the datagram. QByteArray msg = ProtocolParser::generateDatagram(CMD_MESSAGE, mName, mChannel->localAddress(), mChannel->localPort(), mTxtSend->toPlainText()); // Writes to peer. if (mChannel->writeDatagram(msg, mPeerAddr, mPeerPort) == -1) { mTxtRecv->append(tr("** ERROR! **\nSend msg to %1@%2:%3 failed!") .arg(mPeerName) .arg(mPeerAddr.toString()) .arg(mPeerPort)); }
// Displays in recv area. mTxtRecv->append(FMT_CLI_MSG_SELF .arg(getTimestamp()) .arg(mTxtSend->toPlainText())); // Clears the send area for next msg to be sent. mTxtSend->clear(); }
|
Chatting - client receives message:
void ChatDialog::processIncomingMsg(const QByteArray& msg, QString peerAddr, quint16 peerPort) { // Displays in recv area. ProtocolParser pp(msg); mTxtRecv->append(FMT_CLI_MSG .arg(getTimestamp()) .arg(mPeerName) .arg(peerAddr) .arg(peerPort) .arg(pp.getMsg())); }
|
There're many enhancement points:
-
Meeting not implemented so far
-
Only English supported: internationalization needed
-
Buggy system: Unit / integration tests needed
-
Careless design
-
Poor reusability
-
Data/Engine/UI (MVC) pattern should be used.
-
Can only be used in LAN. To support WAN, NAT and Heart-beat protocol is needed.
阅读(2089) | 评论(4) | 转发(0) |