Escrever e responder a mensagens do Pub/Sub

ID da região

O REGION_ID é um código abreviado que a Google atribui com base na região que seleciona quando cria a sua app. O código não corresponde a um país ou uma província, embora alguns IDs de regiões possam parecer semelhantes aos códigos de países e províncias usados frequentemente. Para apps criadas após fevereiro de 2020, REGION_ID.r está incluído nos URLs do App Engine. Para apps existentes criadas antes desta data, o ID da região é opcional no URL.

Saiba mais acerca dos IDs de regiões.

O Pub/Sub oferece mensagens assíncronas fiáveis de muitos para muitos entre aplicações. As aplicações de publicadores podem enviar mensagens para um tópico, e outras aplicações podem subscrever esse tópico para receber as mensagens.

Este documento descreve como usar a biblioteca cliente do Google Cloud para enviar e receber mensagens do Pub/Sub numa app Java 8.

Pré-requisitos

  • Siga as instruções em "Olá, mundo!" para Java 8 no App Engine para configurar o seu ambiente e projeto, e para compreender como as apps Java 8 do App Engine estão estruturadas.
  • Anote e guarde o ID do projeto, porque vai precisar dele para executar a aplicação de exemplo descrita neste documento.

Clonar a app de exemplo

Copie as apps de exemplo para a sua máquina local e navegue para o diretório pubsub:

git clone https://github.com/GoogleCloudPlatform/java-docs-samples
cd java-docs-samples/appengine-java8/pubsub

Criar um tópico e uma subscrição

Crie um tópico e uma subscrição, que inclui a especificação do ponto final para o qual o servidor Pub/Sub deve enviar pedidos:

 bv
# Configure the topic
gcloud pubsub topics create YOUR_TOPIC_NAME

# Configure the push subscription
gcloud pubsub subscriptions create YOUR_SUBSCRIPTION_NAME \
    --topic=YOUR_TOPIC_NAME \
    --push-endpoint=https://YOUR_PROJECT_ID.REGION_ID.r.appspot.com/push-handlers/receive_messages?token=YOUR_TOKEN \
    --ack-deadline=10

Substitua YOUR_TOKEN por um token aleatório secreto. O ponto final de envio usa-o para validar pedidos.

Para usar o Pub/Sub com autenticação, crie outra subscrição:

# Configure the push subscription
gcloud pubsub subscriptions create YOUR_SUBSCRIPTION_NAME \
    --topic=YOUR_TOPIC_NAME \
    --push-auth-service-account=YOUR-SERVICE-ACCOUNT-EMAIL\
    --push-auth-token-audience=OPTIONAL_AUDIENCE_OVERRIDE\
    --push-endpoint=https://YOUR_PROJECT_ID.REGION_ID.r.appspot.com/push-handlers/receive_messages?token=YOUR_TOKEN \
    --ack-deadline=10
# Your service agent
# `service-{PROJECT_NUMBER}@gcp-sa-pubsub.iam.gserviceaccount.com` needs to have the
# `iam.serviceAccountTokenCreator` role.
PUBSUB_SERVICE_ACCOUNT="service-${PROJECT_NUMBER}@gcp-sa-pubsub.iam.gserviceaccount.com"
gcloud projects add-iam-policy-binding ${PROJECT_ID} \
    --member="serviceAccount:${PUBSUB_SERVICE_ACCOUNT}"\
    --role='roles/iam.serviceAccountTokenCreator'

Substitua YOUR-SERVICE-ACCOUNT-EMAIL pelo email da sua conta de serviço.

Definir variáveis de ambiente

Edite o ficheiro appengine-web.xml para definir as variáveis de ambiente para o seu tópico e token de validação:

<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
  <threadsafe>true</threadsafe>
  <runtime>java8</runtime>

  <env-variables>
    <env-var name="PUBSUB_TOPIC" value="your-topic" />
    <env-var name="PUBSUB_VERIFICATION_TOKEN" value="your-verification-token" />
  </env-variables>
</appengine-web-app>

Revisão de código

A app de exemplo usa a biblioteca do cliente do Google Cloud.

A app de exemplo usa os valores que define no ficheiro appengine-web.xml para configurar as variáveis de ambiente. O controlador de pedidos push usa estes valores para confirmar que o pedido foi enviado pelo Pub/Sub e teve origem numa fonte fidedigna:

String pubsubVerificationToken = System.getenv("PUBSUB_VERIFICATION_TOKEN");

A app de exemplo mantém uma instância da base de dados do Cloud Datastore para armazenar mensagens. O servlet PubSubPush recebe mensagens enviadas por push e adiciona-as à instância da base de dados messageRepository:

@WebServlet(value = "/pubsub/push")
public class PubSubPush extends HttpServlet {

  @Override
  public void doPost(HttpServletRequest req, HttpServletResponse resp)
      throws IOException, ServletException {
    String pubsubVerificationToken = System.getenv("PUBSUB_VERIFICATION_TOKEN");
    // Do not process message if request token does not match pubsubVerificationToken
    if (req.getParameter("token").compareTo(pubsubVerificationToken) != 0) {
      resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
      return;
    }
    // parse message object from "message" field in the request body json
    // decode message data from base64
    Message message = getMessage(req);
    try {
      messageRepository.save(message);
      // 200, 201, 204, 102 status codes are interpreted as success by the Pub/Sub system
      resp.setStatus(102);
    } catch (Exception e) {
      resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
    }
  }

  private Message getMessage(HttpServletRequest request) throws IOException {
    String requestBody = request.getReader().lines().collect(Collectors.joining("\n"));
    JsonElement jsonRoot = JsonParser.parseString(requestBody).getAsJsonObject();
    String messageStr = jsonRoot.getAsJsonObject().get("message").toString();
    Message message = gson.fromJson(messageStr, Message.class);
    // decode from base64
    String decoded = decode(message.getData());
    message.setData(decoded);
    return message;
  }

  private String decode(String data) {
    return new String(Base64.getDecoder().decode(data));
  }

  private final Gson gson = new Gson();
  private MessageRepository messageRepository;

  PubSubPush(MessageRepository messageRepository) {
    this.messageRepository = messageRepository;
  }

  public PubSubPush() {
    this.messageRepository = MessageRepositoryImpl.getInstance();
  }
}

O servlet PubSubPublish interage com a app Web do App Engine para publicar novas mensagens e apresentar mensagens recebidas:

/*
 * Copyright 2017 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.appengine.pubsub;

import com.google.cloud.ServiceOptions;
import com.google.cloud.pubsub.v1.Publisher;
import com.google.protobuf.ByteString;
import com.google.pubsub.v1.ProjectTopicName;
import com.google.pubsub.v1.PubsubMessage;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.http.HttpStatus;

@WebServlet(name = "Publish with PubSub", value = "/pubsub/publish")
public class PubSubPublish extends HttpServlet {

  @Override
  public void doPost(HttpServletRequest req, HttpServletResponse resp)
      throws IOException, ServletException {
    Publisher publisher = this.publisher;
    try {
      String topicId = System.getenv("PUBSUB_TOPIC");
      // create a publisher on the topic
      if (publisher == null) {
        ProjectTopicName topicName =
            ProjectTopicName.newBuilder()
                .setProject(ServiceOptions.getDefaultProjectId())
                .setTopic(topicId)
                .build();
        publisher = Publisher.newBuilder(topicName).build();
      }
      // construct a pubsub message from the payload
      final String payload = req.getParameter("payload");
      PubsubMessage pubsubMessage =
          PubsubMessage.newBuilder().setData(ByteString.copyFromUtf8(payload)).build();

      publisher.publish(pubsubMessage);
      // redirect to home page
      resp.sendRedirect("/");
    } catch (Exception e) {
      resp.sendError(HttpStatus.SC_INTERNAL_SERVER_ERROR, e.getMessage());
    }
  }

  private Publisher publisher;

  public PubSubPublish() {}

  PubSubPublish(Publisher publisher) {
    this.publisher = publisher;
  }
}

Executar o exemplo localmente

Quando executado localmente, pode usar a CLI Google Cloud para fornecer autenticação para usar as APIs Google Cloud. Partindo do princípio de que configurou o seu ambiente conforme descrito em Pré-requisitos, já executou o comando gcloud init, que fornece esta autenticação.

mvn clean package

Em seguida, defina as variáveis de ambiente antes de iniciar a aplicação:

export PUBSUB_VERIFICATION_TOKEN=[your-verification-token]
export PUBSUB_TOPIC=[your-topic]
mvn appengine:run

Simular notificações push

A aplicação pode enviar mensagens localmente, mas não consegue receber mensagens push localmente. No entanto, pode simular uma mensagem push fazendo um pedido HTTP ao ponto final de notificação push local. A amostra inclui o ficheiro sample_message.json.

Pode usar curl ou um cliente httpie para enviar um pedido HTTP POST:

curl -H "Content-Type: application/json" -i --data @sample_message.json "localhost:8080/pubsub/push?token=[your-token]"

Ou

http POST ":8080/pubsub/push?token=[your-token]" < sample_message.json

Resposta:

HTTP/1.1 200 OK
Date: Wed, 26 Apr 2017 00:03:28 GMT
Content-Length: 0
Server: Jetty(9.3.8.v20160314)

Após a conclusão do pedido, pode atualizar localhost:8080 e ver a mensagem na lista de mensagens recebidas.

Em execução no App Engine

Para implementar a app de demonstração no App Engine através da ferramenta de linha de comandos gcloud, execute o seguinte comando a partir do diretório onde se encontra o ficheiro pom.xml:

mvn package appengine:deploy -Dapp.deploy.projectId=PROJECT_ID

Substitua PROJECT_ID pelo ID do seu Google Cloud projeto. Se o seu ficheiro pom.xmlespecificar o seu ID do projeto, não precisa de incluir a propriedade -Dapp.deploy.projectId no comando que executar.

Já pode aceder à aplicação em https://PROJECT_ID.REGION_ID.r.appspot.com. Pode usar o formulário para enviar mensagens, mas não há garantia de que a notificação seja recebida por uma instância específica da sua aplicação. Pode enviar várias mensagens e atualizar a página para ver a mensagem recebida.