Web技術溯源&進入微服務

前言

說學微服務說了一年半了,一直都沒有真的去做,我是要反思的。
其實微服務的基礎理論、結構,思想和意義都已經瞭解的很熟悉了,所差的就是實際的應用學習。選擇學習的目標是spring cloud。dubbo太大太繁雜,而且應用不如cloud廣泛。從長遠來看,spring cloud的發展空間也更大。

spring cloud構建微服務的基礎是spring boot,要學習spring cloud之前,要熟悉spring boot。要熟悉、深入spring boot就要熟悉spring理論和使用、SSM、SSH兩個框架,除了熟練SSM、SSH之外還需要掌握一些數據庫使用,MySql大衆數據庫必須要掌握,此外redis做緩存、mongodb做非關係數據庫也需要懂一些。爲了提高系統性能併發和消息隊列也需要熟練掌握。這些的基礎是必要的,不能爲了微服務而微服務。個人認爲,簡單的系統儘量用簡單的技術實現。微服務提供的是橫向擴展功能的方法。縱向實現功能還是需要依賴基礎技術。系統實現的怎麼樣不是取決於用了多高深的技術,而是取決於能否解決業務需求。

後端後端,也離不開運維,基礎的tomcat、apache要會使用,nginx很方便,結合lua腳本可以實現很多功能,已經是運維的一大利器。此外要會一些shell腳本,方便實現一些頻繁又重複的工作(常見應用部署)。微服務的部署基礎離不開docker容器,需要掌握docker的使用,並會撰寫DockerFile。docker-compose可以方便docker容器的啓動,也必須要掌握。再進階一些,掌握Kunbenetes這個容器管理技術,那就不擔心上線之後擴容的問題了。

微服務帶來了架構的複雜性。這使得部署工作不再像之前那樣上傳一個war包就可以解決。我們需要用一個個容器部署一個個服務,並讓這些容器開啓正確的端口,連接好對應的數據庫。這些複雜性促使我們走向了自動的構建和集成,maven是簡單的構建,它使用簡單,但需要定製功能是變的複雜,XML不適合閱讀。gradle是新一代的構建工具,依賴於groovy實現的dsl腳本給它提供了很大的靈活性,直接通過腳本的方式來定製你的構建任務,gradle還在上升期,缺點是性能不行,但現在使用也比maven方便,要深入gradle需要熟悉groovy腳本還有一些自動集成的思想。再之後呢?我們需要一個工具來做更大的集成(系統與系統,大模塊與大模塊的集成),這時候jenkins就派上用場了,jenkins是開源的自動集成平臺,通過它可以輕鬆的實現測試、打包、部署一條龍,通過監聽git服務,這一系列都是自動進行的,完善的郵件通知插件,可以及時的反饋給你測試的結果和集成的結果,有jenkins和沒jenkins是兩種境界。jenkins是效率革命。

多人開發則還需要git服務器,搭建私服不可或缺。git管理常用gitlab,輕量級別可以用git web。

如果你擅長前端就更好了,但是現在前端可也複雜的很。nodejs、webpack、babel這些就是攔路虎,再深入一些需要學習es6、typescript、vuej、react、h5、微信小程序,一些MVVM框架結構,還有數之不盡的庫和組件。

所幸的是雖然我遲遲沒有進入微服務,以上這些基礎卻搭的七七八八。

一、架構演變

要學習一個技術,還是要從這個技術爲什麼出現來學習起,通過了解技術爲什麼出現,可以明白該技術是爲了解決什麼,該技術和之前技術的關係。通過這種過程可以加深對技術的理解,構建更加紮實的知識體系。

微服務爲什麼會出現?是因爲傳統的技術架構已經無法滿足業務的快速的發展。微服務它不是一個具體的框架,它只是一種架構思想,不具體映射在某一門語言,也不僅僅一種解決方案,平時我們瞭解到的spring cloud 和dubbo這兩個java微服務框架只是其中一種,其它的語言也有自己的微服務框架,如go的go-micro,python的
nameko。

在最遠古時期,人們是怎麼寫web的呢?在http早期,主要以純靜態數據爲基礎,當時帶寬有限,個人電腦也還沒有普及開來,人們僅僅用http來呈現一些簡單html頁面。

後來隨着人們的物質生活漸漸無法滿足精神要求,人們便催生了更加複雜的網頁技術,如css來美化html頁面,一些腳本語言如javascript、actionscript來實現動態的網頁。這裏指的動態是動態的網頁,比如實現點擊特效,實現圖片輪轉。後來這些也無法滿足人們的需求了,人們希望網頁能夠處理更加複雜的功能,來滿足線上的一些業務,這些業務原本來自線下,線下的劣勢就是信息交換成本高,信息渠道窄小,而這恰恰是線上可以解決的。於是正在意義的“動態”網頁出現了,這裏的動態指的是網頁的內容是可以更改的,依據不同的用戶可以呈現不同的內容,同時可以持久化收集到的數據,這一個改變極大的促使了web的發展,也把基於瀏覽器和服務器的BS架構提到了和傳統的客戶端服務器的CS架構的同一地位上。但這時因爲帶寬受限、並且網頁還比較簡陋,CS架構仍然還是比較主流的方案。

動態網頁是怎麼實現的呢?還要從HTTP協議說起,比較web技術的源泉就是HTTP協議,一切的web框架,哪怕玩出花來,其根本也還是離不開HTTP協議。

HTTP協議是TCP上層的協議,屬於應用層,HTTP的實現是要基於TCP的,TCP是一個可靠的長連接通訊協議,屬於傳輸層,常常用來做通訊和遊戲服務,實時服務等對實時性要求高的應用。傳統的CS架構也很多是用TCP協議實現的。HTTP是怎麼實現的呢?簡單的說就是,我需要請求你的個人博客頁面,於是我向你的服務器的80端口通過TCP發送一條消息,包含了HTTP請求頭和請求體,你收到了這條消息,於是向我回復你的網站頁面,其中包括html、css、js腳本,再之後你的服務器就主動的斷開了TCP連接。實現了根據不同的請求頭回復不同的頁面的應用就是web server,常見的tomcat、apache、nginx、IIS就是web服務器。

HTTP是一個無狀態的協議,它每次請求都像一次全新的請求,每個請求處理完就會主動斷開(後來有了keep-alive長連接),上一次請求和下一次請求沒有關聯,對於一臺服務器而言,這種請求方式節省了帶寬,服務器避免維護過多的連接,減少了系統資源的佔用,同時又能爲更多的用戶提供服務。但也正是因爲這種請求完就斷開的原因,導致HTTP連接的建立成本很高。這種高是相對TCP連接而言的,後來有了keep-alive已經好了很多。HTTP的意義在於,它提供了一種設計良好的信息傳輸方式,使得服務器可以爲更多的用戶提供服務。專家設計的HTTP協議是比較精細的,其中很多地方存在着過度設計,但是像HTTP協議這種理論從何談起過度設計呢?理論要做到自洽,就要囊括方方面面。

我們有時候需要自己思考,這個技術出現的目的是什麼?它是要解決什麼問題?別人用這個技術,我是否就要用?正如王爽老師在《彙編語言》提出的:
在這裏插入圖片描述

HTTP出現的目的就是爲了更好的做信息的傳輸,相比TCP,它更具體,具體在它的MIME-TYPE,各種各樣的傳輸類型,HTML、CSS、JS只是其中最被使用的幾種,還可以是JSON、XML、圖片、文件、視頻、語音等等。具體在它的請求頭,響應報文,通過這些方式將簡單的TCP複雜了無數倍,來實現更多的功能。如果TCP是一個導線,HTTP就是一個顯示器。HTTP離不開TCP,但是不用HTTP我們也可以實現web功能,我們可以實現一個解析部分HTTP頭的程序來實現一個精簡版HTTP服務器,也可以完全脫離HTTP,通過定製TCP來實現自己的應用層協議,但是通過定製TCP實現的服務器就無法實現和瀏覽器交互了。

實現動態的前提是要能收集用戶的數據,一般通過GET請求附加參數,或者表單POST請求附加請求體,後來又出現了AJAX來實現異步的請求。將收集到的數據存入數據庫之後就完成了持久化,下一次該用戶再請求就可以通過分析數據庫的數據來爲該用戶提供定製化的服務。

這裏存在一個問題就是,HTTP是無狀態的。如何知道每次請求的是誰呢?這個有簡單的思路,我每次請求附帶上我的姓名,服務器不就知道我是誰了嗎?但是每次請求都要附帶實在是太麻煩了,這種麻煩的事能不能有人替我做了呢?於是cookie機制出現了。cookie就是這種你每次請求都會帶上的東西。當然它更復雜,包括過期機制、js限制、域名機制、加密機制等等,而且傳輸的信息也不止是字符串這麼簡單,而是傳輸一個個易於解析的Key-value,就像properties文件那樣。

session機制基於cookie機制。後來又出現了更高級的令牌機制,JWT。這裏不做細談。感興趣可以看我的這篇博客session、token、cookie詳解,和java JWT工具類

解決以上一系列問題之後,想實現動態網頁就很簡單了。那就是脫離web server的襁褓,通過編程語言實現一個http server,這個server具有http解析和響應功能。用極簡的語言描述這個http server要做的事,就是

  • 1、監聽80端口,等待用戶的連接,這時候程序是阻塞的
  • 2、解析HTTP請求頭
  • 3、根據請求頭所攜帶的信息,如請求方法、路徑、GET參數、POST體、Cookie等信息,去獲取你想返回的數據,比如請求/index.html,就讀取index.html文件的內容,GET參數包含id,就獲取數據庫對應ID的文章,並將文章的信息返回,至於你想不想對文章的信息做加工是你的事情,無論你想直接把數據當作字符串返回、還是轉換成JSON格式返回、還是渲染到HTML模板返回都行。根據cookie信息判斷這個用戶是誰,要給他返回什麼內容。在這個地方你能賦予你的Http server極大的自由性,你可以返回牛頭不對馬嘴的東西,也可以返回404告訴前端你沒有這個東西(也許你實際上有),或者告訴前端你的服務器崩潰了(500),或者告訴前端這個東西你沒資格訪問(443),這一切都是你的自由權利。這整個第三部分也就是CGI應用程序實現的。而除了第三部分之外的部分,CGI服務程序HTTP Server實現了。
  • 4、根據HTTP規範返回響應報文,並附帶你想返回的數據
  • 5、斷開與該用戶的連接

實現這個服務器是很簡單的,這裏使用優美的python實現,當然用任何一種可以做IO的語言也可以實現這個http server,

# -*- coding:utf-8 -*-
import socket
import os
import re

"""
一個靜態web服務器,簡單的http server
作者:衡與墨 www.hengyumo.cn
2019-7-13 新建
"""

def parseRequest(request_content):
    """ 解析請求數據
    """
    request_split = request_content.split('\r\n')

    # 請求行
    method, url, http_version = request_split[0].split(' ')

    # 請求頭
    request_headers = {}
    for i in range(1, len(request_split)):
        if request_split[i] == '':
            break
        else:
            key, value = request_split[i].split(': ')
            request_headers[key] = value

    # 請求數據
    request_body = []
    for i in range(2+len(request_headers), len(request_split)):
        request_body.append(request_split[i])

    request_body = '\r\n'.join(request_body)

    # 打包成請求字典
    request = {
        'addr': addr,
        'method': method,
        'url': url,
        'http_version': http_version,
        'headers': request_headers,
        'body': request_body
    }

    return request

def get_files(files_dir='.'):
    """ 獲取某個路徑所有的文件路徑
    """
    files_dir = os.path.join(os.getcwd(), files_dir)
    files_all = []
    def get_files_(files_dir='.', r_path=''):
        if files_dir[-1:] != '/':
            files_dir += '/'
        files = os.listdir(files_dir)
        for file in files:
            file_path = os.path.join(files_dir, file)
            if os.path.isdir(file_path):
                get_files_(file_path, file)
            else:
                if r_path:
                    files_all.append('%s/%s' % (r_path, file))
                else:
                    files_all.append(file)
    get_files_(files_dir)
    return files_all

def loadStatic(static_path='static'):
    """ 加載靜態文件
    """
    statics = get_files(static_path)
    static_path = os.path.join(os.getcwd(), static_path)
    statics_dict = {}
    # 設置下列文件後綴使用二進制讀取
    byte_files_suf = ('jpg', 'png')
    for file_name in statics:
        file_suf = file_name.split('.')[-1]
        file_path = os.path.join(static_path, file_name)
        if file_suf in byte_files_suf:
            file = open(file_path, 'rb')
        else:
            file = open(file_path, 'r')
        statics_dict['/'+file_name] = file.read()
        file.close()
    return statics_dict


if __name__ == '__main__':
    # 加載靜態文件
    statics = loadStatic()

    # family: 套接字家族可以使AF_UNIX或者AF_INET
    # type: 套接字類型可以根據是面向連接的還是非連接分爲SOCK_STREAM或SOCK_DGRAM
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 綁定地址(host,port)到套接字, 在AF_INET下,以元組(host,port)的形式表示地址。
    s.bind(('localhost', 8080))
    while(True):
        # 開始TCP監聽。backlog指定在拒絕連接之前,操作系統可以掛起的最大連接數量。
        # 該值至少爲1,大部分應用程序設爲5就可以了。
        s.listen(3)
        # 被動接受TCP客戶端連接,(阻塞式)等待連接的到來
        conn, addr = s.accept()
        # 接收TCP數據,數據以字符串形式返回,bufsize指定要接收的最大數據量。
        # flag提供有關消息的其他信息,通常可以忽略。
        request_content = conn.recv(1024)

        # 沒有內容的連接,防止keep-alive導致錯誤斷開
        try:
            request = parseRequest(request_content)
        except e:
            conn.close()
            break

        mime_type = {
            'jpg': 'image/jpeg',
            'png': 'image/png',
            'html': 'text/html'
        }
        file_suf = request['url'].split('.')[-1]
        if file_suf in mime_type:
            content_type = mime_type[file_suf]
        else:
            content_type = 'text/html'

        response = 'HTTP/1.1 200 OK\r\nContent-Type:%s\r\n\r\n' % content_type

        print request['url']

        if request['url'] in statics:
            print 'match static'
            response += statics[request['url']]

        # 給客戶端返回內容
        conn.sendall(response)
        # 關閉連接
        conn.close()

感興趣實現過程的,可以看我的這篇博客實現一個靜態web服務器、http server

在我的github還有一個動態實現版本,實現了可插拔的插件添加,語義化的路由,有微型框架的樣子,用起來就像這樣子,

	server = Server()

    # server.addPlugin(StaticPlugin()) 這樣只能添加一個插件

    # 添加多個插件
    plugins = Plugins()
    # 靜態路由支持
    plugins.add(StaticPlugin())
    # 動態路由支持
    plugins.add(RouterPlugin())

    server.addPlugin(plugins)

    # GET /hello
    server.get('/hello', lambda server: server.end('<h1>hello</h1>'))

    server.run()

感興趣可以看:https://github.com/numb-men/easy-http-server

之前提到了簡易http server模型中第3部分其實就是CGI應用程序,不知道CGI的可能有點懵,CGI現在已經很少聽到了,這也是接下來要講到的。

CGI技術是一個源遠流長的技術標準,可以稱呼其爲動態網頁的基礎,什麼是CGI?CGI(Common Gateway Interface),通用網關接口,它是一段程序,運行在服務器上如:HTTP 服務器,提供同客戶端 HTML 頁面的接口。

CGI搭建了HTTP協議到編程語言之間的橋樑,所謂網關的意思也是這個意思。常見的HTTP server都有網關功能,一般裝個插件就可以實現了。Http server替我們做了複雜工作,如響應頭解析,多線程,以及靜態網頁返回等。我們實現的CGI程序就像其中的一個功能模塊,通過設置某個路由解析到我們的CGI程序,來實現我們想要的功能。CGI程序相比自定義http server要做的事情簡單一些,但是也隱藏了太多的細節實現。大名鼎鼎的LAMP就是一個基於CGI的架構,分別指的是Linux、apache、mysql、php。其中php和apache的連接就是通過cgi實現的。遠古時期的perl cgi程序和c/c++ cgi程序也是一個原理。cgi並不是什麼高深的東西。

在這裏插入圖片描述
在apache配置文件中添加,

<Directory "/var/www/cgi-bin">
   AllowOverride None
   Options +ExecCGI
   Order allow,deny
   Allow from all
</Directory>

就可以將/var/www/cgi-bin映射到cgi執行目錄,當請求這些腳本文件時,會將其執行,將執行結果返回。

以下是一段python實現的cgi程序,可以看到http請求頭被http,server寫在了環境變量裏,然後被作爲列表輸出,熟悉java的應該會發現這個和servlet有點像,但是更腳本化。

#!/usr/bin/python
# -*- coding: UTF-8 -*-
# 來自菜鳥教程https://www.runoob.com/python/python-cgi.html

import os

print "Content-type: text/html"
print
print "<meta charset=\"utf-8\">"
print "<b>環境變量</b><br>";
print "<ul>"
for key in os.environ.keys():
    print "<li><span style='color:green'>%30s </span> : %s </li>" % (key,os.environ[key])
print "</ul>"

在這裏插入圖片描述
用c++實現同樣功能的程序:

// 來自菜鳥教程https://www.runoob.com/cplusplus/cpp-web-programming.html
#include <iostream>
#include <stdlib.h>
#include <string>
using namespace std;
 
const string ENV[ 24 ] = {                 
        "COMSPEC", "DOCUMENT_ROOT", "GATEWAY_INTERFACE",   
        "HTTP_ACCEPT", "HTTP_ACCEPT_ENCODING",             
        "HTTP_ACCEPT_LANGUAGE", "HTTP_CONNECTION",         
        "HTTP_HOST", "HTTP_USER_AGENT", "PATH",            
        "QUERY_STRING", "REMOTE_ADDR", "REMOTE_PORT",      
        "REQUEST_METHOD", "REQUEST_URI", "SCRIPT_FILENAME",
        "SCRIPT_NAME", "SERVER_ADDR", "SERVER_ADMIN",      
        "SERVER_NAME","SERVER_PORT","SERVER_PROTOCOL",     
        "SERVER_SIGNATURE","SERVER_SOFTWARE" };   
 
int main ()
{
    
   cout << "Content-type:text/html\r\n\r\n";
   cout << "<html>\n";
   cout << "<head>\n";
   cout << "<title>CGI 環境變量</title>\n";
   cout << "</head>\n";
   cout << "<body>\n";
   cout << "<table border = \"0\" cellspacing = \"2\">";
 
   for ( int i = 0; i < 24; i++ )
   {
       cout << "<tr><td>" << ENV[ i ] << "</td><td>";
       // 嘗試檢索環境變量的值
       char *value = getenv( ENV[ i ].c_str() );  
       if ( value != 0 ){
         cout << value;                                 
       }else{
         cout << "環境變量不存在。";
       }
       cout << "</td></tr>\n";
   }
   cout << "</table><\n";
   cout << "</body>\n";
   cout << "</html>\n";
   
   return 0;
}

可以看的出來CGI技術有多重要了吧,要實現各種編程語言來編寫web,離不開CGI。PS:千萬不要吹自己的語言和框架了,任意一門圖靈完備、庫完備的語言都可以做任何事情,同一種架構思想也有n種實現方式。語言只是工具,要客觀看待。我沒有暗示PHP,更沒有暗示大Java。要走進去,更要能走出來。我不喜歡在別人的抽象上做自己的抽象,那沒有什麼意義,別人費盡心思的抽象就是爲了造福廣大程序員少掉頭髮的,別把問題搞的越來越複雜了,抽象這些抽象並不能提高你的技術水平,就像研究爲什麼1+1=2一樣。

框架是什麼?之前和朋友談及這個話題,我粗淺的做了個比喻,比如某個人買了房子,這個房子沒有裝修,但是提供了自來水、電、網絡的接口,也有衛生間下水道和廚房下水道的出口,房間已經分隔開了,但是還沒有做裝修,還不能住人。爲了住人我們需要鋪地板,做吊頂,粉刷牆壁,買各種傢俱和電器。這些過程我們稱之爲裝修。在這個比喻中,這個沒裝修的房子就是所謂的框架。而我們裝修的過程就是開發自己應用的過程。我們的房子提供的必要的水電接口,這樣我們的房子就有水源和能源輸入,我們的房子提供了排污的管道,這樣我們的房子就有了輸出的接口,同樣還有一個個不同用途的房間和對應的門。這個房間是臥室,那個房間是書房。輸入和輸出可以對比到Http請求報文和Http響應報文。各個房間可以對比到不同的模塊。各個房間的門可以對比到不同的請求路徑或者請求方法。這裏僅用web框架做對比,其它的技術框架擁有更多的解釋。

待續,晚上繼續寫,

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章