开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Tomcat的组织架构及启动原理

发表于 2021-11-07

Tomca组件结构

tomcat.png

connector:主要负责接受浏览器发送过来的tcp连接请求,创建一个request和response

container:接受request和response,并开启线程处理请求封装结果

service:结合connector和container,对外提供接口服务,一个service可以有多个connector(多种连接协议)和一个container(engine,可以理解为servlet容器)

server:为service提供生存环境,负责他的生命周期,它可以包含多个service服务

Tomcat容器结构

Tomcat.png

Engine:servlect的顶层容器,包含一个或多个Host子容器;
Host:虚拟主机,负责web应用的部署和context的创建
Context:web应用上下文,包含多个wrapper,负责web配置的解析、管理所有的web资源
Wrapper:最底层的容器,是对servlet的封装,负责servlet实例的创建、执行、销毁

Tomcat启动过程

Tomcat启动时,执行启动脚本startup.sh->catalina.sh->Bootstrap->Bootstrap.main方法

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
java复制代码    public static void main(String args[]) {
// ....
if (daemon == null) {
// Don't set daemon until init() has completed
Bootstrap bootstrap = new Bootstrap();
try {
// 1)如果daemon是空的话,先执行init方法
bootstrap.init();
} catch (Throwable t) {
handleThrowable(t);
t.printStackTrace();
return;
}
daemon = bootstrap;
}
// ....

// 2)不同命令会调用不同daemon的不同方法,daemon是Bootstrap
String command = "start";
if (args.length > 0) {
command = args[args.length - 1];
}

if (command.equals("startd")) {
args[args.length - 1] = "start";
daemon.load(args);
daemon.start();
} else if (command.equals("stopd")) {
args[args.length - 1] = "stop";
daemon.stop();
} else if (command.equals("start")) {
daemon.setAwait(true);
// 3)若命令是start,调用Boostrap的load方法和start方法
daemon.load(args);
daemon.start();
if (null == daemon.getServer()) {
System.exit(1);
}
} else if (command.equals("stop")) {
daemon.stopServer(args);
} else if (command.equals("configtest")) {
daemon.load(args);
if (null == daemon.getServer()) {
System.exit(1);
}
System.exit(0);
} else {
log.warn("Bootstrap: command \"" + command + "\" does not exist.");
}
// ...
}

private void load(String[] arguments) throws Exception {
// Call the load() method
String methodName = "load";
Object param[];
Class<?> paramTypes[];
if (arguments==null || arguments.length==0) {
paramTypes = null;
param = null;
} else {
paramTypes = new Class[1];
paramTypes[0] = arguments.getClass();
param = new Object[1];
param[0] = arguments;
}
// 4)此处通过反射调用,catalinaDaemon的load方法,catalinaDaemon是啥呢?
Method method =
catalinaDaemon.getClass().getMethod(methodName, paramTypes);
if (log.isDebugEnabled()) {
log.debug("Calling startup class " + method);
}
method.invoke(catalinaDaemon, param);
}

// 1.1) Boostrap的init方法会在main中先执行,由此可知catalinaDaemon就是Catalina类
public void init() throws Exception {
// ...
Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.getConstructor().newInstance();
// ...
catalinaDaemon = startupInstance;
}

public void start() throws Exception {
if (catalinaDaemon == null) {
init();
}

Method method = catalinaDaemon.getClass().getMethod("start", (Class [])null);
method.invoke(catalinaDaemon, (Object [])null);
}

1. Catalina.load()

所以最后Bootstrap.main()->Catalina.load(),load方法中主要做了2个事情:

  1. 创建Digester类,解析conf/server.xml文件
  2. 调用server.init()进行初始化server
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
java复制代码  protected String configFile = "conf/server.xml";  
protected File configFile() {
File file = new File(configFile);
if (!file.isAbsolute()) {
file = new File(Bootstrap.getCatalinaBase(), configFile);
}
return file;
}
public void load() {
// 1)Create and execute our Digester
Digester digester = createStartDigester();

InputSource inputSource = null;
InputStream inputStream = null;
File file = null;
try {
// 1.1)创建文件
file = configFile();
inputStream = new FileInputStream(file);
inputSource = new InputSource(file.toURI().toURL().toString());
// ...
inputSource.setByteStream(inputStream);
digester.push(this);
// 1.2) 解析文件:conf/server.xml
digester.parse(inputSource);
// ...
} catch (Exception e) {
log.warn("Catalina.start using " + getConfigFile() + ": " , e);
return;
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
// Ignore
}
}
}
getServer().setCatalina(this);
getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());
// ...

// 2) Start the new server
try {
getServer().init();
} catch (LifecycleException e) {
if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) {
throw new java.lang.Error(e);
} else {
log.error("Catalina.start", e);
}
}
// ...
}

getServer.init()->LiftcycleBase.init():

  1. 更新server的LifecycleState,并发布状态变化事件
  2. 调用StandServer.initInternal()进行server的初始化
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
java复制代码public abstract class LifecycleBase implements Lifecycle {
@Override
public final synchronized void init() throws LifecycleException {
if (!state.equals(LifecycleState.NEW)) {
invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
}
try {
// 1) 设置状态为initalizing,并发布状态变化的事件
// LifecycleState有12种状态,每种状态都有对应的Event
setStateInternal(LifecycleState.INITIALIZING, null, false);
// 2)初始化
initInternal();
// 3)设置状态为initialized,并发布状态变化事件
setStateInternal(LifecycleState.INITIALIZED, null, false);
} catch (Throwable t) {
handleSubClassException(t, "lifecycleBase.initFail", toString());
}
}

private synchronized void setStateInternal(LifecycleState state, Object data, boolean check)
throws LifecycleException {
// ...
// 1)设置新状态
this.state = state;
// 2)获取状态的事件并发布
String lifecycleEvent = state.getLifecycleEvent();
if (lifecycleEvent != null) {
fireLifecycleEvent(lifecycleEvent, data);
}
}
protected void fireLifecycleEvent(String type, Object data) {
LifecycleEvent event = new LifecycleEvent(this, type, data);
// 循环监听:lifecycleListeners并发布事件LifecycleEvent ??监听器有哪些?如何初始化的?
for (LifecycleListener listener : lifecycleListeners) {
listener.lifecycleEvent(event);
}
}
}

public enum LifecycleState {
NEW(false, null),
INITIALIZING(false, Lifecycle.BEFORE_INIT_EVENT),
INITIALIZED(false, Lifecycle.AFTER_INIT_EVENT),
STARTING_PREP(false, Lifecycle.BEFORE_START_EVENT),
STARTING(true, Lifecycle.START_EVENT),
STARTED(true, Lifecycle.AFTER_START_EVENT),
STOPPING_PREP(true, Lifecycle.BEFORE_STOP_EVENT),
STOPPING(false, Lifecycle.STOP_EVENT),
STOPPED(false, Lifecycle.AFTER_STOP_EVENT),
DESTROYING(false, Lifecycle.BEFORE_DESTROY_EVENT),
DESTROYED(false, Lifecycle.AFTER_DESTROY_EVENT),
FAILED(false, null);

private final boolean available;
private final String lifecycleEvent;
}

image-20211106152425823.png
image-20211106154522867.png

1
2
3
4
5
6
7
java复制代码    protected void initInternal() throws LifecycleException {
super.initInternal();
// Initialize our defined Services
for (Service service : services) {
service.init();
}
}

StandServer.initInternal()->LeftcycelBase.init()->StandService.initeInternal(),standService.initernel()主要实现:

  1. 初始化engine
  2. 初始化connectors
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码    protected void initInternal() throws LifecycleException {
super.initInternal();
// 1) 初始化engine
if (engine != null) {
engine.init();
}
// ...
synchronized (connectorsLock) {
for (Connector connector : connectors) {
try {
// 2)初始化connectors
connector.init();
} catch (Exception e) {
// ...
}
}
}
}

初始化engine.init()->LifecycleBase.init()->StandardEngine.initernal()

初始化connetor.init()->LiftcycleBase.init()->Connector.initernal()->ProtocolHandler.init()

2. Catalina.start()

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
java复制代码    public void start() {
// ...
long t1 = System.nanoTime();

// 1) Start the new server
try {
getServer().start();
} catch (LifecycleException e) {
log.fatal(sm.getString("catalina.serverStartFail"), e);
try {
getServer().destroy();
} catch (LifecycleException e1) {
log.debug("destroy() failed for failed Server ", e1);
}
return;
}

// 2) Register shutdown hook
if (useShutdownHook) {
if (shutdownHook == null) {
shutdownHook = new CatalinaShutdownHook();
}
Runtime.getRuntime().addShutdownHook(shutdownHook);

// If JULI is being used, disable JULI's shutdown hook since
// shutdown hooks run in parallel and log messages may be lost
// if JULI's hook completes before the CatalinaShutdownHook()
LogManager logManager = LogManager.getLogManager();
if (logManager instanceof ClassLoaderLogManager) {
((ClassLoaderLogManager) logManager).setUseShutdownHook(
false);
}
}

if (await) {
await();
stop();
}
}

Catalina.start()->LifecycleBase.start()->StandardServer.startIniternal()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码    @Override
protected void startInternal() throws LifecycleException {
// 1)更新server状态为starting,并发布状态变更事件
fireLifecycleEvent(CONFIGURE_START_EVENT, null);
setState(LifecycleState.STARTING);

globalNamingResources.start();

// 2)初始化service
synchronized (servicesLock) {
for (Service service : services) {
service.start();
}
}
}

StandardServer.startIniternal()->LifecycleBase.start()->StandardService.startIniternal()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码    protected void startInternal() throws LifecycleException {
// 1) Start our defined Container first
if (engine != null) {
synchronized (engine) {
engine.start();
}
}
// ...
// 2) Start our defined Connectors second
synchronized (connectorsLock) {
for (Connector connector: connectors) {
try {
// If it has already failed, don't try and start it
if (connector.getState() != LifecycleState.FAILED) {
connector.start();
}
} catch (Exception e) {
log.error(sm.getString(
"standardService.connector.startFailed",
connector), e);
}
}
}
}

image-20211106161442089.png

1)engine.start()->LifecycleBase.start()->engine.startInternal()->ContainerBase.startInternal()

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
java复制代码  	 protected synchronized void startInternal() throws LifecycleException {
// ...
// 1)启动子容器们的任务提交到线程池中
Container children[] = findChildren();
List<Future<Void>> results = new ArrayList<>();
for (Container child : children) {
results.add(startStopExecutor.submit(new StartChild(child)));
}

MultiThrowable multiThrowable = null;
// 2)采用异步获取结果的方式,启动容器的结果
for (Future<Void> result : results) {
try {
result.get();
} catch (Throwable e) {
log.error(sm.getString("containerBase.threadedStartFailed"), e);
if (multiThrowable == null) {
multiThrowable = new MultiThrowable();
}
multiThrowable.add(e);
}

}
// ...
// Start our thread
threadStart();
}

StandardEngine的子容器是Host,所以:ContainerBase.startInternal()->LifecycleBase.start()->发布事件->HostConfig监听此事件

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
java复制代码    public void lifecycleEvent(LifecycleEvent event) {
// ...
if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) {
check();
} else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
beforeStart();
} else if (event.getType().equals(Lifecycle.START_EVENT)) {
// 调用这个方法
start();
} else if (event.getType().equals(Lifecycle.STOP_EVENT)) {
stop();
}
}

public void start() {
// 发布项目
if (host.getDeployOnStartup()) {
deployApps();
}
}

protected void deployApps() {
File appBase = host.getAppBaseFile();
File configBase = host.getConfigBaseFile();
String[] filteredAppPaths = filterAppPaths(appBase.list());
// Deploy XML descriptors from configBase
deployDescriptors(configBase, configBase.list());
// 发布war包项目
deployWARs(appBase, filteredAppPaths);
// Deploy expanded folders
deployDirectories(appBase, filteredAppPaths);
}

protected void deployWARs(File appBase, String[] files) {
// ...
for (String file : files) {
// ...
File war = new File(appBase, file);
if (file.toLowerCase(Locale.ENGLISH).endsWith(".war") && war.isFile() && !invalidWars.contains(file)) {
ContextName cn = new ContextName(file, true);
if (tryAddServiced(cn.getName())) {
try {
// ...
// 发布war的任务提交到线程池中
results.add(es.submit(new DeployWar(this, cn, war)));
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
removeServiced(cn.getName());
throw t;
}
}
}
}
// 异步获取发布结果
for (Future<?> result : results) {
try {
result.get();
} catch (Exception e) {
log.error(sm.getString("hostConfig.deployWar.threaded.error"), e);
}
}
}

private static class DeployWar implements Runnable {
// ...
@Override
public void run() {
// 发布单个war
config.deployWAR(cn, war);
}
}

protected void deployWAR(ContextName cn, File war) {
// ...
Context context = null;
boolean deployThisXML = isDeployThisXML(war, cn);
try {
// ...
context.addLifecycleListener(listener);
context.setName(cn.getName());
context.setPath(cn.getPath());
context.setWebappVersion(cn.getVersion());
context.setDocBase(cn.getBaseName() + ".war");
// 添加StandardContext到当前Host中
host.addChild(context);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString("hostConfig.deployWar.error", war.getAbsolutePath()), t);
} finally {
// ...
}
// ...
}

到这里StandardContext算是创建了,并且是Host的子容器,然后回到LifecycleBase.start(),执行startInternal()->StandardHaost.startInternal()->ContainerBase.startInternal(),最终又回到父类ContainerBase的startIniternal()方法中,重复执行StandardContext的事件监听ContextConfig和startInternal():

  1. ContextConfig的事件监听中,会加载项目的web.xml文件
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
java复制代码    @Override
public void lifecycleEvent(LifecycleEvent event) {
// ...
// Process the event that has occurred
if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
// 加载web.xml文件
configureStart();
} else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
beforeStart();
} else if (event.getType().equals(Lifecycle.AFTER_START_EVENT)) {
// Restore docBase for management tools
if (originalDocBase != null) {
context.setDocBase(originalDocBase);
}
} else if (event.getType().equals(Lifecycle.CONFIGURE_STOP_EVENT)) {
configureStop();
} else if (event.getType().equals(Lifecycle.AFTER_INIT_EVENT)) {
init();
} else if (event.getType().equals(Lifecycle.AFTER_DESTROY_EVENT)) {
destroy();
}
}
protected synchronized void configureStart() {
// ...
webConfig();
// ...
}
// 为了简单理解,省略了很多步骤的代码
protected void webConfig() {
// ...
WebXml webXml = createWebXml();
// ...
if (!webXml.isMetadataComplete()) {
// ...
// Step 9. Apply merged web.xml to Context
if (ok) {
configureContext(webXml);
}
}
// ...
}
// 解析web.xml配置的信息
private void configureContext(WebXml webxml) {
// ...
// 加载Filter们
for (FilterDef filter : webxml.getFilters().values()) {
if (filter.getAsyncSupported() == null) {
filter.setAsyncSupported("false");
}
context.addFilterDef(filter);
}
for (FilterMap filterMap : webxml.getFilterMappings()) {
context.addFilterMap(filterMap);
}
// ...
// 加载Servlet们
for (ServletDef servlet : webxml.getServlets().values()) {
Wrapper wrapper = context.createWrapper();
// ...
wrapper.setOverridable(servlet.isOverridable());
// 添加到StandardContext的child中,且之后在startInternal()中初始化
context.addChild(wrapper);
}
for (Entry<String, String> entry :
webxml.getServletMappings().entrySet()) {
context.addServletMappingDecoded(entry.getKey(), entry.getValue());
}
// 加载默认页面
for (String welcomeFile : webxml.getWelcomeFiles()) {
if (welcomeFile != null && welcomeFile.length() > 0) {
context.addWelcomeFile(welcomeFile);
}
}
// ...
}
  1. 进入StandardContext.startIniternal()中
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
java复制代码    protected synchronized void startInternal() throws LifecycleException {
// ...
// Call ServletContainerInitializers,调用ServletContainerInitializers的方法
for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry :
initializers.entrySet()) {
try {
entry.getKey().onStartup(entry.getValue(), getServletContext());
} catch (ServletException e) {
log.error(sm.getString("standardContext.sciFail"), e);
ok = false;
break;
}
}
// 运行项目中配置的监听器方法
if (ok) {
if (!listenerStart()) {
log.error(sm.getString("standardContext.listenerFail"));
ok = false;
}
}
// ...
// 初始化servlet们
if (ok) {
if (!loadOnStartup(findChildren())){
log.error(sm.getString("standardContext.servletFail"));
ok = false;
}
}
}
public boolean listenerStart() {
// ...
for (Object instance : instances) {
if (!(instance instanceof ServletContextListener)) {
continue;
}
ServletContextListener listener = (ServletContextListener) instance;
try {
fireContainerEvent("beforeContextInitialized", listener);
if (noPluggabilityListeners.contains(listener)) {
// 应用上下文监听
listener.contextInitialized(tldEvent);
} else {
listener.contextInitialized(event);
}
fireContainerEvent("afterContextInitialized", listener);
} catch (Throwable t) {
// ...
}
}
return ok;
}
public boolean loadOnStartup(Container children[]) {
// ...
for (ArrayList<Wrapper> list : map.values()) {
for (Wrapper wrapper : list) {
try {
// 循环加载servlets
wrapper.load();
} catch (ServletException e) {
// ...
}
}
}
return true;
}

监听:这个监听就是web.xml中配置的,就是启动spring容器的入口

1
2
3
4
xml复制代码<!-- listener -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

初始化servlet:StandardContext.loadOnStartup()->StandardWrapper.load()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@Override
public synchronized void load() throws ServletException {
instance = loadServlet();
if (!instanceInitialized) {
initServlet(instance);
}
// ...
}

private synchronized void initServlet(Servlet servlet)
throws ServletException {
// ...
servlet.init(facade);
// ...
}

最后调用的是:GenericServlet.init()

1
2
3
4
5
java复制代码@Override
public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}

GenericServlet.init()->HttpServletBean.init()->FramworkServlet.initServletBean(),此时已经进入spring-webmvc.jar

1
2
3
4
5
java复制代码public final void init() throws ServletException {
// ...
this.initServletBean();
// ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码    protected final void initServletBean() throws ServletException {
// ...
try {
// 初始化spring容器
this.webApplicationContext = this.initWebApplicationContext();
this.initFrameworkServlet();
} // ...
}
protected WebApplicationContext initWebApplicationContext() {
// ...
if (!this.refreshEventReceived) {
// 此处就是启动DispatcherServlet七大组件的地方!!!
this.onRefresh(wac);
}
// ...
return wac;
}

最后的最后进入:DispatcherServlet.onRefresh()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码protected void onRefresh(ApplicationContext context) {
this.initStrategies(context);
}
protected void initStrategies(ApplicationContext context) {
this.initMultipartResolver(context);
this.initLocaleResolver(context);
this.initThemeResolver(context);
this.initHandlerMappings(context);
this.initHandlerAdapters(context);
this.initHandlerExceptionResolvers(context);
this.initRequestToViewNameTranslator(context);
this.initViewResolvers(context);
this.initFlashMapManager(context);
}

最后附上一张DispatcherSerlvet的类图:

image-20211026180857449.png

2)还有个Connector的初始化:connector.start()->LifecycleBase.start()->Connector.startIniternal()->protocalHandler.start()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码    protected void startInternal() throws LifecycleException {

// Validate settings before starting
if (getPort() < 0) {
throw new LifecycleException(sm.getString(
"coyoteConnector.invalidPort", Integer.valueOf(getPort())));
}

setState(LifecycleState.STARTING);

try {
protocolHandler.start();
} catch (Exception e) {
throw new LifecycleException(
sm.getString("coyoteConnector.protocolHandlerStartFailed"), e);
}
}

总结一下子:

  1. Tomcat的启动
* 启动脚本**startup.sh**,最终执行Bootstrap.main(),且会把命令(start、stop等)作为参数传入,不同参数会调用不同的方法
* 当命令是start时,**Bootstrap.main()**会调用**Catalina.load()**和**Catalina.start()**
  1. Tomcat组件的加载:load()
* 在Catalina.load()中,解析**conf/server.xml**,最后调用getServer.init(),对**server**初始化
4. 调用getServer.init(),会进入**LifecycleBase.init()**,这个方法中会先这是server状态为initializing且发布事件,再调用StandardServer.initInternal()
5. 在StandardServer.initInternal()中,会循环调用service.init()方法,会先进入LifecycleBase.init(),然后进入**StandardService.initInternal()**
6. 在StandardService.initInternal()中会初始化**engine**和**connectors**
  1. Tomcat容器的加载:start()
* Catalina.start(),最后调用getServer.start(),进入LifecycleBase.start(),最后进入StandardServer.startIniternal()
2. 在**StandardServer.startIniternal**()中,会启动engine.start()和connector.start()
3. 在engine.start()中,调用**ContainerBase.startIniternal()**,会将子容器Host的启动任务提交到线程池中,**Host容器启动**过程:
    * 先发布事件,且HostConfig监听到这个事件,在HostConfig中会发布web项目,且创建当前web项目的StandardContext,然后把这个**StandardContext添加到Host的子容器中**
    * 然后执行StandardHost.startIniternal(),这个方法中又回到父类ContainerBase.startIniternal(),然后**重复提交StandardContext的启动任务到线程池中**
4. **StandardContext的启动过程**:
    * 发布事件,ContextConfig监听到这个事件,ContextConfig中加载并**解析项目的web.xml文件**,初始化配置的Listener、Filter、Servlet等信息,**servlet会解析成Wrapper**,**然后添加到Context的子容器中**
    * 然后调用**StandardContext.startIniternal**(),启动Context,这里会调用web.xml配置的监听**contextInitialized(),spring容器在这里得到创建**
    * 在StandardContext.startIniternal()中,然后加载Servlet,最终会调用spring-webmvc.jar中**DispatcherSerlvet.onRefresh()**,**加载Serlvet的七大组件**

再来个简单版的总结

Tomcat启动分成2个部分:

  1. 组件的初始化
  2. 容器的启动

其中组件包括:Server->Services->Engine->Connectors
容器包括:Engine->StandardHost->StandardContext->StandardWrapper

web项目的加载、Spring容器的初始化入口、Servlet容器的加载都在第二步,也就是Catalina.start()中

  • web项目的加载:在启动Host容器前发布的事件,也就是监听HostConfig.start()中,并且创建子容器StandardContext
  • Spring容器的初始化入口:在StandardContext启动过程中执行了web.xml中配置的ServletContextListener的实现类的contextInitialized()
  • Servlet容器的加载:在StandardContext启动过程中执行的loadOnStartup(),最终调用DispatcherServlet.onRefresh(),初始化了Servlet的九大组件

留个大疑问

上文中的ServletContainerInitializer的作用是啥?有什么应用?可以怎么拓展?

作用:它是Servlet3.0提供的接口,只有一个方法,主要作用替代web.xml,通过初始化自定义的Listener、Filter、Servlet等信息

1
2
3
4
5
6
7
java复制代码public interface ServletContainerInitializer {
/**
* @param c 启动时初始化的类
* @param ctx web应用的servlet上下文
*/
void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;
}

应用:spring-web.jar中,SpringServletContainerInitializer实现了此接口

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
java复制代码// 1)通过此注解将WebApplicationInitializer类,添加到onStartup方法的第一个参数中
@HandlesTypes({WebApplicationInitializer.class})
public class SpringServletContainerInitializer implements ServletContainerInitializer {
public SpringServletContainerInitializer() {
}

public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException {
List<WebApplicationInitializer> initializers = new LinkedList();
Iterator var4;
if (webAppInitializerClasses != null) {
var4 = webAppInitializerClasses.iterator();

while(var4.hasNext()) {
Class<?> waiClass = (Class)var4.next();
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) && WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
// 2)创建WebApplicationInitializer对象
initializers.add((WebApplicationInitializer)waiClass.newInstance());
} catch (Throwable var7) {
throw new ServletException("Failed to instantiate WebApplicationInitializer class", var7);
}
}
}
}

if (initializers.isEmpty()) {
servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
} else {
servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
AnnotationAwareOrderComparator.sort(initializers);
var4 = initializers.iterator();

while(var4.hasNext()) {
WebApplicationInitializer initializer = (WebApplicationInitializer)var4.next();
// 3)调用WebApplicationInitializer的方法初始化
initializer.onStartup(servletContext);
}

}
}
}

SpringServletContainerInitializer在Tomcat启动时,容器初始化阶段初始化StandardContext时,被调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码    protected synchronized void startInternal() throws LifecycleException {
// Call ServletContainerInitializers
for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry : initializers.entrySet()) {
try {
// 循环多个ServletContainerInitializer,并调用onStartup()
entry.getKey().onStartup(entry.getValue(), getServletContext());
} catch (ServletException e) {
log.error(sm.getString("standardContext.sciFail"), e);
ok = false;
break;
}
}
}

SpringServletContainerInitializer是如何加载到的?毕竟在第三方jar包中,也是在Tomcat启动初始化容器StandardContext之前,发布的事件,在事件监听ContextConfig中加载的:(方法链路:lifecycleEvent()->configureStart()->webConfig()->processServletContainerInitializers()->WebappServiceLoader.load()),spring-web.jar的**classpath/META-INF/services/**路径下配置了需要初始化的ServletContianerInitializer,由Tomcat的ContextConfig加载初始化,在StandardContext中调用

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码    protected void processServletContainerInitializers(){	
// 1) 指定类型为ServletContainerInitializer
WebappServiceLoader<ServletContainerInitializer> loader = new WebappServiceLoader<>(context);
detectedScis = loader.load(ServletContainerInitializer.class);
// ...
}

// 2)加载META-INF/services/路径下
private static final String SERVICES = "META-INF/services/";
public List<T> load(Class<T> serviceType) throws IOException {
String configFile = SERVICES + serviceType.getName();
// ...
}

image-20211107180834566.png

自定义:ServletContainerInitializer

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
java复制代码//容器启动的时候会将@HandlesTypes指定的这个类型下面的子类(实现类,子接口等)传递过来;
//传入感兴趣的类型;
@HandlesTypes(value={HelloService.class})
public class MyServletContainerInitializer implements ServletContainerInitializer {

/**
* 应用启动的时候,会运行onStartup方法;
*
* Set<Class<?>> arg0:感兴趣的类型的所有子类型;
* ServletContext arg1:代表当前Web应用的ServletContext;一个Web应用一个ServletContext;
*
* 1)、使用ServletContext注册Web组件(Servlet、Filter、Listener)
* 2)、使用编码的方式,在项目启动的时候给ServletContext里面添加组件;
* 必须在项目启动的时候来添加;
* 1)、ServletContainerInitializer得到的ServletContext;
* 2)、ServletContextListener得到的ServletContext;
*/
@Override
public void onStartup(Set<Class<?>> arg0, ServletContext sc) throws ServletException {
// TODO Auto-generated method stub
System.out.println("感兴趣的类型:");
for (Class<?> claz : arg0) {
System.out.println(claz);
}

//注册组件 ServletRegistration
ServletRegistration.Dynamic servlet = sc.addServlet("userServlet", new UserServlet());
//配置servlet的映射信息
servlet.addMapping("/user");


//注册Listener
sc.addListener(UserListener.class);

//注册Filter FilterRegistration
FilterRegistration.Dynamic filter = sc.addFilter("userFilter", UserFilter.class);
//配置Filter的映射信息
filter.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*");

}

}

参考:

  • Tomcat组成与工作原理
  • web项目在tomcat中的启动过程分析

😈如果有错误请大家指正…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

搭建视频监控平台 实时查看猫咪的活动位置

发表于 2021-11-07

一起用代码吸猫!本文正在参与【喵星人征文活动】

一、前言介绍

说起养宠物,已经在现在社会上一种常态,当我们在生活节奏和压力日益加速的时候,养一只宠物,不仅为生活带来了许多欢乐,更给予无法替代的温暖陪伴。

一个不喜欢社交的人,在与他人相处的时候可能感到不安,但他可以在自己的宠物身上获得那种最纯粹的爱与陪伴,宠物不会对人的价值或道德做出评价,它们的陪伴是无条件的。

在工作了一天以后还想到家里还有一个小生命在家里等着你,就感觉自己一天的疲劳都消失了,又有了奋斗的力量。

image.png

image.png

image.png

为了能够实时了解两只小家伙每天在家的活动情况,决定自己搭建一个远程视频监控平台,刚好手上也有闲着的硬件设备。

大概实现的流程是: 本地客户端设备采用ffmpge将本地摄像头、声卡数据编码后,采用rtmp协议推流到服务器,然后手机或者电脑APP拉流显示,完成监控的流程。

二、推流端: 开发过程

2.1 硬件介绍

开发板使用的树莓派4B,淘宝官方店购买的,树莓派的板载硬件资源、软件资源都比较齐全,环境都是现成,拿到板子就可以直接开始开发。

image.png

树莓派4B支持windows远程桌面连接,开发起来非常方便:

image.png

image.png

image.png

2.2 搭建开发环境

因为音视频编码推流用到了ffmpge,在写代码之前先将要用到的库编译好,树莓派因为系统里有编译器,资源都是齐全的,相当于就是一台微型电脑,不需要采用交叉编译这种形式。

下面是编译ffmpge源码要用到的库

image.png

image.png

编译ffmpeg:

1
2
3
4
cpp复制代码sudo apt-get install libomxil-bellagio-dev
./configure --enable-shared --prefix=$PWD/_install --enable-gpl --enable-libx264 --enable-omx-rpi --enable-mmal --enable-hwaccel=h264_mmal --enable-decoder=h264_mmal --enable-encoder=h264_omx --enable-omx

[wbyq@wbyq ffmpeg-4.2.2]$ make && make install

2.4 设计的客户端推流代码: 树莓派上运行

下面贴出了完整的代码,所有功能都在这个.c 文件里。编译的时候指定自己的ffmpge库位置,因为用到了线程、和alsa-lib库,需要指定相应的库。

gcc编译的代码参数如下:

1
cpp复制代码gcc ffmpeg_encode_video_audio.c -I /home/ffmpeg-4.2.2/_install/include -L /home/ffmpeg-4.2.2/_install/lib -lavcodec -lavfilter -lavutil -lswresample -lavdevice -lavformat -lpostproc -lswscale -L/home//_install/lib -lx264 -lm -lpthread -lasound

代码整体分为3个部分:

(1). 摄像头采集部分: 摄像头采用的是标准USB免驱摄像头,支持Linux下标准V4L2框架。

(2). 声卡采集部分: 声卡采用alsa-lib库读取声卡的内容。

(3). 视频编码推流部分: 采用ffmpge接口实现。

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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
cpp复制代码#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <libavutil/avassert.h>
#include <libavutil/channel_layout.h>
#include <libavutil/opt.h>
#include <libavutil/mathematics.h>
#include <libavutil/timestamp.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libswresample/swresample.h>

#include <stdio.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <poll.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>

#include <stdio.h>
#include <stdlib.h>
#include <alsa/asoundlib.h>
#include <signal.h>
#include <pthread.h>


//推流的服务器地址
#define RTMP_SERVE_RADDR "rtmp://17.12.13.19:8888/abc"

#define STREAM_DURATION 10.0 /*录制10秒的视频,由于缓冲的原因,一般只有8秒*/
#define STREAM_FRAME_RATE 15 /* 15 images/s avfilter_get_by_name */
#define STREAM_PIX_FMT AV_PIX_FMT_YUV420P /* default pix_fmt */
#define SCALE_FLAGS SWS_BICUBIC

//固定摄像头输出画面的尺寸
#define VIDEO_WIDTH 640
#define VIDEO_HEIGHT 480

//存放从摄像头读出转换之后的数据
unsigned char YUV420P_Buffer[VIDEO_WIDTH*VIDEO_HEIGHT*3/2];
unsigned char YUV420P_Buffer_temp[VIDEO_WIDTH*VIDEO_HEIGHT*3/2];

/*一些摄像头需要使用的全局变量*/
unsigned char *image_buffer[4];
int video_fd;
pthread_mutex_t mutex;
pthread_cond_t cond;

/*一些audio需要使用的全局变量*/
pthread_mutex_t mutex_audio;

extern int capture_audio_data_init( char *audio_dev);
extern int capture_audio_data(snd_pcm_t *capture_handle,int buffer_frames);
/*
进行音频采集,采集pcm数据并直接保存pcm数据
音频参数:
声道数: 2
采样位数: 16bit、LE格式
采样频率: 44100Hz
*/
#define AudioFormat SND_PCM_FORMAT_S16_LE //指定音频的格式,其他常用格式:SND_PCM_FORMAT_U24_LE、SND_PCM_FORMAT_U32_LE
#define AUDIO_CHANNEL_SET 1 //1单声道 2立体声
#define AUDIO_RATE_SET 44100 //音频采样率,常用的采样频率: 44100Hz 、16000HZ、8000HZ、48000HZ、22050HZ
FILE *pcm_data_file=NULL;

int buffer_frames;
snd_pcm_t *capture_handle;
snd_pcm_format_t format=AudioFormat;


//保存音频数据链表
struct AUDIO_DATA
{
unsigned char* audio_buffer;
struct AUDIO_DATA *next;
};

//定义一个链表头
struct AUDIO_DATA *list_head=NULL;
struct AUDIO_DATA *List_CreateHead(struct AUDIO_DATA *head);
void List_AddNode(struct AUDIO_DATA *head,unsigned char* audio_buffer);
void List_DelNode(struct AUDIO_DATA *head,unsigned char* audio_buffer);
int List_GetNodeCnt(struct AUDIO_DATA *head);

// 单个输出AVStream的包装器
typedef struct OutputStream {
AVStream *st;
AVCodecContext *enc;

/* 下一帧的点数*/
int64_t next_pts;
int samples_count;

AVFrame *frame;
AVFrame *tmp_frame;

float t, tincr, tincr2;

struct SwsContext *sws_ctx;
struct SwrContext *swr_ctx;
} OutputStream;


static int write_frame(AVFormatContext *fmt_ctx, const AVRational *time_base, AVStream *st, AVPacket *pkt)
{
/*将输出数据包时间戳值从编解码器重新调整为流时基 */
av_packet_rescale_ts(pkt, *time_base, st->time_base);
pkt->stream_index = st->index;

/*将压缩的帧写入媒体文件*/
return av_interleaved_write_frame(fmt_ctx, pkt);
}


/* 添加输出流。 */
static void add_stream(OutputStream *ost, AVFormatContext *oc,
AVCodec **codec,
enum AVCodecID codec_id)
{
AVCodecContext *c;
int i;

/* find the encoder */
*codec = avcodec_find_encoder(codec_id);
if (!(*codec)) {
fprintf(stderr, "Could not find encoder for '%s'\n",
avcodec_get_name(codec_id));
exit(1);
}

ost->st = avformat_new_stream(oc, NULL);
if (!ost->st) {
fprintf(stderr, "Could not allocate stream\n");
exit(1);
}
ost->st->id = oc->nb_streams-1;
c = avcodec_alloc_context3(*codec);
if (!c) {
fprintf(stderr, "Could not alloc an encoding context\n");
exit(1);
}
ost->enc = c;

switch ((*codec)->type) {
case AVMEDIA_TYPE_AUDIO:
c->sample_fmt = (*codec)->sample_fmts ? (*codec)->sample_fmts[0] : AV_SAMPLE_FMT_FLTP;
c->bit_rate = 64000; //设置码率
c->sample_rate = 44100; //音频采样率
c->channels= av_get_channel_layout_nb_channels(c->channel_layout);
c->channel_layout = AV_CH_LAYOUT_MONO; ////AV_CH_LAYOUT_MONO 单声道 AV_CH_LAYOUT_STEREO 立体声
c->channels = av_get_channel_layout_nb_channels(c->channel_layout);
ost->st->time_base = (AVRational){ 1, c->sample_rate };
break;

case AVMEDIA_TYPE_VIDEO:
c->codec_id = codec_id;
//码率:影响体积,与体积成正比:码率越大,体积越大;码率越小,体积越小。
c->bit_rate = 400000; //设置码率 400kps
/*分辨率必须是2的倍数。 */
c->width =VIDEO_WIDTH;
c->height = VIDEO_HEIGHT;
/*时基:这是基本的时间单位(以秒为单位)
*表示其中的帧时间戳。 对于固定fps内容,
*时基应为1 / framerate,时间戳增量应为
*等于1。*/
ost->st->time_base = (AVRational){1,STREAM_FRAME_RATE};
c->time_base = ost->st->time_base;
c->gop_size = 12; /* 最多每十二帧发射一帧内帧 */
c->pix_fmt = STREAM_PIX_FMT;
c->max_b_frames = 0; //不要B帧
if (c->codec_id == AV_CODEC_ID_MPEG1VIDEO)
{
c->mb_decision = 2;
}
break;

default:
break;
}

/* 某些格式希望流头分开。 */
if (oc->oformat->flags & AVFMT_GLOBALHEADER)
c->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}

/**************************************************************/
/* audio output */

static AVFrame *alloc_audio_frame(enum AVSampleFormat sample_fmt,
uint64_t channel_layout,
int sample_rate, int nb_samples)
{
AVFrame *frame = av_frame_alloc();
frame->format = sample_fmt;
frame->channel_layout = channel_layout;
frame->sample_rate = sample_rate;
frame->nb_samples = nb_samples;
if(nb_samples)
{
av_frame_get_buffer(frame, 0);
}
return frame;
}

static void open_audio(AVFormatContext *oc, AVCodec *codec, OutputStream *ost, AVDictionary *opt_arg)
{
AVCodecContext *c;
int nb_samples;
int ret;
AVDictionary *opt = NULL;
c = ost->enc;
av_dict_copy(&opt, opt_arg, 0);
ret = avcodec_open2(c, codec, &opt);
av_dict_free(&opt);


/*下面3行代码是为了生成虚拟的声音设置的频率参数*/
ost->t = 0;
ost->tincr = 2 * M_PI * 110.0 / c->sample_rate;
ost->tincr2 = 2 * M_PI * 110.0 / c->sample_rate / c->sample_rate;

//AAC编码这里就固定为1024
nb_samples = c->frame_size;

ost->frame = alloc_audio_frame(c->sample_fmt, c->channel_layout,
c->sample_rate, nb_samples);
ost->tmp_frame = alloc_audio_frame(AV_SAMPLE_FMT_S16, c->channel_layout,
c->sample_rate, nb_samples);

/* copy the stream parameters to the muxer */
avcodec_parameters_from_context(ost->st->codecpar, c);

/* create resampler context */
ost->swr_ctx = swr_alloc();

/* set options */
printf("c->channels=%d\n",c->channels);
av_opt_set_int (ost->swr_ctx, "in_channel_count", c->channels, 0);
av_opt_set_int (ost->swr_ctx, "in_sample_rate", c->sample_rate, 0);
av_opt_set_sample_fmt(ost->swr_ctx, "in_sample_fmt", AV_SAMPLE_FMT_S16, 0);
av_opt_set_int (ost->swr_ctx, "out_channel_count", c->channels, 0);
av_opt_set_int (ost->swr_ctx, "out_sample_rate", c->sample_rate, 0);
av_opt_set_sample_fmt(ost->swr_ctx, "out_sample_fmt", c->sample_fmt, 0);

/* initialize the resampling context */
swr_init(ost->swr_ctx);
}

/* 毫秒级 延时 */
void Sleep(int ms)
{
struct timeval delay;
delay.tv_sec = 0;
delay.tv_usec = ms * 1000; // 20 ms
select(0, NULL, NULL, NULL, &delay);
}


/*
准备虚拟音频帧
这里可以替换成从声卡读取的PCM数据
*/
static AVFrame *get_audio_frame(OutputStream *ost)
{
AVFrame *frame = ost->tmp_frame;
int j, i, v;
int16_t *q = (int16_t*)frame->data[0];
/* 检查我们是否要生成更多帧,用于判断是否结束*/
if (av_compare_ts(ost->next_pts, ost->enc->time_base,STREAM_DURATION, (AVRational){ 1, 1 }) >= 0)
return NULL;

#if 1
//获取链表节点数量
int cnt=0;
while(cnt<=0)
{
cnt=List_GetNodeCnt(list_head);
}

pthread_mutex_lock(&mutex_audio); /*互斥锁上锁*/

//得到节点数据
struct AUDIO_DATA *tmp=list_head;
unsigned char *buffer;

tmp=tmp->next;
if(tmp==NULL)
{
printf("数据为NULL.\n");
exit(0);
}
buffer=tmp->audio_buffer;

//1024*16*1
memcpy(q,buffer,frame->nb_samples*sizeof(int16_t)*ost->enc->channels);//将音频数据拷贝进入frame缓冲区

List_DelNode(list_head,buffer);
free(buffer);
pthread_mutex_unlock(&mutex_audio); /*互斥锁解锁*/
#endif

frame->pts = ost->next_pts;
ost->next_pts += frame->nb_samples;
return frame;
}


/*
*编码一个音频帧并将其发送到多路复用器
*编码完成后返回1,否则返回0
*/
static int write_audio_frame(AVFormatContext *oc, OutputStream *ost)
{
AVCodecContext *c;
AVPacket pkt = { 0 };
AVFrame *frame;
int ret;
int got_packet;
int dst_nb_samples;

av_init_packet(&pkt);
c = ost->enc;

frame = get_audio_frame(ost);

if(frame)
{
/*使用重采样器将样本从本机格式转换为目标编解码器格式*/
/*计算样本的目标数量*/
dst_nb_samples = av_rescale_rnd(swr_get_delay(ost->swr_ctx, c->sample_rate) + frame->nb_samples,
c->sample_rate, c->sample_rate, AV_ROUND_UP);
av_assert0(dst_nb_samples == frame->nb_samples);
av_frame_make_writable(ost->frame);
/*转换为目标格式 */
swr_convert(ost->swr_ctx,
ost->frame->data, dst_nb_samples,
(const uint8_t **)frame->data, frame->nb_samples);
frame = ost->frame;
frame->pts = av_rescale_q(ost->samples_count, (AVRational){1, c->sample_rate}, c->time_base);
ost->samples_count += dst_nb_samples;
}

avcodec_encode_audio2(c, &pkt, frame, &got_packet);

if (got_packet)
{
write_frame(oc, &c->time_base, ost->st, &pkt);
}
return (frame || got_packet) ? 0 : 1;
}


static AVFrame *alloc_picture(enum AVPixelFormat pix_fmt, int width, int height)
{
AVFrame *picture;
int ret;
picture = av_frame_alloc();
picture->format = pix_fmt;
picture->width = width;
picture->height = height;

/* allocate the buffers for the frame data */
av_frame_get_buffer(picture, 32);
return picture;
}


static void open_video(AVFormatContext *oc, AVCodec *codec, OutputStream *ost, AVDictionary *opt_arg)
{
AVCodecContext *c = ost->enc;
AVDictionary *opt = NULL;
av_dict_copy(&opt, opt_arg, 0);
/* open the codec */
avcodec_open2(c, codec, &opt);
av_dict_free(&opt);
/* allocate and init a re-usable frame */
ost->frame = alloc_picture(c->pix_fmt, c->width, c->height);
ost->tmp_frame = NULL;
/* 将流参数复制到多路复用器 */
avcodec_parameters_from_context(ost->st->codecpar, c);
}


/*
准备图像数据
YUV422占用内存空间 = w * h * 2
YUV420占用内存空间 = width*height*3/2
*/
static void fill_yuv_image(AVFrame *pict, int frame_index,int width, int height)
{
int y_size=width*height;
/*等待条件成立*/
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond,&mutex);
memcpy(YUV420P_Buffer_temp,YUV420P_Buffer,sizeof(YUV420P_Buffer));
/*互斥锁解锁*/
pthread_mutex_unlock(&mutex);

//将YUV数据拷贝到缓冲区 y_size=wXh
memcpy(pict->data[0],YUV420P_Buffer_temp,y_size);
memcpy(pict->data[1],YUV420P_Buffer_temp+y_size,y_size/4);
memcpy(pict->data[2],YUV420P_Buffer_temp+y_size+y_size/4,y_size/4);
}


static AVFrame *get_video_frame(OutputStream *ost)
{
AVCodecContext *c = ost->enc;

/* 检查我们是否要生成更多帧---判断是否结束录制 */
if(av_compare_ts(ost->next_pts, c->time_base,STREAM_DURATION, (AVRational){ 1, 1 }) >= 0)
return NULL;

/*当我们将帧传递给编码器时,它可能会保留对它的引用
*内部; 确保我们在这里不覆盖它*/
if (av_frame_make_writable(ost->frame) < 0)
exit(1);

//制作虚拟图像
//DTS(解码时间戳)和PTS(显示时间戳)
fill_yuv_image(ost->frame, ost->next_pts, c->width, c->height);
ost->frame->pts = ost->next_pts++;
return ost->frame;
}

/*
*编码一个视频帧并将其发送到多路复用器
*编码完成后返回1,否则返回0
*/
static int write_video_frame(AVFormatContext *oc, OutputStream *ost)
{
int ret;
AVCodecContext *c;
AVFrame *frame;
int got_packet = 0;
AVPacket pkt = { 0 };
c=ost->enc;
//获取一帧数据
frame = get_video_frame(ost);
av_init_packet(&pkt);

/* 编码图像 */
ret=avcodec_encode_video2(c, &pkt, frame, &got_packet);

if(got_packet)
{
ret=write_frame(oc, &c->time_base, ost->st, &pkt);
}
else
{
ret = 0;
}
return (frame || got_packet) ? 0 : 1;
}


static void close_stream(AVFormatContext *oc, OutputStream *ost)
{
avcodec_free_context(&ost->enc);
av_frame_free(&ost->frame);
av_frame_free(&ost->tmp_frame);
sws_freeContext(ost->sws_ctx);
swr_free(&ost->swr_ctx);
}


//编码视频和音频
int video_audio_encode(char *filename)
{
OutputStream video_st = { 0 }, audio_st = { 0 };
AVOutputFormat *fmt;
AVFormatContext *oc;
AVCodec *audio_codec, *video_codec;
int ret;
int have_video = 0, have_audio = 0;
int encode_video = 0, encode_audio = 0;
AVDictionary *opt = NULL;
int i;

/* 分配输出环境*/
avformat_alloc_output_context2(&oc,NULL,"flv",filename);
fmt=oc->oformat;
//指定编码器
fmt->video_codec=AV_CODEC_ID_H264;
fmt->audio_codec=AV_CODEC_ID_AAC;

/*使用默认格式的编解码器添加音频和视频流,初始化编解码器。 */
if(fmt->video_codec != AV_CODEC_ID_NONE)
{
add_stream(&video_st,oc,&video_codec,fmt->video_codec);
have_video = 1;
encode_video = 1;
}
if(fmt->audio_codec != AV_CODEC_ID_NONE)
{
add_stream(&audio_st, oc, &audio_codec, fmt->audio_codec);
have_audio = 1;
encode_audio = 1;
}

/*现在已经设置了所有参数,可以打开音频视频编解码器,并分配必要的编码缓冲区。 */
if (have_video)
open_video(oc, video_codec, &video_st, opt);

if (have_audio)
open_audio(oc, audio_codec, &audio_st, opt);

av_dump_format(oc, 0, filename, 1);

/* 打开输出文件(如果需要) */
if(!(fmt->flags & AVFMT_NOFILE))
{
ret = avio_open(&oc->pb, filename, AVIO_FLAG_WRITE);
if (ret < 0)
{
fprintf(stderr, "无法打开输出文件: '%s': %s\n", filename,av_err2str(ret));
return 1;
}
}

/* 编写流头(如果有)*/
avformat_write_header(oc,&opt);

while(encode_video || encode_audio)
{
/* 选择要编码的流*/
if(encode_video &&(!encode_audio || av_compare_ts(video_st.next_pts, video_st.enc->time_base,audio_st.next_pts, audio_st.enc->time_base) <= 0))
{
//printf("视频编码一次----->\n");
encode_video = !write_video_frame(oc,&video_st);
}
else
{
//printf("音频编码一次----->\n");
encode_audio = !write_audio_frame(oc,&audio_st);
}
}

av_write_trailer(oc);

if (have_video)
close_stream(oc, &video_st);
if (have_audio)
close_stream(oc, &audio_st);

if (!(fmt->flags & AVFMT_NOFILE))
avio_closep(&oc->pb);
avformat_free_context(oc);
return 0;
}


/*
函数功能: 摄像头设备初始化
*/
int VideoDeviceInit(char *DEVICE_NAME)
{
/*1. 打开摄像头设备*/
video_fd=open(DEVICE_NAME,O_RDWR);
if(video_fd<0)return -1;

/*2. 设置摄像头支持的颜色格式和输出的图像尺寸*/
struct v4l2_format video_formt;
memset(&video_formt,0,sizeof(struct v4l2_format));
video_formt.type=V4L2_BUF_TYPE_VIDEO_CAPTURE; /*视频捕获设备*/
video_formt.fmt.pix.height=VIDEO_HEIGHT; //480
video_formt.fmt.pix.width=VIDEO_WIDTH; //640
video_formt.fmt.pix.pixelformat=V4L2_PIX_FMT_YUYV;
if(ioctl(video_fd,VIDIOC_S_FMT,&video_formt))return -2;
printf("当前摄像头尺寸:width*height=%d*%d\n",video_formt.fmt.pix.width,video_formt.fmt.pix.height);

/*3.请求申请缓冲区的数量*/
struct v4l2_requestbuffers video_requestbuffers;
memset(&video_requestbuffers,0,sizeof(struct v4l2_requestbuffers));
video_requestbuffers.count=4;
video_requestbuffers.type=V4L2_BUF_TYPE_VIDEO_CAPTURE; /*视频捕获设备*/
video_requestbuffers.memory=V4L2_MEMORY_MMAP;
if(ioctl(video_fd,VIDIOC_REQBUFS,&video_requestbuffers))return -3;
printf("video_requestbuffers.count=%d\n",video_requestbuffers.count);

/*4. 获取缓冲区的首地址*/
struct v4l2_buffer video_buffer;
memset(&video_buffer,0,sizeof(struct v4l2_buffer));
int i;
for(i=0;i<video_requestbuffers.count;i++)
{
video_buffer.type=V4L2_BUF_TYPE_VIDEO_CAPTURE; /*视频捕获设备*/
video_buffer.memory=V4L2_MEMORY_MMAP;
video_buffer.index=i;/*缓冲区的编号*/
if(ioctl(video_fd,VIDIOC_QUERYBUF,&video_buffer))return -4;
/*映射地址*/
image_buffer[i]=mmap(NULL,video_buffer.length,PROT_READ|PROT_WRITE,MAP_SHARED,video_fd,video_buffer.m.offset);
printf("image_buffer[%d]=0x%X\n",i,image_buffer[i]);
}
/*5. 将缓冲区加入到采集队列*/
memset(&video_buffer,0,sizeof(struct v4l2_buffer));
for(i=0;i<video_requestbuffers.count;i++)
{
video_buffer.type=V4L2_BUF_TYPE_VIDEO_CAPTURE; /*视频捕获设备*/
video_buffer.memory=V4L2_MEMORY_MMAP;
video_buffer.index=i;/*缓冲区的编号*/
if(ioctl(video_fd,VIDIOC_QBUF,&video_buffer))return -5;
}
/*6. 启动采集队列*/
int opt=V4L2_BUF_TYPE_VIDEO_CAPTURE; /*视频捕获设备*/
if(ioctl(video_fd,VIDIOC_STREAMON,&opt))return -6;
return 0;
}


//YUYV==YUV422
int yuyv_to_yuv420p(const unsigned char *in, unsigned char *out, unsigned int width, unsigned int height)
{
unsigned char *y = out;
unsigned char *u = out + width*height;
unsigned char *v = out + width*height + width*height/4;
unsigned int i,j;
unsigned int base_h;
unsigned int is_u = 1;
unsigned int y_index = 0, u_index = 0, v_index = 0;
unsigned long yuv422_length = 2 * width * height;
//序列为YU YV YU YV,一个yuv422帧的长度 width * height * 2 个字节
//丢弃偶数行 u v
for(i=0; i<yuv422_length; i+=2)
{
*(y+y_index) = *(in+i);
y_index++;
}
for(i=0; i<height; i+=2)
{
base_h = i*width*2;
for(j=base_h+1; j<base_h+width*2; j+=2)
{
if(is_u)
{
*(u+u_index) = *(in+j);
u_index++;
is_u = 0;
}
else
{
*(v+v_index) = *(in+j);
v_index++;
is_u = 1;
}
}
}
return 1;
}


/*
子线程函数: 采集摄像头的数据
*/
void *pthread_read_video_data(void *arg)
{
/*1. 循环读取摄像头采集的数据*/
struct pollfd fds;
fds.fd=video_fd;
fds.events=POLLIN;

/*2. 申请存放JPG的数据空间*/
struct v4l2_buffer video_buffer;
while(1)
{
/*(1)等待摄像头采集数据*/
poll(&fds,1,-1);
/*(2)取出队列里采集完毕的缓冲区*/
video_buffer.type=V4L2_BUF_TYPE_VIDEO_CAPTURE; /*视频捕获设备*/
video_buffer.memory=V4L2_MEMORY_MMAP;
ioctl(video_fd,VIDIOC_DQBUF,&video_buffer);
/*(3)处理图像数据*/
/*YUYV数据转YUV420P*/
pthread_mutex_lock(&mutex); /*互斥锁上锁*/
yuyv_to_yuv420p(image_buffer[video_buffer.index],YUV420P_Buffer,VIDEO_WIDTH,VIDEO_HEIGHT);
pthread_mutex_unlock(&mutex); /*互斥锁解锁*/
pthread_cond_broadcast(&cond);/*广播方式唤醒休眠的线程*/

/*(4)将缓冲区再放入队列*/
ioctl(video_fd,VIDIOC_QBUF,&video_buffer);
}
}

/*
子线程函数: 采集摄像头的数据
*/
void *pthread_read_audio_data(void *arg)
{
capture_audio_data(capture_handle,buffer_frames);
}

//运行示例: ./a.out /dev/video0
int main(int argc,char **argv)
{
if(argc!=3)
{
printf("./app </dev/videoX> <hw:X> \n");
return 0;
}
int err;
pthread_t thread_id;

//创建链表头
list_head=List_CreateHead(list_head);

/*初始化互斥锁*/
pthread_mutex_init(&mutex,NULL);
/*初始化条件变量*/
pthread_cond_init(&cond,NULL);

/*初始化互斥锁*/
pthread_mutex_init(&mutex_audio,NULL);

/*初始化摄像头设备*/
err=VideoDeviceInit(argv[1]);
printf("VideoDeviceInit=%d\n",err);
if(err!=0)return err;
/*创建子线程: 采集摄像头的数据*/
pthread_create(&thread_id,NULL,pthread_read_video_data,NULL);
/*设置线程的分离属性: 采集摄像头的数据*/
pthread_detach(thread_id);

capture_audio_data_init( argv[2]);
/*创建子线程: 采集音频的数据*/
pthread_create(&thread_id,NULL,pthread_read_audio_data,NULL);
/*设置线程的分离属性: 采集摄像头的数据*/
pthread_detach(thread_id);

char filename[100];
time_t t;
struct tm *tme;
//开始音频、视频编码
while(1)
{
//开始视频编码
video_audio_encode(RTMP_SERVE_RADDR);
}
return 0;
}

/*
函数功能: 创建链表头
*/
struct AUDIO_DATA *List_CreateHead(struct AUDIO_DATA *head)
{
if(head==NULL)
{
head=malloc(sizeof(struct AUDIO_DATA));
head->next=NULL;
}
return head;
}

/*
函数功能: 插入新的节点
*/
void List_AddNode(struct AUDIO_DATA *head,unsigned char* audio_buffer)
{
struct AUDIO_DATA *tmp=head;
struct AUDIO_DATA *new_node;
/*找到链表尾部*/
while(tmp->next)
{
tmp=tmp->next;
}
/*插入新的节点*/
new_node=malloc(sizeof(struct AUDIO_DATA));
new_node->audio_buffer=audio_buffer;
new_node->next=NULL;
/*将新节点接入到链表*/
tmp->next=new_node;
}

/*
函数功能:删除节点
*/
void List_DelNode(struct AUDIO_DATA *head,unsigned char* audio_buffer)
{
struct AUDIO_DATA *tmp=head;
struct AUDIO_DATA *p;
/*找到链表中要删除的节点*/
while(tmp->next)
{
p=tmp;
tmp=tmp->next;
if(tmp->audio_buffer==audio_buffer)
{
p->next=tmp->next;
free(tmp);
}
}
}

/*

*/


/*
函数功能:遍历链表,得到节点总数量
*/
int List_GetNodeCnt(struct AUDIO_DATA *head)
{
int cnt=0;
struct AUDIO_DATA *tmp=head;
while(tmp->next)
{
tmp=tmp->next;
cnt++;
}
return cnt;
}


int capture_audio_data_init( char *audio_dev)
{
int i;
int err;

buffer_frames = 1024;
unsigned int rate = AUDIO_RATE_SET;// 常用的采样频率: 44100Hz 、16000HZ、8000HZ、48000HZ、22050HZ
capture_handle;// 一个指向PCM设备的句柄
snd_pcm_hw_params_t *hw_params; //此结构包含有关硬件的信息,可用于指定PCM流的配置

/*注册信号捕获退出接口*/
printf("进入main\n");
/*PCM的采样格式在pcm.h文件里有定义*/
format=SND_PCM_FORMAT_S16_LE; // 采样位数:16bit、LE格式

/*打开音频采集卡硬件,并判断硬件是否打开成功,若打开失败则打印出错误提示*/
if ((err = snd_pcm_open (&capture_handle, audio_dev,SND_PCM_STREAM_CAPTURE,0))<0)
{
printf("无法打开音频设备: %s (%s)\n", audio_dev,snd_strerror (err));
exit(1);
}
printf("音频接口打开成功.\n");


/*分配硬件参数结构对象,并判断是否分配成功*/
if((err = snd_pcm_hw_params_malloc(&hw_params)) < 0)
{
printf("无法分配硬件参数结构 (%s)\n",snd_strerror(err));
exit(1);
}
printf("硬件参数结构已分配成功.\n");

/*按照默认设置对硬件对象进行设置,并判断是否设置成功*/
if((err=snd_pcm_hw_params_any(capture_handle,hw_params)) < 0)
{
printf("无法初始化硬件参数结构 (%s)\n", snd_strerror(err));
exit(1);
}
printf("硬件参数结构初始化成功.\n");

/*
设置数据为交叉模式,并判断是否设置成功
interleaved/non interleaved:交叉/非交叉模式。
表示在多声道数据传输的过程中是采样交叉的模式还是非交叉的模式。
对多声道数据,如果采样交叉模式,使用一块buffer即可,其中各声道的数据交叉传输;
如果使用非交叉模式,需要为各声道分别分配一个buffer,各声道数据分别传输。
*/
if((err = snd_pcm_hw_params_set_access (capture_handle,hw_params,SND_PCM_ACCESS_RW_INTERLEAVED)) < 0)
{
printf("无法设置访问类型(%s)\n",snd_strerror(err));
exit(1);
}
printf("访问类型设置成功.\n");

/*设置数据编码格式,并判断是否设置成功*/
if ((err=snd_pcm_hw_params_set_format(capture_handle, hw_params,format)) < 0)
{
printf("无法设置格式 (%s)\n",snd_strerror(err));
exit(1);
}
fprintf(stdout, "PCM数据格式设置成功.\n");

/*设置采样频率,并判断是否设置成功*/
if((err=snd_pcm_hw_params_set_rate_near (capture_handle,hw_params,&rate,0))<0)
{
printf("无法设置采样率(%s)\n",snd_strerror(err));
exit(1);
}
printf("采样率设置成功\n");

/*设置声道,并判断是否设置成功*/
if((err = snd_pcm_hw_params_set_channels(capture_handle, hw_params,AUDIO_CHANNEL_SET)) < 0)
{
printf("无法设置声道数(%s)\n",snd_strerror(err));
exit(1);
}
printf("声道数设置成功.\n");

/*将配置写入驱动程序中,并判断是否配置成功*/
if ((err=snd_pcm_hw_params (capture_handle,hw_params))<0)
{
printf("无法向驱动程序设置参数(%s)\n",snd_strerror(err));
exit(1);
}
printf("参数设置成功.\n");
/*使采集卡处于空闲状态*/
snd_pcm_hw_params_free(hw_params);

/*准备音频接口,并判断是否准备好*/
if((err=snd_pcm_prepare(capture_handle))<0)
{
printf("无法使用音频接口 (%s)\n",snd_strerror(err));
exit(1);
}
printf("音频接口准备好.\n");

return 0;
}

unsigned char audio_read_buff[2048];
//音频采集线程
int capture_audio_data(snd_pcm_t *capture_handle,int buffer_frames)
{
int err;
//因为frame样本数固定为1024,而双通道,每个采样点2byte,所以一次要发送1024*2*2byte数据给frame->data[0];
/*配置一个数据缓冲区用来缓冲数据*/
//snd_pcm_format_width(format) 获取样本格式对应的大小(单位是:bit)
int frame_byte=snd_pcm_format_width(format)/8;

/*开始采集音频pcm数据*/
printf("开始采集数据...\n");
int i;
char *audio_buffer;
while(1)
{
audio_buffer=malloc(buffer_frames*frame_byte*AUDIO_CHANNEL_SET); //2048
if(audio_buffer==NULL)
{
printf("缓冲区分配错误.\n");
break;
}

/*从声卡设备读取一帧音频数据:2048字节*/
if((err=snd_pcm_readi(capture_handle,audio_read_buff,buffer_frames))!=buffer_frames)
{
printf("从音频接口读取失败(%s)\n",snd_strerror(err));
exit(1);
}

pthread_mutex_lock(&mutex_audio); /*互斥锁上锁*/
memcpy(audio_buffer,audio_read_buff,buffer_frames*frame_byte*AUDIO_CHANNEL_SET);
//添加节点
List_AddNode(list_head,audio_buffer);
pthread_mutex_unlock(&mutex_audio); /*互斥锁解锁*/
}

/*释放数据缓冲区*/
free(audio_buffer);

/*关闭音频采集卡硬件*/
snd_pcm_close(capture_handle);

/*关闭文件流*/
fclose(pcm_data_file);

return 0;
}

三、服务器端: 开发过程

服务器端使用的阿里云服务器,安装ubuntu18.04系统,使用Nginx搭建Rtmp流媒体服务器。

下面是搭建Nginx服务器需要用到的安装包:

1
2
3
4
5
6
7
cpp复制代码xl@xl:~/work_pc$ mkdir nginx      
xl@xl:~/work_pc$ cd nginx/
xl@xl:~/work_pc/nginx$ wget http://nginx.org/download/nginx-1.10.3.tar.gz
xl@xl:~/work_pc/nginx$ wget http://zlib.net/zlib-1.2.11.tar.gz
xl@xl:~/work_pc/nginx$ wget https://ftp.pcre.org/pub/pcre/pcre-8.40.tar.gz
xl@xl:~/work_pc/nginx$ wget https://www.openssl.org/source/openssl-1.0.2k.tar.gz
xl@xl:~/work_pc/nginx$ wget https://github.com/arut/nginx-rtmp-module/archive/master.zip

编译安装nginx:

1
2
3
4
cpp复制代码$ cd nginx-1.8.1/
$ ./configure --prefix=/usr/local/nginx --with-debug --with-pcre=../pcre-8.40 --with-zlib=../zlib-1.2.11 --with-openssl=../openssl-1.0.2k --add-module=../nginx-rtmp-module-master
$ make
$ sudo make install

控制服务器的3个常用指令

1
2
3
cpp复制代码sudo service nginx start
sudo service nginx stop
sudo service nginx restart

编辑/usr/local/nginx/conf/nginx.conf文件,加入RTMP的配置

1
2
3
4
5
6
7
8
cpp复制代码rtmp {  
server {
listen 8888;
application live {
live on;
}
}
}

里面8888是设置的端口,可以自己设置,要保证不能被其他进程占用了,设置完启动服务器即可。 下面然后下面就编写拉流的播放器,查看树莓派推流过来的画面。

四、客户端: 播放器开发过程

播放器端可以直接使用现成的软件,也可以自己开发。

自己开发的话,可以直接使用ffmpeg接口开发,或者使用现成的封装好的框架,libvlc、QTAV等等。

因为我的客户端软件准备采用QT框架开发,我这里就采用QTAV来实现流媒体视频的播放,比较省事,跨平台编译也很方便。

QTAV的官网: www.qtav.org/

前面文章里也介绍了QTAV的编译安装方法: juejin.cn/post/702554…

运行效果如下:

image.png

4.1 widget.cpp 代码

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
cpp复制代码MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);

Widgets::registerRenderers();

this->setWindowTitle("猫猫监控画面");

m_player = new AVPlayer(this);
m_vo = new VideoOutput(this);
m_player->setRenderer(m_vo);

ui->horizontalLayout->addWidget(m_vo->widget());


//播放的进度改变信号
connect(m_player, SIGNAL(positionChanged(qint64)),this, SLOT(updateSliderPosition(qint64)));

//设置间隔时间(ms单位)
m_player->setNotifyInterval(40);

m_player->play("rtmp://17.12.13.19:8888/abc");

}

4.2 widget.h 代码

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
cpp复制代码#include <QMainWindow>

#include <QtAV>
#include <QtAVWidgets>
#include <QDebug>


using namespace QtAV;


QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
Q_OBJECT

public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();

VideoOutput *m_vo;
AVPlayer *m_player;
private slots:
void updateSliderPosition(qint64 pos);
void on_pushButton_clicked();

private:
Ui::MainWindow *ui;
};

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

基于Springboot实现园区招商管理系统

发表于 2021-11-07

项目编码:BS-XX-087

项目介绍

园区招商管理系统。共分为三种角色,超级管理员、企业人员、高级用户。

超级管理员角色具有功能: 系统设置-用户管理、页面管理、角色管理; 招商管理-招商列表; 公司管理-公司列表、我的申请; 投诉管理-投诉列表、我的投诉; 合同管理-合同列表; 项目管理-项目列表、我的项目;

新闻管理-新闻列表;

环境需要

1.运行环境:最好是java jdk 1.8,我们在这个平台上运行的。其他版本理论上也可以。

2.IDE环境:IDEA,Eclipse,Myeclipse都可以。推荐IDEA; 3.tomcat环境:Tomcat 7.x,8.x,9.x版本均可 4.硬件环境:windows 7/8/10 1G内存以上;或者 Mac OS; 5.是否Maven项目: 是;查看源码目录中是否包含pom.xml;若包含,则为maven项目,否则为非maven项目

6.数据库:MySql 8.0版本;

技术栈

\1. 后端:SpringBoot;

\2. 前端:html+thymeleaf;

使用说明

\1. 使用Navicat或者其它工具,在mysql中创建对应名称的数据库,并导入项目的sql文件;

\2. 将项目中application.properties配置文件中的数据库配置改为自己的配置 \3. 使用IDEA/Eclipse/MyEclipse导入项目,Eclipse/MyEclipse导入时,若为maven项目请选择maven;若为maven项目,导入成功后请执行maven clean;maven install命令,配置tomcat,然后运行; \4. 运行项目,输入localhost:8081 登录 \5. 管理员账户:admin 密码:123456

\6. 企业人员和高级用户菜单可通过管理员进行分配;

运行截图

img​

img​

img​

img​

img​

img​

img​

img​

img​

\

​

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

ThreadPoolExecutor细抠源码

发表于 2021-11-07

本文带着以下问题

1
2
3
4
5
6
7
scss复制代码❶ execute方法里为啥要用workQueue.offer(command)这个非阻塞方法呢,而不用put等阻塞方法呢?
❷ 线程池收到Runnable任务紧就start执行了,为什么还要将任务放入集合(workers.add(w))呢?
❸ workers 与 workerQueue 与 ctl的关系?
❹ getTask中为啥使用workQueue.poll(num,timeUnit) 和 take()阻塞方法,为啥不使用非阻塞方法呢?
❺ 线程池里存放的是线程吗?
❻ 线程池使用了两个锁lock,worker使用一个,reentrantLock使用一个,作用分别是什么?
❼ 线程池里怎么区分空闲线程和执行中线程?

理解ThreadPoolExecutor,甚至任何其他的类或组件,我觉得从两个点出发会更顺滑:他的数据结构和结构中的属性变化

而ThreadPoolExecutor的数据结构为

1
2
3
4
5
6
7
arduino复制代码private final HashSet<Worker> workers;
private final BlockingQueue<Runnable> workQueue;
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
class Worker
final Thread thread;
Runnable firstTask;
volatile long completedTasks;

线程池定义了一个worker对象封装了一个 thread 线程和一个 task 任务。可以看出一个关系:一个线程对应一个worker。而 workers 集合显而易见装的就是worker了,即worker创建了加入集合;worker的任务执行完了,worker从集合中删除。

workQueue一个阻塞队列,存放超出核心数量的任务。阻塞和队列都是很有讲究的用意。

ctl是一个巧妙的设计,既表示 workerCount 又表示 runState

本文将ThreadPoolExecutor高深的位运算转换为二进制,以便更直观的理解方法和属性的使用。对加入线程池,执行worker的线程,释放worker的线程,终止线程池等进行细致的理解,以求轻便理解。

状态

NOTE: 代码中的位运算不好直观,我们学习时可以将他们转成十进制和二进制,直观便于理解。使用以下方法操作进制间的转换

1
2
rust复制代码1、二进制 -> 十进制 Integer.parseInt("00111100", 2)
2、十进制 -> 二进制 Integer.toBinaryString(1)

ctl即是又是
既是workerCount:表示有效的线程数
又是runState: 表示线程池的状态

Tips => 强调 ctl即是workerCount:当其值为正数时表示有效的线程数 又是runState:当其值为负数时表示线程的状态

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
arduino复制代码/**
*
* <pre>
* ctl即是又是
* workerCount:表示有效的线程数
* runState: 表示线程池的状态
* </pre>
* <pre>
* Run state is stored in the high-order bits; worker count is stored in the low-order bits. 解释如下:
* | --- 高位 --- | --- 低位 --- |
* | -536870912 --> 0 --> 536870912 |
* | --- 线程状态 --- | --- 线程数量 --- |
* 所以线程状态的变化轨迹是从 -536870912 开始递增,一直到 0;线程数量的变化轨迹是从0 开始递增,一直到 536870912
* 2. ctl直接等于 536870910,再定义两个线程,debug看效果 do it by practise
* </pre>
*
* <pre>
* ctl的初始值:-536870912
* ctl = ctlOf(RUNNING, 0): -536870912 -> 11100000000000000000000000000000 -> size: 32
* </pre>
*/
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));


// COUNT_BITS = 29
private static final int COUNT_BITS = Integer.SIZE - 3;


/**
* <pre>
* (1 << COUNT_BITS) = (1 << 29) -> 536870912 -> 100000000000000000000000000000 -> size: 30
* CAPACITY = ((1 << 29) - 1) -> 536870911 -> 11111111111111111111111111111 -> size: 29
* ~CAPACITY = ~((1 << 29) - 1) -> -536870912 -> 11100000000000000000000000000000 -> size: 32
* </pre>
*/
private static final int CAPACITY = (1 << COUNT_BITS) - 1;


/**
* NOTE: 这几个状态值是有数值顺序的,所以这几个状态值才可以进行大于、小于等操作
*
* runState is stored in the high-order bits
* <pre>
* COUNT_BITS = 29
* RUNNING (-1 << 29): -536870912 -> 11100000000000000000000000000000 -> size: 32
* SHUTDOWN (0 << 29): 0 -> 0 -> -> size: 1
* STOP (1 << 29): 536870912 -> 100000000000000000000000000000 -> size: 30
* TIDYING (2 << 29): 1073741824 -> 1000000000000000000000000000000 -> size: 31
* TERMINATED (3 << 29): 1610612736 -> 1100000000000000000000000000000 -> size: 31
* </pre>
*
* 》》》重要知识点出现了:这几个状态值是有数值顺序的,所以这几个状态值才可以进行大于、小于等操作 》》》
*
* 通过实践得出:这里的常量只是状态的边界值。换句话说,每个状态其实是一个范围,具体如下
* runState: ------- RUNNING -------- )[ ---------- SHUTDOWN --------- )[ ------------ STOP ---------- )[ ------------- TIDYING -------- )[ TERMINATED
* 11100000000000000000000000000000 ~ 0 ~ 100000000000000000000000000000 ~ 1000000000000000000000000000000 ~ 1100000000000000000000000000000 ~ 无穷
*/
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;

Tips => 再次强调:runState的几个常量仅是状态的边界值。换句话说,每个状态其实是一个范围

下图为线程池的状态转换过程

正是由于每个状态其实是一个范围,状态常量仅是状态范围的边界。于是,状态方法的使用就简单明了了

状态方法

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
markdown复制代码/**
* 这个方法:返回负数说明线程状态是RUNNING;返回0说明线程状态是SHUTDOWN;理论上不返回正数
*
* <pre>
* 由于
* ~CAPACITY = ~((1 << 29) - 1) -> -536870912 -> 11100000000000000000000000000000 -> size: 32
* ctl = ctlOf(RUNNING,0) -> -536870912 -> 11100000000000000000000000000000 -> size: 32
* 所以
* c=ctl时,ctl & ~CAPACITY -> -536870912 & -536870912 -> 11100000000000000000000000000000 = -536870912,
* 所以
* 随着ctl ++,runStateOf方法结果也是负数,并从-536870912开始递 +1,一直到 0,所以也可以是说负数表示线程状态是RUNNING(运行状态)时
*
* 举例:
* 当第一次ctl ++后,ctl -> -536870911 -> 11100000000000000000000000000001
* 此时,ctl & ~CAPACITY -> -536870911 & -536870912 -> 11100000000000000000000000000001 & 11100000000000000000000000000000
* NOTE: 当runStateOf等于0时,线程状态就变成了SHUTDOWN
* </pre>
*/
private static int runStateOf(int c) { return c & ~CAPACITY; }


/**
* <pre>
* 由于
* CAPACITY = (1 << 29) - 1 -> 536870911 -> 11111111111111111111111111111 -> size: 29
* ctl = ctlOf(RUNNING, 0) -> -536870912 -> 11100000000000000000000000000000 -> size: 32
* 所以
* c=ctl时,ctl & CAPACITY -> -536870912 & 536870911 -> 00000000000000000000000000000000 = 0
* 所以
* 随着ctl ++,所以workerCountOf方法结果从0开始递 +1,一直到 536870911
* </pre>
*/
private static int workerCountOf(int c) { return c & CAPACITY; }


private static int ctlOf(int rs, int wc) { return rs | wc; }

线程池的执行过程,这个网上说的很明白了
20211107133416

Tips => 正式开始前,再次强调 ctl其值为正数时表示线程数,其值为负数时表示线程状态

添加任务方法 - execute

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
scss复制代码public void execute(Runnable command) {
/*
* Proceed in 3 steps:
* 线程加入线程池的执行过程,见上图
*/
int c = ctl.get();
// 当前线程数小于核心线程数
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
// 在addWorker里执行时,如果其他线程对线程池调用shutdown or shutdownNow or terminate and so on,
// 那么addWorker返回false,从而走到这行代码
c = ctl.get();
}
// =》isRunning(c) = c < SHUTDOWN =》记住一点:ctl小于0即是Running状态
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 如果其他线程对线程池调用shutdown or terminate相关方法,对于刚才加入队列的任务要删除调
if (! isRunning(recheck) && remove(command))
reject(command);
// 假设线程池中只有一个运行着的线程:T1,当main线程走到这行代码时,T1运行完了,并对ctl执行了-1操作后就是0了,此时这行判断为true
// 但是此时addWorker传入的任务是null,疑惑吗?这是因为代码执行到这时,任务task已经加入到workQueue队列了,而在runWorker方法中,如果worker的firstTask是null,那么会从workQueue队列里取任务task执行,所以此处传null给addWorker得以有机会执行t.start(),从而执行runWorker方法。__从这里看出一点:work count是不包括队列中任务的__
else if (workerCountOf(recheck) == 0)
addWorker(null, false); // (1)
}
// 如果执行到这里,两种情况:
// 1. 线程池是RUNNING状态,但workerCount >= corePoolSize并且workQueue已满。使用最大线程数的逻辑
// 2. 线程池已经不是RUNNING状态,即c >= SHUTDOWN。那为什么还要走addWorker方法呢,我的理解是:这是作者代码精简的结果,addWorker方法有c >= SHUTDOWN的判断逻辑
else if (!addWorker(command, false))
reject(command);
}

这里抛出一个问题(问题A):这里为啥要用workQueue.offer(command)这个非阻塞方法呢,而不用put等阻塞方法呢?先想想,文末一起说说

添加任务方法 - addWorker

记住一个前提:进入这个方法的条件是当前线程数<=核心线程数,或队列已满&当前线程数<=最大线程数。有了这个前提就好理解多了

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
ini复制代码private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);

// 线程池不是RUNNING状态时触发
// 如果rs>0,返回false;
// 或者,如果rs=0且firstTask不是null,返回false;
// 或者,如果rs=0且workQueue不是空,返回false
// 这时就有个疑问了,都什么时候rs=0,即c=0呢
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;

// 对c进行CAS操作,直到成功
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}

boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
int rs = runStateOf(ctl.get());

// (rs == SHUTDOWN && firstTask == null)这种情况仅仅适用于execute方法的(1)处情况
if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start(); //(1)
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}

addWorker方法一共做了两件事:1.ctl递增;2.Worker对象加入workers集合并start Worker.thread线程(即线程池中的线程)。再聚合点说,这个方法就是进行start Worker.thread线程。既然是这样,那么有两个疑问:

  • 问题B: 传入的Runnable类型的任务紧接着就进行start了,那么为什么还要workers.add(w)放入集合呢,workers集合存在的意义是什么呢?自己先想想,文末给出答案
  • 请注意一个细节: addWorker方法在什么线程里执行的?这有助于问题A的理解

再问个问题(问题E):线程池(ThreadPoolExecutor)中存放的是线程吗?不是,是一堆的Worker对象,Worker既不是thread线程也不是要执行的任务。那么它是做啥的呢

我们来看下Worker的构造方法

1
2
3
4
5
scss复制代码Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker (1)
this.firstTask = firstTask; // (2)
this.thread = getThreadFactory().newThread(this); // (3)
}

从构造方法知道,我们要执行的任务成为Worker的一个字段,同时Worker还有一个thread字段,看Worker的(3)处代码,我觉得这行很关键,我改下它的同义写法:this.thread.target = this,即worker作为他自身的thread字段的值,从Worker的定义知道,Worker本身也是Runnable的。所以,当执行addWorker方法的(1)处t.start()时,我们的任务也跟着执行了,这个流程如图
20211107140117

明白了这里,Worker.run()和runWorker(Worker)怎么触发的就很容易理解了

执行任务方法 - runWorker

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
ini复制代码Worker的thread字段值,执行thread.start()方法,触发了此方法的执行。

final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts (b)
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
w.lock(); // 这里为啥要加锁,结合interruptIdleWorkers方法一起思考

// 如果线程池正在停止,那么要保证当前线程是中断状态;
// 如果不是的话,则要保证当前线程不是中断状态;
// ctl > STOP
if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP)))
&& !wt.isInterrupted())
wt.interrupt();
try {
// 业务人员自己实现
beforeExecute(wt, task);
try {
task.run();
} catch (Throwable x) {
throw new Error(x);
} finally {
// 业务人员自己实现
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
// completedAbruptly变量来表示在执行任务过程中是否出现了异常,在processWorkerExit方法中会对该变量的值进行判断。
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
  • 请注意一个细节: runWorker方法是在什么线程里执行?这有助于问题D的理解

runWorker方法的目的就是执行任务(即task.run())。它首先执行Worker的firstTask,然后再从workQueue队列里取task继续执行。简单来说,就是取任务 执行,取任务 执行,取任务 执行。在这其中,怎么取,执行前中后会做什么事情,如ctl判断,线程中断检测,w.unlock()和w.lock()等都很重要,一个一个说吧

  • 怎么取:这是getTask()方法的事情,稍后说
  • ctl判断:runStateAtLeast(ctl.get(), STOP) => 是否 ctl.get() > STOP,当线程被中断了,这个方法才返回true
  • w.unlock()和w.lock(),worker的lock相关用于区分线程是否空闲。结合shutdown()方法一起理解,见下文shutdown的部分

从while条件可知:null值对于runWorker()来说有特殊用途:通知获取任务的工作线程结束并退出,所以getTask方法返回null时是很特殊的

执行任务方法 - getTask

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
java复制代码private Runnable getTask() {
boolean timedOut = false; // 上一次的poll()的调用是否超时?

for (;;) {
int c = ctl.get();

// 这里需要记住文章开头的那些状态字段值,才反应的快些
// => c & ~CAPACITY
int rs = runStateOf(c);

/*
* 如果线程池状态rs >= SHUTDOWN,也就是非RUNNING状态,再进行以下判断:
* 1. rs >= STOP,线程池是否正在stop;
* 2. 阻塞队列是否为空。
* 如果以上条件满足,则整个判断条件为true。说明线程池突然终止,
* 因为如果当前线程池状态的值是SHUTDOWN或以上时,不允许再向阻塞队列中添加任务
*
* rs >= SHUTDOWN 说明当前线程池至少处于待关闭状态,不再接受新的任务
* rs >= STOP: 说明不需要在再处理任务了(即便有任务)
* 所以,说明线程池在关闭,那就不执行任务task了
*/
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
// 线程池要/正在关闭了,所以返回null,所以runWorker就退出了。所以代码走到这里含义是此任务的工作线程就要退出了。
// 相应的,ctl当然随之要减一
decrementWorkerCount();
return null;
}

/ => c & CAPACITY
int wc = workerCountOf(c);

boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

// wc > maximumPoolSize什么场景下是true呢?答案是有人动态调整了最大线程数
// timeOut变量表示获取任务是否超时了,即当前工作线程是否是空闲工作线程
// timed表示当前线程池中的线程数量是否超过了规定的数量
// 如果timeOut和timed都为true则表示当前线程池中工作线程数量太多了并且当前工作线程是空闲线程,满足被回收的条件。
// 所以要Decrement进行ctl减一操作。同时return null,意味着此工作线程要退出了
if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) {
// 此工作线程要退出了,ctl随之减一
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}

try {
// 这行语句表达了一个思想:线程只要是存活着的,他就应该执行任务。
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take(); // (1)
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}

我们来分析下getTask方法的(1)处代码:
如果核心线程可超时(allowCoreThreadTimeOut=true),那么在keepAliveTime时间内,核心线程一直是活着的,所以队列一有任务来就执行,否则它就阻塞等着,时刻等着任务来;
或者当前线程数超过核心线程数,那么在keepAliveTime时间内,大于核心线程数的那些线程一直是活着的,所以也是队列一有任务来就执行,否则就阻塞等着,时刻等着任务来;
这里有个疑问(问题D):为什么要用阻塞的方法呢,不阻塞的方法不行吗,答案文末给出

要想更好的理解execute方法,addWorker方法,runWorker方法,getTask方法中的特别是条件判断的逻辑,通过shutdown,shutdownNow,tryTerminate,awaitTermination相结合着更清晰些,下面我们就看下这些方法

以下是关闭相关的方法

shutdown和shutdownNow方法比较相似,类比图如下
20211106211652

shutdown

调用shutdown()方法会进入 SHUTDOWN 状态。在 SHUTDOWN 状态下,线程池不接受新的任务,但是会继续执行任务队列中已有的任务。
怎么证明它此时不接收新的任务了呢,场景 do it by practise

通过调用shutdown()关闭的线程池,关闭以后表现的行为就是不能再提交任务给线程池,但是在关闭前已经提交的任务仍旧会被执行。等到任务队列空了以后线程池才会进入关闭流程

1
2
3
4
5
6
7
8
9
10
11
12
13
scss复制代码public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(SHUTDOWN);
interruptIdleWorkers();
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
tryTerminate();
}

shutdown方法核心由三部分组成:

  1. advanceRunState(SHUTDOWN):改线程池状态为SHUTDOWN
  2. interruptIdleWorkers():中断线程池中空闲线程
  3. tryTerminate():Transitions to TERMINATED state if either (SHUTDOWN and pool and queue empty) or (STOP and pool empty)

advanceRunState

转换线程池状态为入参值,入参值只能是SHUTDOWN or STOP
如果是SHUTDOWN,那么执行完advanceRunState方法后ctl的值>=0,即>=SHUTDOWN。假如当前线程数是3,那么ctl就是3
如果是STOP,那么执行完advanceRunState方法后ctl的值>=536870912,即>=STOP。假如当前线程数是3,那么ctl就是536870912+3

所以,这也证实了线程池的状态是一个范围,而不是一个值,这个范围正如文档开头处所述

1
2
3
4
5
6
7
8
csharp复制代码private void advanceRunState(int targetState) {
for (;;) {
int c = ctl.get();
if (runStateAtLeast(c, targetState) ||
ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))))
break;
}
}

interruptIdleWorkers

方法很明确,就是将workers对应的线程中断。从方法的名称就可以知道功能是对空闲的线程中断。那怎么知道哪些work的线程是空闲的呢

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
java复制代码private void interruptIdleWorkers() {
interruptIdleWorkers(false);
}

private void interruptIdleWorkers(boolean onlyOne) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers) {
Thread t = w.thread;
if (!t.isInterrupted() && w.tryLock()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
} finally {
w.unlock();
}
}
if (onlyOne)
break;
}
} finally {
mainLock.unlock();
}
}

注意这点:w.tryLock(),为啥要试图加锁呢。这时候就要看看runWorker方法了,runWorker执行时是要对worker加锁的(即调用lock)。
所有工作中的线程都需要对Worker加锁,所以在这里通过Worker.tryLock()来判断被检查的工作线程是否是空闲状态,如果是空闲状态则表示可以加锁,然后发送interrupt()命令。在发送中断命令的过程中由于工作线程是处于加锁状态的,所以被中断线程将不能被同时用于执行任务。

tryTerminate

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
scss复制代码final void tryTerminate() {
for (;;) {
int c = ctl.get();
// 线程池为下面三种情况,直接return
// 1.线程池为RUNNING状态,线程池还在运行中,不能终止
// 2.线程池为TIDYING或TERMINATED,因为线程池已经终止了,不用再终止了
// 3.线程池为SHUTDOWN状态 & 线程池队列不为空,队列里有任务,不能终止
if (isRunning(c) ||
runStateAtLeast(c, TIDYING) ||
(runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
return;
// 代码走到这,说明此时线程池是STOP状态或(SHUTDOWN状态且线程池队列是空), 线程池里还有线程
if (workerCountOf(c) != 0) { // Eligible to terminate
//这时候可能只有一个空闲线程了,它是在getTask方法中执行workQueue.take()了的线程,此线程属于空闲线程(在w.lock()外),它正在阻塞着等待着线程来呢。如果不执行中断会一直阻塞。你可能会说,在前面执行interruptIdleWorkers(false)方法时,会中断所有的空闲线程,这里重复执行了吧?试想下如果在执行interruptIdleWorkers(false)时恰好有个工作线程没有空闲,你刚执行完interruptIdleWorkers(false),那个线程就回到while里去调用了getTask方法,这时workQueue中没有任务了,就会调用workQueue.take()一直阻塞。所以每次在工作线程结束时调用tryTerminate方法来尝试中断那个空闲工作线程,避免在队列为空时取任务一直阻塞的情况
interruptIdleWorkers(ONLY_ONE);
return;
}

final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 将线程池状态设置为TIDYING
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
try {
terminated();
} finally {
// 执行terminated后,将线程池状态设置为TERMINATED,线程池结束
ctl.set(ctlOf(TERMINATED, 0));
// 通知awaitTermination方法,线程池结束
termination.signalAll();
}
return;
}
} finally {
mainLock.unlock();
}
// else retry on failed CAS
}
}

shutdownNow

调用shutdownNow()会进入 STOP 状态。在 STOP 状态下线程池既不接受新的任务,也不处理已经在队列中的任务。对于还在执行任务的工作线程,线程池会发起中断请求来中断正在执行的任务,同时会清空任务队列中还未被执行的任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(STOP);
interruptWorkers();
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}

shutdownNow方法核心由四部分组成:

  1. advanceRunState(STOP):改线程池状态为STOP,与advanceRunState(SHUTDOWN)逻辑相同
  2. interruptWorkers():中断线程池中所有线程,这个与interruptIdleWorkers的区别细细体会,这个方法中断所有已经启动的工作线程,即进行中的任务(执行了w.lock,但还没执行w.unlock),这些线程中断可能成功也可能不成功
  3. tryTerminate():前面已说完
  4. drainQueue():从任务队列中取出所有未被执行的任务,未被执行的任务列表会被作为返回值返回给应用程序

interruptWorkers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
csharp复制代码// Interrupts all threads, even if active
private void interruptWorkers() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers)
w.interruptIfStarted();
} finally {
mainLock.unlock();
}
}
void interruptIfStarted() {
Thread t;
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
}
}
}

有个疑问:interruptWorkers是中断线程池中所有的线程(空闲的和执行中的总和),但interruptIfStarted()方法只是中断执行中的线程。如果你有这个疑惑的话,咱们一起看下getState() >= 0这个判断,我们看下runWorker方法,先执行了w.unlock(),再执行w.lock(),在执行w.unlock(),unlock是把state设置为0,lock把state设置为1,又只要执行了runWorker,那么state的值就是>=等于0的了,所以不管空闲与否,state总是>=0,所以interruptWorkers方法这时候执行interruptIfStarted方法,中断的就是所有的线程

drainQueue

将workerQueue队列里的worker返回

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码private List<Runnable> drainQueue() {
BlockingQueue<Runnable> q = workQueue;
ArrayList<Runnable> taskList = new ArrayList<Runnable>();
q.drainTo(taskList);
if (!q.isEmpty()) {
for (Runnable r : q.toArray(new Runnable[0])) {
if (q.remove(r))
taskList.add(r);
}
}
return taskList;
}

问答

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
arduino复制代码❶ execute方法里为啥要用workQueue.offer(command)这个非阻塞方法呢,而不用put等阻塞方法呢?
answer:
以为execute方法是运行在main线程里的,如果使用阻塞方法,那么后面的任务就无法添加到线程池了

❷ 传入的Runnable类型的任务紧接着就进行start了,那么为什么还要workers.add(w)放入集合呢,workers集合存在的意义是什么呢?
answer:
放入workers是为了保存当时的thread和worker,不然后面怎么对worker和thread进行加锁和中断啊,addWorker和runWorker本来就是并行的关系,还要时刻监视着shutdown,shutdownNow,terminate之类的动作

❸ workers 与 workerQueue 与 ctl的关系
workers的size应该=ctl.get;workerQueue与ctl没有关系

❹ getTask中为啥使用workQueue.poll(num,timeUnit) 和 take()阻塞方法,为啥不使用非阻塞方法呢?
answer:
getTask里有一个for自旋,一直找任务执行,如果不使用阻塞方法,那么for自旋将一直占着cpu,这个原理和synchronized的升级原理是一样的

❺ 线程池里存放的是线程吗
answer:
不是,是Worker,Worker是线程池中的线程和任务task之间的纽带

❻ 线程池使用了两个锁lock,worker使用一个,reentrantLock使用一个,作用分别是什么?
answer:
worker的lock是为了区分线程是否为空闲还是运行中
reentrantLock的lock是锁的本质使用

❼ 线程池里怎么区分空闲线程和执行中线程
answer:
worker的thread是否被lock

两个发散性的问题:
❽ 线程池中如何使用ThreadLocal
❾ ThreadPoolExecutor运行在多线程环境中会怎样的

小结

对ThreadPoolExecutor的理解每次都会有新的收获,看似不经意的一行代码,一个判断,实践懂了之后都感叹和震撼作者的编程能力,每次领悟之后,都感觉作者的实现技巧都开启了我以前没见过的窗。

ThreadPoolExecutor

ThreadPoolExecutor属性源码和自己debug实践的例子:ThreadPoolExecutorTest0

原文地址:ThreadPoolExecutor系列说之由浅入深源码解说

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

搞懂钩子方法和模板方法,看完这篇就够了

发表于 2021-11-07

本文节选自《设计模式就该这样学》

通常的模板方法模式中会设计一个abstract的抽象方法,交给它的子类实现,这个方法称为模板方法。而钩子方法,是对于抽象方法或者接口中定义的方法的一个空实现,也是模板方法模式的一种实现方式。

1 模板方法模式中的钩子方法

我们以网络课程创建流程为例:发布预习资料 → 制作课件PPT → 在线直播 → 提交课堂笔记 → 提交源码 → 布置作业 → 检查作业。首先创建AbastractCourse抽象类。

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
java复制代码
public abstract class AbastractCourse {

public final void createCourse(){
//1.发布预习资料
postPreResoucse();

//2.制作课件PPT
createPPT();

//3.在线直播
liveVideo();

//4.上传课后资料
postResource();

//5.布置作业
postHomework();

if(needCheckHomework()){
checkHomework();
}
}

protected abstract void checkHomework();

//钩子方法
protected boolean needCheckHomework(){return false;}

protected void postHomework(){
System.out.println("布置作业");
}

protected void postResource(){
System.out.println("上传课后资料");
}

protected void liveVideo(){
System.out.println("直播授课");
}

protected void createPPT(){
System.out.println("制作课件");
}

protected void postPreResoucse(){
System.out.println("发布预习资料");
}

}

上面代码中有个钩子方法,可能有些小伙伴不是太理解,在此笔者稍做解释。设计钩子方法的主要目的是干预执行流程,使得控制行为流程更加灵活,更符合实际业务的需求。钩子方法的返回值一般为适合条件分支语句的返回值(如boolean、int等)。小伙伴们可以根据自己的业务场景决定是否需要使用钩子方法。
然后创建JavaCourse类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码
public class JavaCourse extends AbastractCourse {
private boolean needCheckHomework = false;

public void setNeedCheckHomework(boolean needCheckHomework) {
this.needCheckHomework = needCheckHomework;
}

@Override
protected boolean needCheckHomework() {
return this.needCheckHomework;
}

protected void checkHomework() {
System.out.println("检查Java作业");
}
}

创建PythonCourse类。

1
2
3
4
5
6
java复制代码
public class PythonCourse extends AbastractCourse {
protected void checkHomework() {
System.out.println("检查Python作业");
}
}

最后编写客户端测试代码。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码
public static void main(String[] args) {
System.out.println("=========架构师课程=========");
JavaCourse java = new JavaCourse();
java.setNeedCheckHomework(false);
java.createCourse();


System.out.println("=========Python课程=========");
PythonCourse python = new PythonCourse();
python.createCourse();
}

通过这样一个案例,相信小伙伴们对模板方法模式有了一个基本的印象。为了加深理解,我们结合一个常见的业务场景进行介绍。

2 使用模板方法模式重构JDBC业务操作

创建一个模板类JdbcTemplate,封装所有的JDBC操作。以查询为例,每次查询的表都不同,返回的数据结构也就都不一样。我们针对不同的数据,都要封装成不同的实体对象。而每个实体封装的逻辑都是不一样的,但封装前和封装后的处理流程是不变的,因此,可以使用模板方法模式设计这样的业务场景。首先创建约束ORM逻辑的接口RowMapper。

1
2
3
4
5
6
7
8
java复制代码
/**
* ORM映射定制化的接口
* Created by Tom.
*/
public interface RowMapper<T> {
T mapRow(ResultSet rs,int rowNum) throws Exception;
}

然后创建封装了所有处理流程的抽象类JdbcTemplate。

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
java复制代码
public abstract class JdbcTemplate {
private DataSource dataSource;

public JdbcTemplate(DataSource dataSource) {
this.dataSource = dataSource;
}

public final List<?> executeQuery(String sql,RowMapper<?> rowMapper,Object[] values){
try {
//1.获取连接
Connection conn = this.getConnection();
//2.创建语句集
PreparedStatement pstm = this.createPrepareStatement(conn,sql);
//3.执行语句集
ResultSet rs = this.executeQuery(pstm,values);
//4.处理结果集
List<?> result = this.parseResultSet(rs,rowMapper);
//5.关闭结果集
rs.close();
//6.关闭语句集
pstm.close();
//7.关闭连接
conn.close();
return result;
}catch (Exception e){
e.printStackTrace();
}
return null;
}

private List<?> parseResultSet(ResultSet rs, RowMapper<?> rowMapper) throws Exception {
List<Object> result = new ArrayList<Object>();
int rowNum = 0;
while (rs.next()){
result.add(rowMapper.mapRow(rs,rowNum++));
}
return result;
}


private ResultSet executeQuery(PreparedStatement pstm, Object[] values) throws SQLException {
for (int i = 0; i < values.length; i++) {
pstm.setObject(i,values[i]);
}
return pstm.executeQuery();
}

private PreparedStatement createPrepareStatement(Connection conn, String sql) throws SQLException {
return conn.prepareStatement(sql);
}

private Connection getConnection() throws SQLException {
return this.dataSource.getConnection();
}
}

创建实体对象Member类。

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
java复制代码
public class Member {

private String username;
private String password;
private String nickname;
private int age;
private String addr;

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public String getNickname() {
return nickname;
}

public void setNickname(String nickname) {
this.nickname = nickname;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getAddr() {
return addr;
}

public void setAddr(String addr) {
this.addr = addr;
}
}

创建数据库操作类MemberDao。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码
public class MemberDao extends JdbcTemplate {
public MemberDao(DataSource dataSource) {
super(dataSource);
}

public List<?> selectAll(){
String sql = "select * from t_member";
return super.executeQuery(sql, new RowMapper<Member>() {
public Member mapRow(ResultSet rs, int rowNum) throws Exception {
Member member = new Member();
//字段过多,原型模式
member.setUsername(rs.getString("username"));
member.setPassword(rs.getString("password"));
member.setAge(rs.getInt("age"));
member.setAddr(rs.getString("addr"));
return member;
}
},null);
}
}

最后编写客户端测试代码。

1
2
3
4
5
6
java复制代码
public static void main(String[] args) {
MemberDao memberDao = new MemberDao(null);
List<?> result = memberDao.selectAll();
System.out.println(result);
}

希望通过这两个案例的业务场景分析,小伙伴们能够对模板方法模式有更深的理解。

关注『 Tom弹架构 』回复“设计模式”可获取完整源码。

【推荐】Tom弹架构:30个设计模式真实案例(附源码),挑战年薪60W不是梦

本文为“Tom弹架构”原创,转载请注明出处。技术在于分享,我分享我快乐!如果本文对您有帮助,欢迎关注和点赞;如果您有任何建议也可留言评论或私信,您的支持是我坚持创作的动力。关注『 Tom弹架构 』可获取更多技术干货!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring Security专栏(基于方法级别的保护)

发表于 2021-11-07

这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战

写在前面

各位小伙伴。 到目前为止,我们已经系统介绍了 Spring Security 中的认证和授权过程。但是请注意,我们讨论的对象是 Web 应用程序,也就是说认证和授权的资源是一系列的 HTTP 端点。那么如果我们开发的不是一个 Web 应用程序呢?认证和授权还能否发挥作用呢?答案是肯定的。

今天我们就来讨论针对方法级别的安全访问策略,确保一个普通应用程序中的每个组件都能具备安全性保障。

这里多唠叨一句,欢迎大家查看我的专栏,目前正在进行的Security专栏和队列并发专栏,设计模式专题(已完结)

全局方法安全机制

明确方法级别的安全机制之前,我们先来剖析一个典型的应用程序具备的各层组件。以 Spring Boot 应用程序为例,我们可以采用经典的分层架构,即将应用程序分成 Controller 层、Service 层和 Repository 层。请注意,三层架构中的 Service 层组件可能还会调用其他的第三方组件。

请注意,默认情况下 Spring Security 并没有启用全局方法安全机制。因此,想要启用这个功能,我们需要使用@EnableGlobalMethodSecurity 注解。正如本专栏前面案例所展示的,一般的做法是创建一个独立的配置类,并把这个注解添加在配置类上,如下所示:

1
2
3
java复制代码@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig

方法调用过滤本质上类似于过滤器机制,也可以分为 PreFilter 和 PostFilter 两大类。其中预过滤(PreFilter)用来对该方法的参数进行过滤,从而获取其参数接收的内容,而后过滤(PostFilter)则用来判断调用者可以在方法执行后从方法返回结果中接收的内容。

请注意,在使用 @EnableGlobalMethodSecurity 注解时,我们设置了“prePostEnabled”为 true,意味着我们启用了 Pre/PostAuthorization 注解,而默认情况下这些注解也是不生效的。同时,我们也需要知道,在 Spring Security 中为实现全局方法安全机制提供了三种实现方法,除了 Pre/PostAuthorization 注解之外.

在本专栏中,我们只讨论最常用的 Pre/PostAuthorization 注解,下面我们来看具体的使用方法。

使用注解实现方法级别授权

针对方法级别授权,Spring Security 提供了 @PreAuthorize 和 @PostAuthorize 这两个注解,分别用于预授权和后授权。

今天我们先来看@PreAuthorize 下期再看 @PostAuthorize

@PreAuthorize 注解

先来看 @PreAuthorize 注解的使用场景。假设在一个基于 Spring Boot 的 Web 应用程序中,存在一个 Web 层组件 OrderController.

该 Controller 会调用 Service 层的组件 OrderService。我们希望对访问 OrderService 层中方法的请求添加权限控制能力,即只有具备“DELETE”权限的请求才能执行 OrderService 中的 deleteOrder() 方法,而没有该权限的请求将直接抛出一个异常,如下图所示(Service 层组件预授权示意图):

image.png

显然,上述流程针对的是预授权的应用场景,因此我们可以使用 @PreAuthorize 注解,

该注解定义如下:

1
2
3
4
5
6
7
8
9
java复制代码@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface PreAuthorize {
 
    //通过SpEL表达式设置访问控制
    String value();
}

要想在应用程序中集成 @PreAuthorize 注解,我们可以创建如下所示的安全配置类,在这个配置类上我们添加了 @EnableGlobalMethodSecurity 注解:

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
java复制代码@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
 
    @Bean
    public UserDetailsService userDetailsService() {
        UserDetailsService service = new InMemoryUserDetailsManager();
 
        UserDetails u1 = User.withUsername("yn1")
                .password("12345")
                .authorities("WRITE")
                .build();
 
        UserDetails u2 = User.withUsername("yn2")
                .password("12345")
                .authorities("DELETE")
                .build();
 
        service.createUser(u1);
        service.createUser(u2);
 
        return service;
    }
 
    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
}

这里,我们创建了两个用户“yn1”和“yn2”,分别具备“WRITE”和“DELETE”权限。然后,我们实现 OrderService 的 deleteOrder() 方法,如下所示:

1
2
3
4
5
6
7
8
java复制代码@Service
public class OrderService {
 
    @PreAuthorize("hasAuthority('DELETE')")
    public void deleteOrder(String orderId) {
        …
    }
}

可以看到,这里使用了 @PreAuthorize 注解来实现预授权。在该注解中,我们通过熟悉的 hasAuthority(‘DELETE’) 方法来判断请求是否具有“DELETE”权限。

上面介绍的这种情况比较简单,我们再来看一个比较复杂的场景,该场景与用户认证过程进行整合。

假设在 OrderService 中存在一个 getOrderByUser(String user) 方法,而出于系统安全性的考虑,我们希望用户只能获取自己创建的订单信息,也就是说我们需要校验通过该方法传入的“user”参数是否为当前认证的合法用户。这种场景下,我们就可以使用 @PreAuthorize 注解:

1
2
3
4
java复制代码@PreAuthorize("#name == authentication.principal.username")
public List<Order> getOrderByUser(String user) {
        …
}

这里我们将输入的“user”参数与通过 SpEL 表达式从安全上下文中获取的“authentication.principal.username”进行比对,如果相同就执行正确的方法逻辑,反之将直接抛出异常。

好,今天我们就讲这一个小点,明天我们讲另一个注解 慢慢学。加油!

总结

这一讲我们关注的重点从 HTTP 端点级别的安全控制转换到了普通方法级别的安全控制。Spring Security 内置了一组非常实用的注解,方便开发人员实现全局方法安全机制,包括用于实现方法级别授权的 @PreAuthorize 和 @PostAuthorize 注解(下期讲)

弦外之音

感谢你的阅读,如果你感觉学到了东西,您可以点赞,关注。也欢迎有问题我们下面评论交流

加油! 我们下期再见!

给大家分享几个我前面写的几篇骚操作

copy对象,这个操作有点骚!

干货!SpringBoot利用监听事件,实现异步操作

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringBoot邮件发送 1邮件传输协议 2开启SM

发表于 2021-11-07

1.邮件传输协议

电子邮件需要在邮件客户端和邮件服务器之间,以及两个邮件服务器之间进行邮件传递,那就必须要遵守一定的规则,这个规则就是邮件传输协议。下面我们分别简单介绍几种协议:

  1. SMTP协议:全称为 Simple Mail Transfer Protocol,简单邮件传输协议。它定义了邮件客户端软件和SMTP邮件服务器之间,以及两台SMTP邮件服务器之间的通信规则。
  2. POP3协议:全称为 Post Office Protocol,邮局协议。它定义了邮件客户端软件和POP3邮件服务器的通信规则。
  3. IMAP协议:全称为 Internet Message Access Protocol,Internet消息访问协议,它是对POP3协议的一种扩展,也是定义了邮件客户端软件和IMAP邮件服务器的通信规则。

2.开启SMTP服务并获取授权码

这里我们以QQ邮箱为例,要想在SpringBoot发送QQ邮件必须先打开QQ邮箱的SMTP功能,默认是关闭的,具体操作如下。进入邮箱→设置→账户,然后找到下面这个

image.png

这里有个验证

image.png

验证完成之后

image.png

3.导入依赖与配置说明

这里我用的是gradle,引入spring-boot-starter-mail模块

1
java复制代码implementation group: 'org.springframework.boot', name: 'spring-boot-starter-mail', version: '2.4.10'

application.yml配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码spring:
mail:
# 邮件服务器地址
host: smtp.qq.com
# SMTP 服务器的端口
port: 587
username: 1786087581@qq.com
# 这里的密码是邮件授权码不是邮箱登陆的密码
# 这里的邮件授权码我随便写的
password: aksdfgsdfgxieiig
# 额外的配置,这里我写了两个,只用其中一个就行了,开启ssl加密,保证安全连接
properties:
mail:
smtp:
socketFactory:
class: javax.net.ssl.SSLSocketFactory
## ssl:
## enable :true
#设置邮件的编码为utf-8
default-encoding: utf-8

补充:

126邮箱SMTP服务器地址:smtp.126.com,端口号:465或者994

2163邮箱SMTP服务器地址:smtp.163.com,端口号:465或者994

yeah邮箱SMTP服务器地址:smtp.yeah.net,端口号:465或者994

qq邮箱SMTP服务器地址:smtp.qq.com,端口号465或587*

4.邮件发送

4.1 简单邮件发送

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
java复制代码
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import java.io.File;
import java.util.Date;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.FileSystemResource;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Api(value = "邮件接口",tags = "邮件接口",description = "邮件接口")
@RequestMapping("/mail")
public class SendMailController {

@Autowired
JavaMailSender javaMailSender;

@GetMapping("/sendMail")
public void sendMail(){
//构建一个邮件对象
SimpleMailMessage message = new SimpleMailMessage();
//设置邮件主题
message.setSubject("这是一封测试邮件");
//设置邮件发送者
message.setFrom("1786087581@qq.com");
//设置邮件接收者,可以有多个接收者
message.setTo("1******40@qq.com","1*******7@163.com");
//设置邮件抄送人,可以有多个抄送人
message.setCc("6666***8@qq.com");
//设置隐秘抄送人,可以有多个
message.setBcc("l*****3@163.com");
//设置邮件发送日期
message.setSentDate(new Date());
//设置邮件的正文
message.setText("测试邮件正文ok");
//发送邮件
javaMailSender.send(message);
}

测试结果 发送成功
image.png

4.2发送带图片资源的邮件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码@GetMapping("/sendMailWithImg")
@ApiOperation(value = "简单带图片邮件发送",notes = "简单带图片邮件发送",produces = "application/json")
public void sendMailWithImg() throws MessagingException {
//创建一个复杂的邮件
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
//设置邮件主题
helper.setSubject("这是一封带图片测试邮件");
//设置邮件发送者
helper.setFrom("1786087581@qq.com");
//设置邮件接收者
helper.setTo("1******40@qq.com");
//设置邮件抄送人
helper.setCc("6666***8@qq.com");
//设置隐秘抄送人
helper.setBcc("l*****3@163.com");
//设置邮件发送日期
helper.setSentDate(new Date());
//设置邮件的正文
helper.setText("<p>这是一封带图片测试邮件,这封邮件包含两种图片,分别如下</p><p>第一张图片:</p><img src='cid:p01'/><p>第二张图片:</p><img src='cid:p02'/>",true);
helper.addInline("p01",new FileSystemResource(new File("C:\Users\hasee\Desktop\9cae14e699762b40a747d4198.jpg")));
helper.addInline("p02",new FileSystemResource(new File("C:\Users\hasee\Desktop\微信图片_202101.jpg")));
javaMailSender.send(mimeMessage);
}

测试结果 发送成功

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

redis 性能测试(三):Redission 分布式锁的并

发表于 2021-11-07

这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战

Redission 分布式锁的并发测试

前言

在单机场景下,可以使用内置锁来实现进程同步,但在分布式场景下需要同步的进程可能位于不同节点,就需要在分布式部署的应用集群中使用分布式锁,即同一个方法只能被一台机器上的一个线程来执行,分布式锁的用途就是解决分布式环境下同一个方法被客户端调用的一致性问题,同时 redis 本身也可能是集群的,

而在集群环境下 Redis自带的分布式锁会面临一个问题,它加锁只作用在一个 Redis 节点上,如果master 因为某些原因进行了主从切换,那么该锁可能就会丢失,因此 Redis 的作者提出了 RedLock 算法来解决这个问题,这里我不讲这个细节,只记住redisssion包它本身已经对 Redlock算法进行了封装,所以它是可以保证 Redis集群和服务节点分布式下的安全性问题。

Redission 在分布式中的应用是非常的广泛的,这篇文章将对 redission 进行分布式压力测试,结果好为后续开发做参考。

一、项目引入 Redission

1
2
3
4
5
xml复制代码        <dependency>
          <groupId>org.redisson</groupId>
          <artifactId>redisson-spring-boot-starter</artifactId>
          <version>3.15.5</version>
      </dependency>

yml 配置

1
2
3
4
5
6
7
8
yaml复制代码spring:
application:
  name: springboot-redisson
redis:
  redisson:
    singleServerConfig:
password: 123456
address: "redis://127.0.0.1:6379"

二、模拟并发请求压测

2.1 提前准备数据

提前在 redis 中将它 NUM 的值设为0。

image-20211105163041541

路过的请求将 该NUM值加1。

2.2 不加锁测试

在这版本中,我们不用分布式锁,将该值加1代码如下

1
scss复制代码redisTemplate.opsForValue().increment("NUM",1);

使用 JMeter 来测试

1) 10线程并发请求

image-20211105163646444

image-20211105163715886

值是没有问题的,下面开始正式测试。

2)1000线程并发请求

image-20211105164151312

1000次并发没有问题,

3)10000线程并发请求

在一秒1w次的并发压力下,出现了问题。

image-20211105164332296

4)5000线程并发测试

此时异常率为 9.58%。

image-20211105164438383

5)3000线程并发测试

正常

image-20211105164639104

6)4000线程并发测试

正常

image-20211105164725951

7)4500 线程并发测试

异常率为3.16%

image-20211105164801096

在经过这次测试后,发现在不加锁的情况下,1s内并发请求达到 4000-4500内就会出现异常现象。

2.3 分布式测试

这里我们通过 Redission 的分布式锁来测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
less复制代码    @PostMapping("/environment/find")
  public ResponseData<List<EnviromentReportVO>> enviList(@RequestBody RequestData<ReportDTO, UserVO> requestData){
      RLock lock = redissonClient.getLock("lei");
      lock.lock();
      System.out.println(Thread.currentThread().getName()+"获得锁");
      try {
      Thread.sleep(10);
          redisSdk.incrementInt("NUM",1);
      } catch (InterruptedException e) {
          e.printStackTrace();
      }finally {
          System.out.println(Thread.currentThread().getName()+"释放锁");
          lock.unlock();
      }
      return null;
  }

原理是在加锁期间,其他锁无法占用该锁,直到该锁到期后才可以占用。

1)5000 线程并发请求

在5000 次并发请求下,是没有问题的,但是使用分布式锁之后,吞吐率下降了几十倍,耗时1分21秒。redis 的值也变为了 5000

image-20211105165553647

image-20211105165709648

2)7000线程并发请求

7000 次并发请求正常,耗时近2分钟。

image-20211105171331522

在进行1000次并发请求时,因为redis无法承受这么大的 IO,会主动进行中断连接,所以就没测了,不过由本轮测试可以看到 Redisson 的锁是起作用的。在高并发下,在实际生产中如果对于并发问题,可以考虑使用Redisson 来应对高并发问题,它能很好的解决分布式下多线程竞争资源问题。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Linux常用命令 - 不古出品 一、常用命令 二、目录相关

发表于 2021-11-07

2021-11-07

Linux常用命令 - 不古出品

  • 一、常用命令
  • 1.1 帮助命令
  • 1.2 关机、重启和注销
  • 1.3切换用户 su
  • 二、目录相关命令
  • 2.1基本命令
  • 2.2目录查看
  • 2.3目录切换
  • 2.4目录操作【增删改查】
  • 2.4.1创建目录-增
  • 2.4.1删除目录-删
  • 2.4.1修改目录-改
  • 2.4.1查找目录-查
  • 三、文件相关命令
  • 3.1 文件操作【增删改查】
  • 3.1.1新建文件-增
  • 3.1.2删除文件-删 rm
  • 3.1.3修改文件-改 vi或vim
  • 3.1.2.1 vi/vim 的使用
  • 3.1.2.2相关命令
  • 3.1.4 文件的查看-查 cat\more\less\tail
  • 3.2 权限修改
  • 四、压缩和解压缩
  • 4.1 方式一:gzip/gunzip
  • 4.2 方式二:zip/unzip
  • 4.3 方式三:tar
  • 五、查找命令
  • 5.1 grep
  • 5.1 find
  • 5.3 locate
  • 5.4 which
  • 六、系统服务
  • 七、网络管理
  • 7.1 主机名配置
  • 7.2 ping
  • 7.3 ifconfig
  • 7.4 nslookup
  • 7.5 telnet
  • 7.6 netstat
  • 八、定时任务
  • 8.1 crontab命令
  • 8.2 配置说明、实例
  • 九、其他命令
  • 9.1 查看当前目录:pwd
  • 9.2 查看进程:ps -ef
  • 9.3 结束进程:kill
  • 9.4 网络通信命令:
  • 9.5 配置网络
  • 9.6 重启网络
  • 9.7 切换用户
  • 9.8 关闭防火墙
  • 9.9 修改文件权限
  • 9.10 清屏
  • 9.11 vi模式下快捷键

一、常用命令

1.1 帮助命令

–help命令

ls –help:查看

shutdown –help:查看shutdown命令相关参数

ifconfig –help:查看网卡信息

–man命令(命令说明书)

man shutdown

注意:man shutdown打开命令说明书之后,使用按键q退出

1.2 关机、重启和注销

–关机

shutdown -h now 立刻关机

shutdown -h 5 5分钟后关机

poweroff 立刻关机

–重启

shutdown -r now 立刻重启

shutdown -r 5 5分钟后重启

reboot 立刻重启

–注销

logout 退出当前登录用户

exit 退出当前登录用户

ctrl+d 退出当前登录用户

1.3切换用户 su

su用于用户之间的切换。但是切换前的用户依然保持登录状态。如果是root 向普通或虚拟用户切换不需要密码,反之普通用户切换到其它任何用户都需要密码验证。

su test:切换到test用户,但是路径还是/root目录

su - test : 切换到test用户,路径变成了/home/test

su : 切换到root用户,但是路径还是原来的路径

su - : 切换到root用户,并且路径是/root

su不足:如果某个用户需要使用root权限、则必须要把root密码告诉此用户。

退出返回之前的用户:exit

二、目录相关命令

2.1基本命令

ls(英文全拼:list files): 列出目录及文件名

cd(英文全拼:change directory):切换目录

pwd(英文全拼:print work directory):显示目前的目录

mkdir(英文全拼:make directory):创建一个新的目录

rmdir(英文全拼:remove directory):删除一个空的目录

cp(英文全拼:copy file): 复制文件或目录

rm(英文全拼:remove): 删除文件或目录

mv(英文全拼:move file): 移动文件与目录,或修改文件与目录的名称

2.2目录查看

命令:ls [-al]

ls 查看当前目录下的所有目录和文件

ls -a 查看当前目录下的所有目录和文件(包括隐藏的文件)

ls -l 或 ll 列表查看当前目录下的所有目录和文件(列表查看,显示更多信息)

ls /dir 查看指定目录下的所有目录和文件 如:ls /usr

2.3目录切换

命令:cd 目录

cd / 切换到根目录

cd /usr 切换到根目录下的usr目录

cd …/ 切换到上一级目录 或者 cd …

cd ~ 切换到home目录

cd - 切换到上次访问的目录

2.4目录操作【增删改查】

2.4.1创建目录-增

命令:mkdir 目录

mkdir aaa 在当前目录下创建一个名为aaa的目录

mkdir /usr/aaa 在指定目录下创建一个名为aaa的目录

2.4.1删除目录-删

命令:rm [-rf] 目录

删除文件:

rm 文件 删除当前目录下的文件

rm -f 文件 删除当前目录的的文件(不询问)

删除目录:

rm -r aaa 递归删除当前目录下的aaa目录

rm -rf aaa 递归删除当前目录下的aaa目录(不询问)

全部删除:

rm -rf * 将当前目录下的所有目录和文件全部删除

rm -rf /* 【自杀命令!慎用!慎用!慎用!】将根目录下的所有文件全部删除

注意:rm不仅可以删除目录,也可以删除其他文件或压缩包,为了方便大家的记忆,无论删除任何目录或文件,都直接使用 rm -rf 目录/文件/压缩包

2.4.1修改目录-改

1、重命名目录

命令:mv 当前目录 新目录

例如:mv aaa bbb 将目录aaa改为bbb

注意:mv的语法不仅可以对目录进行重命名而且也可以对各种文件,压缩包等进行 重命名的操作

2、剪切目录

命令:mv 目录名称 目录的新位置

示例:将/usr/tmp目录下的aaa目录剪切到 /usr目录下面 mv /usr/tmp/aaa /usr

注意:mv语法不仅可以对目录进行剪切操作,对文件和压缩包等都可执行剪切操作

3、拷贝目录

命令:cp -r 目录名称 目录拷贝的目标位置 -r代表递归

示例:将/usr/tmp目录下的aaa目录复制到 /usr目录下面 cp /usr/tmp/aaa /usr

注意:cp命令不仅可以拷贝目录还可以拷贝文件,压缩包等,拷贝文件和压缩包时不 用写-r递归

2.4.1查找目录-查

命令:find 目录 参数 文件名称

示例:find /usr/tmp -name ‘a*’ 查找/usr/tmp目录下的所有以a开头的目录或文件

三、文件相关命令

3.1 文件操作【增删改查】

3.1.1新建文件-增

命令:touch 文件名

示例:在当前目录创建一个名为aa.txt的文件 touch aa.txt

3.1.2删除文件-删 rm

命令:rm -rf 文件名

3.1.3修改文件-改 vi或vim

3.1.2.1 vi/vim 的使用

基本上 vi/vim 共分为三种模式,分别是命令模式(Command mode),输入模式(Insert mode)和底线命令模式(Last line mode)。 这三种模式的作用分别是:

命令模式

用户刚刚启动 vi/vim,便进入了命令模式。

此状态下敲击键盘动作会被Vim识别为命令,而非输入字符。比如我们此时按下i,并不会输入一个字符,i被当作了一个命令。

以下是常用的几个命令:

i 切换到输入模式,以输入字符。

x 删除当前光标所在处的字符。

: 切换到底线命令模式,以在最底一行输入命令。

若想要编辑文本:启动Vim,进入了命令模式,按下i,切换到输入模式。

命令模式只有一些最基本的命令,因此仍要依靠底线命令模式输入更多命令。

输入模式

在命令模式下按下i就进入了输入模式。

在输入模式中,可以使用以下按键:

ESC,退出输入模式,切换到命令模式

底线命令模式

在命令模式下按下:(英文冒号)就进入了底线命令模式。

底线命令模式可以输入单个或多个字符的命令,可用的命令非常多。

在底线命令模式中,基本的命令有(已经省略了冒号):

q 退出程序

w 保存文件

按ESC键可随时退出底线命令模式。

简单的说,我们可以将这三个模式想成底下的图标来表示:

在这里插入图片描述

3.1.2.2相关命令

打开文件

命令:vi 文件名

示例:打开当前目录下的aa.txt文件 vi aa.txt 或者 vim aa.txt

注意:使用vi编辑器打开文件后,并不能编辑,因为此时处于命令模式,点击键盘i/a/o进入编辑模式。

编辑文件

使用vi编辑器打开文件后点击按键:i ,a或者o即可进入编辑模式。

i:在光标所在字符前开始插入

a:在光标所在字符后开始插入

o:在光标所在行的下面另起一新行插入

保存或者取消编辑

保存文件:

第一步:ESC 进入命令行模式

第二步:: 进入底行模式

第三步:wq 保存并退出编辑

取消编辑:

第一步:ESC 进入命令行模式

第二步:: 进入底行模式

第三步:q! 撤销本次修改并退出编辑

3.1.4 文件的查看-查 cat\more\less\tail

文件的查看命令:cat/more/less/tail

cat:看最后一屏

示例:使用cat查看/etc/sudo.conf文件,只能显示最后一屏内容

cat sudo.conf

more:百分比显示

示例:使用more查看/etc/sudo.conf文件,可以显示百分比,回车可以向下一行,空格可以向下一页,q可以退出查看

more sudo.conf

less:翻页查看

示例:使用less查看/etc/sudo.conf文件,可以使用键盘上的PgUp和PgDn向上 和向下翻页,q结束查看

less sudo.conf

tail:指定行数或者动态查看

示例:使用tail -10 查看/etc/sudo.conf文件的后10行,Ctrl+C结束

tail -10 sudo.conf

3.2 权限修改

chmod用于改变文件或目录的访问权限。用户用它控制文件或目录的访问权限。该命令有两种用法。一种是包含字母和操作符表达式的文字设定法;另一种是包含数字的数字设定法。

chmod功能:变更文件或目录的权限

语法:chmod [参数][<权限范围><符号><权限代号>]

– 有关权限代号的部分

r:读取权限,数字代号为“4”

w:写入权限,数字代号为“2”

x:执行或切换权限,数字代号为“1”

不具任何权限,数字代号为“0”

例:

添加权限

chmod u+rwx xxx 添加xxx文件的用户“读写执行”权限

取消权限

chmod u-7 xxx 取消xxx文件的用户“读写执行”权限

四、压缩和解压缩

4.1 方式一:gzip/gunzip

gzip用于压缩文件,gunzip用于解压文件

gzip 文件名 压缩文件为*.gz

gunzip 文件.gz 解压文件

4.2 方式二:zip/unzip

zip用于压缩文件,unzip用于解压文件

zip 文件名 压缩文件为*.zip

unzip 文件.zip 解压文件

4.3 方式三:tar

压缩

命令:tar -zcvf 打包压缩后的文件名 要打包的文件

其中:z:调用gzip压缩命令进行压缩

c:打包文件

v:显示运行过程

f:指定文件名

示例:打包并压缩/usr/tmp 下的所有文件 压缩后的压缩包指定名称为xxx.tar

tar -zcvf ab.tar aa.txt bb.txt

或:tar -zcvf ab.tar *

解压

命令:tar [-zxvf] 压缩文件

其中:x:代表解压

示例:将/usr/tmp 下的ab.tar解压到当前目录下

五、查找命令

5.1 grep

grep命令是一种强大的文本搜索工具

使用实例:

ps -ef | grep sshd 查找指定ssh服务进程

ps -ef | grep sshd | grep -v grep 查找指定服务进程,排除gerp身

ps -ef | grep sshd -c 查找指定进程个数

5.1 find

find命令在目录结构中搜索文件,并对搜索结果执行指定的操作。

find 默认搜索当前目录及其子目录,并且不过滤任何结果(也就是返回所有文件),将它们全都显示在屏幕上。

使用实例:

find . -name “*.log” -ls 在当前目录查找以.log结尾的文件,并显示详细信息。

find /root/ -perm 600 查找/root/目录下权限为600的文件

find . -type f -name “*.log” 查找当目录,以.log结尾的普通文件

find . -type d | sort 查找当前所有目录并排序

find . -size +100M 查找当前目录大于100M的文件

5.3 locate

locate 让使用者可以很快速的搜寻某个路径。默认每天自动更新一次,所以使用locate 命令查不到最新变动过的文件。为了避免这种情况,可以在使用locate之前,先使用updatedb命令,手动更新数据库。如果数据库中没有查询的数据,则会报出locate: can not stat () `/var/lib/mlocate/mlocate.db’: No such file or directory该错误!updatedb即可!

yum -y install mlocate 如果是精简版CentOS系统需要安装locate命令

使用实例:

updatedb 更新系统中文件目录数据库

locate /etc/sh 搜索etc目录下所有以sh开头的文件

locate pwd 查找和pwd相关的所有文件

5.4 which

which命令的作用是在PATH变量指定的路径中,搜索某个系统命令的位置,并且返回第一个搜索结果。

使用实例:

which pwd 查找pwd命令所在路径

which java 查找path中java的路径

六、系统服务

service iptables status –查看iptables服务的状态

service iptables start –开启iptables服务

service iptables stop –停止iptables服务

service iptables restart –重启iptables服务

chkconfig iptables off –关闭iptables服务的开机自启动

chkconfig iptables on –开启iptables服务的开机自启动

七、网络管理

网络和监控命令类似于这些: hostname, ping, ifconfig, iwconfig, netstat, nslookup, traceroute, finger, telnet, ethtool 用于查看 linux 服务器 ip 地址,管理服务器网络配置,通过 telnet 和 ethernet 建立与 linux 之间的网络链接,查看 linux 的服务器信息等。下面让我们看看在 Linux 下的网络和监控命令的使用。

7.1 主机名配置

hostname

hostname 没有选项,显示主机名字

hostname –d 显示机器所属域名

hostname –f 显示完整的主机名和域名

hostname –i 显示当前机器的 ip 地址

7.2 ping

ping 将数据包发向用户指定地址。当包被接收,目标机器发送返回数据包。ping 主要有两个作用:

用来确认网络连接是畅通的。

用来查看连接的速度信息。

如果你 ping zhangge.net 它将返回它的 ip 地址 。你可以通过 ctrl+C 来停止命令。

7.3 ifconfig

查看用户网络配置。它显示当前网络设备配置。对于需要接收或者发送数据错误查找,这个工具极为好用。

7.4 nslookup

nslookup 这个命令在 有 ip 地址时,可以用这个命令来显示主机名,可以找到给定域名的所有 ip 地址。而你必须连接到互联网才能使用这个命令。

例子. nslookup marsge.cn

你也可以使用 nslookup 从 ip 获得主机名或从主机名获得 ip。

7.5 telnet

通过 telnet 协议连接目标主机,如果 telnet 连接可以在任一端口上完成即代表着两台主机间的连接良好。

telnet hostname port – 使用指定的端口 telnet 主机名。这通常用来测试主机是否在线或者网络是否正常。

7.6 netstat

发现主机连接最有用最通用的 Linux 命令。你可以使用”netstat -g”查询该主机订阅的所有多播组(网络)

netstat -nap | grep port 将会显示使用该端口的应用程序的进程 id

netstat -a or netstat –all 将会显示包括 TCP 和 UDP 的所有连接

netstat –tcp or netstat –t 将会显示 TCP 连接

netstat –udp or netstat –u 将会显示 UDP 连接

netstat -g 将会显示该主机订阅的所有多播网络。

八、定时任务

crontab是Unix和Linux用于设置定时任务的指令。通过crontab命令,可以在固定间隔时间,执行指定的系统指令或shell脚本。时间间隔的单位可以是分钟、小时、日、月、周及以上的任意组合。

crontab安装:

yum install crontabs

服务操作说明:

service crond start ## 启动服务

service crond stop ## 关闭服务

service crond restart ## 重启服务

8.1 crontab命令

crontab [-u user] file

crontab [-u user] [ -e | -l | -r ]

参数说明:

-u user:用来设定某个用户的crontab服务

file:file是命令文件的名字,表示将file做为crontab的任务列表文件并载入crontab。

-e:编辑某个用户的crontab文件内容。如果不指定用户,则表示编辑当前用户的crontab文件。

-l:显示某个用户的crontab文件内容。如果不指定用户,则表示显示当前用户的crontab文件内容。

-r:删除定时任务配置,从/var/spool/cron目录中删除某个用户的crontab文件,如果不指定用户,则默认删除当前用户的crontab文件。

命令示例:

crontab file [-u user] ## 用指定的文件替代目前的crontab

crontab -l [-u user] ## 列出用户目前的crontab

crontab -e [-u user] ## 编辑用户目前的crontab

8.2 配置说明、实例

命令:* * * * * command

解释:分 时 日 月 周 命令

第1列表示分钟1~59 每分钟用*或者 */1表示

第2列表示小时0~23(0表示0点)

第3列表示日期1~31

第4列表示月份1~12

第5列标识号星期0~6(0表示星期天)

第6列要运行的命令

配置实例:

先打开定时任务所在的文件:

crontab -e

每分钟执行一次date命令

*/1 * * * * date >> /root/date.txt

每晚的21:30重启apache。

30 21 * * * service httpd restart

每月1、10、22日的4 : 45重启apache。

45 4 1,10,22 * * service httpd restart

每周六、周日的1 : 10重启apache。

10 1 * * 6,0 service httpd restart

每天18 : 00至23 : 00之间每隔30分钟重启apache。

0,30 18-23 * * * service httpd restart

晚上11点到早上7点之间,每隔一小时重启apache

23-7/1 * * * service httpd restart

九、其他命令

9.1 查看当前目录:pwd

命令:pwd 查看当前目录路径

9.2 查看进程:ps -ef

命令:ps -ef 查看所有正在运行的进程

9.3 结束进程:kill

命令:kill pid 或者 kill -9 pid(强制杀死进程) pid:进程号

9.4 网络通信命令:

ifconfig:查看网卡信息

命令:ifconfig 或 ifconfig | more

ping:查看与某台机器的连接情况

命令:ping ip

netstat -an:查看当前系统端口

命令:netstat -an

搜索指定端口

命令:netstat -an | grep 8080

9.5 配置网络

命令:setup

9.6 重启网络

命令:service network restart

9.7 切换用户

命令:su - 用户名

9.8 关闭防火墙

命令:chkconfig iptables off

或者:

iptables -L;

iptables -F;

service iptables stop

9.9 修改文件权限

命令:chmod 777

9.10 清屏

命令:ctrl + l

9.11 vi模式下快捷键

esc后:

保存并退出快捷键:shift+z+z

光标跳到最后一行快捷键:shift+g

删除一行:dd

复制一行内容:y+y

粘贴复制的内容:p

关注微信公众号【不古】,回复linux,领取linux教程

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

如何实现一个支持动态扩容的数组 如何实现一个支持动态扩容的数

发表于 2021-11-07

如何实现一个支持动态扩容的数组

这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战

笔者最近在学习《数据结构与算法之美》,正好借着这个机会边练习边记录一下自己学习的知识点。嘿嘿。

一、数组

什么是数组?

数组是一种线性数据结构,用连续的内存空间存储相同数据类型的数据。

由此我们可以得到数组的两个重要特点:

一是线性结构,顾名思义数据结构呈线状即只有前后两个方向。像数组、链表等都是线性结构,与之相对的就是非线性结构,像树、图等等。

二是连续的内存空间存储相同类型数据。

线性结构和非线性结构 - 副本.png

正是这两个特点,数组支持随机访问,只需要 O(1) 的时间复杂度就可以查询到你想要的数据(当然,前提是你知道这个数据所在的下标)。

但是有利也有弊,一个是数组在插入和删除数据时为了维护数组的连续性,就需要进行大量的数据搬移。如果不需要维护数组内数据的有序性,有一个取巧的方法减少数据搬移的次数。增加数据时,我们只需要将增加的数据和数组的最后一个数据进行交换。删除数据也是一样,将删除的数据和数组最后一个数据进行交换,在将最后一个数据删除即可。另一个是数组的长度是固定的。

数据插入数据 - 副本.png

那么如何实现一个动态扩容的数组呢?

二、扩容思路

例如:在长度为 5 的数组 A[5]={a,b,c,d,e} 的位置 2 也就是 c 所在的位置插入 f ,6 个数据大于了数组的长度,就需要进行扩容操作。

扩容的基本思路就是:创建一个容量更大的新数组,将之前的数组里的数据移到新数组中。

扩容思路 - 副本.png

如图,我们先创建了一个长度为 10 的数组 B[10] ,将数组 A 中的数据移动到数组 B 中,再将数据 c、d、e依次向后移动一位,最后将 f 插入到位置 2 ,就完成了插入扩容的操作。

有了扩容的思路,我们再来看看具体的代码实现。

三、实现代码

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
java复制代码/**
* @author xuls
* @date 2021/11/6 15:44
* 实现支持动态扩容的数组
*/
public class CustomArray <T>{
private int size;
private Object[] data;
​
public CustomArray(){
this(10);
}
​
public CustomArray(int capacity){
data = new Object[capacity];
size = 0;
}
​
//将 t 插入 index 位置
public void insert(int index,T t){
//判断index是否在合理范围
if (index<0 || index>size){
throw new IllegalArgumentException("index 必须 0<= index <= size");
}
//判断是否需要扩容
if (size == data.length){
resetSize(2 * data.length);
}
//数据搬移
for (int i = size -1; i>=index ; i--) {
data[i + 1] = data[i];
}
data[index] = t;
//别忘了++
size++;
}
​
//移除 index 位置的元素
public T remove(int index){
//判断index是否在合理范围
checkIndex(index);
Object removeData = data[index];
//数据搬移
for (int i = index; i <size-1; i++) {
data[i] = data[i+1];
}
size--;
data[size] = null;
//判断是否需要缩容
if (size == data.length / 4 && data.length / 2 !=0){
resetSize(data.length /2);
}
return (T)removeData;
}
​
//获取 index 位置的元素
public T get(int index){
//判断index是否在合理范围
checkIndex(index);
return (T) data[index];
}
​
//将 index 位置的数据更新为 t
private void set(int index ,T t){
//判断移除位置是否正确
checkIndex(index);
data[index]=t;
}
​
//重新设置数组的大小
private void resetSize(int capacity){
Object[] newData = new Object[capacity];
for (int i = 0; i < size; i++) {
newData[i] = data[i];
}
data = newData;
}
​
//检查 index 是否在合理的区间
private void checkIndex(int index){
if (index<0 || index>=size){
throw new IllegalArgumentException("index 必须 0<= index < size");
}
}
​
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("数组 size=").append(size).append(",length = ").append(data.length).append("\n");
builder.append("[");
for (int i = 0; i < size; i++) {
builder.append(data[i]);
if (i != size -1){
builder.append(",");
}
}
builder.append("]");
return builder.toString();
}
}

四、总结

  • 数组是既简单有实用的数据结构,优势是随机访问,劣势是删除增加需要进行大量数据搬移,同时不支持动态扩容。
  • 如果数组存储的数据是无序的话,增加删除时我们只需要将增加删除的数据和数组的最后一个数据进行交换,减少大量的数据搬移。
  • 我们不需要自己实现一个动态扩容的数组,Java 已经提供了动态扩容的 ArrayList,但数组并非无用武之地,因为 ArrayList 无法直接存储基本数据类型,需要进行拆箱和装箱操作,所以对性能要求高的场景数组也可以是你的选择。
  • 看到这了,XDM,点赞评论敷衍一下,求求了。
  • catdan.gif

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…403404405…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%