본문으로 바로가기

TCP Socket Router 예제를 통한 Electron Application 작성방법에 대한 연재가 완료되었습니다.

 

Electron + Vue.js 애플리케이션 만들기

1. Electron 프로젝트 생성하기(Create Electron Application Project)

2. Electron IPC 통신(Electron Architecture, IPC Main/IPC Renderer)

3. TCP Router 기능 구현(Implements TCP Router Communication)

4. 화면 UI 구성(Main Window UI)

5. 환경설정 구현하기(Preferences Window)

6. 메뉴 사용하기(Application Menu & Context Menu)

7. 시스템 트레이 사용 및 창 최소화(System Tray & Minimize to Tray)

8. Bootstrap Vue와 Font Awesome 사용하기(Using Bootstrap Vue & Font Awesome)

9. Dark Layout과 Frameless Window(Dark Layout & Frameless Window)

10. 빌드 및 배포, 자동 업데이트(Build, Auto Updater)

 

 

이번 포스트에서는 메인화면을 구성하고 처리하는 내용에 대해서 설명합니다.

 

화면구성하기

화면 GUI 계획

메인화면은 아래와 같이 구성할 예정입니다. 왼쪽에는 TCP Router의 Listening 목록을 보여주고 해당 항목 선택시 오른쪽에 연결 상세정보를 보여줍니다. 오른쪽 배경에는 이벤트 로그를 출력하는 구성입니다. 그리고 하단에는 TCP Router의 시작/종료, 로그지우기, 환경설정 버튼을 추가하도록 하겠습니다.

메인화면 구성 계획

 

메인화면

아래 그림은 이 포스트의 예제를 구현한 메인화면의 결과입니다.

메인화면, 이벤트 로그

연결정보 상세보기

아래 그림은 연결된 클라이언트의 상세정보를 출력하는 화면입니다. 연결된 클라이언트의 개수와 전송량을 표시하고 현재접속된 클라이언트들의 연결정보와 전송량을 각각 표시합니다.

메인화면, 접속상세정보

TCP Router 기능 처리하기

화면에서 필요한 각종 통신채널을 등록하고 환경설정에서 라우팅 정보를 읽어와서 TCP Router를 시작합니다.

import { ipcMain } from 'electron'
import TCPRouterManager from '../core/tcp-router-manager'
import PreferencesManager from "./window/preferences-window";

const preferences = PreferencesManager.get();
const env = preferences.routers || [];

function launchTCPRouter() {
  const manager = new TCPRouterManager.getInstance();
  const stateQueue = [];
  const errorQueue = [];
  const logQueue = [];
  let pushRenderer = null;
  ipcMain.on("tcp-router-state-init", (event, args) => {
    // console.log(args);
    pushRenderer = event.sender;
    if(stateQueue.length > 0) {
      while(stateQueue.length > 0) {
        const msg = stateQueue.shift();
        pushRenderer.send("tcp-router-state", msg);
      }
    }
    if(logQueue.length > 0) {
      while(logQueue.length > 0) {
        const msg = logQueue.shift();
        pushRenderer.send("tcp-router-log", msg);
      }
    }
    if(errorQueue.length > 0) {
      while(errorQueue.length > 0) {
        const msg = errorQueue.shift();
        pushRenderer.send("tcp-router-error", msg);
      }
    }
  });
  ipcMain.once("tcp-router-state-end", (event, args) => {
    // console.log(args);
    pushRenderer = null;
  });


  manager.on("transfered", ({key, bytes}) => {
      // console.log("Transfered-----" + key + " : " + JSON.stringify(bytes));
      if(pushRenderer) {
        pushRenderer.send("tcp-router-state", manager.state());
      }
  });
  manager.on("connected", ({key, count}) => {
      // console.log("Connected-----" + key + " : " + JSON.stringify(count));
      if(pushRenderer) {
        pushRenderer.send("tcp-router-state", manager.state());
      }
  });
  manager.on("closed", ({key, count}) => {
      // console.log("Closed-----" + key + " : " + JSON.stringify(count));
      if(pushRenderer) {
        pushRenderer.send("tcp-router-state", manager.state());
      }
  });
  manager.on("error", ({key, error}) => {
    // console.log("Error-----" + key + " : " + JSON.stringify(error));
    const msg = (error.code == 'EADDRINUSE') ? "Error, listen port(" + error.port + ") already in use." : error;
    if(pushRenderer) {
      pushRenderer.send("tcp-router-log", { date : new Date().getTime(), text: msg });
      // pushRenderer.send("tcp-router-error", msg);
    } else {
      errorQueue.push(msg);
    }
  });
  manager.on("update", ({key, error}) => {
    if(pushRenderer) {
      // pushRenderer.send("tcp-router-state", {type : 'routers', state : manager.state()});
      pushRenderer.send("tcp-router-state", manager.state());
    } else {
      logQueue.push(manager.state());
    }
  });
  manager.on("log", (log) => { 
    if(pushRenderer) {
      // pushRenderer.send("tcp-router-state", {type : 'routers', state : manager.state()});
      pushRenderer.send("tcp-router-log", log);
    } else {
      logQueue.push(log);
    }
  });
  manager.execute(env, (routers) => {
    console.log("TCP Router initialized...")
    if(pushRenderer) {
      // pushRenderer.send("tcp-router-state", {type : 'routers', state : manager.state()});
      pushRenderer.send("tcp-router-state", manager.state());
    } else {
      stateQueue.push(manager.state());
    }
  });

  ipcMain.on("tcp-router-state", (event, args) => {
    // console.log(args);
    event.sender.send("tcp-router-state", manager.state());
  });
  ipcMain.on("tcp-router-details", (event, args) => {
    // console.log(args);
    event.sender.send("tcp-router-details", manager.details(args));
  });

  ipcMain.on("tcp-router-startup", (event, args) => {
    manager.startup(env, () => {
      event.sender.send("tcp-router-state", manager.state(args));
    });
  });
  ipcMain.on("tcp-router-shutdown", (event, args) => {
    manager.shutdown();
    event.sender.send("tcp-router-state", manager.state(args));
  });
}
export default launchTCPRouter;

메인화면 생성하기

왼쪽과 오른쪽으로 화면을 분리하고 라우팅 목록, 로그보기, 연결상세보기 이벤트들을 처리합니다.

<template>
  <div id="wrapper" @click="clearActiveItem">
    <main>
      <div class="left-side">
        <span class="title">
          TCP Router Connections
        </span>

        <div class="items">
          <div class="item" v-for="(item, key) in tcpRouterState"
            :key="key"
            :class="{'selected': activeItem[key]}"
            @click.prevent.stop="setActiveItem(key)"
          >
            <div class="name">{{ item.listen }}, {{item.host}}:{{item.port}}</div>
            <div class="value">Connections : {{ item.count }}</div>
            <div class="value">Read : {{ item.bytes.clientReadBytes }} bytes</div>
            <div class="value">Write : {{ item.bytes.clientWriteBytes }} bytes</div>
          </div>
        </div>
      </div>

      <div class="right-side" v-if="selectedItem && selectedState">
        <div class="title">Connection Details</div>
        <div class="details">
          <div class="name">{{ selectedState.listen }}, {{selectedState.host}}:{{selectedState.port}}</div>
          <div class="value">Connections : {{ selectedState.count }}</div>
          <div class="value">Client Read : {{ selectedState.bytes.clientReadBytes }} bytes</div>
          <div class="value">Client Write : {{ selectedState.bytes.clientWriteBytes }} bytes</div>
          <div class="value">Server Read : {{ selectedState.bytes.serverReadBytes }} bytes</div>
          <div class="value">Server Write : {{ selectedState.bytes.serverWriteBytes }} bytes</div>

          <div class="value" v-for="(detail, index) in selectedItem" :key="index">
            <div class="name">{{ detail.address }} : {{ detail.port }}</div>
            <div class="value">Client Read : {{ detail.clientReadBytes }} bytes / Client Write : {{ detail.clientWriteBytes }} bytes</div>
            <div class="value">Server Read : {{ detail.serverReadBytes }} bytes / Server Write : {{ detail.serverWriteBytes }} bytes</div>
          </div>
        </div>
      </div>

      <div class="right-side" v-show="!selectedItem">
        <div class="title">Event Log</div>
        <div class="logs" ref="logs">
          <div class="log" v-for="(log, index) in logs" :key="index">
            <span class="event-date">{{ log.date }}</span> : <span>{{ log.text }}</span>
          </div>
        </div>
      </div>
    </main>

    <footer class="controlbox">
      <div class="left-side">
        <div class="doc">
          <button class="alt" @click="handleStartup">Start</button>
          <button @click="handleShutdown">Shutdown</button>
        </div>
      </div>
      <div class="right-side">
        <div class="doc">
          <button class="alt" @click="handleClearLog">Clear Log</button>
          <button @click="handleConfiguration">Configure</button>
        </div>
      </div>
    </footer>
  </div>
</template>

<script>
import { ipcRenderer } from 'electron'

  export default {
    name: 'main-page',
    components: {  },
    data() {
      return {
        tcpRouterState: {},
        activeItem: {},
        selectedItem: null,
        selectedState: null,
        logs: [],
      }
    },
    created() {
        console.log("created....");
        ipcRenderer.on("tcp-router-state", (event, args) => {
          console.log(args);
          this.tcpRouterState = args;
          for(const key in this.activeItem) {
            if(key) {
              this.showDetails(key);
              break;
            }
          }
        });
        ipcRenderer.on("tcp-router-log", (event, log) => {
          console.log(log);
          this.addLog(log);
        });
        
        ipcRenderer.send("tcp-router-state-init");
        ipcRenderer.send("tcp-router-state");
    },
    destroyed() {
        ipcRenderer.send("tcp-router-state-end", "");
    },
    methods: {
      open (link) {
        this.$electron.shell.openExternal(link)
      },
      setActiveItem(key) {
        this.clearActiveItem();
        this.addActiveItem(key);
        this.showDetails(key);
      },
      addActiveItem(key) {
        this.activeItem[key] = key;
        this.activeItem = Object.assign({}, this.activeItem);
      },
      removeActiveItem(key) {
        delete this.activeItem[key];
        this.activeItem = Object.assign({}, this.activeItem);
      },
      clearActiveItem() {
        this.activeItem = {};
        this.selectedItem = null;
        this.selectedState = null;
      },
      showDetails(key) {
        ipcRenderer.once("tcp-router-details", (event, args) => {
          console.log(args);
          this.selectedItem = args;
        });
        ipcRenderer.send("tcp-router-details", key);
        this.selectedState = this.tcpRouterState[key];
      },
      addLog({key, date, text}) {
        
        if(this.logs.length > 100) {
          this.logs.shift();
        }
        this.logs.push({
          key,
          text,
          date: new Date(date).toISOString().replace(/T/, ' ').replace(/\..+/, '')      // replace T with a space
        });

        const container = this.$refs.logs;
          container.scrollTop = container.scrollHeight;
      },
      handleStartup() {
        ipcRenderer.send("tcp-router-startup");
      },
      handleShutdown() {
        ipcRenderer.send("tcp-router-shutdown");
      },
      handleConfiguration() {
        ipcRenderer.send("open-preperences");
      },
      handleClearLog() {
        this.logs.splice(0, this.logs.length)
      }
    }
  }
</script>

......(이하생략)......

메인화면 실행하기

import { app, ipcMain } from 'electron'
import MainWindow from './window/main-window'

import PreferencesManager from "./window/preferences-window";
import launchTCPRouter from './shared/tcp-router-launcher'

let mainWindow = null;
function init() {
  mainWindow = MainWindow.create();
  mainWindow.on('closed', () => {
    mainWindow = null
  });
  
  launchTCPRouter();
    
  ......(이하생략)......

 

실행코드 객체화하기

TCPRouter를 단일하게 수행하기 위해 객체로 구성하고 static 함수를 사용하여 단일 인스턴스를 사용하도록 합니다. 객체 초기화시에 통신을 위한 채널을 등록합니다.

import { ipcMain } from 'electron'
import TCPRouterManager from '../core/tcp-router-manager'

export default (() => {
  class TCPRouterLauncher {
    constructor() {
      this.stateQueue = [];
      this.errorQueue = [];
      this.logQueue = [];
      this.pushRenderer = null;

      this.manager = new TCPRouterManager.getInstance();

      this.bindEvents();
    }

    bindEvents() {

      ipcMain.on("tcp-router-state-init", (event, args) => {
        // console.log(args);
        this.pushRenderer = event.sender;
        if(this.stateQueue.length > 0) {
          while(this.stateQueue.length > 0) {
            const msg = this.stateQueue.shift();
            this.pushRenderer.send("tcp-router-state", msg);
          }
        }
        if(this.logQueue.length > 0) {
          while(this.logQueue.length > 0) {
            const msg = this.logQueue.shift();
            this.pushRenderer.send("tcp-router-log", msg);
          }
        }
        if(this.errorQueue.length > 0) {
          while(this.errorQueue.length > 0) {
            const msg = this.errorQueue.shift();
            this.pushRenderer.send("tcp-router-error", msg);
          }
        }
      });

      ipcMain.once("tcp-router-state-end", (event, args) => {
        // console.log(args);
        this.pushRenderer = null;
      });

      ipcMain.on("tcp-router-state", (event, args) => {
        // console.log(args);
        event.sender.send("tcp-router-state", this.manager.state());
      });

      ipcMain.on("tcp-router-details", (event, args) => {
        // console.log(args);
        event.sender.send("tcp-router-details", this.manager.details(args));
      });

      ipcMain.on("tcp-router-startup", (event, args) => {
        this.execute(() => {
          event.sender.send("tcp-router-state", this.manager.state(args));
        });
      });

      ipcMain.on("tcp-router-shutdown", (event, args) => {
        this.manager.shutdown();
        event.sender.send("tcp-router-state", this.manager.state(args));
      });
      
      this.manager.on("transfered", ({key, bytes}) => {
          // console.log("Transfered-----" + key + " : " + JSON.stringify(bytes));
          if(this.pushRenderer) {
            this.pushRenderer.send("tcp-router-state", this.manager.state());
          }
      });
      this.manager.on("connected", ({key, count}) => {
          // console.log("Connected-----" + key + " : " + JSON.stringify(count));
          if(this.pushRenderer) {
            this.pushRenderer.send("tcp-router-state", this.manager.state());
          }
      });

      this.manager.on("closed", ({key, count}) => {
          // console.log("Closed-----" + key + " : " + JSON.stringify(count));
          if(this.pushRenderer) {
            this.pushRenderer.send("tcp-router-state", this.manager.state());
          }
      });

      this.manager.on("error", ({key, error}) => {
        // console.log("Error-----" + key + " : " + JSON.stringify(error));
        const msg = (error.code == 'EADDRINUSE') ? "Error, listen port(" + error.port + ") already in use." : error;
        if(this.pushRenderer) {
          this.pushRenderer.send("tcp-router-log", { date : new Date().getTime(), text: msg });
          // this.pushRenderer.send("tcp-router-error", msg);
        } else {
          this.errorQueue.push(msg);
        }
      });

      this.manager.on("update", ({key, error}) => {
        if(this.pushRenderer) {
          // this.pushRenderer.send("tcp-router-state", {type : 'routers', state : this.manager.state()});
          this.pushRenderer.send("tcp-router-state", this.manager.state());
        } else {
          this.logQueue.push(this.manager.state());
        }
      });

      this.manager.on("log", (log) => { 
        if(this.pushRenderer) {
          // this.pushRenderer.send("tcp-router-state", {type : 'routers', state : this.manager.state()});
          this.pushRenderer.send("tcp-router-log", log);
        } else {
          this.logQueue.push(log);
        }
      });
    }
  
    execute(callback) {
      const env = [
        {
          listen : 3307,
          host : "192.168.1.24",
          port : 3306
        },
        {
          listen : 9010,
          host : "localhost",
          port : 9090
        },
        {
          listen : 9020,
          host : "192.168.1.24",
          port : 9090
        },
      ];
      this.manager.execute(env, (routers) => {
        console.log("TCP Router initialized...")
        if(this.pushRenderer) {
          // this.pushRenderer.send("tcp-router-state", {type : 'routers', state : this.manager.state()});
          this.pushRenderer.send("tcp-router-state", this.manager.state());
        } else {
          this.stateQueue.push(this.manager.state());
        }

        if(callback && typeof callback === "function") {
          callback();
        }
      });
    }
  }


  return {
      getInstance() {
          if(!TCPRouterLauncher.instance) {
              TCPRouterLauncher.instance = new TCPRouterLauncher();
          }
          return TCPRouterLauncher.instance;
      }
  };
})();

메인화면 실행하기

/src/main/index.js의 init() 함수에 launcherTCPRouter() 함수를 주석처리하고 싱글톤 인스턴스를 생한 후 실행합니다.

let mainWindow = null;
function init() {
  mainWindow = MainWindow.create();
  mainWindow.on('closed', () => {
    mainWindow = null
  });

  TCPRouterLauncher.getInstance().execute();
  
  ......(이하생략)......

 

소스코드

본 포스트 관련 소스코드는 여기에서 다운로드 가능합니다.

728x90