在中提到了基于Socket的TCP/IP简单聊天系统实现了一个多客户端之间护法消息的简单聊天系统。其服务端采用了多线程来处理多个客户端的消息发送,并转发给目的用户。但是由于它是基于Socket的,因此是阻塞的。

    本节我们将通过SocketChannel和ServerSocketChannel来实现同样的功能。

    1、客户端输入消息的格式

        username:msg    username表示要发送的的用户名,msg为发送内容,以冒号分割

    2、实现思路

        实现思路与的实现思路类似,服务端需要保存一份用户名的列表,以便在转发消息时能够查到对应的用户。对于客户端来说,客户端需要能够随时收取服务端转发来的消息,并能够随时通过键盘输入发送消息。

    3、实现过程

        根据以上思路,客户端需要有独立线程,因此需要实现3个类

        (1)客户端主程序ChatClient.java

            该程序根据启动时输入的用户名参数负责启动客户端子线程,由子线程建立与服务端的连接。在主线程中,需要循环读取键盘的输入,将输入的消息通过子线程发送给服务端,交由服务端来转发该消息给客户端

        (2)客户端子线程ClientThread.java

            该线程封装了客户端与服务端的所有操作

            a、在构造函数中,根据创建与服务端的连接

            b、在线程主函数中建立与服务端的连接事件时,发送用户名给服务器进行注册,格式为:username = XXX(启动参数)

            c、发送消息函数:外部的主线程在接收到键盘输入后,调用该函数给服务端发送消息;消息格式为“to:content”(即username:message),为了服务端能够区分是谁发送给to用户的,需要将用户名usename也加进去。from:to:content

            d、关闭函数:在客户端输入bye命令后,关闭客户端与服务器的连接

        (3)服务端主程序ChatServer.java

            该类负责启动服务端,并负责监听客户端的请求,并负责处理客户端的输入/输出和消息转发任务,因此包含以下功能

            a、启动服务器,并监听客户端的连接

            b、在读取到客户端发送的有“username = XXX”字样的消息时,表示客户端第一次建立的连接,将该用户添加到客户端列表中

            c、读取客户端输入的数据,消息格式类似于"from:to:content",第一个冒号":"前的部分为发送者用户名,第二个冒号前的部分为接收者用户名,最后一部分为消息内容。根据该接受者用户名在客户端列表中找到对应的客户端,发送数据给该客户端。

【服务端代码】

package org.test.nio.chat;import java.io.IOException;import java.net.InetSocketAddress;import java.nio.ByteBuffer;import java.nio.CharBuffer;import java.nio.channels.ClosedChannelException;import java.nio.channels.SelectionKey;import java.nio.channels.Selector;import java.nio.channels.ServerSocketChannel;import java.nio.channels.SocketChannel;import java.nio.charset.CharacterCodingException;import java.nio.charset.Charset;import java.nio.charset.CharsetDecoder;import java.nio.charset.CharsetEncoder;import java.util.Hashtable;import java.util.Iterator;public class ChatServer {	public static void main(String[] args) {		// 客户端列表		Hashtable
 clietList = new Hashtable
(); Selector selector = null; ServerSocketChannel server = null; try { // 创建一个Selector selector = Selector.open(); // 创建Socket并注册 server = ServerSocketChannel.open(); server.configureBlocking(false); server.register(selector, SelectionKey.OP_ACCEPT); // 启动监听端口 InetSocketAddress ip = new InetSocketAddress(12345); server.socket().bind(ip); System.out.println("成功启动服务端!"); // TODO 监听事件 while (true) { // 监听事件 selector.select(); // 事件来源列表 Iterator
 it = selector.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey key = it.next(); // 删除该事件 it.remove(); // 判断事件类型 if (key.isAcceptable()) { // 连接事件 ServerSocketChannel server2 = (ServerSocketChannel) key.channel(); SocketChannel channel = server2.accept(); channel.configureBlocking(false); if (channel.isConnectionPending()) { channel.finishConnect(); } channel.register(selector, SelectionKey.OP_READ); System.out.println("客户端连接:" + channel.socket().getInetAddress().getHostName() + channel.socket().getPort()); } else if (key.isReadable()) { // 读取数据事件 SocketChannel channel = (SocketChannel) key.channel(); // 读取数据 CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder(); ByteBuffer buffer = ByteBuffer.allocate(512); channel.read(buffer); buffer.flip(); String msg = decoder.decode(buffer).toString(); System.out.println("收到:" + msg); if(msg.startsWith("username=")){ String username = msg.replaceAll("username=", ""); clietList.put(username, channel); }else{ //转发消息给客户端 String[] arr = msg.split(":"); if(arr.length == 3){ String from = arr[0];//发送者 String to = arr[1];//接受者 String content = arr[2];//发送内容 if(clietList.containsKey(to)){ CharsetEncoder encoder = Charset.forName("UTF-8").newEncoder(); //给接收者发送消息 clietList.get(to).write(encoder.encode(CharBuffer.wrap(from+"】"+content))); } }else{ String from = arr[0]; String content = "来自服务器消息:您未指定接收人"; CharsetEncoder encoder = Charset.forName("UTF-8").newEncoder(); //给接收者发送消息 clietList.get(from).write(encoder.encode(CharBuffer.wrap(content))); } } } } } } catch (ClosedChannelException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (CharacterCodingException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally{ try { selector.close(); server.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }}

        客户端线程

package org.test.nio.chat;import java.io.IOException;import java.net.InetSocketAddress;import java.nio.ByteBuffer;import java.nio.CharBuffer;import java.nio.channels.ClosedChannelException;import java.nio.channels.SelectionKey;import java.nio.channels.Selector;import java.nio.channels.SocketChannel;import java.nio.charset.CharacterCodingException;import java.nio.charset.Charset;import java.nio.charset.CharsetDecoder;import java.nio.charset.CharsetEncoder;import java.util.Iterator;public class ClientThread extends Thread {	private CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder();	private CharsetEncoder encoder = Charset.forName("UTF-8").newEncoder();	private Selector selector = null;	private SocketChannel socket = null;	private SelectionKey clientKey = null;	private String username;		// TODO 启动客户端	public ClientThread(String username){		try {			// 创建Selector			selector = Selector.open();			// 创建并注册Socket			socket = SocketChannel.open();			socket.configureBlocking(false);			clientKey = socket.register(selector, SelectionKey.OP_CONNECT);			// 连接到远程地址			InetSocketAddress ip = new InetSocketAddress("localhost", 12345);			socket.connect(ip);			this.username = username;					} catch (ClosedChannelException e) {			// TODO Auto-generated catch block			e.printStackTrace();		} catch (IOException e) {			// TODO Auto-generated catch block			e.printStackTrace();		}	}	@Override	public void run() {		try {			// 监听事件			while (true) {				selector.select(1);				// 事件来源列表				Iterator
 it = selector.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey key = it.next(); // 删除当前事件 it.remove(); // 判断当前事件类型 if (key.isConnectable()) { // 连接事件 SocketChannel channel = (SocketChannel) key.channel(); if(channel.isConnectionPending()){ channel.finishConnect(); } channel.register(selector, SelectionKey.OP_READ); System.out.println("连接服务器端成功!"); //发送用户名 send("username="+this.username); } else if (key.isReadable()) { // 读取数据事件 SocketChannel channel = (SocketChannel) key.channel();// channel.register(selector, SelectionKey.OP_WRITE); // 读取数据 ByteBuffer buffer = ByteBuffer.allocate(512); channel.read(buffer); buffer.flip(); String msg = decoder.decode(buffer).toString(); System.out.println("【收到:" + msg); } } } } catch (ClosedChannelException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (CharacterCodingException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } void send(String msg) { // TODO 发送消息 try { SocketChannel channel = (SocketChannel) clientKey.channel(); channel.write(encoder.encode(CharBuffer.wrap(msg))); } catch (CharacterCodingException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } // TODO 关闭客户端 public void close() { try { selector.close(); socket.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } }}

客户端

package org.test.nio.chat;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;public class ChatClient {	public static void main(String[] args) {		// TODO Auto-generated method stub		String username = args[0];		ClientThread client = new ClientThread(username);		client.start();		// 输入\输出流		BufferedReader sin = new BufferedReader(new InputStreamReader(System.in));		try {			// 循环读取键盘输入			String readLine;			while ((readLine = sin.readLine().trim()) != null) {				if (readLine.equals("bye")) {					client.close();					System.exit(0);				}				client.send(username + ":" + readLine);			}		} catch (IOException e) {			// TODO Auto-generated catch block			e.printStackTrace();		}	}}

运行结果

服务端

客户端

相对还很简陋,但是确实是NIO的具体应用,喜欢请关注