分类 互联网编程 下的文章

Ура!是HTTP/2!

几个月之前就想给站点加上HTTPS了,我这么个无名小站都被强制上过一次广告(看自己网站的时候发现的= =)。万一变成人家的支付宝红包/淘宝活动/并夕夕推广重灾区了怎么办。直到今天才下定决心来解决这件事情,唉,拖延症病入膏肓啊。

初次接触到HTTP/2这个技术是某一次在看某v开头(咳咳)的魔法工具的时候,见到了Akamai公司做的一个示例。至于Akamai是个啥公司呢,接触过烂橘子的朋友肯定知道,这是一家CDN提供商,提供的服务主要是网络资源的托管一类的。(举个类似的例子,七牛云?)再加上Wappalyzer里面那个蓝色的小闪电看起来好炫!于是就自己动手,丰衣足食了。

其实整个过程中步骤十分简单,大致可以描述如下:

  1. 下载用于申请免费SSL证书的certbot-auto脚本;
  2. certbot-auto -d <domain-name>用于给指定域名申请SSL证书,如果用的是nginx,而且配置文件不是放在/etc/nginx.conf的环境下的话(LNMP说的就是你),可以选择在certbot-auto后添加certonly参数来表示只需要该脚本来申请证书,配置什么的由自己来;
  3. ssl on语句告诉nginx该server需要启用SSL协议;
  4. 如果用的是certbot-auto certonly的话,申请成功后会把证书的地址告诉用户,将该地址添加到nginx对应server的配置中即可。ssl_certificatessl_certificate_key对应的分别是证书的公钥和私钥,使用certbot-auto生成后两者的名字对应为fullchain.pemprivkey.pem,记得不要搞错了;
  5. listen参数的端口从80改为443,再在后面添加ssl http2参数,用于启用HTTP/2;
  6. 新建一个server,监听相同的地址的80端口,然后加一句:
    return 301 https://$server_name$request_uri;

    以使得访问该网站的用户都强制使用HTTPS。当然,如果浏览器支持HTTP/2的话,会自动启用该协议的;

  7. 可以添加这么两行:
    ssl_stapling on;
    ssl_stapling_verify on;

    来缩短SSL证书的查询时间,提高用户体验,具体信息请直接搜ssl stapling。

然后整个人就卡在了第二步,说是什么tk域名的CAA记录查不到,超时,卡了一上午,中午都差点头疼的午睡没睡着。。后来发现是云主机默认的DNS服务器有问题,加了一个Google的8888服务器问题马上解决,我tm。。。

最后顺带提一句,之前在网上看到过HTTP/2本身其实是可以不用SSL协议的,但是网络安全是大势所趋,所以基本上上了HTTP/2的都会用SSL,甚至两者变成了必然关系,很少有见到裸HTTP/2的配置教程了。

在初步接触Java Servlet之后,会发现一个HTTPServlet有这么两个附属对象:ServletConfigServletContext,两者长得很像,又有着不少相同的方法(getAttribute(), getInitParameter(), getServletName()/getServletNames()),在网上搜了一下,发现有讲述两者之间的区别的博客,但是我是在理解之后才看懂的。。

在此简述一下使用Tomcat运行Web服务的三大主要层次,我依次将其命名为Server, Application和Servlet。其中Server指的是Tomcat本身,可以理解为一台服务器上一般只会跑一个Tomcat,所有需要借助Tomcat实现的Web服务都会依赖于这一个Tomcat实例。其次就是Application了,这就是上面所说,需要借助Tomcat实现的服务。一个Web服务就是一个Web项目,内部可以有复杂的Java逻辑代码,可以包括与数据库交互,等等。一个Servlet指的是提供单一Web功能的程序单位。

Application举例说明的话就是登陆系统,从登陆到连接后台,查看数据,修改用户信息、密码等,算是一个完整的后台管理系统服务,仅靠单一的URL是很难(但是是可以)实现整个功能的,起码作为一个便于维护和扩展的项目来说,都会需要多个URL来实现不同的功能,因为这样至少光靠URL地址就能有效地组织整合不同的功能了。这是Application。而Servlet实现的是原子操作,比如说后台管理系统中的登陆,或是获取用户信息,或是修改密码这种单一而清晰的任务。

然后就可以说明了:ServletConfig是Servlet层面的,每个Servlet都有自己独立的ServletConfig;而ServletContext是Application层面的,同一服务/项目下的所有Servlet可以通过ServletContext共享数据。那为什么ServletContext不是Server层面呢?ServletContext中可以存放应用的重要数据,出于安全考虑,一是防止数据窃取,二是防止篡改,不论是无意还是有意。

再仔细看看ServletContext的方法的话,会发现有一个方法名为getServletNames(),返回的是一个Enumeration,也就是说具有多个Servlet名称。相对地,ServletConfig的方法名为getServletName(),返回一个String。这下就更加清晰了,显然一个Servlet只应该有一个名字,那么ServletContext肯定就是Servlet层面之上的了。

此外有一个小坑:声明Application级别的初始变量是在web-app建立context-param子节点,使用ServletContextgetInitParameter()方法获取;Servlet级别的初始变量则需要在web-app节点下先新建servlet子节点,再在servlet子节点下新建init-param节点定义,使用ServletConfiggetInitParameter()方法获取。其中两个param的定义结构相同,使用param-name子节点定义变量名,param-value子节点定义变量值,而显然这个值只能定义为String类型。

例如:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" id="WebApp_ID" version="4.0">

  ...

  <context-param>
    <param-name>contextInitParam</param-name>
    <param-value>someValue</param-value>
  </context-param>

  <servlet>
    <servlet-name>demoServlet</servlet-name>
    <servlet-class>tk.esperz.demoServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
    <init-param>
      <param-name>servletInitParam</param-name>
      <param-value>otherServletsCannotSeeMe</param-value>
    </init-param>
  </servlet>
  <servlet-mapping>
     <servlet-name>demoServlet</servlet-name>
     <url-pattern>/demo.do</url-pattern>
  </servlet-mapping>
</web-app>

其中为该Web项目定义了一个名为contextInitParam的上下文初始化变量,值为someValue,所有该项目中的Servlet都可以通过ServletContext.getInitParameter("contextInitParam")获取;为名为demoServlet的Servlet定义了一个名为servletInitParam的初始化变量,值为otherServletsCannotSeeMe,仅在该Servlet内可以通过ServletConfig.getInitParameter("servletInitParam")获取。凡是用错了方法的都会返回null

按照常规操作,在定义完变量之后应该对其进行初始化,然后再使用。于是就有了这么一段代码:

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // TODO Auto-generated method stub
        PrintWriter out = response.getWriter();
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html; charset=UTF-8");
        out.write("喵喵喵?");
    }

然而在编译并由Tomcat发布后,访问获得的结果却是????。 你是????,我也是????啊??比特流编码和传输给浏览器的MIME信息都已经声明了,为啥还是不对?

直到看了这个 https://blog.csdn.net/tlms_/article/details/78749980

才发现

要先设置好response的属性,然后再获得由这些属性生成的PrintWriter。很是奇怪的逻辑。。 将getWriter()操作放到几个set操作之后,问题顺利解决。

在把Eclipse JEE更新到2019-09之后,打开之前可以运行的,使用Gradle配置的JSP项目运行时,发现所有页面都变成了404。前往Eclipse配置的tomcat临时文件夹查看,发现所写的jsp文件全部没有复制过去。然后在网上搜索发现,是由于JSP搜索路径没有配置好的问题。

https://guides.gradle.org/building-java-web-applications/ 从Gradle的官方教程不难发现,其默认配置的JSP根目录在src/main/webapp下,而不是Eclipse所使用的WebContent

https://stackoverflow.com/questions/18273184/how-to-set-webappdirname-in-gradle 依照这个方法,使用webAppDirName(或者全写为project.webAppDirName)参数指定JSP包的根目录后,方可使其正常搜索WebContent目录下的JSP文件。经过测试,这个参数与是否使用了war插件似乎无关,也就是说,不使用war插件也不能正常识别WebContent目录下的JSP文件,而这时webAppDirName参数是否有效,由于未经测试,则不得而知了。

这个问题与IDE的版本是有关的,在Eclipse JEE 2017-12中可以正常使用,升级到2019-09就不行了。

在设置完webAppDirName之后,再次使用Gradle进行编译的时候,又可能会遇到找不到Servlet的问题。原因推测是在指定JSP目录的时候,破坏了对Java源代码目录的设置。可以注意到Gradle编译的输出项,其中compileJava一项显示的是NO-SOURCE`,显然就是没找到写好的Java源代码文件。

https://stackoverflow.com/questions/31077844/add-another-java-source-directory-to-gradle-script 依照这个方法,显示指定Java源文件的目录后,再重新使用Gradle编译即可解决问题。

添加的参考配置如下:

sourceSets {
    main {
        java {
            srcDir 'src'
        }
    }
}

该配置可用于直接在src目录下建立包名,即使用Eclipse传统的Dynamic Web App向导建立的目录结构。

  1. 官网,Dowload部分选择合适的版本下载。如果没有特别要求,可以直接无脑装个最新版,本地还要配合适的java运行环境,可以也直接挑最新的。当然,若是对java有一定了解的可以去看看有个帮你区分版本区别的页面,找一个满足最低要求的组合就行,如果不想在电脑上修改太多软件配置的话。在下使用的是Java 8 + Tomcat 9。

  2. 选择用installer安装的话可以直接一把梭,因而不作赘述。在此选择Core版本的zip包,下载解压,里面是一套较为标准的Linux文件树目录。

  3. 阅读RUNNING.txt可以获得将Tomcat运行起来所需要的一系列基本配置。本文是参照该指示,从中选取部分关键配置而将Tomcat运行起来的一系列记录。

  4. 高级系统设置-环境变量,建立新项设置CATALINA_HOME。该选项是必须在Tomcat运行前定义的,用于指示Tomcat的家目录,其意义同JAVA_HOME,指向Tomcat的bin目录的上一级。其余变量可以通过下一步的方式,在文件中定义,方便管理和多实例运行。

  5. bin目录下新建setenv.bat,该文件用于提供绝大部分Tomcat运行环境所需要的参数值,Tomcat会自动寻找该文件。(没有的话自动忽略呗,因为要手动配置,所以Apache干脆直接不提供这个文件了)在里面需要明确至少一个选项:JAVA_HOME/JRE_HOME。亦即是说,在配置多实例的Tomcat的时候,允许不同实例使用不同版本的java。不需要将变量值用引号包裹,即使路径里含有空格。

  6. 由于Tomcat提供了默认的配置文件,因而在配置好环境后可跳过软件内部的配置,直接运行。使用bin目录下的脚本进行对应的操作: 启动Tomcat:startup.bat / catalina.bat start 关闭Tomcat:shutdown.bat / catalina.bat stop

  7. 使用默认配置启动Tomcat后,可以访问http://localhost:8080进行测试,会出现一个管理界面,说明配置并启动成功。

  8. 此时若是关注Tomcat的日志输出,会发现出现如淇℃伅一类的乱码。这是由于Windows控制台和Tomcat中文编码不一致导致的。Tomcat提供了汉化的提示信息,上面这个是“信息”二字经过UTF-8编码后,又经GBK解码出现的结果。解决方法有二: 1) 修改conf目录下的logging.properties文件,把java.util.logging.ConsoleHandler.encoding的对应值修改为GBK; 2) 在bin/setenv.bat中添加一行:

    SET JAVA_OPTS=-Duser.language=en

    将JVM的语言设置为英文,这样看着也更熟悉,也避免了中文乱码的问题。当然可以配合第一条使用,不然算是治标不治本。 JAVA_OPTS是传递给JVM的一系列指令,可以填写多个选项,应该是同上一样空格隔开不加引号即可。 然而指导文件中提到,若要限制Tomcat的资源占用,应该在CATALINA_OPTS里而不是这里进行设置。

  9. 若要观察服务器状态,Tomcat提供了该功能,但是出于安全考虑,设计了一套权限管理系统。 修改conf目录下的tomcat-users.xml以注册用户: 在tomcat-users标签里添加:
    <role rolename="tomcat"/>
    <user username="tomcat" password="tomcat" roles="tomcat,manager-gui"/>

    可将用户tomcat以密码tomcat添加至该Tomcat实例的管理用户。由上可见,若是用户要加入不同的组,可以通过逗号隔开即可。 然后就可用tomcat/tomcat用户名-密码对登陆管理界面。