[RUBY] Üben Sie das Erstellen einer einfachen Chat-App mit Docker + Sinatra

Ich vergesse oft, wie man Ruby benutzt, deshalb habe ich in Ruby eine einfache Webanwendung für die Rehabilitation erstellt. Außerdem werde ich aufschreiben, wie ich es machen soll, falls ich es vergesse.

Es ist eine verdammte Chat-App namens Nopochat. Es ist ein epochaler Chat, in dem Sie schwer zu sprechende Inhalte unter Wahrung der psychologischen Sicherheit senden können, indem Sie dem Beitrag ein niedliches Ende von "-mo" hinzufügen.

image.png

Der in diesem Artikel vorgestellte Inhalt dient nur zum Schreiben von Programmen und ist nicht für die Verwendung in einer Produktionsumgebung vorgesehen.

Umgebung

Verwenden Sie Docker. Keine Ruby-Installation erforderlich.

Start von Sinatra

Schneiden Sie zuerst ein geeignetes Verzeichnis aus und starten Sie die Entwicklung.

$ mkdir sinatra-chat && cd $_

Wenn Sie Ihr Projekt mit Git verwalten möchten, empfiehlt es sich, [GitHub gitignore] abzurufen und festzulegen (https://github.com/github/gitignore/blob/master/Ruby.gitignore).

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

Die gewünschte Version des Ruby-Images erhalten Sie unter https://hub.docker.com/_/ruby. Dieses Mal werde ich "2.7-slim" verwenden.

Initialisieren Sie zuerst 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

Nachdem Sie bestätigt haben, dass "Gemfile" und "Gemfile.lock" generiert wurden, schreiben Sie "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

Erstellen Sie "index.rb", den Hauptteil der Anwendung.

index.rb


require 'sinatra'

configure do
  set :bind, '0.0.0.0'
end

get '/' do
  'Hello Sinatra!'
end

Gehen Sie zu http: // localhost: 4567 / und wenn Sie "Hallo Sinatra!" Sehen, sind Sie erfolgreich.

Aktivieren Sie das Neuladen durch erneutes Laden des Browsers

Selbst wenn Sie die Datei bearbeiten, wird sie nur angezeigt, wenn Sie den Sinatra-Server neu starten. Es ist nützlich, das Neuladen beim Neuladen zu aktivieren, bevor Sie mit der Entwicklung beginnen.

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

index.rb


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

Entwicklung der Chat-Funktion

Da der Chat-Inhalt später beibehalten wird, speichern wir ihn vorerst in einer Klassenvariablen namens "@@ chats".

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]}Ebenfalls" }
end

Verwenden Sie erb für die HTML-Vorlage. Wenn Sie das Verzeichnis "views /" ausschneiden und dort "index.erb" speichern, können Sie es mit "erb: index" aufrufen.

views/index.erb


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

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

Zumindest sollten Sie in der Lage sein, zu chatten.

In Datenbank speichern

Speichern Sie den Chat-Inhalt in MySQL. Installieren Sie mysql2 Gem.

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

Holen Sie sich Ihre Lieblingsversion von MySQL unter https://hub.docker.com/_/mysql und verwenden Sie sie. Legen Sie außerdem die Verbindungsinformationen als Umgebungsvariable für die App fest.

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'
(Folgendes wird weggelassen)

Installieren Sie die Pakete, die für die Verwendung von mysql2 Gem erforderlich sind.

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/*
(Folgendes wird weggelassen)

Definieren Sie eine Methode, um den Datenbankclient basierend auf den festgelegten Umgebungsvariablen abzurufen.

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

Dieses Mal besteht die Spezifikation darin, die Datenbank zu initialisieren, wenn auf "GET / initialize" zugegriffen wird (obwohl eine solche Spezifikation im tatsächlichen Betrieb nicht möglich ist ...).

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

Definieren Sie eine Methode zum Ein- und Auslesen von Daten in eine Tabelle namens Chats.

index.rb


def chat_push(content, name="Anonym")
  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

Schreiben Sie "GET /" und "POST /" mit der definierten Methode neu.

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

Starten Sie die App und greifen Sie auf http: // localhost: 4567 / initialize zu, um sie zu starten. Selbst wenn Sie die App neu starten, verschwindet der Inhalt, mit dem Sie chatten, nicht.

Anmeldefunktion

DB (MySQL) wird für die Sitzungsspeicherung verwendet. Definieren Sie eine Benutzertabelle mit einem Benutzernamen und einem Kennwort sowie eine Sitzungstabelle zum Speichern der Sitzungen. Ursprünglich sollte das Passwort so verschlüsselt sein, dass es einen Hash enthält. Sie müssen außerdem regelmäßig Sitzungen löschen.

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')

Definiert die Verarbeitung zum Hinzufügen / Authentifizieren von Benutzern.

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

Definiert den Prozess zum Hinzufügen / Erfassen von Sitzungen.

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

Fügen Sie "require'sinatra / cookies" hinzu, um Cookies zu verwenden.

index.rb


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

Definieren Sie "POST / Login" und "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

Ändern Sie "GET /" und "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

Schreiben Sie den Formularteil von View neu, sodass das Anmeldeformular angezeigt wird, wenn Sie nicht angemeldet sind, und das Beitragsformular angezeigt wird, nachdem Sie sich angemeldet haben.

vieqs/index.erb


<% if name %>
  <p>Hallo<%= name %>Herr.</p>
  <a href="/logout">Ausloggen</a>
  <form action="/" method="post">
    <input name="content" placeholder="Post" />
    <button type="submit">Senden</button>
  </form>
<% else %>
  <form action="login" method="post">
    <input name="name" placeholder="Nutzername">
    <input name="pass" placeholder="Passwort">
    <button type="submit">Einloggen</button>
  </form>
<% end %>

Wenn Sie auf http: // localhost: 4567 / initialize zugreifen und sich als Administrator anmelden können, sind Sie erfolgreich.

Mehrere App

Ändern Sie docker-compose.yml wie folgt. Erstellen Sie zwei Apps und fügen Sie einen neuen Nginx-Container hinzu, der der Webserver sein wird. Schließen Sie Port 4567 in Sinatra und öffnen Sie Port 8080 für Nignx.

Nginx verwendet https://hub.docker.com/_/nginx, indem es Ihre Lieblingsversion abruft.

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
(Folgendes wird weggelassen)

Platzieren Sie die Nginx-Konfigurationsdatei. Versuchen Sie, "app1" und "app2" zu sortieren.

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;
  }
}

Gehen Sie zu http: // localhost: 8080 /. Wenn der Anmeldevorgang nicht erfolgreich ist, wechselt die Sitzung und Sie werden bei jedem Zugriff abgemeldet.

Rufen Sie Rust von Ruby an

Der Vorgang des Hinzufügens von "mo" am Ende des am Anfang erwähnten Wortes

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

Schreiben wir das in Rust und nennen es Ruby.

Holen Sie sich Ihre Lieblingsversion von https://hub.docker.com/_/rust. Dieses Mal werden wir "Rost: 1,46-schlank" verwenden. Erstellen Sie mit dem folgenden Befehl ein Rust-Projekt in einem Verzeichnis namens "rust_lib".

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

Wenn Sie möchten, nehmen Sie es von GitHub .gitignore und stellen Sie es ein.

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

Fügen Sie libc crate in Cargo hinzu und geben Sie den Kistentyp für "dylib" an.

rust_lib/Cargo.toml


[dependencies]
libc = "0.2.77"

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

Ich werde den Prozess in Rust schreiben.

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!("{}Ebenfalls", not_c_s);
    CString::new(not_c_message).unwrap()
}

Bauen.

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

Es ist erfolgreich, wenn rust_lib / target / release / librust_lib.so erstellt wird.

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

Ruby FFI Ich werde einen Prozess schreiben, um Rust von Ruby mit Gem aufzurufen.

$ 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'

Verwenden Sie expand FFI :: Library, um rust_lib / target / release / librust_lib.so zu laden und zu verwenden. Geben Sie das Argument und den Rückgabetyp an, indem Sie auf FFI-Wiki verweisen.

index.rb


#Einstellen des Moduls für den Anruf von Rust
module RustLib
  extend FFI::Library
  ffi_lib('rust_lib/target/release/librust_lib.so')
  attach_function(:add_suffix, [:string], :string)
end

Ändern Sie 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

das ist alles. Das fertige Produkt ist wie folgt.

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

Referenz

Recommended Posts

Üben Sie das Erstellen einer einfachen Chat-App mit Docker + Sinatra
[Docker] [Nginx] Erstellen Sie mit Nginx eine einfache ALB
Erstellen Sie mit Spring Boot eine einfache Such-App
Erstellen Sie ein Docker-Image, um eine einfache Java-App auszuführen
Erstellen Sie eine Chat-App mit WebSocket (Tyrus) + libGDX + Kotlin
Ich habe eine Chat-App erstellt.
Erstellen Sie mit Docker eine Vue3-Umgebung!
[Rails] Ich habe eine einfache Kalender-Mini-App mit benutzerdefinierten Spezifikationen erstellt.
Erstellen Sie mit Docker eine Node.js-Umgebung
Mach eine Sprache! (Einen einfachen Taschenrechner machen ②)
Erstellen einer Timer-App mit Schlamm
Mach eine Sprache! (Einen einfachen Taschenrechner machen ①)
Dekorieren Sie Ihre Sinatra-App mit CSS
Erstellen Sie mit Sinatra eine Familien-ToDo-Liste
[Rails6] Erstelle eine neue App mit Rails [Anfänger]
Erstellen Sie mit Dropwizard eine einfache Webanwendung
Erstellen Sie mit Docker eine PureScript-Entwicklungsumgebung
Erstellen Sie mit Spring Batch eine einfache On-Demand-Charge
[Schienenentnahme] Erstellen Sie eine einfache Entnahmefunktion mit Schienen
Zeichnen Sie Diagramme mit Sinatra und Chartkick
Erstellen eines einfachen Balkendiagramms mit MPAndroidChart
Erstellen Sie mit Sinatra eine Familien-ToDo-Liste
Sinatra App mit ActiveRecord starb in Passenger 6.0.5
Erstellen Sie mit Docker eine Wordpress-Entwicklungsumgebung
Lassen Sie uns eine Taschenrechner-App mit Java erstellen
Ich habe eine Janken App mit Kotlin gemacht
[Rails 5] Erstelle eine neue App mit Rails [Anfänger]
Implementieren Sie eine einfache CRUD mit Go + MySQL + Docker
Ich habe eine Janken App mit Android gemacht
Einfache Konstruktion der Docker + Django-Entwicklungsumgebung
Ich habe eine App für maschinelles Lernen mit Dash (+ Docker) Teil 3 ~ Übung ~ erstellt
[Memo] Erstellen Sie mit Docker ganz einfach eine CentOS 8-Umgebung
Erstellen Sie mit Java + MySQL ein einfaches Bulletin Board
Erstellen Sie eine Laravel / Docker-Umgebung mit VSCode devcontainer
Erstellen Sie mit Docker schnell eine WordPress-Entwicklungsumgebung
Erstellen Sie eine Kotlin-App mit dem OpenJDK Docker-Container
Erstellen Sie einen Team-Chat mit Rails Action Cable
Einfache Erstellung der Docker Compose + Django-Entwicklungsumgebung
CICS-Java-Anwendung ausführen- (1) Führen Sie eine einfache Beispielanwendung aus
Bereiten Sie eine Scraping-Umgebung mit Docker und Java vor
Zeigen Sie eine einfache Hallo Welt mit SpringBoot + IntelliJ
Ein einfaches Stein-Papier-Scheren-Spiel mit JavaFX und SceneBuilder
Erstellen Sie mit Docker eine Spring Boot-Entwicklungsumgebung