├── .hgignore ├── src ├── qmldir ├── Sockets.pluginspec ├── qml-sockets.h ├── qml │ └── TCPSocket.qml ├── tcp.h ├── udp_multicast.h └── tcp_server.h ├── TODO.md ├── test ├── tst_tcp_initial.qml ├── ServerTestSocket.qml ├── tst_tcp.qml ├── tst_udp_multicast.qml ├── tst_tcp_server.qml ├── tst_tcp_server_clientDelegate.qml ├── tst_tcp_server_maxClients.qml └── tst_tcp_match.qml ├── Rakefile ├── qml-sockets.pro ├── LICENSE └── README.md /.hgignore: -------------------------------------------------------------------------------- 1 | ^Makefile$ 2 | ^build/ 3 | \.sublime-\w+$ 4 | ^\.git/ 5 | ^plugin\.pro\.user$ 6 | -------------------------------------------------------------------------------- /src/qmldir: -------------------------------------------------------------------------------- 1 | module org.jemc.qml.Sockets 2 | plugin qml-sockets 3 | 4 | TCPSocket 1.0 qml/TCPSocket.qml 5 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | 2 | - Finish match functionality for TCPSocket 3 | 4 | - Add match functionality to other socket types 5 | 6 | - Add method to TCPServer to stop listening (opposite of listen()) 7 | -------------------------------------------------------------------------------- /test/tst_tcp_initial.qml: -------------------------------------------------------------------------------- 1 | 2 | import QtTest 1.0 3 | import QtQuick 2.0 4 | 5 | import org.jemc.qml.Sockets 1.0 6 | 7 | TestCase { 8 | id: test 9 | name: "TCPSocket initial" 10 | 11 | TCPSocket { 12 | id: socket 13 | } 14 | 15 | function test_state() { 16 | compare(socket.state, 0) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Sockets.pluginspec: -------------------------------------------------------------------------------- 1 | 2 | Joe McIlvain 3 | Copyright 2013 Joe McIlvain 4 | MIT License 5 | Qt Quick 6 | Exposing Qt's C++ socket objects to QML for declarative use. 7 | https://github.com/jemc/qml-sockets 8 | 9 | 10 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | 2 | require 'qt/commander' 3 | 4 | task :default => :test 5 | 6 | task :android do 7 | Qt::Commander::Creator.profiles.select(&:android?).each do |profile| 8 | profile.toolchain.env do 9 | system "#{profile.version.qmake} *.pro -spec android-g++" and 10 | system "make && make install" 11 | end 12 | end 13 | end 14 | 15 | task :install do 16 | system "qmake *.pro && make && make install" 17 | end 18 | 19 | task :test => :install do 20 | system "qmltestrunner" 21 | end 22 | 23 | task :clean do 24 | `make clean && rm Makefile` 25 | end 26 | 27 | task :cleantest => [:clean, :test] 28 | -------------------------------------------------------------------------------- /src/qml-sockets.h: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | 5 | 6 | #include "tcp.h" 7 | #include "tcp_server.h" 8 | #include "udp_multicast.h" 9 | 10 | 11 | class QmlSocketsPlugin : public QQmlExtensionPlugin 12 | { 13 | Q_OBJECT 14 | Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") 15 | 16 | public: 17 | void registerTypes(const char *uri) 18 | { 19 | qmlRegisterType(uri, 1, 0, "AbstractTCPSocket"); 20 | qmlRegisterType(uri, 1, 0, "TCPServer"); 21 | qmlRegisterType(uri, 1, 0, "UDPMulticastSocket"); 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /test/ServerTestSocket.qml: -------------------------------------------------------------------------------- 1 | 2 | import QtTest 1.0 3 | import QtQuick 2.0 4 | 5 | import org.jemc.qml.Sockets 1.0 6 | 7 | TCPSocket { 8 | host: "localhost" 9 | port: server.port 10 | 11 | property var test 12 | property var verified: false 13 | property var response: "" 14 | property var expected: /Welcome/ 15 | 16 | onRead: { 17 | response += message 18 | write("Thanks, Server!") 19 | } 20 | onDisconnected: { 21 | test.verify(response!="", "No response from server") 22 | test.verify(response.search(expected)>=0, 23 | "Unrecognized response from server: %1".arg(response)) 24 | verified = true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /qml-sockets.pro: -------------------------------------------------------------------------------- 1 | 2 | TEMPLATE = lib 3 | 4 | CONFIG += qt plugin c++11 5 | QT += qml quick 6 | 7 | TARGET = $$qtLibraryTarget(qml-sockets) 8 | uri = org.jemc.qml.Sockets 9 | 10 | TARGETDIR = $$[QT_INSTALL_QML]/$$replace(uri, \\., /) 11 | SRCDIR = $$PWD/src 12 | DESTDIR = $$PWD/build/native 13 | 14 | android { 15 | VENDORDIR = $$PWD/vendor/prefix/$$ANDROID_TARGET_ARCH 16 | DESTDIR = $$PWD/build/$$ANDROID_TARGET_ARCH 17 | QMAKE_LIBDIR += $$VENDORDIR/lib 18 | QMAKE_INCDIR += $$VENDORDIR/include 19 | } 20 | 21 | HEADERS += $$SRCDIR/qml-sockets.h \ 22 | $$SRCDIR/tcp.h \ 23 | $$SRCDIR/tcp_server.h \ 24 | $$SRCDIR/udp_multicast.h 25 | 26 | OBJECTS_DIR = $$DESTDIR/.obj 27 | MOC_DIR = $$DESTDIR/.moc 28 | RCC_DIR = $$DESTDIR/.rcc 29 | UI_DIR = $$DESTDIR/.ui 30 | 31 | target.path = $$TARGETDIR 32 | qmldir.files = $$SRCDIR/qmldir 33 | qmldir.path = $$TARGETDIR 34 | qml.files = $$SRCDIR/qml/* 35 | qml.path = $$TARGETDIR/qml 36 | 37 | INSTALLS += target qmldir qml 38 | -------------------------------------------------------------------------------- /test/tst_tcp.qml: -------------------------------------------------------------------------------- 1 | 2 | import QtTest 1.0 3 | import QtQuick 2.0 4 | 5 | import org.jemc.qml.Sockets 1.0 6 | 7 | TestCase { 8 | id: test 9 | name: "TCPSocket" 10 | 11 | TCPSocket { 12 | id: socket 13 | host: "www.example.com" 14 | port: 80 15 | 16 | property var verified: false 17 | property var response: "" 18 | property var expected: /HTTP\/[^\n]* OK/ 19 | 20 | onConnected: write("GET / HTTP/1.1\r\nHost: %1\r\nConnection: close\r\n\r\n".arg(host)) 21 | onRead: response += message 22 | onDisconnected: { 23 | test.verify(response!=undefined, "No response received") 24 | test.verify(response.search(expected)>=0, 25 | "Unrecognized response received: %1".arg(response)) 26 | verified = true 27 | } 28 | } 29 | 30 | function wait_for_disconnect() { while(socket.state!=0) { wait(0) } } 31 | 32 | function test_it() { 33 | socket.connect() 34 | wait_for_disconnect() 35 | verify(socket.verified, "Disconnect hook never ran on socket.") 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/tst_udp_multicast.qml: -------------------------------------------------------------------------------- 1 | 2 | import QtTest 1.0 3 | import QtQuick 2.0 4 | 5 | import org.jemc.qml.Sockets 1.0 6 | 7 | TestCase { 8 | id: test 9 | name: "UDPMulticastSocket" 10 | 11 | UDPMulticastSocket { 12 | id: socket 13 | group: "239.255.250.250" 14 | port: 9131 15 | 16 | property var lastRead 17 | property var readCount: 0 18 | 19 | onRead: { lastRead = message; readCount += 1 } 20 | } 21 | 22 | function wait_for_connect() { while(socket.state!=4) { wait(0) } } 23 | function wait_for_disconnect() { while(socket.state!=0) { wait(0) } } 24 | 25 | function test_it() { 26 | socket.connect() 27 | wait_for_connect() 28 | 29 | compare(socket.lastRead, undefined) 30 | compare(socket.readCount, 0) 31 | socket.write("test") 32 | wait(100) 33 | compare(socket.lastRead, "test") 34 | compare(socket.readCount, 1) 35 | socket.write("other") 36 | wait(100) 37 | compare(socket.lastRead, "other") 38 | compare(socket.readCount, 2) 39 | 40 | socket.disconnect() 41 | wait_for_disconnect() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Joe Eli McIlvain 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/tst_tcp_server.qml: -------------------------------------------------------------------------------- 1 | 2 | import QtTest 1.0 3 | import QtQuick 2.0 4 | 5 | import org.jemc.qml.Sockets 1.0 6 | 7 | TestCase { 8 | id: test 9 | name: "TCPServer" 10 | 11 | TCPServer { 12 | id: server 13 | port: 4998 14 | 15 | property var verified: false 16 | property var response: "" 17 | property var expected: /Thanks/ 18 | 19 | onClientConnected: { 20 | client.write("Welcome") 21 | } 22 | onClientRead: { 23 | response += message 24 | client.disconnect() 25 | } 26 | onClientDisconnected: { 27 | test.verify(response!="", "No response from client") 28 | test.verify(response.search(expected)>=0, 29 | "Unrecognized response from client: %1".arg(response)) 30 | verified = true 31 | } 32 | } 33 | 34 | ServerTestSocket { id: socket; test: test } 35 | 36 | function wait_for_disconnect() { while(socket.state!=0) { wait(0) } } 37 | 38 | function test_it() { 39 | server.listen() 40 | socket.connect() 41 | wait_for_disconnect() 42 | verify(socket.verified, "Disconnect hook never ran on socket.") 43 | verify(server.verified, "Disconnect hook never ran on server.") 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/qml/TCPSocket.qml: -------------------------------------------------------------------------------- 1 | 2 | import QtQuick 2.1 3 | 4 | import org.jemc.qml.Sockets 1.0 5 | 6 | 7 | AbstractTCPSocket { 8 | // The expression to scan for in the incoming buffer 9 | property var expression 10 | 11 | // When the expression is matched in the buffer, 12 | // match is signalled with the matching string 13 | signal match(string matchString, var matchCaptures, string preMatchString) 14 | 15 | // The buffer of text waiting to be matched. 16 | // This buffer is cleared to an empty string in onConnected 17 | property string matchBuffer: "" 18 | 19 | onConnected: matchBuffer = "" 20 | 21 | onRead: { 22 | // Expression matching is disabled by default to save memory (matchBuffer) 23 | if(expression !== undefined) { 24 | // Append the new message to the buffer 25 | matchBuffer += message 26 | 27 | // Pull out each match and pre-match and trigger the signal 28 | var data, idx, str, pre 29 | while(data = matchBuffer.match(expression)) { 30 | idx = matchBuffer.search(expression) 31 | str = data[0] 32 | pre = matchBuffer.slice(0, idx) 33 | matchBuffer = matchBuffer.slice(str.length+idx) 34 | match(str, data.slice(1), pre) 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/tst_tcp_server_clientDelegate.qml: -------------------------------------------------------------------------------- 1 | 2 | import QtTest 1.0 3 | import QtQuick 2.0 4 | 5 | import org.jemc.qml.Sockets 1.0 6 | 7 | TestCase { 8 | id: test 9 | name: "TCPServer#clientDelegate" 10 | 11 | TCPServer { 12 | id: server 13 | port: 4998 14 | 15 | property var verified: false 16 | 17 | clientDelegate: TCPSocket { 18 | property var response: "" 19 | property var expected: /Thanks/ 20 | 21 | onConnected: write("Welcome") 22 | 23 | onRead: { 24 | response += message 25 | disconnect() 26 | } 27 | onDisconnected: { 28 | test.verify(response!="", "No response from client") 29 | test.verify(response.search(expected)>=0, 30 | "Unrecognized response from client: %1".arg(response)) 31 | server.verified = true 32 | } 33 | } 34 | } 35 | 36 | ServerTestSocket { id: socket; test: test } 37 | 38 | function wait_for_disconnect() { while(socket.state!=0) { wait(0) } } 39 | 40 | function test_it() { 41 | server.listen() 42 | socket.connect() 43 | wait_for_disconnect() 44 | verify(socket.verified, "Disconnect hook never ran on socket.") 45 | verify(server.verified, "Disconnect hook never ran on server.") 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # QML Sockets 3 | 4 | Exposing Qt's C++ socket objects to QML for declarative use. 5 | 6 | ## Overview 7 | 8 | Qt [says][QmlLang]: 9 | 10 | > QML offers a highly readable, declarative, JSON-like syntax with support for 11 | > imperative JavaScript expressions combined with dynamic property bindings. 12 | 13 | Qt also provides some very useful socket objects in their [QtNetwork] C++ API. 14 | 15 | However, Qt does not provide these objects in a QML API. This plugin is an 16 | attempt do so in order to simplify socket programming for basic applications. 17 | The QML API provided may not be able to handle some of the more advanced use 18 | cases, but the lowered ceremony it brings to socket programming can get you 19 | from idea to implementation in just a few lines: 20 | 21 | ```qml 22 | import Sockets 1.0 23 | 24 | TCPSocket { 25 | host: "www.the-world.com" 26 | port: 1025 27 | 28 | Component.onCompleted: connect() 29 | 30 | onConnected: write("Hello, World!") 31 | 32 | onRead: console.log("The World says:", message) 33 | 34 | onDisconnected: console.log("Goodbye, Cruel World...") 35 | } 36 | ``` 37 | 38 | [QmlLang]: http://qt-project.org/doc/qt-5/qmlapplications.html 39 | [QtNetwork]: http://qt-project.org/doc/qt-5/qtnetwork-programming.html 40 | -------------------------------------------------------------------------------- /src/tcp.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef QML_SOCKETS_TCP 3 | #define QML_SOCKETS_TCP 4 | 5 | #include 6 | 7 | 8 | class TCPSocket : public QObject 9 | { 10 | Q_OBJECT 11 | Q_ENUMS(QAbstractSocket::SocketState) 12 | 13 | Q_PROPERTY(QString host MEMBER m_host NOTIFY hostChanged) 14 | Q_PROPERTY(uint port MEMBER m_port NOTIFY portChanged) 15 | Q_PROPERTY(QAbstractSocket::SocketState state \ 16 | MEMBER m_state NOTIFY stateChanged) 17 | 18 | signals: 19 | void hostChanged(); 20 | void portChanged(); 21 | void stateChanged(); 22 | 23 | void read(const QString &message); 24 | void connected(); 25 | void disconnected(); 26 | 27 | public: 28 | TCPSocket(QObject* parent = 0) 29 | { (void)parent; assignSocket(); }; 30 | 31 | ~TCPSocket() 32 | { delete m_socket; m_socket = NULL; } 33 | 34 | void assignSocket(QTcpSocket *socket = NULL) 35 | { 36 | // Delete old socket if existent 37 | if(m_socket!=NULL) 38 | delete m_socket; 39 | 40 | // Create new socket or assign passed socket 41 | if(socket!=NULL) 42 | m_socket = socket; 43 | else 44 | m_socket = new QTcpSocket(this); 45 | 46 | // Register event handlers 47 | QObject::connect(m_socket, &QAbstractSocket::stateChanged, 48 | [=](QAbstractSocket::SocketState state) 49 | { setProperty("state", state); }); 50 | 51 | QObject::connect(m_socket, &QAbstractSocket::readyRead, 52 | [=]() { emit read(m_socket->readAll()); }); 53 | 54 | QObject::connect(m_socket, &QAbstractSocket::connected, 55 | [=]() { emit connected(); }); 56 | 57 | QObject::connect(m_socket, &QAbstractSocket::disconnected, 58 | [=]() { emit disconnected(); }); 59 | } 60 | 61 | public slots: 62 | void connect() 63 | { m_socket->connectToHost(m_host, m_port); } 64 | 65 | void disconnect() 66 | { m_socket->disconnectFromHost(); } 67 | 68 | void write(QString message) 69 | { m_socket->write(message.toLocal8Bit()); } 70 | 71 | public: 72 | QString m_host; 73 | uint m_port; 74 | QAbstractSocket::SocketState m_state = QAbstractSocket::UnconnectedState; 75 | QTcpSocket *m_socket = NULL; 76 | }; 77 | 78 | #endif 79 | -------------------------------------------------------------------------------- /test/tst_tcp_server_maxClients.qml: -------------------------------------------------------------------------------- 1 | 2 | import QtTest 1.0 3 | import QtQuick 2.0 4 | 5 | import org.jemc.qml.Sockets 1.0 6 | 7 | TestCase { 8 | id: test 9 | name: "TCPServer#maxClients" 10 | 11 | TCPServer { 12 | id: server 13 | port: 4998 14 | 15 | maxClients: 3 16 | 17 | property var verified_count: 0 18 | property var response: "" 19 | property var expected: /Thanks/ 20 | property var expected_con_counts: [1,2,3,3,3] 21 | property var expected_dis_counts: [2,2,2,1,0] 22 | 23 | onClientConnected: { 24 | // test.verify(clients.length==expected_con_counts.shift()) 25 | client.write("Welcome") 26 | } 27 | onClientRead: { 28 | response += message 29 | client.disconnect() 30 | } 31 | onClientDisconnected: { 32 | // test.verify(clients.length==expected_dis_counts.shift()) 33 | 34 | test.verify(response!="", "No response from client") 35 | test.verify(response.search(expected)>=0, 36 | "Unrecognized response from client: %1".arg(response)) 37 | verified_count += 1 38 | } 39 | 40 | // This tests that using a delegate doesn't break with multiple clients 41 | clientDelegate: TCPSocket { } 42 | } 43 | 44 | ServerTestSocket { id:socket1; test:test } 45 | ServerTestSocket { id:socket2; test:test } 46 | ServerTestSocket { id:socket3; test:test } 47 | ServerTestSocket { id:socket4; test:test } 48 | ServerTestSocket { id:socket5; test:test } 49 | 50 | function wait_for_disconnect() { 51 | while(socket1.state!=0 || 52 | socket2.state!=0 || 53 | socket3.state!=0 || 54 | socket4.state!=0 || 55 | socket5.state!=0) { wait(0) } 56 | } 57 | 58 | function test_it() { 59 | server.listen() 60 | socket1.connect() 61 | socket2.connect() 62 | socket3.connect() 63 | socket4.connect() 64 | socket5.connect() 65 | wait_for_disconnect() 66 | verify(socket1.verified, "Disconnect hook never ran on socket1.") 67 | verify(socket2.verified, "Disconnect hook never ran on socket2.") 68 | verify(socket3.verified, "Disconnect hook never ran on socket3.") 69 | verify(socket4.verified, "Disconnect hook never ran on socket4.") 70 | verify(socket5.verified, "Disconnect hook never ran on socket5.") 71 | verify(server.verified_count==5, "The 5 disconnect hooks didn't run.") 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/udp_multicast.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef QML_SOCKETS_UDP_MULTICAST 3 | #define QML_SOCKETS_UDP_MULTICAST 4 | 5 | #include 6 | 7 | 8 | class UDPMulticastSocket : public QObject 9 | { 10 | Q_OBJECT 11 | Q_ENUMS(QAbstractSocket::SocketState) 12 | 13 | Q_PROPERTY(QString group MEMBER m_group NOTIFY groupChanged) 14 | Q_PROPERTY(uint port MEMBER m_port NOTIFY portChanged) 15 | Q_PROPERTY(QAbstractSocket::SocketState state \ 16 | MEMBER m_state NOTIFY stateChanged) 17 | 18 | signals: 19 | void groupChanged(); 20 | void portChanged(); 21 | void stateChanged(); 22 | 23 | void read(const QString &message); 24 | void connected(); 25 | void disconnected(); 26 | 27 | public: 28 | UDPMulticastSocket(QObject* parent = 0) 29 | { 30 | (void)parent; 31 | m_socket = new QUdpSocket(this); 32 | 33 | QObject::connect(m_socket, &QAbstractSocket::stateChanged, 34 | [=](QAbstractSocket::SocketState state) 35 | { 36 | setProperty("state", state); 37 | if(state==QAbstractSocket::BoundState) emit connected(); 38 | }); 39 | 40 | QObject::connect(m_socket, &QAbstractSocket::readyRead, 41 | [=]() 42 | { 43 | while(m_socket->hasPendingDatagrams()) { 44 | QByteArray datagram; 45 | datagram.resize(m_socket->pendingDatagramSize()); 46 | m_socket->readDatagram(datagram.data(), datagram.size()); 47 | emit read(datagram.data()); 48 | } 49 | }); 50 | 51 | QObject::connect(m_socket, &QAbstractSocket::disconnected, 52 | [=]() { emit disconnected(); }); 53 | }; 54 | 55 | ~UDPMulticastSocket() 56 | { delete m_socket; m_socket = NULL; } 57 | 58 | public slots: 59 | void connect() 60 | { 61 | m_socket->bind(QHostAddress::AnyIPv4, m_port, QUdpSocket::ShareAddress); 62 | m_socket->joinMulticastGroup(QHostAddress(m_group)); 63 | } 64 | 65 | void disconnect() 66 | { 67 | m_socket->leaveMulticastGroup(QHostAddress(m_group)); 68 | m_socket->disconnectFromHost(); 69 | } 70 | 71 | void write(QString message) 72 | { 73 | if(m_state!=QAbstractSocket::BoundState) connect(); 74 | 75 | QByteArray datagram = message.toLocal8Bit(); 76 | m_socket->writeDatagram(datagram.data(), datagram.size(), 77 | QHostAddress(m_group), m_port); 78 | } 79 | 80 | public: 81 | QString m_group; 82 | uint m_port; 83 | QAbstractSocket::SocketState m_state; 84 | QUdpSocket *m_socket = NULL; 85 | }; 86 | 87 | #endif 88 | -------------------------------------------------------------------------------- /test/tst_tcp_match.qml: -------------------------------------------------------------------------------- 1 | 2 | import QtTest 1.0 3 | import QtQuick 2.0 4 | 5 | import org.jemc.qml.Sockets 1.0 6 | 7 | TestCase { 8 | id: test 9 | name: "TCPSocket#match" 10 | 11 | TCPServer { 12 | id: server 13 | port: 4998 14 | 15 | property string welcome 16 | property string welcome2 17 | 18 | function reset() { welcome = ""; welcome2 = "" } 19 | Component.onCompleted: { 20 | reset() 21 | compare(socket.expression, 22 | undefined, "expression matching is disabled by default") 23 | } 24 | 25 | onClientConnected: { 26 | client.write(welcome) 27 | test.wait(15) // Space between messages to separate as packets 28 | client.write(welcome2) 29 | client.disconnect() 30 | } 31 | } 32 | 33 | TCPSocket { 34 | id: socket 35 | host: "127.0.0.1" 36 | port: 4998 37 | 38 | property var matches 39 | property var preMatches 40 | property var captures 41 | 42 | function reset() { matches = []; preMatches = []; captures = [] } 43 | Component.onCompleted: reset() 44 | 45 | onMatch: { 46 | matches .push( matchString ) 47 | preMatches.push(preMatchString ) 48 | captures .push( matchCaptures) 49 | } 50 | 51 | onConnected: { 52 | test.compare(socket.matchBuffer, "", "matchBuffer reset on connect") 53 | } 54 | } 55 | 56 | function initTestCase() { server.listen() } 57 | 58 | function wait_for_connect() { while(socket.state!==2) { wait(0) } } 59 | function wait_for_disconnect() { while(socket.state!==0) { wait(0) } } 60 | 61 | function test_match() { 62 | socket.reset() 63 | server.reset() 64 | 65 | socket.expression = /(.*?)[\r\n]+/ 66 | 67 | server.welcome = "Welcome\nthe\rlovely " 68 | server.welcome2 = "new\n\rclient\r\nthe_rest" 69 | 70 | socket.connect() 71 | wait_for_disconnect() 72 | 73 | compare(socket.matches, ["Welcome\n","the\r","lovely new\n\r","client\r\n"]) 74 | compare(socket.matchBuffer, "the_rest") 75 | } 76 | 77 | function test_prematch() { 78 | socket.reset() 79 | server.reset() 80 | 81 | socket.expression = /[a-z]+(?=[^a-z])/ 82 | server.welcome = "!foo_ba" 83 | server.welcome2 = "r_99_baz!!!" 84 | 85 | socket.connect() 86 | wait_for_disconnect() 87 | 88 | compare(socket.matches, ["foo","bar","baz"]) 89 | compare(socket.preMatches, ["!","_","_99_"]) 90 | compare(socket.captures, [[],[],[]]) 91 | compare(socket.matchBuffer, "!!!") 92 | } 93 | 94 | function test_captures() { 95 | socket.reset() 96 | server.reset() 97 | 98 | socket.expression = /([a-z]+)_([a-z]+)[\r\n]+/ 99 | server.welcome = "foo_ba" 100 | server.welcome2 = "r\nfoo_baz\n" 101 | 102 | socket.connect() 103 | wait_for_disconnect() 104 | 105 | compare(socket.matches, ["foo_bar\n","foo_baz\n"]) 106 | compare(socket.preMatches, ["",""]) 107 | compare(socket.captures, [["foo","bar"],["foo","baz"]]) 108 | compare(socket.matchBuffer, "") 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/tcp_server.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef QML_SOCKETS_TCP_SERVER 3 | #define QML_SOCKETS_TCP_SERVER 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include "tcp.h" 10 | 11 | 12 | class TCPServer : public QObject 13 | { 14 | Q_OBJECT 15 | 16 | Q_PROPERTY(uint port MEMBER m_port NOTIFY portChanged) 17 | Q_PROPERTY(uint maxClients MEMBER m_maxClients NOTIFY maxClientsChanged) 18 | // Q_PROPERTY(QQmlListProperty clients READ clients) 19 | Q_PROPERTY(QQmlComponent* clientDelegate MEMBER m_clientDelegate NOTIFY clientDelegateChanged) 20 | 21 | signals: 22 | void portChanged(); 23 | void maxClientsChanged(); 24 | void clientDelegateChanged(); 25 | 26 | void clientRead(TCPSocket* client, const QString &message); 27 | void clientConnected(TCPSocket* client); 28 | void clientDisconnected(TCPSocket* client); 29 | 30 | public: 31 | TCPServer(QObject* parent = 0) 32 | { 33 | (void)parent; 34 | m_server = new QTcpServer(this); 35 | 36 | QObject::connect(m_server, &QTcpServer::newConnection, 37 | [=]() { 38 | QTcpSocket *client_sock = NULL; 39 | TCPSocket *client = NULL; 40 | 41 | // Forget the new client if client count is already at max 42 | if(m_maxClients!=0 && (uint)m_clients.count()>=m_maxClients) 43 | return; 44 | 45 | // If the clientDelegate was specified, try to instantiate it 46 | if(m_clientDelegate!=NULL) 47 | { 48 | client = qobject_cast( \ 49 | m_clientDelegate->create(QQmlEngine::contextForObject(this))); 50 | 51 | if(client==NULL) 52 | qWarning("TCPServer's clientDelegate component must be"\ 53 | " a TCPSocket. Using default TCPSocket instead.\n"); 54 | }; 55 | 56 | // Otherwise, instantiate the default 57 | if(client==NULL) 58 | client = new TCPSocket(this); 59 | 60 | // Get the next connection, and return if it didn't come through 61 | if((client_sock=m_server->nextPendingConnection())==NULL) 62 | return; 63 | 64 | // Assign the new connection to inside the client wrapper object 65 | client->assignSocket(client_sock); 66 | 67 | // on client.read, emit clientRead 68 | QObject::connect(client, &TCPSocket::read, 69 | [=](const QString &message) { 70 | emit clientRead(client, message); 71 | }); 72 | 73 | // on client.disconncted, emit clientDisconnected 74 | QObject::connect(client, &TCPSocket::disconnected, 75 | [=]() { 76 | m_clients.removeAll(client); 77 | 78 | emit clientDisconnected(client); 79 | client->deleteLater(); 80 | 81 | m_server->newConnection(); 82 | }); 83 | 84 | // emit clientConnected 85 | m_clients.append(client); 86 | 87 | emit clientConnected(client); 88 | client->connected(); 89 | }); 90 | }; 91 | 92 | ~TCPServer() 93 | { delete m_server; m_server = NULL; } 94 | 95 | // // Create the clients QML list property to expose the m_clients QList 96 | // QQmlListProperty clients() 97 | // { return QQmlListProperty( 98 | // (QObject*)this, 99 | // (void*)&m_clients, 100 | // [=](QQmlListProperty *prop) 101 | // { return static_cast< QList *>(prop->data)->count(); }, 102 | // [=](QQmlListProperty *prop, int index) 103 | // { return static_cast< QList *>(prop->data)->at(index); }); } 104 | 105 | public slots: 106 | void listen() 107 | { m_server->listen(QHostAddress::Any, m_port); } 108 | 109 | public: 110 | uint m_port; 111 | uint m_maxClients = 0; 112 | QList m_clients; 113 | QTcpServer* m_server = NULL; 114 | QQmlComponent* m_clientDelegate = NULL; 115 | }; 116 | 117 | #endif 118 | --------------------------------------------------------------------------------