java基于netty NIO的简单聊天室的实现

一、为何要使用netty开发

由于之前已经用Java中的socket写过一版简单的聊天室,这里就不再对聊天室的具体架构进行细致的介绍了,主要关注于使用netty框架重构后带来的改变。对聊天室不了解的同学可以先看下我的博客(《JAVA简单聊天室的实现》)

本篇博客所使用的netty版本为4.1.36,完整工程已上传到Github(https://github.com/Alexlingl/Chatroom),其中lib文件夹下有相应的netty jar包和source包,自行导入即可。

1、为何要重构

之前的聊天室是基于Java原生socket实现的,socket的处理机制属于BIO模型,也就是阻塞IO模型。对于每一个客户端的连接我们都需要启动一个线程来处理,并且该线程会一直阻塞在读取用户数据上面。如此一来,一旦有大量的客户端并发连接我们的服务器,服务器将难以承受。之前用JMeter测试过,单纯使用Java原生socket开发的服务器所能支持的最大并发在2300左右。虽然采用线程池的策略可以在一定程度上提升最大并发数,但也无法超过1W。因此我们需要对其进行重构,使其能够具有更高的性能。

2、为何使用netty框架

使用netty框架主要还是为了提升代码的开发速度,并且减少代码维护成本。使用netty框架开发的程序在复杂度上比使用Java原生NIO类库开发的要小很多。具体可以看下我之前关于解决C10k问题的系列文章,里面有具体的代码。

3、为何netty框架只实现了NIO而没有AIO

前面在解决C10问题时,探究过NIO和AIO的区别,并且使用Java所提供的类库实现了两个小程序,理论上来说AIO性能明显要比NIO高,那为什么netty使用了NIO而不是AIO呢?

官方说法如下:

We obviously did not consider Windows as a serious platform so far, and that's why we were neglecting NIO.2 AIO API which was implemented using IOCP on Windows. (On Linux, it wasn't any faster because it was using the same OS facility - epoll.)

大意就是,windows上面有IOCP来支持AIO的实现,因此AIO的性能会比NIO好。而Linux上面不管是NIO还是AIO,底层都是用epoll实现的,性能差距不大。然而当下绝大部分的服务器还是建立在Linux上,因此没必要使用AIO(使用AIO反而会增加代码的复杂度,增大维护成本)。

二、基于netty NIO的处理模型

1、服务器的类关系

(1)、SubreqServer:创建两个NIO线程组,一个用来监听处理客户端的连接请求,一个用来监听客户端的消息。同时实例化一个ServerBootStrap启动类的对象来启动两个NIO线程组,并且配置必要的参数。
(2)、ChannelInitializer:初始化SocketChannel管道的各项参数,主要有指定解码器和编码器,并指明管道的处理类
(3)、SubreqServerHandler:SocketChannel管道的处理类,负责处理来自客户端的消息

2、客户端的类关系

(1)、SubreqClient:创建一个NIO线程组,用来监听客户端的消息。同时实例化一个ServerBootStrap启动类的对象来启动这个NIO线程组,并且配置必要的参数。最后让主程序阻塞在监听客户端的键盘输入。
(2)、ChannelInitializer:初始化SocketChannel管道的各项参数,主要有指定解码器和编码器,并指明管道的处理类
(3)、SubreqClientHandler:SocketChannel管道的处理类,负责处理来自服务器的消息

三、所涉及类库的源码解读

1、ChannelHandlerAdapter(用来对channel的注册和注销做出反应的类):

(1)、功能

用来实现当用户上线或下线时,通知其他在线的用户。

(2)、类定义

它是ChannelHandler的框架实现

(3)、HandlerAdded()和HandlerRemoved()方法

当有一个channel被注册后将会调用HandlerAdded(),而当有一个channel被注销后将调用HandlerRemoved()方法。并且根据注释我们知道,这两个方法默认不做任何处理,它希望由继承的子类自己去写相应的处理实现。

2、SimpleChannelInboundHandler(用来对接收的消息做出反应)

(1)、功能

用来实现当接收到消息时做出相应反应,如果是服务端,那么将当前消息转发给其他在线的客户端;如果是客户端,就将消息简单地打印出来。

(2)、类定义

这个类是一个泛型类,它只能用于处理一种具体类型的消息。注释中给出一种使用方法,StringHandler继承了SimpleChannelInboundHandler,并且指定泛型变量为String,因此这个继承类只能用于String类型消息的处理。

(2)、channelRead(ChannelHandlerContext ctx, Object msg)

这个方法主要用来处理类型为Object的消息,也就是所有消息。

首先当接收到msg时,先使用accpetInboundMessage()方法来判断该消息是否可以处理。我们来看下该方法的实现。

如果接收到的这个消息应该被处理就返回true,如果它应当被传到ChannelPipeLine的下一个ChannelInboundHandler就返回false。

我们继续来看下ChannelRead()方法。如果接受到的消息可以处理时。它将对消息进行强制转化,将其转为I,并且调用channelRead0()。它是一个抽象类,因此我们在继承SimpleChannelInboundHandler类时,需要根据自己的实际去实现这个方法。

四、关键的代码实现

1、server端

(1)、SubreqServer类

package nettyserverv1;

import java.util.ArrayList;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.serialization.ClassResolvers;
import io.netty.handler.codec.serialization.ObjectDecoder;
import io.netty.handler.codec.serialization.ObjectEncoder;

/**
 * Created by linguolong on 2019/05/08.
 * Chatroom server built using netty framework
 */

public class SubreqServer {
	//保存已注册的用户信息
	public static ArrayList<UserInfo> userlist = new ArrayList<UserInfo>();
	//自动生成注册用户
	static{
		for(int i=0;i<10;i++){
			UserInfo user=new UserInfo();
			user.setUserID("123"+i);
			user.setUserName("user"+i);
			user.setPassword("pwd"+i);
			userlist.add(user);
		}
	}

 public void bind(int port) throws Exception{
  //配置服务端NIO 线程组
  EventLoopGroup boss = new NioEventLoopGroup();
  EventLoopGroup worker = new NioEventLoopGroup();

  ServerBootstrap server = new ServerBootstrap();

  try {
   server.group(boss, worker)
     .channel(NioServerSocketChannel.class)
     .option(ChannelOption.SO_BACKLOG, 100)
     .childHandler(new ChannelInitializer<SocketChannel>() {
      @Override
      protected void initChannel(SocketChannel ch) throws Exception {
       /**
        * 解码器: 构造器传入了两个参数:
        * #1 单个对象序列化后最大字节长度,这是设置是1M;
        * #2 类解析器: weakCachingConcurrentResolver创建线程安全的WeakReferenceMa对类加载器进行缓存,
        * 支持多线程并发访问,当虚拟机内存不足时,会释放缓存中的内存,防止内存泄漏.
        */
       ch.pipeline().addLast(new ObjectDecoder(1024*1024,
         ClassResolvers.weakCachingConcurrentResolver(this.getClass().getClassLoader())) );
       ch.pipeline().addLast(new ObjectEncoder());
       ch.pipeline().addLast(new SubreqServerHandler());
      }
     });

   System.out.println("Start the server success");
   //绑定端口, 同步等待成功
   ChannelFuture future = server.bind(port).sync();

   //等待服务端监听端口关闭
   future.channel().closeFuture().sync();
  } finally {
   //优雅关闭 线程组
   boss.shutdownGracefully();
   worker.shutdownGracefully();
  }
 }

 public static void main(String[] args) {
  SubreqServer server = new SubreqServer();
  try {
   server.bind(18888);
  } catch (Exception e) {
   e.printStackTrace();
  }
 }
}

(2)、SubreqServerHandler类

package nettyserverv1;

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.channel.group.ChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;

/**
 * Created by linguolong on 2019/05/08.
 * Chatroom client built using netty framework
 */

public class SubreqServerHandler extends SimpleChannelInboundHandler<String>{
	//新建一个channelGroup,用于存放连接的channel
	public static ChannelGroup online_channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

	@Override
	public void handlerRemoved(ChannelHandlerContext ctx){
  Channel leave_channel = ctx.channel();
  for (Channel channel : online_channels) {
   if (channel != leave_channel){
    channel.writeAndFlush("[用户 " + leave_channel.remoteAddress() + "]下线了!\n");
   }
  }
  System.out.println(ctx.channel().id()+"下线了");
		//把刚下线的channel移除出在线用户队列
		online_channels.remove(leave_channel);
	}

 @Override
 public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
 	//判断用户是否已经登录
 	if(!check_login(ctx)){
 		//未登录则进行登录验证
 		check_identity(ctx,msg);
 	}else{
 		//已登录则进行消息转发
   Channel coming_channel = ctx.channel();
   for (Channel channel : online_channels) {
    if (channel != coming_channel){
     channel.writeAndFlush("[用户 " + coming_channel.remoteAddress() + "]: " + msg );
    } else {
     channel.writeAndFlush("[我]: " + msg);
    }
   }
 	}

 }

 //用户信息验证,检查用户ID和密码是否正确
 public void check_identity(ChannelHandlerContext ctx, Object msg){
 	UserInfo req = (UserInfo) msg;
  System.out.println("service receive client login req :{"+ req.toString() +"}");
  boolean login_flag = false;
  for(int i=0;i<SubreqServer.userlist.size();i++){
  	 if( SubreqServer.userlist.get(i).getUserID().equalsIgnoreCase(req.getUserID())&&(SubreqServer.userlist.get(i).getPassword().equals(req.getPassword()))){
     login_flag=true;
    }
  }
  if(login_flag){
  	System.out.println("账号"+req.getUserID()+"登录成功");
   ctx.writeAndFlush("您已登录成功~\n");
   //将当前的通道加入在线队列中
   online_channels.add(ctx.channel());
  }
  else{
  	System.out.println("账号"+req.getUserID()+"登录失败");
   ctx.writeAndFlush("登录失败!");
   //关闭连接
  	ctx.close();
  	online_channels.remove(ctx.channel());
  }
 }

 //判断用户是否已经在线
 public boolean check_login(ChannelHandlerContext ctx){
 	boolean online_flag = false;
 	for(int i=0;i<online_channels.size();i++){
 		if(online_channels.contains(ctx.channel())){
 			online_flag = true;
 		}
 	}
		return online_flag;
 }

 @Override
 public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
  //释放资源
  ctx.close();
 }

	@Override
	public void channelInactive(ChannelHandlerContext ctx) throws Exception {

	}

	//ChannelRead0()只能处理类型为String的消息,因此我们这里不能用ChannelRead0()这个方法,这里的第二个参数类型使用了泛型
	@Override
	protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {

	}
}

(3)、用户信息UserInfo类

package nettyserverv1;

import java.io.Serializable;

/**
 * Created by linguolong on 2019/05/08.
 * Chatroom User Infomation
 */

public class UserInfo implements Serializable{
 private String userID;
 private String userName;
 private String password;

 public String getUserID() {
  return userID;
 }

 public void setUserID(String userID) {
  this.userID = userID;
 }

 public String getPassword() {
  return password;
 }

 public void setPassword(String password) {
  this.password = password;
 }

 public String getUserName() {
  return userName;
 }

 public void setUserName(String userName) {
  this.userName = userName;
 }

 @Override
 public String toString() {
  return "SubscribeReq: [messageID]:"+ userID + " [userName]:" +userName
    + " [password]:" +password;
 }

}

2、Client端

(1)、SubreqClient类

package nettyclientv1;

import java.io.BufferedReader;
import java.io.InputStreamReader;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.serialization.ClassResolvers;
import io.netty.handler.codec.serialization.ObjectDecoder;
import io.netty.handler.codec.serialization.ObjectEncoder;

/**
 * Created by linguolong on 2019/05/08.
 * Chatroom client built using netty framework
 */

public class SubreqClient {

 public void connect(int port, String host) throws Exception{
  //配置客户端NIO 线程组
  EventLoopGroup group = new NioEventLoopGroup();
  Bootstrap client = new Bootstrap();

  try {
   client.group(group)
     .channel(NioSocketChannel.class)
     .option(ChannelOption.TCP_NODELAY, true)
     .handler(new ChannelInitializer<SocketChannel>() {
      @Override
      protected void initChannel(SocketChannel ch) throws Exception {
      	//添加解码器
       ch.pipeline().addLast(new ObjectDecoder(1024,
         ClassResolvers.weakCachingConcurrentResolver(this.getClass().getClassLoader())) );
       //添加编码器
       ch.pipeline().addLast(new ObjectEncoder());
       ch.pipeline().addLast(new SubreqClientHandler());
      }
     });

   //异步获取当前已连接的channel
   Channel now_channel = client.connect(host,port).sync().channel();
   BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));

   //异步等待客户端连接端口关闭
//   now_channel.closeFuture().sync();

   //使客户端一直处于输入状态,直到读取到"bye"
   String message = " ";
   while (true) {
   	//读到bye时退出
   	if(message.equals("bye")) break;
   	message = reader.readLine();
    now_channel.writeAndFlush(message+"\n");
   }

   //读到了"bye"字符串,主动断开连接
   now_channel.close();
  } finally {
   //优雅关闭 线程组
   group.shutdownGracefully();
  }
 }

 public static void main(String[] args) {
  SubreqClient client = new SubreqClient();
  try {
   client.connect(18888, "127.0.0.1");
  } catch (Exception e) {
   e.printStackTrace();
  }
 }
}

(2)、SubreqClientHandler类

package nettyclientv1;

import nettyserverv1.UserInfo;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

/**
 * Created by linguolong on 2019/05/08.
 * Chatroom client built using netty framework
 */

public class SubreqClientHandler extends ChannelInboundHandlerAdapter{

 public SubreqClientHandler() {
 }

 /**
  * @param ctx
  * @throws Exception
  */
 @Override
 public void channelActive(ChannelHandlerContext ctx) throws Exception {
 	ctx.writeAndFlush(subReq("1231","user1","pwd1"));
  ctx.flush();
 }

	private UserInfo subReq(String id,String userName,String password){
  UserInfo req = new UserInfo();
  req.setUserID(id);
  req.setUserName(userName);
  req.setPassword(password);

  return req;
 }

 @Override
 public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
 	System.out.print(msg.toString());

 }

 @Override
 public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
  ctx.close();
 }
}

五、说明

1、登录功能的实现:当客户端刚连接上服务器时,便构造一个UserInfo对象,对对象进行编码后发送给服务器。服务器接收到后对其进行解码,验证相应的账户ID和密码是否正确。

到此这篇关于java基于netty NIO的简单聊天室的实现的文章就介绍到这了,更多相关java简单聊天室内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • java编程实现多人聊天室功能

    本文实例为大家分享了java实现多人聊天室的具体代码,供大家参考,具体内容如下 程序源代码及运行截图: server.java //server.java package Socket; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.ServerSocket; impor

  • Java编程实现基于TCP协议的Socket聊天室示例

    本文实例讲述了Java编程实现基于TCP协议的Socket聊天室.分享给大家供大家参考,具体如下: 这里使用Socket套接字进行编程,完成的是基于TCP可靠服务实现服务器与客户端的双通信. Server服务器端: package com.han; import java.awt.Container; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.Win

  • java利用Socket实现聊天室功能实例

    最近研究了下Java socket通信基础,利用代码实现了一个简单的多人聊天室功能,现把代码共享下,希望能帮到有兴趣了解的人. 目录结构: ChatClient: package com.panda.chat; import java.awt.*; import java.awt.event.*; import java.io.*; import java.net.*; @SuppressWarnings("serial") public class ChatClient extend

  • Java NIO实战之聊天室功能详解

    本文实例讲述了Java NIO实战之聊天室功能.分享给大家供大家参考,具体如下: 在工作之余花了两个星期看完了<Java NIO>,总体来说这本书把NIO写的很详细,没有过多的废话,讲的都是重点,只是翻译的中文版看的确实吃力,英文水平太低也没办法,总算也坚持看完了.<Java NIO>这本书的重点在于第四章讲解的"选择器",要理解透还是要反复琢磨推敲:愚钝的我花了大概3天的时间才将NIO的选择器机制理解透并能较熟练的运用,于是便写了这个聊天室程序. 下面直接上代

  • Java continue break制作简单聊天室程序

    Java continue break 制作简单聊天室程序,屏蔽不文明语言,显示每句话聊天时间 package com.swift; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Scanner; public class ChatWithBreakContinue { public static void main(String[] args) { Scanner scan = new Sc

  • Java Socket通信之聊天室功能

    本文实例为大家分享了Java Socket聊天室功能的具体代码,供大家参考,具体内容如下 Client.java import java.io.*; import java.net.*; import java.util.*; public class Client { public int port = 8083; Socket socket = null; public static void main(String[] args) { new Client(); //开始执行 } publ

  • Java GUI编程实现在线聊天室

    引言 综合应用Java的GUI编程和网络编程,实现一个能够支持多组用户同时使用的聊天室软件.该聊天室具有比较友好的GUI界面,并使用C/S模式,支持多个用户同时使用,用户可以自己选择加入或者创建房间,和房间内的其他用户互发信息(文字和图片) 主要功能 客户端的功能主要包括如下的功能: 选择连上服务端 显示当前房间列表(包括房间号和房间名称) 选择房间进入 多个用户在线群聊 可以发送表情(用本地的,实际上发送只发送表情的代码) 退出房间 选择创建房间 房间里没人(房主退出),导致房间解散 显示系统

  • java聊天室的实现代码

    本文实例为大家分享了java实现聊天室的具体代码,供大家参考,具体内容如下 聊天室界面: 源码: public class ClientFrame extends Frame { private TextField textFieldContent = new TextField(); private TextArea textAreaContent = new TextArea(); private Socket socket = null; private OutputStream out

  • java实现简单聊天室单人版

    本文实例为大家分享了java实现简单聊天室的具体代码,供大家参考,具体内容如下 先整理下思路: 1.创建一个通信服务端,传入端口号和相关的流后等待客户端连接,并初始化图形界面. 2.创建一个JFrame,用于写出聊天的界面,这里界面名称由其他类传入. 3.把客户端创建的方法写进JFrame(当然这里很粗糙的方法) 4.设置按钮的监听事件,发送消息和离线功能 首先创建一个服务端的类 import java.io.IOException; import java.net.ServerSocket;

  • Java NIO Selector用法详解【含多人聊天室实例】

    本文实例讲述了Java NIO Selector用法.分享给大家供大家参考,具体如下: 一.Java NIO 的核心组件 Java NIO的核心组件包括:Channel(通道),Buffer(缓冲区),Selector(选择器),其中Channel和Buffer比较好理解 简单来说 NIO是面向通道和缓冲区的,意思就是:数据总是从通道中读到buffer缓冲区内,或者从buffer写入到通道中. 关于Channel 和 Buffer的详细讲解请看:Java NIO 教程 二.Java NIO Se

随机推荐