preface

There are some improvements left over from my last post on Web Terminal, starting with the need to access Websocket and SSH. In view of this, the author brings the second share of Web Terminal to solve the remaining problems in this article.

First, the first thing that needs to be addressed is the need to access WebSockets in Doreamon. In the process of accessing Websocket, the author found that there were few projects like Doreamon that used Egg framework and React and Ts. Correspondingly, there were also few articles related to accessing Websocket. Or the description is vague and dispensable. Therefore, this article will introduce in detail how the author accessed Websocket in Doreamon, of course, there are also some thoughts and problems encountered by the author.

Secondly, in the second part of this paper, the author access SSH to execute basic commands, so as to be able to execute some basic Shell commands. This article describes how to access SSH and also documents some problems, because the SSH access process is relatively simple, the length of this article is less.

Finally, I hope this article can help other students to answer their questions when they need to use Websocket or SSH in similar scenarios. At the same time, you are welcome to correct any mistakes.

Access in Doreamonsocket.io

Firstly, since Doreamon itself uses egg framework, the author uses socket. IO to access Websocket according to official recommendation. IO socket. IO socket. IO socket.

Socket. IO supports websocket and polling data transmission to meet communication requirements in scenarios where web browsers do not support WebSocket.

This sentence can also lead to a later question, which I will describe later. Before starting the practice, I will briefly introduce the basic use of socket. IO

socket.ioBasic use of

1, What is socket. IO?

The official explanation is as follows:

Socket.IO is a library that enables real-time, bidirectional, event-based communication between the browser and the server.

2. Basic use of the server

  • IO. On (‘connection’, (socket) => {}) : Listens for client connections, and the callback function passes the socket

  • Socket. on(‘String’, (MSG) => {}) : listens for client messages

  • Socket.emit (‘String’, (MSG) => {}) : sends a message

3. Basic use of the client

  • IO (‘ws://127.0.0.1:7001/’) : connect to the server

  • Socket. on(‘connect’, (MSG) => {}) : Connect to server successfully

  • Socket. on(‘String’, (MSG) => {}) : listens for server messages

  • Socket.emit (‘String’, (MSG) => {}) : sends a message

The installationsocket.ioThe NPM package

Before you can access socket. IO, you need to install the NPM package

1. Server Server

yarn add egg-socket.io
Copy the code

2. Client Indicates the Client

yarn add socket.io-client
Copy the code

Here is the reason for the NPM package installed above

  • Egg-socket. IO plugin: mounts socket. IO instance to app for use

  • Socket. IO -client: used to establish a connection with the server

  • Thinking & problems encountered

  1. Why not connect to the server directly on the client side using native Websocket?

    Answer: THE author tried to access the native Websocket in the code, but found that it did not work, and then went to find the relevant information.

    IO is not Websocket. It just encapsulates Websocket, polling mechanism and other real-time communication methods into a general interface, and implements the corresponding codes of these real-time mechanisms on the server side. In other words, Websocket is only a subset of socket. IO for real-time communication. Therefore, the Websocket client cannot connect to the socket. IO server, and likewise, the Websocket server cannot connect to the socket. IO client

  2. Why not use @types/socket.io-client to reference socket.io-client declarations?

    Answer: Yarn add @types/ reacte@types /react-dom. Socket. IO -client Therefore, when we use Ts, there is no need to install @types/socket.io-client, and related code completion and interface prompt functions will also be provided

Server Configurationegg-socket.io

1. Enable the plug-in

Since it is a plug-in, the plugin file should be configured first

// app/config/plugin.js
exports.io = {
    enable: true,
    package: 'egg-socket.io'
};
Copy the code

2. Configure config.js

// app/config/config.default.js
exports.io = {
    init: {},// passed to engine.io
    namespace: {
        '/': {
            connectionMiddleware: [ 'connection'].packetMiddleware: []}//'/example': {
        // connectionMiddleware: [ 'connection' ],
        // packetMiddleware: []
        / /}}};Copy the code

The above configuration is quite important, explain the meanings of several parameters:

  • IO is IO (‘ws://127.0.0.1:7001/’) to connect to the server. 127.0.0.1:7001 = 127.0.0.1:7001 = 127.0.0.1:7001 = 127.0.0.1:7001 = 127.0.0.1:7001 The connection for the IO (‘ ws: / / 127.0.0.1:7001 / example ‘)

  • ConnectionMiddleware: Preprocessor middleware that is used to establish connections, where some preprocessing can be done

  • PacketMiddleware: Usually used to pre-process messages, this parameter is not used in this article and won’t be explained too much

Server usageegg-socket.io

1. Directory structure

We confirm the current directory structure before using it. I have added an IO folder to store related files

  • app/io/middleware

  • app/io/controller

Add the Middleware preprocessor Middleware

// app/io/middleware/connection.js
module.exports = () = > {
    return async (ctx, next) => {
        ctx.logger.info('*** SOCKET IO CONNECTION SUCCESS ***');
        ctx.socket.emit('serverMsg'.'\r\n*** SOCKET IO CONNECTION SUCCESS ***\r\n')
        await next();
        ctx.socket.emit('serverMsg'.'\r\n*** SOCKET IO DISCONNECTION ***\r\n')}; };Copy the code

Both connection establishment and termination of communication pass through this middleware

3. Add Controller Controllers

// app/io/controller/home.js
module.exports = app= > {
    return class Controller extends app.Controller {
        async getShellCommand () {
            const { ctx, logger, app } = this;
            const command = ctx.args[0];
            ctx.socket.emit('res', { code: 1.content: 'Message received' });
            logger.info(' ======= command ======= ', command)
        }
    }
}
Copy the code

Egg can handle corresponding events in Controller. In home.js, the author adds a method defined getShellCommand, which can process shell statements sent in terminal

4. Configure routes

// app/router.js
module.exports = app= > {
    const { io } = app
    io.of('/').route('getShellCommand',  io.controller.home.getShellCommand)
    // io.of('/example').route('getShellCommand', io.controller.home.getShellCommand)
}
Copy the code
  • Of (‘/’): to route the file, you can get the IO instance from app, and then use of(‘/’) to match the namespace ‘/’ configured in the configuration file. If you need to match ‘/example’, you can use app.io

  • GetShellCommand events: In the ‘/’ namespace, listening to getShellCommand events will be IO. The controller. Home. GetShellCommand method processing, The getShellCommand event can be emitted from the client by socket.emit(‘getShellCommand’, {command: ‘CD /’})

The configuration and usage of the Doreamon server have been set up, so the client needs to receive the information sent by the server

Client usesocket.io-client

1. Establish a connection on the client

// app/web/utils/socket.ts
import io from 'socket.io-client'

export const Socket = io('the ws: / / 127.0.0.1:7001 /', { transports: ['websocket']})Copy the code

2. Use socket. IO -client

// app/web/pages/webTernimal.tsx
import { Socket } from  '@/utils/socket'

const WebTerminal: React.FC = () = >{...const initSocket = () = > {
        socket.on('connect'.() = > {
            console.log('*** SOCKET IO SERVER CONNECTION SUCCESS ***')})// Send a message
        socket.send('*** CLIENT SEND MESSAGE ***')
        socket.emit('getShellCommand', { command: 'cd /'})}... useEffect(() = > {
        initTerminal()
        initSocket()
    }, [])
}
Copy the code
  • Thinking & Problems encountered:
  1. Do listening events always exist?

The client establishes a connection with the server

After the above operation, the client and the server has been established a good connection, the author introduces some illustrative points encountered in the development

1. After the connection is established, the Code is 101

Looking at the request, we find that when we send the request to establish a connection we get a status code of 101 with Switching Protocols attached. So what does this mean?

First, the 101 code indicates the switch protocol. In this request, you can see the following headers:

  • Upgrade: websocketIndicates that the request is initiated by Websocket protocol
  • Sec-websocket-key: Base 64 code randomly generated by the browser, corresponding to the SEC-websocket-Accep field in the response, providing basic protection against accidental and malicious links
  • Sec-websocket-version: indicates the Version of the WebSocket protocol initiated by the server

After sending the above information, you can see the following headers in the returned response:

  • Sec-websocket-accep: indicates the encrypted sec-websocket-key that is confirmed by the server
  • Connection: UpgradeIndicates that the protocol has been switched
  • Upgrade: websocketIndicates that the protocol to be switched is Websocket

From there, the connection is established

2. Code related to data packets

Once the connection is established, messages can be sent, as shown below

As you can see, each message is accompanied by some numbers. Here are some basic numbers:

The first digit indicates the type of the package

  • 0: open, sent from the server when a new transport is opened
  • 1: close, requests to close this transfer, but does not close the connection itself.
  • 2: ping, sent by the client, the server should reply with a ping packet containing the same data, such as:
    1. The client sends 2 probe
    2. The server sends 3 probe

    The heartbeat packet feedback connection is normal

  • 3: pong, sent by the server
  • 4: message, the actual message, the client and server should invoke their callbacks using data

The second digit indicates the type of message for the package

  • 0, such as 40, sends a message to confirm the connection
  • 2, such as 42, indicates the sent message event

In fact, the communication principle of socket. IO can also be expanded to say, but the theme of this article is not here, I will not expand the description, and too much description will deviate from the theme.

Above, the socket. IO is connected in Doreamon, but the Web Terminal is not enough. It needs to process the input shell statements, and then sSH2 needs to process the basic shell statements, which starts the second part of this article

Access in Doreamonssh2

At first, I wanted to use egg-SSH directly, which is supported by egg framework. However, the application scenario of egg-SSH is not the same as Doreamon. However, in Doreamon usage scenarios, login free shell statements are required, where there are multiple servers in the configuration file, not a single service that is written to death. Therefore, I used SSH2, and whenever we entered a server, we logged directly into the server and then executed the shell statement we typed.

The installationssh2NPM package

yarn add ssh2
Copy the code

The server configures the request

First of all, when entering a host, you need to use the user, password and other information to log in. The author needs to get the information of the corresponding server, but due to time constraints, the author first fixed the information of a server.

Suppose we’re using… For this host, we need to log in to this server first, using user name root and password *** respectively

// app/io/controller/home.js
const { createNewServer } = require('.. /.. /utils/createServer');

module.exports = app= > {
    return class Controller extends app.Controller {

        // async getShellCommand () {
        // const { ctx, logger, app } = this;
        // const command = ctx.args[0];
        // ctx.socket.emit('res', { code: 1, content: 'Message received' });
        // logger.info(' ======= command ======= ', command)
        // }

        async loginServer () {
            const { ctx } = this
            createNewServer({
                host: '*. *. *. *'.username: 'root'.password: '* * *'
            }, ctx)
        }
    }
}


// app/router.js
io.of('/').route('loginServer',  io.controller.home.loginServer)

Copy the code

The above code author made the following two points:

1, write a loginServer method to login… For this server, the getShellCommand added earlier is not needed

2, new loginServer in routing the request, and use the IO. Controller. Home. The loginServer methods in processing the request

3. The createNewServer method is used to log in to the server

Logging In to the Server

// app/utils/createServer.js
const SSHClient = require('ssh2').Client
const utf8 = require('utf8')


const createNewServer = (machineConfig, ctx) = > {
    const ssh = new SSHClient();
    const { host, username, password } = machineConfig
    const { socket } = ctx

    // The connection succeeded
    ssh.on('ready'.() = > {

        socket.emit('serverMsg'.'\r\n*** SSH CONNECTION SUCCESS ***\r\n')

        ssh.shell((err, stream) = > {

            if (err) {
                return socket.send('\r\n*** SSH SHELL ERROR: ' + err.message + ' ***\r\n');
            }

            socket.on('shellCommand'.(command) = > {
                stream.write(command)
            })

            stream.on('data'.(msg) = > {
                socket.emit('serverMsg', utf8.decode(msg.toString('binary')))

            }).on('close'.() = > {
                ssh.end();
            });
        })

    }).on('close'.() = > {
        socket.emit('serverMsg'.'\r\n*** SSH CONNECTION CLOSED ***\r\n')
    }).on('error'.(err) = > {
        socket.emit('serverMsg'.'\r\n*** SSH CONNECTION ERROR: ' + err.message + ' ***\r\n')
    }).connect({
        port: 22,
        host,
        username,
        password
    })
}

module.exports = {
  createNewServer
}

Copy the code

The above code does the following:

1. Log in to the specified server through Connect

2. Stream. write in ssh.shell will execute basic shell statements sent by listening for shellCommand events

3. Stream. on in ssh.shell will listen to the result of the statement execution and send the serverMsg event with the result to the client

Sending shell statements

// app/web/pages/webTernimal/index.tsx
const WebTerminal: React.FC = () = >{...const handleInputText = () = > {
        terminal.write('\r\n')
        if(! inputText.trim()) { terminal.prompt()return
        }

        if (inputTextList.indexOf(inputText) === -1) {
            inputTextList.push(inputText)
            currentIndex = inputTextList.length
        }
        / / socket communication
        socket.emit('shellCommand', inputText + '\r')
        terminal.prompt()
    }
    
    ...
    
    useEffect(() = > {
        if (terminal) {
            onKeyAction()

            socket.on('serverMsg'.(res: string) = > {
                console.log('*** SERVER MESSAGE ***', res)
                if (res) terminal.write(res)
            })

        }
    }, [terminal])
    ...
    
}

Copy the code

The above code does the following

Emit (‘shellCommand’, inputText + ‘\r’); emit(‘shellCommand’, inputText + ‘\r’)

2. After the terminal instance is generated, the socket adds the serverMsg listening event

When accessing SSH2, because the related NPM dependency package, even the Use of Chinese documents, only attached to the USE of NPM, it is too simple, in view of this does not use too much, so I do not make too much introduction to SSH2

However, the NPM package was still being shipped a few days ago, but there is very little documentation, which shows how important it is to write a good official document

Finally, a simple Web Ternimal is set up

Still need to improve the point

[root@*-*-*-* -* /]# delete [root@*-*-*-* /]# delete [root@*-*-*-* /]# delete [root@*-*-*-* /]# delete [root@*-*-*-* /]#

2, socket. IO and SSH2 disconnection processing, currently only add some processing to establish the connection and communication, not disconnection processing

conclusion

This article is mainly divided into two parts, one is to access websocket, and the other is to access SSH2. When looking up relevant materials, I found that many introductions or demos are very simple, and this article is also written for the purpose of making everyone understand. I don’t have much contact time with these two parts. I welcome you to point out some mistakes I made or misunderstood. Having written here, the topic of Web Terminal is over. Finally, I hope to let you know the process of building this system and gain something

A link to the

  • socket.ioOfficial documents:socket.io/docs/v4/
  • Egg official document: eggjs.org/zh-cn/
  • egg-socket.ioOfficial documents:Eggjs.org/zh-cn/tutor…
  • ssh2: www.npmjs.com/package/ssh…
  • How to build a simple Web Terminal (a) : juejin.cn/post/698238…
  • Full code: github.com/DTStack/dor…