### 流程
#### `浏览器-->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>
```