GO — রিমোট প্রসিডিউর কল বা RPC
হিউম্যান ক্লায়েন্ট-সার্ভার কম্যিউনিকেশনের জন্য REST এর জুড়ি নেই। সিম্পল কিছু আর্কিটেকচারাল গাইডলাইন ফলো করলেই ইমপ্লিমেন্ট করা যায়।
কিন্ত সার্ভারই যখন ক্লায়েন্ট হয়ে আরেক সার্ভারে রিক্যুয়েস্ট পাঠায় তখন ব্যাপারটা একটু অন্যরকম। এটাকে বলা যেতে পারে সার্ভার টু সার্ভার বা SOA এর ভাষায় সার্ভিস টু সার্ভিস কম্যিউনিকেশন।
সার্ভার টু সার্ভার কম্যিউনিকেশন দুইভাবে হতে পারেঃ সিনক্রোনাস আর এসিনক্রোনাস। এসিনক্রোনাসকে আমরা ইভেন্ট-ড্রিভেন আর্কিটেকচারের সাহায্যে ইমপ্লিমেন্ট করতে পারি। আজকের এই পোস্ট এসিনক্রোনাস কম্যিউনিকেশন নিয়ে নয়, এটা নিয়ে আরেকদিন আলোচনা করবো।
আজকে আমি আলোচনা করবো মাইক্রোসার্ভিস আর্কিটেকচারে সার্ভিস টু সার্ভিস সিনক্রোনাস কম্যিউনেকশনের একটা ব্যাতিক্রম উপায় নিয়ে, যেটা REST এর অল্টারনেটিভ। যার নাম Remote Procedure Call বা RPC
আসুন আগে জেনে নেই RPC কি?
রিমোট মানে তো আমরা বুঝিই, যেটা লোকাল এর বিপরীত, যেটা আরেক সার্ভারে থাকা কিছুকে ইন্টারনেট এর মাধ্যমে এক্সেস করতে হয়। প্রসিডিউর অনেকটা ফাংশনের মতই। আর কল মানে তো ডাকা এবং এক্সিকিউট করা।
তাহলে RPC মানে দাড়ায়ঃ
রিমোট সার্ভারে থাকা কোনো ফাংশন বা প্রসিডিউরকে নেটওয়ার্কের মাধ্যমে এক্সিকিউট করা
আমাদের প্রোগ্রামে কোনো ফাংশন লিখলে সেটাকে কল করে এক্সিকিউট করতে পারি। কেমন হয়, যদি অন্য সার্ভারে থাকা কোনো ফাংশনকেও এভাবে কল করে এক্সিকিউট করতে পারি? এটার সুবিধা নিয়েই এসেছে RPC, আমরা খুব সহজেই অন্য সার্ভারে থাকা ফাংশনকে আমাদের সার্ভার থেকে কল করে এক্সিকিউট করতে পারি।
তার আগে আসুন জেনে নিই RPC এর আর্কিটেকচার ও ইন্টারনাল সম্বন্ধে
RPC কিভাবে কাজ করে?
উপরের ডায়াগ্রামটা লক্ষ করুন, ১ থেকে ১০ পর্যন্ত নাম্বারিং করা আছে।
অপারেটিং সিস্টেম ২ মোডে কাজ করে, ইউজার মোড আর কার্নেল মোড।
RPC তে প্রথমে ক্লায়েন্ট কোন একটা রিমোট প্রসিডিউরকে দরকারী আর্গুমেন্ট সহ কল করে, এরপর সেটা অপারেটিং সিস্টেমের ক্লায়েন্ট স্টাব নামক অংশে যায়। এই স্টাবের কাজ হলো ফাংশন কল করার সময় ফাংশনে পাঠানো বিভিন্ন আর্গুমেন্টকে বান্ডেল ও আন-বান্ডেল করা এবং ফাংশন থেকে রিটার্ন ভ্যালুকে বান্ডেল ও আন-বান্ডেল করা।
ক্লায়েন্ট স্টাব প্রথমে কলিং প্রসিডিউরের আর্গুমেন্ট গুলিকে বান্ডেল করে, এরপর সেগুলিকে এক সাথে প্যাকেজ করে নেটওয়ার্ক সকেটে পাঠায়, যেটা টিসিপি প্রোটোকল এর মাধ্যমে নেটওয়ার্কের মধ্যে ভ্রমণ করে। সকেটের আগ পর্যন্ত কাজগুলি ওএস এর ইউজার মোডে হয়, আর সকেটের কাজগুলি কার্নেল মোডে হয়। এবার ক্লায়েন্টের সকেট থেকে ডাটা বাইট স্ট্রীম হিসেবে রিমোট সার্ভারের সকেটে গিয়ে পৌছায়। সেখান থেকে সার্ভারের স্টাবে যায়, স্টাব এবার আর্গুমেন্ট গুলিকে আন-বান্ডেল করে। এবং অবশেষে তা সার্ভারে থাকা ঐ প্রসিডিউরে গিয়ে পৌছায়, এবং ক্লায়েন্ট এর কাছ থেকে আসা আর্গুমেন্টকে নিয়ে এক্সিকিউট করে।
প্রসিডিউর এক্সিকিউট করার পর রেজাল্ট যা আসে তা আবার সার্ভারের স্টাবে যায়, স্টাব তখন সেগুলিকে বান্ডেল করে সকেটের মাধ্যমে ক্লায়েন্টকে পাঠায়। ক্লায়েন্ট এর স্টাব সেই রেজাল্টকে আন-বান্ডেল করে আউটপুট দেয় ক্লায়েন্টকে। ব্যস, এভাবেই RPC কাজ করে।
প্র্যাক্টিকাল কোডিং করলে এই কনসেপ্টগুলি বুঝতে সুবিধা হবে। তো চলুন শুরু করি আমাদের কোডিং সেশন
RPC এর জন্য গো তে দারুণ সাপোর্ট দেয়া আছে এর স্ট্যান্ডার্ড লাইব্রেরিতেই। তাই আমরা গো এর কোর দিয়ে আজকে ইম্পিমেন্ট করবো
স্টেপ ১ঃ ফোল্ডার স্ট্রাকচার করা
আমরা প্রথমে অর্গানাইজড ওয়েতে কাজ করার জন্য ফোল্ডার স্ট্রাকচার বানাবো নিচের মত করেঃ
Client, Server ফোল্ডারে ক্লায়েন্ট ও সার্ভারের কোড থাকবে। আর Common ফোল্ডারে উভয়ের জন্য কমন জিনিসগুলি থাকবে।
স্টেপ ২ঃ কমন ইলিমেন্ট তৈরি করা
প্রথমেই আমরা কিছু কমন ইলিমেন্ট তৈরি করে নিবো যেটা ক্লায়েন্ট ও সার্ভার উভয়েই ব্যবহার করবে। এর জন্য common ফোল্ডারে common.go নামে একটা ফাইল বানাই এবং নিচের মত কোড লিখি
package common
type Args struct {
A, B int
}
type Result int
আমরা আর্গুমেন্ট ও রেজাল্ট এর জন্য দুইটা ডাটা স্ট্রাকচার ডিফাইন করে নিলাম। এগুলি কি কাজে লাগবে তা একটু পরই আমরা দেখবো
স্টেপ ৩ঃ সার্ভার বানানো
RPC সার্ভার বানানো খুবই সহজ কাজ গো তে, আমাদেরকে একটা সার্ভার অবজেক্ট নিতে হবে এবং তার সাথে যেকোনো মেথড এটাচ করে দিতে পারবো, এই মেথডগুলিই আমরা রিমোটলি কল করে এক্সিকিউট করতে পারবো।
তবে এই মেথডের কিছু নিয়ম আছেঃ
০১ মেথড গুলি এক্সপোর্টেড হতে হবে
০২ মেথডগুলির ২ টা প্যারামিটার থাকবে এবং ২ টাই এক্সপোর্টেড হতে হবে এবং ২য় টা পয়েন্টার হতে হবে। প্রথম প্যারামিটার হবে RPC প্রসিডিউ এর জন্য আর্গুমেন্ট, আর দ্বিতীয় প্যারামিটার হবে RPC প্রসিডিউর থেকে পাওয়া রেজাল্ট বা রিপ্লাই এর জন্য।
০৩ মেথডের রিটার্ন ভ্যালু অবশ্যই এরর হতে হবে
তো চলুন, প্রথমেই আমরা একটা সার্ভার অবজেক্ট বানাই। এর জন্য যেকোন একটা ডামি স্ট্রাক্ট ডিফাইন করতে পারি এবং সেটা থেকে অবজেক্ট ইন্সটেন্স নিতে পারি
package main
import (
"RPC/common"
"net/rpc"
"net"
"log"
"fmt"
)
type Arith int
func (a *Arith) Multiply(args *common.Args, reply *common.Result) error {
*reply = common.Result(args.A * args.B)
return nil
}
func main() {
arith := new(Arith)
rpc.Register(arith)
l, err := net.Listen("tcp", ":8025")
if err != nil {
log.Fatal(err)
}
fmt.Println("Server started...")
for {
conn, err := l.Accept()
fmt.Println("Client called...")
if err != nil {
log.Fatal(err)
}
go rpc.ServeConn(conn)
}
}
আমরা প্রথমে একটা ডামি স্ট্রাক্ট বানালাম Arith নামে যেটা মূলত একটা int টাইপ। এর সাথে Multiply নামে একটা মেথড যুক্ত করে দিয়েছি যার ২ টা প্যারামিটার পয়েন্টার টাইপঃ প্রথম প্যারামিটার RPC ফাংশনের জন্য আর্গুমেন্ট, আর দ্বিতীয় প্যারামিটার হবে RPC ফাংশন থেকে পাওয়া রেজাল্ট বা রিপ্লাই এর জন্য।
এরপর আমরা reply এর ডিরেফারেন্সড ভ্যালুতে আর্গুমেন্ট থেকে পাওয়া ডাটাকে মাল্টিপ্লাই করে সেটাকে কমন Result ফরমেটে রূপান্তর করেছি। এই রিপ্লাই ডাটা ব্যাক করে যাবে ক্লায়েন্টের কাছে। আমরা ফাংশনের এরর হিসেবে nil রিটার্ন করেছি আপাতত (যেহেতু আমরা রেজাল্ট এর এরর চেকিং করিনি)।
এবার আমরা main() ফাংশনে আমাদের সার্ভার অবজেক্ট এর একটা পয়েন্টার বানালাম new() ফাংশন দিয়ে। এরপর সেটাকে আমরা rpc তে রেজিস্টার করলাম। এতে করে আমরা এবার সার্ভার অবজেক্ট এর সাথে এটাচ থাকা যেকোনো RPC মেথডকে কল করতে পারবো।
এবার আমরা একটা টিসিপি সার্ভার লিসেনার বানালাম যেটা tcp://localhost:8025/ এড্রেসে রান থাকবে। এই এড্রেসেই ক্লায়েন্ট কল করবে
এবার আমরা একটা ইনফিনিট লুপের মাধ্যমে সার্ভারে আসা বিভিন্ন রিক্যুয়েস্টকে হ্যান্ডেল করতে পারবো। সেখানে আমরা লিসেনারের Accept() মেথড থেকে ডিকোড করে ক্লায়েন্ট কানেকশন পেতে পারি এবং সেটাকে rpc দিয়ে সার্ভ করতে পারি। আমরা rpc এর ServeConn কে গোরুটিনে রান করলাম যাতে করে সার্ভারটা নন-ব্লকিং হয়।
স্টেপ ৪ঃ ক্লায়েন্ট বানানো
এবার আমরা RPC ক্লায়েন্ট বানাবো। এটা খুবই সহজ।
প্রথমেই client ফোল্ডারে গিয়ে client.go নামে একটা ফাইল বানাই এবং নিচের মতো কোড লিখিঃ
package main
import (
"net/rpc"
"RPC/common"
"fmt"
)
func main() {
client, _ := rpc.Dial("tcp", ":8025")
var result common.Result
args := &common.Args{A: 2, B: 3}
client.Call("Arith.Multiply", args, &result)
fmt.Println(result)
}
প্রথমেই আমরা RPC সার্ভারের সাথে কানেক্ট করবো, এর জন্য আমরা tcp://localhost:8025/
এই এড্রেসে কানেক্ট করলাম। এরপর কমন ডাটা স্ট্রাকচার এর মাধ্যমে আমরা result আর args নামে দুইটা অবজেক্ট নিলাম যেগুলিকে আমরা RPC ফাংশনে আর্গুমেন্ট হিসেবে পাস করবো। আমরা args হিসেবে দুইটা ইন্টিজার সেন্ড করতেছি যেগুলিকে সার্ভার মাল্টিপ্লাই করে রিপ্লাই পাঠাবে। সেই রিপ্লাইকে আমরা result ভেরিয়েবলে স্টোর করবো। ডিমোর জন্য আমি এরর চেকিং স্কিপ করে গেলাম। তবে প্রোডাকশন লেভেলে কোনোভাবেই যেন এরর স্কিপ না হয়।
দেখুন আমরা ক্লায়েন্ট থেকে কিভাবে করে RPC ফাংশনকে কল করলামঃ
client.Call("Arith.Multiply", args, &result)
আমরা RPC সার্ভারে থাকা Arith অবজেক্টের Multiply মেথডকে কল করলাম, যেটা ২ টা আর্গুমেন্ট একসেপ্ট করেঃ ফাংশন যে ডাটা নিয়ে কাজ করবে সেটা, আর কাজ শেষে যা রিপ্লাই করবে সেটা।
ব্যস, হয়ে গেলো। এবার প্রথমে সার্ভারটিকে রান করুন go run server.go কমান্ড দিয়ে
এরপর ক্লায়েন্টকে রান করুন go run client.go কমান্ড দিয়ে। দেখবেন ক্লায়েন্ট থেকে সার্ভারে RPC রিক্যুয়েস্ট পাঠাবে, সার্ভার সেটাকে প্রসেস করে একটা রিপ্লাই পাঠাবে আবার ক্লায়েন্টের কাছে। আমরা 2, 3 কে সার্ভারে পাঠিয়েছিলাম গুণ করার জন্য। সার্ভার থেকে রিপ্লাই আসবে 6
RPC তে ডাটা আদান প্রদানের ফরমেট
আমরা এখানে ডাটা আদান প্রদান করার জন্য গো বাইনারি ফরমেট gob ব্যবহার করেছি। gob এর সাহায্যে খুব সহজেই দুইটা গো RPC এর মাঝে ডাটা আদান প্রদান করা যাবে। এর বেশ কিছু সুবিধা আছে, এই লিঙ্কে গিয়ে দেখতে পারেন।
এই ডাটা আদান প্রদানের ফরমেটের কিছু অল্টারনেটিভ আছেঃ
01. JSON-RPC
02. XML-RPC
03. gRPC
JSON-RPC তে আমরা JSON ফরমেটে ডাটা আদান প্রদান করতে পারবো। XML-RPC তে আমরা XML ফরমেটে ডাটা আদান প্রদান করতে পারবো। আর gRPC হচ্ছে গুগলের RPC ইমপ্লিমেন্টেশন যেখানে আমরা গুগলের বাইনারি ডাটা ফরমেট Protobuf বা Protocol Buffer ব্যবহার করতে পারবো।
উপরে আমরা যে ইমপ্লিমেন্টেশন দেখালাম সেখানে আমরা প্রোটোকল হিসেবে TCP ব্যবহার করেছি। আমরা চাইলে টিসিপি এর উপরে HTTP প্রোটোকল ব্যবহার করে বেশ কিছু সুবিধা নিতে পারি। চলুন দেখি কিভাবে আমাদের টিসিপি RPC কে HTTP RPC তে রূপান্তর করতে পারিঃ
HTTP RPC সার্ভারে আপগ্রেড করা
package main
import (
"RPC/common"
"net/rpc"
"net"
"net/http"
"fmt"
)
type Arith int
func (a *Arith) Multiply(args *common.Args, reply *common.Result) error {
*reply = common.Result(args.A * args.B)
return nil
}
func main() {
arith := new(Arith)
rpc.Register(arith)
rpc.HandleHTTP() // <---
l, _ := net.Listen("tcp", ":8025")
fmt.Println("Server started...")
http.Serve(l, nil) // <---
}
অল্প কিছু লাইন পরিবর্তন করেছি কেবল। rpc.HandleHTTP() দিয়ে আমরা HTTP রিক্যুয়েস্ট হ্যান্ডেল করতে পারবো RPC সার্ভার দিয়ে। এরপর এবার আমরা একটা টিসিপি সার্ভার লিসেনার বানালাম যেটা tcp://localhost:8025/ এড্রেসে রান থাকবে।
এরপর সেই লিসেনার এর সাহায্যে আমরা http.Serve() মেথড দিয়ে RPC তে HTTP ইমপ্লিমেন্ট করলাম।
HTTP RPC ক্লায়েন্টে আপগ্রেড করা
package main
import (
"net/rpc"
"RPC/common"
"fmt"
)
func main() {
client, _ := rpc.DialHTTP("tcp", ":8025") // <---
var result common.Result
args := &common.Args{A: 2, B: 3}
client.Call("Arith.Multiply", args, &result)
fmt.Println(result)
}
মাত্র একটা ওয়ার্ড পরিবর্তন করেছি আমরা, rpc.Dial() মেথডের বদলে আমরা rpc.DialHTTP() মেথড ব্যবহার করে ক্লায়েন্ট থেকে সার্ভারে HTTP রিক্যুয়েস্ট পাঠাচ্ছি।
সার্ভার ও ক্লায়েন্ট রান করে দেখুন আগের মতই রেজাল্ট পাবেন। বাড়তি সুবিধা হিসেবে আমরা HTTP প্রোটোকলের বিভিন্ন সিক্যুরিটি ফিচার ব্যবহার করতে পারবো।
তো আজকে এই পর্যন্তই। সামনের পর্বে JSON-RPC এবং gRPC নিয়ে লিখবো ইন শা আল্লাহ।
ভালো থাকবেন।
আল্লাহ হাফেজ
Cyan Tarek
Software Engineer at LinkVision Software