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.
Der in diesem Artikel vorgestellte Inhalt dient nur zum Schreiben von Programmen und ist nicht für die Verwendung in einer Produktionsumgebung vorgesehen.
Verwenden Sie Docker. Keine Ruby-Installation erforderlich.
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.
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?
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.
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.
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.
Ä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.
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
Recommended Posts