Americký výrobce čipů Nvidia získal od vlády prezidenta Donalda Trumpa souhlas s prodejem svých pokročilých počítačových čipů používaných k vývoji umělé inteligence (AI) H20 do Číny. Prodej těchto čipů speciálně upravených pro čínský trh by tak mohl být brzy obnoven, uvedla firma na svém blogu. Americká vláda zakázala prodej v dubnu, v době eskalace obchodního sporu mezi oběma zeměmi. Tehdy to zdůvodnila obavami, že by čipy mohla využívat čínská armáda.
3D software Blender byl vydán ve verzi 4.5 s prodlouženou podporou. Podrobnosti v poznámkách k vydání. Videopředstavení na YouTube.
Open source webový aplikační framework Django slaví 20. narozeniny.
V Brestu dnes začala konference vývojářů a uživatelů linuxové distribuce Debian DebConf25. Na programu je řada zajímavých přednášek. Sledovat je lze online.
Před 30 lety, tj. 14. července 1995, se začala používat přípona .mp3 pro soubory s hudbou komprimovanou pomocí MPEG-2 Audio Layer 3.
Výroba 8bitových domácích počítačů Commodore 64 byla ukončena v dubnu 1994. Po více než 30 letech byl představen nový oficiální Commodore 64 Ultimate (YouTube). S deskou postavenou na FPGA. Ve 3 edicích v ceně od 299 dolarů a plánovaným dodáním v říjnu a listopadu letošního roku.
Společnost Hugging Face ve spolupráci se společností Pollen Robotics představila open source robota Reachy Mini (YouTube). Předobjednat lze lite verzi za 299 dolarů a wireless verzi s Raspberry Pi 5 za 449 dolarů.
Dnes v 17:30 bude oficiálně vydána open source počítačová hra DOGWALK vytvořena v 3D softwaru Blender a herním enginu Godot. Release party proběhne na YouTube od 17:00.
McDonald's se spojil se společností Paradox a pracovníky nabírá také pomocí AI řešení s virtuální asistentkou Olivii běžící na webu McHire. Ian Carroll a Sam Curry se na toto AI řešení blíže podívali a opravdu je překvapilo, že se mohli přihlásit pomocí jména 123456 a hesla 123456 a získat přístup k údajům o 64 milionech uchazečů o práci.
Byla vydána (𝕏) červnová aktualizace aneb nová verze 1.102 editoru zdrojových kódů Visual Studio Code (Wikipedie). Přehled novinek i s náhledy a videi v poznámkách k vydání. Ve verzi 1.102 vyjde také VSCodium, tj. komunitní sestavení Visual Studia Code bez telemetrie a licenčních podmínek Microsoftu.
TCP je zkratka Transmission Control Protocol a jde o základní internetový protokol, sloužící k vytvoření spojení mezi počítači a k jejich komunikaci mezi sebou. TCP zaručuje spolehlivé doručení dat ve správném pořadí. Z toho vyplývá i to, kdy je vhodné jej použít: Vždy, když je důležité, aby se veškerá data dostala na druhý konec spojení, i za cenu opakovaného odeslání. Jako příklad lze uvést HTTP (web), FTP, SMTP/POP3/IMAP (e-mail) nebo SSH. Občas je použití TCP ovšem vysloveně nevhodné, a to především v real-time programech, jako jsou třeba hry, kde i když se pár paketů po cestě „ztratí“, tak to moc nevadí. Více informací o TCP je třeba na Wikipedii.
TCP komunikace v Qt probíhá přes QTcpSocket
. To je taková zajímavá třída, která je založená na QIODevice
, což mj. znamená, že s ní dá zacházet jako s I/O zařízením. Můžete zapisovat data metodou write()
a číst je metodou read()
nebo readAll()
. Když přijdou nová data, dozvíte se to díky signálu readyRead()
a když dojde k chybě, tak přijde řada na signál error()
. Pak jsou zde ještě signály specifické pro socket – když se socket připojí nebo odpojí, tak vyšle signál connected()
nebo disconnected()
.
Než tohle všechno ovšem může začít, nejdříve se musíme někam připojit – na nějaký TCP server, který si s námi bude povídat. To se dělá metodou connectToHost()
(nebo ekvivalentním slotem connectToHostImplementation()
), které zadáte IP adresu a port, na který se chcete připojit, a případně ještě specifikujete, zda chcete socket otevřít pro čtení i zápis (výchozí) či jinak.
Jako ukázku TCP síťování v Qt jsem naprogramoval TODO server-klient záležitost. Pro nezasvěcené: TODO jsou poznámky. Úkoly, které je třeba splnit. Tohle je program, který umožňuje uložit si poznámky na server a přistupovat k nim odkudkoliv. Server najdete v článku Konzolové programy v Qt 4 – 3 (TCP server).
Funguje to celé tak, jak by se dalo očekávat. Na serveru musí mít uživatel vytvořený účet, což se dělá editací konfiguračního souboru. Poté se klient může přihlásit se svým jménem a heslem (heslo není posláno jako čistý text, odesílá se jeho SHA1 hash) a prohlížet či upravovat svůj seznam položek, který je uložený na serveru.
Uživatel se nemůže připojit z více míst najednou. Pokud server odpoví, že daný účet je už přihlášen odjinud, dostanete na výběr, zda chcete přihlášení vynutit a „přebrat“ (odpojíte tak to druhé sezení), nebo to vzdát.
Vymyslel jsem si vlastní jednoduchý protokol, kterým spolu klient a server mezi sebou komunikují. Ukážu zde jeho syntaxi, podle které lze vcelku snadno napsat i vlastního klienta (například do konzole). Příkazy jsou v pořadí, jak za sebou obvykle následují v praxi.
Příkaz | Popis |
LOGIN | Server pošle tento příkaz (bez argumentů) hned jak zjistí, že se na něj připojujete. Klient na něj odpovídá stejným příkazem, přičemž jako argumenty (oddělené mezerou) předá přihlašovací jméno a SHA1 hash hesla. |
WRONGLOGIN | Server tímto říká, že se pokoušíte přihlásit se špatnými údaji. |
LOGGED | Tohle pošle server, když přihlášení proběhne úspěšně. Klient na to obvykle zareaguje příkazem LIST. |
ALREADYLOGGED | Server tímto říká, že jste již přihlášen. Můžete se buď odpojit, nebo poslat FORCELOGIN. |
FORCELOGIN | Vynucené přihlášení. Posílá jej klient a používá se stejně jako LOGIN. I stejně funguje, ale navíc odpojí případné přihlášení odjinud. |
LIST | Klient tímto požádá server o seznam položek. |
LISTEMPTY | Server tímto říká, že váš seznam položek je prázdný. |
LISTING | Pokud seznam položek není prázdný, server pošle tento příkaz a za ním seznam položek, které jsou oddělené znakem nového řádku, tedy \n . |
ADD | Klient pošle tento příkaz serveru, když chce přidat položku do seznamu. Text položky tvoří zbytek příkazu. |
LISTFULL | Server tímto říká, že váš seznam položek je plný (v mé implementaci může mít uživatel max. 1000 položek). |
ADDED | Server takto odpoví, pokud byla položka úspěšně uložena. Zbytek příkazu tvoří text položky. |
REMOVE | Klient pošle tento příkaz, když chce smazat položky ze seznamu. Jako argument je třeba předat seznam indexů položek (oddělený mezerami). Indexy začínají od 0 a maximum je 999. |
REMOVED | Server tímto říká, že příkaz REMOVE byl vyřízen. Za příkazem následuje seznam indexů položek (oddělený mezerami), které byly odstraněny. |
INVALIDCMD | Toto server pošle jako odpověď na příkaz, který je neplatný. Můj klient toto nezpracovává, takže v případě, že dojde na tuhle odpověď (nemělo by), vyskočí obecná chybová hláška o neznámém příkazu ze serveru. |
TOOMANYWRONGLOGINS | Server tímto říká, že z klientské IP adresy detekoval během poslední chvíle (výchozí = 60 sekund) příliš mnoho chybných pokusů o přihlášení (výchozí = 10). Můj klient toto nezpracovává, protože se přes něj těžko někdo bude pokoušet o bruteforce login. Ale i kdyby, dostane obecnou chybovou hlášku, jako v předchozím případě. |
To je vše. Když se chcete odpojit, použijte metodu disconnectFromHost()
nebo slot disconnectFromHostImplementation()
(obojí dělá totéž). Pokud pošlete serveru příkaz (jiný než přihlašovací), když nejste přihlášeni, odpojí vás bez další komunikace.
Co se týče techniky vyvíjení programu, navrhnul jsem si GUI v Designeru a tentokrát mě to asi definitivně přesvědčilo, že to je mnohem efektivnější metoda a že obvykle nemá valný význam navrhovat GUI ručně. K programu je tentokrát přibalený i překlad do češtiny. Všechno si to můžete stáhnout v archívu tcpclient.tar.bz2.
tcpclient.h
: API.
#ifndef MAINWINDOW_H #define MAINWINDOW_H #include <QMainWindow> #include <QAbstractSocket> #include <QByteArray> class MyTcpSocket; class QTcpSocket; class QLabel; namespace Ui { class MainWindow; } class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = 0); private: Ui::MainWindow *ui; QTcpSocket* socket; QLabel* statusLabel; void setStatus(const QString& statusmsg); void login(bool force = false); void socketWrite(QByteArray data); void lockGui(bool lock = true); void unlockGui(); private slots: void doConnect(); void gotConnected(); void gotDisconnected(); void gotError(QAbstractSocket::SocketError error); void handleReply(); void addItem(); void removeSelectedItems(); }; #endif // MAINWINDOW_H
tcpclient.cpp
:
#include "mainwindow.h" #include "ui_mainwindow.h" #include <QtGui> #include <QtNetwork> MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { // použijeme navržené GUI ui->setupUi(this); // vytvoříme socket socket = new QTcpSocket(this); // a připojíme jeho signály na sloty, které je zpracují connect(socket, SIGNAL(connected()), SLOT(gotConnected())); connect(socket, SIGNAL(disconnected()), SLOT(gotDisconnected())); connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), SLOT(gotError(QAbstractSocket::SocketError))); connect(socket, SIGNAL(readyRead()), SLOT(handleReply())); // vytvoříme popisek pro zprávy na stavovém řádku statusLabel = new QLabel(); statusBar()->addWidget(statusLabel); // přiřadíme funkce ovládacím prvkům programu: // tlačítko „Connect“ (Připojit) connect(ui->connBtn, SIGNAL(clicked()), SLOT(doConnect())); // tlačítko „Add“ (Přidat) connect(ui->addBtn, SIGNAL(clicked()), SLOT(addItem())); // vstupní pole pro TODO záznam connect(ui->todoEdit, SIGNAL(returnPressed()), SLOT(addItem())); // tlačítko „Remove…“ (Odstranit vybrané) connect(ui->rmBtn, SIGNAL(clicked()), SLOT(removeSelectedItems())); // položka menu „Connect“ (Připojit) connect(ui->actionConnect, SIGNAL(triggered()), SLOT(doConnect())); // položka menu „Disconnect“ (Odpojit) connect(ui->actionDisconnect, SIGNAL(triggered()), socket, SLOT(disconnectFromHostImplementation())); // položka menu „About Qt“ (O Qt) connect(ui->actionAbout_Qt, SIGNAL(triggered()), qApp, SLOT(aboutQt())); // viz níže this->lockGui(); resize(600, 500); } // zakáže relevantní prvky během čekání na odpověď od serveru void MainWindow::lockGui(bool lock) { ui->actionDisconnect->setDisabled(lock); ui->addBtn->setDisabled(lock); ui->rmBtn->setDisabled(lock); } // odemkne zmiňované prvky void MainWindow::unlockGui() { this->lockGui(false); } // zařizuje připojení k serveru void MainWindow::doConnect() { // zamkne tlačítko a položku v menu „Connect“ ui->connBtn->setDisabled(true); ui->actionConnect->setDisabled(true); // nastaví zprávu ve stavovém řádku this->setStatus(tr("Connecting…")); // uložíme si IP adresu a port do proměnných QString strAddr = ui->addressEdit->text(); int port = ui->portEdit->value(); QHostAddress addr; // zkontrolujeme platnost formátu IP adresy if(!addr.setAddress(strAddr)) { QMessageBox::critical(this, tr("Invalid IP address"), tr("You've entered an invalid IP address!")); ui->addressEdit->setFocus(); return; } // zahájíme připojování socket->connectToHost(addr, port); } // zapíše data do socketu void MainWindow::socketWrite(QByteArray data) { socket->write(data); // zajistí okamžitý zápis socket->flush(); } // nastaví zprávu ve stavovém řádku void MainWindow::setStatus(const QString& statusmsg) { QString status = QDate::currentDate().toString(Qt::SystemLocaleShortDate) + " " + QTime::currentTime().toString() + " – " + statusmsg; statusLabel->setText(status); } // provede se po úspěšném připojení void MainWindow::gotConnected() { this->setStatus(tr("Successfully connected!")); // odemkneme v menu akci pro odpojení a zamkneme tu pro připojení ui->actionDisconnect->setEnabled(true); // odemkneme ovládací prvky pro komunikaci se serverem this->unlockGui(); } // provede se po odpojení od serveru void MainWindow::gotDisconnected() { this->setStatus(tr("Disconnected!")); // vyprázdní seznam a patřičně odemkne/zamkne ovl. prvky ui->listWidget->clear(); this->lockGui(); ui->connBtn->setEnabled(true); ui->actionDisconnect->setDisabled(true); ui->actionConnect->setEnabled(true); } // zpracuje chybu od socketu void MainWindow::gotError(QAbstractSocket::SocketError error) { qDebug() << "gotError" << error; // informujeme program o odpojení this->gotDisconnected(); // chybu zobrazíme ve stavovém řádku i v chybovém dialogu this->setStatus(socket->errorString()); QMessageBox::critical(this, tr("Network error"), socket->errorString()); } // zpracuje odpověď či příkaz, který přišel serveru void MainWindow::handleReply() { // příchozí data QByteArray rawdata = socket->readAll(); // argumenty příkazu QList<QByteArray> data = rawdata.split(' '); // samotný příkaz QByteArray command = data.takeFirst(); qDebug() << "handling data:" << rawdata; if(command == "LOGIN") { // server vyžaduje přihlášení this->login(); } else if(command == "LOGGED") { // úspěšné přihlášení this->setStatus(tr("Listing the TODO items…")); // vyžádáme si výpis položek this->socketWrite("LIST"); } else if(command == "LISTEMPTY") { // server informuje, že seznam položek je prázdný this->setStatus(tr("Your TODO list is empty!")); } else if(command == "LISTFULL") { // server informuje, že seznam položek je plný this->unlockGui(); // zakážeme tlačítko pro přidávání položek ui->addBtn->setDisabled(true); this->setStatus(tr("Your TODO list is full!")); QMessageBox::critical(this, tr("Your TODO list is full!"), tr("Server says that your TODO list is full. " "You need to remove some items.")); } else if(command == "LISTING") { // server posílá seznam položek // uložíme si odpověď bez příkazu QString list = rawdata.right( rawdata.size()-command.size()-1 ); // řetězce jsou v odpovědi odděleny znakem \n, takže je převedeme na skutečný seznam ui->listWidget->addItems(list.split(QChar('\n'), QString::SkipEmptyParts)); } else if(command == "ADDED") { // server informuje o úspěšném přidání položky do seznamu this->unlockGui(); ui->todoEdit->clear(); this->setStatus(tr("Your TODO item has been successfully saved on the server")); QString item = rawdata.right( rawdata.size()-command.size()-1 ); ui->listWidget->addItem(item); } else if(command == "REMOVED") { // server informuje o úspěšném odstranění položek ze seznamu // seznam indexů odstraněných položek QList<QByteArray> indexes = rawdata.right( rawdata.size()-command.size()-1 ).split(' '); int i = indexes.size() – 1; // smaže položky i ze seznamu v GUI while(i >= 0) { const QByteArray* index = &indexes.at(i--); delete ui->listWidget->takeItem(index->toInt()); } this->unlockGui(); this->setStatus(tr("Selected items had been removed successfully")); } else if(command == "ALREADYLOGGED") { // server říká, že daný uživatel je již připojen // dialog s dotazem (vynutit přihlášení?) QMessageBox::StandardButton r = QMessageBox::warning(this, tr("User already logged in!"), tr("Your account is already logged in, probably " "from somewhere else. Do you want to force the login " "and disconnect the other client?"), QMessageBox::Yes|QMessageBox::No, QMessageBox::No); if(r == QMessageBox::Yes) this->login(true); else // pokud uživatel nechce vynutit připojení, tak se odpojíme socket->disconnectFromHost(); } else if(command == "WRONGLOGIN") { // server informuje o neúspěšném přihlášení na základě špatných údajů // na chvíli odpojíme informování o chybách – jde o (na první pohled úspěšný) // pokus o eliminaci dvou chybových dialogů na sobě socket->disconnect(SIGNAL(error(QAbstractSocket::SocketError))); // zobrazíme chybový dialog QMessageBox::critical(this, tr("Wrong login"), tr("You've entered a wrong username or password!") + "\n" + tr("Connection has been closed by the remote server.")); // a po zavření dialogu signál opět připojíme connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), SLOT(gotError(QAbstractSocket::SocketError))); } else { QMessageBox::critical(this, tr("Unknown command from the server"), tr("Server has sent an unknown command which could mean " "that your client version is outdated.")); } } // pošle na server požadavek o přihlášení void MainWindow::login(bool force) { // získáme z GUI uživatelské jméno QString user = ui->userEdit->text(); // a zkontrolujeme, jestli náhodou neobsahuje mezery if(user.contains(QChar(' '))) { this->gotDisconnected(); // mezery v uživatelském jménu nemáme rádi QMessageBox::critical(this, tr("Invalid username"), tr("The username cannot contain any whitespaces!")); return; } this->setStatus(tr("Logging in…")); // řetězec s požadavkem QByteArray request; // získáme z GUI heslo, ze kterého spočítáme SHA1 hash QString pass = QCryptographicHash::hash(ui->passEdit->text().toAscii(), QCryptographicHash::Sha1).toHex(); // sestrojíme požadavek ve formátu LOGIN uživatel heslo_v_sha1 request += QString("LOGIN %1 %2").arg(user).arg(pass); // pokud jde o vynucený login, použijeme příkaz FORCELOGIN místo LOGIN if(force) request.prepend("FORCE"); // a odešleme jej this->socketWrite(request); } // přidá položku do seznamu na server void MainWindow::addItem() { QByteArray request, tmp; // přidáme si do požadavku text položky request += ui->todoEdit->text(); // končíme, pokud je prázdný if(request.isEmpty()) return; // přidáme datum a čas, pokud jsou zaškrtlá odpovídající pole if(ui->withTime->isChecked() || ui->withDate->isChecked()) request.prepend("- "); if(ui->withTime->isChecked()) { tmp += QTime::currentTime().toString("HH:mm' '"); request.prepend(tmp); tmp.clear(); } if(ui->withDate->isChecked()) { tmp += QDate::currentDate().toString(Qt::SystemLocaleShortDate); request.prepend(tmp + " "); } // přidáme na začátek požadavku příkaz request.prepend("ADD "); // nastavíme status, zamkneme GUI this->setStatus("Adding the item…"); this->lockGui(); //qDebug() << request; // a odešleme požadavek this->socketWrite(request); } // odstraní vybrané položky ze serveru void MainWindow::removeSelectedItems() { // získáme seznam označených položek QList<QListWidgetItem*> items = ui->listWidget->selectedItems(); // pokud je seznam prázdný, tak o tom informujeme přes status a končíme if(items.isEmpty()) { this->setStatus(tr("No item(s) selected for removal!")); return; } // seřadíme indexy od nejmenšího po největší QList<int> rows; foreach(const QListWidgetItem* item, items) rows << ui->listWidget->row(item); qSort(rows); QByteArray request; // sestrojíme požadavek foreach(int row, rows) request += " " + QByteArray::number(row); request.prepend("REMOVE"); // nastavíme status a zamkneme GUI this->setStatus(tr("Removing selected items…")); this->lockGui(); //qDebug() << "remove request:" << request; // odešleme požadavek this->socketWrite(request); }
main.cpp
: Novinkou je zde nastavení kódování (UTF-8) pro céčkové řetězce. Díky tomu se na server korektně odešlou položky obsahující české znaky.
#include <QApplication> #include <QLocale> #include <QTranslator> #include <QTextCodec> #include "mainwindow.h" int main(int argc, char *argv[]) { QApplication a(argc, argv); a.setApplicationName("tcpclient"); { QTextCodec* unicode = QTextCodec::codecForName("UTF-8"); // nastavení kódování pro C řetězce QTextCodec::setCodecForCStrings(unicode); QTextCodec::setCodecForTr(unicode); } QTranslator tr; tr.load(a.applicationName() + "_" + QLocale::system().name()); a.installTranslator(&tr); MainWindow w; w.setWindowTitle("TODO client"); w.show(); return a.exec(); }
Nástroje: Tisk bez diskuse
Tiskni
Sdílej:
Na MainWindow::handleReply() by se celkem hodil ten stavovy automat z Qt 4.6 . Nebo rozumne RPC, to dlouho v Qt chybi. Kdysi existovala Qt CORBA, ta se snad uz nevyviji. Bez 3rd party knihoven (jako treba ZeroC Ice) je delani middlewaroidnych zalezitosti v Qt znacne neprijemny.
Pekné, a čo tak do ďaľšieho dielu hodit niečo realtime protokoloch.