Hello, i'm building a client-server application using sockets in C. I'm trying to send a structure through a socket, but the values after "deserialization" (the use of htonl-ntohl) come adulterated and i dont know why. Maybe you could help me? Here is the code:

Server.c

#include <stdlib.h>
#include <string.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h> // O_CREAT | O_TRUNC ...ETC
#include "db.h"
#include <pthread.h>
#include "query.h"


pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

//DBHANDLE database; 

#define MAXPENDING 5
#define RCVBUFSIZE 32


void HandleTcpClient(void * data)
{
	QUERY * qry = (QUERY *) data;
	
	int codigo_produto = ntohs(qry->codigo_produto);
	unsigned long preco = ntohl(qry->preco);
	int quantidade = ntohs(qry->quantidade);
	
	qry->codigo_produto = codigo_produto;
	qry->preco = preco;
	qry->quantidade = quantidade;
	
	//database = db_open("aux", O_CREAT | O_TRUNC | O_RDWR, 0666);

	printf("Codigo Produto: %d\n Preço: %u\n Quantidade: %d\n",qry->codigo_produto,qry->preco,qry->quantidade);
		
		//close((clntSock));
		free(qry);
		exit(0);

}

int main(int argc, char * argv[]) {

int servSock;						//id do socket do servidor -> retorno de socket()
int clntSock;						//id do socket do cliente
pthread_t thread1; 

struct sockaddr_in servAddr;	//dados relativos ao socket do servidor(ip,porto)
struct sockaddr_in clntAddr;	//dados relativos ao socket do cliente


unsigned short servPort;		
unsigned int clntLen;
int recvMsgSize;

	QUERY * query; //Estrutura a passar à função handletcpclient
	
	query = (QUERY *) malloc(sizeof(QUERY));
	
	memset(query,0,sizeof(QUERY));


if(argc !=2 )
{
	fprintf(stderr,"Usage: %s <Server Port>\n",argv[0]);
	exit(1);
	
}

	servPort = atoi(argv[1]);
	
	if((servSock = socket(AF_INET,SOCK_STREAM,0)) < 0) //Criação do socket do servidor
	{
			perror("Error with Socket()");
			exit(1);
	}
	
	/* Preenchimento dos dados relativos ao socket criado, para passar às funções bind e accept */
	
	/*Registar endereço local de modo a que os clientes nos possam contactar*/
	
	memset(&servAddr,0,sizeof(servAddr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servAddr.sin_port = htons(servPort);
	
	
	if(bind(servSock,(struct sockaddr*)&servAddr,sizeof(servAddr)) < 0)
	{
		perror("Error with bind()");
		exit(1);
	
	}
	
	/*Activar socket com fila de espera de dimensão MAXPENDING*/
	
	if(listen(servSock,MAXPENDING) < 0)
	{
		perror("Error with listen()");
		exit(1);
	
	}
	
	/*Ciclo de processamento (envio/recepção de mensagens)*/
	
		clntLen = sizeof(clntAddr);
		
		if((clntSock = accept(servSock,(struct sockaddr*)&clntAddr,&clntLen)) < 0)//bloqueante
		{
		perror("Error with accept()");
		exit(1);
	
		}
		
		if((recvMsgSize = recv((clntSock),query,sizeof(QUERY),0)) < 0)
	{
			perror("Error with recv()");
			exit(1);
	}
		
		
//		pthread_create(&thread1,0,&HandleTcpClient,(void *)clntSock);
//		pthread_join(thread1,0);

		HandleTcpClient(query);
		
		close(servSock);
		exit(0);
	
}

client.c

#include <stdlib.h>
#include <string.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include "query.h"

#define RCVBUFSIZE 32

int main(int argc, char * argv[])
 {
	
	int i;
	int sock;
	struct sockaddr_in servAddr;
	unsigned short servPort;
	char * servIP;
	QUERY * command;
	
	command = (QUERY *)malloc(sizeof(QUERY));
	memset(command,0,sizeof(command));
	
	
	if(argc!=8)
	{
		fprintf(stderr,"Usage: %s [<Server IP>] [<Port>] [<Key>][<Description>][<Price>] [<Quantity>][<Localization>] \n",argv[0]);
		exit(1);
	}
	
	servIP = argv[1];
	servPort = atoi(argv[2]);
	
	if((sock = socket(AF_INET,SOCK_STREAM,0)) < 0)//criação do socket do cliente
	{
		perror("Fim");
		exit(1);
	}
	
	/* Preenchimento dos dados relativos ao socket criado(do cliente) para passar à função connect -> Quando connect é executado, o servidor vai conferir os dados!*/
	
	memset(&servAddr,0,sizeof(servAddr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.s_addr = inet_addr(servIP);
	servAddr.sin_port = htons(servPort);
	
	printf("Chegou aqui 1\n");
	
	if(connect(sock,(struct sockaddr*)&servAddr,sizeof(servAddr)) < 0)
	{
		perror("Error with connect()");
		exit(1);
	}
	
	/*Ligação estabelecida...receber instruções */
	
	command->codigo_produto = htons(atoi(argv[3]));//chave
	command->descricao = argv[4];
	command->preco = htonl(atoi(argv[5]));
	command->quantidade = htons(atoi(argv[6]));
	command->localizacao = argv[7];
	//strcpy(command->localizacao,argv[7]);	
	
	if(send(sock, command, sizeof(command),0) != sizeof(command))
	{
		perror("[Client] Data not Sent\n");
		exit(1);
	
	}
	
	close(sock);
	exit(0);
	
	}

query.h

typedef struct {

	int codigo_produto;//chave
	
	char * descricao;
	unsigned int preco;
	int quantidade;
	char * localizacao;
	
}QUERY;

At the command line I call the client.exe like this:

./client.exe 127.0.0.1 8080 120 descript 1000 10 localiz

...and server.exe...

./server.exe 8080

When i execute this server-client application, the only value that manages to pass from client to server is codigo_produto. I dont understand why.

Sorry for this long post, but im a bit late on this and i need another perspective.

Cheers! :)

Quick question, how are you passing your char pointers? From what I can see your passing the pointer value and not the characters that make up the character array.
The pointer values from the client application have no valid meaning in your server application.

Quick question, how are you passing your char pointers?

The char pointers are passed on the command line too: argv[4] and argv[7]. The structure is filled in with information from the command line at the client and then sent to the server, where i print the information just to see if everything is well.

The char pointers are passed on the command line too: argv[4] and argv[7]. The structure is filled in with information from the command line at the client and then sent to the server, where i print the information just to see if everything is well.

I think what your doing is passing the pointer values from the client to server. This won't work, you have to pass the values of the character arrays argv[4] and argv[7]..

Well i know thats not the proper way to pass it, but my problem is with the 3 fields i'm trying to print:

printf("Codigo Produto: %d\n Preço: %u\n Quantidade: %d\n",qry->codigo_produto,qry->preco,qry->quantidade);

Only the first field prints properly. I'm not even trying to print the char pointers since i know im not passing them properly. Could this fact influence the byte stream and adulterate the numeric values im trying to print?

I tried to supress the char* from the structure (tried to send a structure with just int fields in it), and the problem persists. Any suggestions? What am i doing wrong, besides that char* thing?

You know that htons/ntohs is used on unsigned short not int.

What should i use for int then?

Well i just figured out that i shouldnt be sending the struct from client to server. What i should do is send the struct field by field, and assemble it on the server so i can pass it to the handletpclient function.

I'm more aqcuaited with programming in java. And in these cases, when you send several messages (that implement the Serializable interface), you can assemble all together at the other side with a "instanceof " test. In C, how can i know which part of the struct just arrived to the server, so we could assemble the struct correctly?

Maybe with a for loop? Each iteration with the arrival of one part? And on the side of the client, another for loop for sending?

Well i just figured out that i shouldnt be sending the struct from client to server. What i should do is send the struct field by field, and assemble it on the server so i can pass it to the handletpclient function.

I'm more aqcuaited with programming in java. And in these cases, when you send several messages (that implement the Serializable interface), you can assemble all together at the other side with a "instanceof " test. In C, how can i know which part of the struct just arrived to the server, so we could assemble the struct correctly?

Maybe with a for loop? Each iteration with the arrival of one part? And on the side of the client, another for loop for sending?

You really should pick up a good book on programming with network sockets..

I wrote this simple client/server example that passes an array of structures to the server...This is just a very simple example of passing data between the client and the server...its by no means a rigorous program

If you can find anything of use in it, please feel free to use it.

Usage:
run server ./server&

run client ./client 127.0.0.1

server.c

#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <strings.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>

struct mystr
{
	unsigned int one;
	unsigned int two;
	unsigned int three;
	unsigned int four;
};

#define DEFAULT_PROTOCOL 0
#define ARRSIZE 3
#define MAXLINE 7

int main(int argc, char**argv)
{
	int i = 0, n = 0, j = 0;
	struct mystr thestr[ARRSIZE];
	char *ch = (char*)&thestr;

	int listenfd, connfd, val = 1;
	struct sockaddr_in servaddr;

	signal(SIGCHLD, SIG_IGN);

	if ((listenfd = socket(AF_INET, SOCK_STREAM, DEFAULT_PROTOCOL)) < 0)
	{
		perror("socket");
		exit(EXIT_FAILURE);
	}

	setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val));

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(50000);
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

	if ((bind(listenfd, (const struct sockaddr*)&servaddr, sizeof(servaddr))) < 0)
	{
		perror("bind");
		exit(EXIT_FAILURE);
	}

	if ((listen(listenfd, 5)) < 0)
	{
		perror("listen");
		exit(EXIT_FAILURE);
	}

	for (;;)
	{
		connfd = accept(listenfd, NULL, NULL);

		if (fork())
		{
			close(connfd);
		}
		else
		{
			j = 0;
			while ((n = read(connfd, &ch[j], MAXLINE)) > 0)
			{
				j += n;		
			}
			for (i = 0; i < 3; ++i)
			{
				fprintf(stdout, "\none->%d, two->%d, three->%d, four->%d\n", thestr[i].one,\
						 thestr[i].two, thestr[i].three, thestr[i].four);
			}
			exit(EXIT_SUCCESS);
		}
	}
	exit(EXIT_SUCCESS);
}

client.c

#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <strings.h>
#include <unistd.h>
#include <errno.h>

struct mystr
{
	unsigned int one;
	unsigned int two;
	unsigned int three;
	unsigned int four;
};

#define DEFAULT_PROTOCOL 0
#define ARRSIZE 3

int main(int argc, char**argv)
{
	int i = 0;
	int clientfd;
	struct mystr thestr[ARRSIZE];
	struct sockaddr_in servaddr;

	for (i = 0; i < ARRSIZE; ++i)
	{
		fputs("enter the one value->", stdout);
		fscanf(stdin, "%u", &thestr[i].one); 

		fputs("enter the two value->", stdout);
		fscanf(stdin, "%u", &thestr[i].two); 

		fputs("enter the three value->", stdout);
		fscanf(stdin, "%u", &thestr[i].three); 

		fputs("enter the four value->", stdout);
		fscanf(stdin, "%u", &thestr[i].four); 
	}

	if (argc != 2)
	{
		fputs("usage error - a.out <IPaddress>\n", stderr);
		exit(EXIT_FAILURE);
	}

	if ((clientfd = socket(AF_INET, SOCK_STREAM, DEFAULT_PROTOCOL)) < 0)
	{
		perror("socket");
		exit(EXIT_FAILURE);
	}

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(50000);

	if ((inet_pton(AF_INET, argv[1], &servaddr.sin_addr.s_addr)) <= 0)
	{
		fputs("conversion error!\n", stderr);
		exit(EXIT_FAILURE);
	}

	if ((connect(clientfd, (const struct sockaddr*)&servaddr, sizeof(servaddr))) < 0)
	{
		perror("connect");
		exit(EXIT_FAILURE);
	}

	write(clientfd, (char*)&thestr, (ARRSIZE * sizeof(struct mystr)));

	close(clientfd);
	exit(EXIT_SUCCESS);
}

Well for unsigned int fields is very easy and it doesnt give you segmentation fault errors. Yeah i know im dumb and i should be reading books with very simple examples, but happens that i need to do something with a bit more difficulty. I try to pass structures with just one char * field and that segmentation fault error keeps popping.

I'm running out, try to find help elsewhere. Thanks ;)

Well for unsigned int fields is very easy and it doesnt give you segmentation fault errors. Yeah i know im dumb and i should be reading books with very simple examples, but happens that i need to do something with a bit more difficulty. I try to pass structures with just one char * field and that segmentation fault error keeps popping.

I'm running out, try to find help elsewhere. Thanks ;)

Yeah i know im dumb -

No your not dumb but you need to write your programs starting with basic functionality and work up from there.

Well with all the respect, but i've done that...and when i try to go to the next level increasing the complexity, there come some errors i simply cant understand and for all the books i read i cant find an answer.

Like i said earlier, im more experienced with java, and its quite hard for me to switch so radically in programming paradigms you know? 4 days ago i was doing a similar assignment in java and it was so easy...

Anyhow, i used your idea of casting the struct into a char* and then send it through the socket, but the result is always the same. If the struct's fields are all ints, it works fine...but when they're char* it doesnt work. I'll keep researching and coding...when i find a solution i'll post it.

Thanks for the attention;)

When you pass a char pointer in a client server application you have to consider a few things

1. how do inform the recipient the size of the char pointer.
2. I have to allocate the memory for the char pointer in the recipient application.
3. copy the data from the copy buffer into the recipient's allocated memory

If your having this many problems with C why don't you write the application in Java?

Here's the example I gave you but expanded to include character arrays...the next step is to change the character arrays to pointers and handle the extra requirements of communicating the pointer size and data.

again I must state both these programs are by no means rigorous...

server.c

#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <strings.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>

struct mystr
{
	unsigned int one;
	unsigned int two;
	char ch1[11];
	unsigned int three;
	unsigned int four;
	char ch2[11];
};

#define DEFAULT_PROTOCOL 0
#define ARRSIZE 3
#define MAXLINE 7

int main(int argc, char**argv)
{
	int i = 0, n = 0, j = 0;
	struct mystr thestr[ARRSIZE];
	char *ch = (char*)&thestr;

	int listenfd, connfd, val = 1;
	struct sockaddr_in servaddr;

	signal(SIGCHLD, SIG_IGN);

	if ((listenfd = socket(AF_INET, SOCK_STREAM, DEFAULT_PROTOCOL)) < 0)
	{
		perror("socket");
		exit(EXIT_FAILURE);
	}

	setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val));

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(50000);
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

	if ((bind(listenfd, (const struct sockaddr*)&servaddr, sizeof(servaddr))) < 0)
	{
		perror("bind");
		exit(EXIT_FAILURE);
	}

	if ((listen(listenfd, 5)) < 0)
	{
		perror("listen");
		exit(EXIT_FAILURE);
	}

	for (;;)
	{
		connfd = accept(listenfd, NULL, NULL);

		if (fork())
		{
			close(connfd);
		}
		else
		{
			j = 0;
			while ((n = read(connfd, &ch[j], MAXLINE)) > 0)
			{
				j += n;		
			}
			for (i = 0; i < 3; ++i)
			{
				fprintf(stdout, "\none->%d, two->%d, word->%s, three->%d, four->%d, word->%s\n",\
 				thestr[i].one,thestr[i].two, thestr[i].ch1, thestr[i].three, thestr[i].four, \
						thestr[i].ch2);
			}
			exit(EXIT_SUCCESS);
		}
	}
	exit(EXIT_SUCCESS);
}

client.c

#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <strings.h>
#include <unistd.h>
#include <errno.h>

struct mystr
{
	unsigned int one;
	unsigned int two;
	char ch1[11];	
	unsigned int three;
	unsigned int four;
	char ch2[11];
};

#define DEFAULT_PROTOCOL 0
#define ARRSIZE 3

int main(int argc, char**argv)
{
	int i = 0;
	int clientfd;
	struct mystr thestr[ARRSIZE];
	struct sockaddr_in servaddr;

	for (i = 0; i < ARRSIZE; ++i)
	{
		fputs("enter the one value->", stdout);
		fscanf(stdin, "%u", &thestr[i].one); 

		fputs("enter the two value->", stdout);
		fscanf(stdin, "%u", &thestr[i].two); 

		fputs("enter a 10 character word->", stdout);
		fgetc(stdin);
		fgets(thestr[i].ch1, 10, stdin);

		fputs("enter the three value->", stdout);
		fscanf(stdin, "%u", &thestr[i].three); 

		fputs("enter the four value->", stdout);
		fscanf(stdin, "%u", &thestr[i].four); 

		fputs("enter a 10 character word->", stdout);
		fgetc(stdin);
		fgets(thestr[i].ch2, 10, stdin);
	}

	if (argc != 2)
	{
		fputs("usage error - a.out <IPaddress>\n", stderr);
		exit(EXIT_FAILURE);
	}

	if ((clientfd = socket(AF_INET, SOCK_STREAM, DEFAULT_PROTOCOL)) < 0)
	{
		perror("socket");
		exit(EXIT_FAILURE);
	}

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(50000);

	if ((inet_pton(AF_INET, argv[1], &servaddr.sin_addr.s_addr)) <= 0)
	{
		fputs("conversion error!\n", stderr);
		exit(EXIT_FAILURE);
	}

	if ((connect(clientfd, (const struct sockaddr*)&servaddr, sizeof(servaddr))) < 0)
	{
		perror("connect");
		exit(EXIT_FAILURE);
	}

	write(clientfd, (char*)&thestr, (ARRSIZE * sizeof(struct mystr)));

	close(clientfd);
	exit(EXIT_SUCCESS);
}

This is a simple example of passing a char pointer....again I must state both these programs are by no means rigorous...

usage:

./server&

./client 127.0.0.1

enter the first integer-> 12345
enter the second integer-> 895623
enter a string->this is a test string to pass along to the server

client.c

#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <strings.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

void createstring(char **str, char *s)
{
	int i = 0;
	int len = strlen(s);

	*str = (char*)malloc(len * sizeof(char));

	for (i = 0; i < len; ++i)
	{
		(*str)[i] = s[i];
	}
	(*str)[len] = '\0';
}

struct mystr
{
	unsigned int one;
	unsigned int two;
	char *ch;
};

#define DEFAULT_PROTOCOL 0
#define STRBUF 200

int main(int argc, char**argv)
{
	char ch[STRBUF] ;
	struct mystr thestr;
	int clientfd, len = 0;
	struct sockaddr_in servaddr;

	if (argc != 2)
	{
		fputs("usage error - a.out <IPaddress>\n", stderr);
		exit(EXIT_FAILURE);
	}

	if ((clientfd = socket(AF_INET, SOCK_STREAM, DEFAULT_PROTOCOL)) < 0)
	{
		perror("socket");
		exit(EXIT_FAILURE);
	}

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(50000);

	if ((inet_pton(AF_INET, argv[1], &servaddr.sin_addr.s_addr)) <= 0)
	{
		fputs("conversion error!\n", stderr);
		exit(EXIT_FAILURE);
	}

	if ((connect(clientfd, (const struct sockaddr*)&servaddr, sizeof(servaddr))) < 0)
	{
		perror("connect");
		exit(EXIT_FAILURE);
	}

	fputs("enter the first integer->", stdout);
	fscanf(stdin, "%u", &thestr.one);

	fputs("enter the second integer->", stdout);
	fscanf(stdin, "%u", &thestr.two);

	fputs("enter a string->", stdout);
	fgetc(stdin);
	fgets(ch, 199, stdin);

	createstring(&thestr.ch, ch);

	len = strlen(thestr.ch);

	/*fprintf(stdout, "ans->%u\n", thestr.one);
	fprintf(stdout, "ans->%u\n", thestr.two);
	fprintf(stdout, "str->%s\n", thestr.ch);
	fprintf(stdout, "str->%u\n", len);*/

	write(clientfd, (char*)&thestr, (2 * sizeof(unsigned int)));
	write(clientfd, (char*)&len, sizeof(unsigned int));
	write(clientfd, thestr.ch, strlen(thestr.ch));

	close(clientfd);

	free(thestr.ch);
	exit(EXIT_SUCCESS);
}

server.c

#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <strings.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>

struct mystr
{
	unsigned int one;
	unsigned int two;
	char *ch;
};

#define DEFAULT_PROTOCOL 0
#define MAXLINE 4096

int main(int argc, char**argv)
{
	struct mystr thestr;
	int len = 0;
	int listenfd, connfd, val = 1, n = 0, j = 0, i = 0;
	char recvline[MAXLINE + 1];
	struct sockaddr_in servaddr;

	signal(SIGCHLD, SIG_IGN);

	if ((listenfd = socket(AF_INET, SOCK_STREAM, DEFAULT_PROTOCOL)) < 0)
	{
		perror("socket");
		exit(EXIT_FAILURE);
	}

	setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val));

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(50000);
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

	if ((bind(listenfd, (const struct sockaddr*)&servaddr, sizeof(servaddr))) < 0)
	{
		perror("bind");
		exit(EXIT_FAILURE);
	}

	if ((listen(listenfd, 5)) < 0)
	{
		perror("listen");
		exit(EXIT_FAILURE);
	}

	for (;;)
	{
		connfd = accept(listenfd, NULL, NULL);

		if (fork())
		{
			close(connfd);
		}
		else
		{
			close(listenfd);
			while ((n  = read(connfd, &recvline[j], MAXLINE)) > 0)
			{
				j += n;
			}
			thestr = *(struct mystr*)&recvline[0];
			len = *(unsigned int*)&recvline[2 * sizeof(unsigned int)];

			fprintf(stdout, "ans->%u\n", thestr.one);
			fprintf(stdout, "ans->%u\n", thestr.two);
			/*fprintf(stdout, "ans->%u\n", len);*/
	
			thestr.ch = (char*)malloc(len * sizeof(char));

			for (i = 0; i < len; ++i)
			{
				thestr.ch[i] = *(char*)&recvline[3 * sizeof(unsigned int) + i]; 
			}
			thestr.ch[len] = '\0';

			fputs(thestr.ch, stdout);

			close(connfd);
			free(thestr.ch);
			exit(EXIT_SUCCESS);
		}
	}
	exit(EXIT_SUCCESS);
}
Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.