Java NIO实战之聊天室

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

下面直接上代码,jdk1.5以上经过测试,可以支持多人同时在线聊天;

将以下代码复制到项目中便可运行,源码下载地址:聊天室源码

一、服务器端

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;
	//当前聊天室中的用户名称列表
	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();
							//接受新的连接返回和客户端对等的套接字通道
							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_",表示客户端准备进入聊天界面
		//客户端传过来的数据格式是"open_zing",表示名称为zing的用户请求打开聊天窗体
		//用户名称列表有更新,则应将用户名称数据写给每一个已连接的客户端
		if (str.indexOf("open_") != -1) {//客户端连接服务器
			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) {// 客户端发送退出命令
			String uname = str.substring(5);
			//删除此用户名称
			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 {// 读取客户端聊天消息
			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();
	}
}

二、客户端

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;// ok按钮
	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、聊天室窗体

package com.chat.client;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;

/**
 * 聊天室窗体
 * @author zing
 *
 */
public class ChatFrame {

	private JTextArea readContext = new JTextArea(18, 30);// 显示消息文本框
	private JTextArea writeContext = new JTextArea(6, 30);// 发送消息文本框

	private DefaultListModel modle = new DefaultListModel();// 用户列表模型
	private JList list = new JList(modle);// 用户列表

	private JButton btnSend = new JButton("发送");// 发送消息按钮
	private JButton btnClose = new JButton("关闭");// 关闭聊天窗口按钮

	private JFrame frame = new JFrame("ChatFrame");// 窗体界面

	private String uname;// 用户姓名

	private ClientService service;// 用于与服务器交互

	private boolean isRun = false;// 是否运行

	public ChatFrame(ClientService service, String uname) {
		this.isRun = true;
		this.uname = uname;
		this.service = service;
	}

	// 初始化界面控件及事件
	private void init() {
		frame.setLayout(null);
		frame.setTitle(uname + " 聊天窗口");
		frame.setSize(500, 500);
		frame.setLocation(400, 200);
		//设置可关闭
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		//不能改变窗体大小
		frame.setResizable(false);
		//聊天消息显示区带滚动条
		JScrollPane readScroll = new JScrollPane(readContext);
		readScroll.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
		frame.add(readScroll);
		//消息编辑区带滚动条
		JScrollPane writeScroll = new JScrollPane(writeContext);
		writeScroll.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
		frame.add(writeScroll);
		frame.add(list);
		frame.add(btnSend);
		frame.add(btnClose);
		readScroll.setBounds(10, 10, 320, 300);
		readContext.setBounds(0, 0, 320, 300);
		readContext.setEditable(false);//设置为不可编辑
		readContext.setLineWrap(true);// 自动换行
		writeScroll.setBounds(10, 315, 320, 100);
		writeContext.setBounds(0, 0, 320, 100);
		writeContext.setLineWrap(true);// 自动换行
		list.setBounds(340, 10, 140, 445);
		btnSend.setBounds(150, 420, 80, 30);
		btnClose.setBounds(250, 420, 80, 30);
		//窗体关闭事件
		frame.addWindowListener(new WindowAdapter() {
			@Override
			public void windowClosing(WindowEvent e) {
				isRun = false;
				service.sendMsg("exit_" + uname);
				System.exit(0);
			}
		});

		//发送按钮事件
		btnSend.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				String msg = writeContext.getText().trim();
				if(msg.length() > 0){
					service.sendMsg(uname + "^" + writeContext.getText());
				}
				//发送消息后,去掉编辑区文本,并获得光标焦点
				writeContext.setText(null);
				writeContext.requestFocus();
			}
		});

		//关闭按钮事件
		btnClose.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				isRun = false;
				service.sendMsg("exit_" + uname);
				System.exit(0);
			}
		});

		//右边名称列表选择事件
		list.addListSelectionListener(new ListSelectionListener() {
			@Override
			public void valueChanged(ListSelectionEvent e) {
				// JOptionPane.showMessageDialog(null,
				// list.getSelectedValue().toString());
			}
		});

		//消息编辑区键盘按键事件
		writeContext.addKeyListener(new KeyListener() {

			@Override
			public void keyTyped(KeyEvent e) {
				// TODO Auto-generated method stub

			}

			//按下键盘按键后释放
			@Override
			public void keyReleased(KeyEvent e) {
				//按下enter键发送消息
				if(e.getKeyCode() == KeyEvent.VK_ENTER){
					String msg = writeContext.getText().trim();
					if(msg.length() > 0){
						service.sendMsg(uname + "^" + writeContext.getText());
					}
					writeContext.setText(null);
					writeContext.requestFocus();
				}
			}

			@Override
			public void keyPressed(KeyEvent e) {
				// TODO Auto-generated method stub

			}
		});
	}

	// 此线程类用于轮询读取服务器发送的消息
	private class MsgThread extends Thread {
		@Override
		public void run() {
			while (isRun) {
				String msg = service.receiveMsg();
				if (msg != null) {
					//若是名称列表数据,则更新聊天窗体右边的列表
					if (msg.indexOf("[") != -1 && msg.lastIndexOf("]") != -1) {
						msg = msg.substring(1, msg.length() - 1);
						String[] userNames = msg.split(",");
						modle.removeAllElements();
						for (int i = 0; i < userNames.length; i++) {
							modle.addElement(userNames[i].trim());
						}
					} else {
						//将聊天数据设置到聊天消息显示区
						String str = readContext.getText() + msg;
						readContext.setText(str);
						readContext.selectAll();//保持滚动条在最下面
					}
				}
			}
		}
	}

	// 显示界面
	public void show() {
		this.init();
		service.sendMsg("open_" + uname);
		MsgThread msgThread = new MsgThread();
		msgThread.start();
		this.frame.setVisible(true);
	}
}

Java NIO实战之聊天室

时间: 2024-12-26 15:47:25

Java NIO实战之聊天室的相关文章

java socket控制台版本聊天室程序源码下载

原文:java socket控制台版本聊天室程序源码下载 代码下载地址:http://www.zuidaima.com/share/1550463257578496.htm java socket控制台版本聊天室程序源码下载,学习的时候写的,适合学习java基础 java网络编程基础用 标签: java socket 控制台 聊天室 源码话题: 网络编程 java socket控制台版本聊天室程序源码下载,布布扣,bubuko.com

Node实战之聊天室

Node实战之聊天室 Node如何同时处理Http和WebSocket 1.只出现在用户访问聊天程序网站时:Web浏览器->Http请求->Node服务器->Http响应->Web浏览器 2.在用户聊天时持续发生:Web浏览器->WebSocket数据发送->Node服务器->WebSocket数据接收->Web浏览器 开始搭建 1.创建程序文档结构(如下图所示) 2.指明依赖项 程序的依赖项是在package.json文件中指明的.这个文件总是被放在程序的

JAVA WebSocKet ( 简单的聊天室 )

1, 前端代码 登入页 -> login.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>聊天室登入接口</title> <script type="text/javascript" src="JavaScript/jQuery.js"></script> <

Java网络编程案例---聊天室

网络编程是指编写运行在多个设备(计算机)的程序,这些设备都通过网络连接起来. java.net包中JavaSE的API包含有类和接口,它们提供低层次的通信细节.你可以直接使用这些类和接口,来专注于解决问题,而不用关注通信细节. java.net包中提供了两种常见的网络协议的支持: TCP:TCP是传输控制协议的缩写,它保障了两个应用程序之间的可靠通信.通常用于互联网协议,被称TCP/IP. UDP:UDP是用户数据报协议的缩写,一个无连接的协议.提供了应用程序之间要发送的数据的数据报. 本案例以

Java Socket 多人聊天室实现

实现功能 服务器端: 1.服务器可以监听多个Socket连接. 2.服务器将收到的消息转发到所有Socket连接上. 客户端: 1.客户端实时监听来自服务器的消息. 2.客户端随时可以向服务器发送消息. 解决思路 服务器通过循环等待的方式实现监听多个连接,每次启用新的线程处理连接,连接建立后加到一个List中保存,每次收到消息时都遍历数组转发消息,达到向所有客户端广播的目的. 客户端使用两个线程处理,一个线程负责实时收信息,一个线程负责实时发送信息. 关键代码 服务器多线程监听客户端连接: 1

重构 JAVA 聊天室 —— CS 模式的简单架构实现

前言 自从开始弄起数据挖掘之后,已经很久没写过技术类的博客了,最近学校 JAVA 课设要求实现一个聊天室,想想去年自己已经写了一个了,但是有些要求到的功能我也没实现,但看着原有的代码想了想加功能好像有那么点点难,于是就想着重构,也正好之前有看到别人写的CS架构的代码,感觉扩展性还不错,就试着写了写,写完这个聊天室后,还同时写了一个教学白板,那个白板基于这个聊天室的代码仅仅花了三四个小时就完成了!所以,有一个好的架构还是很重要的.下面就开始介绍我重构后的聊天室(代码已上传到github) 功能介绍

语音聊天室可以用.net制作吗?

现在网络上很多有些想要自主制作语音聊天室软件的朋友在问是否可以用.net制作吗?从理论上说是可以的,但实际真正制作的时候肯定会有很多问题,而且关于这方面的资料也很少,杭州雅顾语音聊天室制作公司觉得目前还是应该用JAVA来制作语音聊天室. 因为单纯用.net功能没办法通过DHTML的能力实现,所以无论用什么语言都需要客户端加载一些本地代码,比如相应的组件(控件),ActiveX或 Java控件的运行环境很容易得到,但现在一般的机器上还没有装.NET,所以现在只能使用ActiveX或Java组件来做

JavaSE项目之聊天室

引子: 当前,互联网 体系结构的参考模型主要有两种,一种是OSI参考模型,另一种是TCP/IP参考模型. 一.OSI参考模型,即开放式通信系统互联参考模型(OSI/RM,Open Systems Interconnection Reference Model),是国际标准化组织(ISO)提出的一个试图使各种计算机在世界范围内互连为网络的标准框架,简称OSI. OSI参考模型将实现网络互连的通信协议分为7层,自上而下分别是: 第7层应用层:OSI中的最高层,为用户提供各项互联网应用,如公司老板通过

Java NIO示例:多人网络聊天室

一个多客户端聊天室,支持多客户端聊天,有如下功能: 功能1: 客户端通过Java NIO连接到服务端,支持多客户端的连接 功能2:客户端初次连接时,服务端提示输入昵称,如果昵称已经有人使用,提示重新输入,如果昵称唯一,则登录成功,之后发送消息都需要按照规定格式带着昵称发送消息 功能3:客户端登录后,发送已经设置好的欢迎信息和在线人数给客户端,并且通知其他客户端该客户端上线 功能4:服务器收到已登录客户端输入内容,转发至其他登录客户端. 功能5 TODO:客户端下线检测  方案是:客户端在线的时候