Пример пакета сервера на Golang

в 7:38, , рубрики: Программирование

В языке Go, по сути, есть две основных сущности: исполняемые файлы, и пакеты. В этой статье предлагаю рассмотреть вторую на небольшом примере.

Пакет — это библиотека функций и структур. По своему назначению она напоминает стандартные, всем хорошо известные, линкуемые библиотеки. Пакет в Go определяет область видимости. Если название переменных или структур начинается с маленькой буквы, то они локальные (область видимости пакета), если с большой, то экспортируемые. Локальные структуры и функции можно использовать только внутри пакета, глобальные внутри и вне пакета. Данную особенность легко понять на примере работы с пакетом json, входящей в состав стандартных библиотек языка.

Подобный код будет возвращать ошибку.

type Link struct {
	name  string
	url string
	title string
	class string
}

links := make(map[string]Link)
if err = json.Unmarshal(response, &links); err != nil {
    return err
}


Дело в том, что, что мы используем функции из пакета json, передавая структуру c полями локальной видимости (в функции Unmarshal к полям структуры Link просто нет доступа).

Код пакета должен располагаться в соответствии с его именем, то есть если пакет называется ru/sezn/server, то файлы *.go будут находиться в папке server, которая будет подпапкой sezn и ru.

Рассмотрим пакет простого веб-сервера, который используется в наших веб приложениях: http://sezn.ru/ и http://hashcode.ru/

В языке Go существует своя собственная библиотека представляющая http-сервер. Поскольку большинство разработчиков имеют более одного сайта, использовать напрямую встроенный http-сервер не получится (в системе есть только один 80 порт). Для запуска мы будем использовать модуль fastCGI веб-сервера Apache2. Ниже приводится файл настройки виртуального хотса приложения.

<VirtualHost *:80>
   ServerAdmin webmaster@hashcode.ru

   DocumentRoot /path/to/bin/
   ServerName sezn.ru

   ErrorLog /var/log/apache2/sezn_error.log
   LogLevel warn
   CustomLog /var/log/apache2/sezn_warning.log combined

   AddHandler fastcgi-script our_bin
   FastCgiExternalServer /path/to/bin/our_bin -host 127.0.0.1:3489

   RewriteEngine On
   RewriteCond %{DOCUMENT_ROOT}%{REQUEST_FILENAME} !-f
   RewriteRule ^(.*)$ /our_bin [QSA,L]
</VirtualHost>

Пакет сервера реализован в виде простой структуры с набором методов для обработки запросов файлов, регистрации вьюшек, проверки “капчи” и так далее. В основе системы роутинга мы используем библиотеку Gorilla (http://www.gorillatoolkit.org/). Для поддержки капчи была взята библиотека https://github.com/dchest/captcha.

Основная структура сервера:

type Server struct {
	Router    *mux.Router
	Transport string
	Addres    string
	MediaPath string
	CachedFiles map[string]*CachedFile
	fn404     func(w http.ResponseWriter, r *http.Request)
}

Для более быстрой работы мы используем кэширвоание всех файлов в папке с медией.

type CachedFile struct {
	FileName  string
	FullPath  string
	FilePath  string
	FileExt   string
	FileData  []byte
}

Сервер реализует следующий публичный интерфейс.

AddNamedHandler(pattern string, handler func(http.ResponseWriter, *http.Request), name string)
AddHandler(pattern string, handler func(http.ResponseWriter, *http.Request))
Reverse(name string) *mux.Route
Run() error
SetRootMediaPath(path string)
Set404ErrorHandler(fn func(w http.ResponseWriter, r *http.Request))
ServeHTTP(w http.ResponseWriter, r *http.Request)

А таже интерфейс с областью видимости пакета.

cacheFiles()
renderFile(w http.ResponseWriter, filename string) error

Во время инициализации, мы лишь создаем основные структуры.

func InitServer(transport, addres string) *Server {
	server := &Server{Router: &mux.Router{}, Transport: transport, Addres: addres}	
        return server
}

Затем, по ходу инициализации модулей, основное приложение добавляет именованные/не именованные функции обработчики вызывая метод AddNamedHandler или AddHandler.

Код функции регистрации обработчика.

func (server *Server) AddNamedHandler(pattern string, handler func(http.ResponseWriter, *http.Request), name string) {
	server.Router.HandleFunc(pattern, handler).Name(name)
}

Для запуска сервера необходимо вызвать метод Run. В нем мы кэшируем медиа файлы (к вопросу о кэшировании http://meta.hashcode.ru/questions/1158/) и регистрируем обработчик fastCGI протокола.

func (server *Server) Run() error {
	server.cacheFiles()
	l, err := net.Listen(server.Transport, server.Addres)
	if err != nil {
		return err
	}

	fcgi.Serve(l, server.Router)
	return nil
}

До того, как будет вызван метод Run, необходимо установить обработчик, который будет вызваться если запрашиваемый файл не будет найден.

func (server *Server) Set404ErrorHandler(fn func(w http.ResponseWriter, r *http.Request)) {
	server.fn404 = fn
	server.Router.NotFoundHandler = server
}

Здесь есть тонкий момент, мы регистрируем обработчик ошибки 404, передавая библиотеке Gorilla наш сервер. Сервер реализует интерфейс обработки HTTP запросов. За это отвечает метод ServeHTTP.

func (server *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	err := server.renderFile(w, r.URL.Path)
	if err != nil {
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		w.WriteHeader(http.StatusNotFound)
		server.fn404(w, r)
	}
}

Мы регистрируем обработчики всех вьюшек в системе роутинга библиотеки Gorilla. Если обработчик не найден, то мы пробуем найти файл, с таким именем. Если файла нет, то вызываем обработчик ошибки 404.

Метод считывания медиа файлов в память:

func (server *Server) cacheFiles() {
	server.CachedFiles = make(map[string]*CachedFile)
	dir, _ := filepath.Abs(server.MediaPath)
	
 	filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
		if info.IsDir() {
			return nil 		
		}
		file, err := ioutil.ReadFile(path)
		filePath := strings.Replace(path, dir, "", -1)
		server.CachedFiles[filePath] = &CachedFile {
			FileName: info.Name(),
			FullPath: path,
			FilePath: filePath,
			FileExt: filepath.Ext(path),
			FileData: file,
		}
		return nil
	});
}

В момент запроса файла, нам необходимо лишь посмотреть вхождение соответствующего пути в словарь с файлами и сформировать ответ. Если файла с запрашиваем адресом не найдено, мы попробуем найти его на диске.

func (server *Server) renderFile(w http.ResponseWriter, filename string) error {
	var file []byte
	var ext string
	var err error 
	
	if cachedFile, exist := server.CachedFiles[filename]; exist {
		file = cachedFile.FileData
		ext = cachedFile.FileExt
	} else {
		file, err = ioutil.ReadFile(server.MediaPath + filename)
		if err != nil {
			return err
		}
		ext = filepath.Ext(server.MediaPath + filename)
	}
	
	if ext != "" {
		w.Header().Set("Content-Type", mime.TypeByExtension(ext))
	}
	if file != nil {
		w.Write(file)		
	} 

	return nil
}

Имя “server” далеко не уникальное. Мы размещаем пакеты используя нотацию Java. Когда мы захотим экспортировать пакет, полное имя будет выглядеть как ru/sezn/server. Для создания пакета мы используем программу поставляемую с языком.

go get ru/sezn/server

Вот в принципе и все. Буду рад ответить на вопросы в комментариях к посту или в соответствующей ветке на ХэшКоде (http://hashcode.ru/questions/tagged/go/).

P. S. Данный пакет переделывался в последний раз под Go RC1 и может не работать с более поздней/ранней версией языка.

Автор: DevExpert

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js