مهندس نرمافزار ساده
یکم عمیقتر در مورد net/http
یکم مقدمه
زبون برنامهنویسی Go (یا Golang) از وقتی اومده، خیلی جاها استفاده شده و الحق و الانصاف کد زدن باهاش خیلی حال میده (این تجربهی منه، امیدوارم تجربهی شما هم همین باشه :) )
با این زبون کارای زیادی میشه کرد که یکی از اونا نوشتن وبسرویس عه. وقتی بحث وبسرویس هم که میاد وسط پروتکل HTTP یه خورده مهم میشه. یکی از خوبیهای گولنگ اینه که بدون نیاز به کتابخونههای خارجی، از این پروتکل واسه نوشتن وبسرویس پشتیبانی میکنه.
تو این پست قراره یه شیرجهی نسبتا عمیق بزنیم تو پکیج net/http.
هیچی با net/http
خب اگه بخوایم یه وب سرویس بنویسیم که هیچ کاری نکنه (آره، دقیقا هیچ کاری نکنه، فقط request های http رو بگیره و بریزه دور) کار خیلی سادهای داریم. با تیکه کد زیر میتونیم همچین کدی بزنیم:
package main
import "net/http"
func main() {
http.ListenAndServe(":8080", nil)
}
این تیکه کد یه سرور بالا میاره و رو پورت ۸۰۸۰ گوش میده که request های http رو بگیره. ولی هنوز مشخص نکردیم که با اونا چیکار کنه. برای اینکه یه کاری بکنیم رو request ها باید یه http.Handler بهش پاس بدیم (به جای همون nil توی کد بالا)
این http.Handler چیزی نیست جز یه interface کوچولو. کدش هم ایناها:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
سلام دنیا با net/http
برای یه سلامدنیای ساده، کافیه یه type تعریف کنیم (یا از یه جای دیگه گیر بیاریم) که این Interface رو implement بکنه. اون وقت یه Handler داریم. مثلا این شکلی:
package main
import "net/http"
type myHandler struct {}
func (m *myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello world!\n"))
}
func main() {
http.ListenAndServe(":8080", &myHandler{})
}
الان رو پورت ۸۰۸۰ یه وبسرویس داریم که فقط سلام میده.
خب تا اینجا که چیز خاصی نگفتیم. بریم که دیگه عمیق تر بشیم.
تابع ListenAndServe دقیقا چیکار میکنه؟
اگه پیادهسازی این تابع رو نگاه کنید، میبینید که یه Instance از http.Server میسازه و ListenAndServe اون رو صدا میزنه.
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
خب که الان چی؟ باید بفهمیم متود ListenAndServe روی http.Server چیکار میکنه. خب باز بریم پیادهسازیش رو ببینم. ایناها (حجم کدهایی که دارم میارم اینجا داره زیاد میشه. یه خورده باید کمترشون کنم!):
func (srv *Server) ListenAndServe() error {
if srv.shuttingDown() {
return ErrServerClosed
}
addr := srv.Addr
if addr == "" {
addr = ":http"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return srv.Serve(ln)
}
اگه از چک کردن در حال خاموش شدن سرور و تعریف آدرس که بگذریم، این کد داره یه http.Listener از پکیج net میگیره و اون رو پاس میده به متود Serve. تو این پست قصد ندارم راجع به پکیج net و موجودیتهای، مثل Listener، توضیح بدم. همین که بدونیم واسه قبول کردن کانکشن tcp به درد میخوره کافیه.
بازم بریم توی Serve؟ باشه بریم ببینیم چه خبره. کدشو میارم ولی با یکم خلاصه سازی :). اونجاهای که سه نقطه گذاشتم کد اصلی رو پاک کردم. اگه فرصت شد بعدا توضیح میدم چیکار میکنن.
func (srv *Server) Serve(l net.Listener) error {
...
// some initialization shit
...
for {
rw, err := l.Accept()
if err != nil {
...
// handling possible shitty errors
...
}
...
//blah blah blah
go c.serve(connCtx)
}
}
همونطور که میبینید بعد از یه سری Initialization ها مثل آماده کردن context و ... ، یه حلقهی بینهایت داریم (اگه ارور نخوریم البته) که منتظره یکی ریکوئست بزنه و accept اش بکنه. این کار رو که کرد یه کانکشن میگیره و روی اون کانکشن serve اش رو با یه context صدا میزنه. البته که یه دونه go انداخته پشتش که توی یه goroutine جدا اجرا بشه.
خب همینجا میخوام یه نتیجه کوچولو بگیرم. بر خلاف زبون هایی مثل پایتون که برای اجرای یه سرور باید با چیزی مثل uwsgi یا gunicorn چند تا process از کد ران بکنیم تا درخواستهای همزمان رو جواب بده، گولنگ خودش بلده request های همزمان رو هندل کنه (حالا جو میدم. صرفا یه goroutine درست میکنه)
فقط فعلا نپرسید اون serve که با s کوچیک نوشته شده چیکار میکنه. شاید بعدا اومدم تو یه پست دیگه توضیح دادم. (البته احتمال اینکه خودتون زودتر برید و بخونیدش بیشتره!)
خب تا اینجا با نحوهی Serve شدن Handler نسبتا آشنا شدیم. یکم هم راجع به ResponseWrite توضیح میدم و بقیهاش بمونه برای بعد.
خب حالا http.ResponseWriter چیه؟
اینم چیزی نیست جز یه Interface ساده. کدش رو هم الان میارم. بفرماید:
net/http/header.go
type Header map[string][]string
net/http/server.go
type ResponseWriter interface {
Header() Header
Write([]byte) (int, error)
WriteHeader(statusCode int)
}
سه تا تابع ساده داره. Header و Write و WriteHeader
اولی Header هایی که قراره به کاربر فرستاده بشه رو برمیگردونه. یه سری تابع هم داره که بشه باهاش header اضافه یا کم کرد.
با Write باید body جواب رو پر بکنیم. میتونه یه html باشه. میتونه یه json باشه یا هر چیز دیگه (البته واسه اینا ابزارهای بهتر و راحت تری هست)
با WriteHeader هم همهی Header ها و به اضافهی status code رو میفرسته که بره بشینه تو header جواب کاربر. البته اگه کالش نکنیم هم با کد ۲۰۰ خودش کال میکنه!
مگه ما نگفتیم این یه Interface عه؟ خب پیادهسازیش کجاست؟
نسخهی پیادهسازی شدهاش که پکیج net/http استفاده میکنه، تایپ http.response عه. یه تایپ متاسفانه اکسپورت نشده (البته احتمالا دلیل داشتن برای این کار ولی بعضی وقتا اذیت میکنه) که باعث میشه وقتی داریم Handler مینویسیم هیچ دسترسی سطح پایین به HTTP Response ای که میخوایم به کاربر برگردونیم نداشته باشیم.
ماهیت این پیاده سازی هم با توجه به استفاده از buffer تو پیادهسازیش، باعث میشه اگه فقط یه بار WriteHeader کال بشه، دیگه نتونیم تو هیچ چیز از Response دست ببریم. این ماهیت باعث میشه تو نوشتن بعضی از Middleware ها اذیت بشیم.
خب به نظرم تا همینجا فعلا بسه. تو پست بعدیم میخوام راجع به مشکلاتی که ممکنه با ResponseWrite بخوریم و اینکه چطوری حلشون کنیم صحبت بکنم. این پست رو میتونید از اینجا مشاهده کنید.
پ.ن: لطفا نظراتتون رو بنویسید. خیلی مهمه و کمک زیادی میکنه به بالا رفتن کیفیت تو ادامهی کار.
مطلبی دیگر از این انتشارات
خلاصه مختصر مفید GoLang (پارت دوم)
مطلبی دیگر از این انتشارات
Go Developer Roadmap part 2
مطلبی دیگر از این انتشارات
پیاده سازی یک سرویس قابل تست در Golang - قسمت ۱