第4章_Java仿微信全栈高性能后台+移动客户端
基于web端使用netty和websocket来做一个简单的聊天的小练习。实时通信有三种方式:Ajax轮询、Long pull、websocket,现在很多的业务场景,比方说聊天室、或者手机端online的一些在线的联机的小游戏,其实它们都会需要去做到实时通信。如何做到实时的双向通信呢?Ajax轮询和Long pull,我们来说一下。迄今为止有些小项目还是这样做的。Ajax轮询的原理非常简单,它是通过js使用Ajax的方式异步地让浏览器每隔一段时间,比如说十秒,或者半分钟发送一次请求到后端,去询问服务器有没有一些相关的新的消息或者新的状态的更新。如果有,把这些数据拿出来拿到前端再进行一个相应的渲染,但是这种方式它是一种死循环,会一直地不停地循环地去获得后端数据。同时,我们使用的是Ajax,浏览器不需要去一直刷新。在一些后台管理系统的首页,我们有一些相应的控制面板,控制面板里面有一些业务流程或者一些数据状态的更新,我们往往会通过这种Ajax轮询去做。
Long pull的原理跟Ajax轮询其实是类似的,是差不多的。它也是采用的一种循环的方式,只不过它请求的方式不太好,它是一种阻塞的模型。当客户端发起请求之后,如果服务器没有响应,那么它就一直不会去响应,它会一直卡住,那么直到我们的服务器返回一个response为止。那么这个时候只有在返回之后,客户端它才会再次建立请求。那么如此的循环,肯定是不太好的。它的这种性能也是非常的差。从上面这两种方式其实看的出来,它们都是在不停地建立HTTP连接,然后会等待服务器处理,这样子其实是一种被动的响应。它的缺点也是非常的明显,它这两种方式其实是比较耗资源的,另外性能也不是很好。
接下来讲一下websocket。websocket它其实是由H5提出的,它是一种协议。也就是说原版的HTTP它的协议是没有变化,又或者说是这两者其实都是不一样的东西。HTTP它本身就不支持长链接,我们在之前的例子里面我们有使用到HTTP1.1。HTTP1.1它其实是支持Keep Alive,长链接。HTTP1.0它是没有的。websocket其实就是基于HTTP的一种协议,或者说又是使用了HTTP的协议来完成了一小部分的握手。那么简单来讲,我们客户端发起请求到服务端,那么服务端它其实会找到一个对应的副助理,找到之后服务器会和客户端一直保持链接,为客户端进行服务,并且它可以主动地推送一些消息给我们的客户端。websocket它是一种什么样的协议呢?它有哪些优点?首先websocket它是一个持久化的协议,相对于HTTP这种非持久的协议来讲,它是持久化的。HTTP的一个生命周期是通过一个request来界定。也就是说,有一个request请求到我们的后端,那么后端对应的也会返回一个response响应给我们的客户端。或者有多个request,去对应我们多个response,它们都是一一对应的。有多少个request的请求,就会有多少个response响应,它们的数量是一一对应的,它们是不会有一些相应的偏差。那么在这个时候我们的response其实也是一个被动的,它不能由服务器主动地发起响应,必须要先有一个request请求,才会有一个response响应,所以我们的websocket在这个时候就出现了,它使得我们的资源不会像之前的这种方式这么地浪费,并且它也是非常的主动,只要建立链接,一旦链接建立完毕之后,那么服务端就可以一直不停地主动地推送消息给我们的客户端,客户端可以不需要去请求服务端也是可以达到这样的目的。只需建立一次HTTP请求,就可以做到源源不断的信息的传输。就像你在手机上玩一些在线游戏,一开始建立一个链接之后那么你就一直保持在线了,除非你自己去断开链接再去重新连接,在游戏大厅我们经常看到一些其他的游戏玩家比如购买了一些道具产品或者获得了一些奖杯,这就是websocket服务端向客户端进行主动推送的场景。这几种就是即时通信的一个简单的了解。


做一个基于网页端的聊天的小练习。后端会使用netty来搭建服务端。我们会创建一个websocket server,用于和客户端进行链接。第三步是针对subGroup做的子处理器childHandler。我们会有一个初始化器是针对websocket的initializer,没有就暂且给它定义为一个null。Server定义好之后就需要对这个Server进行一个绑定端口并且以同步的方式去进行使用。serverBootstrap.bind(8088).sync();会返回一个相应的异步的future,它是一个channelFuture。throws Exception在外面抛出异常。针对这个future是需要去进行一个相应channel的一个监听。每个客户端它一链接之后都会有一个channel。它关闭的时候我们会对它做一个监听。future.channel().closeFuture().sync();也是用的一个同步的方式。最后一步不要忘了,要针对这个两个线程mainGroup和subGroup做一个优雅的关闭。mainGroup.shutdownGracefully();
subGroup.shutdownGracefully();
websocket的Server端的代码就已经是OK。下一节会把childHandler对它的一个初始化器进行一个编写。
server启动类编写完毕之后,我们需要写一个childHandler它的一个子处理器,它的一个初始化器,会把所有的Handler都聚在一起。
WSServerInitializer需要继承ChannelInitializer,泛型是SocketChannel。第一步需要从channel里面获取对应的pipeline。有了pipeline之后就需要添加一些相应的处理器,一些相应的助手类Handler。首先添加的一些我们是会使用到netty官方所提供的。首先我们现在是使用的是websocket,那么websocket它是基于一个HTTP的协议,所以在这里第一个我们需要去把一个HttpServerCodeconnect给加与进来。HttpServerCodec是一个编解码器。在Http上有一些相应的数据流的传输,数据流有大有小,所以在这个部分我们有一些相应的大数据流的处理的话,我们要去写,所以我们需要使用到netty对一个大数据流去写的一个支持,这个类叫做io.netty.handler.stream.ChunkedWriteHandler。
* A {@link ChannelHandler} that adds support for writing a large data stream
当前这个类io.netty.handler.stream.ChunkedWriteHandler是添加了支持,是为写一些相应的大数据流。我们需要把当前的这个Handler加入到pipeline管道里面去。

下一步我们会对HttpMessage进行一些相应的处理。既然是HttpMessage,那么我们其实就会使用到一个request和response。HttpObjectAggregator是一个聚合器,1024*64是一个消息的长度。它是创建了一个实例,这里会放一个长度,这个长度是我们消息的一个最大的一个长度。它是一个maxContentLength,最大的一个内容的长度。

它是一个聚合器,它聚合了FullHttpRequest和FullHttpResponse。

这个response我们在之前是有使用到的,就是我们的第一个Hello netty~的小例子。它其实就是把它们聚合到了一起。只要是我们使用netty对Http进行一些相应的编程,几乎我们当前的这样的一个Handler是必须要去使用的,所以说我们把它加入到当前的初始化器里面去就可以了。我们当前的助手类是非常有用的,当你不想去管一些Http消息的时候,我们就直接把这个Handler丢到我们的一个管道里面去,让netty自己去处理。
这三个handler就是针对Http的一个相应的处理。
// TODO Auto-generated method stub
ChannelPipeline pipeline = ch.pipeline(); // websocket 基于http协议,所以要有http编解码器
pipeline.addLast(new HttpServerCodec());
// 对写大数据流的支持
pipeline.addLast(new ChunkedWriteHandler());
// 对httpMessage进行聚合,聚合成FullHttpRequest或者FullHttpResponse
// 几乎在netty中的编程,都会使用到此handler
pipeline.addLast(new HttpObjectAggregator(1024*64)); // ==================== 以上是用于支持http协议 ==========================
有了http的一个相关的协议的一个支持,相应的handler都有了之后,接下来我们就需要去添加一个websocket的相应的一个支持。websocket的handler是WebSocketServerProtocolHandler,协议的一个助手类。websocketPath在一开始建立链接的时候会使用到,就是说我们会有一个协议号ws:以及我们的一个IP和端口外加我们的路径。这个路径大家可以去自定义,你可以去定成你的名字。一般我们把它定义为ws就代表是websocket就可以了。这个WebSocketServerProtocolHandler它是一个我们服务器的一个针对WebSocket处理的协议,它是用于指定给我们客户端访问的路由。WebSocketServerProtocolHandler它处理了所有的一些繁重的复杂的事情,它是帮你去处理的,并且它是跑在一个websocket server。它其实也会帮你去处理一些额外的动作,它会帮你管理一些握手的动作,其实握手的动作就包含关闭、Ping、Pong。其实这个Ping、Pong就可以理解为它就是心跳。Text和Binary相应的一些传输,传输的单位它其实是以frames进行传输的,这个我们在后续的测试中会进行演示,并且它也会把这个frames传输到下一个handler,在管道里面一个一个往下面去传。握手动作就是handshaking。如何用心跳的方式和客户端切断一个链接。close是关闭,ping是请求,pong是响应。这就是websocket的一个协议的handler。

/* websocket 服务器处理的协议,用于指定给客户端连接访问的路由 : /ws
* 本handler会帮你处理一些繁重的复杂的事
* 会帮你处理握手动作:handshaking(close, ping, pong)ping + pong = 心跳
* 对于websocket来讲,都是以frames进行传输的,不同的数据类型对应的frames也不同
*/
pipeline.addLast(new WebSocketServerProtocolHandler("/ws")); // 自定义的handler
pipeline.addLast(null);
最后就需要添加我们的一个自定义的一个相应的handler助手类。这样的一个助手类主要就是用于去读取用户的消息并且对用户的消息进行相应的处理,处理完毕之后你再去发给相应的客户端。当前的这个初始化器已经是都编写完毕了。编写完毕之后我们需要把这个WSServerInitializer.java放到我们的childHandler里面去。这个时候就已经是把这整个相应的管道所需要处理的一些编解码器、handler等等都加到了我们这个子处理器childHandler里面去了。我们下一节会来讲一下这个自定义的handler。接下来我们就需要去添加属于自己的一个自定义的handler。那么咱们这个handler主要是针对客户端传过来的一些相应的消息,我们进行相应的处理,处理完毕之后,我们再可以把相应的消息传给客户端。
新增加一个自定义的handler。对于我们传输数据,它的一个传输数据的一个载体是frames,这个frames在netty中是会为websocket专门去处理用户的文本对象。

在websocket中用于传输text。用户消息是在msg里面。这个msg是在载体TextWebSocketFrame里面。所以我们就需要把相应的消息从这个里面去获取,去拿出来。msg.text()就可以获取我们从客户端发过来的相应的一串字符串。不管是有多少个客户端,只要是任意一个客户端发消息过来,我们都会把所有的相应的消息转发给所有客户端,在所有的客户端上都可以进行一个相应的展示。我们是针对channel去发送,因为我们客户端是对应到我们的每一个channel。这两个方法其实就是在我们的客户端创建完了之后,创建完了之后那么我们的handler就会触发这两个方法handlerAdded、handlerRemoved。handlerRemoved就是用户离开我们的客户端,就是我们的浏览器关闭了就会离开,离开之后它就会自动把我们相应的channel给移除。handlerAdded方法需要获取客户端对应的channel(通道),并且把channel添加到某一个管理的容器。ChannelGroup是用于记录和管理所有客户端的channel,它可以把相应的channel都保存到一整个group组里面去。DefaultChannelGroup它是用来对应我们这个接口ChannelGroup进行一个相应的初始化。我们可以使用一个全局的EventExecutor就可以去做一个相应的初始化了。当客户端和服务端进行了一个连接之后,客户端就有了一个相应的双向通道,这样的一个channel。我们有了channel之后就需要把这个channel添加到我们这个ChannelGroup里面去吧。如果用户把浏览器关闭或者刷新,由于clients是进行一个自动管理,它会自动地把我们相应的channel给移除。clients.remove(ctx.channel());在handlerRemoved方法里面是多余的,只要handler被移除,那么clients里面它自动地会把对应的channel给移除掉。当客户端和服务端连接之后,channel就会有了,有了之后默认系统会给它分配一个id,并且这个id会有一个长id和一个短id。一开始channel已经是有了,系统会为你分配一个很长的一串字符串作为一个唯一的id。使用asLongText()获取的一个id是唯一的。asShortText()是会把我们当前的一个id进行一个相应的精简,精简之后如果整个系统比较大的话,使用ShortText这样的获取一个短id的话是不太靠谱的,因为它有可能会出现重复。现在针对我们的handlerAdded以及handlerRemoved,我们都对clients里面的handler做了相应的添加,移除是自动移除的。数据获取之后要刷到对应的所有的客户端。既然要刷,我们会使用channel.writeAndFlush(Object msg)就直接去刷。如何去刷?有一种方式。所有的channel都在clients里面被管理,我们尽量获取它做一个循环。针对循环里面的每一个channel做一个输出就可以了。channel.writeAndFlush(Object msg)把我们的数据向缓冲区里面去写,缓冲区写完之后再把这相应的数据刷到客户端。channel.writeAndFlush(Object msg)不可以填一个字符串。msg是一个Object对象,但是Object对象不可以去使用String。因为我们传输的载体是一个frame,是一个TextWebSocketFrame。所以在这个部位也是一样,我们需要使用TextWebSocketFrame进行一个载体把我们相应的消息数据放到这个载体里面再刷到我们所有的客户端。这个是通过循环去做一个相应的writeAndFlush,把消息刷到客户端。

ChannelGroup clients它自己本身也提供了一个对应的方法,clients.writeAndFlush(Object message)也是一模一样的。这两种方式你都可以去做相应的处理和操作。当前的ChatHandler已经是编写完毕,对于我们的整个后端都已经是OK了。下一节我们会开始在HTML页面里面写一个相应的javascript把websocket做一个相应的编写,去接收数据、发送数据等等。
前面几节我们已经把netty的后端、服务端进行了一个相应的编写,接下来我们就需要去对前端做编码。在做编码之前我们需要对websocket的一些相应的API先进行一个大致的讲解和了解。首先我们要通过一个客户端和服务端去进行链接,那么链接的话它就会有一个桥梁,那么这个桥梁就相当于是在我们浏览器里面通过一个URL地址去访问。我们可以来new一个websocket,去声明一个websocket的变量,后面会加上一个参数。这个参数其实分为三部分,第一部分是ws://,这个其实就是一个协议,它是指的是我们要通过websocket协议和后端去进行一个链接,这个其实就是相当于是我们在浏览器里面的地址栏,去输入一个HTTP一个协议,或者说是一个https,道理都是一样的,然后去加上我们服务器所在的IP,另外它的端口号也是需要去加上的。那么通过这种方式我们就可以去连接到我们的后端,然后再来看一下它们的一个生命周期。那么在我们的后端,channel它是有相应的生命周期。在前端我们的websocket,它其实也有这样几个生命周期,首先onopen(),onopen()是指当我们的客户端和服务端建立连接的时候,会触发onopen()事件。那么这些事件我们在后续的代码里面我们都会一一进行讲解。onmessage()这个是指收到消息的时候会触发,那么比方说我们的一个服务端向客户端主动地发送了一些相应的消息。只要发送消息,那么在客户端我们就会在onmessage()里面去接收到这样相应的消息。那么onopen()是会触发一次,onmessage()的话只要是服务端和客户端还在连接着中的状态,只要是有推送,那么我们就会有接收到相应的消息。接下来是onerror(),onerror()是出现了一些相应的问题、异常,比方说我们后端出现了异常,我们后端主动关闭了服务器,那么我们在前端的onerror()里面会接收到一些相应的错误事件。然后onclose(),onclose()是指我们的客户端和服务端的连接关闭之后呢就会触发这样的一个函数。onopen()和onclose()是一一对应的。这四个就是前端websocket的生命周期所对应的函数。send()是websocket主动发送的方法。我们可以在前端获取相应的内容,比如说可以抓取用户输入的内容,然后再往这个send里面放。放完之后我们就可以直接把相应的消息发送到我们的后端,我们后端netty这边就会接收到用户发来的相应的消息。Socket.close()事件,当用户他不想去做相应的操作,那么他可以去触发一个按钮,比方说关闭链接shutdown(),那么我们可以直接调用js的方法,通过Socket.close()就可以把当前咱们的这样的一个客户端和服务端的链接给进行关闭。这些就是前端websocket的js的一些相应的API。

把ChatHandler处理类放到初始化器里面去。使用hbuilder这个工具就可以开发相应的前端的页面。HBuilder是基于eclipse的一个二次开发,它和使用eclipse其实是差不多的。

我们首先要编写一个组件,这个组件包含两个小部分。一个是要用户输入相应的文字去发送消息,另外一个就是接收我们服务端推送过来的相应的消息。我们可以定义为一个div。页面效果是已经有了:

我们在前面其实定义了一个handler,这个handler我们是定义了一个ws,其实这就是一个请求到我们后端。只要你是要请求,是一个websocket,那么我们就必须要加上这个ws进行一个相应的路由。我们在前端也进行一个添加,添加ws。那么这个时候CHAT.socket对象,就是CHAT对象里面的socket对象就已经有了,并且它是一个和我们的后端、服务端进行了一个链接。有了之后我们就可以去做发送操作了。

我们要对生命周期进行一个演示。让它的生命周期都进行一个打印和展示。onopen()、onmessage()、onerror()、onclose()都有。e.data就可以获取从服务端推送过来的相应的消息。这个时候就需要使用原生的js。这个html就是它当前的html,因为我们需要做一个累加。连接拼接完毕之后在中间可以加一个换行。一开始页面加载完毕之后是对这个对象CHAT做一个初始化,里面的相应的方法全部都有了。所以说我们在最后是需要做一个初始化我们的链接。这样子就可以在我们加载完毕之后对我们的这个websocket做一个初始化链接。在这个chat部位就是发送消息。调用CHAT对象里面的chat方法。这里整体的前端的代码都已经是写完了,写完了我们下一节会做测试。
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<div>发送消息:</div>
<input type="text" id="msgContent"/>
<input type="button" value="点我发送" onclick="CHAT.chat()"/> <div>接受消息</div>
<div id="receiveMsg" style="background-color: gainsboro;"></div> <script type="application/javascript"> window.CHAT = {
socket: null,
init: function(){
if (window.WebSocket){
CHAT.socket = new WebSocket("ws://192.168.1.104:8080/ws");
CHAT.socket.onopen = function() {
console.log("连接建立成功...");
},
CHAT.socket.onmessage = function(e) {
console.log("接收到消息:"+e.data);
var receiveMsg = document.getElementById("receiveMsg");
var html = receiveMsg.innerHTML;
receiveMsg.innerHTML = html + <br/> +e.data;
},
CHAT.socket.onerror = function() {
console.log("发生错误...");
},
CHAT.socket.onclose = function() {
console.log("连接关闭...");
}
} else {
alert("浏览器不支持websocket协议......");
}
},
chat: function(){
var msg = document.getElementById("msgContent");
CHAT.socket.send(msg.value);
}
}; CHAT.init(); </script>
</body>
</html>
端口号不要写错,ip也不要写错,后面的ws路由更不要去写错。

index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<div>发送消息:</div>
<input type="text" id="msgContent"/>
<input type="button" value="点我发送" onclick="CHAT.chat()"/> <div>接受消息</div>
<div id="receiveMsg" style="background-color: gainsboro;"></div> <script type="application/javascript"> window.CHAT = {
socket: null,
init: function(){
if (window.WebSocket){
CHAT.socket = new WebSocket("ws://192.168.1.104:8088/ws");
CHAT.socket.onopen = function() {
console.log("连接建立成功...");
},
CHAT.socket.onmessage = function(e) {
console.log("接收到消息:"+e.data);
var receiveMsg = document.getElementById("receiveMsg");
var html = receiveMsg.innerHTML;
receiveMsg.innerHTML = html + "<br/>" + e.data;
},
CHAT.socket.onerror = function() {
console.log("发生错误...");
},
CHAT.socket.onclose = function() {
console.log("连接关闭...");
}
} else {
alert("浏览器不支持websocket协议......");
}
},
chat: function(){
var msg = document.getElementById("msgContent");
CHAT.socket.send(msg.value);
}
}; CHAT.init(); </script>
</body>
</html>





我们自己自定义的HandlerChatHandler把接收到的客户端的数据做了一个包装,会以一个TextWebSocketFrame的形式进行发送。另外这一块我们是进行了一个大循环,ChannelGroup是针对所有客户端的channel的管理组,我们直接通过这个管理组的writeAndFlush可以进行一个相应的发送。当服务端主动切断链接之后,客户端发现链接关闭了那么它也会catch到这样的一个相应的事件并且进行一个展示。
/imooc-netty-hello/src/main/java/com/imooc/netty/websocket/ChatHandler.java
/** * Copyright © 2018Nathan.Lee.Salvatore. All rights reserved. * * @Title: ChatHandler1.java * @Prject: imooc-netty-hello * @Package: com.imooc.netty.websocket * @Description: 处理消息的handler
* TextWebSocketFrame: 在netty中,是用于为websocket专门处理文本的对象,frame是消息的载体 * @author: ZHONGZHENHUA * @date: 2018年11月12日 上午6:00:36 * @version: V1.0 */
package com.imooc.netty.websocket; import java.time.LocalDateTime; import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.concurrent.GlobalEventExecutor; /**
* @author ZHONGZHENHUA
*
*/
public class ChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame>{ // 用于记录和管理所有客户端的channel
private static ChannelGroup clients =
new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); @Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
// TODO Auto-generated method stub
// 获取客户端传输过来的消息
String content = msg.text();
System.out.println("接收到的数据:" + content); /* for(Channel channel: clients) {
channel.writeAndFlush(msg);
channel.writeAndFlush(
new TextWebSocketFrame(
"[服务器在]" + LocalDateTime.now()
+ "接受到消息, 消息为:" + content));
}*/ // 下面这个方法,和上面的for循环,一致
clients.writeAndFlush(
new TextWebSocketFrame(
"[服务器在]" + LocalDateTime.now()
+ "接受到消息, 消息为:" + content)); }
/**
* 当客户端连接服务端之后(打开连接)
* 获取客户端的channel,并且放到ChannelGroup中去进行管理
*/
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
// TODO Auto-generated method stub
clients.add(ctx.channel());
} @Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
// TODO Auto-generated method stub
// 当触发handlerRemoved,ChannelGroup会自动移除对应客户单的channel
//clients.remove(ctx.channel());
System.out.println(ctx.channel().id());
System.out.println("客户端断开,channel对应的长id为:" + ctx.channel().id().asLongText());
System.out.println("客户端断开,channel对应的短id为:" + ctx.channel().id().asShortText());
} }


如果有大量的群发送的话,那么建议使用这种channel组的方式去进行writeAndFlush发送。关闭浏览器之后会有断开事件。这个断开事件是在ChatHandler的handlerRemoved,在channel的一个相应的生命周期里面。断开之后就可以获得断开的某一个客户端的channel id吧。长id类似于uid,另外它有短id。这个短id其实是长id的最后一部分,这种情况下如果有大量的客户端,那么很有可能channel的短id是类似的。但是对于这种长id的话,长id肯定保证是唯一的。

控制台,首先连接建立,连接建立之后会有相应的内容全部都进行相应的展示。点开Network我们可以观察它的一个传输的载体,我们之前讲服务端的时候讲到一个frame,这边由于是我们需要重新刷新的,重新刷新之后我们再来做相应的链接。这个ws就是我们建立的请求,它是一个桥梁的链接。这个Frames就是一个载体,它的一个所有的一个上传或者说是下载都是我们把内容的传输都会在这一部分。就是Frames它是一个载体,这个绿色的箭头就是代表我们的消息发送到了服务端,所以它就会是一个绿色的。如果说服务端向客户端进行推送的话,那么它是向下的红色箭头。向下是下载的数据,服务器向客户端进行推送,那么相应的消息也全部都展示了出来。Frames是一个载体,它是针对我们后端所设置的,就是我们所设置的泛型TextWebSocketFrame。这个length是消息的总长度,我们是在web端进行的测试。在手机端也可以进行测试。

这个URL地址是HBuilder帮我们进行发布,它是发布在了8020这个端口。http://127.0.0.1:8020/WebChat/index.html?__hbt=1542274157180改为http://192.168.1.104:8020/WebChat/index.html也可以访问。

基于web端和手机端的websocket就已经是完成了,同时基本流程已经实现了。



实战部分是基于仿微信的一个聊天。
第4章_Java仿微信全栈高性能后台+移动客户端的更多相关文章
- 第3章_Java仿微信全栈高性能后台+移动客户端
当服务器构建完毕并且启动之后,我们通过网页URL地址就可以访问这台服务器,并且服务器会向网页输出Hello Netty这样几个字. Netty有三种线程模型:单线程.多线程.主从线程.Netty官方推 ...
- “全栈2019”113篇Java基础学习资料及总结
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...
- 微信小程序商城构建全栈应用 Thinkphp5
课程——微信小程序商城构建全栈应用[目录]第1章 前言:不同的时代,不同的Web第2章 环境,工具与准备工作第3章 模块,路由与获取请求参数第4章 构建验证层第5章 REST与RESTFul第6章 A ...
- 微信小程序云开发-从0打造云音乐全栈小程序
第1章 首门小程序“云开发”课程,你值得学习本章主要介绍什么是小程序云开发以及学习云开发的重要性,并介绍项目的整体架构,真机演示项目功能,详细介绍整体课程安排.课程适用人群以及需要掌握的前置知识.通过 ...
- C蛮的全栈之路-序章 技术栈选择与全栈工程师
目录 C蛮的全栈之路-序章 技术栈选择与全栈工程师C蛮的全栈之路-node篇(一) 环境布置C蛮的全栈之路-node篇(二) 实战一:自动发博客 博主背景 985院校毕业,至今十年C++开发工作经验, ...
- python全栈开发中级班全程笔记(第二模块、第四章(三、re 正则表达式))
python全栈开发笔记第二模块 第四章 :常用模块(第三部分) 一.正则表达式的作用与方法 正则表达式是什么呢?一个问题带来正则表达式的重要性和作用 有一个需求 : 从文件中读取所有联 ...
- python全栈开发中级班全程笔记(第二模块、第四章)(常用模块导入)
python全栈开发笔记第二模块 第四章 :常用模块(第二部分) 一.os 模块的 详解 1.os.getcwd() :得到当前工作目录,即当前python解释器所在目录路径 impor ...
- python全栈开发中级班全程笔记(第二模块、第三章)(员工信息增删改查作业讲解)
python全栈开发中级班全程笔记 第三章:员工信息增删改查作业代码 作业要求: 员工增删改查表用代码实现一个简单的员工信息增删改查表需求: 1.支持模糊查询,(1.find name ,age fo ...
- 全栈开发工程师微信小程序-中(下)
全栈开发工程师微信小程序-中(下) 微信小程序视图层 wxml用于描述页面的结构,wxss用于描述页面的样式,组件用于视图的基本组成单元. // 绑定数据 index.wxml <view> ...
随机推荐
- 万字总结:学习MySQL优化原理,这一篇就够 了!【转】
说起MySQL的查询优化,相信大家收藏了一堆奇技淫巧:不能使用SELECT *.不使用NULL字段.合理创建索引.为字段选择合适的数据类型..... 你是否真的理解这些优化技巧?是否理解其背后的工作原 ...
- Model compatibility cannot be checked because the database does not contain model metadata. Ensure that IncludeMetadataConvention has been added to the DbModelBuilder conventions
Model compatibility cannot be checked because the database does not contain model metadata. Ensure t ...
- Java 框架
Netty: Netty是由JBOSS提供的一个java开源框架.Netty提供异步的.事件驱动的网络应用程序框架和工具,用以快速开发高性能.高可靠性的网络服务器和客户端程序. 也就是说,Netty ...
- c#将DataTable内容导出为CSV文件
写了个类: class DataTableAndCSV { public static DataTable csvToDataTable(string file) { string strConn = ...
- Android开发中dp、dpi、px的区别(转)
一.基本概念 - dp:安卓中的相对大小 - dpi:(dot per inch)每英寸像素多少 - px:像素点 二.详细说明 1.px和dpi - px: 平常所说的1920×1080只是像素数量 ...
- 解析Ceph: RBDCache 背后的世界
转自:https://www.ustack.com/blog/ceph-internal-rbdcache/ RBDCache 是Ceph的块存储接口实现库 Librbd 的用来在客户端侧缓存数据的目 ...
- HANA aggregate 数字聚合
在project 1 里面 具有服务店代码,金额.应该上一层aggregate 就自动聚合了.可是并没有.要自己手工设置一下.在columns 右键变成——convert to aggregated ...
- 简单使用location.hash的方法 ,怎么做,有什么用? 简单的js路由页面方法。
hash 属性是一个可读可写的字符串,该字符串是URL的锚部分(从#号开始的部分).语法location.hash刚开始我真不知道hash有什么用,直到我在项目中遇上一个最大的问题.而且很恶心的体验 ...
- MariaDB Galera Cluster环境搭建及高可用测试
一.服务器概况Galera Cluster需要至少三个节点,在此次实验过程中,三个节点IP地址:192.168.56.101192.168.56.102192.168.56.103OS为centos ...
- LeetCode Can Place Flowers
原题链接在这里:https://leetcode.com/problems/can-place-flowers/description/ 题目: Suppose you have a long flo ...