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脚本可以实现很多功能(openresty),已经是运维的一大利器。此外要会一些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框架做对比,其它的技术框架拥有更多的解释。

servlet其实就是一个cgi,不过被封装了,使用更加方便而已。再之后,PHP、JSP、.ASP这些后端模板渲染技术出现了,其原理主要基于对脚本的转译,实际还是类似cgi程序的结构。(这里不细究各个技术出现的具体时间,只给出大概演变脉络)//TODO,后续会对这个部分继续补充完善。

随着应用的不断复杂化,简单的cgi程序结构或者说后端渲染脚本已经无法满足业务的需求了。工程师开始将系统进行分层,著名的MVC思想出现了。

曾经,所有的代码都写在一起,Html中东嵌西嵌,包含着CSS样式、Js脚本、后端脚本,后端脚本中还耦合着访问数据库的代码,数据库与实体的映射代码。这一点,使用过JDBC的应该深有体会。JDBC查询的结构只是结果集QuerySet,还要对结果集进行处理才能映射到实体对象,而每次都要去做这个结果集映射实在太麻烦了。业务的代码和访问数据库的代码没有做到隔离,两个模块可能同时要用到一个实体,那么代码无法复用。业务的代码也未得到有效的复用。如果这些都只是复用的问题的话,那么路由的复杂性就更让人头疼了,一堆的if else、switch在路由中出现(Servlet),代码变得难以维护。有聪明的工程师,做出了改进:

  • 第一点,对系统进行分层
    所有的实体都可以独立出一个个类,将访问数据库实现实体映射的类也独立出来,将实现业务逻辑的一个个类也独立出来,将路由控制独立出来。于是系统被分成了多个不同的层级,实体层作为Model/Entity,实体和数据库映射层作为DAO/Repository,操作实体实现业务逻辑的作为Service,实现路由控制页面跳转的作为Controller,而一系列JSP页面作为视图层View。这就是大名鼎鼎的MVC结构。MVC有结构统一,易于维护,实现了代码复用等等优点。现在的MVC并不是简单的只含有Model、View、Controller,而是随着系统的不同而改变层级。通常在Java开发中,主要划分为四层,由高到低分别是,controller,service,dao,entity。而更详细的,还可以增加,权限过滤层,异常处理层,缓存层,Bean组件层,DTO数据传输对象层,VO视图呈现对象层,POJO-Java简单对象层,等等。
  • 第二点,数据库和实体映射实现
    传统我们访问数据库的流程是,1、获取连接,2、获取语句句柄,3、设置SQL语句,4、获取结果集,5、遍历结果集映射数据到实体中,6、关闭连接、语句句柄、结果集。
    以下代码是一JDBC查询的封装方法,可以清晰的看到上面的过程
    /**
     * 选择多行
     *
     * @param sql sql
     * @return JSONArray
     * @throws SQLException e
     */
    @Override
    public JSONArray select(String sql) throws SQLException {
        Connection connection = dataSource.getConnection();
        Statement statement = connection.createStatement();
        ResultSet resultSet = statement.executeQuery(sql);
        JSONArray jsonArray = ResultToJson.resultSetToJsonArray(resultSet);
        release(connection, statement, resultSet);
        return jsonArray;
    }

这种方式有以下缺点:1、资源浪费,因为每次申请连接都需要耗费大量的资源,不管是数据库那里还是连接方,连接仅仅使用了一个查询就被释放,造成了资源浪费。2、代码冗余严重,在代码中充斥着重复的样板代码,(上述代码直接映射数据到JSON,倒还好一些,如果要映射到实体,就会出现大量的样本代码),程序员觉得这样不行,于是ORM框架出现了。

对象关系映射(Object Relational Mapping)简称为ORM,是当今web开发中无法分离的一部分,它实现了从抽象的关系到和现实世界更靠近的对象之间的转换。简单的说,就是把数据库的表映射到对象。Java中常常会用到的ORM框架有,MyBatis,Hibernate,Spring Templete,Spring Data JPA。如果有系统学习Java的,想必已经很熟悉这些了。ORM框架的主要目的就是少写代码,使代码好维护。

2020年5月6日 09点16分
先写到这,,

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值