Java NIO实现的C/S模式多人聊天工具

小弟初学NIO,做了个控制台聊天工具,不知道代码写的如何,望大神们批评指点。

服务器端,两个线程,一个处理客户端请求和转发消息,另一个处理服务器管理员指令,上代码:

package kindz.onlinechat;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.CancelledKeyException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.ClosedSelectorException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

public class Server {

    private static List<SocketChannel> clientList = new LinkedList<SocketChannel>();// 客户端列表
    private static Selector clientManager = null;// 通道管理器
    private static ServerSocketChannel server = null;// 服务器通道
    private static ByteBuffer buff = ByteBuffer.allocate(1500);// 缓冲器
    private static int port = 3333;

    public static void main(String[] args) {
	if (args.length > 0) {
	    try {
		port = Integer.parseInt(args[0]);
	    } catch (NumberFormatException e) {
		System.out.println("端口号只能为数字");
		return;
	    }
	}

	try {
	    // 初始化失败直接退出
	    if (!init())
		return;

	    while (clientManager.isOpen()) {
		select();

		// 获取就绪的key列表
		Set<SelectionKey> keys = clientManager.selectedKeys();

		// 遍历事件并处理
		Iterator<SelectionKey> it = keys.iterator();
		while (it.hasNext()) {
		    SelectionKey key = it.next();
		    // 判断key是否有效
		    if (!key.isValid()) {
			it.remove();// 要移除
			continue;
		    }

		    if (key.isAcceptable()) {// 有请求
			accept(key);
		    } else if (key.isReadable()) {// 有数据
			broadcast(key);
		    }

		    it.remove();
		}
	    }
	} catch (ClosedSelectorException | CancelledKeyException e) {// 一定是其他线程关闭了管理器
	} finally {
	    try {
		if (clientManager != null)
		    clientManager.close();
	    } catch (IOException e) {
	    }

	    try {
		if (server != null)
		    server.close();
	    } catch (IOException e) {
	    }

	    closeAll();
	    System.out.println("服务器已停止");
	}
    }

    // 初始化

    private static boolean init() {
	System.out.println("服务器启动中...");

	try {
	    // 获取管理器
	    clientManager = Selector.open();
	} catch (IOException e) {
	    System.out.println("服务器启动失败,原因:通道管理器无法获取");
	    return false;
	}

	try {
	    // 打开通道
	    server = ServerSocketChannel.open();
	} catch (IOException e) {
	    System.out.println("服务器启动失败,原因:socket通道无法打开");
	    return false;
	}

	try {
	    // 绑定端口
	    server.socket().bind(new InetSocketAddress(port));
	} catch (IOException e) {
	    System.out.println("服务器启动失败,原因:端口号不可用");
	    return false;
	}

	try {
	    // 设置成非阻塞模式
	    server.configureBlocking(false);
	} catch (IOException e) {
	    System.out.println("服务器启动失败,原因:非阻塞模式切换失败");
	    return false;
	}

	try {
	    // 注册到管理器中,只监听接受连接事件
	    server.register(clientManager, SelectionKey.OP_ACCEPT);
	} catch (ClosedChannelException e) {
	    System.out.println("服务器启动失败,原因:服务器通道已关闭");
	    return false;
	}

	Thread service = new Thread(new ServerService(clientManager));// 提供管理员指令服务线程
	service.setDaemon(true);// 设置为后台线程
	service.start();

	System.out.println("服务器启动成功");
	return true;
    }

    // 等待事件
    private static void select() {
	try {
	    // 等待事件
	    clientManager.select();
	} catch (IOException e) {
	    // 忽略未知的异常
	}
    }

    // 此方法获取请求的socket通道并添加到客户端列表中,当然还要注册到管理器中
    private static void accept(SelectionKey key) {
	SocketChannel socket = null;

	try {
	    // 接受请求的连接
	    socket = ((ServerSocketChannel) key.channel()).accept();
	} catch (IOException e) {// 连接失败
	}

	if (socket == null)
	    return;

	SocketAddress address = null;

	try {
	    address = socket.getRemoteAddress();
	    // 注册
	    socket.configureBlocking(false);
	    socket.register(clientManager, SelectionKey.OP_READ);
	} catch (ClosedChannelException e) {// 注册失败
	    try {
		if (socket != null)
		    socket.close();
	    } catch (IOException e1) {
	    }
	    return;
	} catch (IOException e) {
	    try {
		if (socket != null)
		    socket.close();
	    } catch (IOException e1) {
	    }
	    return;
	}
	// 添加到客户端列表中
	clientList.add(socket);
	System.out.println("主机" + address + "连接到服务器");
    }

    // 此方法接收数据并发送个客户端列表的每一个人
    private static void broadcast(SelectionKey key) {
	SocketChannel sender = (SocketChannel) key.channel();
	// 方法结束不清理
	buff.clear();

	int status = -1;
	try {
	    // 读取数据
	    status = sender.read(buff);
	} catch (IOException e) {// 未知的io异常
	    status = -1;
	}

	if (status <= 0) {// 异常断开连接,并移除此客户端
	    remove(sender);
	    return;
	}

	// 发送给每一个人
	for (SocketChannel client : clientList) {
	    // 除了他或她自己
	    if (client == sender)
		continue;
	    buff.flip();
	    try {
		client.write(buff);
	    } catch (IOException e) {// 发送失败,移除此客户端
		remove(client);
	    }
	}
    }

    private static void remove(SocketChannel client) {
	SocketAddress address = null;// 存储主机地址信息

	clientList.remove(client);// 从列表中移除

	try {
	    address = client.getRemoteAddress();//获取客户端地址信息
	} catch (IOException e1) {
	}

	try {
	    client.close();// 关闭连接
	} catch (IOException e1) {
	}
	client.keyFor(clientManager).cancel();// 反注册
	System.out.println("与主机" + address + "断开连接");
    }

    // 关闭列表中全部通道
    private static void closeAll() {
	for (SocketChannel client : clientList) {
	    try {
		if (client != null)
		    client.close();
	    } catch (IOException e) {
	    }
	}
    }
}

客户端也是两个线程,一个循环等待接收消息,另一个处理用户输入以及发送:

package kindz.onlinechat;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Scanner;

public class Client implements Runnable {

    private static String ip = null;
    private static String name = null;
    private static String serverHost="127.0.0.1";//服务器地址
    private static int port=3333;//服务器端口号
    private static SocketChannel socket = null;// 与服务器连接通道

    public static void main(String[] args) {
	if(args.length>0){

	    if (args[0].indexOf(':') == -1) {
		System.err.println("目标地址格式不正确");
		return;
	    }

	    serverHost = args[0].split(":")[0];
	    try {
		port = Integer.parseInt(args[0].split(":")[1]);
	    } catch (NumberFormatException e) {
		System.err.println("端口号只能为数字");
		return;
	    }
	}

	try {
	    // 初始化失败退出程序
	    if (!init())
		return;

	    ByteBuffer buff = ByteBuffer.allocate(1500);// 字节缓冲器

	    while (socket.read(buff) != -1) {// 读取信息
		String msg = new String(buff.array(), 0, buff.position());// 转成字符串
		buff.clear();// 清理
		System.out.println(msg);
	    }

	} catch (IOException e) {
	    System.out.println("与服务器断开连接");
	} finally {
	    close();
	    System.out.println("程序已退出");
	}
    }

    // 输入线程
    @SuppressWarnings("resource")
    public void run() {
	try {
	    Scanner sc = new Scanner(System.in);
	    // 循环等待输入
	    while (sc.hasNextLine()) {
		String msg = sc.nextLine();

		if (".exit".equals(msg)){
		    socket.write(ByteBuffer.wrap((name+"-"+ip+"下线了").getBytes()));// 发送下线信息
		    break;
		}

		msg = name + "-" + ip + ":" + msg;
		socket.write(ByteBuffer.wrap(msg.getBytes()));
	    }
	} catch (IOException e) {
	    System.out.println("与服务器断开连接");
	} finally {
	    close();
	}
    }

    // 初始化程序
    private static boolean init() {
	System.out.println("正在连接至服务器...");
	try {
	    socket = SocketChannel
		    .open(new InetSocketAddress(serverHost, port));// 打开通道
	} catch (IOException e) {
	    System.out.println("无法连接到服务器");
	    return false;
	}
	System.out.println("已连接至服务器");

	try {
	    InetAddress address = InetAddress.getLocalHost();// 获取本机网络信息
	    ip = address.getHostAddress();// 本机ip
	    name = address.getHostName();// 主机名
	    socket.write(ByteBuffer.wrap((name+"-"+ip+"上线了").getBytes()));// 发送上线信息
	} catch (IOException e) {
	    System.out.println("网络异常");
	    return false;
	}

	Thread thread = new Thread(new Client());
	thread.setDaemon(true);// 设置后台线程
	thread.start();
	return true;
    }

    // 关闭通道
    private static void close() {
	try {
	    if (socket != null)
		socket.close();
	} catch (IOException e) {
	}
    }
}

写完感觉还是挺简单的,但是本人一直从事java web开发,异常处理做的比较少,不知道我这个处理的怎么样。

由于没有那么多好基友帮忙测试,我还写了虚拟客户端来模拟好基友:

package kindz.onlinechat;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Random;
import java.util.concurrent.TimeUnit;

public class VClient implements Runnable {

    private String ip = null;
    private String name = null;
    private String serverHost = "127.0.0.1";// 服务器ip地址
    private int port = 3333;// 服务器端口号
    private SocketChannel socket = null;// 与服务器连接通道
    private static String[] msgs = {
	    "大家好",
	    "好困啊",
	    "今天该干什么啊",
	    "我这任务好多,先不聊了",
	    "jQuery是继prototype之后又一个优秀的Javascript库。它是轻量级的js库 ,它兼容CSS3,还兼容各种浏览器(IE 6.0+, FF 1.5+, Safari 2.0+, Opera 9.0+),jQuery2.0及后续版本将不再支持IE6/7/8浏览器。jQuery使用户能更方便地处理HTML(标准通用标记语言下的一个应用)、events、实现动画效果,并且方便地为网站提供AJAX交互。jQuery还有一个比较大的优势是,它的文档说明很全,而且各种应用也说得很详细,同时还有许多成熟的插件可供选择。jQuery能够使用户的html页面保持代码和html内容分离,也就是说,不用再在html里面插入一堆js来调用命令了,只需要定义id即可[8]。",
	    "谁那有咖啡", "谁有时间帮我去取个快递", ".exit" };
    private Random random=new Random();

    public VClient(String serverHost, int port) {
	this.serverHost = serverHost;
	this.port = port;
    }

    public void run() {
	try {
	    // 初始化失败退出程序
	    if (!init())
		return;

	    ByteBuffer buff = ByteBuffer.allocate(1500);// 字节缓冲器

	    while (!Thread.interrupted()&&socket.read(buff) != -1) {// 读取信息,中断退出
		String msg = new String(buff.array(), 0, buff.position());// 转成字符串
		buff.clear();// 清理
		System.out.println(msg);
	    }

	} catch (IOException e) {
	    System.out.println("与服务器断开连接");
	} finally {
	    close();
	    System.out.println("程序已退出");
	}
    }

    // 初始化程序
    private boolean init() {
	System.out.println("正在连接至服务器...");
	try {
	    socket = SocketChannel
		    .open(new InetSocketAddress(serverHost, port));// 打开通道
	} catch (IOException e) {
	    System.out.println("无法连接到服务器");
	    return false;
	}
	System.out.println("已连接至服务器");

	try {
	    InetAddress address = InetAddress.getLocalHost();// 获取本机网络信息
	    ip = address.getHostAddress();// 本机ip
	    name = address.getHostName();// 主机名
	    socket.write(ByteBuffer.wrap((name + "-" + ip + "上线了").getBytes()));// 发送上线信息
	} catch (IOException e) {
	    System.out.println("网络异常");
	    return false;
	}

	Thread thread = new Thread(new Daemon());// 私有内部类
	thread.setDaemon(true);// 设置后台线程
	thread.start();
	return true;
    }

    // 关闭通道
    private void close() {
	try {
	    if (socket != null)
		socket.close();
	} catch (IOException e) {
	}
    }

    // 私有内部类、守护线程、输出用
    private class Daemon implements Runnable {

	public void run() {
	    try {
		// 自动循环发送消息
		while (true) {
		    String msg = msgs[random.nextInt(msgs.length)];// 随便拿一个写好的信息

		    if (".exit".equals(msg)) {
			socket.write(ByteBuffer.wrap((name + "-" + ip + "下线了")
				.getBytes()));// 发送下线信息
			break;
		    }

		    msg = name + "-" + ip + ":" + msg;
		    socket.write(ByteBuffer.wrap(msg.getBytes()));

		    TimeUnit.SECONDS.sleep(random.nextInt(9) + 2);//模拟用户输入过程,2-10秒,平均6秒发一次
		}
	    } catch (IOException e) {
		System.out.println("与服务器断开连接");
	    } catch (InterruptedException e) {
		System.out.println("与服务器断开连接");
	    } finally {
		close();
	    }
	}
    }

}

虚拟客户端只是个线程,我给它配了个管理器:

package kindz.onlinechat;

import java.util.Random;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class VManager implements Runnable {

    private static String serverHost = "127.0.0.1";
    private static int port = 3333;
    private static int num = 3;// 初始化虚拟客户端数目
    private static int min = 5;// 最小虚拟客户端数目
    private static int max = 15;// 最大虚拟客户端数目
    private static Random random=new Random();

    @SuppressWarnings("resource")
    public static void main(String[] args) {

	if (args.length > 0) {

	    if (args[0].indexOf(':') == -1) {
		System.err.println("目标地址格式不正确");
		return;
	    }

	    serverHost = args[0].split(":")[0];
	    try {
		port = Integer.parseInt(args[0].split(":")[1]);
	    } catch (NumberFormatException e) {
		System.err.println("端口号只能为数字");
		return;
	    }
	}

	if (args.length > 1) {
	    try {
		num = Integer.parseInt(args[1]);
	    } catch (NumberFormatException e) {
		System.err.println("初始化数目只能为数字");
		return;
	    }
	}

	if (args.length > 2) {
	    try {
		min = Integer.parseInt(args[2]);
	    } catch (NumberFormatException e) {
		System.err.println("最小数目只能为数字");
		return;
	    }
	}

	if (args.length > 3) {
	    try {
		max = Integer.parseInt(args[3]);
	    } catch (NumberFormatException e) {
		System.err.println("最大数目只能为数字");
		return;
	    }
	}

	if (max < num) {
	    System.err.println("初始化数量不能大于最大数量");
	    return;
	}

	if (max < min) {
	    System.err.println("最小数量不能大于最大数量");
	    return;
	}

	Thread manager = new Thread(new VManager());
	manager.start();

	Scanner sc = new Scanner(System.in);

	String arg = null;// 指令
	while (sc.hasNextLine()) {
	    arg = sc.nextLine();// 输入指令

	    if ("shutdown".equals(arg)) {
		manager.interrupt();
		break;
	    } else {
		System.out.println("未知的指令");
	    }
	}
    }

    public void run() {
	ExecutorService manager = Executors.newFixedThreadPool(max);// 线程池管理器

	// 初始化几个客户端
	for (int i = 0; i < num; i++) {
	    manager.execute(new VClient(serverHost, port));
	}

	try {
	    int interval=(int)(2*48000.0/min)+1;//用户上线间隔时间范围
	    while (!Thread.interrupted()) {
		TimeUnit.MILLISECONDS.sleep(random.nextInt(interval));// 用户大概48秒会下线,尽量保持最少用户个数
		manager.execute(new VClient(serverHost, port));
	    }
	} catch (InterruptedException e) {
	} finally {
	    System.out.println("正在停止所有客户端...");
	    manager.shutdownNow();// 中断所有客户端
	}
    }
}

虚拟客户端大概平均6秒发一次消息,每次下线的几率是1/8,所以虚拟客户端大概在上线48秒左右的时候会下线,因此想要保证(只能是尽量保证)最小虚拟客户端数量,只要保证48秒内上线固定数量的用户即可,不过在这里,虚拟客户端增加的频率也是随机的,感觉更真实些。

本人想转Java底层的工作,因此在努力学习中,望大神们能够为小弟点出不对的地方。

想学习Java NIO的童鞋也可以借鉴一下我的代码。

愿与CSDN上的Coder们一起在技术的道路上飞奔。

时间: 2024-08-07 18:33:10

Java NIO实现的C/S模式多人聊天工具的相关文章

java NIO的多路复用及reactor模式【转载】

本文转载自:http://www.blogjava.net/hello-yun/archive/2012/10/17/389729.html java nio从1.4版本就出现了,而且依它优异的性能赢得了广大java开发爱好者的信赖.我很纳闷,为啥我到现在才接触,难道我不是爱好者,难道nio不优秀.经过长达半分钟的思考,我意识到:时候未到.以前总是写那些老掉牙的web程序,唉,好不容易翻身啦,现在心里好受多了.因为真不想自己到了30岁,还在说,我会ssh,会ssi,精通javascript,精通

Java NIO中的Glob模式详解

Java NIO中的Glob模式详解 作者:chszs,转载需注明.博客主页:http://blog.csdn.net/chszs 一.什么是Glob? 在编程设计中,Glob是一种模式,它使用通配符来指定文件名.例如:.java就是一个简单的Glob,它指定了所有扩展名为"java"的文件.Glob模式中广泛使用了两个通配符""和"?".其中星号表示"任意的字符或字符组成字符串",而问号则表示"任意单个字符&quo

JAVA NIO学习记录2-非阻塞式网络通信

一.阻塞与非阻塞 传统的IO 流都是阻塞式的.也就是说,当一个线程调用read() 或write() 时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务.因此,在完成网络通信进行IO 操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量客户端时,性能急剧下降. Java NIO 是非阻塞模式的.当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务.线程通常将非阻塞IO 的空闲时间用于在其他通道上执行I

reactor模式与java nio

?? Reactor是由Schmidt, Douglas C提出的一种模式,在高并发server实现中广泛採用. 改模式採用事件驱动方式,当事件出现时,后调用对应的事件处理代码(Event Handler). 这个模式是高并发server的基础.如nginx和lighttpd. 这两种对大并发,但每一个请求处理都非常快的场景非常适合. 通常的web訪问就是这个特点. 结构 包含了5个部分,当中handle和Synchronous Event Demultiplexer有OS实现. Handles

JAVA NIO non-blocking模式实现高并发服务器

JAVA NIO non-blocking模式实现高并发服务器 分类: JAVA NIO2014-04-14 11:12 1912人阅读 评论(0) 收藏 举报 目录(?)[+] Java自1.4以后,加入了新IO特性,NIO. 号称new IO. NIO带来了non-blocking特性. 这篇文章主要讲的是如何使用NIO的网络新特性,来构建高性能非阻塞并发服务器. 文章基于个人理解,我也来搞搞NIO.,求指正. 在NIO之前 服务器还是在使用阻塞式的java socket. 以Tomcat最

java nio

NIO 是java nonblocking(非阻塞) IO 的简称,在jdk1.4 里提供的新api .Sun 官方标榜的特性如下: 为所有的原始类型提供(Buffer)缓存支持.字符集编码解码解决方案. Channel :一个新的原始I/O 抽象. 支持锁和内存映射文件的文件访问接口. 提供多路(non-bloking) 非阻塞式的高伸缩性网络I/O . Java NIO非堵塞应用通常适用用在I/O读写等方面,我们知道,系统运行的性能瓶颈通常在I/O读写,包括对端口和文件的操作上,之前,在打开

JAVA NIO 内存映射(转载)

原文地址:http://blog.csdn.net/fcbayernmunchen/article/details/8635427 Java类库中的NIO包相对于IO 包来说有一个新功能是内存映射文件,日常编程中并不是经常用到,但是在处理大文件时是比较理想的提高效率的手段.本文我主要想结合操作系统中(OS)相关方面的知识介绍一下原理. 在传统的文件IO操作中,我们都是调用操作系统提供的底层标准IO系统调用函数 read().write() ,此时调用此函数的进程(在JAVA中即java进程)由当

Java NIO(六) Selector

Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件.这样,一个单独的线程可以管理多个channel,从而管理多个网络连接. 下面是本文所涉及到的主题列表: 为什么使用Selector? Selector的创建 向Selector注册通道 SelectionKey 通过Selector选择通道 wakeUp() close() 完整的示例 为什么使用Selector? 仅用单个线程来处理多个Channels的好处是,只需要更少的

Java NIO (二) 缓冲区(Buffer)

缓冲区(Buffer):一个用于特定基本数据类型的容器,由 java.nio 包定义的,所有缓冲区都是 Buffer 抽象类的子类. Java NIO 中的Buffer 主要用于和NIO中的通道(Channel)进行交互, 数据从通道(Channel)读入缓冲区(Buffer)或者从缓冲区(Buffer)写入通道(Channel).如下,我画的一个简图,Chanenl直接和数据源或者目的位置接触,Buffer作为中介这,从一个Channel中读取数据,然后将数据写入另一个Channel中. Bu