Java NIO实战之聊天室

简介:

在工作之余花了两个星期看完了《Java NIO》。整体来说这本书把NIO写的非常具体,没有过多的废话,讲的都是重点,仅仅是翻译的中文版看的确实吃力。英文水平太低也没办法,总算也坚持看完了。《Java NIO》这本书的重点在于第四章解说的“选择器”,要理解透还是要重复琢磨推敲。愚钝的我花了大概3天的时间才将NIO的选择器机制理解透并能较熟练的运用。于是便写了这个聊天室程序。

以下直接上代码。jdk1.5以上经过測试,能够支持多人同一时候在线聊天;

将下面代码拷贝到项目中便可执行。源代码下载地址:聊天室源代码

一、server端

package com.chat.server;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.Vector;

/**
 * 聊天室:服务端
 * @author zing
 * 
 */
public class ChatServer implements Runnable {

	//选择器
	private Selector selector;
	//注冊ServerSocketChannel后的选择键
	private SelectionKey serverKey;
	//标识是否执行
	private boolean isRun;
	//当前聊天室中的username称列表
	private Vector<String> unames;
	//时间格式化器
	SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

	/**
	 * 构造函数
	 * @param port 服务端监控的端口号
	 */
	public ChatServer(int port) {
		isRun = true;
		unames = new Vector<String>();
		init(port);
	}

	/**
	 * 初始化选择器和服务器套接字
	 * 
	 * @param port 服务端监控的端口号
	 */
	private void init(int port) {
		try {
			//获得选择器实例
			selector = Selector.open();
			//获得服务器套接字实例
			ServerSocketChannel serverChannel = ServerSocketChannel.open();
			//绑定端口号
			serverChannel.socket().bind(new InetSocketAddress(port));
			//设置为非堵塞
			serverChannel.configureBlocking(false);
			//将ServerSocketChannel注冊到选择器,指定其行为为"等待接受连接"
			serverKey = serverChannel.register(selector, SelectionKey.OP_ACCEPT);
			printInfo("server starting...");
		} catch (IOException e) {
			e.printStackTrace();
		}

	}

	@Override
	public void run() {
		try {
			//轮询选择器选择键
			while (isRun) {
				//选择一组已准备进行IO操作的通道的key,等于1时表示有这种key
				int n = selector.select();
				if (n > 0) {
					//从选择器上获取已选择的key的集合并进行迭代
					Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
					while (iter.hasNext()) {
						SelectionKey key = iter.next();
						//若此key的通道是等待接受新的套接字连接
						if (key.isAcceptable()) {
							//记住一定要remove这个key,否则之后的新连接将被堵塞无法连接服务器
							iter.remove();
							//获取key相应的通道
							ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
							//接受新的连接返回和client对等的套接字通道
							SocketChannel channel = serverChannel.accept();
							if (channel == null) {
								continue;
							}
							//设置为非堵塞
							channel.configureBlocking(false);
							//将这个套接字通道注冊到选择器,指定其行为为"读"
							channel.register(selector, SelectionKey.OP_READ);
						}
						//若此key的通道的行为是"读"
						if (key.isReadable()) {
							readMsg(key);
						}
						//若次key的通道的行为是"写"
						if (key.isWritable()) {
							writeMsg(key);
						}
					}
				}
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * 从key相应的套接字通道上读数据
	 * @param key 选择键
	 * @throws IOException
	 */
	private void readMsg(SelectionKey key) throws IOException {
		//获取此key相应的套接字通道
		SocketChannel channel = (SocketChannel) key.channel();
		//创建一个大小为1024k的缓存区
		ByteBuffer buffer = ByteBuffer.allocate(1024);
		StringBuffer sb = new StringBuffer();
		//将通道的数据读到缓存区
		int count = channel.read(buffer);
		if (count > 0) {
			//翻转缓存区(将缓存区由写进数据模式变成读出数据模式)
			buffer.flip();
			//将缓存区的数据转成String
			sb.append(new String(buffer.array(), 0, count));
		}
		String str = sb.toString();
		//若消息中有"open_",表示client准备进入聊天界面
		//client传过来的数据格式是"open_zing",表示名称为zing的用户请求打开聊天窗口
		//username称列表有更新,则应将username称数据写给每个已连接的client
		if (str.indexOf("open_") != -1) {//client连接服务器
			String name = str.substring(5);
			printInfo(name + " online");
			unames.add(name);
			//获取选择器已选择的key并迭代
			Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
			while (iter.hasNext()) {
				SelectionKey selKey = iter.next();
				//若不是服务器套接字通道的key,则将数据设置到此key中
				//并更新此key感兴趣的动作
				if (selKey != serverKey) {
					selKey.attach(unames);
					selKey.interestOps(selKey.interestOps() | SelectionKey.OP_WRITE);
				}
			}
		} else if (str.indexOf("exit_") != -1) {// client发送退出命令
			String uname = str.substring(5);
			//删除此username称
			unames.remove(uname);
			//将"close"字符串附加到key
			key.attach("close");
			//更新此key感兴趣的动作
			key.interestOps(SelectionKey.OP_WRITE);
			//获取选择器上的已选择的key并迭代
			//将更新后的名称列表数据附加到每个套接字通道key上,并重设key感兴趣的操作
			Iterator<SelectionKey> iter = key.selector().selectedKeys().iterator();
			while (iter.hasNext()) {
				SelectionKey selKey = iter.next();
				if (selKey != serverKey && selKey != key) {
					selKey.attach(unames);
					selKey.interestOps(selKey.interestOps() | SelectionKey.OP_WRITE);
				}
			}
			printInfo(uname + " offline");
		} else {// 读取client聊天消息
			String uname = str.substring(0, str.indexOf("^"));
			String msg = str.substring(str.indexOf("^") + 1);
			printInfo("("+uname+")说:" + msg);
			String dateTime = sdf.format(new Date());
			String smsg = uname + " " + dateTime + "\n  " + msg + "\n";
			Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
			while (iter.hasNext()) {
				SelectionKey selKey = iter.next();
				if (selKey != serverKey) {
					selKey.attach(smsg);
					selKey.interestOps(selKey.interestOps() | SelectionKey.OP_WRITE);
				}
			}
		}
	}

	/**
	 * 写数据到key相应的套接字通道
	 * @param key
	 * @throws IOException
	 */
	private void writeMsg(SelectionKey key) throws IOException {
		SocketChannel channel = (SocketChannel) key.channel();
		Object obj = key.attachment();
		//这里必要要将key的附加数据设置为空。否则会有问题
		key.attach("");
		//附加值为"close",则取消此key,并关闭相应通道
		if (obj.toString().equals("close")) {
			key.cancel();
			channel.socket().close();
			channel.close();
			return;
		}else {
			//将数据写到通道
			channel.write(ByteBuffer.wrap(obj.toString().getBytes()));
		}
		//重设此key兴趣
		key.interestOps(SelectionKey.OP_READ);
	}

	private void printInfo(String str) {
		System.out.println("[" + sdf.format(new Date()) + "] -> " + str);
	}

	public static void main(String[] args) {
		ChatServer server = new ChatServer(19999);
		new Thread(server).start();
	}
}

二、client

1、服务类。用于与服务端交互

package com.chat.client;

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

public class ClientService {
	private static final String HOST = "127.0.0.1";
	private static final int PORT = 19999;
	private static SocketChannel sc;
	
	private static Object lock = new Object();
	
	private static ClientService service;
	
	public static ClientService getInstance(){
		synchronized (lock) {
			if(service == null){
				try {
					service = new ClientService();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			return service;
		}
	}

	private ClientService() throws IOException {
		sc = SocketChannel.open();
		sc.configureBlocking(false);
		sc.connect(new InetSocketAddress(HOST, PORT));
	}

	public void sendMsg(String msg) {
		try {
			while (!sc.finishConnect()) {
			}
			sc.write(ByteBuffer.wrap(msg.getBytes()));
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	public String receiveMsg() {
		ByteBuffer buffer = ByteBuffer.allocate(1024);
		buffer.clear();
		StringBuffer sb = new StringBuffer();
		int count = 0;
		String msg = null;
		try {
			while ((count = sc.read(buffer)) > 0) {
				sb.append(new String(buffer.array(), 0, count));
			}
			if (sb.length() > 0) {
				msg = sb.toString();
				if ("close".equals(sb.toString())) {
					msg = null;
					sc.close();
					sc.socket().close();
				}
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		return msg;
	}

}

2、登陆窗口,用户设置名称

package com.chat.client;

import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTextField;

/**
 * 设置名称窗口
 * 
 * @author zing
 * 
 */
public class SetNameFrame extends JFrame {
	private static final long serialVersionUID = 1L;
	private static JTextField txtName;// 文本框
	private static JButton btnOK;// okbutton
	private static JLabel label;// 标签

	public SetNameFrame() {
		this.setLayout(null);
		Toolkit kit = Toolkit.getDefaultToolkit();
		int w = kit.getScreenSize().width;
		int h = kit.getScreenSize().height;
		this.setBounds(w / 2 - 230 / 2, h / 2 - 200 / 2, 230, 200);
		this.setTitle("设置名称");
		this.setDefaultCloseOperation(EXIT_ON_CLOSE);
		this.setResizable(false);
		txtName = new JTextField(4);
		this.add(txtName);
		txtName.setBounds(10, 10, 100, 25);
		btnOK = new JButton("OK");
		this.add(btnOK);
		btnOK.setBounds(120, 10, 80, 25);
		label = new JLabel("[w:" + w + ",h:" + h + "]");
		this.add(label);
		label.setBounds(10, 40, 200, 100);
		label.setText("<html>在上面的文本框中输入名字<br/>显示器宽度:" + w + "<br/>显示器高度:" + h
				+ "</html>");

		btnOK.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				String uname = txtName.getText();
				ClientService service = ClientService.getInstance();
				ChatFrame chatFrame = new ChatFrame(service, uname);
				chatFrame.show();
				setVisible(false);
			}
		});
	}

	public static void main(String[] args) {
		SetNameFrame setNameFrame = new SetNameFrame();
		setNameFrame.setVisible(true);
	}

}

3、聊天室窗口

 

}








本文转自mfrbuaa博客园博客,原文链接:http://www.cnblogs.com/mfrbuaa/p/5124066.html,如需转载请自行联系原作者

相关文章
|
15天前
|
存储 SQL 安全
Java 安全性编程:基本概念与实战指南
【4月更文挑战第27天】在当今的软件开发领域,安全性编程是一个至关重要的方面。Java,作为广泛使用的编程语言之一,提供了多种机制来保护应用免受常见的安全威胁。本博客将探讨 Java 安全性编程的基本概念,并通过实际示例来展示如何实现这些安全措施。
23 3
|
5天前
|
存储 监控 安全
JVM工作原理与实战(十六):运行时数据区-Java虚拟机栈
JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了运行时数据区、Java虚拟机栈等内容。
11 0
|
6天前
|
Java
Java中的多线程编程:基础知识与实战技巧
【5月更文挑战第6天】多线程编程是Java中的一个重要特性,它允许我们在一个程序中同时执行多个任务。本文将介绍Java多线程的基础知识,包括线程的创建、启动、同步和通信,以及如何在Java中实现多线程编程。通过实例代码和解析,帮助读者深入理解Java多线程编程的概念和应用。
|
10天前
|
存储 Java 数据格式
Java实战:轻松掌握文件重命名与路径提取技巧
Java实战:轻松掌握文件重命名与路径提取技巧
18 0
|
12天前
|
设计模式 算法 安全
Java多线程编程实战:从入门到精通
【4月更文挑战第30天】本文介绍了Java多线程编程的基础,包括线程概念、创建线程(继承`Thread`或实现`Runnable`)、线程生命周期。还讨论了线程同步与锁(同步代码块、`ReentrantLock`)、线程间通信(等待/通知、并发集合)以及实战技巧,如使用线程池、线程安全设计模式和避免死锁。性能优化方面,建议减少锁粒度和使用非阻塞算法。理解这些概念和技术对于编写高效、可靠的多线程程序至关重要。
|
13天前
|
XML Java 测试技术
Java异常处理神器:Guava Throwables类概念与实战
【4月更文挑战第29天】在Java开发中,异常处理是保证程序稳定性和可靠性的关键。Google的Guava库提供了一个强大的工具类Throwables,用于简化和增强异常处理。本篇博客将探讨Throwables类的核心功能及其在实战中的应用。
27 2
|
13天前
|
安全 Java 测试技术
利用Java反射机制提高Spring Boot的代码质量:概念与实战
【4月更文挑战第29天】Java反射机制提供了一种强大的方法来在运行时检查或修改类和对象的行为。在Spring Boot应用中,合理利用反射可以提高代码的灵活性和可维护性。本篇博客将探讨Java反射的核心概念,并展示如何通过反射提高Spring Boot项目的代码质量。
29 0
|
13天前
|
存储 Java 大数据
JAVA:编程的艺术与实战解析
JAVA:编程的艺术与实战解析
20 2
|
15天前
|
监控 Java API
Java 模块化设计:概念与实战应用
【4月更文挑战第27天】模块化设计是现代软件开发的关键,它帮助开发者构建可管理、可维护的大型系统。Java 平台的模块化支持始于 Java 9,引入了一种全新的模块系统。
22 3
|
15天前
|
设计模式 算法 Java
Java 设计模式:探索策略模式的概念和实战应用
【4月更文挑战第27天】策略模式是一种行为设计模式,它允许在运行时选择算法的行为。在 Java 中,策略模式通过定义一系列的算法,并将每一个算法封装起来,并使它们可以互换,这样算法的变化不会影响到使用算法的客户。
22 1