使用 WebSocket 实现一个网页版的聊天室(摸鱼更隐蔽)
点击关注公众号,利用碎片时间学习
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"> <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 实现一个网页版的聊天室(摸鱼更隐蔽)相关推荐
- Asp.Net Mvc基于Fleck开发的多人网页版即时聊天室
一.项目的核心说明 1.Fleck这个是实现websocket一个比较简单第三方组件,它不需要安装额外的容器.本身也就几个接口可供调用. 2.项目是基于.net framework 4.7.2 ,在v ...
- Java Dome(实现一个简易版QQ聊天室)
目录 前言 流程图示 Dome结构预览 Message类 服务端用户登录与查询与消息转发 服务端代码 客户端登录 客户端消息接收与发送 注意点 客户端代码 运行结果示意 前言 这玩意就是一个基于TCP ...
- 【项目实战】- 基于SpringBoot+WebScoket+Vue+ElementUI实现一个网页版地球聊天软件
项目介绍 项目已开源gitee: https://gitee.com/gdones/gd-webchart 技术选型 后端:SpringBoot(WEB)+ JWT + MyBatis-plus +M ...
- 网页版在线聊天java Socket实现
注:本文引用地址http://www.jb51.net/article/84689.htm 本文为大家分享了一个满足在线网页交流需求的实例,由于java Socket实现的网页版在线聊天功能,供大家参 ...
- Python 写了一个网页版的「P图软件」,惊呆了!
作者 | 小欣 来源 | Python爱好者集中营 今天是开工第一天,这篇文章可以算作是虎年的第一篇干货技术类文章了,今天小编用Python做了一个网页版的"P图软件",大致的流程 ...
- [html] 如果让你实现一个网页版的办公表格(类似excel),你觉得是否可行?如果可行应该怎么做?
[html] 如果让你实现一个网页版的办公表格(类似excel),你觉得是否可行?如果可行应该怎么做? 尝试过写,底层还是用table,td,tr. 难点在于范围性选择单元格,横纵单元格合并互相不影响 ...
- jquery 背景特效实现_html5实现的仿网页版微信聊天界面效果源码
码农那点事儿 关注我们,一起学习进步 这是一款基于html5实现的仿网页版微信聊天界面效果源码,可实现微信网页版聊天界面效果,在编辑框编辑文字之后按Ctrl+Enter键即可提交文字到聊天对话框上.整 ...
- html5仿网页版微信聊天界面代码
2019独角兽企业重金招聘Python工程师标准>>> html5仿网页版微信聊天界面代码 转载于:https://my.oschina.net/u/1266171/blog/783 ...
- 用Python写了一个网页版的美图秀秀,惊呆了
今天小编用Python做了一个网页版的"P图软件",大致的流程在于我们可以将上传的照片进行黑白处理.铅笔素描处理.模糊化处理等一系列操作,具体如下 下面我们来看一下该整个网页是怎么 ...
- QQ版网络聊天室完整项目+MFC\C++\C(更改服务器IP可实现异机沟通)
QQ版网络聊天室完整项目+MFC\C++\C 资源地址1 资源地址2 项目简介 服务器端部分代码 客户端响应部分代码 数据库连接 理论~ 实例展示 资源地址1 https://github.com/M ...
最新文章
- keras神经网络回归预测_如何使用Keras建立您的第一个神经网络来预测房价
- VMware Horizon虚拟桌面工具箱之审计与远程协助
- 凤凰涅槃:从 iBatis 到 MyBatis
- 使用WebLogic共享库连续交付ADF应用程序
- 深入理解计算机系统----读书笔记
- Python笔记-多线程爬虫实例
- Oracle在JavaOne上宣布Java EE 8将会延期至2017年底
- 广义表头尾链表存储结构_单向循环链表的存储结构和操作
- http 性能测试 wrk使用教程
- C++const类型
- 国内十大HR系统品牌
- 计算机没有无线网卡驱动程序,win10系统的无线网卡驱动没了的解决方法
- R语言检验时间序列中是否存在自相关性:使用box.test函数执行box-pierce检验验证时间序列中是否存在自相关性
- VS2015 无法打开包括文件string.h等
- 一个简单todos的例子
- 可三维成像(16,32线等)激光雷达调研
- 交互设计流程是怎样的?
- 红糖水白糖水的转换--郝汉森
- 阿里开发者招聘节 | 面试题14:如何实现两金额数据相加(最多小数点两位)
- 尚硅谷SpringBoot学习笔记
热门文章
- 【C++】关于日历编程的一点思考
- 潇洒学校丹丹老师分享免喷涂材料注塑工艺的五大要点
- 【设计】资料合集(1-121)副业学习会
- 京东物流数据安全体系
- linux释放cpu命令,linux内存清理和释放命令
- phalapi 数据库锁_2.5 PhalApi 连接多个数据库
- 程序员学习时间的由来
- Processing笔记04—鼠标键盘识别
- 网吧游戏服务器虚拟机,用虚拟机亲自体验网咖无盘系统,终于知道网咖系统快的原因了...
- 双系统linux分区扩容,Win7 下Ubuntu14.04 双系统无损扩容