C++ - Websocket Programming #2

 This time, we will implement a websocket client in a C++ program instead of a browser. There are various libraries that can implement WebSocket in C++ programs. In this article, I will introduce an example that can implement WebSocket with only one cpp file and one header file.


easywsclient

The websocket library I would recommend and use is https://github.com/dhbaird/easywsclient.

Download easywsclient.cpp and easywsclient.hpp files from the above site and save them in your project directory.  And this site provides websocket example files example-client-cpp11.cpp and example-client.cpp files.

And this site provides websocket example files example-client-cpp11.cpp and example-client.cpp files. I will modify these files slightly and connect to Node-RED websocket.


Build working C++ example on node-red

First, download the source code from github.

[spypiggy@localhost study]$ git clone https://github.com/dhbaird/easywsclient.git
Cloning into 'easywsclient'...
remote: Enumerating objects: 292, done.
remote: Counting objects: 100% (3/3), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 292 (delta 0), reused 2 (delta 0), pack-reused 289
Receiving objects: 100% (292/292), 94.27 KiB | 278.00 KiB/s, done.
Resolving deltas: 100% (170/170), done.
[spypiggy@localhost study]$ ll
total 0
drwxrwxr-x. 3 spypiggy spypiggy 122 Jul 16 00:22 aria
drwxrwxr-x. 2 spypiggy spypiggy  40 Jul 16 05:09 curl
drwxrwxr-x. 4 spypiggy spypiggy 247 Jul 31 04:56 easywsclient
drwxrwxr-x. 2 spypiggy spypiggy  69 Jul 15 08:45 json
drwxrwxr-x. 2 spypiggy spypiggy  51 Jul 12 08:31 mmap
drwxrwxr-x. 2 spypiggy spypiggy  63 Jul 12 07:52 pipe
drwxrwxr-x. 3 spypiggy spypiggy 201 Jul 30 09:09 stt
drwxrwxr-x. 2 spypiggy spypiggy  93 Jul 15 06:02 websocket
[spypiggy@localhost study]$ cd easywsclient/
[spypiggy@localhost easywsclient]$ ll
total 60
-rw-rw-r--. 1 spypiggy spypiggy  1092 Jul 31 04:56 COPYING
-rw-rw-r--. 1 spypiggy spypiggy 21300 Jul 31 04:56 easywsclient.cpp
-rw-rw-r--. 1 spypiggy spypiggy  2757 Jul 31 04:56 easywsclient.hpp
-rw-rw-r--. 1 spypiggy spypiggy   958 Jul 31 04:56 example-client.cpp
-rw-rw-r--. 1 spypiggy spypiggy  1207 Jul 31 04:56 example-client-cpp11.cpp
-rw-rw-r--. 1 spypiggy spypiggy   959 Jul 31 04:56 example-server.js
-rw-rw-r--. 1 spypiggy spypiggy   572 Jul 31 04:56 Makefile
-rw-rw-r--. 1 spypiggy spypiggy   449 Jul 31 04:56 package.json
-rw-rw-r--. 1 spypiggy spypiggy  4803 Jul 31 04:56 README.md
drwxrwxr-x. 2 spypiggy spypiggy    85 Jul 31 04:56 test

And let's modify the example-client.cpp file a bit. The source code is very simple and very easy to understand. For reference, this example was built on a Linux virtual machine and it is different from the host where Node-RED is running, so the address of the host where Node-RED is running must be entered correctly.

#include "easywsclient.hpp"
//#include "easywsclient.cpp" // <-- include only if you don't want compile separately
#ifdef _WIN32
#pragma comment( lib, "ws2_32" )
#include <WinSock2.h>
#endif
#include <assert.h>
#include <stdio.h>
#include <string>

using easywsclient::WebSocket;
static WebSocket::pointer ws = NULL;

void handle_message(const std::string & message)
{
    printf(">>> %s\n", message.c_str());
    if (message == "world") { ws->close(); }
}

int main()
{
#ifdef _WIN32
    INT rc;
    WSADATA wsaData;

    rc = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (rc) {
        printf("WSAStartup Failed.\n");
        return 1;
    }
#endif

    ws = WebSocket::from_url("ws://192.168.150.1:1880/ws/simple");
    assert(ws);
    ws->send("goodbye");
    ws->send("hello");
    while (ws->getReadyState() != WebSocket::CLOSED) {
      ws->poll();
      ws->dispatch(handle_message);
    }
    delete ws;
#ifdef _WIN32
    WSACleanup();
#endif
    return 0;
}

<modified exampleclient.cpp>

This source code specifies the server where Node-RED is running in the websocket address. And if the connection is successful, "goodbye" and "hello" are sent and the packet sent by the websocket server is output.

Build the program together with easywsclient.cpp. I'll try running it right away with the build.

[spypiggy@localhost easywsclient]$ g++ example-client.cpp easywsclient.cpp -o wstest
[spypiggy@localhost easywsclient]$ ./wstest
>>> Sun Jul 31 2022 21:04:08 GMT+0900 (Korean Standard Time)
>>> Sun Jul 31 2022 21:04:13 GMT+0900 (Korean Standard Time)
>>> Sun Jul 31 2022 21:04:18 GMT+0900 (Korean Standard Time)
>>> Sun Jul 31 2022 21:04:23 GMT+0900 (Korean Standard Time)

It can be seen that the packets sent every 5 seconds from the websocket server are properly outputted. Similarly, on the Noed-RED screen, it can also be confirmed that "goodbye" and "hello" sent by the client were properly received.

<Node-RED screen that receives websocket packets from c++ client>


I made a C++ example that can use websocket very easily.

easywsclient provides functions that can send not only string but also binary data. This time, we will create an example of sending binary data.


Send binary data from easywsclient

Install node-red-contrib-image-output which can output images to Node-RED. Information on this node and installation related information is available at https://flows.nodered.org/node/node-red-contrib-image-output.

In the c++ example, we will read the image file and send binary data using websocket. This node receives the contents of jpg and png files as msg.payload and displays the image contents. 

The example code is a bit long, but there is nothing difficult about it.

The following example code is a slight variation of the easywsclient original code. Therefore, an error occurs when using David Baird's easywsclient.


#include "easywsclient.hpp"
//#include "easywsclient.cpp" // <-- include only if you don't want compile separately
#ifdef _WIN32
#pragma comment( lib, "ws2_32" )
#include <WinSock2.h>
#endif
#include <assert.h>
#include <stdio.h>
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
using easywsclient::WebSocket;
static WebSocket::pointer ws = NULL;

void handle_message(const std::string & message)
{
    printf(">>> %s\n", message.c_str());
    if (message == "world") { ws->close(); }
}


uint8_t * read_image(const char *imagefile, int *fsize){
	struct stat st;
	stat(imagefile, &st);
	off_t size = st.st_size;
	*fsize = size;	
	std::cout << "Fiel size:" << size << std::endl;	
	
	FILE *fh = fopen(imagefile, "rb");
	if (NULL == fh) return NULL;

	uint8_t *buf = (uint8_t *)malloc(size);
	fread(buf, sizeof(uint8_t), size, fh);
	fclose(fh);
	return buf;
}

int main(int argc, char * argv[])
{
#ifdef _WIN32
    INT rc;
    WSADATA wsaData;

    rc = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (rc) {
        printf("WSAStartup Failed.\n");
        return 1;
    }
#endif
    if(argc < 2) {
        printf("enter image filename");
        return (0);
    }
    int fsize;
    uint8_t *buf = read_image(argv[1], &fsize);
    if(NULL == buf){
        return 0;
    }
    std::vector<uint8_t> buf_vec(buf, buf + fsize);
	
    ws = WebSocket::from_url("ws://192.168.150.1:1880/ws/simple");
    assert(ws);
    rc = ws->sendBinary(buf_vec);
    std::cout << "websocket send :" << rc << " bytes" << endl;

while (ws->getReadyState() != WebSocket::CLOSED) { ws->poll(); ws->dispatch(handle_message); } delete ws; free(buf); #ifdef _WIN32 WSACleanup(); #endif return 0; }

<websocket example that sends binary image data>

In general, uint8_t (unsigned char) is used for binary data transmission, but it is a bit unusual to use STL's std::vector<type> as a parameter to send binary data. Now build and run the above code.

[spypiggy@localhost easywsclient]$ g++ example-client2.cpp easywsclient.cpp -o wstest
[spypiggy@localhost easywsclient]$ ./wstest sample.jpg
Fiel size:417006
>>> Sun Jul 31 2022 21:48:52 GMT+0900 (Korean Standard Time)
>>> Sun Jul 31 2022 21:48:57 GMT+0900 (Korean Standard Time)
>>> Sun Jul 31 2022 21:49:02 GMT+0900 (Korean Standard Time)

In Node-RED, you can see that the image sent by the C++ program to the websocket is displayed properly.




easywsclient improvement

David Baird's easywsclient is a good code, but from my experience it needs some improvement.

  • Functions such as Send and SendBinary that send data are void types. It is recommended that these functions be returned as many bytes as sent.
  • The sending function send does not need to be asynchronous.
  • Finally, the close function does not close the socket. It only sends a packet to the server to terminate the websocket. Therefore, if you clear the websocet locally before shutting down on the server, the socket does not close. Therefore, if the socket is not closed in the destructor, it is recommended to close it.

The changed source code is available at https://github.com/raspberry-pi-maker/easywsclient.


Wrapping up

The easywsclient introduced in this article is made with very concise code and is very easy to use. If you need to implement a websocket client in a C++ program, we recommend using this code.

Finally, if you want to use it in an actual program, you can give the poll function a parameter in the example code above. The parameter is in ms and is returned if there is no packet to read within this time.

That is, implement it like this:

while (ws->getReadyState() != WebSocket::CLOSED) {
	ret = ws->poll(1);	//1ms timeout
	if(0 == ret){	// no data to read
	}
	else{	//read data
		ws->dispatch(handle_message);
	});                
	}	  
}

And if the dispatch callback implementation is awkward, you can handle reading the packet directly as follows.

while (ws->getReadyState() != WebSocket::CLOSED) {
        WebSocket::pointer wsp = &*(ws); // <-- because a unique_ptr cannot be copied into a lambda  
	ret = ws->poll(1);	//1ms timeout
	if(0 == ret){	// no data to read
	}
	else{	//read data
		ws->dispatch([wsp](void *p, const std::string & message) {
		printf(">>> %s\n", message.c_str());
		cout << (const char *)p <<endl;
	});                
	}	  
}

And if you need more, you can directly edit the easywsclient.cpp and easywsclient.hpp files and use them.

댓글

이 블로그의 인기 게시물

MQTT - C/C++ Client

RabbitMQ - C++ Client #1 : Installing C/C++ Libraries

C/C++ - Everything about time, date