MVC如何使用开源分页插件shenniu.pager.js
最近比较忙,前期忙公司手机端接口项目,各种开发+调试+发布现在几乎上线无问题了;虽然公司项目忙不过在期间抽空做了两件个人觉得有意义的事情,一者使用aspnetcore开发了个人线上项目(要说线上其实只能ip访问,没有域名哈哈),其架构组成由:aspnetcore1.0.0+redis+ postgressql+TaskMainForm服务,这个项目在后期会开源出来供大家分享学习,站点地址点这里心声网;一者是目前正在做的后台管理框架一叶子,现目前刚好吧js分页插件shenniu.pager.js写完,个人觉得还是可以的,这也是本章将要和大家分享的内容;那么开始今天的分享内容,希望各位多多扫码支持:
. 为什么采用js分页及效果图
. 在view中如何使用及分享个后台方法
. 开发者视角阅读shenniu.pager.js代码
下面一步一个脚印的来分享:
. 为什么采用js分页及效果图
首先,咋们来了解下市面上mvc两种常用的分页方式:跳转分页和ajax分页;跳转分页意思就是页面重定向到指定的页面并通过传递分页需要的参数,从而获取数据后通过Modal来绑定数据,这个每次都会刷下页面体验上不是很好;ajax分页通过异步js请求某个接口,然后从接口获取到数据后,再赋值到展示的界面上,这种方式是不会刷新页面,从而保证了用户体验;
下面来看下这次分享的js分页插件效果图:
图一:
图二:
图三:
看效果图好像看不出来什么东西,我只能说没办法,以后争取弄个gif动态图片吧,后面代码才是关键
. 在view中如何使用及分享个后台方法
首先,为了页面样式好看我使用了bootstrap+ace样式框架,样式效果就是如上面几张图所示(这里是样式和js文件);由于该插件是采用jquery格式书写的所以需要引用jquery.js,如上面图所示使用到了日期选择框,因为我采用的样式都是基于h5的设计所以这里引用的日期选择插件bootstrap-datepicker.min.js和她的样式bootstrap-datepicker3.min.css;该实例需要的引用文件都好了,下面看下截图:
再来,咋们就开始使用shenniu.pager.js,我们需要在点击“查询”按钮的时候去调用这个插件,然后通过插件去获取后台接口返回的数据,并绑定到页面展示出来,所以有了如下代码:
1 var snTool = new shenniuTool();
2
3 $("button[id='btnSearch']").on("click", function () {
4
5 var data = {
6 txtName: $("input[name='txtName']").val(),
7 nStatus: $("select option:selected").val(),
8 dOperateTime: $("input[name='dOperateTime']").val()
9 };
10
11 snTool.listFun({
12 showId: "divShowResult", //内容显示的div的Id
13 url: "/Menu/Search",
14 data: data,
15 pageSize: 2, //每页N条
16 headText: [
17 { nickName: "全选", name: "Id", colType: "checkbox" },
18 { nickName: "名称", name: "Name", colType: "label", isModalHeadText: true },
19 { nickName: "链接", name: "Link" },
20 { nickName: "状态", name: "EnableDes" },
21 { nickName: "操作人", name: "OperatorDes" },
22 { nickName: "操作时间", name: "OperateTime", format: "yyyy-MM-dd" },
23 { nickName: "操作", name: "Id", colType: "operate" }
24 ],
25 editeOption: {
26 url: "/Menu/Edit",
27 title: "编辑",
28 height: 500
29 },
30 viewOption: {
31 url: "/Menu/Details",
32 title: "查看",
33 height: 500
34 },
35 delOption: {
36 url: "/Menu/Delete",
37 title: "删除",
38 height: 500
39 },
40 modalExt: modalExt
41 });
42 });
注意参数url: "/Menu/Search",这个指向的就是后台接口,那么咋们来看下我后台咋们写的:
1 [HttpGet]
2 public JsonResult Search()
3 {
4 var moPageResult = new StageModel.MoPageResult<dynamic>();
5
6 try
7 {
8
9 var txtName = Request.Params["txtName"];
10 var nStatus = string.IsNullOrWhiteSpace(Request.Params["nStatus"]) ? -1 : Convert.ToInt32(Request.Params["nStatus"]);
11 var dOperateTime = string.IsNullOrWhiteSpace(Request.Params["dOperateTime"]) ? Convert.ToDateTime("1991-01-01") : Convert.ToDateTime(Request.Params["dOperateTime"]);
12 var data = db.MoMenus.AsQueryable();
13 if (!string.IsNullOrWhiteSpace(txtName))
14 {
15 data = data.Where(b => b.Name.Contains(txtName));
16 }
17 if (nStatus >= 0)
18 {
19 data = data.Where(b => b.IsEnable == (nStatus == (int)StageEnumHelper.ComStatus.启用));
20 }
21 if (dOperateTime > Convert.ToDateTime("1991-01-01"))
22 {
23 data = data.Where(b => b.OperateTime >= dOperateTime && b.OperateTime < dOperateTime.AddDays(1));
24 }
25
26 moPageResult.MoPageContent(
27 data,
28 b => b.OperateTime,
29 b => new
30 {
31 Id = b.Id,
32 Name = b.Name,
33 Link = b.Link,
34 Des = b.Des,
35 IsEnable = b.IsEnable,
36 Operator = b.Operator,
37
38 OperatorDes = b.MoUserInfo.NickName,
39 EnableDes = b.IsEnable ? "启用" : "禁用",
40 OperateTime = b.OperateTime
41 });
42
43 }
44 catch (Exception ex)
45 {
46 }
47 return Json(moPageResult, JsonRequestBehavior.AllowGet);
48 }
后台接口Request.Params获取的几个参数就是从前端
var data = {
txtName: $("input[name='txtName']").val(),
nStatus: $("select option:selected").val(),
dOperateTime: $("input[name='dOperateTime']").val()
};
传递过来的,分别代码了视图中的名称,状态,操作时间等查询条件;下面来看下,后台这儿没有看到获取类似分页的当前页数和分页记录数的操作,是封装到了MoPageResult类中的MoPageContent()中,来看下MoPageResult类代码如:
1 #region 分页数据返回
2
3 public class MoPageResult<TResult> where TResult : class, new()
4 {
5
6 public MoPageResult()
7 {
8
9 }
10
11 public IQueryable<TResult> MoResult;
12
13 /// <summary>
14 /// 总页数
15 /// </summary>
16 public int PageTotal { get; set; }
17
18
19 /// <summary>
20 /// 当前页数
21 /// </summary>
22 public int CurrentPage { get; set; }
23
24 /// <summary>
25 /// 分页记录数
26 /// </summary>
27 public int PageSize { get; set; }
28
29 /// <summary>
30 /// 分页方法
31 /// </summary>
32 /// <typeparam name="TKey"></typeparam>
33 /// <param name="query"></param>
34 /// <param name="order_desc"></param>
35 public void MoPageContent<T, TKey>(IQueryable<T> query, Expression<Func<T, TKey>> desc, Expression<Func<T, TResult>> selector = null, bool isDesc = true) where T : class,new()
36 {
37
38 if (HttpContext.Current == null) { return; }
39 var Request = HttpContext.Current.Request;
40
41 this.PageSize = string.IsNullOrWhiteSpace(Request.Params["pageSize"]) ? 15 : Convert.ToInt32(Request.Params["pageSize"]);
42 this.CurrentPage = string.IsNullOrWhiteSpace(Request.Params["currentPage"]) ? 1 : Convert.ToInt32(Request.Params["currentPage"]);
43
44 var nTotal = query.Count();
45 this.PageTotal = nTotal / this.PageSize + (nTotal % this.PageSize > 0 ? 1 : 0);
46
47 if (selector != null)
48 {
49 if (isDesc)
50 {
51 query = query.OrderByDescending(desc);
52 }
53 else
54 {
55 query = query.OrderBy(desc);
56 }
57 this.MoResult = query.
58 Skip((this.CurrentPage - 1) * this.PageSize).
59 Take(this.PageSize).
60 Select(selector);
61 }
62 }
63
64 }
65
66 #endregion
MoPageContent()中默认是获取了pagesize,currentpage参数,这样减少了用户操作性,并且此方法承担了计算总页数和执行分页语句的角色,注意最后查询语句Select(selector),selector是Expression<Func<T, TResult>>类型,这个T有条件约束where T : class,new();我在调用该分页类的使用传递的T是dynamic,因为赖人如我觉得匿名类更方便;唯一遗憾的是select输出暂时无法直接对某个属性直接使用方法;
最后,插件使用还需要注意一个地方,就是时间,如果后台不处理时间直接DateTime的json格式化,那么在插件获取出来的时间格式如:
这个时候就需要在使用shenniu.pager.js插件时候在属性headText中,指定时间列的格式如:
{ nickName: "操作时间", name: "OperateTime", format: "yyyy-MM-dd" }
使用format格式化时间格式,这个插件兼容的给有:yyyy,MM,dd,HH,mm,ss,相信满足大家需要了;
. 开发者视角阅读shenniu.pager.js代码
首先,我们从上而下,映入眼帘的是插件属性:
var defOption = {
showId: "divShowResult", //内容显示的div
url: "", //ajax数据来源地址
headText: [
{ nickName: "A", colType: "checkbox", name: "Id" },
{ nickName: "B", colType: "label", name: "Name", isModalHeadText: true } //isModalHeadText:是否是模式窗体头部显示的信息
],
data: {}, //查询条件
editeOption: {
url: "",
title: "编辑",
width: 500,
height: 500
}, //编辑地址,不包括id
viewOption: {
url: "",
title: "查看",
width: 500,
height: 500
}, //查看地址
delOption: {
url: "",
title: "删除",
width: 500,
height: 500
}, //删除地址
currentPage: 1, //当前页数
pageSize: 15, //分页记录数
showPageTab: 6, //展示6个页数
modalExt: null, //模式窗体对象
//可忽略
callback: function () { }, //回调函数
tabId: "tab001",
loading: "努力加载中,等会吧...", //可以直接写出<img src=''/>
sucFun: function (data) { },
befFun: function () { },
errFun: function () { },
comFun: function () { },
timeout: 60000 //超时60S
};
$.extend(defOption, option);
View Code
里面已经包括了注释说明,看起来应该不是问题; $.extend(defOption, option); 这段代码意思是吧用户传递进来的参数和插件里面默认的参数合并,用户大于插件直接可以覆盖相同属性的值;
再来,看请求后台的方法:
1 //请求后台
2 function ajaxFun(option) {
3
4 if (option) {
5 $.extend(defOption, option);
6 }
7
8 //获取分页参数
9 var hidPageSize = defOption.pageSize;
10 var hidCurrentPage = defOption.currentPage;
11
12 if ($("form input[name='pageSize']").val()) {
13 hidPageSize = $("form input[name='pageSize']").val();
14 }
15 if ($("form input[name='currentPage']").val()) {
16 hidCurrentPage = $("form input[name='currentPage']").val();
17 }
18
19 //合并用户查询条件和分页参数条件
20 var searchData = {
21 pageSize: hidPageSize,
22 currentPage: hidCurrentPage
23 };
24 $.extend(searchData, defOption.data);
25 //请求后台数据
26 $.ajax({
27
28 url: defOption.url,
29 type: "get",
30 data: searchData,
31 dataType: "json",
32 timeout: defOption.timeout,
33
34 async: true,
35 beforeSend: defOption.befFun,
36 success: defOption.sucFun,
37 });
38 }
这个方法就是请求接口获取数据的方法,里面默认获取了页面中的pageSize,currentPage两个分页所需要的参数,这里采用的是get方式来请求,当然可以写成post,不过需要后台支持post就行了;
我们再看查询列表方法:
1 //查询列表
2 listFun: function (option) {
3
4 if (option) {
5 $.extend(defOption, option);
6 }
7
8 //默认格式
9 var tab = [];
10 tab.push('<table id="' + defOption.tabId + '" class="table table-bordered table-hover">');
11 tab.push('<thead><tr role="row">');
12
13 for (var i in defOption.headText) {
14
15 var head = defOption.headText[i];
16
17 if (head.colType == "label") {
18 tab.push('<th class="center" tabindex="0" rowspan="1" colspan="1">' + head.nickName + '</th>');
19 } else if (head.colType == "checkbox") {
20 tab.push('<th class="center " rowspan="1" colspan="1" aria-label="">');
21 tab.push(' <label class="pos-rel">');
22 tab.push(' <input type="checkbox" name="cbAll" class="ace">');
23 tab.push(' <span class="lbl">' + head.nickName + '</span>');
24 tab.push(' </label>');
25 tab.push('</th>');
26 } else {
27 tab.push('<th class="center" tabindex="0" rowspan="1" colspan="1">' + head.nickName + '</th>');
28 }
29 }
30 tab.push('</tr></thead>');
31 tab.push('<tbody><tr><td class="text-center" colspan="' + defOption.headText.length + '">' + defOption.loading + '</td></tr></tbody>');
32 tab.push('</table>');
33 tab.push('<div id="divPager" class="text-center"></div>');
34 $("#" + defOption.showId).html(tab.join(''));
35 //全选事件
36 $("input[type='checkbox'][name='cbAll']").on("click", function () {
37
38 var cbStatus = $(this).is(":checked");
39 if (cbStatus) {
40 $("input[name='cb']:checkbox").prop("checked", true);
41 } else {
42 $("input[name='cb']:checkbox").prop("checked", false);
43 }
44 });
45
46 //数据返回成功处理
47 defOption.sucFun = function (data) {
48
49 var head = $("table[id='" + defOption.tabId + "'] tbody");
50 if (data) {
51 if (data.MoResult) {
52 //遍历table展示的数据
53 var rows = [];
54 $.each(data.MoResult, function (i, item) {
55 rows.push('<tr>');
56
57 var modalHeadText = "";
58 for (var h_i in defOption.headText) {
59 var head = defOption.headText[h_i];
60 var item_val = item[head.name];
61 if (item_val && typeof (item_val) != "undefined") { } else { item_val = ""; }
62
63 //时间格式化
64 if (head.format && item_val.length > 0) {
65 console.log(item_val);
66 var date = new Date(parseInt(item_val.replace("/Date(", "").replace(")/", ""), 10));
67 item_val = head.format.
68 replace("yyyy", date.getFullYear()).
69 replace("MM", date.getMonth() + 1).
70 replace("dd", date.getDate()).
71 replace("HH", date.getHours()).
72 replace("mm", date.getMinutes()).
73 replace("ss", date.getMilliseconds());
74 }
75
76 //获取模式窗体头部信息
77 if (modalHeadText.length <= 0) { modalHeadText = head.isModalHeadText ? item_val : "" };
78 if (head.colType == "label") {
79
80
81 rows.push('<td class="center">' + item_val + '</td>');
82 } else if (head.colType == "checkbox") {
83 rows.push('<td class="center">');
84 rows.push(' <label class="pos-rel">');
85 rows.push(' <input type="checkbox" name="cb" value="' + item_val + '" class="ace">');
86 rows.push(' <span class="lbl"></span>');
87 rows.push(' </label>');
88 rows.push('</td>');
89 } else if (head.colType == "operate") {
90
91 rows.push('<td class="center"><div class="hidden-sm hidden-xs action-buttons">');
92 if (defOption.editeOption.url.length > 0) {
93 var editOption = $.extend({}, defOption.editeOption);
94 editOption.url += "/" + item_val;
95 editOption.title += modalHeadText.length > 0 ? "-" + modalHeadText : "";
96
97 var op = JSON.stringify(editOption);
98 rows.push('<a class="blue" data-item=\'' + op + '\' href="javascript:;"><i class="ace-icon fa fa-pencil bigger-130"></i></a>');
99 }
100 if (defOption.viewOption.url.length > 0) {
101
102 var viewOption = $.extend({}, defOption.viewOption);
103 viewOption.url += "/" + item_val;
104 viewOption.title += modalHeadText.length > 0 ? "-" + modalHeadText : "";
105
106 var op = JSON.stringify(viewOption);
107 rows.push('<a class="blue" data-item=\'' + op + '\' href="javascript:;"><i class="ace-icon fa fa-search-plus bigger-130"></i></a>');
108 }
109 if (defOption.delOption.url.length > 0) {
110
111 var delOption = $.extend({}, defOption.delOption);
112 delOption.url += "/" + item_val;
113 delOption.title += modalHeadText.length > 0 ? "-" + modalHeadText : "";
114
115 var op = JSON.stringify(delOption);
116 rows.push('<a class="blue" data-item=\'' + op + '\' href="javascript:;"><i class="ace-icon fa fa-trash-o bigger-130"></i></a>');
117 }
118 rows.push('</div></td>');
119 }
120 else {
121
122 rows.push('<td class="center">' + item_val + '</td>');
123 }
124 }
125 rows.push('</tr>');
126 });
127
128 //页数展示
129 if (data.MoResult.length > 0) {
130 var pager = [];
131 pager.push('<div class="text-center">');
132 pager.push(' <ul class="pagination" style="margin-top:0px">');
133 var nPager = defOption.showPageTab;//每次展示6个分页
134 //上一页
135 var nprev = (data.CurrentPage - 1 >= 1 ? data.CurrentPage - 1 : 1);
136 pager.push(' <li class="paginate_button previous" aria-controls="dynamic-table" tabindex="0" id="dynamic-table_previous">');
137 pager.push(' <a href="javascript:;" name="npager" data-page="' + nprev + '">上一页</a>');
138 pager.push(' </li>');
139
140 //当前页之前页码
141 var preTotal = data.CurrentPage - nPager >= 1 ? data.CurrentPage - nPager : 1;
142 for (var i = preTotal; i < data.CurrentPage ; i++) {
143
144 pager.push(' <li class="paginate_button ' + (i == data.CurrentPage ? "active disabled" : "") + '" aria-controls="dynamic-table">');
145 pager.push(' <a href="javascript:;" name="npager" data-page="' + i + '">' + i + '</a>');
146 pager.push(' </li>');
147 }
148
149 //当前页
150 pager.push(' <li class="paginate_button active disabled" aria-controls="dynamic-table">');
151 pager.push(' <a href="javascript:;" name="npager" data-page="' + data.CurrentPage + '">' + data.CurrentPage + '</a>');
152 pager.push(' </li>');
153
154 //当前页以后页码
155 var nextTotal = data.CurrentPage + nPager > data.PageTotal ? data.PageTotal : data.CurrentPage + nPager;
156 for (var i = data.CurrentPage + 1; i <= nextTotal; i++) {
157
158 pager.push(' <li class="paginate_button ' + (i == data.CurrentPage ? "active disabled" : "") + '" aria-controls="dynamic-table">');
159 pager.push(' <a href="javascript:;" name="npager" data-page="' + i + '">' + i + '</a>');
160 pager.push(' </li>');
161 }
162 //下一页
163 var nnext = (data.PageTotal < data.CurrentPage + 1 ? data.PageTotal : data.CurrentPage + 1);
164 pager.push(' <li class="paginate_button next" aria-controls="dynamic-table" tabindex="0" id="dynamic-table_next">');
165 pager.push(' <a href="javascript:;" name="npager" data-page="' + nnext + '">下一页</a>');
166 pager.push(' </li>');
167
168 pager.push(' </ul>');
169 //分页查询条件
170 pager.push('<div style="display:none">');
171 pager.push(' <form>');
172 pager.push(' <input type="hidden" name="pageSize" value="' + defOption.pageSize + '"/>');
173 pager.push(' <input type="hidden" name="currentPage" value="' + defOption.currentPage + '"/>');
174 pager.push(' </form>');
175 pager.push('</div>');
176
177 pager.push('</div>');
178
179 //移除加载中
180 //head.html("");
181 //添加结果
182 head.html(rows.join(''));
183 $("div[id='divPager']").html(pager.join(''));
184 //操作按钮事件
185 $("a[data-item]").on("click", function () {
186
187 var data_Item = $(this).attr("data-item");
188 if (data_Item) {
189 var data_Item_Obj = JSON.parse(data_Item);
190
191 defOption.modalExt.modalFun({
192 width: data_Item_Obj.width,
193 height: data_Item_Obj.height,
194 url: data_Item_Obj.url,
195 title: data_Item_Obj.title,
196 callback: defOption.callback
197 });
198 }
199 });
200 //绑定分页按钮事件
201 $("a[name='npager']").on("click", function () {
202
203 var nPager = $(this).attr("data-page");
204 if (nPager.length <= 0) { return; }
205
206 $("form input[name='currentPage']").val(nPager);
207
208 //执行请求后台
209 ajaxFun(defOption);
210 });
211 } else {
212 head.html("<tr><td class=\"text-center\" colspan=\"" + defOption.headText.length + "\">未能查询到数据!</td></tr>");
213 $("div[id='divPager']").html("");
214 }
215 } else {
216 head.html("<tr><td class=\"text-center\" colspan=\"" + defOption.headText.length + "\">未能查询到数据。</td></tr>");
217 $("div[id='divPager']").html("");
218 }
219 } else {
220 head.html("<tr><td class=\"text-center\" colspan=\"" + defOption.headText.length + "\">未能查询到数据</td></tr>");
221 $("div[id='divPager']").html("");
222 }
223 };
224 if (option) {
225 $.extend(defOption, option);
226 }
227
228 //执行请求后台
229 ajaxFun(defOption);
230 }
View Code
这个方法体挺长的,主要操作是:
默认格式展示列表头部并呈现出加载中的提示=》绑定复选框全选事件=》创建数据返回成功函数sucFun()=》调用请求后台方法ajaxFun();
再来看函数sucFun()等到数据返回后执行的操作是:
遍历json返回数据展示到table中(其中包括了时间格式化的处理,复选框,label及操作按钮类型operate的初始化)=》页数展示及事件绑定(目前只有上一页,当前页之前页码,当前页,当前页以后页码,下一页的效果展示,分页查询条件(生成pagesize和currentPage隐藏控件),绑定分页按钮事件)
以上就是shenniu.pager.js整个插件内容,不多且清晰,感觉分享给大家挺高兴,下面贴出示例中用到的js文件和css文件可以在这里下载
git地址:
https://github.com/shenniubuxing3 nuget发布包:https://www.nuget.org/profiles/shenniubuxing3
蛋疼的Apple IOS Push通知协议
简介
Apple Push通知机制其实很简单,就是Apple的APNs服务器做为中间人,把消息推送到对应的设备上。
一张来自Apple文档的图:
当然,示意图看起来简单,但是还有一些实际的问题。
比如,如何区分Provicer的?如何区分设备的?
简单而言,是这样的:
每个应用都有一个自己的证书(certificate),开发者可以从苹果那里获得;
应用可以到APNs服务器上注册(register),然后得到一个device_token,开发者要自己保存好,推送时就要用这个来区分不同的设备。
注意,token并不是设备唯一标识码。token是可以改变的,因此APNs提供了一个feedback服务,开发者可以得到失效的token。
对于每个设备只存储最后的一条push,所以如果连续发很多条push,设备没有及时接收的话,后面的push会覆盖前面的。
Provider和APNs服务器,APNs服务器和用户设备之间的通迅都是SSL/TLS协议的。这点要比国内的推送服务商要做的好,国内的推送服务都是http接口的,完全没有加密。
Push都是对于设备而言的。所以敏感信息不要通过push来传递。
Push是尽量送达的,Push消息可能会丢失,所以不要用Push来传递可靠数据。
Apple Push的协议蛋疼之处
上面只是有一些要注意的小地方,下面来说下真正蛋疼的东西:Apple Push的协议。
首先,明显的是设计不良:
协议里的command,现在实际上也用来表示Version。
比如command = 0, 1, 2就分别表示发送消息的三个版本。
更蛋疼的是前面0,1两个版本现在的文档都找不到了。这个让维护老代码的人情何以堪?想调试下bug,结果发现官方文档都消失了,那得多蛋疼。
Apple的文档里只说到:
Field name
Length
Discussion
Command
1 byte
Populate with the number 2.
可能在你看这篇文章的时候,变成了“Populate with the number 3”了。。
正常人看到这里只会觉得一头雾水,为什么这个command是2?怎么想得到这个Command居然是和版本相关的。。我是从别的一些实现代码里才知道有三个版本的。
协议格式混乱
比如这个feedback的格式:
deviceToken就是固定好了是32字节的,前面还要加一个Token length。
有人可能说,这是考虑了以后token大于32字节的。那干脆应该为feedback的回应包加上版本号。
发送者可以批量发送消息,但是只有出错的时候才会返回出错消息的ID。
这个乍看起来,没什么问题。但是当你想要实现一个Push客户端的时候,就知道蛋疼之处了:
发送者连续发了1,2,3,4 ... 100 条消息,已经写到socket里去了,这里APNs服务器回应说第57条消息失败了。
那发送者得从第58条开始,重新再发。那发送者得把前面已经发送出去了的消息缓存起来!
好的,缓存一下也没关系,那么到底缓存多少个消息呢?1024个?2048个?天知道。
要是发送者的网速快,一下子把4096个消息都发出去了呢?那怎么办?
好吧,也许你会说4096个消息体比较大小,Apple的服务器的TCP socket缓冲区都满了,你发不了这么多的消息。
我只是想发个Push消息而已,难道还要推算APNs服务器的socket缓冲区的大小?要是它的网络框架也做了缓冲呢?要是发送者的网络框架也做了缓冲呢?
是不是每次发送时,都要等待数据全都写到socket里去了?
开发者测试用的沙箱服务器是个摆设
文档上写了在开发环境可以用这个域名,gateway.sandbox.push.apple.com。但是坑爹的是,这个域名实际上是个摆设,你可以建立SSL连接,也可以正常发送消息。
但是你的设备却是收不到消息的。要是在实现自己的客户端时,用这个来测试,就蛋疼了,一次次检查自己的代码,看是否有问题,最终却发现是对方的服务器有问题。。
只有程序员才能明白这种蛋疼的心情。
不支持分组发送
现在的国内流行的推送服务,如百度推送等,都支持分组的配置,这样大大节省了带宽,提离了发送效率。
可能还有一些蛋疼的地方,忘记了,下篇blog说下一些实现自己的客户端要注意的事项及一个Java的实现ZPush。
参考:
https://developer.apple.com/library/ios/technotes/tn2265/_index.html 这个文档很值得参考,说到了一些实现的要点。
http://support.apple.com/kb/HT3576?viewlocale=zh_CN&locale=zh_CN
https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/ApplePushService.html#//apple_ref/doc/uid/TP40008194-CH100-SW12
https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/CommunicatingWIthAPS.html
https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW1
浅谈国内域名注册商与国外域名注册商的区别与优势
不管我们在国内还是国外注册域名,肯定有不同的优势与需求,可能老董太过于推荐说GODADDY的优势没有提到其他的国外、国内注册商有些朋友可能获取知识比较局限,认为老董有什么目的性。我在很多文章中都有强调,GODADDY是我比较喜欢的域名注册商,我也有其他的网站,这个博客仅仅是用来分享GODADDY相关的优惠和域名信息。
国外域名注册商的优点:
一般我们选择国外注册商可能较多的以GODADDY、NAME.COM、NAMECHEAP、ENOM、NAMESILO等商家为主,由于用户的使用量以及当初优惠的推广,以及支持支付宝付款,很多人比较多的选择GODADDY商家(应该没错吧),然后如果我们有信用卡或者PAYPAL付款,可以选择其他的海外商家。对于价格,目前基本上差不多。
海外域名注册商的管理是比较自由(方便),比如我们需要过户到其他商家,或者是账户之间的PUSH,直接就可以操作,当然这些在国内的大部分商家中也是可以的,但是有些商家还是会卡住我们的
国外的域名相对来说比较安全,比如个人信息之类的按照ICANN的规定必须真实,但是我们可以模拟真实,不一定要全部真实。如果我们比较重要的域名一定要真实信息,这样即便有问题也不会被限制。
国内的域名如果你网站有某种问题,会有被限制的可能,这个事情在国内已经发生了很多次。
国内域名注册商的特点:
从早期的新网互联、新网、厦门书生等商家,现在已经有很多商家的出现,包括比较大平台的比如爱名、易名、万网、西数等平台,而且国内的平台也趋于稳定和开放,以前商家都限制我们转出或者续费较贵的,现在我们也可以选择转出转入或者是直接过户到其他商家。所以我们也看到很多域名在国内商家注册的也比较多。
对于国内的注册商来说,毕竟我们是使用中文比较熟悉,中文站点我们选择应该比较多,而且大部分支持支付宝付款,如果我们内容比较正规,可以选择国内的商家,如果是有外贸类需求之类的或者是有些问题内容的站点,还是选择海外。
总之,国内、国外商家各有优势,我们看各自的喜欢和需要。
本文转自博客园知识天地的博客,原文链接:浅谈国内域名注册商与国外域名注册商的区别与优势,如需转载请自行联系原博主。
搭建Docker私有仓库--自签名方式
为了能集中管理我们创建好的镜像,方便部署服务,我们会创建私有的Docker仓库。通读了一遍官方文档,Docker为了确保安全使用TLS,需要CA认证,认证时间长的要钱啊,免费过期时间太短,还是用自签名比较简单。
准备环境
环境:两台Centos 7 虚拟机
》服务器IP:10.57.220.244 ,作为Docker仓库使用
》客户端IP:10.57.220.220 ,作为客户端来上传或拉取镜像
》域名:lpxxn.com
两台机器上均已安装好Docker 版本为 17.03.0-ce
如果你和我一样没有用真域名,只需要在客户机上修改一下hosts文件
生成自签名证书
在服务器主机上生成自签名证书,创建一个文件夹用于存放证书
mkdir -p certs
生成证书
openssl req -newkey rsa:4096 -nodes -sha256 -keyout certs/lpxxn.com.key -x509 -days 365 -out certs/lpxxn.com.crt
需要注意的是在填写的时候Common Name和你的域名是一至的。
ll certs文件夹就可以看到生成好的两个文件
运行仓库镜像,如果本地没有相应的镜像会从Docker服务器上下载,然后才启动,可以用docker ps命令查看是否已经有窗口在运行。
docker run -d -p 5000:5000 --restart=always --name registry_https -v `pwd`/certs:/home/certs -e REGISTRY_HTTP_TLS_CERTIFICATE=/home/certs/lpxxn.com.crt -e REGISTRY_HTTP_TLS_KEY=/home/certs/lpxxn.com.key registry:2
你也可以指定本机的目录保存上传好的docker镜像
docker run -d -p 5000:5000 -v `pwd`/dockerregister:/var/lib/registry --restart=always --name registry_https -v `pwd`/certs:/home/certs -e REGISTRY_HTTP_TLS_CERTIFICATE=/home/certs/lpxxn.com.crt -e REGISTRY_HTTP_TLS_KEY=/home/certs/lpxxn.com.key registry:2
到这里服务器就启动好了,最后一步是把生成好的lpxxn.com.crt复制到客户端,你可以用自己的方式复制, 我用的scp先复制到/home/test目录下,再复制到/etc/pki/ca-trust/source/anchors目录下
scp -r lpxxn.com.crt li@10.57.220.220:/home/test
配置客户端
把服务器端生成的的lpxxn.com.crt复制到客户端服务器的 /etc/pki/ca-trust/source/anchors目录下,ll查看 一下
更新证书,然后重新启动docker 。
update-ca-trust
service docker stop && service docker start
ok.使用curl查看一下仓库
curl https://lpxxn.com:5000/v2/_catalog
可以正常访问了。再使用docker命令上传下载试试
使用docker tag 标记本地的镜像centos:6为 lpxxn.com:5000/centos6:1.0
push到仓库
执行push 命令
docker push lpxxn.com:5000/centos6:1.0
查看仓库信息
使用curl 查看仓库有哪些镜像和版本
curl https://lpxxn.com:5000/v2/_catalog
curl https://lpxxn.com:5000/v2/centos6/tags/list
从仓库拉取镜像
先把本地的镜像删除
docker rmi lpxxn.com:5000/centos6:1.0
docker rmi centos:6
拉取然后run
docker pull lpxxn.com:5000/centos6:1.0
本文转自lpxxn博客园博客,原文链接:http://www.cnblogs.com/li-peng/p/6511331.html,如需转载请自行联系原作者
搭建Docker私有仓库--自签名方式
为了能集中管理我们创建好的镜像,方便部署服务,我们会创建私有的Docker仓库。通读了一遍官方文档,Docker为了确保安全使用TLS,需要CA认证,认证时间长的要钱啊,免费过期时间太短,还是用自签名比较简单。
准备环境
环境:两台Centos 7 虚拟机
》服务器IP:10.57.220.244 ,作为Docker仓库使用
》客户端IP:10.57.220.220 ,作为客户端来上传或拉取镜像
》域名:lpxxn.com
两台机器上均已安装好Docker 版本为 17.03.0-ce
如果你和我一样没有用真域名,只需要在客户机上修改一下hosts文件
生成自签名证书
在服务器主机上生成自签名证书,创建一个文件夹用于存放证书
mkdir -p certs
生成证书
openssl req -newkey rsa:4096 -nodes -sha256 -keyout certs/lpxxn.com.key -x509 -days 365 -out certs/lpxxn.com.crt
需要注意的是在填写的时候Common Name和你的域名是一至的。
ll certs文件夹就可以看到生成好的两个文件
运行仓库镜像,如果本地没有相应的镜像会从Docker服务器上下载,然后才启动,可以用docker ps命令查看是否已经有窗口在运行。
docker run -d -p 5000:5000 --restart=always --name registry_https -v `pwd`/certs:/home/certs -e REGISTRY_HTTP_TLS_CERTIFICATE=/home/certs/lpxxn.com.crt -e REGISTRY_HTTP_TLS_KEY=/home/certs/lpxxn.com.key registry:2
你也可以指定本机的目录保存上传好的docker镜像
docker run -d -p 5000:5000 -v `pwd`/dockerregister:/var/lib/registry --restart=always --name registry_https -v `pwd`/certs:/home/certs -e REGISTRY_HTTP_TLS_CERTIFICATE=/home/certs/lpxxn.com.crt -e REGISTRY_HTTP_TLS_KEY=/home/certs/lpxxn.com.key registry:2
到这里服务器就启动好了,最后一步是把生成好的lpxxn.com.crt复制到客户端,你可以用自己的方式复制, 我用的scp先复制到/home/test目录下,再复制到/etc/pki/ca-trust/source/anchors目录下
scp -r lpxxn.com.crt li@10.57.220.220:/home/test
配置客户端
把服务器端生成的的lpxxn.com.crt复制到客户端服务器的 /etc/pki/ca-trust/source/anchors目录下,ll查看 一下
更新证书,然后重新启动docker 。
update-ca-trust
service docker stop && service docker start
ok.使用curl查看一下仓库
curl https://lpxxn.com:5000/v2/_catalog
可以正常访问了。再使用docker命令上传下载试试
使用docker tag 标记本地的镜像centos:6为 lpxxn.com:5000/centos6:1.0
push到仓库
执行push 命令
docker push lpxxn.com:5000/centos6:1.0
查看仓库信息
使用curl 查看仓库有哪些镜像和版本
curl https://lpxxn.com:5000/v2/_catalog
curl https://lpxxn.com:5000/v2/centos6/tags/list
从仓库拉取镜像
先把本地的镜像删除
docker rmi lpxxn.com:5000/centos6:1.0
docker rmi centos:6
拉取然后run
docker pull lpxxn.com:5000/centos6:1.0
搭建个私有docker镜像仓库
公有仓库和私有仓库:
速度:公有仓库走的公网,速度较慢;私有仓库走的是内网,即局域网;
安全性:公有仓库存放在公共硬盘上;私有仓库存在自己服务器硬盘上。
公有仓:
最权威的,但速度比较慢:
https://hub.docker.com/
首先登陆:
$ docker login -usmallsoup
Password:
WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
Login Succeeded打标签,push镜像到hub仓库:
docker tag zookeeper:3.5 smallsoup/zookeeper:3.5
docker push smallsoup/zookeeper:3.5
已push成功,可以在hub上看到:
私有仓:
用docker提供的registry在本地搭建私有仓:
docker pull registry:2.5.2
docker run -d -p 5000:5000 registry:2.5.2
docker tag zookeeper:3.5 localhost:5000/zookeeper:3.5
docker push zookeeper:3.5 localhost:5000/zookeeper:3.5
因没有设置安全性,所以直接可以push上去。
由于是本地仓库,所以pull的速度很快。
[root@localhost micro-service]# docker pull localhost:5000/zookeeper:3.5
3.5: Pulling from zookeeper
Digest: sha256:3474ec46da9db9dc27a431f9645a2df9c91d5b969f591fe0ccd4c40f2bfd1579
Status: Image is up to date for localhost:5000/zookeeper:3.5
但是这个私有仓不能满足我们的需求,生产线上万一该私有仓服务器故障,其他服务器也无法接管。再者,也没有页面可以便于管理。
业内出现的harbor,主要提供 Dcoker Registry 管理UI,可基于角色访问控制, AD/LDAP 集成,日志审核等功能,完全的支持中文,非常适用于生产环境。
harbor私有仓库搭建
github地址:
https://github.com/goharbor/harbor/releases
下载地址:
https://storage.googleapis.com/harbor-releases/harbor-offline-installer-v1.5.3.tgz
这个链接速度太慢,可以在这里下载:
http://harbor.orientsoft.cn/
以下使用的harbor版本是harbor-offline-installer-v1.5.0.tgz
首先解压:
tar -zxf harbor-offline-installer-v1.5.0.tgz然后运行./install脚本进行安装,如果需要特殊设置,可以先修改harbor.cfg和docker-compose.yml后在进行./install安装操作
[Step 4]: starting Harbor ...
Creating network "harbor_harbor" with the default driver
Creating harbor-log ... done
Creating harbor-adminserver ...
Creating redis ... error
Creating harbor-db ...
Creating registry ...
Creating harbor-adminserver ... done
ERROR: for redis Cannot create container for service redis: b'Conflict. The container name "/redis" is already in use
Creating harbor-db ... done
Creating registry ... done
Creating harbor-ui ... done
Creating nginx ... done
ERROR: for redis Cannot create container for service redis: b'Conflict. The container name "/redis" is already in use by container "c3813d66ccad284d3529227fabf3d5c19cb991237de8d3e72fc470ffd2cbfa99". You have to remove (or rename) that container to be able to reuse that name.'
ERROR: Encountered errors while bringing up the project.
安装过程中报以上错误,是因为服务器上已经有了名为redis的容器名,和harbor将要安装的redis容器名重名,需要rename服务器上已有的redis容器名为micro-service-redis:
$ docker ps -a --filter name=redis
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c3813d66ccad hub.c.163.com/public/redis:2.8.4 "/run.sh" 2 days ago Up 42 hours 0.0.0.0:6379->6379/tcp redis
$ docker rename redis micro-service-redis
$ docker ps -aq --filter name=redis
c3813d66ccad
$ docker ps -a --filter name=redis
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c3813d66ccad hub.c.163.com/public/redis:2.8.4 "/run.sh" 2 days ago Up 42 hours 0.0.0.0:6379->6379/tcp micro-service-redis然后重新执行./install
[Step 4]: starting Harbor ...
Creating network "harbor_harbor" with the default driver
Creating harbor-log ... done
Creating redis ... done
Creating harbor-db ... done
Creating harbor-adminserver ... done
Creating registry ... done
Creating harbor-ui ... done
Creating harbor-jobservice ...
Creating nginx ...
ERROR: for harbor-jobservice UnixHTTPConnectionPool(host='localhost', port=None): Read timed out. (read timeout=60)
ERROR: for nginx UnixHTTPConnectionPool(host='localhost', port=None): Read timed out. (read timeout=60)
ERROR: for jobservice UnixHTTPConnectionPool(host='localhost', port=None): Read timed out. (read timeout=60)
ERROR: for proxy UnixHTTPConnectionPool(host='localhost', port=None): Read timed out. (read timeout=60)
ERROR: An HTTP request took too long to complete. Retry with --verbose to obtain debug information.
If you encounter this issue regularly because of slow network conditions, consider setting COMPOSE_HTTP_TIMEOUT to a higher value (current value: 60).
又报以上的错,可能是由于网络问题,导致失败,重新./install试试:
[Step 4]: starting Harbor ...
Creating network "harbor_harbor" with the default driver
Creating harbor-log ... done
Creating redis ... done
Creating harbor-db ... done
Creating harbor-adminserver ... done
Creating registry ... done
Creating harbor-ui ... done
Creating nginx ... done
Creating harbor-jobservice ... done
----Harbor has been installed and started successfully.----
Now you should be able to visit the admin portal at http://hub.smallsoup.com.
For more details, please visit https://github.com/vmware/harbor .
成功了。
可以访问harbor部署服务器IP:docker-compose.yml中80映射到宿主机上的端口;
用户名是admin,密码是harbor.cfg中harbor_admin_password的值访问管理页面:
可以创建一个私有仓库micro-service:
在系统管理->用户管理中添加用户,然后点开上一步创建的项目-->>成员-->>新建成员,并设置权限。
项目管理员:有pull和push以及项目其他管理权限;
开发人员:有pull和push权限;
访客:只有pull权限。
将该项目的各个微服务image push到harbor的micro-service项目里:
$ docker images |grep -v "vmware"
REPOSITORY TAG IMAGE ID CREATED SIZE
api-gateway-zuul latest 8a814cf9bb65 23 hours ago 476MB
course-service latest 673d4501353e 23 hours ago 462MB
course-edge-service latest 854d5d8bddaa 23 hours ago 484MB
message-thrift-python-service latest 4317a76b387e 24 hours ago 926MB
user-edge-service latest ff07d54a02ba 25 hours ago 469MB
user-thrift-service latest 02dd6fd0f239 26 hours ago 456MB
python-base latest 81ad8926a9d9 26 hours ago 926MB
zookeeper 3.5 c41e1dcd86e4 2 weeks ago 128MB
smallsoup/zookeeper 3.5 c41e1dcd86e4 2 weeks ago 128MB
localhost:5000/zookeeper 3.5 c41e1dcd86e4 2 weeks ago 128MB
elasticsearch latest 5acf0e8da90b 2 weeks ago 486MB
registry 2.5.2 96ca477b7e56 3 weeks ago 37.8MB
registry 2 2e2f252f3c88 3 weeks ago 33.3MB
python 3.6 4f13b7f2138e 4 weeks ago 918MB
openjdk 8-jre 66bf39162ea7 4 weeks ago 443MB
mysql latest 6a834f03bd02 4 weeks ago 484MB
hub.c.163.com/public/redis 2.8.4 4888527e1254 2 years ago 190MB
打标签:
docker tag openjdk:8-jre 192.168.1.103:80/micro-service/openjdk:8-jre查看镜像:
$ docker images |grep -v "vmware" | grep open
openjdk 7-jre e4c851ec3393 4 weeks ago 329MB
192.168.1.103:80/micro-service/openjdk 8-jre 66bf39162ea7 4 weeks ago 443MB
openjdk 8-jre 66bf39162ea7 4 weeks ago 443MBpush镜像:
$ docker push 192.168.1.103:80/micro-service/openjdk:8-jre
The push refers to repository [192.168.1.103:80/micro-service/openjdk]
Get https://192.168.1.103:80/v2/: http: server gave HTTP response to HTTPS client
push报错。由于默认采用的是http协议,即harbor.cfg中的ui_url_protocol值。https的比较麻烦,需要生成证书等步骤,可以参考:
为Harbor设置Https
http://gapme.cn/2017/10/25/harbor-ui-https/
这里暂且用http的方式。
以上报错解决办法:
在”/etc/docker/“目录下,创建”daemon.json“文件。在文件中写入:
{
"insecure-registries": [
"hub.smallsoup.com:80",
"192.168.1.103:80"
]
}重启docker:
systemctl restart docker
docker重启后,./install或者docker-compose down;docker-compose up -d重启harbor即可。
将基础镜像和各个服务镜像push到库上:
题外话:
在安装过程中,将80端口映射到宿主机的8081端口,push的时候遇到很多问题(报错80端口连接拒绝,大概就是这个issue
https://github.com/goharbor/harbor/issues/192),查找了很多资料,还是放弃了,最后映射到宿主机80端口,push一切ok。
由于用域名的方式push得设置hosts以及端口转发,比较麻烦,以上采用了IP:PORT方式:
删除用域名打的标签:
docker rmi -f hub.smallsoup.com:80/micro-service/openjdk:8-jre
原文发布时间为:2018-10-06
本文作者: 小碗汤
本文来自云栖社区合作伙伴“我的小碗汤”,了解相关信息可以关注“我的小碗汤"
【Kubernetes系列】第6篇 Ingress controller - nginx组件介绍
1. 概述
在上一篇文章中我们介绍了如何通过helm进行安装部署traefik组件,文中还提到常用的ingress controller除了traefik还有Nginx、HAProxy、Kong等,在本篇文章中我们就介绍如何安装部署Nginx-ingress,只有在经过积累不同组件的使用经验之后,我们才能更好的比较其优劣,形成最佳实践。
2. nginx-ingress组件的安装部署
2.1 通过helm查找nginx-ingress
# step1: 通过helm查找nginx-ingress
> helm search nginx-ingress
> helm inspect stable/nginx-ingress
2.2 镜像下载及上传
部分企业由于服务器没有外网访问策略以及防火墙的原因无法获取国外Docker镜像,所以我们事先需要将所需镜像准备好,并上传到企业私有镜像仓库。
# step2: 镜像准备
> docker pull quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.25.1
> docker tag quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.25.1 registry.hankercloud.com/ingress-controller/nginx-ingress-controller:0.25.1
> docker push registry.hankercloud.com/ingress-controller/nginx-ingress-controller:0.25.1
>
> docker pull k8s.gcr.io/defaultbackend-amd64:1.5
> docker tag k8s.gcr.io/defaultbackend-amd64:1.5 registry.hankercloud.com/google_containers/defaultbackend-amd64:1.5
> docker push registry.hankercloud.com/google_containers/defaultbackend-amd64:1.5
2.3 组件部署
在上一篇博客中,我们是采用Deployment模式部署的traefik组件,这次我们采用DaemonSet的模式来部署nginx-ingress组件。
# step3: 组件部署
> helm install stable/nginx-ingress --name nginx-ingress --namespace=kube-system \
--set fullnameOverride=nginx-ingress \
--set controller.kind=DaemonSet \
--set controller.daemonset.useHostPort=true \
--set controller.metrics.enabled=true \
--set controller.image.repository=registry.hankercloud.com/ingress-controller/nginx-ingress-controller \
--set defaultBackend.image.repository=registry.hankercloud.com/google_containers/defaultbackend-amd64
# step4: 检查部署是否成功
> helm list
> kubectl get all -n kube-system
> kubectl logs $POD_NAME -n kube-system
2.4 负载均衡配置及域名解析处理
本次我们采用DaemonSet部署nginx-ingress组件,并且使用了主机的80和443接口用来分别接收http和https请求,我们将相应的域名解析到nginx-ingress Pod所在的主机IP之后,就可以通过域名来进行相应的域名访问了。
但上述配置方式无法做到高可用,当nginx-ingress的Pod实例故障或者其所在主机发生故障时,会导致相应的域名无法访问,所以建议在公有云购买负载均衡设备并配置相应的后端服务器列表以实现高可用的目的。
2.5 安装调试
在上文中我们通过helm部署了一个wordpress应用,本文我们继续通过该应用进行域名访问,在本机控制台输入 > curl -i http://10.0.0.182 -H 'Host: blog.hankercloud.com',如果看到有正常返回则说明部署成功。
3. 企业场景及解决方案
3.1 如何做内外网的隔离
Step1: 我们首先部署了两个ingress组件,其中之一是接收内网访问请求,另外一个是接收外网访问请求,相应配置如下:
# 内网nginx-ingress配置声明:
spec:
template:
spec:
containers:
- args:
- /nginx-ingress-controller
- --default-backend-service=kube-system/nginx-ingress-default-backend
- --election-id=ingress-controller-leader
- --ingress-class=nginx
- --configmap=kube-system/nginx-ingress-controller
# 外网nginx-ingress配置声明:
spec:
template:
spec:
containers:
- args:
- /nginx-ingress-controller
- --default-backend-service=kube-system/nginx-ingress-external-default-backend
- --election-id=ingress-controller-leader
- --ingress-class=nginx-external
- --configmap=kube-system/nginx-ingress-external-controller
两者的主要区别在于参数 --ingress-class 设置的值是不一样的
Step2: 对于需要暴露到公网的域名,修改其ingress的定义,相应配置参考如下:
metadata:
name: www
annotations:
kubernetes.io/ingress.class: "nginx-external"
Step3: 检查是否配置成功,执行 kubectl exec ${POD_NAME} -n kube-system cat /etc/nginx/nginx.conf 查看配置文件中是否已经包含外网域名的相关配置,并在本地进行测试验证
4. 参考资料
https://kubernetes.github.io/ingress-nginx/deploy/
https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/
Linux之Docker 添加用户认证
Linux之Docker 添加用户认证私有仓库的TLS加密
以上仓库使用明文的方式,并且没有认证。存在较大的安全隐患,下面介绍使用TLS加密以及用户认证。
为docker仓库添加证书加密功能
docker远程主机访问私有仓库,默认必须使用TLS加密
1 生成证书
[root@toto6 ~]# mkdir -p certs[root@toto6 ~]# openssl req -newkey rsa:4096 -nodes -sha256 -keyout certs/toto.com.key -x509 -days 365 -out certs/toto.com.crtGenerating a 4096 bit RSA private key…++…++writing new private key to ‘certs/toto.com.key’You are about to be asked to enter information that will be incorporatedinto your certificate request.What you are about to enter is what is called a Distinguished Name or a DN.There are quite a few fields but you can leave some blankFor some fields there will be a default value,If you enter ‘.’, the field will be left blank.Country Name (2 letter code) [XX]:CNState or Province Name (full name) []:shaanxiLocality Name (eg, city) [Default City]:xi’anOrganization Name (eg, company) [Default Company Ltd]:westosOrganizational Unit Name (eg, section) []:dockerCommon Name (eg, your name or your server’s hostname) []:toto.comEmail Address []:root@toto.com[root@toto6 ~]# ls certs/toto.com.crt toto.com.key # 成功生成证书1
2、重新启动registry容器:需要先删除之前开启的容器“
[root@toto6 ~]# docker rm -f registryregistry
重新加密开启容器:
root@toto6 ~]# docker run -d ##-d:打入后台
–restart=always > --name registry \-v “$(pwd)”/certs:/certs
-v:手动指定数据卷的挂载
-e REGISTRY_HTTP_ADDR=0.0.0.0:443 ##-e:编辑registry的参数;监听443端口-e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/toto.com.crt ##使用证书为生成的证书-e REGISTRY_HTTP_TLS_KEY=/certs/toto,com.key ##使用的私钥-p 443:443 ##端口映射registry:2 ##仓库名
查看容器运行情况以及端口开启情况:
[root@toto6 ~]# docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESd3063593b314 registry:2 “/entrypoint.sh /etc…” 33 seconds ago Up 31 seconds 0.0.0.0:443->443/tcp, 5000/tcp registry[root@toto6 ~]# netstat -antlpActive Internet connections (servers and established)Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program nametcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 656/sshdtcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN 778/mastertcp 0 0 172.25.13.160:22 172.25.13.250:59964 ESTABLISHED 20132/sshd: root@pttcp6 0 0 :::22 :: LISTEN 656/sshdtcp6 0 0 ::1:25 :: LISTEN 778/mastertcp6 0 0 :::443 :: LISTEN 21555/docker-proxy1
到次registry服务端的TLS加密已经设置好了,但是diocker客户端需要连接这个仓库,也就需要相同的证书才能进行访问。
docker客户端的设置。
设置docker客户端的证书
[root@toto6 ~]# mkdir -p /etc/docker/certs.d/toto.com # 创建该目录,名称和证书域名一致[root@toto6 ~]# cd /etc/docker/certs.d/toto.com[root@toto6 toto.com]# cp /root/certs/toto.com.crt ca.crt # 将生成的证书拷贝到该目录中ca.crt[root@toto6 toto.com]# lsca.crt
证书域名解析的更改
[root@toto6 toto.com]# vim /etc/hosts172.25.13.160 toto6 toto.com123 验证部署是否成功
修改本地镜像标签为固定格式:域名/进行名称
[root@toto6 toto.com]# docker tag nginx:v4 toto.com/nginx1上传镜像:
[root@toto6 toto.com]# docker push toto.com/nginxThe push refers to repository [toto.com/nginx]49cb414524e0: Pushed668afdbd4462: Pushed
latest: digest: sha256:a3e3cbec11f49a4fdebedf975fadbe6dc8cd9e26835fc3018353d7d7f3bdf93b size: 739
作者:若无其事的苹果 来源:CSDN 原文:https://blog.csdn.net/qq_36016375/article/details/96048239 版权声明:本文为博主原创文章,转载请附上博文链接!
Python全栈 MongoDB 数据库(数据的修改)
修改操作符的使用
$set
修改一个域的值,增加一个域
阿哲年龄修改为33
db.class1.update({name:'阿哲'},{$set:{age:33}})
如果sex域不存在则会添加这个域
db.class1.update({name:'阿蓉'},{$set:{sex:'w'}})
$unset
删除一个域
删除sex域
db.class1.update({name:'小陈'},{$unset:{sex:''}})
* 每个操作符可以同时操作多项
db.class1.update({name:'陈'},{$set:{age:36,sex:'m'}})
* 一次修改可以同时使用多个操作符
db.class1.update({name:'阿宝'},{$set:{name:'老王'},$unset:{sex:''}})
$rename
修改域名
将sex域名改为gender
db.class1.update({sex:{$exists:true}},{$rename:{sex:'gender'}},false,true)
$setOnInsert
如果第三个参数为true且插入数据,则表示插入文档的补充内容。如果不插入文档则不起作用
如果插入新文档则setOnInsert中键值对也作为文档内容
db.class1.update({name:'阿文'},{$set:{age:32},$setOnInsert:{gender:'m'}},true)
$inc
加减修改器
db.class1.update({},{$inc:{age:-1}},false,true)
$mul
乘法修改器
db.class2.update({},{$mul:{age:2}},false,true)
* $inc $mul 参数可以使整数小数正数负数
$min
如果筛选文档指定域的值小于min值则不修改,大于min值则修改为min值
如果age大于18则修改为18
db.class2.update({},{$min:{age:18}},false,true)
$max
如果筛选文档指定域的值大于max值则不修改,小于max值则修改为max值
将年龄不到30的修改为30
db.class1.update({},{$max:{age:30}},false,true)
数组修改器
$push
向数组中添加一项
向score数组中添加一项
db.class2.update({name:'小亮'},{$push:{score:91}})
$pushAll
向数组中添加多项
db.class2.update({name:'小明'},{$pushAll:{score:[5,10]}})
$pull
从数组中删除一项
db.class2.update({name:'小明'},{$pull:{score:10}})
$pullAll
从数组中删除多项
db.class2.update({name:'小明'},{$pullAll:{score:[58,5]}})
$each
对多个值进行逐一操作
db.class2.update({name:'小明'},{$push:{score:{$each:[60,10]}}})
$position
指定插入位置
配合each使用将数据插入到指定位置
db.class2.update({name:'小红'},{$push:{score:{$each:[5],$position:1}}})
$sort
对数组进行排序
对数组进行排序
db.class2.update({name:'小明'},{$push:{score:{$each:[],$sort:1}}})
$pop
弹出一项
1表示弹出数组中最后一项,-1表示弹出第一项
db.class2.update({name:'小明'},{$pop:{score:-1}})
$addToSet
向数组中添加一项,但是不允许添加重复内容
如果数组中没有80则添加80
db.class2.update({name:'小红'},{$addToSet:{score:80}})
时间类型
mongodb 中支持时间格式 : ISODate()
1. 使用new Date() 自动生成当前时间
db.class0.insert({book:'Python入门',date:new Date()})
2. 使用 ISODate() 生成当前时间
db.class0.insert({book:'Python精通',date:ISODate()})
3. 获取计算机时间生成时间格式字符串 Date()
db.class0.insert({book:'Python疯狂',date:Date()})
指定时间:
ISODate()
功能: 生成mongodb时间存储类型
参数: 不加参数生成当前时间
指定时间格式参数:
"2018-01-01 12:12:12"
"20180101 12:12:12"
"20180101"
db.class0.insert({book:'Python崩溃',date:ISODate("2018-07-13 11:23:36")})
时间戳:
通过当前的时间生成的一个时间节点标志
valueOf()
生成某个标准时间的时间戳
db.class0.insert({book:'Python重生',date:ISODate().valueOf()})
Null 类型:
值 : null
1. 如果某个域存在却没有值可以设置为null
表示date没有实际意义的值
db.class0.insert({book:'Python编程',date:null})
2. 在查找时可以找到值为null或者不存在某个域的文档
查找到date值为null或者不存在date域的文档
db.class0.find({date:null},{_id:0})
数组的下标操作方式:
可以通过 域名.下标 的方式具体操作数组的某一项
查找数组 0 项大于90的文档
db.class2.find({'score.0':{$gt:90}},{_id:0})
将score 第1项改为10
db.class2.update({name:'小红'},{$set:{'score.1':10}})
内部文档操作(Object):
文档内部某个域的值还是一个文档,则这个文档称为内部文档类型数据
* 通过 外部域.内部文档域 的方式引用内部文档中某个域的值进行操作
db.class3.find({'books.title':'狂人日记'},{_id:0})
db.class3.update({"books.title":'骆驼祥子'},{$set:{"books.price":48.6}})
查找结果的下标引用
可以通过下标的方式获取查找结果的某一项
获取查找结果的 第 2 项
db.class1.find({},{_id:0})[2]
综合示例 :
使用之前的grade数据库
1.将小红年龄改为8岁,兴趣爱好变为跳舞画画
db.class.update({$set:{age:8,hobby:['dance','draw']}})
2. 追加小明兴趣爱好 唱歌
db.class.update({$push:{hobby:'sing'}})
3. 追加小王兴趣爱好,吹牛,打篮球
db.class.update({$pushAll:{hobby:['吹牛','basketball']}})
4. 小李兴趣多了跑步唱歌,但是要确保不和以前的重复
db.class.update({$addToSet:{hobby:{$each:['running','sing']}}})
5. 将该班所有同学年龄加1
db.class.update({},{$inc:{age:1}},false,ture)
6. 删除小明的sex属性
db.class.update({$unset:{sex:''}})
7.删除小李兴趣中的第一项
db.class.update({$pop:{hobby:-1}})
8,删除小红兴趣中的画画和唱歌
db.class.update({$pullAll:{hobby:['draw','sing']}})
9. 为小红增加一个域,为 score:{english:93,chinese:92,match:78}
db.class.update({$set:{score:{english:93,chinese:92,match:78}}})
10. 给小红数学成绩加5分
db.class.update({$inc:{'score.math':5}})
11. 小明的第一爱好改为computer
db.class.update({$set:{'hobby.0':'computer'}})
索引:
指建立指定键值及所在文档中存储位置的对照清单,
使用索引可以方便我们进行快速查找,减少数据遍历次数,从而提高查找效率
Mongodb创建索引:
ensureIndex()
功能 :
创建索引
参数 :
第一个为对哪个域创建索引
第二个为索引的选项
对name域创建索引
db.class1.ensureIndex({name:1})
* 1表示正向索引 -1表示逆向索引
查看某个集合中的索引:
db.class1.getIndexes()
* _id是系统为每个集合自动创建的索引
自定义索引名称
通过第二个参数传入索引选项实现
db.class1.ensureIndex({age:1},{name:'ageIndex'})
* 同一个域不能重复创建相同的索引,一个集合中索引名也不要相同
删除索引:
dropIndex()
功能:
删除一个索引
参数:
索引名或者索引键值对
db.class1.dropIndex({name:-1})
db.class1.dropIndex("ageIndex")
dropIndexes()
功能:
删除所有索引
删除class1中所有索引,但是不会删除_id索引
db.class1.dropIndexes()
其他索引类型:
复合索引:
同时根据多个域创建一个索引
根据name和age域查询都为索引查询,比单独创建两个索引表更节省空间
db.class1.ensureIndex({name:1,age:-1})
数组和子文档索引:
如果对某个数组和子文档域创建索引,那么根据数组和子文档的查找均为索引查找
如果对score域创建索引则 下面的查找也是索引查找
db.class2.find({'score.0':60},{_id:0})
覆盖索引:
查找操作需要获取的域,只有索引域没有其他域。
此时索引表可以直接提供给用户想要的内容,提高查找效率
唯一索引:
创建的索引,索引域值无重复,此时可以创建唯一索引
唯一索引数据结构更加便于查找
对name创建唯一索引,name值不能有重复
db.class1.ensureIndex({name:1},{unique:true})
* 当对某个域创建唯一索引,该域就不能再插入重复数据
稀疏索引:
只针对有指定域的文档创建索引表,如果某个文档没有该域则不会插入到索引表中
对age创建稀疏索引
db.class1.ensureIndex({age:1},{sparse:true})
索引约束:
1. 索引表也需要占用一定的磁盘空间
2. 当数据发生更新时索引表也要随之更新
综上:
1. 数据量比较大时更适合创建索引,数据量较小时没有必要付出索引代价
2. 频繁进行查找操作而不是更新删除插入操作,此时更适合使用索引
固定集合:
mongodb中可以创建大小固定的集合,称之为固定集合。
特点:
1.插入速度更快,顺序查找更快
2.可以控制集合的空间大小
3.能够自动淘汰早期数据
使用:
日志处理
临时缓存
创建:
db.createCollection(collection,{capped:true,size:10000,max:1000})
参数:
capped:true:
创建固定集合
size:10000:
固定集合的大小 字节数
max :1000:
表示最多多少条文档
创建一个固定集合
db.createCollection('log',{capped:true,size:10000,max:3})