[RUBY] Entraînez-vous à créer une application de chat simple avec Docker + Sinatra

J'oublie souvent comment utiliser Ruby, j'ai donc créé une application web simple en Ruby pour la rééducation. Aussi, j'écrirai comment le faire au cas où je l'oublierais.

C'est une putain d'application de chat appelée Nopochat. C'est un chat qui fait date que vous pouvez envoyer du contenu difficile à parler tout en maintenant la sécurité psychologique en ajoutant une jolie fin de "-mo" au message.

image.png

Le contenu présenté dans cet article est destiné uniquement à la pratique d'écriture de programmes et n'est pas destiné à être utilisé dans un environnement de production.

environnement

Utilisez Docker. Aucune installation de Ruby requise.

Lancement de Sinatra

Tout d'abord, coupez un répertoire approprié et démarrez le développement.

$ mkdir sinatra-chat && cd $_

Si vous souhaitez gérer votre projet avec Git, c'est une bonne idée de récupérer et de définir GitHub gitignore.

$ git init
$ curl https://raw.githubusercontent.com/github/gitignore/master/Ruby.gitignore -o .gitignore

Obtenez la version de l'image Ruby que vous souhaitez utiliser sur https://hub.docker.com/_/ruby. Cette fois, j'utiliserai «2.7-slim».

Commencez par initialiser Gemfile.

$ docker run --rm --volume $(pwd):/app --workdir /app ruby:2.7-slim bundle init
$ docker run --rm --volume $(pwd):/app --workdir /app ruby:2.7-slim bundle add sinatra

Après avoir confirmé que Gemfile et Gemfile.lock ont été générés, écrivez Dockerfile.

Dockerfile


FROM ruby:2.7-slim
WORKDIR /app

COPY Gemfile ./
COPY Gemfile.lock ./
RUN bundle config --local set path 'vendor/bundle'
RUN bundle install

CMD bundle exec ruby index.rb

docker-compose.yml


version: '3'
services:
  app:
    build: .
    volumes:
      - .:/app
      - /app/vendor/bundle
    ports:
      - 127.0.0.1:4567:4567

Créez ʻindex.rb`, qui est le corps principal de l'application.

index.rb


require 'sinatra'

configure do
  set :bind, '0.0.0.0'
end

get '/' do
  'Hello Sinatra!'
end

Allez sur http: // localhost: 4567 / et si vous voyez "Hello Sinatra!", Vous avez réussi.

Activer le rechargement par rechargement du navigateur

Dans l'état actuel des choses, même si vous modifiez le fichier, il ne sera reflété que si vous redémarrez le serveur Sinatra. Il est utile d'activer le rechargement lors du rechargement avant de commencer le développement.

$ docker-compose run --rm app bundle add sinatra-contrib

index.rb


 require 'sinatra'
+require 'sinatra/reloader' if settings.development?

Développement de la fonction de chat

Puisque le contenu du chat sera conservé plus tard, nous le stockerons dans une variable de classe appelée @@ chats pour le moment.

index.rb


get '/' do
  @@chats ||= []
  erb :index, locals: {
    chats: @@chats.map{ |chat| add_suffix(chat) }.reverse
  }
end

post '/' do
  @@chats ||= []
  @@chats.push({ content: params['content'], time: Time.now } )
  redirect back
end

def add_suffix(chat)
  { **chat, content: "#{chat[:content]}Aussi" }
end

Utilisez erb pour le modèle HTML. Si vous coupez le répertoire views / et y stockez ʻindex.erb, vous pouvez l'appeler avec ʻerb: index.

views/index.erb


<form action="/" method="post">
  <input name="content" placeholder="Publier" />
  <button type="submit">Envoyer</button>
</form>

<table>
  <% chats.each do |chat| %>
    <tr>
      <td><%= chat[:content] %></td>
      <td><%= chat[:time] %></td>
    </tr>
  <% end %>
</table>

Au minimum, vous devriez pouvoir discuter.

Enregistrer dans la base de données

Enregistrez le contenu du chat sur MySQL. Installez mysql2 Gem.

$ docker-compose run --rm app bundle add mysql2

Obtenez votre version préférée de MySQL sur https://hub.docker.com/_/mysql et utilisez-la. Définissez également les informations de connexion en tant que variable d'environnement pour l'application.

docker-compose.yml


 version: '3'
 services:
   app:
     build: .
     volumes:
       - .:/app
       - /app/vendor/bundle
     ports:
       - 127.0.0.1:4567:4567
+    environment:
+      - MYSQL_HOST=db
+      - MYSQL_USER=root
+      - MYSQL_PASS=secret
+      - MYSQL_DATABASE=nopochat_development
+  db:
+    image: mysql:5.7
+    volumes:
+      - .:/app
+      - /var/lib/mysql
+    environment:
+      - MYSQL_ROOT_PASSWORD=secret

index.rb


 require 'sinatra'
 require 'sinatra/reloader' if settings.development?
+require 'mysql2'
(Ce qui suit est omis)

Installez les packages requis pour utiliser mysql2 Gem.

Dockerfile


 FROM ruby:2.7-slim
 WORKDIR /app
+RUN apt-get update && apt-get install -y \
+  build-essential \
+  libmariadb-dev \
+  && apt-get clean \
+  && rm -rf /var/lib/apt/lists/*
(Ce qui suit est omis)

Définissez une méthode pour obtenir le client de base de données en fonction des variables d'environnement définies.

index.rb


def db_client()
  Mysql2::Client.default_query_options.merge!(:symbolize_keys => true)
  Mysql2::Client.new(
    :host => ENV['MYSQL_HOST'],
    :username => ENV['MYSQL_USER'],
    :password => ENV['MYSQL_PASS'],
    :database => ENV['MYSQL_DATABASE']
  )
end

Cette fois, la spécification est d'initialiser la base de données lors de l'accès à GET / initialize (bien qu'une telle spécification ne soit pas possible en fonctionnement réel ...)

index.rb


get '/initialize' do
  client = Mysql2::Client.new(
    :host => ENV['MYSQL_HOST'],
    :username => ENV['MYSQL_USER'],
    :password => ENV['MYSQL_PASS']
  )
  client.query("DROP DATABASE IF EXISTS #{ENV['MYSQL_DATABASE']}")
  client.query("CREATE DATABASE IF NOT EXISTS #{ENV['MYSQL_DATABASE']} CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci")
  client = db_client
  client.query(<<-EOS
      CREATE TABLE IF NOT EXISTS chats (
        id   INT AUTO_INCREMENT,
        name TEXT,
        content TEXT,
        time DATETIME,
        PRIMARY KEY(id)
    )
    EOS
  )
  redirect '/'
end

Définissez une méthode pour entrer et sortir des données d'une table appelée chats.

index.rb


def chat_push(content, name="Anonyme")
  db_client.prepare(
    "INSERT into chats (name, content, time) VALUES (?, ?, NOW())"
  ).execute(name, content)
end

def chats_fetch()
  db_client.query("SELECT * FROM chats ORDER BY time DESC")
end

Réécrivez GET / et POST / en utilisant la méthode définie.

index.rb


 get '/' do
-  @@chats ||= []
+  chats = chats_fetch
   erb :index, locals: {
-    chats: @@chats.map{ |chat| add_suffix(chat) }.reverse
+    chats: chats.map{ |chat| add_suffix(chat) }
   }
 end

 post '/' do
-  @@chats ||= []
-  @@chats.push({ content: params['content'], time: Time.now } )
+  chat_push(params['content'])
   redirect back
 end

Lancez l'application et accédez à http: // localhost: 4567 / initialize pour la lancer. Désormais, même si vous redémarrez l'application, le contenu avec lequel vous avez discuté ne disparaîtra pas.

Fonction de connexion

DB (MySQL) est utilisé pour le stockage de session. Définissez une table ʻusers avec un nom d'utilisateur et un mot de passe et une tablesessions` pour stocker les sessions. À l'origine, le mot de passe doit être chiffré afin qu'il ait un hachage. Vous devez également supprimer périodiquement des sessions.

index.rb


  client.query(<<-EOS
    CREATE TABLE IF NOT EXISTS users (
      id   INT AUTO_INCREMENT,
      name VARCHAR(255) UNIQUE,
      password TEXT,
      PRIMARY KEY(id),
      INDEX key_index (name)
    );
    EOS
  )
  client.query(<<-EOS
    CREATE TABLE IF NOT EXISTS sessions (
      id   INT AUTO_INCREMENT,
      session_id VARCHAR(255) UNIQUE,
      value_json JSON,
      PRIMARY KEY(id),
      INDEX key_index (session_id)
    );
    EOS
  )
  user_push('admin', 'admin')

Définit le traitement d'ajout / d'authentification d'utilisateur.

index.rb


def user_push(name, pass)
  db_client.prepare(
    "INSERT into users (name, password) VALUES (?, ?)"
  ).execute(name, pass)
end

def user_fetch(name, pass)
  result = db_client.prepare("SELECT * FROM users WHERE name = ?").execute(name).first
  return unless result
  result[:password] == pass ? result : nil
end

Définit le processus d'ajout / d'acquisition de session.

index.rb


def session_save(session_id, obj)
  db_client.prepare(
    "INSERT into sessions (session_id, value_json) VALUES (?, ?)"
  ).execute(session_id, JSON.dump(obj))
end

def session_fetch(session_id)
  return if session_id == ""
  result = db_client.prepare("SELECT * FROM sessions WHERE session_id = ?").execute(session_id).first
  return unless result
  JSON.parse(result&.[](:value_json))
end

Ajoutez `require'sinatra / cookies '' pour utiliser les cookies.

index.rb


 require 'sinatra'
 require 'sinatra/reloader' if settings.development?
+require 'sinatra/cookies'
 require 'mysql2'

Définissez «POST / login» et «GET / logout».

index.rb


post '/login' do
  if user = user_fetch(params['name'], params['pass'])
    cookies[:session_id] = SecureRandom.uuid if cookies[:session_id].nil? || cookies[:session_id] == ""
    session_save(cookies[:session_id], { name: user[:name] })
  end
  redirect back
end

get '/logout' do
  cookies[:session_id] = nil
  redirect back
end

Modifiez «GET /» et «POST /».

index.rb


 get '/' do
+  name = session_fetch(cookies[:session_id])&.[]("name")
   chats = chats_fetch
   erb :index, locals: {
+    name: name,
     chats: chats.map{ |chat| add_suffix(chat) }
   }
 end

 post '/' do
-  chat_push(params['content'])
+  name = session_fetch(cookies[:session_id])&.[]("name")
+  chat_push(params['content'], name)
   redirect back
 end

Réécrivez la partie formulaire de View afin que le formulaire de connexion s'affiche lorsque vous n'êtes pas connecté et que le formulaire de publication s'affiche après la connexion.

vieqs/index.erb


<% if name %>
  <p>Bonjour<%= name %>M.</p>
  <a href="/logout">Se déconnecter</a>
  <form action="/" method="post">
    <input name="content" placeholder="Publier" />
    <button type="submit">Envoyer</button>
  </form>
<% else %>
  <form action="login" method="post">
    <input name="name" placeholder="Nom d'utilisateur">
    <input name="pass" placeholder="mot de passe">
    <button type="submit">S'identifier</button>
  </form>
<% end %>

Si vous pouvez accéder à http: // localhost: 4567 / initialize et vous connecter en tant qu'utilisateur ʻadmin`, vous avez réussi.

Application multiple

Modifiez docker-compose.yml comme suit. Créez deux applications et ajoutez un nouveau conteneur Nginx qui sera le serveur Web. Fermez le port 4567 dans Sinatra et ouvrez le port 8080 pour Nignx.

Nginx récupère votre version préférée sur https://hub.docker.com/_/nginx et l'utilise.

docker-compose.yml


 version: '3'
 services:
-  app:
+  app1: &app
     build: .
     volumes:
       - .:/app
       - /app/vendor/bundle
-    ports:
-      - 127.0.0.1:4567:4567
     environment:
       - MYSQL_HOST=db
       - MYSQL_USER=root
       - MYSQL_PASS=secret
       - MYSQL_DATABASE=nopochat_development
+  app2:
+    <<: *app
+  web:
+    image: nginx:1.19-alpine
+    volumes:
+      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
+    ports:
+      - 127.0.0.1:8080:80
(Ce qui suit est omis)

Placez le fichier de configuration Nginx. Essayez de trier ʻapp1 et ʻapp2.

nginx/default.conf


upstream apps {
  server app1:4567;
  server app2:4567;
}

server {
  listen 80;
  proxy_set_header Host $host:8080;

  location / {
    proxy_pass http://apps;
  }
}

Accédez à http: // localhost: 8080 /. Si le processus de connexion échoue, la session changera et vous serez déconnecté à chaque fois que vous y accédez.

Appeler Rust de Ruby

Le processus d'ajout de "mo" à la fin du mot mentionné au début

def add_suffix(chat)
  { **chat, content: "#{chat[:content]}Aussi" }
end

Écrivons ceci dans Rust et appelons-le depuis Ruby.

Obtenez votre version préférée sur https://hub.docker.com/_/rust. Cette fois, nous utiliserons "rust: 1.46-slim". Créez un projet Rust dans un répertoire appelé rust_lib avec la commande suivante.

$ docker run \
  --rm \
  --volume $(pwd):/app \
  --workdir /app \
  --env USER=root \
  rust:1.46-slim cargo new rust_lib --lib
$ cd rust_lib 

Si vous le souhaitez, prenez-le sur GitHub .gitignore et définissez-le.

$ curl https://raw.githubusercontent.com/github/gitignore/master/Rust.gitignore -o .gitignore

Ajoutez libc crate dans Cargo et spécifiez le type de crate à «" dylib "».

rust_lib/Cargo.toml


[dependencies]
libc = "0.2.77"

[lib]
name = "rust_lib"
crate-type = ["dylib"]

J'écrirai le processus dans Rust.

rust_lib/src/lib.rs


extern crate libc;
use libc::*;
use std::ffi::{CStr, CString};

#[no_mangle]
pub extern fn add_suffix(s: *const c_char) -> CString {
    let not_c_s = unsafe { CStr::from_ptr(s) }.to_str().unwrap();
    let not_c_message = format!("{}Aussi", not_c_s);
    CString::new(not_c_message).unwrap()
}

Construire.

$ docker run \
  --rm \
  --volume $(pwd):/app \
  --workdir /app \
  rust:1.46-slim cargo build

Il réussit si rust_lib / target / release / librust_lib.so est construit.

$ nm target/release/librust_lib.so | grep add_suffix
00000000000502c0 T add_suffix

Ruby FFI J'écrirai un processus pour appeler Rust depuis Ruby en utilisant Gem.

$ docker-compose run --rm app1 bundle add ffi

index.rb


 require 'sinatra'
 require 'sinatra/reloader' if settings.development?
 require 'sinatra/cookies'
 require 'mysql2'
+require 'ffi'

ʻExtend FFI :: Library pour lire et utiliser rust_lib / target / release / librust_lib.so`. Spécifiez l'argument et le type de retour en vous référant à FFI wiki.

index.rb


#Configuration du module pour appeler depuis Rust
module RustLib
  extend FFI::Library
  ffi_lib('rust_lib/target/release/librust_lib.so')
  attach_function(:add_suffix, [:string], :string)
end

Modifiez «GET /».

index.rb


 get '/' do
   name = session_fetch(cookies[:session_id])&.[]("name")
   chats = chats_fetch
   erb :index, locals: {
     name: name,
-    chats: chats.map{ |chat| add_suffix(chat) }
+    chats: chats.map{ |chat| { **chat, content: RustLib::add_suffix(chat[:content]).force_encoding("UTF-8") } }
   }
 end

c'est tout. Le produit fini est le suivant.

https://github.com/s2terminal/sinatra-chat

référence

Recommended Posts

Entraînez-vous à créer une application de chat simple avec Docker + Sinatra
[docker] [nginx] Créer un ALB simple avec nginx
Créez une application de recherche simple avec Spring Boot
Créer une image docker pour exécuter une application Java simple
Créez une application de chat avec WebSocket (Tyrus) + libGDX + Kotlin
J'ai créé une application de chat.
Créez un environnement Vue3 avec Docker!
[Rails] J'ai créé une mini-application de calendrier simple avec des spécifications personnalisées.
Créer un environnement Node.js avec Docker
Faites une langue! (Faire une simple calculatrice ②)
Créer une application de minuterie avec de la boue
Faites une langue! (Faire une simple calculatrice ①)
Décorez votre application Sinatra avec CSS
Faites une liste de choses à faire en famille avec Sinatra
[Rails6] Créer une nouvelle application avec Rails [Débutant]
Créez une application Web simple avec Dropwizard
Créer un environnement de développement PureScript avec Docker
Créez un lot à la demande simple avec Spring Batch
[Retrait des rails] Créez une fonction de retrait simple avec des rails
Dessinez des graphiques avec Sinatra et Chartkick
Créer un graphique à barres simple avec MPAndroidChart
Faites une liste de choses à faire en famille avec Sinatra
L'application Sinatra avec ActiveRecord est morte dans Passenger 6.0.5
Créer un environnement de développement Wordpress avec Docker
Créons une application de calcul avec Java
J'ai créé une application Janken avec kotlin
[Rails 5] Créer une nouvelle application avec Rails [Débutant]
Implémentez un CRUD simple avec Go + MySQL + Docker
J'ai créé une application Janken avec Android
Construction d'un environnement de développement simple Docker + Django
J'ai créé une application d'apprentissage automatique avec Dash (+ Docker) part3 ~ Practice ~
[Memo] Créez facilement un environnement CentOS 8 avec Docker
Créez un tableau d'affichage simple avec Java + MySQL
Créer un environnement Laravel / Docker avec VSCode devcontainer
Créez rapidement un environnement de développement WordPress avec Docker
Créer une application Kotlin à l'aide du conteneur OpenJDK Docker
Créez une discussion d'équipe avec Rails Action Cable
Construction de l'environnement de développement Simple Docker Compose + Django
Application Java CICS-Run - (1) Exécutez un exemple d'application simple
Préparer un environnement de scraping avec Docker et Java
Afficher un simple Hello World avec SpringBoot + IntelliJ
Un simple jeu de ciseaux-papier-pierre avec JavaFX et SceneBuilder
Créer un environnement de développement Spring Boot avec docker