在Java网络编程基础(四)中提到了基于Socket的TCP/IP简单聊天系统实现了一个多客户端之间护法消息的简单聊天系统。其服务端采用了多线程来处理多个客户端的消息发送,并转发给目的用户。但是由于它是基于Socket的,因此是阻塞的。
本节我们将通过SocketChannel和ServerSocketChannel来实现同样的功能。
1、客户端输入消息的格式
username:msg username表示要发送的的用户名,msg为发送内容,以冒号分割
2、实现思路
实现思路与Java网络编程基础(四)的实现思路类似,服务端需要保存一份用户名的列表,以便在转发消息时能够查到对应的用户。对于客户端来说,客户端需要能够随时收取服务端转发来的消息,并能够随时通过键盘输入发送消息。
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) { // 客户端列表 HashtableclietList = 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); // 事件来源列表 Iteratorit = 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的具体应用,喜欢请关注