点击关注公众号,利用碎片时间学习

WebSocket简介

WebSocket协议是完全重新设计的协议,旨在为Web上的双向数据传输问题提供一个切实可行的解决方案,使得客户端和服务器之间可以在任意时刻传输消息,因此,这也就要求它们异步地处理消息回执

WebSocket特点:

  • HTML5 中的协议,实现与客户端与服务器双向,基于消息的文本或二进制数据通信

  • 适合于对数据的实时性要求比较强的场景,如通信、直播、共享桌面,特别适合于客户端与服务端频繁交互的情况下,如实时共享、多人协作等平台

  • 采用新的协议,后端需要单独实现

  • 客户端并不是所有浏览器都支持

WebSocket通信握手

在从标准的 HTTP 或者 HTTPS协议切换到WebSocket时,将会使用一种称为握手的机制 ,因此,使用WebSocket的应用程序将始终以HTTP/S作为开始,然后再执行升级。这个升级动作发生的确切时刻特定于应用程序;它可能会发生在启动时,也可能会发生在请求了某个特定的URL之后

下面是WebSocket请求和响应的标识信息:

客户端的请求:

  • Connection属性中标识Upgrade,表示客户端希望连接升级

  • Upgrade属性中标识为Websocket,表示希望升级成 Websocket 协议

  • Sec-WebSocket-Key属性,表示随机字符串,服务器端会用这些数据来构造出一个 SHA-1 的信息摘要。把 “Sec-WebSocket-Key” 加上一个特殊字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后计算 SHA-1 摘要,之后进行 BASE-64 编码,将结果做为 “Sec-WebSocket-Accept” 头的值,返回给客户端。如此操作,可以尽量避免普通 HTTP 请求被误认为 Websocket 协议。

  • Sec-WebSocket-Version属性,表示支持的 Websocket 版本,RFC6455 要求使用的版本是 13,之前草案的版本均应当弃用

服务器端响应:

  • Upgrade属性中标识为websocket

  • Connection告诉客户端即将升级的是 Websocket 协议

  • Sec-WebSocket-Accept这个则是经过服务器确认,并且加密过后的Sec-WebSocket-Key

Netty为WebSocket数据帧提供的支持

由 IETF 发布的WebSocket RFC,定义了6种帧,Netty为它们每种都提供了一个POJO实现

实战

首先,定义WebSocket服务端,其中创建了一个Netty提供ChannelGroup变量用来记录所有已经连接的客户端channel,而这个ChannelGroup就是用来完成群发和单聊功能的

//定义websocket服务端
public class WebSocketServer {private static EventLoopGroup bossGroup = new NioEventLoopGroup(1);private static EventLoopGroup workerGroup = new NioEventLoopGroup();private static ServerBootstrap bootstrap = new ServerBootstrap();private static final int PORT =8761;//创建 DefaultChannelGroup,用来保存所有已经连接的 WebSocket Channel,群发和一对一功能可以用上private final static ChannelGroup channelGroup =new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);public static void startServer(){try {bootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).childHandler(new WebSocketServerInitializer(channelGroup));Channel ch = bootstrap.bind(PORT).sync().channel();System.out.println("打开浏览器访问: http://127.0.0.1:" + PORT + '/');ch.closeFuture().sync();} catch (Exception e) {e.printStackTrace();}finally{bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}}public static void main(String[] args) {startServer();}
}

接下来,初始化Pipeline,向当前Pipeline中注册所有必需的ChannelHandler,主要包括:用于处理HTTP请求编解码的HttpServerCodec、自定义的处理HTTP请求的HttpRequestHandler、用于处理WebSocket帧数据以及升级握手的WebSocketServerProtocolHandler以及自定义的处理TextWebSocketFrame数据帧和握手完成事件的WebSocketServerHanlder

public class WebSocketServerInitializer extends ChannelInitializer<SocketChannel>{/*websocket访问路径*/private static final String WEBSOCKET_PATH = "/ws";private ChannelGroup channelGroup;public WebSocketServerInitializer(ChannelGroup channelGroup){this.channelGroup=channelGroup;} @Overrideprotected void initChannel(SocketChannel ch) throws Exception {//用于HTTP请求的编解码ch.pipeline().addLast(new HttpServerCodec());//用于写入一个文件的内容ch.pipeline().addLast(new ChunkedWriteHandler());//用于http请求的聚合ch.pipeline().addLast(new HttpObjectAggregator(64*1024));//用于WebSocket应答数据压缩传输ch.pipeline().addLast(new WebSocketServerCompressionHandler());//处理http请求,对非websocket请求的处理ch.pipeline().addLast(new HttpRequestHandler(WEBSOCKET_PATH));//根据websocket规范,处理升级握手以及各种websocket数据帧ch.pipeline().addLast(new WebSocketServerProtocolHandler(WEBSOCKET_PATH, "", true));//对websocket的数据进行处理,主要处理TextWebSocketFrame数据帧和握手完成事件ch.pipeline().addLast(new WebSocketServerHanlder(channelGroup));}
}

HttpRequestHandler用来处理HTTP请求,首先会先确认当前的HTTP请求是否指向了WebSocket的URI,如果是那么HttpRequestHandler将调用FullHttpRequest对象上的retain方法,并通过调用fireChannelRead(msg)方法将它转发给下一个ChannelInboundHandler(之所以调用retain方法,是因为调用channelRead0方法完成之后,会进行资源释放)

接下来,读取磁盘上指定路径的index.html文件内容,将内容封装成ByteBuf对象,之后,构造一个FullHttpResponse响应对象,将ByteBuf添加进去,并设置请求头信息。最后,调用writeAndFlush方法冲刷所有写入的消息

public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest>{private static final File INDEX = new File("D:/学习/index.html");private String websocketUrl;public HttpRequestHandler(String websocketUrl){this.websocketUrl = websocketUrl;}@Overrideprotected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {if(websocketUrl.equalsIgnoreCase(msg.getUri())){//如果该HTTP请求指向了websocketUrl的URL,那么直接交给下一个ChannelInboundHandler进行处理ctx.fireChannelRead(msg.retain());}else{//生成index页面的具体内容,并送往浏览器ByteBuf content = loadIndexHtml(); FullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, OK, content);res.headers().set(HttpHeaderNames.CONTENT_TYPE,"text/html; charset=UTF-8");HttpUtil.setContentLength(res, content.readableBytes());sendHttpResponse(ctx, msg, res);}}public static ByteBuf loadIndexHtml(){FileInputStream fis = null;InputStreamReader isr = null;BufferedReader  raf = null;StringBuffer content = new StringBuffer();try {fis = new FileInputStream(INDEX);isr = new InputStreamReader(fis);raf = new BufferedReader(isr);String s = null;// 读取文件内容,并将其打印while((s = raf.readLine()) != null) {content.append(s);}} catch (Exception e) {// TODO Auto-generated catch blocke.printStackTrace();} finally {try {fis.close();isr.close();raf.close();} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}}return Unpooled.copiedBuffer(content.toString().getBytes());}/*发送应答*/private static void sendHttpResponse(ChannelHandlerContext ctx,FullHttpRequest req,FullHttpResponse res) {// 错误的请求进行处理 (code<>200).if (res.status().code() != 200) {ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(),CharsetUtil.UTF_8);res.content().writeBytes(buf);buf.release();HttpUtil.setContentLength(res, res.content().readableBytes());}// 发送应答.ChannelFuture f = ctx.channel().writeAndFlush(res);//对于不是长连接或者错误的请求直接关闭连接if (!HttpUtil.isKeepAlive(req) || res.status().code() != 200) {f.addListener(ChannelFutureListener.CLOSE);}}
}

前面的HttpRequestHandler处理器只是用来管理HTTP请求和响应的,而实际对传输的WebSocket数据帧的处理是交由WebSocketServerHanlder 进行(其中只对TextWebSocketFrame类型的数据帧进行处理)。

WebSocketServerHanlder 处理时通过重写userEventTriggered方法,并监听握手成功的事件,当新客户端的WebSocket握手成功之后,它将通过把通知消息写到ChannelGroup中的所有channel来通知所有已经连接的客户端,然后它将这个新的channel加入到该ChannelGroup中,并且还为每个channel随机生成了一个用户

之后,如果接收到了TextWebSocketFrame消息时,会先根据当前channel拿到用户,并解析发送的文本帧信息,确认是群聊还是单聊,最后,构造TextWebSocketFrame响应内容,通过writeAndFlush进行冲刷

/*** 对websocket的文本数据帧进行处理**/
public class WebSocketServerHanlder extends SimpleChannelInboundHandler<TextWebSocketFrame>{private ChannelGroup channelGroup;public WebSocketServerHanlder(ChannelGroup channelGroup){this.channelGroup=channelGroup;}@Overrideprotected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {//获取当前channel用户名String userName=UserMap.getUser(ctx.channel().id().asLongText());//文本帧String content= msg.text();System.out.println("Client: "+ userName+" received [ "+content+" ]");String toName = null;//判断是单聊还是群发(单聊会通过  user@ msg 这种格式进行传输文本帧)if(content.contains("@")){String[] str= content.split("@");content=str[1];//获取单聊的用户toName = str[0];}if(null!=toName){Iterator<Channel> it=channelGroup.iterator();while(it.hasNext()){Channel channel=it.next();//找到指定的用户if(UserMap.getUser(channel.id().asLongText()).equals(toName)){//单聊channel.writeAndFlush(new TextWebSocketFrame(userName+"@"+content));}}}else{channelGroup.remove(ctx.channel());//群发实现channelGroup.writeAndFlush(new TextWebSocketFrame(userName+"@"+content));channelGroup.add(ctx.channel());}}@Overridepublic void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {//检测事件,如果是握手成功事件,做点业务处理if(evt==WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE){String channelId = ctx.channel().id().asLongText();//随机为当前channel指定一个用户名UserMap.setUser(channelId);System.out.println("新的客户端连接:"+UserMap.getUser(channelId));//通知所有已经连接的 WebSocket 客户端新的客户端已经连接上了channelGroup.writeAndFlush(new TextWebSocketFrame(UserMap.getUser(channelId)+"加入群聊"));//将新的 WebSocket Channel 添加到 ChannelGroup 中channelGroup.add(ctx.channel());}else{super.userEventTriggered(ctx, evt);}}
}

index.html内容

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>基于WebSocket实现网页版群聊</title>
</head>
<body>
<script type="text/javascript">   var userName= null;        var socket;        var myDate = new Date();if (!window.WebSocket) {window.WebSocket = window.MozWebSocket;}if (window.WebSocket) {socket = new WebSocket("ws://127.0.0.1:8761/ws");socket.onmessage = function(event) { var info = document.getElementById("jp-container");var dataObj=event.data;if(dataObj.indexOf("@")!=-1){var arr = dataObj.split('@');var sendUser;var acceptMsg;for(var i=0;i<arr.length;i++){if(i==0){sendUser = arr[i];}else{acceptMsg =arr[i];}}if(userName==sendUser){return;}        var talk= document.createElement("div");talk.setAttribute("class", "talk_recordboxme");talk.innerHTML = sendUser+':';var recordtext= document.createElement("div");recordtext.setAttribute("class", "talk_recordtextbg");talk.appendChild(recordtext);var talk_recordtext=document.createElement("div");talk_recordtext.setAttribute("class", " talk_recordtext");var h3=document.createElement("h3");h3.innerHTML =acceptMsg;talk_recordtext.appendChild(h3);var span=document.createElement("span");span.innerHTML =myDate.toLocaleTimeString();span.setAttribute("class", "talk_time");talk_recordtext.appendChild(span);talk.appendChild(talk_recordtext);}else{var talk= document.createElement("div");talk.style.textAlign="center";var font = document.createElement("font");font.color='#212121';font.innerHTML = dataObj+': '+myDate.toLocaleString( ); talk.appendChild(font);}info.appendChild(talk);};socket.onopen = function(event) {console.log("Socket 已打开");};socket.onclose = function(event) {console.log("Socket已关闭");};} else {alert("Your browser does not support Web Socket.");}function send(message) {if (!window.WebSocket) { return; }if (socket.readyState == WebSocket.OPEN) {var info = document.getElementById("jp-container");var talk= document.createElement("div");talk.setAttribute("class", "talk_recordbox");var user = document.createElement("div");user.setAttribute("class", "user");talk.appendChild(user);var recordtext= document.createElement("div");recordtext.setAttribute("class", "talk_recordtextbg");talk.appendChild(recordtext);var talk_recordtext=document.createElement("div");talk_recordtext.setAttribute("class", " talk_recordtext");var h3=document.createElement("h3");h3.innerHTML =message;talk_recordtext.appendChild(h3);var span=document.createElement("span");span.innerHTML =myDate.toLocaleTimeString();span.setAttribute("class", "talk_time");talk_recordtext.appendChild(span);talk.appendChild(talk_recordtext);info.appendChild(talk );socket.send(message);} else {alert("The socket is not open.");}}
</script><br>
<br>
<div class="talk"><div class="talk_title"><span>群聊</span></div><div class="talk_record" style="background: #EEEEF4;"><div id="jp-container" class="jp-container"></div></div><form onsubmit="return false;"><div class="talk_word">&nbsp;<input class="add_face" id="facial" type="button" title="添加表情" value="" /><input class="messages emotion" autocomplete="off" name="message" value="在这里输入文字" onFocus="if(this.value=='在这里输入文字'){this.value='';}"  onblur="if(this.value==''){this.value='在这里输入文字';}"  /><input class="talk_send" type="button" title="发送" value="发送"  onclick="send(this.form.message.value)" /></div></form>
</div>

样式

body{font-family:verdana, Arial, Helvetica, "宋体", sans-serif;font-size: 12px;
}body ,div ,dl ,dt ,dd ,ol ,li ,h1 ,h2 ,h3 ,h4 ,h5 ,h6 ,pre ,form ,fieldset ,input ,P ,blockquote ,th ,td ,img,
INS {margin: 0px;padding: 0px;border:0;
}
ol{list-style-type: none;
}
img,input{border:none;
}a{color:#198DD0;text-decoration:none;
}
a:hover{color:#ba2636;text-decoration:underline;
}
a{blr:expression(this.onFocus=this.blur())}/*去掉a标签的虚线框,避免出现奇怪的选中区域*/
:focus{outline:0;}.talk{height: 480px;width: 335px;margin:0 auto;border-left-width: 1px;border-left-style: solid;border-left-color: #444;
}
.talk_title{width: 100%;height:40px;line-height:40px;text-indent: 12px;font-size: 16px;font-weight: bold;color: #afafaf;background:#212121;border-bottom-width: 1px;border-bottom-style: solid;border-bottom-color: #434343;font-family: "微软雅黑";
}
.talk_title span{float:left}
.talk_title_c {width: 100%;height:30px;line-height:30px;
}
.talk_record{width: 100%;height:398px;overflow: hidden;border-bottom-width: 1px;border-bottom-style: solid;border-bottom-color: #434343;margin: 0px;
}
.talk_word {line-height: 40px;height: 40px;width: 100%;background:#212121;
}
.messages {height: 24px;width: 240px;text-indent:5px;overflow: hidden;font-size: 12px;line-height: 24px;color: #666; background-color: #ccc;border-radius: 3px;-moz-border-radius: 3px;-webkit-border-radius: 3px;
}
.messages:hover{background-color: #fff;}
.talk_send{width:50px;height:24px;line-height: 24px;font-size:12px;border:0px;margin-left: 2px;color: #fff;background-repeat: no-repeat;background-position: 0px 0px;background-color: transparent;font-family: "微软雅黑";
}
.talk_send:hover {background-position: 0px -24px;
}
.talk_record ul{ padding-left:5px;}
.talk_record li {line-height: 25px;
}
.talk_word .controlbtn a{margin: 12px;
}
.talk .talk_word .order {float:left;display: block;height: 14px;width: 16px;   background-repeat: no-repeat;background-position: 0px 0px;
}.talk .talk_word .loop {float:left;display: block;height: 14px;width: 16px;background-repeat: no-repeat;background-position: -30px 0px;
}
.talk .talk_word .single {float:left;display: block;height: 14px;width: 16px;background-repeat: no-repeat;background-position: -60px 0px;
}
.talk .talk_word .order:hover,.talk .talk_word .active{background-position: 0px -20px;text-decoration: none;
}
.talk .talk_word .loop:hover{background-position: -30px -20px;text-decoration: none;
}
.talk .talk_word .single:hover{background-position: -60px -20px;text-decoration: none;
}/*讨论区*/
.jp-container .talk_recordbox{min-height:80px;color: #afafaf;padding-top: 5px;padding-right: 10px;padding-left: 10px;padding-bottom: 0px;
}.jp-container .talk_recordbox:first-child{border-top:none;}
.jp-container .talk_recordbox:last-child{border-bottom:none;}
.jp-container .talk_recordbox .talk_recordtextbg{float:left;width:10px;height:30px;display:block;background-repeat: no-repeat;background-position: left top;}
.jp-container .talk_recordbox .talk_recordtext{-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px;background-color:#b8d45c;width:240px;height:auto;display:block;padding: 5px;float:left;color:#333333;
}
.jp-container .talk_recordbox h3{font-size:14px;padding:2px 0 5px 0;text-transform:uppercase;font-weight: 100;}
.jp-container .talk_recordbox .user {float:left;display:inline;height: 45px;width: 45px;margin-top: 0px;margin-right: 5px;margin-bottom: 0px;margin-left: 0px;font-size: 12px;line-height: 20px;text-align: center;
}
/*自己发言样式*/
.jp-container .talk_recordboxme{display:block;min-height:80px;color: #afafaf; padding-top: 5px;padding-right: 10px;padding-left: 10px;padding-bottom: 0px;
}
.jp-container .talk_recordboxme .talk_recordtextbg{float:right;width:10px;height:30px;display:block;background-repeat: no-repeat;background-position: left top;}.jp-container .talk_recordboxme .talk_recordtext{-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px;background-color:#fcfcfc;width:240px;height:auto;padding: 5px;color:#666;font-size:12px;float:right;}
.jp-container .talk_recordboxme h3{font-size:14px;padding:2px 0 5px 0;text-transform:uppercase;font-weight: 100;color:#333333;}
.jp-container .talk_recordboxme .user{float:right;height: 45px;width: 45px;margin-top: 0px;margin-right: 10px;margin-bottom: 0px;margin-left: 5px;font-size: 12px;line-height: 20px;text-align: center;display:inline;
}
.talk_time{color: #666;text-align: right;width: 240px;display: block;
}

测试

首先,启动三个窗口

群聊

单聊

总结

本文,基于Netty实战了一个WebSocket协议实现的网页版聊天室服务器,从代码上可以看出,基于Netty的WebSocket的实现还是非常简单、容易实现的。

但是WebSocket协议使用上还是存在局限的,比如需要浏览器的支持。但是毕竟WebSocket代表了Web技术的一种重要进展,可以扩宽我们的视野,在一些特定的工作场景中,可以帮助我们解决一些问题

来源:blog.csdn.net/wzljiayou/article/details/110506164

推荐:最全的java面试题库PS:因为公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。点“在看”支持我们吧!

使用 WebSocket 实现一个网页版的聊天室(摸鱼更隐蔽)相关推荐

  1. Asp.Net Mvc基于Fleck开发的多人网页版即时聊天室

    一.项目的核心说明 1.Fleck这个是实现websocket一个比较简单第三方组件,它不需要安装额外的容器.本身也就几个接口可供调用. 2.项目是基于.net framework 4.7.2 ,在v ...

  2. Java Dome(实现一个简易版QQ聊天室)

    目录 前言 流程图示 Dome结构预览 Message类 服务端用户登录与查询与消息转发 服务端代码 客户端登录 客户端消息接收与发送 注意点 客户端代码 运行结果示意 前言 这玩意就是一个基于TCP ...

  3. 【项目实战】- 基于SpringBoot+WebScoket+Vue+ElementUI实现一个网页版地球聊天软件

    项目介绍 项目已开源gitee: https://gitee.com/gdones/gd-webchart 技术选型 后端:SpringBoot(WEB)+ JWT + MyBatis-plus +M ...

  4. 网页版在线聊天java Socket实现

    注:本文引用地址http://www.jb51.net/article/84689.htm 本文为大家分享了一个满足在线网页交流需求的实例,由于java Socket实现的网页版在线聊天功能,供大家参 ...

  5. Python 写了一个网页版的「P图软件」,惊呆了!

    作者 | 小欣 来源 | Python爱好者集中营 今天是开工第一天,这篇文章可以算作是虎年的第一篇干货技术类文章了,今天小编用Python做了一个网页版的"P图软件",大致的流程 ...

  6. [html] 如果让你实现一个网页版的办公表格(类似excel),你觉得是否可行?如果可行应该怎么做?

    [html] 如果让你实现一个网页版的办公表格(类似excel),你觉得是否可行?如果可行应该怎么做? 尝试过写,底层还是用table,td,tr. 难点在于范围性选择单元格,横纵单元格合并互相不影响 ...

  7. jquery 背景特效实现_html5实现的仿网页版微信聊天界面效果源码

    码农那点事儿 关注我们,一起学习进步 这是一款基于html5实现的仿网页版微信聊天界面效果源码,可实现微信网页版聊天界面效果,在编辑框编辑文字之后按Ctrl+Enter键即可提交文字到聊天对话框上.整 ...

  8. html5仿网页版微信聊天界面代码

    2019独角兽企业重金招聘Python工程师标准>>> html5仿网页版微信聊天界面代码 转载于:https://my.oschina.net/u/1266171/blog/783 ...

  9. 用Python写了一个网页版的美图秀秀,惊呆了

    今天小编用Python做了一个网页版的"P图软件",大致的流程在于我们可以将上传的照片进行黑白处理.铅笔素描处理.模糊化处理等一系列操作,具体如下 下面我们来看一下该整个网页是怎么 ...

  10. QQ版网络聊天室完整项目+MFC\C++\C(更改服务器IP可实现异机沟通)

    QQ版网络聊天室完整项目+MFC\C++\C 资源地址1 资源地址2 项目简介 服务器端部分代码 客户端响应部分代码 数据库连接 理论~ 实例展示 资源地址1 https://github.com/M ...

最新文章

  1. keras神经网络回归预测_如何使用Keras建立您的第一个神经网络来预测房价
  2. VMware Horizon虚拟桌面工具箱之审计与远程协助
  3. 凤凰涅槃:从 iBatis 到 MyBatis
  4. 使用WebLogic共享库连续交付ADF应用程序
  5. 深入理解计算机系统----读书笔记
  6. Python笔记-多线程爬虫实例
  7. Oracle在JavaOne上宣布Java EE 8将会延期至2017年底
  8. 广义表头尾链表存储结构_单向循环链表的存储结构和操作
  9. http 性能测试 wrk使用教程
  10. C++const类型
  11. 国内十大HR系统品牌
  12. 计算机没有无线网卡驱动程序,win10系统的无线网卡驱动没了的解决方法
  13. R语言检验时间序列中是否存在自相关性:使用box.test函数执行box-pierce检验验证时间序列中是否存在自相关性
  14. VS2015 无法打开包括文件string.h等
  15. 一个简单todos的例子
  16. 可三维成像(16,32线等)激光雷达调研
  17. 交互设计流程是怎样的?
  18. 红糖水白糖水的转换--郝汉森
  19. 阿里开发者招聘节 | 面试题14:如何实现两金额数据相加(最多小数点两位)
  20. 尚硅谷SpringBoot学习笔记

热门文章

  1. 【C++】关于日历编程的一点思考
  2. 潇洒学校丹丹老师分享免喷涂材料注塑工艺的五大要点
  3. 【设计】资料合集(1-121)副业学习会
  4. 京东物流数据安全体系
  5. linux释放cpu命令,linux内存清理和释放命令
  6. phalapi 数据库锁_2.5 PhalApi 连接多个数据库
  7. 程序员学习时间的由来
  8. Processing笔记04—鼠标键盘识别
  9. 网吧游戏服务器虚拟机,用虚拟机亲自体验网咖无盘系统,终于知道网咖系统快的原因了...
  10. 双系统linux分区扩容,Win7 下Ubuntu14.04 双系统无损扩容