O servidor consistirá em n threads para realizar o controle das conexões, mensagens e chamadas de funções executadas no servidor. Um conjunto de n encadeamentos estará sempre pronto para executar/atender solicitações de entrada.
O encadeamento das filas estará escutando continuamente a porta "p" para solicitações de entrada. Assim que uma nova solicitação chegar ao servidor master_server, ela será inserida na fila.
As políticas disponíveis são First Come First Serve (FCFS) e Shortest Job First (SJF).
Para garantir e proteger a fila e outras estruturas de dados compartilhadas em vários encadeamentos para evitar condições de corrida, implementamos bloqueios por mutex.
O modo padrão para programas de socket é Blocking, porém utilizamos o metodo Nonblocking. Isso significa que para chamadas FCNTL() ou IOCTL(), o programa de chamada continuará, mesmo que a chamada de E/S possa não ter sido concluída. Se a chamada de E/S não puder ser concluída, ela retornará com ERRNO EWOULDBLOCK. Para testarmos a conclusão de qualquer chamada de soquete, utlizamos a função SELECT() que retorna EWOULDBLOCK.
Command:
> gcc -c master_server.c functions.c actions.c color.h && gcc master_server.c functions.c actions.c color.h -std=c99 -lpthread -Wall -o master_server && ./master_server [port]
Example:
> gcc -c master_server.c functions.c actions.c color.h && gcc master_server.c functions.c actions.c color.h -std=c99 -lpthread -Wall -o master_server && ./master_server 8000
Command:
gcc -c client.c color.h && gcc client.c color.h -std=c99 -Wall -o client && ./client [hostname] [server_ip] [server_port]
Example:
gcc -c client.c color.h && gcc client.c color.h -std=c99 -Wall -o client && ./client $('hostname') 192.168.10.2 8000
- void chatloop(char *hostname, int socketFd);
- void buildMessage(char *result, char *hostname, char *msg);
- void setupAndConnect(struct sockaddr_in *serverAddr, struct hostent *host, int socketFd, long port);
- void setNonBlock(int fd);
- void interruptHandler(int sig);
Declaração:
- void setupAndConnect(struct [address serveraddr], struct [hostname], int [socket descriptor], long [port]);
Definição:
- Utilizamos para realizar o setup da conexão.
Funcionamento:
- Utilizamos memset para preencher a memória com valores 0.
- Configuramos as variáveis de conexão para serverAddr.
- O cliente tenta realizar uma conexão com o servidor, sendo que um retorno da função connect() < 0 significa que houve um erro ao tentar conectar.
Declaração:
- void chatloop(char *hostname, int socketFd);
Definição:
- Utilizamos para receber a entrada do cliente e mostrar a saída.
Funcionamento:
- Instanciamos um struct que recebe um fd (clientFds) válido, e três arrays do tipo char para o buffer.
- O array fullMsg contém todas as informações geradas pela função buildMessage.
- O array chatBuffer recebe a entrada do cliente, já o msgBuffer é para receber a resposta do servidor.
- Iniciamos o loop e redefinimos o conjunto de fd cada vez que select () o modifica.
- Esperamos por um fd válido utilizando a função select() != -1
Declaração:
- void setNonBlock(int [file_descriptor]);
Definição:
- Utilizado para definirmos as chamadas sockets para NONBLOCK
Funcionamento:
- Utilizamos a função fcntl(fd, F_GETFL) com o parâmetro F_GETFL que retorna as flags do fd correspondente.
- Atribuimos o retorno a variável 'flags' e verificamos se há alguma flag atribuída.
- Se haver alguma flag atribuída atribuimos a flag o modo nonblock pelo parâmetro O_NONBLOCK
- Concluímos utilizando a função fcntl(fd, F_SETFL, flags) para definir a flag do fd
Declaração:
- void interruptHandler(int sig_unused)
Definição:
- Utilizado para notificar o servidor quando o cliente executa o comando /exit
Funcionamento:
- No bloco de execução da função chatLoop, é identificado o comando /exit
- A função interruptHandler é chamada com o parâmetro -1
- Utilizamos a função close() passando o file_descriptor do socket do cliente
- A função exit(1) termina o programa
- void startConn(int socketFd);
- void bindSocket(struct sockaddr_in *serverAddr, int socketFd, long port);
- void removeClient(connDataVars *data, int clientSocketFd);
- void *newClientHandler(void *data);
- void *clientHandler(void *chv);
- void *messageHandler(void *data);
- void queueDestroy(queue *q);
- queue* queueInit(void);
- void queuePush(queue q, char msg);
- char* queuePop(queue *q);
- void buildMessage(char *fullMsg, char *msgBuffer, int clientSocketFd);
- int splitBuffer(char *fullMsg);
Declaração:
- void startConn(int socketFd);
Definição:
- Quando o cliente solicita uma conexão, esta função à configura e cria uma threads para controlar a conexão e outra para a troca de menssagens.
Funcionamento:
- Definimos as variáveis de conexão.
- data.queue recebe o ptr da função queueInit() na qual tem por função iniciar a fila e alocar espaço para ela.
- Para data.clientListMutex, alocamos um espaço da memória com malloc() e definimos o tipo pthread_mutex_t.
- Logo, iniciamos o mutex com pthread_mutex_init(data.clientListMutex, NULL), o valor NULL utilizado significa que utilizaremos os atributos padrão do mutex.
- Criamos uma thread para controlar a conexão do cliente e passamos data* como parâmetro que contém as informações da conexão.
- Com FD_ZERO() definimos '0' nos bits do file_descriptor.
- Com FD_SET() definimos o file_descriptor.
- Criamos uma thread para controlar a troca de mensagens entre os hosts.
- Com pthread_join aguardamos até a thread de destino seja encerrada.
- Limpamos a fila com queueDestroy().
- Utilizando pthread_mutex_destroy(data.clientListMutex) destruimos o objeto mutex.
- Após destruirmos o mutex, desalocamos o espaço de memória utilizado pela função destruída.
Declaração:
- queue* queueInit(void);
Definição:
- Esta função inicia e aloca espaço para a fila de mensagens.
Funcionamento:
- Alocamos espaço na memória com malloc().
- Inicializamos as variáveis da struct.
- Alocamos espaço na memória com malloc() para q->mutex, q->notFull e q->notEmpty, Logo, iniciamos as variáveis de condição utilizando pthread_cond_init().
- Retornamos o ponteiro da fila que é utilizado por startConn, pela variável data.queue.
Declaração:
- void queueDestroy(queue *q);
Definição:
- Esta função limpa a fila.
Funcionamento:
- Utilizamos pthread_mutex_destroy() em q->mutex, q->notFull e q->notEmpty para limpar o conteúdo da fila.
- Ainda utilizamos free() para desalocar espaço na memória para q->mutex, q->notFull e q->notEmpty, inclusive para a fila representada pelo ponteiro *q.
Declaração:
- void queuePush(queue q, char msg);
Definição:
- Empurra a mensagem para o final da fila.
Funcionamento:
- Com q->buffer[q->tail], acessamos o final da fila e empurramos a menssagem para este espaço.
- q->tail que é a variável que representa o valor do final da fila é incrementada.
- Verificamos se q->tail == MAX_BUFFER, isso significa que o espaço alocado para a fila é igual ao buffer e a fila está cheia.
- Verificamos se q->tail == q->head, se for significa que a fila está cheia.
- q->empty recebe o valor 0, que significa que algo foi alocado na fila.
Declaração:
- char* queuePop(queue *q);
Definição:
- Tiramos a mensagem do topo da fila.
Funcionamento:
- Com q->buffer[q->tail], acessamos o final da fila e empurramos a menssagem para este espaço.
- q->head que é a variável que representa o valor do final da fila é incrementada.
- Verificamos se q->head == MAX_BUFFER, isso significa que a fila já foi toda percorrida, então q->head recebe 0 e volta para o início da fila.
- Verificamos se q->head == q->head, se for significa que a fila está vazia, então q->empty recebe valor 0.
- q->full recebe o valor 0, que significa que algo foi retirado da fila e ela não está mais cheia.
Declaração:
- void bindSocket(struct sockaddr_in *serverAddr, int socketFd, long port);
Definição:
- Associamos o socket do servidor com seu endereço local para que o servidor se 'ligue', para que os clientes possam usar esse endereço para se conectar ao servidor.
Funcionamento:
- Utilizando memset zeramos o endereço de serverAddr.
- Configuramos AF_INET para podermos realizar conexões pela internet.
- A porta padrão do servidor está definida para 8000, mas ao inicializarmos o servidor podemos passar por parâmetro outra porta.
Declaração:
- void removeClient(connDataVars *data, int clientSocketFd);
Definição:
- Remove o soquete da lista de soquetes ativos do cliente e o fecha.
Funcionamento:
- Utilizando pthread_mutex_lock(data->clientListMutex) na lista de clientes para manipularmos esta fila.
- Percorremos a lista para achar o socket do cliente que será encerrado.
- Utilizamos a função close() para fechar a conexão.
- Decrementamos data->numClients, que representa o número de clientes conectados.
Declaração:
- void *newClientHandler(void *data);
Definição:
- Thread para lidar com novas conexões. Adiciona o fd do cliente à lista de fds de clientes e gera uma nova thread clientHandler para ele.
Funcionamento:
- Criamos um loop infinito para esta função utilizando while(1).
- clientSocketFd recebe o retorno do fd do cliente da função accept() que é utilizada para criar um socket para o cliente que solicita um connect().
- Bloqueamos a lista de clientes para adicionar um novo cliente, e adiciona este cliente a lista se o número máximo de clientes não for excedido.
- Utilizando FD_ISSET negado, esperamos que ele retorne 1 se não haver nenhum file_descriptor definido no conjunto de file_descriptor apontado por fd_set conforme função FD_ISSET(fd , &fdset).
- Incluimos clientSocketFd ao conjunto de file_descriptor apontado por &(chatData->serverReadFds).
- Criamos uma nova thread para lidar com as mensagens do cliente, definida na função clientHandler().
- Se a thread for criada incrementamos (chatData->numClients) o número de clientes, senão fechamos o socked utilizando close().
- Por fim liberamos o mutex da lista de clientes com pthread_mutex_unlock().
Declaração:
- void *clientHandler(void *chv);
Definição:
- O "produtor" - ouve as mensagens do cliente para adicionar à fila de mensagens.
Funcionamento:
- Definimos as variáveis de buffer msgBuffer e fullMsg.
- msgBuffer[] é o buffer no qual conteúdo é uma mensagem recebida do servidor.
- fullMsg[] é o buffer no qual contem todas as informações [clientSocketFd:hostname:message].
- Criamos um loop infinito para a função.
- Se o cliente digitar /exit removemos ele da lista de clientes e fechamos o socket.
- Senão aguardamos a fila não estar cheia antes de enviar mensagem, e comparamos com pthread_cond_wait() para verificar a situação do mutex aplicado à fila.
- Obtemos o bloqueio da fila com pthread_mutex_lock().
- Construimos o buffer fullMsg chamando buildMessage(fullMsg, msgBuffer, clientSocketFd).
- Enviamos a mensagem para a fila chamando queuePush(q, fullMsg).
- Desbloqueamos a fila utilizando pthread_mutex_unlock(), e definimos a variável de condição com pthread_cond_signal(q->notEmpty).
- Essa função é usadas para desbloquear encadeamentos bloqueados em uma variável de condição.
Declaração:
- void *messageHandler(void *data);
Definição:
- O "consumidor" - espera que a fila tenha mensagens, então as retira e transmite para os clientes.
Funcionamento:
- Definimos as variáveis para manipular.
- Iniciamos um loop infinito para a função.
- Obtemos o bloqueio da utilizando filapthread_mutex_lock().
- Aguardamos ela não estar vazia utilizando pthread_cond_wait(q->notEmpty, q->mutex).
- Chamamos queuePop(q) para retirar a mensagem da fila.
- Desbloqueamos a fila utilizando pthread_mutex_unlock().
- Definimos a variável de condição utilizando pthread_cond_signal(q->notFull).
- Essa função é usadas para desbloquear encadeamentos bloqueados em uma variável de condição.
- Preparamos a mensagem para enviar para o cliente utilizando a função splitBuffer(fullMsg), no qual separa as informações contidas no buffer utilizando delimitadores com a função strsep().
- Após isso enviamos a mensagem chamando a função write().