### 流程
#### `浏览器-->nginx-->静态页面-->ws请求-->nginx-->daphne(django-channels)`
 
### 环境
`centos 7`  
`nginx 1.12`  
`python 3.6`  
`daphne 2.3.0`  
`django 2.2.2`  
`docker 4.0.1`  
`channels 2.2.0`  
`channels-redis   2.4.0`
 
### 一、django-channels的websocket实现
#### 创建虚拟环境
```bash
pip install virtualenv
cd /data/app/
virtualenv venv
source venv/bin/activate
```
#### 安装相关软件包
```bash
pip install -u django channels asgi_redis channels_redis
```
#### 创建项目
```bash
django-admin startproject xtermWS
```
#### 创建app
```bash
cd xtermWS
python manage.py startapp dockerCmd
```
#### 项目结构如下
```bash
xtermWS
├── dockerCmd
│   ├── admin.py
│   ├── apps.py
│   ├── consumers.py #类似view,需要手动创建
│   ├── __init__.py
│   ├── models.py
│   ├── routing.py #类似urls,需要手动创建
│   └── views.py
├── manage.py
└── xtermWS
    ├── asgi.py #类似wsgi,需要手动创建
    ├── __init__.py
    ├── routing.py #类似urls,需要手动创建
    ├── settings.py
    ├── urls.py
    └── wsgi.py
```
#### 配置项目settings.py
```bash
vim xtermWS/settings.py
```
```bash
ALLOWED_HOSTS = ['*'] 
INSTALLED_APPS = [
    'channels', # channels
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'dockerCmd.apps.DockercmdConfig', #新创建的dockerCmd
]
...
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')], #项目模板路径
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]
#asgi配置
ASGI_APPLICATION = 'xtermWS.routing.application'
#日志配置
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'normal': {
            'format': '%(levelname)s | %(asctime)s | app: %(module)s pid: %(process)d th: %(thread)d | %(message)s',
        },
    },
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',  
            'formatter': 'normal',
        }
    },
    'loggers': {
        'django': {
            'handlers': ['console'],
            'level': 'INFO',
            'propagate': True,
        },
    },
}
#channel_redis配置
CHANNEL_LAYERS = {
  'default': {
    'BACKEND': 'channels_redis.core.RedisChannelLayer',
    'CONFIG': {
      'hosts': [('127.0.0.1', 6379)],
    },
  },
}
```
#### 创建xtermWS/routing.py
```bash
vim xtermWS/routing.py
```
```python
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
import dockerCmd.routing
application = ProtocolTypeRouter({
    'websocket': AuthMiddlewareStack(
    URLRouter(
      dockerCmd.routing.websocket_urlpatterns
    )
  ),
})
```
#### 创建dockerCmd/routing.py
```bash
vim dockerCmd/routing.py
```
```python
from django.urls import re_path
from dockerCmd import consumers
websocket_urlpatterns = [
  re_path(r'^ws/(?P<cid>[^/]+)$', consumers.CommandConsumer),
]
```
#### 创建dockerCmd/consumers.py
```bash
vim dockerCmd/consumers.py
```
```python
from channels.generic.websocket import WebsocketConsumer
import docker
import threading
import logging
logger = logging.getLogger('django')
class CommandConsumer(WebsocketConsumer):
    def connect(self):
        self.container_id = self.scope['url_route']['kwargs']['cid']
        self.accept()
        self.client=docker.APIClient()
        #推送logs
        self.send(text_data=self.client.logs(self.container_id,stdout=True, stderr=True).decode('utf-8'))
        #self.send(text_data=self.client.attach(container_id,stderr=True,stdout=True,demux=True))
        self.socket=self.client.attach_socket(self.container_id, params={'stdin': 1, 'stream': 1})
        #开启线程获取stdout,stdin,logs数据stream数据
        self.stop_thread=False
        self.t = threading.Thread(target=self.send_stream_log)
        self.t.start()
    def disconnect(self, close_code):
        #关闭线程
        #关闭socket
        self.stop_thread=True
        self.socket._sock.send('stop\r\n'.encode('utf-8'))
        #socket关闭
        self.socket.close()
        #容器关闭删除
        self.client.stop(self.container_id)
        self.client.wait(self.container_id)
        self.client.remove_container(self.container_id)
        #client关闭
        self.client.close()
    def receive(self, text_data):
        self.socket._sock.send(text_data.encode('utf-8'))
        logger.info('CommandConsumer:receive')
    def send_stream_log(self):
        for b in self.client.attach(self.container_id,stderr=True,stdout=True,stream=True,demux=True):
            logger.info(b)
            if self.stop_thread:
                break
            if b[0]:
                self.send(text_data=b[0].decode('utf-8'))
            if b[1]:
                self.send(text_data=b[1].decode('utf-8'))
        logger.info('退出线程')
```
#### 创建asgi.py
```bash
vim xtermWS/asgi.py
```
```python
import os
import django
from channels.routing import get_default_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xtermWS.settings")
django.setup()
application = get_default_application()
```
 
#### 二、docker安装redis
```bash
docker run -d -p 6379:6379 --name redis redis
```
 
### 三、supervisor管理daphne
#### 安装supervisor
```bash
yum install -y supervisor
```
#### 创建配置文件
```bash
vim /etc/supervisord.d/daphne.ini
```
```bash
[program:daphne]
directory=/data/app/xtermWS/
environment=PATH="/data/app/venv/bin"
command=/data/app/venv/bin/daphne xtermWS.asgi:application -b 0.0.0.0 -p 9000
autostart=true
autorestart=true
stdout_logfile=/var/daphne/daphne_ws.log
redirect_stderr=true
```
#### 创建日志目录
```bash
mkdir /var/daphne
```
#### 启动supervisord
```bash
supervisord -c /etc/supervisord.conf
```
#### 启动/停止daphne
```bash
supervisorctl start daphne
supervisorctl stop daphne
```
 
### 四、nginx安装配置
#### 安装nginx
```bash
yum install nginx -y
```
#### 配置nginx
```bash
vim /etc/nginx/nginx.conf
```
```ini
...
upstream channels-backend-prod {
    server 127.0.0.1:9000;
}
server{
    listen  80;
    server_name  domain;
    location /ws {
        proxy_pass http://channels-backend-prod;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_redirect off;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Host $server_name;
    }
}
...
```
#### 启动nginx
```bash
systemctl start nginx
```
 
### 五、xterm.js安装配置
#### 安装xterm.js
```bash
npm install xterm
``` 
#### 拷贝到项目静态文件目录
```bash
cp -r node_modules/xterm xtermWS/xtermWS/static/
```
  
### 六、前端配置
```html
...
//引入xtrem的css和js文件
<link rel="stylesheet" href="{% static 'xterm/xterm.css' %}" />
<script src="{% static 'xterm/xterm.js' %}" ></script>
<script src="{% static 'xterm/addons/attach/attach.js' %}" ></script>
...
//显示终端窗口
<div id="terminal-container">
</div>
...
<script type="text/javascript">
...
$(document).ready(function(){
    // 声明一个websocket变量
    var term_websocket;
    Terminal.applyAddon(attach);
    const term = new Terminal({
        windowsMode:true,
        rows:10
    });
    //关闭之前的websocket
    if (typeof(term_websocket) != "undefined"){
        term_websocket.close();
    }
    const container = document.getElementById('terminal-container');
    term.open(container);
    const protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://';
    const port = location.port ? `:${location.port}` : '';
    const socketUrl = `${protocol}${location.hostname}${port}/ws/容器id`;
    term_websocket = new WebSocket(socketUrl);
    term_websocket.onopen = (ev) => { term.attach(term_websocket); };
    term_websocket.onclose = function(ev){
        console.log('Connection closed.');
    };                   
});
...
</script>
```