说明:本文主要写的是即时聊天功能的一些设计思路,文末有完整项目地址,仅有后端代码。本人经验尚浅,有错误、不妥之处望各位大神与本人联系,不胜感激。文章中有些许借用的地方,如侵权,请及时联系本人,立马删除。
邮箱:2540628013@qq.com
本项目用到的技术
netty框架,为什么要使用netty框架呢?
Bio
BIO 有的称之为 basic(基本) IO,有的称之为 block(阻塞) IO,主要应用于文件 IO 和网络 IO, 这里不再说文件 IO, 在 JDK1.4 之前,我们建立网络连接的时候只能采用 BIO,需要先在服务端启动一个 ServerSocket,然后在客户端启动 Socket 来对服务端进行通信,默认情况下服务端需要对每个请求建立一个线程等待请求,而客户端发送请求后,先咨询服务端是否有线程响应,如果没有则会一直等待或者遭到拒绝,如果有的话,客户端线程会等待请求结束后才继续执行, 这就是阻塞式 IO。
//BIO 服务器端程序
public class TCPServer {
public static void main(String[] args) throws Exception {
//1.创建 ServerSocket 对象
ServerSocket ss = new ServerSocket(9999);
while (true) {
//2.监听客户端
Socket s = ss.accept(); //阻塞
// 3.从连接中取出输入流来接收消息
InputStream is = s.getInputStream();
//阻塞
byte[] b = new byte[10];
is.read(b);
String clientIP = s.getInetAddress().getHostAddress();
System.out.println(clientIP + "说:" + new String(b).trim());
//4.从连接中取出输出流并回话
OutputStream os = s.getOutputStream();
os.write("hello".getBytes());
//5.关闭
s.close();
}
}
}
上述代码编写了一个服务器端程序,绑定端口号 9999,accept 方法用来监听客户端连接, 如果没有客户端连接,就一直等待,程序会阻塞到这里。
//BIO 客户端程序
public class TCPClient {
public static void main(String[] args) throws Exception {
while (true) {
//1.创建 Socket 对象
Socket s = new Socket("127.0.0.1", 9999);
//2.从连接中取出输出流并发消息
OutputStream os = s.getOutputStream();
System.out.println("请输入:");
Scanner sc = new Scanner(System.in);
String msg = sc.nextLine();
os.write(msg.getBytes());
//3.从连接中取出输入流并接收回话
InputStream is = s.getInputStream();
//阻塞
byte[] b = new byte[20];
is.read(b);
System.out.println("说:" + new String(b).trim());
//4.关闭
s.close();
}
}
}
上述代码编写了一个客户端程序,通过 9999 端口连接服务器端,getInputStream 方法用来 等待服务器端返回数据,如果没有返回,就一直等待,程序会阻塞到这里。
结果请拷贝到自己idea自行查看
这个仅仅只是简单的演示代码,如果真正的使用怎么做呢?加线程呗,一个用户过来就新建一个线程,然后每个线程里面一个while循环阻塞在里面,这是很恐怖的一件事,这些就会带来下面三个问题:
1、资源受限,大量的线程阻塞在那里,对于服务器来说是很浪费资源的一件事。
2、线程切换频繁,我们知道java线程如果优先级相同是抢占式的也就是随机的,线程数量过多,对于单核cpu切换来说是很影响性能的。
3、上面例子可以看到Bio是以byte为单位的。
Nio
Jdk1.4之后提出了Nio,NIO 和 BIO 有着相同的目的和作用,但是它们的实现方式完全不同,BIO 以流的方式处理数据,**而 NIO 以块的方式处理数据,**块 I/O 的效率比流 I/O 高很多。另外,NIO 是非阻塞式的, 这一点跟 BIO 也很不相同,使用它可以提供非阻塞式的高伸缩性网络。 NIO 主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器)。传统的 BIO 基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通 道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。
这里借大神“闪电侠”举的例子说明一下:
(强烈推荐Netty入门学习小册https://juejin.im/book/5b4bc28bf265da0f60130116)
在一家幼儿园里,小朋友有上厕所的需求,小朋友都太小以至于你要问他要不要上厕所,他才会告诉你。幼儿园一共有 100 个小朋友,有两种方案可以解决小朋友上厕所的问题:
每个小朋友配一个老师。每个老师隔段时间询问小朋友是否要上厕所,如果要上,就领他去厕所,100 个小朋友就需要 100 个老师来询问,并且每个小朋友上厕所的时候都需要一个老师领着他去上,这就是IO模型,一个连接对应一个线程。
所有的小朋友都配同一个老师。这个老师隔段时间询问所有的小朋友是否有人要上厕所,然后每一时刻把所有要上厕所的小朋友批量领到厕所,这就是 NIO 模型,所有小朋友都注册到同一个老师,对应的就是所有的连接都注册到一个线程,然后批量轮询。
这就是 NIO 模型解决线程资源受限的方案,实际开发过程中,我们会开多个线程,每个线程都管理着一批连接,相对于 IO 模型中一个线程管理一条连接,消耗的线程资源大幅减少。
但是传统的Nio网络编程是很麻烦的一件事,想去了解的自行百度,这里不过多赘述。
那netty到底是什么?官方解释:Netty 是一个异步事件驱动的网络应用框架,用于快速开发可维护的高性能服务器和客户端。netty就是一个对Jdk的Nio进行封装的一个框架,和其他框架一样,目的就是让你用得爽。
webSocket
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
(注意:如果对于协议很了解的话,这里也可以使用自己写的协议,但是对于中小型企业或者非专门做即时聊天的企业来说,这是得不偿失的)
这里主要注意几个大坑,包括后面也会说这个问题
1、返回对象的包裹
//第一个坑,这里整合了webSocket对象后一定要用TextWebSocketFrame或者其他WebSocketFrame对象进行包裹
public void sendWebSocket(Channel channel, String res) {
if (channel != null && channel.isActive()) {
channel.writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(res)));
}
}
2、跨域问题的处理,如果跨域没有处理,一切等于0,这个坑一定要注意
/**
* Http返回
*
* @param ctx
* @param request
* @param response
*/
private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest request, FullHttpResponse response) {
// 返回应答给客户端
if (response.status().code() != 200) {
ByteBuf buf = Unpooled.copiedBuffer(response.status().toString(), CharsetUtil.UTF_8);
response.content().writeBytes(buf);
buf.release();
//允许跨域访问 设置头部信息
response.headers().