zoukankan      html  css  js  c++  java
  • 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 + "
      " + msg + "
    ";
    			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、聊天室窗口

    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("发送");// 发送消息button
    	private JButton btnClose = new JButton("关闭");// 关闭聊天窗口button
    
    	private JFrame frame = new JFrame("ChatFrame");// 窗口界面
    
    	private String uname;// 用户姓名
    
    	private ClientService service;// 用于与server交互
    
    	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);
    			}
    		});
    
    		//发送button事件
    		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();
    			}
    		});
    
    		//关闭button事件
    		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
    				
    			}
    		});
    	}
    
    	// 此线程类用于轮询读取server发送的消息
    	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);
    	}
    }
    


  • 相关阅读:
    switch 语句注意事项
    line-height 和 font-size的关系
    HTTP 缓存
    hashchange事件的认识
    面向对象的写法,见到就添,持续更新。。。
    chrome浏览器开发者工具之同步修改至本地
    history对象的一些知识点
    你不知道的函数节流,提高你的JS性能!
    玩媒体查询,就是这么简单粗暴!
    css中clip-path属性的运用
  • 原文地址:https://www.cnblogs.com/mfrbuaa/p/5124066.html
Copyright © 2011-2022 走看看