编写网页shell终端项目

前一篇文章,讲述了网页版Shell终端的概念,以及OnTheSSH在针对网页编程方面的架构设计,本篇文章通过一个真实可用的demo项目,介绍如何用http技术,将Web前台、后台和OnTheSSH的核心模块连接起来。

下图是demo项目的框架:

出于简单性和教学性的目的、又考虑到java语言的普遍性特点,demo项目使用java/servlet和javascript为主要技术,来搭建shell终端的前后台,您自己在开发项目时,可灵活选择编程语言和技术框架,当前能实现Web的编程语言和架构非常多。

整个Shell终端窗口(前台)被封装在index.html文件中,后台由多个Servlet组成,前台和后台被组装在一个Tomcat的项目中。OnTheSSH的核心模块是一个exe执行文件,后台Servlet通过Socket和OnTheSSH的核心模块进行关联。

demo项目从这里下载:web_terminal.7z

解压后有三个文件夹:

  • apache-tomcat-9.0.6 — 运行Servlet项目的容器
  • java_src — Servlet源代码
  • socket_api — OnTheSSH核心模块(封装成Socket服务的exe程序)

系统启动

1)启动OnTheSSH核心模块Socket服务:

socket_api>onthessh_socket_api.exe 2222   #2222是Socket服务侦听端口

2)启动Tomcat

# 进入Tomcat的bin目录
cd D:\MySelf\project\OnTheSSH\PACKAGE\web_terminal\apache-tomcat-9.0.6\bin
# 启动Tomcat
catalina.bat run

上图是启动Tomcat后的日志输出。倒数第四行显示了demo项目(onthessh)已在Tomcat中加载,倒数第三行显示Tomcat的http服务端口是8080.

3)现在可以通过浏览器访问了:

地址栏中的地址 127.0.0.1:8080/onthessh/ 表示正在访问demo项目的前台(index.html)。

Servlet项目

Servlet是Java语言最原始的在Web编程领域的技术框架,经过这些年的发展,技术框架早已日新月异,Servlet反倒是不被大多数Java程序员熟悉了,因此在这里详细介绍Servlet的配置、部署、和运行Servlet的容器(Tomcat)的简单使用。

Tomcat的目录结构是这样的:

apache-tomcat-9.0.6/
  | -- bin/
  | -- conf/
  | -- lib/
  | -- logs/
  | -- temp/
  | -- webapps/
        | -- onthessh   //demo项目
              | -- index.html   //前台
              | -- WEB-INF
                    | -- classes/  //后台Servlet(编译后的.class文件)
                    | -- lib/    //项目引用的第三方库(.jar文件)
                    | -- web.xml   //项目配置(Servlet和Url的映射关系)

在web.xml文件中,需要为每个Servlet做配置,下面代码是其中的一个:

    <servlet>
        <servlet-name>OpenSessionServlet</servlet-name>
        <servlet-class>com.onthessh.OpenSession</servlet-class>
    </servlet>
    ......
    <servlet-mapping>
        <servlet-name>OpenSessionServlet</servlet-name>
        <url-pattern>/OpenSession</url-pattern>
    </servlet-mapping>

OpenSessionServlet是Servlet的名称,对应的.class文件是com.onthessh.OpenSession,位置就在WEB-INF/classes/com/onthessh/OpenSession.class。<servlet-mapping>标签中定义了这个Servlet的Url是“/OpenSession”,对应浏览器地址是 http://<IP>:<PORT>/onthessh/OpenSession,其中“onthessh”是demo项目的名称。

Servlet源码分析

demo项目中一共有6个Servlet,分别对应核心模块的6个Socket服务接口,具体的功能和参数在下一篇文章中有详细说明,这里聚焦在Servlet的代码编写形式和调用Socket的细节上,还是以OpenSession.java这个Servlet为例:

public class OpenSession extends HttpServlet
{
	private static final long serialVersionUID = 1L;
    private Gson gson;

    @Override
    public void init() throws ServletException 
    {
        super.init();
        gson = new Gson();
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException 
    {
        request.setCharacterEncoding("UTF-8");
        response.setContentType("application/json;charset=UTF-8");
        response.setCharacterEncoding("UTF-8");
        
        PrintWriter out = response.getWriter();
        
        try 
        {
        	String reqJson = readRequestBody(request); //read http post body json    	
        	System.out.println("req:" + reqJson);

编写Servlet类必须继承父类HttpServlet,在重新实现父类的init()方法中初始化了gson变量,它是用来做Json编码和解析的,也是这个demo引用的唯一的外部库(jar文件)。

doPost()方法也是重新实现父类的方法,从名称可以理解这个Servlet只接受POST方式的请求,在方法中设置了Servlet的请求和返回字符集(utf-8),设置了http属性content-type为”application/json”,因为demo中所有的Servlet,请求参数和返回数据都是Json。

try中的readRequestBody(request)获得前台发送来的请求数据,注意数据并不是POST常用的name=value格式,而是放在HTTP的“body”中的Json。

        	// Parse the body json
        	Map<String, Object> map = gson.fromJson(reqJson, Map.class);
        	String addr = map.get("addr").toString();
        	String user = map.get("user").toString();
        	String passwd = map.get("passwd").toString();
        	int rows = (int) Double.parseDouble(map.get("rows").toString());
        	int cols = (int) Double.parseDouble(map.get("cols").toString());
        	
        	// Build socket json
        	Map<String, Object> socketMap = new HashMap<>();
        	socketMap.put("method", "onthessh::terminal::open_session");
        	Map<String, Object> data = new HashMap<>();
        	data.put("addr", addr);
        	data.put("user", user);
        	data.put("passwd", passwd);
        	data.put("rows", rows);
        	data.put("cols", cols);
        	socketMap.put("data", data);        	
        	String socketJson = gson.toJson(socketMap);
        	
        	String rspJson = socket(socketJson);	//call socket api 
        	out.print(rspJson);
            out.flush();

Servlet接收到Json数据后,不能直接发送给核心模块,因为还需要告诉核心模块Json数据是干什么用的。因此发送给核心模块Socket服务的数据结构大致是这样的:

{
    "method": "onthessh::terminal::open_session",
    "data": {
        "addr": "127.0.0.1:2222",
        "user": "dyf",
        "passwd": "dyf",
        "rows": 24,
        "cols": 120
    }
}

method告诉核心模块要调用的API,”onthessh::terminal::open_session”表示要创建远端Linux的session,data是前台发送来的数据。

接下来分析一下socket(socketJson)方法内部是怎样操作的:

	    	socket = new Socket("127.0.0.1", 2222);
	    	
	    	String head = String.format("%10d", json.getBytes().length) + "\r\n";	//length head
	    	
	    	//send
	    	OutputStream out = socket.getOutputStream();
	    	out.write((head+json).getBytes());
	    	
	    	//read length head
	    	InputStream in = socket.getInputStream();
	    	byte[] lenBytes = new byte[12];
	    	in.read(lenBytes);
	    	String lenStr = new String(lenBytes, "utf-8").trim();
	    	int len = Integer.parseInt(lenStr);
	    	//read body json
	    	byte[] bytes = new byte[len];
	    	in.read(bytes);
	    	String body = new String(bytes, "utf-8");
	    	
	    	return body;

出于简单性目的,创建与核心模块的Socket的地址和端口是硬编码的。在成功创建Socket后,先发送一个12字节的长度单元,在发送json数据,长度单元的格式大致如下:

"        94\r\n"

这里的94表示Json数据的字节长度,94前面用空格补齐,加上后面的’\r\n’,一共12字节。这种先发送一个长度单元,在发送具体的数据,在Socket通讯中比较常见,其目的是便于处理”粘包“问题。Servlet在接收核心模块发来的数据时,同样也是要先接收12字节的长度单元。

下篇文章将介绍前端index.html的代码详细。