前一篇文章,讲述了网页版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的代码详细。
