项目部署-

项目部署-Ubuntu

参考文章

需要更新的地

  1. uwsgi中http和socket的不同

  2. nginx中server部分

  3. docker的基本使用

wsgi

1.1 什么是wsgi

wsgi全称是web server gateway interface,是一套接口标准协议/规范,指定了web服务器和python web应用或框架之间的标准接口,以提高python框架在服务器之间的移植性。官方文档

我们可以将wsgi分成两个组件:

  • server:常见的又uWSGI,gunicorn
  • Application:Django,Flask等中实现的application

1.2 wsgi有什么用

web应用处理请求的具体流程:

  1. 用户通过浏览器将请求发送至服务器
  2. web服务器将请求转交给web应用程序,web应用程序处理请求
  3. web应用程序将处理结果返回给服务器,服务器返回响应结果给浏览器
  4. 浏览器收到响应并展示出来

img

相应的web服务器web应用都有很多,它们如何进行通信呢,就需要指定一种通信标准WSGI就是一种通信的协议规范。

wsgi作为web服务器web应用之间的桥梁,主要有两个作用:

  • Web服务器知道如何调用python应用程序,并把请求发送给python应用程序
  • python应用程序知道用户的请求是什么,以及如何将处理结果返回Web服务器

1.3 wsgi的两个方面

wsgi接口具有两个方面:服务器网关 以及 应用程序框架端

server端会先收到用户的请求,然后会根据规范的要求调用application端

  1. 服务器

    WSGI服务器端接受HTTP请求,构建environ对象,然后调用application对象,最后将对象返回的HTTP Response返回给浏览器

    Web服务器,主要是实现相应的信息转换,将网络请求中的信息,按照HTTP协议将内容拿出,同时按照WSGI协议组装成新的数据,同时将提供的start_response传递给Application。最后接收Application返回的内容,按照WSGI协议解析出。最终按照HTTP协议组织好内容返回就完成了一次请求。

    server端需要知道如何去找application的入口,我们在使用server服务器(gunicorn、uwsgi)时都需要指定application的,例如:

    gunicorn:配置参数的wsgi_app

    wsgi_app
    Default: None
    
    A WSGI application path in pattern $(MODULE_NAME):$(VARIABLE_NAME).
    

    uwsgi:配置文件的module参数

    module = for_test.wsgi:application     //  指定wsgi模块下的application对象
    

    下面实践一个简单的server服务器:

    """
    Created by zhoujianhui on 2021-01-06
    """
    from cookie_test.asgi import application
    
    __author__ = '周剑辉'
    
    # coding:utf-8
    """
    desc: WSGI服务器实现
    """
    from wsgiref.util import setup_testing_defaults
    from wsgiref.simple_server import make_server
    
    # A relatively simple WSGI application. It's going to print out the
    # environment dictionary after being updated by setup_testing_defaults
    def simple_app(environ, start_response):
        setup_testing_defaults(environ)
    
        status = '200 OK'
        headers = [('Content-type', 'text/plain; charset=utf-8')]
    
        start_response(status, headers)
    
        ret = [("%s: %s\n" % (key, value)).encode("utf-8")
               for key, value in environ.items()]
        return ret
    
    with make_server('', 8000, simple_app) as httpd:
        print("Serving on port 8000...")
        httpd.serve_forever()
    

    文中简单实现了一个 application和服务器

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rPQgWntu-1624859753218)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210330003216559.png)]

  2. application/应用程序

    应用程序对象只是一个接受两个参数的可调用对象。。术语“对象”不应被误解为需要实际的对象实例:函数,方法,类或具有__call__方法的实例都可以用作应用程序对象。

    当server按照WSGI的规范调用了application之后,application就可以开始处理客户端的请求了,处理请求之后,application对象需要返回处理结果给server端。处理请求和返回结果这两个事情,都和server调用application对象时传递的两个参数有关。

    application接收两个参数,两个参数分别是:environ和start_response

    • environ是一个字典,里面储存了HTTP request相关的所有内容,比如header、请求参数等等
    • start_response是一个WSGI 服务器传递过来的函数,用于将response header,状态码传递给Server。

    调用 start_response 函数负责将响应头、状态码传递给服务器, 响应体则由application函数返回给服务器, 一个完整的http response 就由这两个函数提供。

    下面实现一个简单的application

    HELLO_WORLD = b"Hello world!\n"
    
    def application(environ, start_response):
        status = '200 OK'
        response_headers = [('Content-type', 'text/plain')]
        start_response(status, response_headers)
        return [HELLO_WORLD]
    

    我们可以看下django源码中application:

    • 1.首先是 wsgi.py

      application = get_wsgi_application()
      
    • get_wsgi_application是一个函数,返回 WSGIHandler实例对象

      def get_wsgi_application():
          """
          The public interface to Django's WSGI support. Return a WSGI callable.
      
          Avoids making django.core.handlers.WSGIHandler a public API, in case the
          internal WSGI implementation changes or moves in the future.
          """
          django.setup(set_prefix=False)
          return WSGIHandler()
      
    • WSGIHandler类是一个实现了__call__方法的类

      class WSGIHandler(base.BaseHandler):
          request_class = WSGIRequest
      
          def __init__(self, *args, **kwargs):
              super().__init__(*args, **kwargs)
              self.load_middleware()
      
          def __call__(self, environ, start_response):
              set_script_prefix(get_script_name(environ))
              signals.request_started.send(sender=self.__class__, environ=environ)
              request = self.request_class(environ)
              response = self.get_response(request)
      
              response._handler_class = self.__class__
      
              status = '%d %s' % (response.status_code, response.reason_phrase)
              response_headers = [
                  *response.items(),
                  *(('Set-Cookie', c.output(header='')) for c in response.cookies.values()),
              ]
              start_response(status, response_headers)
              if getattr(response, 'file_to_stream', None) is not None and environ.get('wsgi.file_wrapper'):
                  # If `wsgi.file_wrapper` is used the WSGI server does not call
                  # .close on the response, but on the file wrapper. Patch it to use
                  # response.close instead which takes care of closing all files.
                  response.file_to_stream.close = response.close
                  response = environ['wsgi.file_wrapper'](response.file_to_stream, response.block_size)
              return response
      
      

      我们可以看到,WSGIHandler的**call**方法有两个参数environ和start_response,并返回 response

  3. wsgi中间件

    那么middleware是一种运行在server和application中间的应用(一般都是Python应用)。middleware同时具备server和application角色,对于server来说,它是一个application;对于application来说,它是一个server。

    通常,中间件的存在对于接口的“服务器/网关”和“应用程序/框架”双方都是透明的,并且不需要任何特殊支持。希望将中间件合并到应用程序中的用户只需将中间件组件提供给服务器(就好像它是一个应用程序一样),并配置中间件组件来调用该应用程序,就好像中间件组件是一个服务器一样。当然,中间件包装的“应用程序”实际上可能是包装另一个应用程序的另一个中间件组件,依此类推,从而创建了所谓的“中间件堆栈”。

    参考:中间件:兼顾双方的组件

uWSGI

2.1 uWSGI是什么

  • uWSGI是一个服务器,实现了uwsgi协议、wsgi协议和http协议。

  • uwsgi协议

    是uWSGI独有的协议,uWSGI服务器使用的本地协议

    它是一个二进制协议,可以携带任何类型的数据。一个uwsgi分组的头4个字节描述了这个分组包含的数据类型。

WSGI,uwsgi, uWSGI: 实现过程图解:

在这里插入图片描述

2.2 uWSGI下载

  • 去官网下载最新版(官方教程和相关技术文章都是以最新版编写的)

    https://uwsgi-docs.readthedocs.io/en/latest/Download.html页面,下载Stable/LTS版本的源文件

  • 解压、安装

    tar -xzvf uwsgi.tar.gz 
    # 进入解压目录
    sudo python3 setup.py install
    
  • 运行一下uWSGI

注意:如果你在虚拟环境运行你的项目,你必须在虚拟环境中安装uWSGI

2.3 uWSGI简单使用

  1. 第一个WSGI应用

    将下面代码写入first.py: 我们将其作为wsgi协议的application

    def application(env, start_response):
        start_response('200 OK', [('Content-Type','text/html')])
        return [b"H&W,goodbye!"]
    

    uWSGI作为wsgi协议的server,调用这个application,将请求传入。

    uwsgi --http 127.0.0.1:9090 --wsgi-file first.py
    

    然后浏览器访问这个ip+端口:OK!

  2. 添加并发和监控

    uWSGI启动一个单一的进程和一个单一的线程,但是想增加并发性。

    使用 --process 选项增加进程 使用 --threads选项增加线程

    uwsgi --http :9090 --wsgi-file first.py --master --processes 4 --threads 2 --stats 127.0.0.1:9191
    

    生成4个进程(每个进程2个线程),一个master进程 (在Inc死掉的时候会生成它们) 和HTTP路由器 (见前面)。

    对你的应用进行几次请求,然后telnet到端口9191,你会获得大量有趣的信息。你可能想要使用”uwsgitop” (仅需 pip install 来安装它),这是一个类似于top的工具,用来监控实例。

  3. 将其放在Nginx之后

    通常将nginx作为服务器最前端,如果是静态请求,nginx自己处理,如果是动态请求,nginx将动态请求通过 uWSGI传给应用程序。

    一个常用的nginx配置如下:

    location / {
    include uwsgi_params;
    uwsgi_pass 127.0.0.1:3031;
    }

    使用命令行运行uWSGI:

    uwsgi --socket 127.0.0.1:3031 --wsgi-file foobar.py --master --processes 4 --threads 2 --stats 127.0.0.1:9191
    
    

    已经移除了HTTP路由器,因为我们的“worker” (被分配给uWSGI的进程) 本地使用uwsgi协议。

    命令行比较繁琐,我们是用配置文件运行

    uwsgi配置文件-uWSGI支持多种配置风格,比如 xml,ini,json 等等都可以。下面使用ini。

    [uwsgi]
    socket = 127.0.0.1:3031
    chdir = /home/foobar/myproject/
    wsgi-file = myproject/wsgi.py
    processes = 4
    threads = 2
    stats = 127.0.0.1:9191
    

    运行配置文件:

    uwsgi yourfile.ini
    

    列出比较常用的配置参数

    chdir=/xxx/xxx # 指定项目目录
    home=/xxx/xxx # 指定虚拟环境变量
    wsgi-file=xxx # 指定加载WSGI文件
    socket=xxx # 指定uwsgi的客户端将要连接的socket的路径(使用UNIX socket的情况)或者地址(使用网络地址的情况)。
    callable=xxx # uWSGI加载的模块中哪个变量将被调用
    master=true # 指定启动主进程
    processes=4 # 设置工作进程的数量
    threads=2 # 设置每个工作进程的线程数
    vacuum=true # 当服务器退出时自动删除unix socket文件和pid文件
    logfile-chmod=644 # 指定日志文件的权限
    daemonize=%(chdir)/xxx.log # 进程在后台运行,并将日志打印到指定文件
    pidfile=%(chdir)/xxx.pid # 在失去权限前,将主进程pid写到指定的文件
    uid=xxx # uWSGI服务器运行时的用户id
    gid=xxx # uWSGI服务器运行时的用户组id
    procname-prefix-spaced=xxx # 指定工作进程名称的前缀
    

更多配置参数官方文档

2.4 uwsgi部署中遇到的问题

  1. uwsgi启动次数太多问题

    问题描述:

    打开nginx端口,一直显示 Internal Server Error

    打开uwsgi的错误日志:

    *** Starting uWSGI 2.0.19.1 (64bit) on [Tue Mar 30 15:48:09 2021] ***
    compiled with version: 9.3.0 on 02 March 2021 12:41:15
    os: Linux-3.10.0-957.el7.x86_64 #1 SMP Thu Nov 8 23:39:32 UTC 2018
    nodename: bjxg-bd-slave03
    machine: x86_64
    clock source: unix
    pcre jit disabled
    detected number of CPU cores: 48
    current working directory: /data/tornado/manage_project/business_management_system
    detected binary path: /data/anaconda3/envs/jianhui/bin/uwsgi
    chdir() to /data/tornado/manage_project/business_management_system
    your processes number limit is 4096
    your memory page size is 4096 bytes
    detected max file descriptor number: 102400
    lock engine: pthread robust mutexes
    thunder lock: disabled (you can enable it with --thunder-lock)
    probably another instance of uWSGI is running on the same address (127.0.0.1:5556).
    bind(): Address already in use [core/socket.c line 769]
    — no python application found, check your startup logs for errors —
    [pid: 352529|app: -1|req: -1/40] 43.228.38.242 () {42 vars in 842 bytes} [Tue Mar 30 07:48:18 2021] GET /list/ => generated 21 bytes in 0 msecs (HTTP/1.1 500) 2 headers in 83 bytes (0 switches on core 0)
    — no python application found, check your startup logs for errors —
    [pid: 352529|app: -1|req: -1/41] 43.228.38.242 () {42 vars in 842 bytes} [Tue Mar 30 07:48:19 2021] GET /list/ => generated 21 bytes in 0 msecs (HTTP/1.1 500) 2 headers in 83 bytes (0 switches on core 0)

    重点在probably another instance of uWSGI is running on the same address (127.0.0.1:5556).

    这句日志的直白的翻译出来就是:已经有uWSGI 实例运行在 这个地址上了

    解决方法:

    杀死所有在5556端口上的进程,然后重新启动uWSGI

    sudo fuser -k 5556/tcp
    uwsgi  [配置文件]
    

    uwsgi的日志很重要,一定要重视!!!

  2. uwsgi安装启动 错误描述

    uwsgi: error while loading shared libraries:libssl.so.1.1: cannot open shared object file: No such file or directory

    解决方法

    • cp /data/anaconda3/lib/libssl.so.1.1 /lib64/—方法1

    • ln -s /data/anaconda3/lib/libssl.so.1.1 /lib64/—方法2

    • 上述两个方法都可以,但是lib64 文件夹没有相关权限,针对这种情况,解决方法如下:

      • 方法一:用anaconda创建虚拟环境并且用conda install 安装uWSGI,这样下载的uWSGI没有问题
      • 方法二:用gunicorn部署服务

2.5 http和socket(UNIX以及TCP)的不同

Nginx

可以参考的文章

nginx

就是要让你搞懂Nginx,这篇就够了!

Nginx 从入门到实践,万字详解!

3.1 下载

  • 首先更新软件源

    可能出现 vim中删除键无法使用的情况,解决方法如下:

一、vi编辑模式下Backspace无法退格删除

命令模式下输入:set nocp

二、vi编辑模式下上下左右出现字母

sudo vi /etc/vim/vimrc.tiny

修改set compatible 为 set nocompatible 设置是否兼容

添加 set backspace=2 设置 backspace可以删除任意字符

解决方案:

1、使用国内的软件源在软件下载速度方面相对比较快,找到软件源文件sources.list的位置

img

2、在更新软件源之前建议先把原来的软件源文件进行备份,避免更改错误造成无法恢复。

img

3、备份完成后,打开软件源文件进行更改,下图是把软件源修改为网易软件源。修改完成后保存即可。

img

img

5、修改完成后使用sudo apt-get update命令进行更新软件源即可

  • 下载安装nginx
sudo add-apt-repository ppa:nginx/stable
apt-get update
apt-get install nginx

​ 使用 service --status-all命令查看一下:

 [ + ]  network-manager
 [ + ]  networking
 [ + ]  nginx
 [ + ]  ondemand
 [ - ]  plymouth
 [ - ]  plymouth-log
......

看到带+号的nginx,表示一切OK

  • 查看

    • 查看 linux对应的ip

    • 使用同一局域网的主机,通过浏览器访问 这个ip,如果能看到下面这个,Nginx服务正常。

    img

3.2 基本使用

3.2.1 upstream
  • 定义一组服务器,这些服务器可以监听不同的端口,而且监听在TCP和UNIX域套接字的服务器可以混用。

  • 分配方式

    1. 轮询

      请求按照时间顺序轮流分配到不同的后端服务器

    2. weight权重

      指定轮询比率,weight和访问几率成正比。

      upstream backend {
          server backend1.example.com weight=5;
          server 127.0.0.1:8080       max_fails=3 fail_timeout=30s;
          server unix:/tmp/backend3;
      }
      

      上面的例子,如果有七个请求,前五个发送到第一个服务器,第六和第七分别发送到第二个和第三个服务器。

    3. ip_hash

      每个请求按照访问ip(即Nginx的前置服务器或者客户端IP)的hash结果分配,这样每个访客会固定访问一个后端服务器,可以解决session一致问题。

      upstream backend {
          ip_hash;
          server 192.168.1.101:7777;
          server 192.168.1.102:8888;
          server 192.168.1.103:9999;
      }
      
    4. fair

      按照后端响应的时间来分配请求,响应时间短的后端服务器有限分配请求。

      upstream backend {
          server 192.168.1.101;
          server 192.168.1.102;
          server 192.168.1.103;
          fair;
      }
      
  • 语法

    语法为 server address [parameters]

    参数说明:

    server:关键字,必选

    address:主机名、域名、ip或unix socket,也可指定端口号,必选

    parameters:可选参数,如下:

    1.down 当前server已停用

    2.backup:备用服务器,其余服务器挂掉或者很忙的时候才分配请求

    3.weight:表示当前server负载权重,权重越大被请求的概率越大。默认是1

    4.max_fails和fail_timeout一般会关联使用,如果某台server在fail_timeout时间内出现了max_fails次连接失败,那么Nginx会认为其已经挂掉了,从而在fail_timeout时间内不再去请求它,fail_timeout默认是10s,max_fails默认是1,即默认情况是只要发生错误就认为服务器挂掉了,如果将max_fails设置为0,则表示取消这项检查。

    e.g.:

    upstream backend {
        server    backend1.example.com weight=5;
        server    127.0.0.1:8080 max_fails=3 fail_timeout=30s;
        server    unix:/tmp/backend3;           
    }
    

    img

3.2.2 Server
3.2.3

管理系统后端的nginx文件

upstream business-front{
    server 172.25.102.72:17401;
    fair;
}

upstream business-backend{
    server 172.25.102.72:17411;
    fair;
}

server {
    listen 80;
    server_name admin.business.infinities.com.cn;
    access_log /data/logs/admin.business.infinities.com.cn.access.log;
    error_log /data/logs/admin.business.infinities.com.cn.error.log;
    root /data/tornado/manage_project/business_management_system;

    location / {
      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_pass http://business-front/;
    }

    location /api/ {
      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_pass http://business-backend/;
    }

}

docker-简单使用

镜像和容器的不同

  • 镜像

    是一个静态文件,一个独立的文件系统,有一层层的只读层构成

    img

多个只读层叠在一起,多个只读层通过指针连接在一起,linux将这些层封装合并起来,提供一个统一的视角。

统一文件系统(union file system)技术能够将不同的层整合成一个文件系统,为这些层提供了一个统一的视角,这样就隐藏了多层的存在,在用户的角度看来,只存在一个文件系统。

  • 容器

    容器是运行中的镜像,它比镜像多了一个可读可写层

    docker 容器=镜像+可读可写层

    img

  • 镜像和容器的不同

    镜像中包含了应用程序运行的所必须的操作系统应用文件,但不包含内核

    容器镜像多了一层可读可写层

    运行中的容器被定义为一个统一的文件系统加上隔离的进程空间和包含其中的进程

    10张图带你深入理解Docker容器和镜像

  • 容器创建的指令

    第一种:

    1.docker create

    镜像添加了一个可读写层,构成了一个新的容器,但是这个容器没有运行

    2.docker start <容器id>

    为容器创建了一个进程隔离空间

    第二种:

    docker run [OPTIONS]

    创建并启动一个容器

    **-d, --detach=false, 指定容器运行于前台还是后台,如果是后台 -d 或者 -d=true即可 **
    -i, --interactive=false, 打开STDIN,用于控制台交互
    -t, --tty=false, 分配tty设备,该可以支持终端登录,默认为false
    -u, --user="", 指定容器的用户
    -a, --attach=[], 登录容器(必须是以docker run -d启动的容器)
    -w, --workdir="", 指定容器的工作目录
    -c, --cpu-shares=0, 设置容器CPU权重,在CPU共享场景使用
    -e, --env=[], 指定环境变量,容器中可以使用该环境变量
    -m, --memory="", 指定容器的内存上限
    -P, --publish-all=false, 指定容器暴露的端口 e.g:-p 17411:80 **
    -p, --publish=[], 指定容器暴露的端口
    -h, --hostname="", 指定容器的主机名
    -v, --volume=[], 给容器挂载存储卷,挂载到容器的某个目录
    e.g.

    **-v /data/tornado/manage_project/business_management_system:/code **
    –volumes-from=[], 给容器挂载其他容器上的卷,挂载到容器的某个目录
    –cap-add=[], 添加权限,权限清单详见:http://linux.die.net/man/7/capabilities
    –cap-drop=[], 删除权限,权限清单详见:http://linux.die.net/man/7/capabilities
    –cidfile="", 运行容器后,在指定文件中写入容器PID值,一种典型的监控系统用法
    –cpuset="", 设置容器可以使用哪些CPU,此参数可以用来容器独占CPU
    –device=[], 添加主机设备给容器,相当于设备直通
    –dns=[], 指定容器的dns服务器
    –dns-search=[], 指定容器的dns搜索域名,写入到容器的/etc/resolv.conf文件
    –entrypoint="", 覆盖image的入口点
    –env-file=[], 指定环境变量文件,文件格式为每行一个环境变量

1.创建镜像和容器

1.用Dockerfile 来创建镜像

参考文档

Docker — 从入门到实践

Docker Dockerfile

Docker 创建镜像

Docker Dockerfile 定制镜像

docker build来创建一个新的镜像。

1.docker build 构建镜像

docker是一个C/S架构的应用,分为Docker客户端(docker命令)和Docker引擎(dockerd守护进程),

Docker 的引擎提供了一组 REST API客户端通过这组APIDocker引擎交互,从而完成各种任务

首先需要了解的一点 docker build命令构建镜像,其实并非在本地构建,而是在服务端,也就是docker引擎中构

我们构建镜像的时候,经常会将一些文件复制进镜像,比如COPY 、ADD命令,而镜像在**服务端(引擎)**中构建,如何让服务端获取本地文件呢。

我们拿下面这条命令举例:

helloworld-app
├── Dockerfile
└── docker
    ├── app-1.0-SNAPSHOT.jar
    ├── hello.txt
    └── html
        └── index.html

我们在文件夹 helloworld-app中运行:

docker build -t hello-app:1.0 docker

我们指定docker 文件夹 路径为上下文的路径,这时docker build会将这个路径下的所有内容打包,然后上传给Docker引擎,这样Docker引擎收到这个上下文包,然后 Dockerfile 如果COPY 的话是从这个上下文包中复制文件到服务器。

如果在 Dockerfile 中这么写:

COPY hello.txt .

这并不是要复制执行 docker build 命令所在的目录下的 hello.txt,也不是复制 Dockerfile 所在目录下的 hello.txt,而是复制 上下文(context) 目录下的 hello.txt

但是上面构建镜像的命令会出错:

unable to prepare context: unable to evaluate symlinks in Dockerfile path: lstat /Users/haohao/opensource/helloworld-app/docker/Dockerfile: no such file or directory

找不到Dockerfile文件

这里我们要明白的一点,docker build 构建镜像,docker客户端默认从 构建上下文的路径找名字为Dockerfile的构建文件,实际的Dockerfile文件不在上下文路径docker文件夹下面,所以出错。

我们可以指定 Dockerfile文件 :参数 -f <指定的Dockerfile文件>

$ docker build -f Dockerfile -t hello-app:1.0 docker                                                                                 
Sending build context to Docker daemon  96.61MB
Step 1/3 : FROM busybox
 ---> d8233ab899d4
Step 2/3 : COPY hello.txt .
 ---> 3305fc373120
Step 3/3 : COPY html/index.html .
 ---> efdefc4e6eb2
Successfully built efdefc4e6eb2
Successfully tagged hello-app:1.0

上下文件路径不要放一些用不到的内容,这样会导致构建镜像缓慢且易失败

  • 首先你得创建一个Dockerfile
2 .Dockerfile的基本语法
  • 使用 **#**来注释

  • FROM

    指令告诉 Docker 使用哪个镜像作为基础

    这必须是第一条指令。

    有可以拿来使用的服务类的镜像:如 nginx、redis、mongo和mysql等

    有一些方便开发、构建、运行各种语言应用的镜像:如node、python、openjdk等

    e.g.

    FROM python:3.7.10

  • RUN

    • run 命令是在当前镜像的基础上执行指定命令,然后提交为新的镜像

    • 用于执行后面跟着的命令行命令。有以下两种格式

      ​ 1.shell格式

      RUN <命令行命令>

      e.g.

      RUN pip install -r /code/requirements.txt -i https://mirrors.aliyun.com/pypi/simple

      像是直接在命令行中输入命令一样

      2.exec 格式

      RUN [“可执行文件”, “参数1”, “参数2”]

      像是函数式的调用

    • 每次执行run都相当于创建了一个新的镜像,如果创建的镜像过多,会显得臃肿,且有层数限制,不能超过127层。

      所以 考虑将类似功能的 run 命令 用 && 连接起来,并用 **\ **换行:

      e.g:

      FROM debian:jessie
      RUN buildDeps=‘gcc libc6-dev make’
      && apt-get update
      && apt-get install -y $buildDeps
      && wget -O redis.tar.gz “http://download.redis.io/releases/redis-3.2.5.tar.gz”
      && mkdir -p /usr/src/redis
      && tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1
      && make -C /usr/src/redis
      && make -C /usr/src/redis install
      && rm -rf /var/lib/apt/lists/*
      && rm redis.tar.gz
      && rm -r /usr/src/redis
      && apt-get purge -y --auto-remove $buildDeps

  • COPY

    复制指令,从上下文目录复制文件到容器内指定路径

    格式:

    COPY [–chown=:] <源路径1>… <目标路径>

    [–chown=:]:可选参数,用户改变复制到容器内文件的拥有者和属组。

    <源路径>:源文件或者源目录,可以使用通配符表达式

    COPY hom* /mydir/

    注意:是从上下文目录中复制文件到 容器里(无论此前的本地的上下文路径是什么,此时容器内的上下文路径都是 . )

  • CMD

    容器是进程,那么在启动这个进程(容器)时,需要指定运行得程序参数

    我们通过docker run命令创建并启动一个容器得时候,可以在命令最后面指定容器主进程

    其启动程序就是容器应用进程,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,其它辅助进程不是它需要关心的东西。

    例如:

    docker run -i -t ubunu /bin/bash   //表示容器启动时立即在容器内打开一个shell终端
    
    docker run ubuntu /bin/ps    //表示容器启动后立即运行 /bin/ps命令,显示容器的当前进程。
    

    除了这种方式,我们可以在Dockfile文件通过CMD命令指定容器启动时要执行得命令:

    有以下两种格式:

    1.shell 格式

    ​ CMD <命令>

    ​ e.g:

    ​ CMD sh deploy.sh

    2.exec 格式

    ​ CMD [“可执行文件”,“参数1”,“参数2”…]

    ​ e.g.

    ​ CMD [“bash”, “/code/entrypoint.sh”]

    exec格式在解析得时候会被解析成JSON,此时会把 CMD后面跟着得程序作为1号进程(1号进程详细解释看1号进程

    shell格式会自动在前面加上 sh -c

    比如:

    CMD echo $HOME
    

    在实际执行中,会将其变更为:

    CMD [ "sh", "-c", "echo $HOME" ]
    

    但是这种方式不够直观,如果我不想用shell进行执行,或者如果容器中没有 shell程序,docker容器就不能运行。

    A better option is to use the exec form of the ENTRYPOINT/CMD instructions which looks like this

    所以推荐用exec方式

  • ENTRYPOINT 入口点

    ENTRYPOINT命令和CMD命令一样,都是指定容器内启动程序和参数。ENTRYPOINT和CMD一样,也有两种格式(exec格式和shell格式)

    ENTRYPOINT在docker容器创建启动(docker run)得时候也可以被

    替换,需要通过docker run得参数 --entrypoint来指定。

    当指定了 ENTRYPOINT 后,CMD 的含义就发生了改变,不再是直接的运行其命令,而是将 CMD 的内容作为参数传给 ENTRYPOINT 指令,换句话说实际执行时,将变为:

    <ENTRYPOINT> "<CMD>"
    

    那么有了 CMD 后,为什么还要有 ENTRYPOINT 呢?这种 <ENTRYPOINT> "<CMD>" 有什么好处么?让我们来看几个场景。

    #场景一:让镜像变成像命令一样使用

    假设我们需要一个得知自己当前公网 IP 的镜像,那么可以先用 CMD 来实现:

    FROM ubuntu:18.04
    RUN apt-get update \
        && apt-get install -y curl \
        && rm -rf /var/lib/apt/lists/*
    CMD [ "curl", "-s", "http://myip.ipip.net" ]
    

    假如我们使用 docker build -t myip . 来构建镜像的话,如果我们需要查询当前公网 IP,只需要执行:

    $ docker run myip
    当前 IP:61.148.226.66 来自:北京市 联通
    

    嗯,这么看起来好像可以直接把镜像当做命令使用了,不过命令总有参数,如果我们希望加参数呢?比如从上面的 CMD 中可以看到实质的命令是 curl,那么如果我们希望显示 HTTP 头信息,就需要加上 -i 参数。那么我们可以直接加 -i 参数给 docker run myip 么?

    $ docker run myip -i
    docker: Error response from daemon: invalid header field value "oci runtime error: container_linux.go:247: starting container process caused \"exec: \\\"-i\\\": executable file not found in $PATH\"\n".
    

    我们可以看到可执行文件找不到的报错,executable file not found。之前我们说过,跟在镜像名后面的是 command,运行时会替换 CMD 的默认值。因此这里的 -i 替换了原来的 CMD,而不是添加在原来的 curl -s http://myip.ipip.net 后面。而 -i 根本不是命令,所以自然找不到。

    那么如果我们希望加入 -i 这参数,我们就必须重新完整的输入这个命令:

    $ docker run myip curl -s http://myip.ipip.net -i
    

    这显然不是很好的解决方案,而使用 ENTRYPOINT 就可以解决这个问题。现在我们重新用 ENTRYPOINT 来实现这个镜像:

    FROM ubuntu:18.04
    RUN apt-get update \
        && apt-get install -y curl \
        && rm -rf /var/lib/apt/lists/*
    ENTRYPOINT [ "curl", "-s", "http://myip.ipip.net" ]
    

    这次我们再来尝试直接使用 docker run myip -i

    $ docker run myip
    当前 IP:61.148.226.66 来自:北京市 联通
    
    $ docker run myip -i
    HTTP/1.1 200 OK
    Server: nginx/1.8.0
    Date: Tue, 22 Nov 2016 05:12:40 GMT
    Content-Type: text/html; charset=UTF-8
    Vary: Accept-Encoding
    X-Powered-By: PHP/5.6.24-1~dotdeb+7.1
    X-Cache: MISS from cache-2
    X-Cache-Lookup: MISS from cache-2:80
    X-Cache: MISS from proxy-2_6
    Transfer-Encoding: chunked
    Via: 1.1 cache-2:80, 1.1 proxy-2_6:8006
    Connection: keep-alive
    
    当前 IP:61.148.226.66 来自:北京市 联通
    

    可以看到,这次成功了。这是因为当存在 ENTRYPOINT 后,CMD 的内容将会作为参数传给 ENTRYPOINT,而这里 -i 就是新的 CMD,因此会作为参数传给 curl,从而达到了我们预期的效果。

    #场景二:应用运行前的准备工作

    启动容器就是启动主进程,但有些时候,启动主进程前,需要一些准备工作。

    比如 mysql 类的数据库,可能需要一些数据库配置、初始化的工作,这些工作要在最终的 mysql 服务器运行之前解决。

    此外,可能希望避免使用 root 用户去启动服务,从而提高安全性,而在启动服务前还需要以 root 身份执行一些必要的准备工作,最后切换到服务用户身份启动服务。或者除了服务外,其它命令依旧可以使用 root 身份执行,方便调试等。

    这些准备工作是和容器 CMD 无关的,无论 CMD 为什么,都需要事先进行一个预处理的工作。这种情况下,可以写一个脚本,然后放入 ENTRYPOINT 中去执行,而这个脚本会将接到的参数(也就是 <CMD>)作为命令,在脚本最后执行。比如官方镜像 redis 中就是这么做的:

    FROM alpine:3.4
    ...
    RUN addgroup -S redis && adduser -S -G redis redis
    ...
    ENTRYPOINT ["docker-entrypoint.sh"]
    
    EXPOSE 6379
    CMD [ "redis-server" ]
    

    可以看到其中为了 redis 服务创建了 redis 用户,并在最后指定了 ENTRYPOINTdocker-entrypoint.sh 脚本。

    #!/bin/sh
    ...
    # allow the container to be started with `--user`
    if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then
    	find . \! -user redis -exec chown redis '{}' +
    	exec gosu redis "$0" "$@"
    fi
    
    exec "$@"
    

    该脚本的内容就是根据 CMD 的内容来判断,如果是 redis-server 的话,则切换到 redis 用户身份启动服务器,否则依旧使用 root 身份执行。比如:

    $ docker run -it redis id
    uid=0(root) gid=0(root) groups=0(root)
    
  • ENV

    ENV 定义镜像的环境变量,格式如下:

    第一种:

    ENV 变量名1 变量值1 变量名2 变量值2

    第二种:

    ENV 变量名1=变量值1 变量名2=变量值2 变量名3=变量值3

    e.g.

    ENV VERSION=1.0 DEBUG=on\
    	NAME="Happy Feet"
    

    后面得下列指令可以支持环境变量展开: ADDCOPYENVEXPOSEFROMLABELUSERWORKDIRVOLUMESTOPSIGNALONBUILDRUN都可以使用前面设置得环境变量

    docker run时ENV的变量会加载到容器内部

    可以在Dockfile中指定,也可以 通过在 docker run 加 --env来指定

  • ARG

    ARG和ENV一样,都是设置环境变量,区别在与ARG设置的环境变量只在 创建 镜像时存在,在容器内不存在。

    还需要注意的有两点:

    • 默认值

      如果 只是 单单的定义变量 但是没有定义值,如

      ARG  变量值
      

      此时 默认变量值 为 空字符串("")

    • 覆盖

      我们在构建 镜像时,可以在 docker build 命令后面加 参数 --build-arg 变量名=变量值 来覆盖 在Dockerfile中定义的 ARG 参数值,但是如果没有在Dockerfile用ARG 定义变量,在构建镜像时(docker build)时,就会出现警告:

      [Warning] One or more build-args [build_env] were not consumed
      
  • VOLUME

    问题:

    容器是基于镜像创建的,容器比镜像多了一层可读可写层,容器中进程产生的数据都保存在这个可读可写层,一旦容器关闭,数据就会丢失。

    针对上面的问题,docker

2.查询、删除、进入

  • 查询镜像

    docker images

  • 查询容器

    docker ps -a

  • 停止运行中的容器

    docker stop [容器ID或容器名]

  • 重启容器

    容器ID或容器名 :不管容器是否启动,直接重启容器

  • 进入到容器中

    docker exec -it <容器的名字或者容器id> /bin/bash

  • 删除容器

    docker rm <容器ID>

  • 删除镜像

    docker rmi <镜像ID>

  • 注意

    1. 删除前需要保证容器是停止的 stop

    2. 需要注意删除镜像和容器的命令不一样。 docker rmi ID ,其中 容器(rm) 和 镜像(rmi)****

    3. 顺序需要先删除容器

3.安装程序包

  1. 同步

    **apt-get update **#同步 /etc/apt/sources.list 和 /etc/apt/sources.list.d 中列出的源的索引,获取到最新的软件包

  2. 安装

    apt-get install <程序的包名>

screen-简单使用

用途

用 xshell 连接 linux,在linux上起了一个服务,这个服务不能中断,我不能一直开着电脑啊!

GNU Screen可以看作是窗口管理器的命令行界面版本。它提供了统一的管理多个会话的界面和相应的功能。

  • 会话恢复

    只要Screen本身没有终止,在其内部运行的会话都可以恢复。这一点对于远程登录的用户特别有用——即使网络连接中断,用户也不会失去对已经打开的命令行会话的控制。只要再次登录到主机上执行screen -r就可以恢复会话的运行。同样在暂时离开的时候,也可以执行分离命令detach,在保证里面的程序正常运行的情况下让Screen挂起(切换到后台)。这一点和图形界面下的VNC很相似。

  • 会话共享

    Screen可以让一个或多个用户从不同终端多次登录一个会话,并共享会话的所有特性(比如可以看到完全相同的输出)。它同时提供了窗口访问权限的机制,可以对窗口进行密码保护。

基本使用

  • 安装

    docker 安装 screen:

    **apt-get update **#同步 /etc/apt/sources.list 和 /etc/apt/sources.list.d 中列出的源的索引,获取到最新的软件包

    apt-get install screen

  • 启动一个会话

    命令:screen -S <会话的名字> <需要执行的命令>

    e.g.

    screen -S busssiness_test python manage.py runserver 0.0.0.0:5556
    
  • 离开会话

    按住键盘上的ctrl键,然后依次按a和d,这是回到主回话,这时可以创建其他回话或者离开主回话

  • 恢复创建的会话

    • 如果记得回话的名字

      screen -r <回话的名字>

      如果不记得,可以使用命令 查看已经创建的回话

      screen -ls

      root@f55bf55a5951:/code# screen -ls
      There is a screen on:
      	593.bus_test	(04/21/21 15:40:20)	(Detached)
      1 Socket in /run/screen/S-root
      

      593是回话id,可以直接 screen -r <会话id>进行回复

    • 如果不记得回话的名字你只创建了一个回话

      screen -r

  • 查看已经创建的回话

    screen -ls

  • 恢复问题

    有时候恢复screen时出现 **There is no screen to be resumed matching **

    这种情况,输入命令:

    screen -d ****

    然后再使用恢复命令恢复就ok

  • 退出screen

    exit

  • 其他命令

    Ctrl + a,d #暂离当前会话
    Ctrl + a,c #在当前screen会话中创建一个子会话
    Ctrl + a,w #子会话列表
    Ctrl + a,p #上一个子会话
    Ctrl + a,n #下一个子会话
    Ctrl + a,0-9 #在第0窗口至第9子会话间切换

shell脚本简单使用

Shell是什么?1分钟理解Shell的概念!

一篇教会你写90%的shell脚本

  • shell

    shell是应用程序,起到连接用户内核的作用,用户操作shell命令发送给内核,处理完成后将结果反馈给用户

    1. shell脚本

    shell命令集合的文件,不用编译解释执行

1.shell变量

在 Bash shell中,每一个变量的值都是字符串,无论你给变量赋值时有没有使用引号,值都会以字符串的形式存储。

1.变量操作
  • 创建普通变量

    1.name=zhoujianhui

    2.name=‘zhoujianhui’

    3.name=“zhoujianhui”

    三种方式创建变量都可以,注意等号两边不要有空格

  • 创建只可变函数体中使用的局部变量:local name=“test” (local修饰的变量只能在函数内使用)

  • 使用变量

    echo $name

    echo ${name}

    因为第二种限定边界,在变量中使用比较好,例如:

    第一种:

    ​ 脚本aa

    name=zhoujianhui
    echo "my name is $namedingding!"
    

    执行语句:

    $ sh aa
    my name is !
    

    ​ 解释器会把$namedingding当成一个变量(其值为空)。

  • 变量重新赋值

    name=‘zhoujianfeng’

  • 只读变量

    name=“只读” -> readonly name

    e.g.

    # bigdataservice @ bjxg-bd-slave03 in /data/tornado/manage_project
    $ name=zhoujianfeng
    # bigdataservice @ bjxg-bd-slave03 in /data/tornado/manage_project
    $ readonly name
    # bigdataservice @ bjxg-bd-slave03 in /data/tornado/manage_project
    $ name='nishishui'
    -bash: name: readonly variable
    
  • 删除变量

    unset name;

supervisor的简单使用

Supervisor使用详解

详解Supervisor进程守护监控

Supervisor的作用与配置

supervisor 介绍

supervisor是用python开发的一套进程管理程序,它可以将命令行进程转化成自己的子进程(后台守护进程)

,并且监控子进程的状态,在子进程挂掉的时候将其重启或者报警

安装

1.Redhat、Centos下安装

yum install supervisor

2.Debian/Ubuntu下安装

apt-get install supervisor

3.pip 安装 supervisor

pip install supervisor
2.字

配置

1.主配置文件的生成

如果用apt-get方式下载,则自动生成主配置文件 supervisord.conf,跳过这一步,如果使用pip install方式安装,则需要手动生成:

​ 1.生成主配置文件

cd /etc
mkdir supervisor    #创建supervisor目录
cd /etc/supervisor
echo_supervisord_conf > /etc/supervisor/supervisord.conf    #生成配置文件
mkdir conf.d            #生成放置具体配置文件的目录
2.修改与具体配置	   

手动生成 主配置文件后,打开主配置文件 supervisord.conf这个文件:

在最后面添加:

[include]
files = /etc/supervisor/conf.d/*.conf

在启动supervisord时,它会加载conf.d文件夹中的所有.conf配置文件(conf.d文件夹下有每个的配置文件对应一个需要启动和监控的守护进程)

2.主配置文件具体参数
root@0697f6cfb9a4:/code# whereis supervisor #查看supervisor文件在哪里
supervisor: /etc/supervisor /usr/share/man/man1/supervisor.1.gz
root@0697f6cfb9a4:/code# ls /etc/supervisor/ #supervisor的配置文件所在地
conf.d	supervisord.conf
root@0697f6cfb9a4:/code# cat /etc/supervisor/supervisord.conf  #supervisor的配置文件

supervisord.conf的内容:

; supervisor config file

[unix_http_server]
file=/var/run/supervisor.sock ; (the path to the socket file)
chmod=0700 ; sockef file mode (default 0700)

[supervisord]
logfile=/var/log/supervisor/supervisord.log ; (main log file;default $CWD/supervisord.log)
pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
childlogdir=/var/log/supervisor ; (‘AUTO’ child log dir, default $TEMP)

; the below section must remain in the config file for RPC
; (supervisorctl/web interface) to work, additional interfaces may be
; added by defining them in separate rpcinterface: sections
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

[supervisorctl]
serverurl=unix:///var/run/supervisor.sock ; use a unix:// URL for a unix socket

; The [include] section can just contain the “files” setting. This
; setting can list multiple files (separated by whitespace or
; newlines). It can also contain wildcards. The filenames are
; interpreted as relative to this file. Included files cannot
; include files themselves.

[include]
files = /etc/supervisor/conf.d/*.conf

  • 主配置文件相关参数

    以上配置文件用到几个部分:

    • [unix_http_server]:这部分设置HTTP服务器监听的UNIX domain socket
      • file: 指向UNIX domain socket,即file=/var/run/supervisor.sock
      • chmod:启动时改变supervisor.sock的权限
    • [supervisord]:与supervisord有关的全局配置需要在这部分设置
      • logfile: 指向记录supervisord进程的log文件
      • pidfile:pidfile保存子进程的路径
      • childlogdir:子进程log目录设为AUTO的log目录
    • [supervisorctl]:
      • serverurl:进入supervisord的URL, 对于UNIX domain sockets, 应设为 unix:///absolute/path/to/file.sock
    • [include]:如果配置文件包含该部分,则该部分必须包含一个files键:
      • files:包含一个或多个文件,这里包含了/etc/supervisor/conf.d/目录下所有的.conf文件,可以在该目录下增加我们自己的配置文件,在该配置文件中增加[program:x]部分,用来运行我们自己的程序,如下:
    • [program:x]:配置文件必须包括至少一个program,x是program名称,必须写上,不能为空
      • command:包含一个命令,当这个program启动时执行
      • directory:执行子进程时supervisord暂时切换到该目录
      • user:账户名
      • startsecs:进程从STARING状态转换到RUNNING状态program所需要保持运行的时间(单位:秒)
      • redirect_stderr:如果是true,则进程的stderr输出被发送回其stdout文件描述符上的supervisord
      • stdout_logfile:将进程stdout输出到指定文件
      • stdout_logfile_maxbytes:stdout_logfile指定日志文件最大字节数,默认为50MB,可以加KB、MB或GB等单位
      • stdout_logfile_backups:要保存的stdout_logfile备份的数量

常用命令

  • 启动主进程命令

    supervisord -c /etc/supervisord.conf #初始启动 #初始启动

  • supervisor命令

    **<项目的名字> **在每个子进程的配置文件首行指定

    supervisorctl status //查看所有进程的状态
    supervisorctl stop <项目的名字> //停止es
    supervisorctl start es <项目的名字> //启动es
    supervisorctl restart <项目的名字> //重启es
    supervisorctl update //配置文件修改后使用该命令加载新的配置
    supervisorctl reload //重新启动配置中的所有程序

例子

​ 因为项目有耗时任务,所以需要用celery进行异步程序执行,但是celery启动命令不能后台执行,需要用supervisor进行托管。

在/etc/supervisor/conf.d/文件夹下,新建 celery_worker.conf文件,文件内容如下:

[program:CeleryWork]
#CeleryWork  为程序的名称 x是program名称,必须写上,不能为空
command=celery -A business_management_system worker -l info
#需要执行的命令
directory=/code
#命令执行的目录
#environment=ASPNETCORE__ENVIRONMENT=Production
#环境变量

stopsignal=INT
autostart=true
#是否自启动
autorestart=true
#是否自动重启
startsecs=3
#自动重启时间间隔(s)
stderr_logfile=/code/celeryworker.err.log
#错误日志文件
stdout_logfile=/code/celeryworker.out.log
#输出日志文件
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
完整版:https://download.csdn.net/download/qq_27595745/89522468 【课程大纲】 1-1 什么是java 1-2 认识java语言 1-3 java平台的体系结构 1-4 java SE环境安装和配置 2-1 java程序简介 2-2 计算机中的程序 2-3 java程序 2-4 java类库组织结构和文档 2-5 java虚拟机简介 2-6 java的垃圾回收器 2-7 java上机练习 3-1 java语言基础入门 3-2 数据的分类 3-3 标识符、关键字和常量 3-4 运算符 3-5 表达式 3-6 顺序结构和选择结构 3-7 循环语句 3-8 跳转语句 3-9 MyEclipse工具介绍 3-10 java基础知识章节练习 4-1 一维数组 4-2 数组应用 4-3 多维数组 4-4 排序算法 4-5 增强for循环 4-6 数组和排序算法章节练习 5-0 抽象和封装 5-1 面向过程的设计思想 5-2 面向对象的设计思想 5-3 抽象 5-4 封装 5-5 属性 5-6 方法的定义 5-7 this关键字 5-8 javaBean 5-9 包 package 5-10 抽象和封装章节练习 6-0 继承和多态 6-1 继承 6-2 object类 6-3 多态 6-4 访问修饰符 6-5 static修饰符 6-6 final修饰符 6-7 abstract修饰符 6-8 接口 6-9 继承和多态 章节练习 7-1 面向对象的分析与设计简介 7-2 对象模型建立 7-3 类之间的关系 7-4 软件的可维护与复用设计原则 7-5 面向对象的设计与分析 章节练习 8-1 内部类与包装器 8-2 对象包装器 8-3 装箱和拆箱 8-4 练习题 9-1 常用类介绍 9-2 StringBuffer和String Builder类 9-3 Rintime类的使用 9-4 日期类简介 9-5 java程序国际化的实现 9-6 Random类和Math类 9-7 枚举 9-8 练习题 10-1 java异常处理 10-2 认识异常 10-3 使用try和catch捕获异常 10-4 使用throw和throws引发异常 10-5 finally关键字 10-6 getMessage和printStackTrace方法 10-7 异常分类 10-8 自定义异常类 10-9 练习题 11-1 Java集合框架和泛型机制 11-2 Collection接口 11-3 Set接口实现类 11-4 List接口实现类 11-5 Map接口 11-6 Collections类 11-7 泛型概述 11-8 练习题 12-1 多线程 12-2 线程的生命周期 12-3 线程的调度和优先级 12-4 线程的同步 12-5 集合类的同步问题 12-6 用Timer类调度任务 12-7 练习题 13-1 Java IO 13-2 Java IO原理 13-3 流类的结构 13-4 文件流 13-5 缓冲流 13-6 转换流 13-7 数据流 13-8 打印流 13-9 对象流 13-10 随机存取文件流 13-11 zip文件流 13-12 练习题 14-1 图形用户界面设计 14-2 事件处理机制 14-3 AWT常用组件 14-4 swing简介 14-5 可视化开发swing组件 14-6 声音的播放和处理 14-7 2D图形的绘制 14-8 练习题 15-1 反射 15-2 使用Java反射机制 15-3 反射与动态代理 15-4 练习题 16-1 Java标注 16-2 JDK内置的基本标注类型 16-3 自定义标注类型 16-4 对标注进行标注 16-5 利用反射获取标注信息 16-6 练习题 17-1 顶目实战1-单机版五子棋游戏 17-2 总体设计 17-3 代码实现 17-4 程序的运行与发布 17-5 手动生成可执行JAR文件 17-6 练习题 18-1 Java数据库编程 18-2 JDBC类和接口 18-3 JDBC操作SQL 18-4 JDBC基本示例 18-5 JDBC应用示例 18-6 练习题 19-1 。。。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值