قصة برنامج للوصول لمخدم في المكتب من اﻹنترنت

السلام عليكم

بدأت هذه القصة في عام 2016م…

لدينا في المكتب خدمة إنترنت من نوع  ADSL وطريقة إتصاله بالإنترنت تختلف عن باقي خدمات اﻹنترنت اللاسلكية مثل 3G والـ 4G حيث أن أهم ما يميزه أن رقم الـ IP المستخدم لدخول النت يمكن الوصول إليه من أي جهاز في النت وهو يمثل رقم الراوتر أي أنه public، ويمكن عمل ما يُعرف بالـ port forwarding لتحويل رقم بورت معين إلى جهاز لابتوب المتصل مع ذلك الراوتر ، وفي حالتي لدي مخدم صغير في المكتب كنت أود الوصول إليه – وباقي زملائا في العمل- من الخارج من أي مكان في اﻹنترنت. لكن المشكلة أن رقم الـ IP الخاص براوتر الـ ADSL هو غير ثابت ، يتغير مع كل إتصال مع النت، أي أنه dynamic وليس static إلا أنه public.

مثلاً رقم الراوتر اﻵن هو 41.209.65.57 وعند كتابته في المتصفح يقوم مخدم المكتب بإظهار صفحته الرئيسة. لكن غالباً عند قراءتك لهذه التدوينة سوف يتغير إلى رقم جديد بعد يوم أو يومين أو وقت أقل إذا حدث إنقطاع للكهرباء.

لحل هذه المشكلة كان يجب استخدام إسم  domain name بدلاً من استخدام الرقم المتغير، وفي كل مرة يتم ربط هذا الإسم بالرقم الجديد، فبذلك يظل اﻹسم ثابتاً بالنسبة للأجهزة العميلة، مثلاً اﻹسم كان code-server.sd وهو إسم غير موجود في النت، إنما قمت بإضافته في ملف  /etc/hosts في نظام لينكس ، أو ملف

c:\windows\system32\drivers\etc\hosts

  في نظام وندوز. ثم نقوم بكتابة الرقم المتغيير معه، وكلما يتغير الرقم نقوم بتغيير تلك المعلومة في الملف، وبالنسبة للمستخدم لا يحس بأي تغيير لأنه يستخدم الإسم code-server.sd للوصول للمخدم بغض النظر عن رقمه الحالي.

مثلاً هذه محتويات الملف /etc/hosts في جهاز اللابتوب -ونظام تشغيله لينكس- استخدمه اﻵن، لاحظوا السطر اﻷخير به عنوان ورقم المخدم الذي نتكلم عنه:

127.0.0.1       localhost   global.com
127.0.1.1       L40-laptop
#192.168.0.105   L40-laptop
192.168.1.98     raspi2
192.168.0.101  mini-server mini-server.sd
0::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
 ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters

# The following lines are desirable for IPv6 capable hosts
::1     ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters

41.209.65.57          code-server.sd

كانت فكرة الحل هي كتابة برنامج بلغة php في موقع في اﻹنترنت تابع لنا، مثلاً http://www.code.sd ليقوم بندائه المخدم في المكتب كل فترة ليعلمه برقمه الحالي، مثلاً كل ساعة، فيقوم هذا البرنامج أو صفحة php بإنشاء ملف نصي به رقم المخدم الحالي. الخطوة الثانية هي كتابة برنامج أكثر تعقيداً لقراءة هذا الرقم كل فترة ثم تحديث هذه المعلومة في ملف hosts  كما في الشكل التالي:

update-server

قُمت بتسمية البرنامج الذي سوف نضعه في جهاز المستخدم server-update واﻷجهزة هي: لينكس، وجهاز RaspberryPi، ثم لاحقاً بعد كتابة البرنامج تم اﻹحتياج إلى نُسخة تعمل في نظام وندوزأي أنها ثلاث منصات تحتاج لبرنامج تتم كتابته بلغة برمجة متعددة المنصات. كانت الخيارات هي: لغة باسكال، ولغة shell script و لغة جافا، وأخيراً لغة سي أو سي ++

بالنسبة لخيار لغة باسكال متمثلة في FPC/Lazarus كانت موجودة في لينكس وراسبري باي، لكن كانت نُسختان مختلفات من المترجم، ومكتبة التخاطب مع النت HTTP لم تكن موجودة مع المكتبة القياسية للغة، كان لابد من تثبيت ما يُعرف بالـ component وهي تحتاج إلى بيئة لازاراس كاملة أي GUI وبالنسبة لجهاز راسبري باي لم تكن هذه البيئة موجودة فقط المترجم FPC ، لذلك تم استبعادها لعدم إمكانية إنتاج ملف تنفيذي لمنصة RaspberryPi.

أما لغة shell script التابعة لنظام لينكس، كانت متوفرة في المنصتين، لكن لم تكن لدي خبرة  كافية لاستخدامها في برنامج متوسط التعقيد، حيث يحتاج لقراءة معلومة من النت ثم تحديث ملف نصي بهذه المعلومة، وحاولت استخدامها لقراءة معلومة من النت وإظهارها لكن وجدت بها صعوبة كبيرة كما أن ليس لها بيئة تطوير مشجعة. فقمت باستبعادها إيضاً.

الخيار الثالث أو ربما اﻷول كان لغة جافا، لكن لا أتذكر بالضبط لٍم لم أختارها، – كان هذا قبل حوالي ثلاثة أعوام – فهي كانت مناسبة، حيث توجد آلة جافا اﻹفتراضية في كلا المنصتين باﻹضافة إلى أن نُسخة الملف التنفيذي سوف تعمل بدون أي تعديل في نظام لينكس ونظام راسبري باي. لكن ربما إحتياجها لتلك اﻵلة اﻹفتراضية أن تكون موجودة في اللابتوب الذي نريد تشغيل هذا البرناج البسيط فيه لم يشجعني على استخدام جافا، حيث أنها مكتبة كبيرة نقوم بتثبيتها عندما يكون البرنامج يستحق تثبيت آلة جافا اﻹفتراضية، لكن ليس لمثل هذه البرامج البسيطة التي لا تزيد أن تكون عبارة عن script

لا أدري هل كانت لغة بايثون من الخيارات أم لا، حيث أن خبرتي بها قليلة جداً، لكن بها نفس مشكلة جافا حيث تحتاج لتثبيت مفسرها ومكتباته في أي جهاز نريد تثبيت هذا البرنامج به.

في النهاية  وقع الخيار على لغة سي، فهي متعددة المنصات وينتج عنها ملف تنفيذي لا يحتاج لمكتبة أو لأي منصة إضافية لتشغيله، وكنا قد درسناها في السنة الثالثة في الجامعة، أي قبل أكثر من 18 عام! وهو زمن طويل حيث كُنا في السنة الثالثة في جامعة السودان عام 1998 أي نهاية القرن الماضي. وطوال هذه الفترة لم استخدم لغة سي أو سي++ في العمل، مع أننا درسناها لمدة عام تقريباً، لكن الدراسة الأكاديمية مختلفة إلى حد كبير عن اﻹحتياج في بيئة العمل الحقيقية. مع أني عملت في مجال برامج اﻹتصالات والتي تُناسبها لغة سي وسي++ إلا أننا استخدمنا لغة أوبجكت باسكال، متمثلة في بيئة التطوير دلفي نظراً لسهولتها الكبيرة وتوفيرها لبيئة متكاملة للتطوير لم يكن ينافسها إلا بيئة فوجوال بيسك من حيث السهولة وسرعة إنتاج البرامج لكن دلفي كانت تتفوق بأداء أعلى يقارب أداء برامج لغة سي.

في كثير من المرات حاولت إعادة دراسة لغة سي والبحث عن استخدامها لكن كل مرة أفشل إما بسبب عدم وجود بيئة متكاملة مقارنة بدلفي أو بسبب عدم وجود تطبيق مناسب لاستخدامها. لكن هذه المرة وجدت تطبيق سهل مناسب وبيئة مناسبة NetBeans فقررت استخدامها لإنجاز هذا البرنامج البسيط.

قمت بعمل تجارب لقراءة معلومة صفحة من النت وقد احتجت لتثبيت مكتبة إضافية وهي مكتبة curl كذلك تطلبت طريقة معينة للترجمة لتضمين هذه المكتبة أثناء الترجمة، أي أن استخدام لغة سي لم يكن بالسهولة المناسبة مع سهولة البرنامج المطلوب.

في النهاية تمكنت – بفضل الله- من كتابة البرنامج وتشغيله بنجاح في بيئة لينكس، ثم إعادة ترجمته في جهاز الراسبري باي – حيث يوجد به مترجم سي أيضاً- فأصبحت هُناك نُسختان تنفيذيان من البرنامج تعملان مباشرة في أي جهاز يحتوي على لينكس أو راسبري باي ولا يحتاج البرنامج للغة سي أو أي مكتبة منها. لكن البرناج كان معقداً قليلاً وذو مقروئية متدنية تصّعب اﻷمر على من يريد فهمه أن تعديله.

في بداية العام الذي يليه قررت لسبب ما إعادة كتابة نفس البرنامج باستخدام لغة البرمجة Go حيث بدأت تعلمها في هذا الوقت، وكان أول تطبيق عملي هو برنامج server-update . فقمت بالبحث عن قراءة معلومة من صفحة في النت بهذه اللغة، وقد كانت سهلة ولا تحتاج لمكتبة خارجية إنما كل ما احتجت إليه وجدته في المكتبات القياسية لها. ولغة Go بها نفس ميزة لغة سي، حيث أنه ينتج عنها ملف تنفيذي يعمل في منصة التشغيل المستهدفة دون الحاجة لمكتبات إضافية أو منصة إضافية لتشغيل البرنامج. وفي النهاية كان البرنامج أكثر سهولة وأكثر وضوحاً من سابقه المكتوب بلغة سي. وكان أحد الميزات المهمة في مترجم لغة Go أنه يدعم ما يُعرف بالـ cross-compilation أي يمكن إنتاج برامج تنفيذية لمنصات مختلفة من منصة واحدة، مثلاً من بيئة لينكس يمكن إنتاج برامج تنفيذية لبيئة وندوز وراسبري باي وماكنتوش وغيرها، دون أن تقوم بنقل مصدر البرنامج إلى منصة أخرى لترجمتها وهذه الميزة لم أجدها في لغة سي أو لغة باسكال – مع أن مترجم فري باسكال به هذه الميزة لكن عملياً تطبيقها صعب.

بعد هذا البرنامج كانت إنطلاقة بالنسبة لي لاستخدام لغة Go في العمل في برامج صغيرة ثم برامج أكثر تعقيداً ومازلت استخدمها إلى اﻵن وكتبت بها عدد من البرامج التي تعمل اﻵن في ضمن عدد من اﻷنظمة. وكانت بديل جيد لعدد من برامج جافا حيث أن برامج Go أسرع في التنفيذ ولا تستهلك موارد كبيرة مثل الذاكرة إلا أن برنامجها التنفيذي أكبر بكثير من نظيرتها لغة جافا ولغة سي. فقد كان حجم الملف التنفيذي لبرنامج server-update حجمه 15 كيلوبايت فقط أما نظيره المكتوب بلغة Go فكان حجمه حوالي 6 ميغابايت!

لم أرجع أو أفكر للرجوع لاستخدام لغة سي بعد هذا البرنامج ولم أفكر باستخدام لغة سي++ واﻵن كلا اللغتين في تراجع مستمر بالنسبة للاستخدام حيث يميل كل من يريد الدخول للبرمجة لتعلم لغة أكثر سهولة كذلك فإن الشركات تستخدم اللغات الحديثة فقط في المشروعات البرمجية الجديدة. ويتم استخدام لغة سي وسي++ في المشاريع القديمة التي لها عشرات السنين والتي يتعذر نقلها إلى لغة برمجة جديدة، كذلك فإن لغة سي تُستخدم في اﻷنظمة المُدمجة والمعالجات الدقيقة micro-controllers.

وقبل الختام هذا مصدر البرنامج بلغة سي:

/* 
 * File:   main.c
 * Author: motaz
 *
 * Created on February 10, 2016, 1:35 PM
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <curl/curl.h>

char *server;
char *localserver;

function_pt(void *ptr, size_t size, size_t nmemb, void *stream){
    
  //server = malloc(size);
    //strncpy(server, ptr, size);
    server = ptr;
}

// Local net
function_pt_local(void *ptr, size_t size, size_t nmemb, void *stream){
    
    //server = malloc(size);
    //strncpy(server, ptr, size);
    localserver = ptr;
}

int getURL(void){
    
  CURL *curl;
  curl = curl_easy_init();
  if(curl) {
    printf("Reading from site..\n");
    curl_easy_setopt(curl, CURLOPT_URL, "http://code.sd/..file.txt");
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 20L);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, function_pt);
    curl_easy_perform(curl);
    curl_easy_cleanup(curl);
  }
    return 0;
}


void copyFiles(){
    
    FILE *fp;
    FILE *output;
    char * line = NULL;
    size_t len = 0;
    ssize_t read;   
    

    fp = fopen("hosts.tmp", "r");
    output = fopen("/etc/hosts", "w");
   
    while ((read = getline(&line, &len, fp)) != -1) {
        
        fputs(line, output);
    }
    fclose(fp);
    fclose(output);    
}

int main(int argc, char** argv) {

    FILE *fp;
    FILE *output;
    char * line = NULL;
    size_t len = 0;
    ssize_t read;   

    char *myserver;
    char* ok = NULL;
    
    getURL();
    myserver = server;

    printf("Server: [%s] \n", myserver);
    fp = fopen("//etc//hosts", "r");
    output = fopen("hosts.tmp", "w");
    int found = -1;
   
    while ((read = getline(&line, &len, fp)) != -1) {
        
        char* ptr=strstr(line, "code-server");
        if (ptr != NULL) {
            fputs(myserver, output);
            fputs("      ", output);
            fputs("code-server.sd\n", output);
            found = 1;
            printf("Found\n");
                    
        }
        else {
          fputs(line, output);
        }
    }
    
    if (found == -1){
       fputs(myserver, output);
       fputs("    ", output);
       fputs("code-server.sd\n", output);
       printf("New\n");
    }
    fclose(fp);
    fclose(output);
    copyFiles();
   
    return (EXIT_SUCCESS);
}

وهذا نفس البرنامج بلغة Go ويزيد عليه إضافة ليعمل في بيئة وندوز

// server-update2 project main.go
package main

import (
	"bufio"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"runtime"
	"strings"
)

func isLinux() bool {

	return runtime.GOOS == "linux"
}

func getTempFileName() string {
	if isLinux() {
		return "/tmp/hosts.tmp"
	} else {
		return "c:\\windows\\temp\\hosts.tmp"
	}
}

func getHostsFileName() string {
	if isLinux() {
		return "/etc/hosts"
	} else {
		return "c:\\windows\\system32\\drivers\\etc\\hosts"
	}
}

func main() {
	ip := getIP("http://code.sd/..file.txt")
	if (ip != "") && (len(ip) < 100) {
		readIntoTemp(ip)
		copyFile(getTempFileName(), getHostsFileName())
	} else {
		println("Unable to read IP")
	}
}

func getIP(url string) string {

	resp, err := http.Get(url)
	if err == nil {

		body, err2 := ioutil.ReadAll(resp.Body)
		if err2 == nil {
			line := string(body[:])
			return line
		} else {
			println("Error: " + err2.Error())
		}
	}
	return ""
}

func readIntoTemp(ip string) {

	file, _ := os.Open(getHostsFileName())
	destfile, _ := os.Create(getTempFileName())

	var found bool = false
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		line := scanner.Text()
		if strings.Contains(line, "code-server.sd") {
			println("Found, IP = " + ip)
			found = true
			line = ip + "          code-server.sd"
		}
		destfile.WriteString(line + "\n")
	}
	if !found {
		println("New entry for code-server.sd")
		destfile.WriteString(ip + "       code-server.sd\n")
	}

	file.Close()
	destfile.Close()
}

func copyFile(source string, dest string) bool {

	sourceFile, err := os.Open(source)
	if err == nil {
		defer sourceFile.Close()
		destFile, err2 := os.Create(dest)
		if err2 == nil {
			defer destFile.Close()
			io.Copy(destFile, sourceFile)
		} else {
			println("Error: " + err2.Error())
		}
	}
	return err == nil
}

الجدير بالذكر أنه بعد فترة تعطل الراوتر، فاشتريت رواتر جديد من نوع DLink فوجدت به خدمة إضافية من الشركة المُنتجة له وهي خدمة Dynamic DNS حيث قمت بعمل حساب عندهم واختيار إسم دومين جديد ثم ربط هذا الحساب بالراوتر، فيقوم الراوتر بتحديث العنوان كلما تغير رقم الـ IP الخاص به، وبذلك اصبح هُناك حل سهل لا يحتاج لبرمجة أو توزيع برنامج للمستخدمين. لكن بقي البرامج الذي قمت بكتابته يعمل ليكون كإحتياطي في حالة تعطل الراوتر أو توقف الخدمة المجانية التابعة لشركة DLink

 

 

الكاتب: أبو إياس

مهندس برمجيات

رأيان حول “قصة برنامج للوصول لمخدم في المكتب من اﻹنترنت”

  1. شكرا اخ ابو اياس على المقال و التجربة الجميلة.
    حدثت معي تجربة مشابهة سنة 2000 حيث كان الأتصال المتوفر هو عن طريق ال dialup فقط وكما تعلم كان يوفر عنوان public ip ولكنه يتغير في كل اتصال جديد، فقمت بكتابة برنامج بلغة دلفي لإرسال العنوان الجديد عبر الإيميل مع كل اتصال جديد، و في الطرف الآخر كتبت برنامج لقراءة الايميل بصورة آلية للحصول على العنوان الجديد.

أضف تعليق