0%

这两天需要做音视频播放相关的东西,所以重新找了目前android下的解码库。Android自带的解码库支持不全,因此很多第三方播放器都是自带解码器,绝大部分都是使用FFMpeg作为解码库。我11年的时候也弄过视频播放器,当时也是基于FFMpeg来做。那时候网上有关Android视频解码库的资料不多,只在git上找到一个人移植FFMpeg,把它弄下来编译,有兴趣可以看看当时的文章:Android 视频播放器 faplayer 编译 。

言归正传,今天的主角是大名鼎鼎的VLC,做过视频播放器的人,应该都听过它大名,基本上所有平台都有它的身影。Android上今年也发布了beta版。这次编译不太顺利,主要是因为编译前我看了网上一些其他人写的编译过程。然后选了一个来按照它编译。最后在一个地方折腾了好久。原因是:那篇文章是去年的,现在VLC项目重新更新了,编译方式简化了。当然大部分编译过程是一致的。最后查了半天,找到了VLC官网的编译说明,

最后还是按照官方的重新编译一次。有关VLC的说明这里就不说了,有兴趣可以去官网了解。

官方编译说明点这里

1、下面是我编译的环境:

  • 主机系统:window7 64位旗舰版
  • 虚拟机:Vmware Workstation 7.1.2
  • Linux版本:Ubuntu 10.10
  • AndroidSDK:adt-bundle-linux-x86-20130522
  • JDK:jdk-7u25-linux-i586.tar
  • NDK:android-ndk-r8e-linux-x86.tar

版本说明:SDK、JDK、NDK我目前都是使用最新的版本,都是官网下载。唯独Linux是比较旧的版本,因为这个系统我用了两年多,一直没有升级到12.04。这个最后也导致我编译的时候遇到一些因为某些软件部版本低,导致编译中断。所以如果你还没装Linux系统,建议你直接安装12.04稳定版。AndroidSDK是google的集成包,里面包含Eclipse和ADT以及4.2的SDK。建议大家如果没有配置好环境,就下载这个集成包。在配置JDK、SDK、NDK前,先把LVC编译需要的一些第三方软件包安装好。

2、请保证你的机器已经安装了下面软件(最好安装该软件最新版)

  • apt-get install gcc
  • apt-get install g++
  • apt-get build-dep vlc
  • apt-get install git
  • apt-get install wget
  • apt-get install autoconf
  • apt-get install libtool
  • apt-get install subversion
  • apt-get install cmake
  • apt-get install ant

这个是必须安装的软件,而且最好是安装最新版的软件,我编译的时候,因为ant使用了比较旧的版本,导致生成APK的时候失败了,ant必须是1.8以上的版本才行。

3、JDK安装配置

从Oracle官网下载JDK:点击这里

我是用最新的JDK7,32位还是64位根据你的系统来选,我这里选择的是32位

下载压缩包解压后放到一个目录,我这里放到/home/mythou/android-dev/目录下,下面SDK、NDK我也会放到这目录下。解压后,需要配置环境变量,网上也很多教程,我简单说说:

终端输入:sudo gedit /etc/profile

添加环境变量,路径是你解压后jdk的路径,下面是我实际配置的路径。方便终端使用,加入PATH路径里面。

export JAVA_HOME=/home/mythou/android-dev/jdk/jdk1.7.0_25
export JRE_HOME=/home/mythou/android-dev/jdk/jdk1.7.0_25/jre
export CLASSPATH=.:$CLASSPATH:$JAVA_HOME/lib:$JAVA_HOME/jre/lib
export PATH=$JAVA_HOME/bin:$JAVA_HOME/jre/bin:$PATH:$HOME/bin:$ANT_HOME

修改environment配置

终端输入:sudo gedit /etc/environment

PATH=”/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games” export JAVA_HOME=/home/mythou/android-dev/jdk/jdk1.7.0_25
export JRE_HOME=/home/mythou/android-dev/jdk/jdk1.7.0_25/jre
export CLASSPATH=.:$CLASSPATH:$JAVA_HOME/lib:$JAVA_HOME/jre/lib

4、配置Android SDK

1、从Google android官网下载SDK集成开发包:点击这里

我这里选择的是Linux 32-bit,建议下载ADT Bundle的集成开发包,如果单纯下载SDK,还得下载配置eclipse和ADT,比较复杂。下载后也是解压到目标文件夹,我这里是:/home/mythou/android-dev/adt-bundle-linux-x86-20130522目录下。

同样需要配置环境变量

终端输入:sudo gedit /etc/profile 增加下面配置。

export ANDROID_SDK=/home/mythou/android-dev/adt-bundle-linux-x86-20130522/sdk
PATH=$PATH:$ANDROID_SDK/tools:$ANDROID_SDK/platform-tools

5、配置NDK

从Google官网下载DNK:点击这里

同样,我这里下载Linux的32bit版。

下载后,同样解压到目标文件夹,我这里的是:/home/mythou/android-dev/

解压后,终端输入:sudo gedit /etc/profile 增加下面配置。

export ANDROID_NDK=/home/mythou/android-dev/android-ndk-r8e PATH=$PATH:$NDKR6B:$ANDROID_NDK

到这里,JDK、NDK、SDK都已经配置好,你可以打开adt-bundle-linux-x86-20130522目录下的eclipse软件,如果能正常打开说明环境基本配置好。

你也可以直接编个应用测试一下。或者终端检查版本,例如检查JDK版本:

6、其他环境变量配置

除了上面几个,还有部分环境变量需要配置,当然你可以选择直接在终端配置,不写入配置文件,看个人喜欢。因为我经常需要重复编译,

所以写在配置文件比较方便。

增加ant变量

export ANT_HOME=/home/mythou/android-dev/apache-ant-1.8.0

需要说明的是,我的ant是自己重新下载源码包安装,因为系统默认安装版本太低。后面会说到这个问题。

需要配置CPU类型,Android支持的CPU类型包含ARM和X86,所以编译前需要指定CPU类型。

  • X86系列的

  • ARM的Cortex-A8 or Cortex-A9系列

export ANDROID_ABI=armeabi-v7a

  • ARMv6

export ANDROID_ABI=armeabi

  • ARMv6 不带 FPU

export ANDROID_ABI=armeabi
export NO_FPU=1

  • ARMv5 或者 模拟器

export ANDROID_ABI=armeabi
export NO_ARMV6=1

  • MIPS 系列

上面你可以直接输入环境变量或者在profile设置。上面配置,我编译的时候,选择了ANDROID_ABI=armeabi-v7a 因为我测试机器是ARM A9系列的。到这里,环境配置已经完成。如果你是修改etc/profile 需要注销重启,或者终端更新用户配置source /etc/profile才能生效。

下面是我profile最后配置的样式

复制代码

export ANDROID_NDK=/home/mythou/android-dev/android-ndk-r8e
export NDKR5C=/home/mythou/ndkr5c
export NDKR6B=/home/mythou/ndkr6b
PATH=$PATH:$NDKR6B:$ANDROID_NDK

export ANDROID_ABI=armeabi-v7a

export JAVA_HOME=/home/mythou/android-dev/jdk/jdk1.7.0_25
export JRE_HOME=/home/mythou/android-dev/jdk/jdk1.7.0_25/jre
export CLASSPATH=.:$CLASSPATH:$JAVA_HOME/lib:$JAVA_HOME/jre/lib
export ANT_HOME=/home/mythou/android-dev/apache-ant-1.8.0 export PATH=$JAVA_HOME/bin:$JAVA_HOME/jre/bin:$PATH:$HOME/bin:$ANT_HOME

export ANDROID_SDK=/home/mythou/android-dev/adt-bundle-linux-x86-20130522/sdk
PATH=$PATH:$ANDROID_SDK/tools:$ANDROID_SDK/platform-tools

复制代码

7、获取源码:

从git获取源码:

git clone git://git.videolan.org/vlc-ports/android.git

获取后,它会在你当前目录下,创建android目录,里面包含下面文件

此时其实还没有下载VLC源码,这个是VLC的目录结构和编译的配置。

执行:

这个默认是编译debug版本,如果需要编译release版本,需要执行

第一次编译先编译debug版。

此时才开始下载VLC的源码。大概有200M的源码。

然后就会自动编译直到生成VLC的apk文件为止。

8、编译过程遇到问题

虽然是自动编译,不过中间多多少少会有问题,下面是我编译过程遇到问题。

(1)checkout的问题

这个貌似是大小写不匹配。可以修改complie或者再执行sh compile.sh都可以解决。

(2)libmpeg2库没法下载

需要说明一点是编译过程中,需要下载很多第三方的库,所以导致整个编译过程很漫长。当然这个跟你网络情况有关。

既然无法下载库,只能手动自己下载一个了,自己百度找一个libmpeg2库下载下来,放到指定文件夹即可。

把下载的libmpeg2库,拷贝到下面路径:

(3)config.sub文件太旧问题

10.10版本的Ubuntu,存在系统文件太旧额问题

下载这两个文件替换即可:config.guess和config.sub,将此两个文件拷贝到/usr/share/misc目录下覆盖,重新执行编译命令。

(4)gettext版本太低

这个就是我上面说的getext需要0.18版本。自己手动找个0.18版本,然后编译安装。

这个解决后,就可以成功把解码库编译出来:

我这里编译的是armeabi-v7a版本,会自动把解码库拷贝到LVC的android工程下的libs文件夹下。

接着编译打包apk的时候出现了一个问题:

ant版本太低问题,需要ant1.8或以上版本。老方法,自己找对应版本编译安装。

最后就可以成功编译出vlc的apk安装包:

因为是debug版,出来的是VLC-debug.apk文件。

到这里就算圆满编译出来,你也可以把java的工程移植出来放到eclipse上面使用修改。(vlc-android目录下面就是对应的android应用工程)

如果是移植出来还要把java-libs文件夹下面关联的3个工程移植出来。Eclipse下的工程关联:

下面放两张运行截图:

  

下面提供eclipse下可以使用的工程,我只在4.2SDK下编译,其他的没有尝试。

整个包含解码库的编译工程太大了,有1.6G,我就不放上来了,有兴趣可以自己编译。只给出eclipse下可以编译的工程,方便改界面。

不过还是建议自己编译一下,因为现在的VLC只是beta版,后面出正式版,还需要重新编译解码库。

VLC在Eclipse下可用工程源码:

(VLC官方已经多次更新,而且处于beta版,所以不再提供老版本下载,有需要的朋友请自己编译)

有问题的朋友可以留意:

Edited by mythou

原创博文,转载请标明出处:http://www.cnblogs.com/mythou/p/3162595.html

CentOS/Linux下设置IP地址

1.临时生效设置

1.1修改IP地址

#ifconfig eth0 192.168.100.100

1.2修改网关地址

#route add default gw 192.168.100.1 dev eth0

1.3修改DNS

echo “nameserver 8.8.8.8” >> /etc/resolv.conf

2.永久生效设置

2.1IP地址永久设置生效

vi /etc/sysconfig/network-scripts/ifcfg-eth0

DEVICE=eth0                    #设备名称,网卡对应的名称
HWADDR=00:0C:29:93:2A:5C            #网卡MAC地址(物理地址)
TYPE=Ethernet                    #网卡类型以太网模式
UUID=8ad6b8b6-6905-4a4b-a1ac-1c1cab22305f  #全局唯一标识符
ONBOOT=yes                    #系统启动时是否激活此设备
NM_CONTROLLED=yes   #network manger的参数,实时生效,修改后无需要重启网卡立即生效。
BOOTPROTO=static                  #网卡获得ip地址的方式(默认为dhcp,表示自动获取)
IPADDR=192.168.0.107                #IP地址
GATEWAY=192.168.0.1                #网关 (可以单独设置,也可以在此处设置)
PREFIX=24
DNS1=192.168.1.1

NETMASK=255.255.255.0          #子网掩码

BROADCAST=192.168.1.255  #网卡广播地址

2.2网关地址永久设置生效

在网卡中未设置,在这里设置

vi /etc/sysconfig/network

    NETWORKING=yes                #表示系统是否使用网络,no表示不能使用网络
    HOSTNAME=doiido                #设置本机的主机名,要和/etc/hosts中设置的主机名相同
    GATEWAY=192.168.100.1      #设置网关的IP地址

2.3DNS永久设置

在网卡中未设置,在此文件中设置

修改/etc/resolv.conf文件
    # vi /etc/resolv.conf
    nameserver 8.8.8.8                      #google域名服务器
    nameserver 114.144.114.114      #国内域名服务器

3.重启网卡

方法一:

service network restart #重启网络服务

service network stop    #关闭网络服务

service network start   #启动网络服务

方法二:

#/etc/init.d/network stop
  # /etc/init.d/network start
  # /etc/init.d/network restart

网卡状态查询

service network status

查看当前路由及网关信息

   # netstat -r

  上篇《Docker基础入门及示例》文章介绍了Docker部署,以及相关.net core 的打包示例。这篇文章我将以oss.offical.site站点为例,主要介绍下在linux机器下完整的部署流程,.net core在docker容器中的运行已经介绍,这里.net core运行环境我会介绍直接在linux运行的场景,内容主要包含以下几个部分:

1. 基础工具和Linux环境准备

2. .Net Core环境安装及端口配置

3. Nginx的安装配置

4. Supervisor守护进程安装配置

一. 基础工具和Linux环境准备

    工具介绍:

  Ubuntu:azure云端linux主机

  Xshell:免费的linux客户端工具

  FileZilla: 免费文件上传(sftp、ftp)工具

   vs2017:  开发工具

  本次部署主要是通过Nginx负载代理.net core服务,提供对外站点访问。.net core 本身的宿主则通过其自带的Kestrel服务运行。如果你是云主机请记得在管理控制台添加80访问端口。

  那这里我先创建一个www文件夹,作为后边存放站点文件的根目录,同时设置相关权限,这里可能会有一个小的需要注意的问题,现在很多云主机直接登录后权限是很低的,需要使用:sudo -s 命令提升权限,否则创建文件夹会出现权限不足的错误。

  1. mkdir /home/www/ossoffical     // 创建ossoffical站点文件夹

  2. chown [-R] 账号名称 ossoffical    // 把账号名称添加到www文件夹的所有者中,保证后续sftp上传文件夹等在无法提权的情况下也能操作

  二. .Net Core环境安装及端口配置

  1. 安装,这里参照微软官网即可,我这里使用的是Ubuntu 14.04版本,主要执行以下几条命令

sudo sh -c ‘echo “deb [arch=amd64] https://apt-mo.trafficmanager.net/repos/dotnet-release/ trusty main” > /etc/apt/sources.list.d/dotnetdev.list’

sudo apt-key adv –keyserver hkp://keyserver.ubuntu.com:80 –recv-keys 417A0893

sudo apt-get update

sudo apt-get install dotnet-dev-1.0.1

  2. vs端口相关设置

  这里需要注意一下,当前版本创建的.net core项目在不制定具体端口的情况下,会默认使用5000端口。但是这个在后续的版本中会移除,见官方文档:

  并且我也希望自己指定具体的端口,这样以后站点多了之后防止冲突,我修改自己项目中的Program文件中内容,指定8000端口,如图:

  请注意先后的顺序,否则在vs中可能会出现调试无法打开的情况,发布当前项目。

  3. 上传项目文件运行

  a. 通过filezilla上传项目文件到ossoffical目录下

  b. 通过命令客户端进入ossoffical目录(cd /home/www/ossoffical),执行:dotnet OSS.Offical.Site.dll(我当前站点项目程序集名称),结果如图:

 

  退出执行ctrl+c 。当然我们也可以执行 “ dotnet OSS.Offical.Site.dll & ” ,让其在后台运行,只是这个更方便让我们查看。这个时候你可以访问对应的8000端口即可访问(云主机请注意开放访问端口)。

三. Nginx安装

  1. 执行:sudo apt-get install nginx  命令

  2. 安装完成之后访问对应的80端口,应该如下:

  3. 配置代理

  a. mkdir /etc/nginx/hosts   创建一个文件夹,用来放置站点配置文件

  b. 执行 cd /etc/nginx/hosts   进入目录, 执行: vi ossoffical   按 “i” 添加如下内容:

server {
listen 80;

index index.html index.htm;

server_name www.osscoder.com; #域名

location / {
proxy_pass http://127.0.0.1:8000; # 刚才设置的地址端口
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection keep-alive;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}

  退出保存。

  4. 修改nginx配置文件,vi /etc/nginx/nginx.conf

  在http节点末尾,添加 :include /etc/nginx/hosts/*;

  5.  重启nginx服务器: sudo service nginx restart(reload)

  6. 进入站点目录,dotnet OSS.Offical.Site.dll & ,确保站点正确运行,访问 www.osscoder.com,如下:

四. 守护进程安装配置

  为了保证服务能够稳定运行,我们安装守护进程以确保在应用程序出现异常中断时能够正常重启。

   1. 安装,执行:sudo apt-get install supervisor

  2. 安装成功后进入:/ect/supervisor/conf.d/ 目录,添加 ossoffical.conf 文件,添加如下内容:

[program:OSS.Offical.Site]
command=dotnet OSS.Offical.Site.dll //要执行的命令
directory=/home/www/ossoffical //命令执行的目录
environment=ASPNETCORE__ENVIRONMENT=Production #环境变量
user=osscoder //进程执行的用户身份
stopsignal=INT
autostart=true
autorestart=true
startsecs=3    //自动重启间隔
stderr_logfile=/var/log/ossoffical.err.log    //标准错误日志
stdout_logfile=/var/log/ossoffical.out.log     //标准输出日志

   把对应的目录和名称换掉即可,请删除注释,否则有时会出现命令执行失败的情况。完毕之后重启supervisor:

  sudo service supervisor stop

 sudo service supervisor start

  完成之后可以杀掉进程或者重启机器测试。

我最近正在开发OSS系列开源项目,现在已有OSS.Common,OSS.HttpOSS.Social 和 OSS.PayCenter 几个项目,如果你也有兴趣,请联系我或者关注公众号OSSCoder

直接在终端中执行aria2c --enable-rpc --rpc-allow-origin-all可直接开启RPC服务。
这种方法并不能进行个性化的参数设置,需要用到下面的方法。

创建配置文件

1
2
3
$ mkdir ~/.config/aria2
$ touch ~/.config/aria2/aria2.session
$ touch ~/.config/aria2/aria2.conf

以配置文件方式启动Aria2

使用aria2c --conf-path=$HOME/.config/aria2/aria2.conf启动aria2,也可添加-D选项,后台启动aria2。
建议将以上命令添加到alias中或启用开机自启动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
## '#'开头为注释内容, 选项都有相应的注释说明, 根据需要修改 ##
## 被注释的选项填写的是默认值, 建议在需要修改时再取消注释 ##

## 进度保存相关 ##

# 从会话文件中读取下载任务
#input-file=aria2.session
# 在Aria2退出时保存`错误/未完成`的下载任务到会话文件
#save-session=aria2.session
# 定时保存会话, 0为退出时才保存, 需1.16.1以上版本, 默认:0
#save-session-interval=60

## 文件保存相关 ##

# 文件的保存路径, 默认: 当前启动位置
#dir=/tmp/Downloads
# 启用磁盘缓存, 0为禁用缓存, 需1.16以上版本, 默认:16M
#disk-cache=32M
# 文件预分配方式, 能有效降低磁盘碎片, 默认:prealloc
# 预分配所需时间: none < falloc ? trunc < prealloc
# falloc和trunc则需要文件系统和内核支持
# NTFS建议使用falloc, EXT3/4建议trunc, MAC 下需要注释此项
#file-allocation=none
# 断点续传
continue=true

## 下载连接相关 ##

# 最大同时下载任务数, 运行时可修改, 默认:5
#max-concurrent-downloads=5
# 同一服务器连接数, 添加时可指定, 默认:1
max-connection-per-server=5
# 最小文件分片大小, 添加时可指定, 取值范围1M -1024M, 默认:20M
# 假定size=10M, 文件为20MiB 则使用两个来源下载; 文件为15MiB 则使用一个来源下载
min-split-size=10M
# 单个任务最大线程数, 添加时可指定, 默认:5
#split=5
# 整体下载速度限制, 运行时可修改, 默认:0
#max-overall-download-limit=0
# 单个任务下载速度限制, 默认:0
#max-download-limit=0
# 整体上传速度限制, 运行时可修改, 默认:0
#max-overall-upload-limit=0
# 单个任务上传速度限制, 默认:0
#max-upload-limit=0
# 禁用IPv6, 默认:false
#disable-ipv6=true
# 连接超时时间, 默认:60
#timeout=60
# 最大重试次数, 设置为0表示不限制重试次数, 默认:5
#max-tries=5
# 设置重试等待的秒数, 默认:0
#retry-wait=0

## RPC相关设置 ##

# 启用RPC, 默认:false
enable-rpc=true
# 允许所有来源, 默认:false
rpc-allow-origin-all=true
# 允许非外部访问, 默认:false
rpc-listen-all=true
# 事件轮询方式, 取值:[epoll, kqueue, port, poll, select], 不同系统默认值不同
#event-poll=select
# RPC监听端口, 端口被占用时可以修改, 默认:6800
#rpc-listen-port=6800
# 设置的RPC授权令牌, v1.18.4新增功能, 取代 --rpc-user 和 --rpc-passwd 选项
#rpc-secret=<TOKEN>
# 设置的RPC访问用户名, 此选项新版已废弃, 建议改用 --rpc-secret 选项
#rpc-user=<USER>
# 设置的RPC访问密码, 此选项新版已废弃, 建议改用 --rpc-secret 选项
#rpc-passwd=<PASSWD>
# 是否启用 RPC 服务的 SSL/TLS 加密,
# 启用加密后 RPC 服务需要使用 https 或者 wss 协议连接
#rpc-secure=true
# 在 RPC 服务中启用 SSL/TLS 加密时的证书文件,
# 使用 PEM 格式时,您必须通过 --rpc-private-key 指定私钥
#rpc-certificate=/path/to/certificate.pem
# 在 RPC 服务中启用 SSL/TLS 加密时的私钥文件
#rpc-private-key=/path/to/certificate.key

## BT/PT下载相关 ##

# 当下载的是一个种子(以.torrent结尾)时, 自动开始BT任务, 默认:true
#follow-torrent=true
# BT监听端口, 当端口被屏蔽时使用, 默认:6881-6999
listen-port=51413
# 单个种子最大连接数, 默认:55
#bt-max-peers=55
# 打开DHT功能, PT需要禁用, 默认:true
enable-dht=false
# 打开IPv6 DHT功能, PT需要禁用
#enable-dht6=false
# DHT网络监听端口, 默认:6881-6999
#dht-listen-port=6881-6999
# 本地节点查找, PT需要禁用, 默认:false
#bt-enable-lpd=false
# 种子交换, PT需要禁用, 默认:true
enable-peer-exchange=false
# 每个种子限速, 对少种的PT很有用, 默认:50K
#bt-request-peer-speed-limit=50K
# 客户端伪装, PT需要
peer-id-prefix=-TR2770-
user-agent=Transmission/2.77
# 当种子的分享率达到这个数时, 自动停止做种, 0为一直做种, 默认:1.0
seed-ratio=0
# 强制保存会话, 即使任务已经完成, 默认:false
# 较新的版本开启后会在任务完成后依然保留.aria2文件
#force-save=false
# BT校验相关, 默认:true
#bt-hash-check-seed=true
# 继续之前的BT任务时, 无需再次校验, 默认:false
bt-seed-unverified=true
# 保存磁力链接元数据为种子文件(.torrent文件), 默认:false
bt-save-metadata=true

注意

使用前需要修改配置dir为下载目录,input-filesave-session为aria2.session所在路径,才可以正常启动。
aria2.conf中的路径支持绝对路径和相对路径,遗憾的是并不支持环境变量,如果多用户使用,需要每个用户一个配置文件。推荐使用绝对路径。

参考资料:http://aria2c.com/usage.html

知识点:

在Linux系统上安装Nginx服务器,配置图片访问路径

通过ftp上传图片到,指定路径,通过浏览器访问指定路径中的图片

参考博客:http://blog.csdn.net/maoyuanming0806/article/details/78067446
http://blog.csdn.net/csdn\_lqr/article/details/53334583

安装Nginx服务器

1.到官网下载nginx   http://nginx.org/en/download.html

2.安装的依赖环境准备

 a. 安装gcc的环境, yum install gcc-c++

 b.安装第三方开发包

   yum install -y pcre pcre-devel

   yum install -y zlib zlib-devel

   yum install -y openssl openssl-devel

3.nginx安装步骤

  a.  将nginx的源码包上传到linux系统上

  b.解压缩  tar zxf  nginx-1.13.8.tar.gz

  c.使用configure命令创建一个makeFile文件

   移动到  cd nginx-1.13.8.tar.gz

   执行命令:

    ./configure  –prefix=/usr/local/nginx  –pid-path=/var/run/nginx/nginx.pid  –lock-path=/var/lock/nginx.lock  –error-log-path=/var/log/nginx/error.log  –http-log-path=/var/log/nginx/access.log   –with-http_gzip_static_module   –http-client-body-temp-path=/var/temp/nginx/client –http-proxy-temp-path=/var/temp/nginx/proxy –http-fastcgi-temp-path=/var/temp/nginx/fastcgi  –http-uwsgi-temp-path=/var/temp/nginx/uwsgi   –http-scgi-temp-path=/var/temp/nginx/scgi 

      执行后效果:

           查看:

执行安装命令

         make

         make install

安装成功后,移动到 cd /usr/local/nginx 下 查看:

  d:启动nginx服务器

    进入 cd   /usr/local/nginx/sbin下

    有个ngnix可执行文件

    执行 ./nginx   命令启动服务器

通过进程查看是否启动

远程windows上访问:nginx安装成功

配置图片存放,访问路径

   在/usr/local/nginx/conf/nginx.conf 主配置文件中 添加一个location并指定实际路径

location /images/ { root /home/ftpadmin/health/; autoindex on; }

截图如下:

修改完成后,进入到/usr/local/nginx/sbin 目录下

执行 ./nginx -s reload 从新启动nginx服务器

说明:

   1)root则是将images映射到/home/ftpadmin/hatlth/images/
   2)autoindex on便是打开浏览功能。

修改用户访问权限

   chown ftpadmin /home/ftpadmin

chmod 777 -R /home/ftpadmin

通过ftp,上传图片到/home/ftpadmin/health/目录下

在nginx启动的情况下,通过浏览器测试,/home/ftpadmin/health/1.jpg

Linux下路由配置命令

1. 添加主机路由

1
2
route add -host 192.168.1.11 dev eth0
route add -host 192.168.1.12 gw 192.168.1.1

2. 添加网络路由

1
2
3
4
route add -net 192.168.1.11 netmask 255.255.255.0 eth0
route add -net 192.168.1.11 netmask 255.255.255.0 gw 192.168.1.1
route add -net 192.168.1.0/24 eth0
route add -net 192.168.1.0/24 gw 192.168.1.1

3. 添加默认网关

1
route add default gw 192.168.1.1

4. 删除路由

1
route del -host 192.168.1.11 dev eth0

5. 删除默认路由

1
route del default gw 192.168.1.1

Linux下配置永久路由的几种方式

1. 在/etc/rc.local里添加路由信息

1
2
3
route add -net 192.168.1.0/24 dev eth0

route add -net 192.168.1.0 netmask 255.255.255.0 gw 192.168.1.1

2. 在/etc/sysconfig/network里追加

1
GATEWAY=[网关IP或者网关网卡名称]

3. /etc/sysconfig/static-routes

1
2
3
any net 192.168.1.0/24 gw 192.168.1.1

any net 192.168.1.0 netmask 255.255.255.0 gw 192.168.1.1

4. 开启IP转发

1.临时开启

1
echo "1" > /proc/sys/net/ipv4/ip_forward

2.永久开启

1
vim /etc/sysctl.conf

修改net.ipv4.ip_forward=1

实验:配置双网卡主机同时使用内网和外网

本实验的背景是笔者在实践中遇到过的一个问题,本实验尽量还原当时的网络环境。仅当做一份笔记,同时分享给遇到此问题的同学。
奈何我现在没有硬件呀(T_T)…只能拿VMware WorkstationeNSP来模拟实验环境了。

问题背景

如下图拓扑所示,如果去掉client节点,内网外网就是相互隔离的网络。

但是实际情况是,client节点既需要访问外网资源,又需要访问内网资源,而client只能配置一条默认路由。如果将默认路由配置在外网网卡,client可以访问172.16.2.0/24网络的资源和外网的资源,但是其余内网资源将无法访问;如果将默认路由配置在内网网卡,client虽然可以完全访问内网资源,但是却不能访问外网资源。
怎么解决呢?

实验环境

  1. VMware Workstation Pro
  2. 4台最小化安装的CentOS 7.3虚拟机
  3. 华为eNSP模拟器

实验拓扑

去掉client节点,内网和外网是隔离的网络。
外网网络为10.0.0.0/16,代表运营商的接入网;
内网网络为172.16.0.0/16,代表内网部分(该部分为了安全,不允许对互联网的直接访问)。
其中

  • client为双网卡的主机,两网卡网段分别为10.0.0.0/16172.16.2.0/24
  • server2172.16.2.0/24网段的服务器;
  • server3172.16.3.0/24网段的服务器;
  • server4172.16.4.0/24网段的服务器。

网络规划

IP分配

节点名称

IP地址

子网掩码

备注

client

10.0.0.101

255.255.0.0

client的外网网卡

172.16.2.101

255.255.255.0

client的内网网卡

Server2

172.16.2.11

255.255.255.0

172.16.2.0/24网段的某台服务器

Server3

172.16.3.11

255.255.255.0

172.16.3.0/24网段的某台服务器

Server4

172.16.4.11

255.255.255.0

172.16.4.0/24网段的某台服务器

内网路由器

172.16.2.254

255.255.255.0

172.16.2.0/24的网关

172.16.3.254

255.255.255.0

172.16.3.0/24的网关

172.16.4.254

255.255.255.0

172.16.4.0/24的网关

虚机网卡类型

网络名称

VMware网卡类型

网络范围

运营商网络

桥接

10.0.0.0/16

VMnet2

仅主机

172.16.2.0/24

VMnet3

仅主机

172.16.3.0/24

VMnet4

仅主机

172.16.4.0/24

内网路由器如何实现呢?
VMnet2、VMnet3、VMnet4均为仅主机模式,那么常规情况下,只有其网络内的各计算机之间才可以通信,要怎样才能实现三个网络间的通信呢?
答案是使用华为eNSP模拟器中的Cloud。使用Cloud设备可以将eNSP中的路由器和VMware虚拟机的网卡连接起来。

配置内网环境

配置eNSP的路由器

接口

IP地址

子网掩码

G0/0/0

172.16.2.254

255.255.255.0

G0/0/1

172.16.3.254

255.255.255.0

G0/0/2

172.16.4.254

255.255.255.0

1
2
3
4
5
6
7
8
9
10
11
<huawei>system-view
[huawei]int g0/0/0
[Huawei-GigabitEthernet0/0/0]ip address 172.16.2.254 255.255.255.0
[Huawei-GigabitEthernet0/0/0]quit
[huawei]int g0/0/1
[Huawei-GigabitEthernet0/0/1]ip address 172.16.3.254 255.255.255.0
[Huawei-GigabitEthernet0/0/1]quit
[huawei]int g0/0/2
[Huawei-GigabitEthernet0/0/2]ip address 172.16.4.254 255.255.255.0
[Huawei-GigabitEthernet0/0/2]quit
[huawei]

修改虚机的IP地址

client

网卡名称

IP地址

子网掩码

默认网关

备注

ens33

10.0.0.101

255.255.0.0

10.0.0.1

外网网卡

ens37

172.16.2.101

255.255.255.0

内网网卡

server2

网卡名称

IP地址

子网掩码

默认网关

备注

ens33

172.16.2.11

255.255.255.0

172.16.2.254

server3

网卡名称

IP地址

子网掩码

默认网关

备注

ens33

172.16.3.11

255.255.255.0

172.16.3.254

server4

网卡名称

IP地址

子网掩码

默认网关

备注

ens33

172.16.4.11

255.255.255.0

172.16.4.254

在server上搭建HTTP服务

server2为例:
使用Python创建一个简单的HTTP服务

1
2
3
cd ~
echo "server2" > index.html
python -m SimpleHTTPServer 8080

对照试验

client上访问外网

1
ping www.baidu.com -c 4

client上访问server2

1
ping 172.16.2.11 -c 4

client上访问server3

1
ping 172.16.3.11 -c 4

client上访问server4

1
ping 172.16.4.11 -c 4

在对照试验中可以看到,在client将默认网关配置在外网网卡的情况下,双网卡的client可以正常访问外网和内网的172.16.2.0/24部分,而172.16.3.0/24172.16.4.0/24是不能访问到的。

为什么client能访问172.16.2.0/24网络,而不能访问172.16.0.0/16的其余网络呢?
因为client位于172.16.2.0/24网络内,在网络内进行通信,数据包不用发送至其他网络,当然默认网关也就不起作用了。
而当client172.16.0.0/16的其余网络通信时,client的路由表没有记载发往目的地址的路径,所以client只能傻傻的把数据包发送给默认网关,从此数据包和真正的目的地址就“南辕北辙”了。当然ping的结果就是网络不可达。

路由配置

client上查看路由表

1
route

client上添加路由

1
route add -net 172.16.0.0/16 gw 172.16.2.254

永久配置,则在/etc/rc.local里添加路由信息

1
route add -net 172.16.0.0/16 gw 172.16.2.254

查看路由表

1
route

实验结果

检测连通性

1
ping www.baidu.com -c 4

1
ping 172.16.2.11 -c 4

1
ping 172.16.3.11 -c 4

1
ping 172.16.4.11 -c 4

访问内外网资源

1
curl http://www.baidu.com/

1
curl http://172.16.2.11:8080/index.html
1
curl http://172.16.3.11:8080/index.html
1
curl http://172.16.4.11:8080/index.html

解决的办法很简单,就是1条命令而已。
但是蕴藏在这条命令背后的原理、概念、理论,则需要我们进行深究!

本文链接:https://www.cnblogs.com/connect/p/linux-static-route.html

嵌入式系统经常会通过串口打印调试信息,在Linux环境下,可以使用stty设置串口波特率等参数,然后使用cat就可以正确捕获串口输出的调试信息。

http://blog.csdn.net/zoomdy/article/details/50921336
mingdu.zheng at gmail dot com

stty查看串口参数

1
stty -F /dev/ttyS0 -a

查看串口1(/dev/ttyS0)当前的参数,包括波特率、数据位等。

stty设置串口参数

1
stty -F /dev/ttyS0 ispeed 115200 ospeed 115200 cs8

该命令将串口1(/dev/ttyS0)设置成115200波特率,8位数据模式。一般情况下设置这两个参数就可以了,如果显示数据乱码,可能还需要设置其它参数,使用man查看stty其它设置选项。

cat打印串口数据

1
cat /dev/ttyS0

串口数据就可以在终端上显示了。

cygwin

这种方法同样适用于cygwin环境。

嵌入式系统经常会通过串口打印调试信息,在Linux环境下,可以使用stty设置串口波特率等参数,然后使用cat就可以正确捕获串口输出的调试信息。

http://blog.csdn.net/zoomdy/article/details/50921336
mingdu.zheng at gmail dot com

stty查看串口参数

1
stty -F /dev/ttyS0 -a

查看串口1(/dev/ttyS0)当前的参数,包括波特率、数据位等。

stty设置串口参数

1
stty -F /dev/ttyS0 ispeed 115200 ospeed 115200 cs8

该命令将串口1(/dev/ttyS0)设置成115200波特率,8位数据模式。一般情况下设置这两个参数就可以了,如果显示数据乱码,可能还需要设置其它参数,使用man查看stty其它设置选项。

cat打印串口数据

1
cat /dev/ttyS0

串口数据就可以在终端上显示了。

cygwin

这种方法同样适用于cygwin环境。

  从程序的结构来看,live项目包括了四个基本库、程序入口类(在mediaServer中)和一些测试代码(在testProgs中)。

  四个基本静态库是UsageEnvironmentBasicUsageEnvironmentgroupsockliveMedia

UsageEnvironment:

  包括抽象类UsageEnvironment和抽象类TaskScheduler,这两个类用于事件调度,其中包括实现了对事件的异步读取、对事件句柄的设置及对错误信息的输出等;该库中还有一个HashTable,这是一个通用的HashTable,在整个项目中都可以使用它,当然该HashTable也是一个抽象类。

BasicUsageEnvironment:

  主要是对UsageEnvironment中对应类的实现。 

  BasicUsageEnvironment和UsageEnvironment中的类都是用于整个系统的基础功能类.比如UsageEnvironment代表了整个系统运行的环境,它提供了错误记录和错误报告的功能,无论哪一个类要输出错误,就需要保存UsageEnvironment的指针.而TaskScheduler则提供了任务调度功能.整个程序的运行发动机就是它,它调度任务,执行任务(任务就是一个函数).TaskScheduler由于在全局中只有一个,所以保存在了UsageEnvironment中.而所有的类又都保存了UsageEnvironment的指针,所以谁想把自己的任务加入调度中,那是很容易的.在此还看到一个结论:整个live555(服务端)只有一个线程.

  BasicUsageEnvironment和UsageEnvironment中的类初始化:

// Begin by setting up our usage environment:
TaskScheduler* scheduler = BasicTaskScheduler::createNew();
env = BasicUsageEnvironment::createNew(*scheduler); 

  在服务端中的main()函数,可见其创建一个RTSPServer类实例后,和在客服端的main函数连接成功,开始播放后,即进入相同的一个函数:

env->taskScheduler().doEventLoop(); // does not return

  在函数定义在BasicTaskScheduler0.cpp中:

复制代码

void BasicTaskScheduler0::doEventLoop(char* watchVariable) { // Repeatedly loop, handling readble sockets and timed events:
while (1) { if (watchVariable != NULL && *watchVariable != 0) break;
SingleStep();
}
}

复制代码

  BasicTaskScheduler0从TaskScheduler派生,所以还是一个任务调度对象,所以依然说明任务调度对象是整个程序的发动机。循环中每次走一步:SingleStep():

复制代码

void BasicTaskScheduler::SingleStep(unsigned maxDelayTime)
{
fd_set readSet = fReadSet; // make a copy for this select() call //计算select socket们时的超时时间。
DelayInterval const& timeToDelay = fDelayQueue.timeToNextAlarm(); struct timeval tv_timeToDelay;

    tv\_timeToDelay.tv\_sec \= timeToDelay.seconds();
    tv\_timeToDelay.tv\_usec \= timeToDelay.useconds(); // Very large "tv\_sec" values cause select() to fail. // Don't make it any larger than 1 million seconds (11.5 days)
    const long MAX\_TV\_SEC = MILLION; if (tv\_timeToDelay.tv\_sec > MAX\_TV\_SEC) 
    {
            tv\_timeToDelay.tv\_sec \= MAX\_TV\_SEC;
    } // Also check our "maxDelayTime" parameter (if it's > 0):
    if (maxDelayTime > 0
            && (tv\_timeToDelay.tv\_sec > (long)maxDelayTime/MILLION || (tv\_timeToDelay.tv\_sec == (long)maxDelayTime/MILLION && tv\_timeToDelay.tv\_usec > (long)maxDelayTime%MILLION))) 
    {
            tv\_timeToDelay.tv\_sec \= maxDelayTime/MILLION;
            tv\_timeToDelay.tv\_usec \= maxDelayTime%MILLION;
    } //先执行socket的select操作,以确定哪些socket任务(handler)需要执行。
    int selectResult = select(fMaxNumSockets, &readSet, NULL, NULL, &tv\_timeToDelay); if (selectResult < 0) 
    { #if defined(\_\_WIN32\_\_) || defined(\_WIN32)
            int err = WSAGetLastError(); // For some unknown reason, select() in Windoze sometimes fails with WSAEINVAL if // it was called with no entries set in "readSet".  If this happens, ignore it:
            if (err == WSAEINVAL && readSet.fd\_count == 0) 
            {
                    err \= 0; // To stop this from happening again, create a dummy readable socket:
                    int dummySocketNum = socket(AF\_INET, SOCK\_DGRAM, 0);
                    FD\_SET((unsigned)dummySocketNum, &fReadSet);
            } if (err != 0) 
            { #else
                    if (errno != EINTR && errno != EAGAIN) 
                    { #endif
                            // Unexpected error - treat this as fatal:

#if !defined(_WIN32_WCE) perror(“BasicTaskScheduler::SingleStep(): select() fails”); #endif exit(0);
}
} // Handle any delayed event that may have come due: //执行一个最迫切的延迟任务。
fDelayQueue.handleAlarm(); // Call the handler function for one readable socket:
HandlerIterator iter(*fReadHandlers);
HandlerDescriptor* handler; // To ensure forward progress through the handlers, begin past the last // socket number that we handled:
if (fLastHandledSocketNum >= 0)
{ //找到上次执行的socket handler的下一个
while ((handler = iter.next()) != NULL)
{ if (handler->socketNum == fLastHandledSocketNum) break;
} if (handler == NULL)
{
fLastHandledSocketNum = -1;
iter.reset(); // start from the beginning instead
}
} //从找到的handler开始,找一个可以执行的handler,不论其状态是可读,可写,还是出错,执行之。
while ((handler = iter.next()) != NULL)
{ if (FD_ISSET(handler->socketNum, &readSet) && FD_ISSET(handler->socketNum, &fReadSet) /* sanity check */ && handler->handlerProc != NULL)
{
fLastHandledSocketNum = handler->socketNum; // Note: we set “fLastHandledSocketNum” before calling the handler, // in case the handler calls “doEventLoop()” reentrantly.
(*handler->handlerProc)(handler->clientData, SOCKET_READABLE); break;
}
} //如果寻找完了依然没有执行任何handle,则从头再找。
if (handler == NULL && fLastHandledSocketNum >= 0)
{ // We didn’t call a handler, but we didn’t get to check all of them, // so try again from the beginning:
iter.reset(); while ((handler = iter.next()) != NULL)
{ if (FD_ISSET(handler->socketNum, &readSet) && FD_ISSET(handler->socketNum, &fReadSet) /* sanity check */ && handler->handlerProc != NULL)
{
fLastHandledSocketNum = handler->socketNum; // Note: we set “fLastHandledSocketNum” before calling the handler, // in case the handler calls “doEventLoop()” reentrantly.
(*handler->handlerProc)(handler->clientData, SOCKET_READABLE); break;
}
} //依然没有找到可执行的handler。
if (handler == NULL) fLastHandledSocketNum = -1;//because we didn’t call a handler
}
}

复制代码

总结为以下四步:
1> 为所有需要操作的socket执行select。
2> 找到第一个应执行的延迟任务并执行之。
3> 找出第一个应执行的socket任务(handler)并执行之。

  可见,每一步中只执行三个任务队列中的一项。到这里,我们不尽要问这些socket handlerdelay task是哪里来的?

  DelayQueue类:译为"延迟队列",它是一个队列,每一项代表了一个要调度的任务(在它的fToken变量中保存)。同时保存了这个任务离执行时间点的剩余时间。可以预见,它就是在TaskScheduler中用于管理调度任务的东西。注意:此队列中的任务只被执行一次!执行完后这一项即被无情抛弃!

  HandlerSet类:Handler集合。Handler是什么呢?它是一种专门用于执行socket操作的任务(函数),HandlerSet被TaskScheduler用来管理所有的socket任务(增删改查)。所以TaskScheduler中现在已调度两种任务了:socket任务(handlerSet)和延迟任务(DelayQueue).其实TaskScheduler还调度第三种任务:Event,介个后面再说。

  HandlerDescriptor类:socket handlet描述。

  socket handler加入执行队列后会一直存在,而delay task在执行完一次后会立即弃掉。

  socket handler保存在队列BasicTaskScheduler0::HandlerSet* fHandlers中,通过调用envir().taskScheduler().turnOnBackgroundReadHandling(fClientSocket,(TaskScheduler::BackgroundHandlerProc*)&incomingRequestHandler, this)这样的方式加入到socket handler队列中:

复制代码

void BasicTaskScheduler::turnOnBackgroundReadHandling(int socketNum,
BackgroundHandlerProc* handlerProc, void* clientData)
{ if (socketNum < 0) return;

FD\_SET((unsigned)socketNum, &fReadSet);

fReadHandlers\->assignHandler(socketNum, handlerProc, clientData); if (socketNum+1 > fMaxNumSockets) {
    fMaxNumSockets \= socketNum+1;
}

}

复制代码

  socket handler添加时为什么需要那些参数呢?socketNum是需要的,因为要select socket(socketNum即是socket()返回的那个socket对象)。conditionSet也是需要的,它用于表明socket在select时查看哪种装态,是可读?可写?还是出错?proc和clientData这两个参数就不必说了(真有不明白的吗?)。再看BackgroundHandlerProc的参数,socketNum不必解释,mask是什么呢?它正是对应着conditionSet,但它表明的是select之后的结果,比如一个socket可能需要检查其读/写状态,而当前只能读,不能写,那么mask中就只有表明读的位被设置。

复制代码

void HandlerSet::assignHandler(int socketNum,
TaskScheduler::BackgroundHandlerProc* handlerProc, void* clientData)
{ // First, see if there’s already a handler for this socket:
HandlerDescriptor* handler;
HandlerIterator iter(*this); while ((handler = iter.next()) != NULL) { if (handler->socketNum == socketNum) break;
} if (handler == NULL) { // No existing handler, so create a new descr:
handler = new HandlerDescriptor(fHandlers.fNextHandler);
handler->socketNum = socketNum;
}

handler\->handlerProc = handlerProc;
handler\->clientData = clientData;

}

复制代码

  delay task保存在队列BasicTaskScheduler0::DelayQueue fDelayQueue中,通过调用envir().taskScheduler().scheduleDelayedTask(0,  (TaskFunc*)FramedSource::afterGetting, this)这样的方式加入到delay task队列中,需要传入task延迟等待的微秒(百万分之一秒)数(第一个参数):

复制代码

TaskToken BasicTaskScheduler0::scheduleDelayedTask(int64_t microseconds,TaskFunc* proc, void* clientData)
{ if (microseconds < 0)
    microseconds = 0; //DelayInterval 是表示时间差的结构
  DelayInterval timeToDelay((long) (microseconds / 1000000),(long) (microseconds % 1000000)); //创建delayQueue中的一项
  AlarmHandler* alarmHandler = new AlarmHandler(proc, clientData,timeToDelay); //加入DelayQueue
  fDelayQueue.addEntry(alarmHandler); //返回delay task的唯一标志
  return (void*) (alarmHandler->token());
}

复制代码

  delay task的执行都在函数fDelayQueue.handleAlarm()中,handleAlarm()在类DelayQueue中实现。看一下handleAlarm():

复制代码

void DelayQueue::handleAlarm()
{ //如果第一个任务的执行时间未到,则同步一下(重新计算各任务的等待时间)。
  if (head()->fDeltaTimeRemaining != DELAY_ZERO)
    synchronize(); //如果第一个任务的执行时间到了,则执行第一个,并把它从队列中删掉。
  if (head()->fDeltaTimeRemaining == DELAY_ZERO) { // This event is due to be handled:
    DelayQueueEntry* toRemove = head();
    removeEntry(toRemove); // do this first, in case handler accesses queue //执行任务,执行完后会把这一项销毁。
    toRemove->handleTimeout();
  }
}

复制代码

  可能感觉奇怪,其它的任务队列都是先搜索第一个应该执行的项,然后再执行,这里干脆,直接执行第一个完事。那就说明第一个就是最应该执行的一个吧?也就是等待时间最短的一个吧?那么应该在添加任务时,将新任务跟据其等待时间插入到适当的位置而不是追加到尾巴上吧?猜得对不对还得看fDelayQueue.addEntry(alarmHandler)这个函数是怎么执行的。

复制代码

void DelayQueue::addEntry(DelayQueueEntry* newEntry)
{ //重新计算各项的等待时间
  synchronize(); //取得第一项
  DelayQueueEntry* cur = head(); //从头至尾循环中将新项与各项的等待时间进行比较
  while (newEntry->fDeltaTimeRemaining >= cur->fDeltaTimeRemaining) { //如果新项等待时间长于当前项的等待时间,则减掉当前项的等待时间。 //也就是后面的等待时几只是与前面项等待时间的差,这样省掉了记录插入时的时间的变量。
    newEntry->fDeltaTimeRemaining -= cur->fDeltaTimeRemaining; //下一项
    cur = cur->fNext;
  } //循环完毕,cur就是找到的应插它前面的项,那就插它前面吧
  cur->fDeltaTimeRemaining -= newEntry->fDeltaTimeRemaining; // Add “newEntry” to the queue, just before “cur”:
  newEntry->fNext = cur;
  newEntry->fPrev = cur->fPrev;
  cur->fPrev = newEntry->fPrev->fNext = newEntry;
}

复制代码

groupsock:

  groupsock库中包括了GroupEId、Groupsock、GroupsockHelper、NetAddress、NetInterface等类。

  这个是放在单独的库Groupsock中。它封装了socket操作,支持udp多播放支持和一对多单播的功能,tcp socket创建。Groupsock类有两个构造函数:

  一个是ASM(即任意源组播模型):

复制代码

// Constructor for a source-independent multicast group
Groupsock::Groupsock(UsageEnvironment& env, struct in_addr const& groupAddr,
Port port, u_int8_t ttl)
:OutputSocket(env, port),//创建udp socket
deleteIfNoMembers(False), isSlave(False),
fIncomingGroupEId(groupAddr, port.num(), ttl),
fDests(NULL), fTTL(ttl)
{
addDestination(groupAddr, port);//记录组播地址

if (!socketJoinGroup(env, socketNum(), groupAddr.s\_addr)) {//如果非组播地址,则不会进入里面,组播地址,则加入组播
    if (DebugLevel >= 1) {
        env << \*this << ": failed to join group: " << env.getResultMsg() << "\\n";
    }
} // Make sure we can get our source address:
if (ourSourceAddressForMulticast(env) == 0) {//获取本机的地址
    if (DebugLevel >= 0) { // this is a fatal error
        env << "Unable to determine our source address: " << env.getResultMsg() << "\\n";
    }
} if (DebugLevel >= 2) env << \*this << ": created\\n";

}

复制代码

  另一个是SSM(指定信源组播模型):

复制代码

// Constructor for a source-specific multicast group
Groupsock::Groupsock(UsageEnvironment& env, struct in_addr const& groupAddr, struct in_addr const& sourceFilterAddr,Port port)
: OutputSocket(env, port),//创建udp socket
deleteIfNoMembers(False), isSlave(False),
fIncomingGroupEId(groupAddr, sourceFilterAddr, port.num()),
fDests(NULL), fTTL(255)
{
addDestination(groupAddr, port);//记录组播地址 // First try a SSM join. If that fails, try a regular join:
if (!socketJoinGroupSSM(env, socketNum(), groupAddr.s_addr, sourceFilterAddr.s_addr)) {//如果非组播地址,则退出
if (DebugLevel >= 3) {
env << *this << “: SSM join failed: “ << env.getResultMsg();
env << “ - trying regular join instead\n”;
} if (!socketJoinGroup(env, socketNum(), groupAddr.s_addr)) {//如果是组播地址则加入
if (DebugLevel >= 1) {
env << *this << “: failed to join group: “ << env.getResultMsg() << “\n”;
}
}
} if (DebugLevel >= 2) env << *this << “: created\n”;
}

复制代码

  根据上面两个构造函数,我们发现只是加入组播,真正的创建组播在哪里呢?就是在构造函数的初始化列表中的OutputSocket,这个类最终调用了Socket的setupDatagramSocket函数:

复制代码

int setupDatagramSocket(UsageEnvironment& env, Port port,
#ifdef IP_MULTICAST_LOOP
Boolean setLoopback #else Boolean #endif )
{ if (!initializeWinsockIfNecessary()) {
socketErr(env, “Failed to initialize ‘winsock’: “); return -1;
} int newSocket = socket(AF_INET, SOCK_DGRAM, 0); //创建udp socket
if (newSocket < 0) {
socketErr(env, “unable to create datagram socket: “); return newSocket;
} if (setsockopt(newSocket, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuseFlag, sizeof reuseFlag) < 0) {//设置socket 可重用
socketErr(env, “setsockopt(SO_REUSEADDR) error: “);
closeSocket(newSocket); return -1;
} #if defined(__WIN32__) || defined(_WIN32)
// Windoze doesn’t properly handle SO_REUSEPORT or IP_MULTICAST_LOOP
#else #ifdef SO_REUSEPORT if (setsockopt(newSocket, SOL_SOCKET, SO_REUSEPORT, (const char*)&reuseFlag, sizeof reuseFlag) < 0) {//端口复用
socketErr(env, “setsockopt(SO_REUSEPORT) error: “);
closeSocket(newSocket); return -1;
} #endif #ifdef IP_MULTICAST_LOOP const u_int8_t loop = (u_int8_t)setLoopback; if (setsockopt(newSocket, IPPROTO_IP, IP_MULTICAST_LOOP, (const char*)&loop, sizeof loop) < 0) {//数据送到本地回环接口
socketErr(env, “setsockopt(IP_MULTICAST_LOOP) error: “);
closeSocket(newSocket); return -1;
} #endif
#endif

// Note: Windoze requires binding, even if the port number is 0
netAddressBits addr = INADDR\_ANY; #if defined(\_\_WIN32\_\_) || defined(\_WIN32)

#else
if (port.num() != 0 || ReceivingInterfaceAddr != INADDR_ANY) { #endif
if (port.num() == 0) addr = ReceivingInterfaceAddr;

    MAKE\_SOCKADDR\_IN(name, addr, port.num()); if (bind(newSocket, (struct sockaddr\*)&name, sizeof name) != 0) {//绑定socket
        char tmpBuffer\[100\];
        sprintf(tmpBuffer, "bind() error (port number: %d): ",
        ntohs(port.num()));
        socketErr(env, tmpBuffer);
        closeSocket(newSocket); return -1;
    } #if defined(\_\_WIN32\_\_) || defined(\_WIN32)

#else } #endif

// Set the sending interface for multicasts, if it's not the default:
if (SendingInterfaceAddr != INADDR\_ANY) { struct in\_addr addr;
    addr.s\_addr \= SendingInterfaceAddr; if (setsockopt(newSocket, IPPROTO\_IP, IP\_MULTICAST\_IF, (const char\*)&addr, sizeof addr) < 0) {//设置组播的默认网络接口
        socketErr(env, "error setting outgoing multicast interface: ");
        closeSocket(newSocket); return -1;
    }
} return newSocket;

}

复制代码

  而GroupsockHelper.cpp不但定义了setupDatagramSocket函数,还定义了udp Socket的读(readSocket)写(writeSocket)函数。

     上面讲的udp的组播和单播,下面分析一下tcp的单播。

  首先是tcp socket创建:

复制代码

int setupStreamSocket(UsageEnvironment& env,
Port port, Boolean makeNonBlocking)
{ if (!initializeWinsockIfNecessary()) {
socketErr(env, “Failed to initialize ‘winsock’: “); return -1;
} int newSocket = socket(AF_INET, SOCK_STREAM, 0);//创建tcp socket if (newSocket < 0) {
socketErr(env, “unable to create stream socket: “); return newSocket;
} if (setsockopt(newSocket, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuseFlag, sizeof reuseFlag) < 0) { //设置socket 复用
socketErr(env, “setsockopt(SO_REUSEADDR) error: “);
closeSocket(newSocket); return -1;
} // SO_REUSEPORT doesn’t really make sense for TCP sockets, so we // normally don’t set them. However, if you really want to do this // #define REUSE_FOR_TCP
#ifdef REUSE_FOR_TCP #if defined(__WIN32__) || defined(_WIN32)
// Windoze doesn’t properly handle SO_REUSEPORT
#else #ifdef SO_REUSEPORT if (setsockopt(newSocket, SOL_SOCKET, SO_REUSEPORT, (const char*)&reuseFlag, sizeof reuseFlag) < 0) {//设置端口复用
socketErr(env, “setsockopt(SO_REUSEPORT) error: “);
closeSocket(newSocket); return -1;
} #endif
#endif
#endif

// Note: Windoze requires binding, even if the port number is 0

#if defined(__WIN32__) || defined(_WIN32)
#else
if (port.num() != 0 || ReceivingInterfaceAddr != INADDR_ANY) { #endif MAKE_SOCKADDR_IN(name, ReceivingInterfaceAddr, port.num()); if (bind(newSocket, (struct sockaddr*)&name, sizeof name) != 0) {//绑定socket char tmpBuffer[100];
sprintf(tmpBuffer, “bind() error (port number: %d): “, ntohs(port.num()));
socketErr(env, tmpBuffer);
closeSocket(newSocket); return -1;
} #if defined(__WIN32__) || defined(_WIN32)
#else } #endif

if (makeNonBlocking) { if (!makeSocketNonBlocking(newSocket)) {
        socketErr(env, "failed to make non-blocking: ");
        closeSocket(newSocket); return -1;
    }
} return newSocket;

}

复制代码

  那程序是怎么判断用组播还是单播,是tcp还是udp呢?

  在RTSP的setupMediaSubsession()函数中的“SETUP”会向服务端确定是否支持单播或者组播,当收到服务器的应该后,进行下面的处理:

复制代码

… … if (streamUsingTCP) {//如果客服端将streamUsingTcp设置为1,则表明rtp和rtcp进行tcp传输;如果为0,则是udp传输。tcp传输只能是单播,udp传输则可能是单播也可能是组播,服务端的设置,如果服务端支持组播,那只能是组播,反之则是单播。 // Tell the subsession to receive RTP (and send/receive RTCP) // over the RTSP stream:
if (subsession.rtpSource() != NULL)//rtp连接
subsession.rtpSource()->setStreamSocket(fInputSocketNum, subsession.rtpChannelId);//rtpSource()函数获取的fRTPSource在initiate函数根据sdp描述的编码类型创建的
if (subsession.rtcpInstance() != NULL)//rtcp连接
subsession.rtcpInstance()->setStreamSocket(fInputSocketNum, subsession.rtcpChannelId);//rtcpInstance()函数获取的fRTCPInstance在initiate函数中创建的
} else {//udp传输 // Normal case. // Set the RTP and RTCP sockets’ destination address and port // from the information in the SETUP response:
subsession.setDestinations(fServerAddress); //它管理着一个本地socket和多个目的地址,因为是UDP,所以只需知道对方地址和端口即可发送数据。所以我们从服务器获取了新的rtp和rtcp端口加入到目标地址中。
}
… …

复制代码

  setStreamSocket函数最终调用的是RTPInterface::setStreamSocket,将rtp和rtcp的socket保存到streams->fStreamSocketNum(注意:tcp单播:服务端的socket是绑定的,每次客服端请求便会创建一个clientsocket, 而客服端rtsp rtp rtcp协议用就是用的这个socket;udp单播,live555没有采用connect的方式,直接setDestinations函数加入到fDests链表中,发送时候的目标地址在此链表中****),当rtp和rtcp发送数据的时候调用的RTPInterface::sendPacket函数将会用到:

复制代码

void RTPInterface::sendPacket(unsigned char* packet, unsigned packetSize)
{ // Normal case: Send as a UDP packet:
fGS->output(envir(), fGS->ttl(), packet, packetSize);//udp传输 // Also, send over each of our TCP sockets:
for (tcpStreamRecord* streams = fTCPStreams; streams != NULL; streams = streams->fNext) {//tcp传输
sendRTPOverTCP(packet, packetSize, streams->fStreamSocketNum, streams->fStreamChannelId);
}
}

复制代码

  setDestinations()函数定义在MediaSubsession.cpp中:

复制代码

void MediaSubsession::setDestinations(unsigned defaultDestAddress)
{ // Get the destination address from the connection endpoint name // (This will be 0 if it’s not known, in which case we use the default)
unsigned destAddress = connectionEndpointAddress(); if (destAddress == 0) destAddress = defaultDestAddress; struct in_addr destAddr; destAddr.s_addr = destAddress; // The destination TTL remains unchanged:
int destTTL = ~0; // means: don’t change

if (fRTPSocket != NULL) {
    Port destPort(serverPortNum);//这里serverPortNum是在服务端应答中获取rtp的端口号
    fRTPSocket->changeDestinationParameters(destAddr, destPort, destTTL);//这里的fRTPSocket是在前面的initiate函数中创建的

} if (fRTCPSocket != NULL && !isSSM()) { // Note: For SSM sessions, the dest address for RTCP was already set.
Port destPort(serverPortNum+1);//这个是rtcp的端口号
fRTCPSocket->changeDestinationParameters(destAddr, destPort, destTTL);//这里的fRTCPSocket在前面的initiate函数创建的
}
}

复制代码

changeDestinationParameters()定义在Groupsock.cpp中:

复制代码

//改变目的地址的参数 //newDestAddr是新的目的地址 //newDestPort是新的目的端口 //newDestTTL是新的TTL
void Groupsock::changeDestinationParameters( struct in_addr const& newDestAddr,
Port newDestPort, int newDestTTL)
{ if (fDests == NULL) return; //获取第一个目的地址(此处不是很明白:fDest是一个单向链表,每次添加一个目的地址, //都会把它插入到最前目,难道这个函数仅改变最后一个添加的目的地址?)
struct in_addr destAddr = fDests->fGroupEId.groupAddress(); if (newDestAddr.s_addr != 0) { if (newDestAddr.s_addr != destAddr.s_addr && IsMulticastAddress(newDestAddr.s_addr))
{ //如果目的地址是一个多播地址,则离开老的多播组,加入新的多播组。
socketLeaveGroup(env(), socketNum(), destAddr.s_addr);
socketJoinGroup(env(), socketNum(), newDestAddr.s_addr);
}
destAddr.s_addr = newDestAddr.s_addr;
}

portNumBits destPortNum \= fDests->fGroupEId.portNum(); if (newDestPort.num() != 0) { if (newDestPort.num() != destPortNum && IsMulticastAddress(destAddr.s\_addr))
    { //如果端口也不一样,则先更改本身socket的端口 //(其实是关掉原先的socket的,再以新端口打开一个socket)。

changePort(newDestPort); //然后把新的socket加入到新的多播组。 // And rejoin the multicast group:
socketJoinGroup(env(), socketNum(), destAddr.s_addr);
}
destPortNum = newDestPort.num();
fDests->fPort = newDestPort;
}

u\_int8\_t destTTL \= ttl(); if (newDestTTL != ~0)
    destTTL \= (u\_int8\_t) newDestTTL; //目标地址的所有信息都在fGroupEId中,所以改变成员fGroupEId。
fDests->fGroupEId = GroupEId(destAddr, destPortNum, destTTL); //(看起来这个函数好像只用于改变多播时的地址参数, //以上分析是否合理,肯请高人指点)

}

复制代码

liveMedia:

  是很重要的一个库,其不仅包含了实现RTSP Server的类,还包含了针对不同流媒体类型(如TS流、PS流等)编码的类。在该库中,基类是Medium,层次关系非常清晰。在该库中,有几个很重要的类,如RTSPServer、ServerMediaSession、RTPSink、RTPInterface、FramedSource等。  

  从上面这个主要的类结构可以看出,liveMedia库中的基类为Medium,其下又有几个非常重要的部分,一个是×××Subsession,除Medium父类外,所有的×××Subsession类都继承于ServerMediaSubsession;一个是×××Source,MediaSource的frameSource下主要包含FramedFileSource、RTPSource、FramedFilter等几个主要的部分;一个是MediaSink,以继承于RTPSink的类居多。

  此外,还包含了用于处理packet的BufferedPacketFactory和BufferedPacket及其相关子类,用于处理流分析的StreamParser及其子类。

公用类:

  RTPInterface类:发送rtp rtcp数据包;startNetworkReading函数注册tcp读取rtp rtcp数据的回调函数,注册udp读取rtp rtcp数据的回调函数。

  RTCPInstance类:rtcp协议的实现,创建RTPInterface类实例fRTCPInterface。

服务端实现需要以下几个基类:  

  RTSPServer:创建rtsp分tcp socket,注册任务incomingConnectionHandler,在此任务函数里,accept接收客服端连接,创建RTSPClientSession类;

  RTSPClientSession类:注册任务incomingRequestHandler,在此任务函数里,readSocket读取客服端发送的信息,并且解析出cmdName,收到”OPTIONS”命令则通过handleCmd_OPTIONS函数应答,收到”DESCRIBE”则通过handleCmd_DESCRIBE函数应答,收到 “SETUP”命令则通过handleCmd_SETUP函数应答,收到 “TEARDOWN”、 “PLAY”、”PAUSE”、”GET_PARAMETER”通过handleCmd_withinSession函数应答,如果都不是则通过handleCmd_notSupported函数应答。  

    ServerMediaSubsession类:多媒体流可能包含几个子流,每个流都带有fTrackId,比如视频流track1,音频流track2

  ServerMediaSession类:一个多媒体流增加addSubsession子流,产生generateSDPDescription SDP描述。

      FramedSource类:提供虚函数doGetNextFrame函数去获取信息,此函数具体实现在派生类中,比如我的是在OpenFileSource类实现。

  RTPSink类:服务端数据是从 FramedSource流到 RTPSink,并且创建RTPInterface类实例fRTPInterface。

单播:

  StreamState类:startPlaying函数开始播放,endPlaying函数结束播放,pause函数暂停播放。

  Destinations类:udp连接,保存isTCP为false,目标地址,rtp的端口,rtcp的端口;tcp连接,保存isTcp为TRUE,tcp socket值,rtp rtcp通道ID。

  OnDemandServerMediaSubsession类:单播的时候,通过getStreamParameters函数创建udp socket,创建FramedSource, 创建RTPSink,创建StreamState,创建Destinations,当收到“PLAY”的命令时调用startStream开始播放,当收到“PAUSE”时调用pauseStream函数暂停播放,当收到“TERADOWN”时停止播放。 

组播:

  PassiveServerMediaSubsession类:getStreamParameters函数改变rtp和rtcp的目标地址。

客服端实现需要以下几个基类:

  RTPSource:客户端数据则是从 RTPSource 流到 XXXFileSink, 并且创建RTPInterface类实例fRTPInterface。

  MediaSubsession类:initiate函数创建rtp rtcp socket,并且根据服务端传来的编码类型fCodecName来创建相应的RTPSource,创建RTCPInstance;解析SDP的各种属性;setDestinations函数设置服务端地址。

  RTSPClient: 创建tcp socket,连接rtsp服务端,sendOptionsCmd函数发送“OPTIONS”命令; describeURL函数则是发送“DESCRIBE”命令;announceSDPDescription函数则是发送“ANNOUNCE”命令;setupMediaSubsession函数则是发送“SETUP”命令,并且设置socket的目标地址;playMediaSession函数则是发送“PLAY”命令;pauseMediaSession函数则是发送“PAUSE”命令;等等。主要是RTSP的通信处理。

  基本上,整个liveMedia库的主要类结构就是这样。

  在http://www.live555.com上的相关文档中提到穿透防火墙的问题,方法是开启一个HTTP的tunnel,然后我们可以在liveMedia库中找到一个RTSPOverHTTPServer的类,该类解决了这样的问题。

mediaServer:

   Live555MediaServer提供了main函数,DynamicRTSPServer继承了RTSPServer并重写了虚函数lookupServerMediaSession。用不到。

Kubernetes 部署 Mysql 8.0 数据库(单节点)-腾讯云开发者社区-腾讯云

Excerpt

Mysql 是我们常用的关系型数据库,在项目开发、测试、部署到生成环境时,经常需要部署一套 Mysql 进行数据存储。这里介绍下如何在 Kubernetes 环境中部署用于开发、测试的环境的 Mysql 数据库,当然,部署的是单节点模式,并非用于生产环境的主从或集群模式。整理了一份328页MySQ学习笔记


系统环境:

  • Mysql 版本:8.0.19
  • Kubernetes 版本:1.17.4
  • 操作系统版本:CentOS 7.8

一、简介

Mysql 是我们常用的关系型数据库,在项目开发、测试、部署到生成环境时,经常需要部署一套 Mysql 进行数据存储。这里介绍下如何在 Kubernetes 环境中部署用于开发、测试的环境的 Mysql 数据库,当然,部署的是单节点模式,并非用于生产环境的主从或集群模式。

单节点的 Mysql 部署简单,且配置存活探针,能保证快速检测 Mysql 是否可用,当不可用时快速进行重启。

二、Mysql 参数配置

在使用 Kubernetes 部署应用后,一般会习惯与将应用的配置文件外置,用 ConfigMap 存储,然后挂载进入镜像内部。这样,只要修改 ConfigMap 里面的配置,再重启应用就能很方便就能够使应用重新加载新的配置,很方便。

创建 ConfigMap 存储 Mysql 配置文件

创建 Kubernetes 的 ConfigMap 资源,用于存储 Mysql 的配置文件 my.cnf 内容:

mysql-config.yaml

1
apiVersion: v1 kind: ConfigMap metadata: name: mysql-config labels: app: mysql data: my.cnf: |- [client] default-character-set=utf8mb4 [mysql] default-character-set=utf8mb4 [mysqld] max_connections = 2000 secure_file_priv=/var/lib/mysql sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION

通过 Kubectl 工具部署 ConfigMap

通过 kubectl 工具部署 Kubernetes ConfigMap 资源,命令如下:

  • -n:指定部署应用的 Namespace 命名空间。
1
$ kubectl create -f mysql-config.yaml -n mydlqcloud

三、Mysql 数据存储

Kubernetes 部署的应用一般都是无状态应用,部署后下次重启很可能会漂移到不同节点上,所以不能使用节点上的本地存储,而是徐亚网络存储对应用数据持久化,PV 和 PVC 是 Kubernetes 用于与储空关联的资源,可与不同的存储驱动建立连接,存储应用数据,所以接下来我们要创建 Kubernetes PV、PVC 资源。

PV、PVC 资料可以参考:

http://www.mydlq.club/article/38/

创建 PV、PVC 绑定 Mysql 存储空间

PV 支持多种存储驱动,不同存储驱动的 PV 配置方式是不同的,需要根据你的存储系统来配置 PV 参数。这里用的是 NFS 存储(共享网络文件存储系统),可以按照以下方式进行配置:

mysql-storage.yaml

1
## PV apiVersion: v1 kind: PersistentVolume metadata: name: mysql labels: app: mysql #设置 pv 的 label 标签 spec: capacity: storage: 50Gi #设置 pv 存储资源大小 accessModes: - ReadWriteOnce mountOptions: - hard - nfsvers=4.1 nfs: #指定使用 NFS 存储驱动 server: 192.168.2.11 #指定 NFS 服务器 IP 地址 path: /nfs/mysql #指定 NFS 共享目录的位置,且需提前在该目录中创建 mysql 目录 persistentVolumeReclaimPolicy: Retain --- ## PVC kind: PersistentVolumeClaim apiVersion: v1 metadata: name: mysql spec: resources: requests: storage: 50Gi #设置 pvc 存储资源大小 accessModes: - ReadWriteOnce selector: matchLabels: app: mysql #根据 Label 选择对应 PV

通过 Kubectl 工具部署 PV、PVC

通过 kubectl 工具部署 Kubernetes PV、PVC 资源,命令如下:

  • -n:指定部署应用的 Namespace 命名空间。
1
$ kubectl create -f mysql-storage.yaml -n mydlqcloud

四、Kubernetes 部署 Mysql

创建 Deployment 部署 Mysql

创建用于 Kubernetes Deployment 来配置部署 Mysql 的参数,需要配置 Mysql 的镜像地址、名称、版本号,还要配置其 CPU 与 Memory 资源的占用,通过环境变量配置 Mysql 的 root 用户默认密码,配置探针监测应用可用性,配置 Volume 挂载之前创建的 PV、PVC、ConfigMap 资源等等,内容如下:

mysql-deploy.yaml

1
## Service apiVersion: v1 kind: Service metadata: name: mysql labels: app: mysql spec: type: NodePort ports: - name: mysql port: 3306 targetPort: 3306 nodePort: 30336 selector: app: mysql --- ## Deployment apiVersion: apps/v1 kind: Deployment metadata: name: mysql labels: app: mysql spec: replicas: 1 selector: matchLabels: app: mysql template: metadata: labels: app: mysql spec: containers: - name: mysql image: mysql:8.0.19 ports: - containerPort: 3306 env: - name: MYSQL_ROOT_PASSWORD ## 配置Root用户默认密码 value: "123456" resources: limits: cpu: 2000m memory: 512Mi requests: cpu: 2000m memory: 512Mi livenessProbe: initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 5 successThreshold: 1 failureThreshold: 3 exec: command: ["mysqladmin", "-uroot", "-p${MYSQL_ROOT_PASSWORD}", "ping"] readinessProbe: initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 5 successThreshold: 1 failureThreshold: 3 exec: command: ["mysqladmin", "-uroot", "-p${MYSQL_ROOT_PASSWORD}", "ping"] volumeMounts: - name: data mountPath: /var/lib/mysql - name: config mountPath: /etc/mysql/conf.d/my.cnf subPath: my.cnf - name: localtime readOnly: true mountPath: /etc/localtime volumes: - name: data persistentVolumeClaim: claimName: mysql - name: config configMap: name: mysql-config - name: localtime hostPath: type: File path: /etc/localtime

参数简介:

  • ports: 配置镜像映射端口。
  • env: 镜像环境变量配置,其中 MYSQL_ROOT_PASSWORD 是 Mysql 镜像用于配置 root 用户默认密码变量。
  • resources: 配置 CPU、Memory 资源限制,可以通过配置该值来配置 Pod 的 QoS 级别。
  • livenessProbe: 配置存活探针,定时检测 Mysql 应用运行状态,如果检测到 Mysql 挂掉将进行重启操作。
  • readinessProbe: 配置就绪探针,定时检测 Mysql 应用启动状态,如果启动成功将允许流量涌入,启动失败将进行重启操作。
  • command: 探针执行探测时执行的探测命令。
  • volumeMounts: 存储卷挂载配置,用于镜像内存储的挂载配置,与 volumes 中对于的 name 进行绑定。
  • volumes: 存储卷配置,可配置使用 pvc、hostPath、emptyDir、nfs 等存储,需要配置 name 值与 VolumeMounts 进行绑定。

通过 Kubectl 工具部署 PV、PVC

通过 kubectl 工具部署 Deployment 来创建 Msyql,命令如下:

  • -n:指定部署应用的 Namespace 命名空间。
1
$ kubectl create -f mysql-deploy.yaml -n mydlqcloud

五、测试 Mysql 是否能够正常使用

接下来启动个 Mysql 镜像,使用里面的 Msyql 客户端工具,对部署在 Kubernetes 中的 Mysql 进行连接,测试是否能够正常可用。

这里本人 Kubernetes 集群地址为 192.168.2.11 部署的 Mysql 的 NodePort 端口号为 30336,所以这里启动一个对应版本的 Mysql 镜像,然后进入镜像内部,输入 Mysql 登录命令进行测试。

运行一个 mysql 镜像,并且进入镜像内部:

1
$ docker run -it mysql:8.0.19 /bin/bash

在镜像内部命令行中输入 mysql 登录命令,测试是否能够正常登录:

1
$ mysql -h 192.168.2.11 -P 30336 --user=root --password=123456

显示如下:

1
Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 592 Server version: 8.0.19 MySQL Community Server - GPL Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql>

可以看到,已经成功连接数据库,说明数据库能正常使用。

示例地址: